From 21166861402ee3d20b747339e421ba297e8c80d4 Mon Sep 17 00:00:00 2001 From: hardwoong Date: Mon, 24 Nov 2025 02:41:51 +0900 Subject: [PATCH 1/2] feat: Create Week9 Mission --- .../mission/controller/MissionController.java | 47 ++++++++++++ .../mission/converter/MissionConverter.java | 72 +++++++++++++++++++ .../umc/domain/mission/dto/MissionReqDTO.java | 4 ++ .../umc/domain/mission/dto/MissionResDTO.java | 55 ++++++++++++++ .../domain/mission/entity/UserMission.java | 8 +++ .../exception/code/MissionErrorCode.java | 1 + .../exception/code/MissionSuccessCode.java | 1 + .../mission/repository/MissionRepository.java | 17 ++--- .../repository/UserMissionRepository.java | 15 ---- .../service/MissionCommandService.java | 3 + .../service/MissionCommandServiceImpl.java | 31 +++++--- .../mission/service/MissionQueryService.java | 8 +++ .../service/MissionQueryServiceImpl.java | 19 +++++ .../review/controller/ReviewController.java | 21 ++++++ .../review/converter/ReviewConverter.java | 31 ++++++++ .../umc/domain/review/dto/ReviewResDTO.java | 22 ++++++ .../review/repository/ReviewRepository.java | 22 +++--- .../review/service/ReviewQueryService.java | 9 +++ .../service/ReviewQueryServiceImpl.java | 24 +++++++ .../umc/global/validation/CheckPage.java | 19 +++++ .../validator/CheckPageValidator.java | 20 ++++++ 21 files changed, 402 insertions(+), 47 deletions(-) create mode 100644 src/main/java/com/example/umc/domain/review/service/ReviewQueryService.java create mode 100644 src/main/java/com/example/umc/domain/review/service/ReviewQueryServiceImpl.java create mode 100644 src/main/java/com/example/umc/global/validation/CheckPage.java create mode 100644 src/main/java/com/example/umc/global/validation/validator/CheckPageValidator.java diff --git a/src/main/java/com/example/umc/domain/mission/controller/MissionController.java b/src/main/java/com/example/umc/domain/mission/controller/MissionController.java index a5b5c96..a2a828d 100644 --- a/src/main/java/com/example/umc/domain/mission/controller/MissionController.java +++ b/src/main/java/com/example/umc/domain/mission/controller/MissionController.java @@ -1,23 +1,33 @@ package com.example.umc.domain.mission.controller; +import com.example.umc.domain.mission.converter.MissionConverter; import com.example.umc.domain.mission.dto.MissionReqDTO; import com.example.umc.domain.mission.dto.MissionResDTO; +import com.example.umc.domain.mission.entity.Mission; import com.example.umc.domain.mission.exception.code.MissionSuccessCode; import com.example.umc.domain.mission.service.MissionCommandService; +import com.example.umc.domain.mission.service.MissionQueryService; import com.example.umc.global.apiPayload.ApiResponse; +import com.example.umc.global.apiPayload.code.status.SuccessStatus; +import com.example.umc.global.validation.CheckPage; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("/api/v1/missions") @RequiredArgsConstructor +@Validated @Tag(name = "미션", description = "미션 관련 API") public class MissionController { private final MissionCommandService missionCommandService; + private final MissionQueryService missionQueryService; // 미션 도전하기 @PostMapping("/challenge") @@ -38,4 +48,41 @@ public ApiResponse createMission( MissionResDTO.CreateMissionDTO response = missionCommandService.createMission(dto); return ApiResponse.onSuccess(MissionSuccessCode.MISSION_CREATED, response); } + + @GetMapping("/stores/{storeId}") + @Operation(summary = "특정 가게의 미션 목록 조회", + description = "특정 가게의 모든 미션을 페이지 기반 페이징하여 조회합니다. 한 페이지에 10개씩 조회됩니다.") + public ApiResponse getStoreMissionList( + @Parameter(description = "가게 ID", required = true) @PathVariable Long storeId, + @Parameter(description = "페이지 번호 (1 이상)", required = true, example = "1") + @RequestParam(defaultValue = "1") @CheckPage Integer page + ) { + Page missionPage = missionQueryService.getStoreMissions(storeId, page); + MissionResDTO.MissionPreViewListDTO response = MissionConverter.toMissionPreViewListDTO(missionPage); + return ApiResponse.onSuccess(SuccessStatus._OK, response); + } + + @GetMapping("/my-ongoing") + @Operation(summary = "내가 진행중인 미션 목록 조회", + description = "로그인한 사용자가 진행중인 미션을 페이지 기반 페이징하여 조회합니다. 한 페이지에 10개씩 조회됩니다.") + public ApiResponse getMyOngoingMissionList( + @Parameter(description = "사용자 ID", required = true) @RequestParam Long userId, + @Parameter(description = "페이지 번호 (1 이상)", required = true, example = "1") + @RequestParam(defaultValue = "1") @CheckPage Integer page + ) { + Page userMissionPage = missionQueryService.getMyOngoingMissions(userId, page); + MissionResDTO.UserMissionPreViewListDTO response = MissionConverter.toUserMissionPreViewListDTO(userMissionPage); + return ApiResponse.onSuccess(SuccessStatus._OK, response); + } + + // 진행중인 미션 완료 처리 + @PatchMapping("/complete") + @Operation(summary = "진행중인 미션 완료 처리", + description = "진행중인 미션을 완료 상태로 변경하고, 변경된 미션 정보를 조회하여 반환합니다.") + public ApiResponse completeMission( + @RequestBody @Valid MissionReqDTO.CompleteMissionDTO dto + ) { + MissionResDTO.CompleteMissionDTO response = missionCommandService.completeMission(dto); + return ApiResponse.onSuccess(MissionSuccessCode.MISSION_COMPLETED, response); + } } diff --git a/src/main/java/com/example/umc/domain/mission/converter/MissionConverter.java b/src/main/java/com/example/umc/domain/mission/converter/MissionConverter.java index 60769fd..1b05989 100644 --- a/src/main/java/com/example/umc/domain/mission/converter/MissionConverter.java +++ b/src/main/java/com/example/umc/domain/mission/converter/MissionConverter.java @@ -6,8 +6,10 @@ import com.example.umc.domain.mission.entity.UserMission; import com.example.umc.domain.store.entity.Store; import com.example.umc.domain.user.entity.User; +import org.springframework.data.domain.Page; import java.time.LocalDateTime; +import java.util.List; public class MissionConverter { @@ -46,4 +48,74 @@ public static Mission toMission(MissionReqDTO.CreateMissionDTO dto, Store store) .missionPoint(dto.missionPoint()) .build(); } + + // Entity -> MissionPreViewDTO + public static MissionResDTO.MissionPreViewDTO toMissionPreViewDTO(Mission mission) { + return MissionResDTO.MissionPreViewDTO.builder() + .missionId(mission.getMissionId()) + .storeName(mission.getStore().getStoreName()) + .missionMoney(mission.getMissionMoney()) + .missionPoint(mission.getMissionPoint()) + .region(mission.getRegion()) + .createdAt(mission.getCreatedAt()) + .build(); + } + + // Page -> MissionPreViewListDTO + public static MissionResDTO.MissionPreViewListDTO toMissionPreViewListDTO(Page missionPage) { + List missionList = missionPage.stream() + .map(MissionConverter::toMissionPreViewDTO) + .toList(); + + return MissionResDTO.MissionPreViewListDTO.builder() + .missionList(missionList) + .listSize(missionList.size()) + .currentPage(missionPage.getNumber() + 1) // 0-based를 1-based로 변환 + .totalPages(missionPage.getTotalPages()) + .totalElements(missionPage.getTotalElements()) + .isFirst(missionPage.isFirst()) + .isLast(missionPage.isLast()) + .build(); + } + + // UserMission -> UserMissionPreViewDTO + public static MissionResDTO.UserMissionPreViewDTO toUserMissionPreViewDTO(UserMission userMission) { + return MissionResDTO.UserMissionPreViewDTO.builder() + .challengeMissionId(userMission.getChallengeMissionId()) + .storeName(userMission.getStore().getStoreName()) + .missionMoney(userMission.getMission().getMissionMoney()) + .missionPoint(userMission.getMission().getMissionPoint()) + .region(userMission.getMission().getRegion()) + .status(userMission.getStatus()) + .challengeAt(userMission.getChallengeAt()) + .createdAt(userMission.getCreatedAt()) + .build(); + } + + // Page -> UserMissionPreViewListDTO + public static MissionResDTO.UserMissionPreViewListDTO toUserMissionPreViewListDTO(Page userMissionPage) { + List userMissionList = userMissionPage.stream() + .map(MissionConverter::toUserMissionPreViewDTO) + .toList(); + + return MissionResDTO.UserMissionPreViewListDTO.builder() + .userMissionList(userMissionList) + .listSize(userMissionList.size()) + .currentPage(userMissionPage.getNumber() + 1) // 0-based를 1-based로 변환 + .totalPages(userMissionPage.getTotalPages()) + .totalElements(userMissionPage.getTotalElements()) + .isFirst(userMissionPage.isFirst()) + .isLast(userMissionPage.isLast()) + .build(); + } + + // UserMission -> CompleteMissionDTO + public static MissionResDTO.CompleteMissionDTO toCompleteMissionDTO(UserMission userMission) { + return MissionResDTO.CompleteMissionDTO.builder() + .challengeMissionId(userMission.getChallengeMissionId()) + .status(userMission.getStatus()) + .completedAt(userMission.getCompletedAt()) + .updatedAt(userMission.getUpdatedAt()) + .build(); + } } diff --git a/src/main/java/com/example/umc/domain/mission/dto/MissionReqDTO.java b/src/main/java/com/example/umc/domain/mission/dto/MissionReqDTO.java index 08fbd46..12b4d05 100644 --- a/src/main/java/com/example/umc/domain/mission/dto/MissionReqDTO.java +++ b/src/main/java/com/example/umc/domain/mission/dto/MissionReqDTO.java @@ -15,4 +15,8 @@ public record CreateMissionDTO( Long missionMoney, Long missionPoint) { } + + public record CompleteMissionDTO( + Long challengeMissionId) { + } } diff --git a/src/main/java/com/example/umc/domain/mission/dto/MissionResDTO.java b/src/main/java/com/example/umc/domain/mission/dto/MissionResDTO.java index 748b09a..aec5e75 100644 --- a/src/main/java/com/example/umc/domain/mission/dto/MissionResDTO.java +++ b/src/main/java/com/example/umc/domain/mission/dto/MissionResDTO.java @@ -1,8 +1,11 @@ package com.example.umc.domain.mission.dto; +import com.example.umc.domain.mission.enums.Region; +import com.example.umc.domain.mission.enums.UserMissionStatus; import lombok.Builder; import java.time.LocalDateTime; +import java.util.List; public class MissionResDTO { @Builder @@ -16,4 +19,56 @@ public record CreateMissionDTO( Long missionId, LocalDateTime createdAt ) {} + + @Builder + public record MissionPreViewDTO( + Long missionId, + String storeName, + Long missionMoney, + Long missionPoint, + Region region, + LocalDateTime createdAt + ) {} + + @Builder + public record MissionPreViewListDTO( + List missionList, + Integer listSize, + Integer currentPage, + Integer totalPages, + Long totalElements, + Boolean isFirst, + Boolean isLast + ) {} + + @Builder + public record UserMissionPreViewDTO( + Long challengeMissionId, + String storeName, + Long missionMoney, + Long missionPoint, + Region region, + UserMissionStatus status, + LocalDateTime challengeAt, + LocalDateTime createdAt + ) {} + + @Builder + public record UserMissionPreViewListDTO( + List userMissionList, + Integer listSize, + Integer currentPage, + Integer totalPages, + Long totalElements, + Boolean isFirst, + Boolean isLast + ) {} + + @Builder + public record CompleteMissionDTO( + Long challengeMissionId, + UserMissionStatus status, + java.time.LocalDateTime completedAt, + java.time.LocalDateTime updatedAt + ) {} } diff --git a/src/main/java/com/example/umc/domain/mission/entity/UserMission.java b/src/main/java/com/example/umc/domain/mission/entity/UserMission.java index bff92a1..737d6bd 100644 --- a/src/main/java/com/example/umc/domain/mission/entity/UserMission.java +++ b/src/main/java/com/example/umc/domain/mission/entity/UserMission.java @@ -49,4 +49,12 @@ public class UserMission extends BaseEntity { @Column(name = "success_id", length = 100) private String successId; + + // 미션 상태를 완료로 변경 + public void updateStatus(UserMissionStatus status) { + this.status = status; + if (status == UserMissionStatus.COMPLETED) { + this.completedAt = java.time.LocalDateTime.now(); + } + } } diff --git a/src/main/java/com/example/umc/domain/mission/exception/code/MissionErrorCode.java b/src/main/java/com/example/umc/domain/mission/exception/code/MissionErrorCode.java index 177db0a..d66993f 100644 --- a/src/main/java/com/example/umc/domain/mission/exception/code/MissionErrorCode.java +++ b/src/main/java/com/example/umc/domain/mission/exception/code/MissionErrorCode.java @@ -12,6 +12,7 @@ public enum MissionErrorCode implements BaseErrorCode { MISSION_NOT_FOUND(HttpStatus.NOT_FOUND, "MISSION404_1", "해당 미션을 찾지 못했습니다."), MISSION_ALREADY_CHALLENGED(HttpStatus.BAD_REQUEST, "MISSION400_1", "이미 도전 중인 미션입니다."), + MISSION_ALREADY_COMPLETED(HttpStatus.BAD_REQUEST, "MISSION400_2", "이미 완료된 미션입니다."), ; private final HttpStatus status; diff --git a/src/main/java/com/example/umc/domain/mission/exception/code/MissionSuccessCode.java b/src/main/java/com/example/umc/domain/mission/exception/code/MissionSuccessCode.java index c815b05..e945ce1 100644 --- a/src/main/java/com/example/umc/domain/mission/exception/code/MissionSuccessCode.java +++ b/src/main/java/com/example/umc/domain/mission/exception/code/MissionSuccessCode.java @@ -12,6 +12,7 @@ public enum MissionSuccessCode implements BaseCode { MISSION_CHALLENGED(HttpStatus.CREATED, "MISSION201_1", "미션 도전이 성공적으로 등록되었습니다."), MISSION_CREATED(HttpStatus.CREATED, "MISSION201_2", "미션이 성공적으로 생성되었습니다."), + MISSION_COMPLETED(HttpStatus.OK, "MISSION200_1", "미션이 성공적으로 완료되었습니다."), ; private final HttpStatus status; diff --git a/src/main/java/com/example/umc/domain/mission/repository/MissionRepository.java b/src/main/java/com/example/umc/domain/mission/repository/MissionRepository.java index 54521e9..fbfddff 100644 --- a/src/main/java/com/example/umc/domain/mission/repository/MissionRepository.java +++ b/src/main/java/com/example/umc/domain/mission/repository/MissionRepository.java @@ -9,8 +9,6 @@ import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; -import java.util.List; - @Repository public interface MissionRepository extends JpaRepository { // 1. 홈 화면 미션 목록 조회 - 사용자가 아직 수행하지 않은 특정 지역 미션 조회 @@ -39,16 +37,11 @@ Page findAvailableMissionsWithCompletedCount(@Param("userId") Long use @Param("region") Region region, Pageable pageable); - // 2. 홈 화면 미션 목록 조회 - 커서 방식 페이징 (다음 페이지) @Query("SELECT m FROM Mission m " + - "JOIN FETCH m.store s " + - "LEFT JOIN UserMission um ON m.missionId = um.mission.missionId AND um.user.userId = :userId " + - "WHERE m.region = :region " + - "AND um.mission.missionId IS NULL " + - "AND m.missionId < :cursor " + + "WHERE m.store.storeId = :storeId " + "ORDER BY m.missionId DESC") - List findAvailableMissionsNextPage(@Param("userId") Long userId, - @Param("region") Region region, - @Param("cursor") Long cursor, - Pageable pageable); + Page findByStoreStoreId( + @Param("storeId") Long storeId, + Pageable pageable + ); } \ No newline at end of file diff --git a/src/main/java/com/example/umc/domain/mission/repository/UserMissionRepository.java b/src/main/java/com/example/umc/domain/mission/repository/UserMissionRepository.java index fc41f3b..ae65cc6 100644 --- a/src/main/java/com/example/umc/domain/mission/repository/UserMissionRepository.java +++ b/src/main/java/com/example/umc/domain/mission/repository/UserMissionRepository.java @@ -9,12 +9,9 @@ import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; -import java.util.List; - @Repository public interface UserMissionRepository extends JpaRepository { - // 1. 진행중/완료 미션 조회 - status를 @Param으로 받아서 처리 @Query("SELECT um FROM UserMission um " + "JOIN FETCH um.mission m " + "JOIN FETCH um.store s " + @@ -23,16 +20,4 @@ public interface UserMissionRepository extends JpaRepository Page findUserMissionsByStatus(@Param("userId") Long userId, @Param("status") UserMissionStatus status, Pageable pageable); - - // 2. 커서 페이징 - 2번째 페이지 이상 조회 (기본 조회가 아닌 경우) - @Query("SELECT um FROM UserMission um " + - "JOIN FETCH um.mission m " + - "JOIN FETCH um.store s " + - "WHERE um.user.userId = :userId AND um.status = :status " + - "AND um.challengeMissionId < :cursor " + - "ORDER BY um.challengeMissionId DESC") - List findUserMissionsNextPage(@Param("userId") Long userId, - @Param("status") UserMissionStatus status, - @Param("cursor") Long cursor, - Pageable pageable); } \ No newline at end of file diff --git a/src/main/java/com/example/umc/domain/mission/service/MissionCommandService.java b/src/main/java/com/example/umc/domain/mission/service/MissionCommandService.java index 0f6c401..1f08a44 100644 --- a/src/main/java/com/example/umc/domain/mission/service/MissionCommandService.java +++ b/src/main/java/com/example/umc/domain/mission/service/MissionCommandService.java @@ -9,4 +9,7 @@ public interface MissionCommandService { // 미션 추가하기 MissionResDTO.CreateMissionDTO createMission(MissionReqDTO.CreateMissionDTO dto); + + // 진행중인 미션 완료 처리 + MissionResDTO.CompleteMissionDTO completeMission(MissionReqDTO.CompleteMissionDTO dto); } diff --git a/src/main/java/com/example/umc/domain/mission/service/MissionCommandServiceImpl.java b/src/main/java/com/example/umc/domain/mission/service/MissionCommandServiceImpl.java index e0f29a6..107d6a4 100644 --- a/src/main/java/com/example/umc/domain/mission/service/MissionCommandServiceImpl.java +++ b/src/main/java/com/example/umc/domain/mission/service/MissionCommandServiceImpl.java @@ -5,6 +5,7 @@ import com.example.umc.domain.mission.dto.MissionResDTO; import com.example.umc.domain.mission.entity.Mission; import com.example.umc.domain.mission.entity.UserMission; +import com.example.umc.domain.mission.enums.UserMissionStatus; import com.example.umc.domain.mission.exception.MissionException; import com.example.umc.domain.mission.exception.code.MissionErrorCode; import com.example.umc.domain.mission.repository.MissionRepository; @@ -41,16 +42,8 @@ public MissionResDTO.ChallengeMissionDTO challengeMission(MissionReqDTO.Challeng Mission mission = missionRepository.findById(dto.missionId()) .orElseThrow(() -> new MissionException(MissionErrorCode.MISSION_NOT_FOUND)); - // 가게 정보는 미션에서 가져옴 Store store = mission.getStore(); - // 이미 도전 중인 미션인지 확인 (선택적 - 필요시 구현) - // boolean alreadyChallenged = - // userMissionRepository.existsByUserAndMission(user, mission); - // if (alreadyChallenged) { - // throw new MissionException(MissionErrorCode.MISSION_ALREADY_CHALLENGED); - // } - // UserMission 엔티티 생성 UserMission userMission = MissionConverter.toUserMission(mission, user, store); @@ -77,4 +70,26 @@ public MissionResDTO.CreateMissionDTO createMission(MissionReqDTO.CreateMissionD // 응답 DTO 생성 return MissionConverter.toCreateMissionDTO(mission); } + + @Override + @Transactional + public MissionResDTO.CompleteMissionDTO completeMission(MissionReqDTO.CompleteMissionDTO dto) { + // UserMission 조회 + UserMission userMission = userMissionRepository.findById(dto.challengeMissionId()) + .orElseThrow(() -> new MissionException(MissionErrorCode.MISSION_NOT_FOUND)); + + // 진행중인 미션인지 확인 + if (userMission.getStatus() != UserMissionStatus.IN_PROGRESS) { + throw new MissionException(MissionErrorCode.MISSION_ALREADY_COMPLETED); + } + + // 상태를 완료로 변경 + userMission.updateStatus(UserMissionStatus.COMPLETED); + + // DB 저장 + userMissionRepository.save(userMission); + + // 변경된 미션을 조회하여 응답 DTO 생성 + return MissionConverter.toCompleteMissionDTO(userMission); + } } diff --git a/src/main/java/com/example/umc/domain/mission/service/MissionQueryService.java b/src/main/java/com/example/umc/domain/mission/service/MissionQueryService.java index 3ab16aa..3e6c605 100644 --- a/src/main/java/com/example/umc/domain/mission/service/MissionQueryService.java +++ b/src/main/java/com/example/umc/domain/mission/service/MissionQueryService.java @@ -1,6 +1,14 @@ package com.example.umc.domain.mission.service; +import org.springframework.data.domain.Page; + +import com.example.umc.domain.mission.entity.Mission; +import com.example.umc.domain.mission.entity.UserMission; + public interface MissionQueryService { // 미션 존재 여부 확인 boolean existsById(Long missionId); + + Page getStoreMissions(Long storeId, Integer page); + Page getMyOngoingMissions(Long userId, Integer page); } \ No newline at end of file diff --git a/src/main/java/com/example/umc/domain/mission/service/MissionQueryServiceImpl.java b/src/main/java/com/example/umc/domain/mission/service/MissionQueryServiceImpl.java index 5265c72..2eacc8b 100644 --- a/src/main/java/com/example/umc/domain/mission/service/MissionQueryServiceImpl.java +++ b/src/main/java/com/example/umc/domain/mission/service/MissionQueryServiceImpl.java @@ -1,7 +1,13 @@ package com.example.umc.domain.mission.service; +import com.example.umc.domain.mission.entity.Mission; +import com.example.umc.domain.mission.entity.UserMission; +import com.example.umc.domain.mission.enums.UserMissionStatus; import com.example.umc.domain.mission.repository.MissionRepository; +import com.example.umc.domain.mission.repository.UserMissionRepository; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -11,9 +17,22 @@ public class MissionQueryServiceImpl implements MissionQueryService { private final MissionRepository missionRepository; + private final UserMissionRepository userMissionRepository; @Override public boolean existsById(Long missionId) { return missionRepository.existsById(missionId); } + + @Override + public Page getStoreMissions(Long storeId, Integer page) { + PageRequest pageRequest = PageRequest.of(page - 1, 10); + return missionRepository.findByStoreStoreId(storeId, pageRequest); + } + + @Override + public Page getMyOngoingMissions(Long userId, Integer page) { + PageRequest pageRequest = PageRequest.of(page - 1, 10); + return userMissionRepository.findUserMissionsByStatus(userId, UserMissionStatus.IN_PROGRESS, pageRequest); + } } \ No newline at end of file diff --git a/src/main/java/com/example/umc/domain/review/controller/ReviewController.java b/src/main/java/com/example/umc/domain/review/controller/ReviewController.java index c7f9901..8d16323 100644 --- a/src/main/java/com/example/umc/domain/review/controller/ReviewController.java +++ b/src/main/java/com/example/umc/domain/review/controller/ReviewController.java @@ -1,18 +1,24 @@ package com.example.umc.domain.review.controller; +import com.example.umc.domain.review.converter.ReviewConverter; import com.example.umc.domain.review.dto.ReviewReqDTO; import com.example.umc.domain.review.dto.ReviewResDTO; import com.example.umc.domain.review.dto.ReviewResponseDto; +import com.example.umc.domain.review.entity.Review; import com.example.umc.domain.review.exception.code.ReviewSuccessCode; import com.example.umc.domain.review.service.ReviewCommandService; +import com.example.umc.domain.review.service.ReviewQueryService; import com.example.umc.domain.review.service.ReviewService; import com.example.umc.global.apiPayload.ApiResponse; import com.example.umc.global.apiPayload.code.status.SuccessStatus; +import com.example.umc.global.validation.CheckPage; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import java.util.List; @@ -21,11 +27,13 @@ @RestController @RequestMapping("/api/v1/reviews") @RequiredArgsConstructor +@Validated @Tag(name = "리뷰", description = "리뷰 관련 API") public class ReviewController { private final ReviewService reviewService; private final ReviewCommandService reviewCommandService; + private final ReviewQueryService reviewQueryService; // 리뷰 작성 @PostMapping("") @@ -53,4 +61,17 @@ public ApiResponse> getMyReviews( SuccessStatus code = SuccessStatus._OK; return ApiResponse.onSuccess(code, reviews); } + + @GetMapping("/my") + @Operation(summary = "내가 작성한 리뷰 목록 조회", + description = "로그인한 사용자가 작성한 모든 리뷰를 페이지 기반 페이징하여 조회합니다. 한 페이지에 10개씩 조회됩니다.") + public ApiResponse getMyReviewList( + @Parameter(description = "사용자 ID", required = true) @RequestParam Long userId, + @Parameter(description = "페이지 번호 (1 이상)", required = true, example = "1") + @RequestParam(defaultValue = "1") @CheckPage Integer page + ) { + Page reviewPage = reviewQueryService.getMyReviews(userId, page); + ReviewResDTO.ReviewPreViewListDTO response = ReviewConverter.toReviewPreViewListDTO(reviewPage); + return ApiResponse.onSuccess(SuccessStatus._OK, response); + } } diff --git a/src/main/java/com/example/umc/domain/review/converter/ReviewConverter.java b/src/main/java/com/example/umc/domain/review/converter/ReviewConverter.java index 92dd30c..f69728f 100644 --- a/src/main/java/com/example/umc/domain/review/converter/ReviewConverter.java +++ b/src/main/java/com/example/umc/domain/review/converter/ReviewConverter.java @@ -5,6 +5,9 @@ import com.example.umc.domain.review.entity.Review; import com.example.umc.domain.store.entity.Store; import com.example.umc.domain.user.entity.User; +import org.springframework.data.domain.Page; + +import java.util.List; public class ReviewConverter { @@ -25,4 +28,32 @@ public static Review toReview(ReviewReqDTO.CreateReviewDTO dto, User user, Store .score(dto.score()) .build(); } + + // Entity -> ReviewPreViewDTO + public static ReviewResDTO.ReviewPreViewDTO toReviewPreViewDTO(Review review) { + return ReviewResDTO.ReviewPreViewDTO.builder() + .reviewId(review.getReviewId()) + .storeName(review.getStore().getStoreName()) + .score(review.getScore()) + .reviewText(review.getReviewText()) + .createdAt(review.getCreatedAt()) + .build(); + } + + // Page -> ReviewPreViewListDTO + public static ReviewResDTO.ReviewPreViewListDTO toReviewPreViewListDTO(Page reviewPage) { + List reviewList = reviewPage.stream() + .map(ReviewConverter::toReviewPreViewDTO) + .toList(); + + return ReviewResDTO.ReviewPreViewListDTO.builder() + .reviewList(reviewList) + .listSize(reviewList.size()) + .currentPage(reviewPage.getNumber() + 1) // 0-based를 1-based로 변환 + .totalPages(reviewPage.getTotalPages()) + .totalElements(reviewPage.getTotalElements()) + .isFirst(reviewPage.isFirst()) + .isLast(reviewPage.isLast()) + .build(); + } } diff --git a/src/main/java/com/example/umc/domain/review/dto/ReviewResDTO.java b/src/main/java/com/example/umc/domain/review/dto/ReviewResDTO.java index 77803a0..592df9a 100644 --- a/src/main/java/com/example/umc/domain/review/dto/ReviewResDTO.java +++ b/src/main/java/com/example/umc/domain/review/dto/ReviewResDTO.java @@ -2,7 +2,9 @@ import lombok.Builder; +import java.math.BigDecimal; import java.time.LocalDateTime; +import java.util.List; public class ReviewResDTO { @Builder @@ -10,4 +12,24 @@ public record CreateReviewDTO( Long reviewId, LocalDateTime createdAt ) {} + + @Builder + public record ReviewPreViewDTO( + Long reviewId, + String storeName, + BigDecimal score, + String reviewText, + LocalDateTime createdAt + ) {} + + @Builder + public record ReviewPreViewListDTO( + List reviewList, + Integer listSize, + Integer currentPage, + Integer totalPages, + Long totalElements, + Boolean isFirst, + Boolean isLast + ) {} } diff --git a/src/main/java/com/example/umc/domain/review/repository/ReviewRepository.java b/src/main/java/com/example/umc/domain/review/repository/ReviewRepository.java index 36fdc35..5e8ff5d 100644 --- a/src/main/java/com/example/umc/domain/review/repository/ReviewRepository.java +++ b/src/main/java/com/example/umc/domain/review/repository/ReviewRepository.java @@ -2,6 +2,8 @@ import com.example.umc.domain.review.entity.Review; import com.querydsl.core.types.Predicate; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; @@ -12,23 +14,19 @@ @Repository public interface ReviewRepository extends JpaRepository { - // 1. 리뷰 작성 - 서비스 계층에서 Spring Data JPA의 save() 사용 예정 - // Review review = Review.builder() - // .user(user) - // .store(store) - // .reviewText(reviewText) - // .score(score) - // .build(); - // reviewRepository.save(review); - - // 2. 특정 사용자의 특정 가게 리뷰 조회 @Query("SELECT r FROM Review r WHERE r.user.userId = :userId AND r.store.storeId = :storeId") Review findByUserIdAndStoreId(@Param("userId") Long userId, @Param("storeId") Long storeId); - // 3. 특정 가게의 모든 리뷰 조회 @Query("SELECT r FROM Review r WHERE r.store.storeId = :storeId") List findByStoreId(@Param("storeId") Long storeId); - // 4. 내가 작성한 리뷰 조회 (동적 쿼리) - QueryDSL List searchMyReviews(Predicate predicate); + + @Query("SELECT r FROM Review r " + + "WHERE r.user.userId = :userId " + + "ORDER BY r.reviewId DESC") + Page findByUserUserId( + @Param("userId") Long userId, + Pageable pageable + ); } diff --git a/src/main/java/com/example/umc/domain/review/service/ReviewQueryService.java b/src/main/java/com/example/umc/domain/review/service/ReviewQueryService.java new file mode 100644 index 0000000..15b4fab --- /dev/null +++ b/src/main/java/com/example/umc/domain/review/service/ReviewQueryService.java @@ -0,0 +1,9 @@ +package com.example.umc.domain.review.service; + +import org.springframework.data.domain.Page; + +import com.example.umc.domain.review.entity.Review; + +public interface ReviewQueryService { + Page getMyReviews(Long userId, Integer page); +} diff --git a/src/main/java/com/example/umc/domain/review/service/ReviewQueryServiceImpl.java b/src/main/java/com/example/umc/domain/review/service/ReviewQueryServiceImpl.java new file mode 100644 index 0000000..7756f03 --- /dev/null +++ b/src/main/java/com/example/umc/domain/review/service/ReviewQueryServiceImpl.java @@ -0,0 +1,24 @@ +package com.example.umc.domain.review.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.example.umc.domain.review.entity.Review; +import com.example.umc.domain.review.repository.ReviewRepository; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class ReviewQueryServiceImpl implements ReviewQueryService { + + private final ReviewRepository reviewRepository; + + @Override + public Page getMyReviews(Long userId, Integer page) { + PageRequest pageRequest = PageRequest.of(page - 1, 10); + return reviewRepository.findByUserUserId(userId, pageRequest); + } +} diff --git a/src/main/java/com/example/umc/global/validation/CheckPage.java b/src/main/java/com/example/umc/global/validation/CheckPage.java new file mode 100644 index 0000000..afb28cd --- /dev/null +++ b/src/main/java/com/example/umc/global/validation/CheckPage.java @@ -0,0 +1,19 @@ +package com.example.umc.global.validation; + +import com.example.umc.global.validation.validator.CheckPageValidator; +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +import java.lang.annotation.*; + +@Documented +@Constraint(validatedBy = CheckPageValidator.class) +@Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER }) +@Retention(RetentionPolicy.RUNTIME) +public @interface CheckPage { + String message() default "페이지 번호는 1 이상이어야 합니다."; + + Class[] groups() default {}; + + Class[] payload() default {}; +} diff --git a/src/main/java/com/example/umc/global/validation/validator/CheckPageValidator.java b/src/main/java/com/example/umc/global/validation/validator/CheckPageValidator.java new file mode 100644 index 0000000..1c46a98 --- /dev/null +++ b/src/main/java/com/example/umc/global/validation/validator/CheckPageValidator.java @@ -0,0 +1,20 @@ +package com.example.umc.global.validation.validator; + +import com.example.umc.global.validation.CheckPage; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import org.springframework.stereotype.Component; + +@Component +public class CheckPageValidator implements ConstraintValidator { + + @Override + public boolean isValid(Integer value, ConstraintValidatorContext context) { + if (value == null) { + return true; // null은 @NotNull로 처리 + } + + // 페이지 번호가 1 미만인 경우 검증 실패 + return value >= 1; + } +} From 0fc79a39aee93b97d6528de1270e79e346c5ca39 Mon Sep 17 00:00:00 2001 From: hardwoong Date: Mon, 15 Dec 2025 10:10:13 +0900 Subject: [PATCH 2/2] =?UTF-8?q?refact:=20=ED=94=BC=EB=93=9C=EB=B0=B1=20?= =?UTF-8?q?=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mission/controller/MissionController.java | 33 ++- .../mission/converter/MissionConverter.java | 217 +++++++++--------- .../mission/service/MissionQueryService.java | 4 +- .../service/MissionQueryServiceImpl.java | 4 +- .../review/converter/ReviewConverter.java | 14 +- .../review/repository/ReviewRepository.java | 8 +- .../service/ReviewQueryServiceImpl.java | 5 +- .../global/apiPayload/dto/PagingInfoDTO.java | 12 + .../umc/global/common/util/PageUtil.java | 21 ++ 9 files changed, 178 insertions(+), 140 deletions(-) create mode 100644 src/main/java/com/example/umc/global/apiPayload/dto/PagingInfoDTO.java create mode 100644 src/main/java/com/example/umc/global/common/util/PageUtil.java diff --git a/src/main/java/com/example/umc/domain/mission/controller/MissionController.java b/src/main/java/com/example/umc/domain/mission/controller/MissionController.java index a2a828d..6876bf6 100644 --- a/src/main/java/com/example/umc/domain/mission/controller/MissionController.java +++ b/src/main/java/com/example/umc/domain/mission/controller/MissionController.java @@ -33,8 +33,7 @@ public class MissionController { @PostMapping("/challenge") @Operation(summary = "미션 도전하기", description = "가게의 미션을 도전 중인 미션에 추가합니다.") public ApiResponse challengeMission( - @RequestBody @Valid MissionReqDTO.ChallengeMissionDTO dto - ) { + @RequestBody @Valid MissionReqDTO.ChallengeMissionDTO dto) { MissionResDTO.ChallengeMissionDTO response = missionCommandService.challengeMission(dto); return ApiResponse.onSuccess(MissionSuccessCode.MISSION_CHALLENGED, response); } @@ -43,45 +42,39 @@ public ApiResponse challengeMission( @PostMapping("") @Operation(summary = "미션 추가", description = "가게에 미션을 추가합니다.") public ApiResponse createMission( - @RequestBody @Valid MissionReqDTO.CreateMissionDTO dto - ) { + @RequestBody @Valid MissionReqDTO.CreateMissionDTO dto) { MissionResDTO.CreateMissionDTO response = missionCommandService.createMission(dto); return ApiResponse.onSuccess(MissionSuccessCode.MISSION_CREATED, response); } @GetMapping("/stores/{storeId}") - @Operation(summary = "특정 가게의 미션 목록 조회", - description = "특정 가게의 모든 미션을 페이지 기반 페이징하여 조회합니다. 한 페이지에 10개씩 조회됩니다.") + @Operation(summary = "특정 가게의 미션 목록 조회", description = "특정 가게의 모든 미션을 페이지 기반 페이징하여 조회합니다. 한 페이지에 10개씩 조회됩니다.") public ApiResponse getStoreMissionList( @Parameter(description = "가게 ID", required = true) @PathVariable Long storeId, - @Parameter(description = "페이지 번호 (1 이상)", required = true, example = "1") - @RequestParam(defaultValue = "1") @CheckPage Integer page - ) { + @Parameter(description = "페이지 번호 (1 이상)", required = true, example = "1") @RequestParam(defaultValue = "1") @CheckPage Integer page) { Page missionPage = missionQueryService.getStoreMissions(storeId, page); MissionResDTO.MissionPreViewListDTO response = MissionConverter.toMissionPreViewListDTO(missionPage); return ApiResponse.onSuccess(SuccessStatus._OK, response); } @GetMapping("/my-ongoing") - @Operation(summary = "내가 진행중인 미션 목록 조회", - description = "로그인한 사용자가 진행중인 미션을 페이지 기반 페이징하여 조회합니다. 한 페이지에 10개씩 조회됩니다.") + @Operation(summary = "내가 진행중인 미션 목록 조회", description = "로그인한 사용자가 진행중인 미션을 페이지 기반 페이징하여 조회합니다. 한 페이지에 10개씩 조회됩니다.") public ApiResponse getMyOngoingMissionList( @Parameter(description = "사용자 ID", required = true) @RequestParam Long userId, - @Parameter(description = "페이지 번호 (1 이상)", required = true, example = "1") - @RequestParam(defaultValue = "1") @CheckPage Integer page - ) { - Page userMissionPage = missionQueryService.getMyOngoingMissions(userId, page); - MissionResDTO.UserMissionPreViewListDTO response = MissionConverter.toUserMissionPreViewListDTO(userMissionPage); + @Parameter(description = "페이지 번호 (1 이상)", required = true, example = "1") @RequestParam(defaultValue = "1") @CheckPage Integer page) { + Page userMissionPage = missionQueryService + .getUserMissionsByStatus(userId, com.example.umc.domain.mission.enums.UserMissionStatus.IN_PROGRESS, + page); + MissionResDTO.UserMissionPreViewListDTO response = MissionConverter + .toUserMissionPreViewListDTO(userMissionPage); return ApiResponse.onSuccess(SuccessStatus._OK, response); } // 진행중인 미션 완료 처리 @PatchMapping("/complete") - @Operation(summary = "진행중인 미션 완료 처리", - description = "진행중인 미션을 완료 상태로 변경하고, 변경된 미션 정보를 조회하여 반환합니다.") + @Operation(summary = "진행중인 미션 완료 처리", description = "진행중인 미션을 완료 상태로 변경하고, 변경된 미션 정보를 조회하여 반환합니다.") public ApiResponse completeMission( - @RequestBody @Valid MissionReqDTO.CompleteMissionDTO dto - ) { + @RequestBody @Valid MissionReqDTO.CompleteMissionDTO dto) { MissionResDTO.CompleteMissionDTO response = missionCommandService.completeMission(dto); return ApiResponse.onSuccess(MissionSuccessCode.MISSION_COMPLETED, response); } diff --git a/src/main/java/com/example/umc/domain/mission/converter/MissionConverter.java b/src/main/java/com/example/umc/domain/mission/converter/MissionConverter.java index 1b05989..2cfc8df 100644 --- a/src/main/java/com/example/umc/domain/mission/converter/MissionConverter.java +++ b/src/main/java/com/example/umc/domain/mission/converter/MissionConverter.java @@ -6,6 +6,8 @@ import com.example.umc.domain.mission.entity.UserMission; import com.example.umc.domain.store.entity.Store; import com.example.umc.domain.user.entity.User; +import com.example.umc.global.apiPayload.dto.PagingInfoDTO; +import com.example.umc.global.common.util.PageUtil; import org.springframework.data.domain.Page; import java.time.LocalDateTime; @@ -13,109 +15,114 @@ public class MissionConverter { - // Entity -> DTO (ChallengeMission) - public static MissionResDTO.ChallengeMissionDTO toChallengeMissionDTO(UserMission userMission) { - return MissionResDTO.ChallengeMissionDTO.builder() - .challengeMissionId(userMission.getChallengeMissionId()) - .createdAt(userMission.getCreatedAt()) - .build(); - } - - // DTO -> Entity (ChallengeMission) - public static UserMission toUserMission(Mission mission, User user, Store store) { - return UserMission.builder() - .user(user) - .mission(mission) - .store(store) - .challengeAt(LocalDateTime.now()) - .build(); - } - - // Entity -> DTO (CreateMission) - public static MissionResDTO.CreateMissionDTO toCreateMissionDTO(Mission mission) { - return MissionResDTO.CreateMissionDTO.builder() - .missionId(mission.getMissionId()) - .createdAt(mission.getCreatedAt()) - .build(); - } - - // DTO -> Entity (CreateMission) - public static Mission toMission(MissionReqDTO.CreateMissionDTO dto, Store store) { - return Mission.builder() - .store(store) - .region(dto.region()) - .missionMoney(dto.missionMoney()) - .missionPoint(dto.missionPoint()) - .build(); - } - - // Entity -> MissionPreViewDTO - public static MissionResDTO.MissionPreViewDTO toMissionPreViewDTO(Mission mission) { - return MissionResDTO.MissionPreViewDTO.builder() - .missionId(mission.getMissionId()) - .storeName(mission.getStore().getStoreName()) - .missionMoney(mission.getMissionMoney()) - .missionPoint(mission.getMissionPoint()) - .region(mission.getRegion()) - .createdAt(mission.getCreatedAt()) - .build(); - } - - // Page -> MissionPreViewListDTO - public static MissionResDTO.MissionPreViewListDTO toMissionPreViewListDTO(Page missionPage) { - List missionList = missionPage.stream() - .map(MissionConverter::toMissionPreViewDTO) - .toList(); - - return MissionResDTO.MissionPreViewListDTO.builder() - .missionList(missionList) - .listSize(missionList.size()) - .currentPage(missionPage.getNumber() + 1) // 0-based를 1-based로 변환 - .totalPages(missionPage.getTotalPages()) - .totalElements(missionPage.getTotalElements()) - .isFirst(missionPage.isFirst()) - .isLast(missionPage.isLast()) - .build(); - } - - // UserMission -> UserMissionPreViewDTO - public static MissionResDTO.UserMissionPreViewDTO toUserMissionPreViewDTO(UserMission userMission) { - return MissionResDTO.UserMissionPreViewDTO.builder() - .challengeMissionId(userMission.getChallengeMissionId()) - .storeName(userMission.getStore().getStoreName()) - .missionMoney(userMission.getMission().getMissionMoney()) - .missionPoint(userMission.getMission().getMissionPoint()) - .region(userMission.getMission().getRegion()) - .status(userMission.getStatus()) - .challengeAt(userMission.getChallengeAt()) - .createdAt(userMission.getCreatedAt()) - .build(); - } - - // Page -> UserMissionPreViewListDTO - public static MissionResDTO.UserMissionPreViewListDTO toUserMissionPreViewListDTO(Page userMissionPage) { - List userMissionList = userMissionPage.stream() - .map(MissionConverter::toUserMissionPreViewDTO) - .toList(); - - return MissionResDTO.UserMissionPreViewListDTO.builder() - .userMissionList(userMissionList) - .listSize(userMissionList.size()) - .currentPage(userMissionPage.getNumber() + 1) // 0-based를 1-based로 변환 - .totalPages(userMissionPage.getTotalPages()) - .totalElements(userMissionPage.getTotalElements()) - .isFirst(userMissionPage.isFirst()) - .isLast(userMissionPage.isLast()) - .build(); - } - - // UserMission -> CompleteMissionDTO - public static MissionResDTO.CompleteMissionDTO toCompleteMissionDTO(UserMission userMission) { - return MissionResDTO.CompleteMissionDTO.builder() - .challengeMissionId(userMission.getChallengeMissionId()) - .status(userMission.getStatus()) - .completedAt(userMission.getCompletedAt()) - .updatedAt(userMission.getUpdatedAt()) - .build(); - } + // Entity -> DTO (ChallengeMission) + public static MissionResDTO.ChallengeMissionDTO toChallengeMissionDTO(UserMission userMission) { + return MissionResDTO.ChallengeMissionDTO.builder() + .challengeMissionId(userMission.getChallengeMissionId()) + .createdAt(userMission.getCreatedAt()) + .build(); + } + + // DTO -> Entity (ChallengeMission) + public static UserMission toUserMission(Mission mission, User user, Store store) { + return UserMission.builder() + .user(user) + .mission(mission) + .store(store) + .challengeAt(LocalDateTime.now()) + .build(); + } + + // Entity -> DTO (CreateMission) + public static MissionResDTO.CreateMissionDTO toCreateMissionDTO(Mission mission) { + return MissionResDTO.CreateMissionDTO.builder() + .missionId(mission.getMissionId()) + .createdAt(mission.getCreatedAt()) + .build(); + } + + // DTO -> Entity (CreateMission) + public static Mission toMission(MissionReqDTO.CreateMissionDTO dto, Store store) { + return Mission.builder() + .store(store) + .region(dto.region()) + .missionMoney(dto.missionMoney()) + .missionPoint(dto.missionPoint()) + .build(); + } + + // Entity -> MissionPreViewDTO + public static MissionResDTO.MissionPreViewDTO toMissionPreViewDTO(Mission mission) { + return MissionResDTO.MissionPreViewDTO.builder() + .missionId(mission.getMissionId()) + .storeName(mission.getStore().getStoreName()) + .missionMoney(mission.getMissionMoney()) + .missionPoint(mission.getMissionPoint()) + .region(mission.getRegion()) + .createdAt(mission.getCreatedAt()) + .build(); + } + + // Page -> MissionPreViewListDTO + public static MissionResDTO.MissionPreViewListDTO toMissionPreViewListDTO(Page missionPage) { + List missionList = missionPage.stream() + .map(MissionConverter::toMissionPreViewDTO) + .toList(); + + PagingInfoDTO pagingInfo = PageUtil.toPagingInfo(missionPage); + + return MissionResDTO.MissionPreViewListDTO.builder() + .missionList(missionList) + .listSize(missionList.size()) + .currentPage(pagingInfo.currentPage()) + .totalPages(pagingInfo.totalPages()) + .totalElements(pagingInfo.totalElements()) + .isFirst(pagingInfo.isFirst()) + .isLast(pagingInfo.isLast()) + .build(); + } + + // UserMission -> UserMissionPreViewDTO + public static MissionResDTO.UserMissionPreViewDTO toUserMissionPreViewDTO(UserMission userMission) { + return MissionResDTO.UserMissionPreViewDTO.builder() + .challengeMissionId(userMission.getChallengeMissionId()) + .storeName(userMission.getStore().getStoreName()) + .missionMoney(userMission.getMission().getMissionMoney()) + .missionPoint(userMission.getMission().getMissionPoint()) + .region(userMission.getMission().getRegion()) + .status(userMission.getStatus()) + .challengeAt(userMission.getChallengeAt()) + .createdAt(userMission.getCreatedAt()) + .build(); + } + + // Page -> UserMissionPreViewListDTO + public static MissionResDTO.UserMissionPreViewListDTO toUserMissionPreViewListDTO( + Page userMissionPage) { + List userMissionList = userMissionPage.stream() + .map(MissionConverter::toUserMissionPreViewDTO) + .toList(); + + PagingInfoDTO pagingInfo = PageUtil.toPagingInfo(userMissionPage); + + return MissionResDTO.UserMissionPreViewListDTO.builder() + .userMissionList(userMissionList) + .listSize(userMissionList.size()) + .currentPage(pagingInfo.currentPage()) + .totalPages(pagingInfo.totalPages()) + .totalElements(pagingInfo.totalElements()) + .isFirst(pagingInfo.isFirst()) + .isLast(pagingInfo.isLast()) + .build(); + } + + // UserMission -> CompleteMissionDTO + public static MissionResDTO.CompleteMissionDTO toCompleteMissionDTO(UserMission userMission) { + return MissionResDTO.CompleteMissionDTO.builder() + .challengeMissionId(userMission.getChallengeMissionId()) + .status(userMission.getStatus()) + .completedAt(userMission.getCompletedAt()) + .updatedAt(userMission.getUpdatedAt()) + .build(); + } } diff --git a/src/main/java/com/example/umc/domain/mission/service/MissionQueryService.java b/src/main/java/com/example/umc/domain/mission/service/MissionQueryService.java index 3e6c605..7dea4f8 100644 --- a/src/main/java/com/example/umc/domain/mission/service/MissionQueryService.java +++ b/src/main/java/com/example/umc/domain/mission/service/MissionQueryService.java @@ -4,11 +4,13 @@ import com.example.umc.domain.mission.entity.Mission; import com.example.umc.domain.mission.entity.UserMission; +import com.example.umc.domain.mission.enums.UserMissionStatus; public interface MissionQueryService { // 미션 존재 여부 확인 boolean existsById(Long missionId); Page getStoreMissions(Long storeId, Integer page); - Page getMyOngoingMissions(Long userId, Integer page); + + Page getUserMissionsByStatus(Long userId, UserMissionStatus status, Integer page); } \ No newline at end of file diff --git a/src/main/java/com/example/umc/domain/mission/service/MissionQueryServiceImpl.java b/src/main/java/com/example/umc/domain/mission/service/MissionQueryServiceImpl.java index 2eacc8b..f9ca7b8 100644 --- a/src/main/java/com/example/umc/domain/mission/service/MissionQueryServiceImpl.java +++ b/src/main/java/com/example/umc/domain/mission/service/MissionQueryServiceImpl.java @@ -31,8 +31,8 @@ public Page getStoreMissions(Long storeId, Integer page) { } @Override - public Page getMyOngoingMissions(Long userId, Integer page) { + public Page getUserMissionsByStatus(Long userId, UserMissionStatus status, Integer page) { PageRequest pageRequest = PageRequest.of(page - 1, 10); - return userMissionRepository.findUserMissionsByStatus(userId, UserMissionStatus.IN_PROGRESS, pageRequest); + return userMissionRepository.findUserMissionsByStatus(userId, status, pageRequest); } } \ No newline at end of file diff --git a/src/main/java/com/example/umc/domain/review/converter/ReviewConverter.java b/src/main/java/com/example/umc/domain/review/converter/ReviewConverter.java index f69728f..2614d81 100644 --- a/src/main/java/com/example/umc/domain/review/converter/ReviewConverter.java +++ b/src/main/java/com/example/umc/domain/review/converter/ReviewConverter.java @@ -5,6 +5,8 @@ import com.example.umc.domain.review.entity.Review; import com.example.umc.domain.store.entity.Store; import com.example.umc.domain.user.entity.User; +import com.example.umc.global.apiPayload.dto.PagingInfoDTO; +import com.example.umc.global.common.util.PageUtil; import org.springframework.data.domain.Page; import java.util.List; @@ -46,14 +48,16 @@ public static ReviewResDTO.ReviewPreViewListDTO toReviewPreViewListDTO(Page { List searchMyReviews(Predicate predicate); @Query("SELECT r FROM Review r " + - "WHERE r.user.userId = :userId " + - "ORDER BY r.reviewId DESC") - Page findByUserUserId( + "WHERE r.user.userId = :userId") + Page findByUserUserIdOrderByReviewIdDesc( @Param("userId") Long userId, - Pageable pageable - ); + Pageable pageable); } diff --git a/src/main/java/com/example/umc/domain/review/service/ReviewQueryServiceImpl.java b/src/main/java/com/example/umc/domain/review/service/ReviewQueryServiceImpl.java index 7756f03..254ad5a 100644 --- a/src/main/java/com/example/umc/domain/review/service/ReviewQueryServiceImpl.java +++ b/src/main/java/com/example/umc/domain/review/service/ReviewQueryServiceImpl.java @@ -3,6 +3,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -18,7 +19,7 @@ public class ReviewQueryServiceImpl implements ReviewQueryService { @Override public Page getMyReviews(Long userId, Integer page) { - PageRequest pageRequest = PageRequest.of(page - 1, 10); - return reviewRepository.findByUserUserId(userId, pageRequest); + PageRequest pageRequest = PageRequest.of(page - 1, 10, Sort.by("reviewId").descending()); + return reviewRepository.findByUserUserIdOrderByReviewIdDesc(userId, pageRequest); } } diff --git a/src/main/java/com/example/umc/global/apiPayload/dto/PagingInfoDTO.java b/src/main/java/com/example/umc/global/apiPayload/dto/PagingInfoDTO.java new file mode 100644 index 0000000..ced4aa5 --- /dev/null +++ b/src/main/java/com/example/umc/global/apiPayload/dto/PagingInfoDTO.java @@ -0,0 +1,12 @@ +package com.example.umc.global.apiPayload.dto; + +import lombok.Builder; + +@Builder +public record PagingInfoDTO( + Integer currentPage, + Integer totalPages, + Long totalElements, + Boolean isFirst, + Boolean isLast) { +} diff --git a/src/main/java/com/example/umc/global/common/util/PageUtil.java b/src/main/java/com/example/umc/global/common/util/PageUtil.java new file mode 100644 index 0000000..ce6fc3c --- /dev/null +++ b/src/main/java/com/example/umc/global/common/util/PageUtil.java @@ -0,0 +1,21 @@ +package com.example.umc.global.common.util; + +import com.example.umc.global.apiPayload.dto.PagingInfoDTO; +import org.springframework.data.domain.Page; + +public class PageUtil { + + /** + * Page 객체로부터 1-based 페이징 정보를 생성합니다. + * Spring Data의 Page는 0-based이지만, API 응답은 1-based로 변환합니다. + */ + public static PagingInfoDTO toPagingInfo(Page page) { + return PagingInfoDTO.builder() + .currentPage(page.getNumber() + 1) // 0-based를 1-based로 변환 + .totalPages(page.getTotalPages()) + .totalElements(page.getTotalElements()) + .isFirst(page.isFirst()) + .isLast(page.isLast()) + .build(); + } +}