diff --git a/src/main/java/com/example/demo/domain/member/repository/MemberMissionRepository.java b/src/main/java/com/example/demo/domain/member/repository/MemberMissionRepository.java index 9606943..5c01660 100644 --- a/src/main/java/com/example/demo/domain/member/repository/MemberMissionRepository.java +++ b/src/main/java/com/example/demo/domain/member/repository/MemberMissionRepository.java @@ -9,6 +9,8 @@ import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; @Repository public interface MemberMissionRepository extends JpaRepository { @@ -49,6 +51,13 @@ Long countChallengingMissionsByRegion( @Param("region") com.example.demo.global.enums.Region region ); + // 진행 중인 미션 목록 조회 + @Query("SELECT mm FROM MemberMission mm " + + "WHERE mm.member.id = :memberId " + + "AND mm.isComplete = false " + + "ORDER BY mm.createdAt DESC") + Page findChallengingMissions(@Param("memberId") Long memberId, Pageable pageable); + // 중복 도전 체크 (미완료 상태) boolean existsByMemberAndMissionAndIsCompleteFalse(Member member, Mission mission); } \ No newline at end of file diff --git a/src/main/java/com/example/demo/domain/mission/controller/MissionController.java b/src/main/java/com/example/demo/domain/mission/controller/MissionController.java index 1e3c547..b07ca7c 100644 --- a/src/main/java/com/example/demo/domain/mission/controller/MissionController.java +++ b/src/main/java/com/example/demo/domain/mission/controller/MissionController.java @@ -9,12 +9,16 @@ import com.example.demo.global.apiPayload.code.status.SuccessStatus; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.web.bind.annotation.*; +import org.springframework.validation.annotation.Validated; +import com.example.demo.global.validation.annotation.CheckPage; +@Validated @RestController @RequiredArgsConstructor @RequestMapping("/api/missions") @@ -71,4 +75,44 @@ public ApiResponse challengeMission( result ); } + + @GetMapping("/stores/{storeId}/missions") + @Operation(summary = "특정 가게의 미션 목록 조회", description = "특정 가게의 미션 목록을 페이징하여 조회합니다.") + @ApiResponses({ + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "성공"), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "잘못된 요청"), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "가게를 찾을 수 없음") + }) + public ApiResponse getStoreMissions( + @Parameter(description = "가게 ID", required = true) + @PathVariable Long storeId, + + @Parameter(description = "페이지 번호 (1부터 시작)", required = false) + @RequestParam(defaultValue = "1") + @CheckPage + Integer page + ) { + MissionResponseDTO.MissionPreViewListDTO result = missionQueryService.getStoreMissions(storeId, page); + return ApiResponse.of(SuccessStatus.MISSION_OK, result); + } + + @GetMapping("/my/challenging") + @Operation(summary = "내가 진행중인 미션 목록 조회", description = "내가 진행중인 미션 목록을 페이징하여 조회합니다.") + @ApiResponses({ + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "성공"), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "잘못된 요청"), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "회원을 찾을 수 없음") + }) + public ApiResponse getMyChallengingMissions( + @Parameter(description = "회원 ID", required = true) + @RequestParam Long memberId, + + @Parameter(description = "페이지 번호 (1부터 시작)", required = false) + @RequestParam(defaultValue = "1") + @CheckPage + Integer page + ) { + MissionResponseDTO.MemberMissionPreViewListDTO result = missionQueryService.getMyChallengingMissions(memberId, page); + return ApiResponse.of(SuccessStatus.MISSION_OK, result); + } } \ No newline at end of file diff --git a/src/main/java/com/example/demo/domain/mission/converter/MissionConverter.java b/src/main/java/com/example/demo/domain/mission/converter/MissionConverter.java index 43cba64..1c5c6a8 100644 --- a/src/main/java/com/example/demo/domain/mission/converter/MissionConverter.java +++ b/src/main/java/com/example/demo/domain/mission/converter/MissionConverter.java @@ -64,4 +64,60 @@ public static MissionResponseDTO.ChallengeResultDTO toChallengeResultDTO(MemberM .startedAt(memberMission.getCreatedAt()) .build(); } + + // Page -> MissionPreViewListDTO + public static MissionResponseDTO.MissionPreViewListDTO toMissionPreViewListDTO(Page missionPage) { + List missionList = missionPage.getContent().stream() + .map(MissionConverter::toMissionPreViewDTO) + .toList(); + + return MissionResponseDTO.MissionPreViewListDTO.builder() + .missionList(missionList) + .listSize(missionPage.getSize()) + .totalPage(missionPage.getTotalPages()) + .totalElements(missionPage.getTotalElements()) + .isFirst(missionPage.isFirst()) + .isLast(missionPage.isLast()) + .build(); + } + + // Mission -> MissionPreViewDTO + public static MissionResponseDTO.MissionPreViewDTO toMissionPreViewDTO(Mission mission) { + return MissionResponseDTO.MissionPreViewDTO.builder() + .missionId(mission.getId()) + .storeName(mission.getStore().getName()) + .missionContent(mission.getContent()) + .point(mission.getPoint()) + .deadline(mission.getDeadline()) + .build(); + } + + // Page -> MemberMissionPreViewListDTO + public static MissionResponseDTO.MemberMissionPreViewListDTO toMemberMissionPreViewListDTO(Page memberMissionPage) { + List missionList = memberMissionPage.getContent().stream() + .map(MissionConverter::toMemberMissionPreViewDTO) + .toList(); + + return MissionResponseDTO.MemberMissionPreViewListDTO.builder() + .missionList(missionList) + .listSize(memberMissionPage.getSize()) + .totalPage(memberMissionPage.getTotalPages()) + .totalElements(memberMissionPage.getTotalElements()) + .isFirst(memberMissionPage.isFirst()) + .isLast(memberMissionPage.isLast()) + .build(); + } + + // MemberMission -> MemberMissionPreViewDTO + public static MissionResponseDTO.MemberMissionPreViewDTO toMemberMissionPreViewDTO(MemberMission memberMission) { + Mission mission = memberMission.getMission(); + return MissionResponseDTO.MemberMissionPreViewDTO.builder() + .memberMissionId(memberMission.getId()) + .storeName(mission.getStore().getName()) + .missionContent(mission.getContent()) + .point(mission.getPoint()) + .deadline(mission.getDeadline()) + .startedAt(memberMission.getCreatedAt()) + .build(); + } } \ No newline at end of file diff --git a/src/main/java/com/example/demo/domain/mission/dto/MissionResponseDTO.java b/src/main/java/com/example/demo/domain/mission/dto/MissionResponseDTO.java index bd29e07..0cfb0cc 100644 --- a/src/main/java/com/example/demo/domain/mission/dto/MissionResponseDTO.java +++ b/src/main/java/com/example/demo/domain/mission/dto/MissionResponseDTO.java @@ -51,4 +51,55 @@ public static class ChallengeResultDTO { private LocalDate deadline; private LocalDateTime startedAt; } + + @Builder + @Getter + @NoArgsConstructor + @AllArgsConstructor + public static class MissionPreViewListDTO { + List missionList; + Integer listSize; + Integer totalPage; + Long totalElements; + Boolean isFirst; + Boolean isLast; + } + + @Builder + @Getter + @NoArgsConstructor + @AllArgsConstructor + public static class MissionPreViewDTO { + Long missionId; + String storeName; + String missionContent; + Integer point; + LocalDate deadline; + } + + @Builder + @Getter + @NoArgsConstructor + @AllArgsConstructor + public static class MemberMissionPreViewListDTO { + List missionList; + Integer listSize; + Integer totalPage; + Long totalElements; + Boolean isFirst; + Boolean isLast; + } + + @Builder + @Getter + @NoArgsConstructor + @AllArgsConstructor + public static class MemberMissionPreViewDTO { + Long memberMissionId; + String storeName; + String missionContent; + Integer point; + LocalDate deadline; + LocalDateTime startedAt; + } } \ No newline at end of file diff --git a/src/main/java/com/example/demo/domain/mission/repository/MissionRepository.java b/src/main/java/com/example/demo/domain/mission/repository/MissionRepository.java index 99adb6c..8be7bd6 100644 --- a/src/main/java/com/example/demo/domain/mission/repository/MissionRepository.java +++ b/src/main/java/com/example/demo/domain/mission/repository/MissionRepository.java @@ -8,6 +8,9 @@ import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import com.example.demo.domain.store.entity.Store; import java.time.LocalDate; @@ -34,4 +37,6 @@ Page findAvailableMissionsByRegion( @Param("now") LocalDate now, Pageable pageable ); + + Page findAllByStore(Store store, PageRequest pageRequest); } \ No newline at end of file diff --git a/src/main/java/com/example/demo/domain/mission/service/MissionQueryService.java b/src/main/java/com/example/demo/domain/mission/service/MissionQueryService.java index d248806..6670166 100644 --- a/src/main/java/com/example/demo/domain/mission/service/MissionQueryService.java +++ b/src/main/java/com/example/demo/domain/mission/service/MissionQueryService.java @@ -1,10 +1,17 @@ package com.example.demo.domain.mission.service; import com.example.demo.domain.member.entity.MemberMission; +import com.example.demo.domain.mission.dto.MissionResponseDTO; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; public interface MissionQueryService { + // 진행중인 미션 조회 Page getChallengingMissions(Long memberId, Pageable pageable); - Page getCompletedMissions(Long memberId, Pageable pageable); + + // 특정 가게의 미션 목록 조회 + MissionResponseDTO.MissionPreViewListDTO getStoreMissions(Long storeId, Integer page); + + // 내가 진행중인 미션 목록 조회 + MissionResponseDTO.MemberMissionPreViewListDTO getMyChallengingMissions(Long memberId, Integer page); } \ No newline at end of file diff --git a/src/main/java/com/example/demo/domain/mission/service/MissionQueryServiceImpl.java b/src/main/java/com/example/demo/domain/mission/service/MissionQueryServiceImpl.java index 591fc55..4c51038 100644 --- a/src/main/java/com/example/demo/domain/mission/service/MissionQueryServiceImpl.java +++ b/src/main/java/com/example/demo/domain/mission/service/MissionQueryServiceImpl.java @@ -4,14 +4,17 @@ import com.example.demo.domain.member.entity.MemberMission; import com.example.demo.domain.member.repository.MemberMissionRepository; import com.example.demo.domain.member.repository.MemberRepository; +import com.example.demo.domain.mission.converter.MissionConverter; +import com.example.demo.domain.mission.dto.MissionResponseDTO; import com.example.demo.domain.mission.entity.Mission; import com.example.demo.domain.mission.repository.MissionRepository; +import com.example.demo.domain.store.entity.Store; +import com.example.demo.domain.store.repository.StoreRepository; import com.example.demo.global.apiPayload.code.status.ErrorStatus; import com.example.demo.global.apiPayload.exception.GeneralException; -import com.example.demo.global.enums.Region; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.PageRequest; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -19,26 +22,40 @@ @RequiredArgsConstructor @Transactional(readOnly = true) public class MissionQueryServiceImpl implements MissionQueryService { - private final MemberMissionRepository memberMissionRepository; private final MissionRepository missionRepository; private final MemberRepository memberRepository; + private final StoreRepository storeRepository; @Override - public Page getChallengingMissions(Long memberId, Pageable pageable) { - // 회원 존재 확인 - memberRepository.findById(memberId) - .orElseThrow(() -> new GeneralException(ErrorStatus.MEMBER_NOT_FOUND)); + public MissionResponseDTO.MissionPreViewListDTO getStoreMissions(Long storeId, Integer page) { + // 1. Store 조회 및 검증 + Store store = storeRepository.findById(storeId) + .orElseThrow(() -> new GeneralException(ErrorStatus.STORE_NOT_FOUND)); + + // 2. 페이징 설정 (페이지는 0부터 시작하므로 -1) + PageRequest pageRequest = PageRequest.of(page - 1, 10); - return memberMissionRepository.findChallengingMissions(memberId, pageable); + // 3. 미션 조회 + Page missionPage = missionRepository.findAllByStore(store, pageRequest); + + // 4. DTO 변환 + return MissionConverter.toMissionPreViewListDTO(missionPage); } @Override - public Page getCompletedMissions(Long memberId, Pageable pageable) { - // 회원 존재 확인 - memberRepository.findById(memberId) + public MissionResponseDTO.MemberMissionPreViewListDTO getMyChallengingMissions(Long memberId, Integer page) { + // 1. Member 조회 및 검증 + Member member = memberRepository.findById(memberId) .orElseThrow(() -> new GeneralException(ErrorStatus.MEMBER_NOT_FOUND)); - return memberMissionRepository.findCompletedMissions(memberId, pageable); + // 2. 페이징 설정 (페이지는 0부터 시작하므로 -1) + PageRequest pageRequest = PageRequest.of(page - 1, 10); + + // 3. 진행중인 미션 조회 + Page memberMissionPage = memberMissionRepository.findChallengingMissions(memberId, pageRequest); + + // 4. DTO 변환 + return MissionConverter.toMemberMissionPreViewListDTO(memberMissionPage); } } \ No newline at end of file diff --git a/src/main/java/com/example/demo/domain/review/controller/ReviewController.java b/src/main/java/com/example/demo/domain/review/controller/ReviewController.java index 03d3cce..39547e2 100644 --- a/src/main/java/com/example/demo/domain/review/controller/ReviewController.java +++ b/src/main/java/com/example/demo/domain/review/controller/ReviewController.java @@ -8,24 +8,26 @@ import com.example.demo.domain.review.service.ReviewQueryService; import com.example.demo.global.apiPayload.ApiResponse; import com.example.demo.global.apiPayload.code.status.SuccessStatus; +import com.example.demo.global.validation.annotation.CheckPage; +import org.springframework.validation.annotation.Validated; // Swagger Annotations (springdoc) import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; // Validation import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; import org.springframework.web.bind.annotation.*; @RestController @RequiredArgsConstructor @RequestMapping("/api/reviews") @Tag(name = "리뷰 API", description = "리뷰 관련 API") +@Validated public class ReviewController { private final ReviewCommandService reviewCommandService; @@ -46,25 +48,21 @@ public ApiResponse createReview( } @GetMapping("/my") - @Operation(summary = "내 리뷰 조회", description = "내가 작성한 리뷰를 조회합니다.") - public ApiResponse getMyReviews( - @Parameter(description = "회원 ID", required = true) @RequestParam Long memberId, - @Parameter(description = "가게 ID") @RequestParam(required = false) Long storeId, - @Parameter(description = "최소 별점") @RequestParam(required = false) Float minStar, - @Parameter(description = "최대 별점") @RequestParam(required = false) Float maxStar, - @Parameter(description = "페이지 번호") @RequestParam(defaultValue = "0") int page, - @Parameter(description = "페이지 크기") @RequestParam(defaultValue = "10") int size + @Operation(summary = "내가 작성한 리뷰 목록 조회", description = "내가 작성한 리뷰 목록을 페이징하여 조회합니다.") + @ApiResponses({ + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "성공"), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "잘못된 요청") + }) + public ApiResponse getMyReviews( + @Parameter(description = "회원 ID", required = true) + @RequestParam Long memberId, + + @Parameter(description = "페이지 번호 (1부터 시작)", required = false) + @RequestParam(defaultValue = "1") + @CheckPage + Integer page ) { - Page reviews = reviewQueryService.getMyReviews( - memberId, - storeId, - minStar, - maxStar, - PageRequest.of(page, size) - ); - return ApiResponse.of( - SuccessStatus.REVIEW_OK, - ReviewConverter.toReviewListDTO(reviews) - ); + ReviewResponseDTO.ReviewPreViewListDTO result = reviewQueryService.getMyReviews(memberId, page); + return ApiResponse.of(SuccessStatus.REVIEW_OK, result); } } \ No newline at end of file diff --git a/src/main/java/com/example/demo/domain/review/converter/ReviewConverter.java b/src/main/java/com/example/demo/domain/review/converter/ReviewConverter.java index a08f514..0c2f4cb 100644 --- a/src/main/java/com/example/demo/domain/review/converter/ReviewConverter.java +++ b/src/main/java/com/example/demo/domain/review/converter/ReviewConverter.java @@ -58,4 +58,28 @@ public static List toReviewImageList(List imageUrls, Review .build()) .collect(Collectors.toList()); } + + public static ReviewResponseDTO.ReviewPreViewListDTO toReviewPreViewListDTO(Page reviewPage) { + List reviewList = reviewPage.getContent().stream() + .map(ReviewConverter::toReviewPreViewDTO) + .toList(); + + return ReviewResponseDTO.ReviewPreViewListDTO.builder() + .reviewList(reviewList) + .listSize(reviewPage.getSize()) + .totalPage(reviewPage.getTotalPages()) + .totalElements(reviewPage.getTotalElements()) + .isFirst(reviewPage.isFirst()) + .isLast(reviewPage.isLast()) + .build(); + } + + public static ReviewResponseDTO.ReviewPreViewDTO toReviewPreViewDTO(Review review) { + return ReviewResponseDTO.ReviewPreViewDTO.builder() + .storeName(review.getStore().getName()) + .score(review.getStar()) + .body(review.getContent()) + .createdAt(review.getCreatedAt().toLocalDate()) + .build(); + } } \ No newline at end of file diff --git a/src/main/java/com/example/demo/domain/review/dto/ReviewResponseDTO.java b/src/main/java/com/example/demo/domain/review/dto/ReviewResponseDTO.java index c55c25f..63776de 100644 --- a/src/main/java/com/example/demo/domain/review/dto/ReviewResponseDTO.java +++ b/src/main/java/com/example/demo/domain/review/dto/ReviewResponseDTO.java @@ -5,6 +5,7 @@ import lombok.Getter; import lombok.NoArgsConstructor; +import java.time.LocalDate; import java.time.LocalDateTime; import java.util.List; @@ -54,4 +55,28 @@ public static class PageInfoDTO { private Boolean isFirst; private Boolean isLast; } + + @Builder + @Getter + @NoArgsConstructor + @AllArgsConstructor + public static class ReviewPreViewListDTO { + List reviewList; + Integer listSize; + Integer totalPage; + Long totalElements; + Boolean isFirst; + Boolean isLast; + } + + @Builder + @Getter + @NoArgsConstructor + @AllArgsConstructor + public static class ReviewPreViewDTO { + String storeName; + Float score; + String body; + LocalDate createdAt; + } } \ No newline at end of file diff --git a/src/main/java/com/example/demo/domain/review/repository/ReviewRepository.java b/src/main/java/com/example/demo/domain/review/repository/ReviewRepository.java index f395fd5..487c6bd 100644 --- a/src/main/java/com/example/demo/domain/review/repository/ReviewRepository.java +++ b/src/main/java/com/example/demo/domain/review/repository/ReviewRepository.java @@ -7,11 +7,13 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; @Repository public interface ReviewRepository extends JpaRepository, ReviewRepositoryCustom { - // 기존 메서드들 Page findByMemberOrderByCreatedAtDesc(Member member, Pageable pageable); Page findByStoreOrderByCreatedAtDesc(Store store, Pageable pageable); + Page findAllByMember(Member member, PageRequest pageRequest); } \ No newline at end of file diff --git a/src/main/java/com/example/demo/domain/review/service/ReviewQueryService.java b/src/main/java/com/example/demo/domain/review/service/ReviewQueryService.java index 353ec06..a500d18 100644 --- a/src/main/java/com/example/demo/domain/review/service/ReviewQueryService.java +++ b/src/main/java/com/example/demo/domain/review/service/ReviewQueryService.java @@ -1,9 +1,10 @@ package com.example.demo.domain.review.service; +import com.example.demo.domain.review.dto.ReviewResponseDTO; import com.example.demo.domain.review.entity.Review; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; public interface ReviewQueryService { Page getMyReviews(Long memberId, Long storeId, Float minStar, Float maxStar, Pageable pageable); -} \ No newline at end of file + ReviewResponseDTO.ReviewPreViewListDTO getMyReviews(Long memberId, Integer page);} \ No newline at end of file diff --git a/src/main/java/com/example/demo/domain/review/service/ReviewQueryServiceImpl.java b/src/main/java/com/example/demo/domain/review/service/ReviewQueryServiceImpl.java index f688d93..8540b36 100644 --- a/src/main/java/com/example/demo/domain/review/service/ReviewQueryServiceImpl.java +++ b/src/main/java/com/example/demo/domain/review/service/ReviewQueryServiceImpl.java @@ -2,13 +2,15 @@ import com.example.demo.domain.member.entity.Member; import com.example.demo.domain.member.repository.MemberRepository; +import com.example.demo.domain.review.converter.ReviewConverter; +import com.example.demo.domain.review.dto.ReviewResponseDTO; import com.example.demo.domain.review.entity.Review; import com.example.demo.domain.review.repository.ReviewRepository; import com.example.demo.global.apiPayload.code.status.ErrorStatus; import com.example.demo.global.apiPayload.exception.GeneralException; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.PageRequest; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -21,12 +23,18 @@ public class ReviewQueryServiceImpl implements ReviewQueryService { private final MemberRepository memberRepository; @Override - public Page getMyReviews(Long memberId, Long storeId, Float minStar, Float maxStar, Pageable pageable) { - // 회원 존재 확인 + public ReviewResponseDTO.ReviewPreViewListDTO getMyReviews(Long memberId, Integer page) { + // 1. Member 조회 및 검증 Member member = memberRepository.findById(memberId) .orElseThrow(() -> new GeneralException(ErrorStatus.MEMBER_NOT_FOUND)); - // 기존 Repository 메서드 사용 (간단한 조회) - return reviewRepository.findByMemberOrderByCreatedAtDesc(member, pageable); + // 2. 페이징 설정 (페이지는 0부터 시작하므로 -1) + PageRequest pageRequest = PageRequest.of(page - 1, 10); + + // 3. 리뷰 조회 + Page reviewPage = reviewRepository.findAllByMember(member, pageRequest); + + // 4. DTO 변환 + return ReviewConverter.toReviewPreViewListDTO(reviewPage); } } \ No newline at end of file diff --git a/src/main/java/com/example/demo/global/apiPayload/code/status/ErrorStatus.java b/src/main/java/com/example/demo/global/apiPayload/code/status/ErrorStatus.java index dcd40cb..367eeb6 100644 --- a/src/main/java/com/example/demo/global/apiPayload/code/status/ErrorStatus.java +++ b/src/main/java/com/example/demo/global/apiPayload/code/status/ErrorStatus.java @@ -15,6 +15,7 @@ public enum ErrorStatus implements BaseErrorCode { FORBIDDEN(HttpStatus.FORBIDDEN, "COMMON403", "금지된 요청입니다."), NOT_FOUND(HttpStatus.NOT_FOUND, "COMMON404", "리소스를 찾을 수 없습니다."), INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "COMMON500", "서버 에러, 관리자에게 문의 바랍니다."), + INVALID_PAGE(HttpStatus.BAD_REQUEST, "PAGE400", "페이지 번호는 1 이상이어야 합니다."), // 회원 관련 에러 MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "MEMBER404", "사용자가 없습니다."), diff --git a/src/main/java/com/example/demo/global/apiPayload/exception/handler/GlobalExceptionHandler.java b/src/main/java/com/example/demo/global/apiPayload/exception/handler/GlobalExceptionHandler.java index a0c51f0..cb719e4 100644 --- a/src/main/java/com/example/demo/global/apiPayload/exception/handler/GlobalExceptionHandler.java +++ b/src/main/java/com/example/demo/global/apiPayload/exception/handler/GlobalExceptionHandler.java @@ -5,11 +5,14 @@ import com.example.demo.global.apiPayload.exception.GeneralException; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; import org.springframework.validation.FieldError; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestControllerAdvice; +import jakarta.validation.ConstraintViolation; +import jakarta.validation.ConstraintViolationException; import java.util.HashMap; import java.util.Map; @@ -51,4 +54,21 @@ public ApiResponse handleException(Exception e) { e.getMessage() ); } + + @ExceptionHandler(ConstraintViolationException.class) + public ResponseEntity>> handleConstraintViolationException( + ConstraintViolationException e + ) { + Map errors = new HashMap<>(); + + for (ConstraintViolation violation : e.getConstraintViolations()) { + String propertyPath = violation.getPropertyPath().toString(); + String message = violation.getMessage(); + errors.put(propertyPath, message); + } + + return ResponseEntity + .status(HttpStatus.BAD_REQUEST) + .body(ApiResponse.onFailure(ErrorStatus.INVALID_PAGE, errors)); + } } \ No newline at end of file diff --git a/src/main/java/com/example/demo/global/validation/annotation/CheckPage.java b/src/main/java/com/example/demo/global/validation/annotation/CheckPage.java new file mode 100644 index 0000000..8b0a9ef --- /dev/null +++ b/src/main/java/com/example/demo/global/validation/annotation/CheckPage.java @@ -0,0 +1,17 @@ +package com.example.demo.global.validation.annotation; + +import com.example.demo.global.validation.validator.CheckPageValidator; +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +import java.lang.annotation.*; + +@Documented +@Constraint(validatedBy = CheckPageValidator.class) +@Target({ElementType.PARAMETER, ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface CheckPage { + String message() default "페이지 번호는 1 이상이어야 합니다."; + Class[] groups() default {}; + Class[] payload() default {}; +} \ No newline at end of file diff --git a/src/main/java/com/example/demo/global/validation/validator/CheckPageValidator.java b/src/main/java/com/example/demo/global/validation/validator/CheckPageValidator.java new file mode 100644 index 0000000..83f878b --- /dev/null +++ b/src/main/java/com/example/demo/global/validation/validator/CheckPageValidator.java @@ -0,0 +1,19 @@ +package com.example.demo.global.validation.validator; + +import com.example.demo.global.validation.annotation.CheckPage; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; + +public class CheckPageValidator implements ConstraintValidator { + + @Override + public boolean isValid(Integer value, ConstraintValidatorContext context) { + + if (value == null) { + return true; + } + + // 페이지는 1 이상 + return value >= 1; + } +} \ No newline at end of file