From 17bb5cf219ebd1bb417c0f504cf8341eeb46fc1b Mon Sep 17 00:00:00 2001 From: Eugene Shin Date: Sun, 17 May 2026 23:40:23 +0900 Subject: [PATCH] mission/#07 --- .../dto/response/MissionResponseDto.java | 22 ++++++- .../dto/response/ReviewResponseDto.java | 17 +++++ .../review/repository/ReviewRepository.java | 28 ++++++++ .../user/controller/UserAuthController.java | 3 +- .../user/controller/UserController.java | 33 ++++++++-- .../domain/user/converter/UserConverter.java | 57 ++++++++++++++++ .../user/dto/request/UserRequestDto.java | 7 ++ .../UserDoingMissionRepository.java | 6 ++ .../domain/user/service/UserService.java | 66 +++++++++++++++++++ .../handler/GeneralExceptionAdvice.java | 15 +++-- 10 files changed, 238 insertions(+), 16 deletions(-) diff --git a/src/main/java/com/umc/umc10th/domain/mission/dto/response/MissionResponseDto.java b/src/main/java/com/umc/umc10th/domain/mission/dto/response/MissionResponseDto.java index 87f30c7..877a1e9 100644 --- a/src/main/java/com/umc/umc10th/domain/mission/dto/response/MissionResponseDto.java +++ b/src/main/java/com/umc/umc10th/domain/mission/dto/response/MissionResponseDto.java @@ -2,7 +2,6 @@ import lombok.Builder; -import java.time.LocalDate; import java.util.List; public class MissionResponseDto { @@ -22,4 +21,25 @@ public record GetMission ( public record CountMissions( int count ){} + + @Builder + public record GetMissionsPaged( + List missions, + Integer pageNumber, + Integer pageSize, + Integer totalPages, + Long totalElements, + Boolean first, + Boolean last + ) { + @Builder + public record GetMission( + Long missionId, + String storeName, + String title, + String content, + Integer reward, + String status + ) {} + } } diff --git a/src/main/java/com/umc/umc10th/domain/review/dto/response/ReviewResponseDto.java b/src/main/java/com/umc/umc10th/domain/review/dto/response/ReviewResponseDto.java index ea186de..dd81a0a 100644 --- a/src/main/java/com/umc/umc10th/domain/review/dto/response/ReviewResponseDto.java +++ b/src/main/java/com/umc/umc10th/domain/review/dto/response/ReviewResponseDto.java @@ -19,4 +19,21 @@ public record ReviewItem ( LocalDateTime createdAt ){} } + + @Builder + public record GetMyReviewsPaged( + List reviews, + Boolean hasNext, + String nextCursor, + Integer pageSize + ) { + @Builder + public record ReviewItem( + Long reviewId, + String nickname, + String stars, + String content, + LocalDateTime createdAt + ) {} + } } diff --git a/src/main/java/com/umc/umc10th/domain/review/repository/ReviewRepository.java b/src/main/java/com/umc/umc10th/domain/review/repository/ReviewRepository.java index e03e344..5e75d27 100644 --- a/src/main/java/com/umc/umc10th/domain/review/repository/ReviewRepository.java +++ b/src/main/java/com/umc/umc10th/domain/review/repository/ReviewRepository.java @@ -2,8 +2,10 @@ import com.umc.umc10th.domain.review.entity.Review; +import com.umc.umc10th.domain.review.enums.Stars; import org.springframework.data.domain.Page; 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.Query; import org.springframework.data.repository.query.Param; @@ -29,4 +31,30 @@ boolean existsByStoreAndUser( @Param("storeId") Long storeId, @Param("userId") Long userId ); + + Slice findReviewsByUser_IdOrderByIdDesc( + Long userId, + Pageable pageable + ); + + Slice findReviewsByUser_IdAndIdLessThanOrderByIdDesc( + Long userId, + Long idCursor, + Pageable pageable + ); + + Slice findReviewsByUser_IdOrderByStarsDescIdDesc( + Long userId, + Pageable pageable + ); + + @Query("SELECT r FROM Review r WHERE r.user.id = :userId " + + "AND (r.stars < :starsCursor OR (r.stars = :starsCursor AND r.id < :idCursor)) " + + "ORDER BY r.stars DESC, r.id DESC") + Slice findReviewsByUser_IdOrderByStarsDesc( + @Param("userId") Long userId, + @Param("starsCursor") Stars starsCursor, + @Param("idCursor") Long idCursor, + Pageable pageable + ); } \ No newline at end of file diff --git a/src/main/java/com/umc/umc10th/domain/user/controller/UserAuthController.java b/src/main/java/com/umc/umc10th/domain/user/controller/UserAuthController.java index e7693b5..648e7a7 100644 --- a/src/main/java/com/umc/umc10th/domain/user/controller/UserAuthController.java +++ b/src/main/java/com/umc/umc10th/domain/user/controller/UserAuthController.java @@ -5,6 +5,7 @@ import com.umc.umc10th.domain.user.service.UserService; import com.umc.umc10th.global.apipayload.ApiResponse; import com.umc.umc10th.global.apipayload.code.BaseSuccessCode; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; @@ -16,7 +17,7 @@ public class UserAuthController { private final UserService userService; // @PostMapping("/signup") -// public ApiResponse getInfo(@RequestBody UserRequestDto.CreateUser dto) { +// public ApiResponse getInfo(@Valid @RequestBody UserRequestDto.CreateUser dto) { // BaseSuccessCode code = UserSuccessCode.OK; // userService.createUser(dto); // return ApiResponse.onSuccess(code); diff --git a/src/main/java/com/umc/umc10th/domain/user/controller/UserController.java b/src/main/java/com/umc/umc10th/domain/user/controller/UserController.java index 021b8d3..bdb17fd 100644 --- a/src/main/java/com/umc/umc10th/domain/user/controller/UserController.java +++ b/src/main/java/com/umc/umc10th/domain/user/controller/UserController.java @@ -1,16 +1,16 @@ package com.umc.umc10th.domain.user.controller; import com.umc.umc10th.domain.mission.dto.response.MissionResponseDto; +import com.umc.umc10th.domain.review.dto.response.ReviewResponseDto; import com.umc.umc10th.domain.user.apipayload.code.UserSuccessCode; +import com.umc.umc10th.domain.user.dto.request.UserRequestDto; import com.umc.umc10th.domain.user.dto.response.UserResponseDto; import com.umc.umc10th.domain.user.service.UserService; import com.umc.umc10th.global.apipayload.ApiResponse; import com.umc.umc10th.global.apipayload.code.BaseSuccessCode; +import jakarta.validation.Valid; 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.*; @RestController @RequestMapping("/api/users") @@ -26,11 +26,30 @@ public ApiResponse getInfo() { } @GetMapping("/me/missions") - public ApiResponse getMissions(@RequestParam Long locationId, @RequestParam String status) { - BaseSuccessCode code = UserSuccessCode.OK; - return ApiResponse.onSuccess(code, userService.getMissions(locationId, status)); + public ApiResponse getMissions( + @RequestParam Long locationId, + @RequestParam(defaultValue = "10") Integer pageSize, + @RequestParam(defaultValue = "0") Integer pageNumber, + @RequestBody @Valid UserRequestDto.GetMissionsRequest request) { + + return ApiResponse.onSuccess( + UserSuccessCode.OK, + userService.getMissions(locationId, pageSize, pageNumber, request) + ); } + @GetMapping("/me/reviews") + public ApiResponse getMyReviews( + @RequestParam Long userId, + @RequestParam(defaultValue = "10") Integer pageSize, + @RequestParam(defaultValue = "-1") String cursor, + @RequestParam(defaultValue = "ID") String query) { + + return ApiResponse.onSuccess( + UserSuccessCode.OK, + userService.getMyReviews(userId, pageSize, cursor, query) + ); + } // @GetMapping("/me/missions/count") // public ApiResponse countMissions(@RequestParam Long locationId) { // BaseSuccessCode code = UserSuccessCode.OK; diff --git a/src/main/java/com/umc/umc10th/domain/user/converter/UserConverter.java b/src/main/java/com/umc/umc10th/domain/user/converter/UserConverter.java index 1ca67d2..0675689 100644 --- a/src/main/java/com/umc/umc10th/domain/user/converter/UserConverter.java +++ b/src/main/java/com/umc/umc10th/domain/user/converter/UserConverter.java @@ -1,4 +1,61 @@ package com.umc.umc10th.domain.user.converter; +import com.umc.umc10th.domain.mission.dto.response.MissionResponseDto; +import com.umc.umc10th.domain.review.dto.response.ReviewResponseDto; +import com.umc.umc10th.domain.review.entity.Review; +import com.umc.umc10th.domain.user.entity.UserDoingMission; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Slice; + +import java.util.List; + public class UserConverter { + + public static MissionResponseDto.GetMissionsPaged toGetMissionsPaged( + Page page) { + + List missions = + page.getContent().stream() + .map(udm -> MissionResponseDto.GetMissionsPaged.GetMission.builder() + .missionId(udm.getMission().getId()) + .storeName(udm.getMission().getStore().getStoreName()) + .title(udm.getMission().getTitle()) + .content(udm.getMission().getContent()) + .reward(udm.getMission().getReward()) + .status(udm.getIsCompleted() ? "COMPLETED" : "IN_PROGRESS") + .build()) + .toList(); + + return MissionResponseDto.GetMissionsPaged.builder() + .missions(missions) + .pageNumber(page.getNumber()) + .pageSize(page.getSize()) + .totalPages(page.getTotalPages()) + .totalElements(page.getTotalElements()) + .first(page.isFirst()) + .last(page.isLast()) + .build(); + } + + public static ReviewResponseDto.GetMyReviewsPaged toGetMyReviewsPaged( + Slice slice, String nextCursor) { + + List reviews = + slice.getContent().stream() + .map(r -> ReviewResponseDto.GetMyReviewsPaged.ReviewItem.builder() + .reviewId(r.getId()) + .nickname(r.getUser().getName()) + .stars(r.getStars().name()) + .content(r.getContent()) + .createdAt(r.getCreatedAt()) + .build()) + .toList(); + + return ReviewResponseDto.GetMyReviewsPaged.builder() + .reviews(reviews) + .hasNext(slice.hasNext()) + .nextCursor(nextCursor) + .pageSize(slice.getSize()) + .build(); + } } diff --git a/src/main/java/com/umc/umc10th/domain/user/dto/request/UserRequestDto.java b/src/main/java/com/umc/umc10th/domain/user/dto/request/UserRequestDto.java index 22109e0..19715cf 100644 --- a/src/main/java/com/umc/umc10th/domain/user/dto/request/UserRequestDto.java +++ b/src/main/java/com/umc/umc10th/domain/user/dto/request/UserRequestDto.java @@ -3,6 +3,7 @@ import com.umc.umc10th.domain.user.enums.Provider; import com.umc.umc10th.domain.user.enums.ServiceRole; import com.umc.umc10th.domain.user.enums.Sex; +import jakarta.validation.constraints.*; import lombok.Builder; import java.time.LocalDate; @@ -20,4 +21,10 @@ public record CreateUser ( String address, String phone ){} + + @Builder + public record GetMissionsRequest( + @NotNull(message = "사용자 ID는 필수입니다") + Long userId + ) {} } diff --git a/src/main/java/com/umc/umc10th/domain/user/repository/UserDoingMissionRepository.java b/src/main/java/com/umc/umc10th/domain/user/repository/UserDoingMissionRepository.java index cee16aa..5616ef8 100644 --- a/src/main/java/com/umc/umc10th/domain/user/repository/UserDoingMissionRepository.java +++ b/src/main/java/com/umc/umc10th/domain/user/repository/UserDoingMissionRepository.java @@ -21,4 +21,10 @@ Page findMyMissions( @Param("status") String status, Pageable pageable ); + + Page findAllByUser_IdAndMission_Location_Id( + Long userId, + Long locationId, + Pageable pageable + ); } \ No newline at end of file diff --git a/src/main/java/com/umc/umc10th/domain/user/service/UserService.java b/src/main/java/com/umc/umc10th/domain/user/service/UserService.java index 15098ae..f883641 100644 --- a/src/main/java/com/umc/umc10th/domain/user/service/UserService.java +++ b/src/main/java/com/umc/umc10th/domain/user/service/UserService.java @@ -3,7 +3,10 @@ import com.umc.umc10th.domain.mission.dto.response.MissionResponseDto; import com.umc.umc10th.domain.review.dto.response.ReviewResponseDto; import com.umc.umc10th.domain.review.entity.Review; +import com.umc.umc10th.domain.review.enums.Stars; import com.umc.umc10th.domain.review.repository.ReviewRepository; +import com.umc.umc10th.domain.user.converter.UserConverter; +import com.umc.umc10th.domain.user.dto.request.UserRequestDto; import com.umc.umc10th.domain.user.dto.response.UserResponseDto; import com.umc.umc10th.domain.user.entity.User; import com.umc.umc10th.domain.user.entity.UserDoingMission; @@ -13,6 +16,7 @@ import org.springframework.data.domain.Page; 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; @@ -78,4 +82,66 @@ public ReviewResponseDto.GetMyReviews getMyReviews() { .reviews(items) .build(); } + + public MissionResponseDto.GetMissionsPaged getMissions( + Long locationId, + Integer pageSize, + Integer pageNumber, + UserRequestDto.GetMissionsRequest request + ) { + + PageRequest pageRequest = PageRequest.of(pageNumber, pageSize); + + Page page = + userDoingMissionRepository.findAllByUser_IdAndMission_Location_Id( + request.userId(), locationId, pageRequest + ); + + return UserConverter.toGetMissionsPaged(page); + } + + public ReviewResponseDto.GetMyReviewsPaged getMyReviews( + Long userId, Integer pageSize, String cursor, String query) { // ← 파라미터 분리 + + PageRequest pageRequest = PageRequest.of(0, pageSize); + Slice slice; + + if (!cursor.equals("-1")) { + String[] split = cursor.split(":"); + switch (query.toUpperCase()) { + case "ID" -> { + Long idCursor = Long.parseLong(split[1]); + slice = reviewRepository.findReviewsByUser_IdAndIdLessThanOrderByIdDesc( + userId, idCursor, pageRequest + ); + } + case "STARS" -> { + Stars starsCursor = Stars.valueOf(split[0]); + Long idCursor = Long.parseLong(split[1]); + slice = reviewRepository.findReviewsByUser_IdOrderByStarsDesc( + userId, starsCursor, idCursor, pageRequest + ); + } + default -> throw new IllegalArgumentException("유효하지 않은 정렬 기준입니다: " + query); + } + } else { + slice = switch (query.toUpperCase()) { + case "STARS" -> reviewRepository.findReviewsByUser_IdOrderByStarsDescIdDesc( + userId, pageRequest + ); + default -> reviewRepository.findReviewsByUser_IdOrderByIdDesc( + userId, pageRequest + ); + }; + } + + String nextCursor = null; + if (slice.hasNext() && !slice.getContent().isEmpty()) { + List content = slice.getContent(); + Review last = content.get(content.size() - 1); + nextCursor = last.getStars().name() + ":" + last.getId(); + } + + return UserConverter.toGetMyReviewsPaged(slice, nextCursor); + } } \ No newline at end of file diff --git a/src/main/java/com/umc/umc10th/global/apipayload/handler/GeneralExceptionAdvice.java b/src/main/java/com/umc/umc10th/global/apipayload/handler/GeneralExceptionAdvice.java index 365ca2a..2040958 100644 --- a/src/main/java/com/umc/umc10th/global/apipayload/handler/GeneralExceptionAdvice.java +++ b/src/main/java/com/umc/umc10th/global/apipayload/handler/GeneralExceptionAdvice.java @@ -11,7 +11,8 @@ import org.springframework.web.bind.annotation.RestControllerAdvice; import java.net.BindException; -import java.util.stream.Collectors; +import java.util.HashMap; +import java.util.Map; @Slf4j @RestControllerAdvice @@ -28,16 +29,16 @@ public ResponseEntity> handleProjectException( } @ExceptionHandler(MethodArgumentNotValidException.class) - public ResponseEntity> handleMethodArgumentValidation( - MethodArgumentNotValidException e) { + public ResponseEntity>> handleMethodArgumentValidation( + MethodArgumentNotValidException e + ) { - String message = e.getBindingResult().getFieldErrors().stream() - .map(fe -> fe.getField() + ": " + fe.getDefaultMessage()) - .collect(Collectors.joining(", ")); + Map errors = new HashMap<>(); + e.getBindingResult().getFieldErrors().forEach(error -> { errors.put(error.getField(), error.getDefaultMessage()); }); BaseErrorCode code = GeneralErrorCode.BAD_REQUEST; return ResponseEntity.status(code.getStatus()) - .body(ApiResponse.onFailure(code, null)); + .body(ApiResponse.onFailure(code, errors)); } @ExceptionHandler(BindException.class)