diff --git a/src/main/java/com/example/umc9th/Umc9thApplication.java b/src/main/java/com/example/umc9th/Umc9thApplication.java index cf0063f..b29c29f 100644 --- a/src/main/java/com/example/umc9th/Umc9thApplication.java +++ b/src/main/java/com/example/umc9th/Umc9thApplication.java @@ -2,7 +2,10 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; +@EnableJpaAuditing @SpringBootApplication public class Umc9thApplication { diff --git a/src/main/java/com/example/umc9th/domain/member/repository/MemberRepository.java b/src/main/java/com/example/umc9th/domain/member/repository/MemberRepository.java index f9b12fa..dd716aa 100644 --- a/src/main/java/com/example/umc9th/domain/member/repository/MemberRepository.java +++ b/src/main/java/com/example/umc9th/domain/member/repository/MemberRepository.java @@ -1,6 +1,7 @@ package com.example.umc9th.domain.member.repository; import com.example.umc9th.domain.member.entity.Member; +import jakarta.persistence.Id; import org.springframework.data.jpa.repository.JpaRepository; import java.util.Optional; @@ -8,4 +9,5 @@ public interface MemberRepository extends JpaRepository { // 활성 상태인 회원 단건 조회 Optional findByIdAndIsActiveTrue(Long id); + } diff --git a/src/main/java/com/example/umc9th/domain/mission/controller/MemberMissionController.java b/src/main/java/com/example/umc9th/domain/mission/controller/MemberMissionController.java index c769c01..57bc3fe 100644 --- a/src/main/java/com/example/umc9th/domain/mission/controller/MemberMissionController.java +++ b/src/main/java/com/example/umc9th/domain/mission/controller/MemberMissionController.java @@ -4,22 +4,23 @@ import com.example.umc9th.domain.mission.dto.res.MemberMissionResDTO; import com.example.umc9th.domain.mission.exception.code.MissionSuccessCode; import com.example.umc9th.domain.mission.service.command.MemberMissionCommandService; +import com.example.umc9th.domain.mission.service.query.MemberMissionQueryService; +import com.example.umc9th.global.annotation.ValidPage; import com.example.umc9th.global.apiPayload.ApiResponse; -import io.swagger.v3.oas.annotations.Operation; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; @RestController @RequiredArgsConstructor @RequestMapping("/api") -public class MemberMissionController { +@Validated +public class MemberMissionController implements MemberMissionControllerDocs { private final MemberMissionCommandService memberMissionCommandService; + private final MemberMissionQueryService memberMissionQueryService; - @Operation( - summary = "미션 도전하기", - description = "현재 회원(me)에 대해 특정 미션 도전 상태를 추가합니다. (과제에서는 하드코딩 회원 사용)" - ) + @Override @PostMapping("/members/me/missions") public ApiResponse challengeMission( @Valid @RequestBody MemberMissionReqDTO.ChallengeDTO dto @@ -27,4 +28,19 @@ public ApiResponse challengeMission( MemberMissionResDTO.ChallengeDTO response = memberMissionCommandService.challengeMission(dto); return ApiResponse.onSuccess(MissionSuccessCode.MISSION_CHALLENGED, response); } + + // 내가 진행중인 미션 목록 조회 + @Override + @GetMapping("/members/me/missions/in-progress") + public ApiResponse getMyOngoingMissions( + @RequestParam Long memberId, + @RequestParam @Valid @ValidPage Integer page + ) { + MemberMissionResDTO.OngoingMissionListDTO dto = + memberMissionQueryService.getMyOngoingMissions(memberId, page); + + return ApiResponse.onSuccess(MissionSuccessCode.MY_ONGOING_MISSIONS_FOUND, dto); + } } + + diff --git a/src/main/java/com/example/umc9th/domain/mission/controller/MemberMissionControllerDocs.java b/src/main/java/com/example/umc9th/domain/mission/controller/MemberMissionControllerDocs.java new file mode 100644 index 0000000..f22bf9d --- /dev/null +++ b/src/main/java/com/example/umc9th/domain/mission/controller/MemberMissionControllerDocs.java @@ -0,0 +1,44 @@ +package com.example.umc9th.domain.mission.controller; + +import com.example.umc9th.domain.mission.dto.req.MemberMissionReqDTO; +import com.example.umc9th.domain.mission.dto.res.MemberMissionResDTO; +import com.example.umc9th.domain.mission.dto.res.MissionResDTO; +import com.example.umc9th.global.annotation.ValidPage; +import com.example.umc9th.global.apiPayload.ApiResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import jakarta.validation.Valid; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; + +public interface MemberMissionControllerDocs { + + @Operation( + summary = "미션 도전하기", + description = "현재 회원(me)에 대해 특정 미션 도전 상태를 추가합니다. (과제에서는 하드코딩 회원 사용)" + ) + @ApiResponses({ + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "성공"), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "실패") + }) + ApiResponse challengeMission( + @Valid @RequestBody MemberMissionReqDTO.ChallengeDTO dto + ); + + @Operation( + summary = "내가 진행 중인 미션 목록 조회", + description = "현재 회원(me)이 도전 중이며 아직 완료하지 않은 미션들을 page 단위(1페이지당 10개)로 조회합니다. page는 1 이상의 정수입니다." + ) + @ApiResponses({ + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "성공"), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "실패") + }) + ApiResponse getMyOngoingMissions( + @RequestParam Long memberId, + @RequestParam @Valid @ValidPage Integer page + ); + + + +} diff --git a/src/main/java/com/example/umc9th/domain/mission/controller/MissionController.java b/src/main/java/com/example/umc9th/domain/mission/controller/MissionController.java new file mode 100644 index 0000000..3244de6 --- /dev/null +++ b/src/main/java/com/example/umc9th/domain/mission/controller/MissionController.java @@ -0,0 +1,34 @@ +package com.example.umc9th.domain.mission.controller; + +import com.example.umc9th.domain.mission.dto.res.MissionResDTO; +import com.example.umc9th.domain.mission.exception.code.MissionSuccessCode; +import com.example.umc9th.domain.mission.service.query.MissionQueryService; +import com.example.umc9th.global.annotation.ValidPage; +import com.example.umc9th.global.apiPayload.ApiResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api") +@Validated +public class MissionController implements MissionControllerDocs { + + private final MissionQueryService missionQueryService; + + @Override + @GetMapping("/restaurants/{restaurantId}/missions") + public ApiResponse getMissionsByRestaurant( + @PathVariable Long restaurantId, + @RequestParam @Valid @ValidPage Integer page + ) { + MissionResDTO.RestaurantMissionListDTO dto = + missionQueryService.findMissionsByRestaurant(restaurantId, page); + + return ApiResponse.onSuccess(MissionSuccessCode.RESTAURANT_MISSIONS_FOUND, dto); + } +} diff --git a/src/main/java/com/example/umc9th/domain/mission/controller/MissionControllerDocs.java b/src/main/java/com/example/umc9th/domain/mission/controller/MissionControllerDocs.java new file mode 100644 index 0000000..ee37d51 --- /dev/null +++ b/src/main/java/com/example/umc9th/domain/mission/controller/MissionControllerDocs.java @@ -0,0 +1,28 @@ +package com.example.umc9th.domain.mission.controller; + +import com.example.umc9th.domain.mission.dto.res.MissionResDTO; +import com.example.umc9th.domain.review.dto.res.ReviewResDTO; +import com.example.umc9th.global.annotation.ValidPage; +import com.example.umc9th.global.apiPayload.ApiResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import jakarta.validation.Valid; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestParam; + +public interface MissionControllerDocs { + + @Operation( + summary = "특정 가게의 미션 목록 조회", + description = "restaurantId에 해당하는 가게의 미션 목록을 page 단위(1페이지당 10개)로 조회합니다. page는 1 이상의 값이어야 합니다." + ) + @ApiResponses({ + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "성공"), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "실패") + }) + ApiResponse getMissionsByRestaurant( + @PathVariable Long restaurantId, + @RequestParam @Valid @ValidPage Integer page + ); + +} diff --git a/src/main/java/com/example/umc9th/domain/mission/converter/MemberMissionConverter.java b/src/main/java/com/example/umc9th/domain/mission/converter/MemberMissionConverter.java index 348fe30..5acd74f 100644 --- a/src/main/java/com/example/umc9th/domain/mission/converter/MemberMissionConverter.java +++ b/src/main/java/com/example/umc9th/domain/mission/converter/MemberMissionConverter.java @@ -2,8 +2,13 @@ import com.example.umc9th.domain.member.entity.Member; import com.example.umc9th.domain.mission.dto.res.MemberMissionResDTO; +import com.example.umc9th.domain.mission.dto.res.MissionResDTO; import com.example.umc9th.domain.mission.entity.Mission; import com.example.umc9th.domain.mission.entity.mapping.MemberMission; +import org.springframework.data.domain.Page; + +import java.util.List; +import java.util.Objects; public class MemberMissionConverter { @@ -26,4 +31,40 @@ public static MemberMissionResDTO.ChallengeDTO toChallengeDTO(MemberMission memb .createdAt(memberMission.getCreatedAt()) .build(); } + + + // 진행중인 미션 1개 -> DTO + public static MemberMissionResDTO.OngoingMissionDTO toOngoingMissionDTO(MemberMission memberMission) { + Mission mission = memberMission.getMission(); + + return MemberMissionResDTO.OngoingMissionDTO.builder() + .memberMissionId(memberMission.getId()) + .missionId(mission.getId()) + .missionName(mission.getMissionName()) + .deadline(mission.getDeadline()) + .missionPoint(mission.getMissionPoint()) + .restaurantId(mission.getRestaurant().getId()) + .restaurantName(mission.getRestaurant().getRestaurantName()) + .challengedAt(memberMission.getCreatedAt()) + .build(); + } + + // Page -> OngoingMissionPageDTO + public static MemberMissionResDTO.OngoingMissionListDTO toOngoingMissionListDTO(Page page) { + + List missionList = + page.getContent().stream() + .filter(Objects::nonNull) + .map(MemberMissionConverter::toOngoingMissionDTO) + .toList(); + + return MemberMissionResDTO.OngoingMissionListDTO.builder() + .missions(missionList) + .listSize(page.getSize()) + .totalPage(page.getTotalPages()) + .totalElements(page.getTotalElements()) + .isFirst(page.isFirst()) + .isLast(page.isLast()) + .build(); + } } diff --git a/src/main/java/com/example/umc9th/domain/mission/converter/MissionConverter.java b/src/main/java/com/example/umc9th/domain/mission/converter/MissionConverter.java new file mode 100644 index 0000000..50bb62c --- /dev/null +++ b/src/main/java/com/example/umc9th/domain/mission/converter/MissionConverter.java @@ -0,0 +1,35 @@ +package com.example.umc9th.domain.mission.converter; + +import com.example.umc9th.domain.mission.dto.res.MissionResDTO; +import com.example.umc9th.domain.mission.entity.Mission; +import org.springframework.data.domain.Page; + +import java.util.Objects; + +public class MissionConverter { + + public static MissionResDTO.RestaurantMissionDTO toRestaurantMissionDTO(Mission mission) { + return MissionResDTO.RestaurantMissionDTO.builder() + .missionId(mission.getId()) + .missionName(mission.getMissionName()) + .deadline(mission.getDeadline()) + .missionPoint(mission.getMissionPoint()) + .restaurantId(mission.getRestaurant().getId()) + .restaurantName(mission.getRestaurant().getRestaurantName()) + .build(); + } + + public static MissionResDTO.RestaurantMissionListDTO toRestaurantMissionPageDTO(Page page) { + return MissionResDTO.RestaurantMissionListDTO.builder() + .missions(page.getContent().stream() + .filter(Objects::nonNull) + .map(MissionConverter::toRestaurantMissionDTO) + .toList()) + .listSize(page.getSize()) + .totalPage(page.getTotalPages()) + .totalElements(page.getTotalElements()) + .isFirst(page.isFirst()) + .isLast(page.isLast()) + .build(); + } +} diff --git a/src/main/java/com/example/umc9th/domain/mission/dto/res/MemberMissionResDTO.java b/src/main/java/com/example/umc9th/domain/mission/dto/res/MemberMissionResDTO.java index c5b62bf..25070e5 100644 --- a/src/main/java/com/example/umc9th/domain/mission/dto/res/MemberMissionResDTO.java +++ b/src/main/java/com/example/umc9th/domain/mission/dto/res/MemberMissionResDTO.java @@ -2,7 +2,9 @@ import lombok.Builder; +import java.time.LocalDate; import java.time.LocalDateTime; +import java.util.List; public class MemberMissionResDTO { @@ -14,4 +16,29 @@ public record ChallengeDTO( Boolean isCompleted, LocalDateTime createdAt ) {} + + + // 진행중인(미완료) 미션 1개 정보 + @Builder + public record OngoingMissionDTO( + Long memberMissionId, + Long missionId, + String missionName, + LocalDate deadline, + Integer missionPoint, + Long restaurantId, + String restaurantName, + LocalDateTime challengedAt + ) {} + + // 내가 진행중인 미션 목록 + 페이징 정보 + @Builder + public record OngoingMissionListDTO( + List missions, + Integer listSize, + Integer totalPage, + Long totalElements, + Boolean isFirst, + Boolean isLast + ) {} } diff --git a/src/main/java/com/example/umc9th/domain/mission/dto/res/MissionResDTO.java b/src/main/java/com/example/umc9th/domain/mission/dto/res/MissionResDTO.java new file mode 100644 index 0000000..a5ea176 --- /dev/null +++ b/src/main/java/com/example/umc9th/domain/mission/dto/res/MissionResDTO.java @@ -0,0 +1,31 @@ +package com.example.umc9th.domain.mission.dto.res; + +import lombok.Builder; + +import java.time.LocalDate; +import java.util.List; + +public class MissionResDTO { + + // 단일 미션 정보 + @Builder + public record RestaurantMissionDTO( + Long missionId, + String missionName, + LocalDate deadline, + Integer missionPoint, + Long restaurantId, + String restaurantName + ) {} + + // 특정 가게의 미션 목록 + 페이징 정보 + @Builder + public record RestaurantMissionListDTO( + List missions, + Integer listSize, + Integer totalPage, + Long totalElements, + Boolean isFirst, + Boolean isLast + ) {} +} diff --git a/src/main/java/com/example/umc9th/domain/mission/exception/code/MissionSuccessCode.java b/src/main/java/com/example/umc9th/domain/mission/exception/code/MissionSuccessCode.java index 6b174dd..30c5b58 100644 --- a/src/main/java/com/example/umc9th/domain/mission/exception/code/MissionSuccessCode.java +++ b/src/main/java/com/example/umc9th/domain/mission/exception/code/MissionSuccessCode.java @@ -13,6 +13,17 @@ public enum MissionSuccessCode implements BaseSuccessCode { HttpStatus.CREATED, "MISSION201_1", "미션 도전에 성공했습니다." + ), + RESTAURANT_MISSIONS_FOUND( + HttpStatus.OK, + "MISSION200_1", + "특정 가게의 미션 목록을 성공적으로 조회했습니다." + ), + + MY_ONGOING_MISSIONS_FOUND( + HttpStatus.OK, + "MISSION200_2", + "내가 진행 중인 미션 목록을 성공적으로 조회했습니다." ); private final HttpStatus status; diff --git a/src/main/java/com/example/umc9th/domain/mission/repository/MissionRepository.java b/src/main/java/com/example/umc9th/domain/mission/repository/MissionRepository.java index 952d9f4..de9368f 100644 --- a/src/main/java/com/example/umc9th/domain/mission/repository/MissionRepository.java +++ b/src/main/java/com/example/umc9th/domain/mission/repository/MissionRepository.java @@ -1,7 +1,9 @@ package com.example.umc9th.domain.mission.repository; import com.example.umc9th.domain.mission.entity.Mission; +import com.example.umc9th.domain.restaurant.entity.Restaurant; import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; @@ -28,4 +30,9 @@ Page findAvailableMissionsByRegionId( @Param("today") LocalDate today, Pageable pageable ); + + Page findAllByRestaurant( + Restaurant restaurant, + PageRequest pageRequest + ); } diff --git a/src/main/java/com/example/umc9th/domain/mission/service/query/MemberMissionQueryService.java b/src/main/java/com/example/umc9th/domain/mission/service/query/MemberMissionQueryService.java new file mode 100644 index 0000000..2eca9c0 --- /dev/null +++ b/src/main/java/com/example/umc9th/domain/mission/service/query/MemberMissionQueryService.java @@ -0,0 +1,7 @@ +package com.example.umc9th.domain.mission.service.query; + +import com.example.umc9th.domain.mission.dto.res.MemberMissionResDTO; + +public interface MemberMissionQueryService { + MemberMissionResDTO.OngoingMissionListDTO getMyOngoingMissions(Long memberId, Integer page); +} diff --git a/src/main/java/com/example/umc9th/domain/mission/service/query/MemberMissionQueryServiceImpl.java b/src/main/java/com/example/umc9th/domain/mission/service/query/MemberMissionQueryServiceImpl.java new file mode 100644 index 0000000..3d8ac9d --- /dev/null +++ b/src/main/java/com/example/umc9th/domain/mission/service/query/MemberMissionQueryServiceImpl.java @@ -0,0 +1,44 @@ +package com.example.umc9th.domain.mission.service.query; + +import com.example.umc9th.domain.member.entity.Member; +import com.example.umc9th.domain.member.exception.MemberException; +import com.example.umc9th.domain.member.exception.code.MemberErrorCode; +import com.example.umc9th.domain.member.repository.MemberRepository; +import com.example.umc9th.domain.mission.converter.MemberMissionConverter; +import com.example.umc9th.domain.mission.dto.res.MemberMissionResDTO; +import com.example.umc9th.domain.mission.entity.mapping.MemberMission; +import com.example.umc9th.domain.mission.repository.MemberMissionRepository; +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; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class MemberMissionQueryServiceImpl implements MemberMissionQueryService { + + private final MemberMissionRepository memberMissionRepository; + private final MemberRepository memberRepository; + + private static final Long HARD_CODED_MEMBER_ID = 1L; + + @Override + public MemberMissionResDTO.OngoingMissionListDTO getMyOngoingMissions(Long memberId, Integer page) { + + // 컨트롤러에서 받은 memberId로 조회 + Member member = memberRepository.findByIdAndIsActiveTrue(memberId) + .orElseThrow(() -> new MemberException(MemberErrorCode.NOT_FOUND)); + + int pageIndex = page - 1; + int size = 10; // 한 페이지당 10개 + + PageRequest pageRequest = PageRequest.of(pageIndex, size); + + Page result = + memberMissionRepository.findAllByMember_IdAndIsCompletedFalse(member.getId(), pageRequest); + + return MemberMissionConverter.toOngoingMissionListDTO(result); + } +} diff --git a/src/main/java/com/example/umc9th/domain/mission/service/query/MissionQueryService.java b/src/main/java/com/example/umc9th/domain/mission/service/query/MissionQueryService.java new file mode 100644 index 0000000..93d5a46 --- /dev/null +++ b/src/main/java/com/example/umc9th/domain/mission/service/query/MissionQueryService.java @@ -0,0 +1,9 @@ +package com.example.umc9th.domain.mission.service.query; + +import com.example.umc9th.domain.mission.dto.res.MissionResDTO; + +public interface MissionQueryService { + + // 특정 가게의 미션 목록 조회 + MissionResDTO.RestaurantMissionListDTO findMissionsByRestaurant(Long restaurantId, Integer page); +} diff --git a/src/main/java/com/example/umc9th/domain/mission/service/query/MissionQueryServiceImpl.java b/src/main/java/com/example/umc9th/domain/mission/service/query/MissionQueryServiceImpl.java new file mode 100644 index 0000000..458b744 --- /dev/null +++ b/src/main/java/com/example/umc9th/domain/mission/service/query/MissionQueryServiceImpl.java @@ -0,0 +1,42 @@ +package com.example.umc9th.domain.mission.service.query; + +import com.example.umc9th.domain.mission.converter.MissionConverter; +import com.example.umc9th.domain.mission.dto.res.MissionResDTO; +import com.example.umc9th.domain.mission.entity.Mission; +import com.example.umc9th.domain.mission.repository.MissionRepository; +import com.example.umc9th.domain.restaurant.entity.Restaurant; +import com.example.umc9th.domain.restaurant.exception.RestaurantException; +import com.example.umc9th.domain.restaurant.exception.code.RestaurantErrorCode; +import com.example.umc9th.domain.restaurant.repository.RestaurantRepository; +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; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class MissionQueryServiceImpl implements MissionQueryService { + private final MissionRepository missionRepository; + private final RestaurantRepository restaurantRepository; + + @Override + public MissionResDTO.RestaurantMissionListDTO findMissionsByRestaurant(Long restaurantId, Integer page) { + + // 1) 가게 존재 여부 확인 + Restaurant restaurant = restaurantRepository.findById(restaurantId) + .orElseThrow(() -> new RestaurantException(RestaurantErrorCode.NOT_FOUND)); + + int pageIndex = page - 1; + int size = 10; //한 페이지 10개 + + PageRequest pageRequest = PageRequest.of(pageIndex, size); + + // 3) 해당 가게의 미션 목록 조회 + Page result = missionRepository.findAllByRestaurant(restaurant, pageRequest); + + // 4) DTO로 변환 + return MissionConverter.toRestaurantMissionPageDTO(result); + } +} diff --git a/src/main/java/com/example/umc9th/domain/restaurant/repository/RestaurantRepository.java b/src/main/java/com/example/umc9th/domain/restaurant/repository/RestaurantRepository.java index 88d4f80..bc0dba3 100644 --- a/src/main/java/com/example/umc9th/domain/restaurant/repository/RestaurantRepository.java +++ b/src/main/java/com/example/umc9th/domain/restaurant/repository/RestaurantRepository.java @@ -1,8 +1,10 @@ package com.example.umc9th.domain.restaurant.repository; -import com.example.umc9th.domain.member.entity.Food; import com.example.umc9th.domain.restaurant.entity.Restaurant; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Optional; + public interface RestaurantRepository extends JpaRepository { + Optional findByRestaurantName(String restaurantName); // 특정 식당의 리뷰 조회 } diff --git a/src/main/java/com/example/umc9th/domain/review/controller/ReviewController.java b/src/main/java/com/example/umc9th/domain/review/controller/ReviewController.java index e86b578..0fee1d3 100644 --- a/src/main/java/com/example/umc9th/domain/review/controller/ReviewController.java +++ b/src/main/java/com/example/umc9th/domain/review/controller/ReviewController.java @@ -6,10 +6,13 @@ import com.example.umc9th.domain.review.entity.Review; import com.example.umc9th.domain.review.exception.code.ReviewSuccessCode; import com.example.umc9th.domain.review.service.command.ReviewCommandService; +import com.example.umc9th.domain.review.service.query.ReviewQueryService; import com.example.umc9th.domain.review.service.query.ReviewQueryServiceImpl; +import com.example.umc9th.global.annotation.ValidPage; import com.example.umc9th.global.apiPayload.ApiResponse; import com.example.umc9th.global.apiPayload.code.GeneralSuccessCode; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponses; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; @@ -19,26 +22,38 @@ @RestController @RequiredArgsConstructor @RequestMapping("/api") -public class ReviewController { - private final ReviewQueryServiceImpl reviewQueryService; +public class ReviewController implements ReviewControllerDocs{ + private final ReviewQueryService reviewQueryService; private final ReviewCommandService reviewCommandService; + // 가게의 리뷰 목록 조회 + @Override + @GetMapping("/reviews") + public ApiResponse getReviews( + @RequestParam String restaurantName, + @RequestParam Integer page + ){ + + ReviewSuccessCode code = ReviewSuccessCode.FOUND; + return ApiResponse.onSuccess(code, reviewQueryService.findReview(restaurantName, page)); + } + // 내 리뷰 조회 // /api/reviews/me?memberId=7 // /api/reviews/me?memberId=7&type=restaurant&query=반이학생마라탕마라반 // /api/reviews/me?memberId=7&type=rating&query=4 // /api/reviews/me?memberId=7&type=both&query=반이학생마라탕마라반&4 + @Override @GetMapping("/reviews/me") - public ApiResponse myReviews( + public ApiResponse myReviews( @RequestParam Long memberId, - @RequestParam(required = false) String type, - @RequestParam(required = false) String query + @RequestParam @Valid @ValidPage Integer page ) { - List list = reviewQueryService.searchMyReviews(memberId, type, query); - ReviewResDTO.MyReviews dto = ReviewConverter.toMyReviews(list); + ReviewResDTO.MyReviewListDTO dto = reviewQueryService.findMyReviews(memberId, page); return ApiResponse.onSuccess(ReviewSuccessCode.MY_REVIEWS_FOUND, dto); } + // 가게에 리뷰 추가하기 @Operation(summary = "가게에 리뷰 추가하기", description = "특정 가게(storeId)에 대한 리뷰를 작성합니다.") @PostMapping("/restaurant/{restaurantId}/reviews") @@ -50,4 +65,6 @@ public ApiResponse createReview( return ApiResponse.onSuccess(ReviewSuccessCode.REVIEW_CREATED, response); } + + } diff --git a/src/main/java/com/example/umc9th/domain/review/controller/ReviewControllerDocs.java b/src/main/java/com/example/umc9th/domain/review/controller/ReviewControllerDocs.java new file mode 100644 index 0000000..277063c --- /dev/null +++ b/src/main/java/com/example/umc9th/domain/review/controller/ReviewControllerDocs.java @@ -0,0 +1,33 @@ +package com.example.umc9th.domain.review.controller; + +import com.example.umc9th.domain.review.dto.res.ReviewResDTO; +import com.example.umc9th.global.annotation.ValidPage; +import com.example.umc9th.global.apiPayload.ApiResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import jakarta.validation.Valid; +import org.springframework.web.bind.annotation.RequestParam; + +public interface ReviewControllerDocs { + + @Operation( + summary = "가게의 리뷰 목록 조회", + description = "특정 가게의 리뷰를 모두 조회합니다. 페이지네이션으로 제공합니다." + ) + @ApiResponses({ + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "성공"), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "실패") + }) + ApiResponse getReviews(String restaurantName, Integer page); + + @Operation(summary = "내가 작성한 리뷰 목록 조회", + description = "memberId가 작성한 리뷰를 page 단위(1 이상의 정수, 1페이지당 10개)로 조회합니다.") + @ApiResponses({ + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "성공"), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "실패") + }) + ApiResponse myReviews( + @RequestParam Long memberId, + @RequestParam @Valid @ValidPage Integer page + ); +} diff --git a/src/main/java/com/example/umc9th/domain/review/converter/ReviewConverter.java b/src/main/java/com/example/umc9th/domain/review/converter/ReviewConverter.java index dce5ccd..f85a843 100644 --- a/src/main/java/com/example/umc9th/domain/review/converter/ReviewConverter.java +++ b/src/main/java/com/example/umc9th/domain/review/converter/ReviewConverter.java @@ -5,9 +5,12 @@ import com.example.umc9th.domain.review.dto.req.ReviewReqDTO; import com.example.umc9th.domain.review.dto.res.ReviewResDTO; import com.example.umc9th.domain.review.entity.Review; +import org.springframework.data.domain.Page; +import java.time.LocalDate; import java.util.ArrayList; import java.util.List; +import java.util.Objects; public class ReviewConverter { public static ReviewResDTO.MyReview toMyReview(Review review) { @@ -21,20 +24,33 @@ public static ReviewResDTO.MyReview toMyReview(Review review) { } public static ReviewResDTO.MyReviews toMyReviews(List list) { - List reviews = new ArrayList<>(); - if (list != null) { - for (Review review : list) { - if (review != null) { - reviews.add(toMyReview(review)); - } - } - } + List reviews = + list == null + ? List.of() + : list.stream() + .filter(Objects::nonNull) + .map(ReviewConverter::toMyReview) + .toList(); + return ReviewResDTO.MyReviews.builder() .reviews(reviews) .totalCount(reviews.size()) .build(); } + public static ReviewResDTO.MyReviewListDTO toMyReviewPageDTO(Page result) { + return ReviewResDTO.MyReviewListDTO.builder() + .reviewList(result.getContent().stream() + .map(ReviewConverter::toMyReview) + .toList()) + .listSize(result.getSize()) + .totalPage(result.getTotalPages()) + .totalElements(result.getTotalElements()) + .isFirst(result.isFirst()) + .isLast(result.isLast()) + .build(); + } + // 리뷰 생성 : DTO -> Entity public static Review toReview(ReviewReqDTO.CreateReview dto, Member member, Restaurant restaurant) { return Review.builder() @@ -56,4 +72,30 @@ public static ReviewResDTO.CreateReview toCreateReview(Review review) { .createdAt(review.getCreatedAt()) .build(); } + + // result -> DTO + public static ReviewResDTO.ReviewPreViewListDTO toReviewPreViewListDTO(Page result) { + return ReviewResDTO.ReviewPreViewListDTO.builder() + .reviewList(result.getContent().stream() + .map(ReviewConverter::toReviewPreviewDTO) + .toList()) + .listSize(result.getSize()) + .totalPage(result.getTotalPages()) + .totalElements(result.getTotalElements()) + .isFirst(result.isFirst()) + .isLast(result.isLast()) + .build(); + } + + + public static ReviewResDTO.ReviewPreViewDTO toReviewPreviewDTO( + Review review + ){ + return ReviewResDTO.ReviewPreViewDTO.builder() + .ownerNickname(review.getMember().getName()) + .score(review.getRating()) + .body(review.getReviewContent()) + .createdAt(LocalDate.from(review.getCreatedAt())) + .build(); + } } diff --git a/src/main/java/com/example/umc9th/domain/review/dto/res/ReviewResDTO.java b/src/main/java/com/example/umc9th/domain/review/dto/res/ReviewResDTO.java index e3cae2a..7327d82 100644 --- a/src/main/java/com/example/umc9th/domain/review/dto/res/ReviewResDTO.java +++ b/src/main/java/com/example/umc9th/domain/review/dto/res/ReviewResDTO.java @@ -1,7 +1,11 @@ package com.example.umc9th.domain.review.dto.res; +import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDate; import java.time.LocalDateTime; import java.util.List; @@ -17,6 +21,17 @@ public static class MyReview { private LocalDateTime createdAt; } + // 내 리뷰 목록 + 페이징 정보 + @Builder + public record MyReviewListDTO( + List reviewList, + Integer listSize, + Integer totalPage, + Long totalElements, + Boolean isFirst, + Boolean isLast + ) {} + @Builder @Getter public static class MyReviews { @@ -34,4 +49,40 @@ public record CreateReview( Double rating, LocalDateTime createdAt ){} + + @Builder + public record ReviewPreViewListDTO( + List reviewList, + Integer listSize, + Integer totalPage, + Long totalElements, + Boolean isFirst, + Boolean isLast + ){} + + // static class로 구현 +// @Builder +// @Getter +// @NoArgsConstructor +// @AllArgsConstructor +// public static class ReviewPreViewListDTO( +// List reviewList, +// Integer listSize, +// Integer totalPage, +// Long totalElements, +// Boolean isFirst, +// Boolean isLast +// ){} + + @Builder + public record ReviewPreViewDTO( + String ownerNickname, + Double score, + String body, + LocalDate createdAt + ){} + + + + } diff --git a/src/main/java/com/example/umc9th/domain/review/exception/code/ReviewErrorCode.java b/src/main/java/com/example/umc9th/domain/review/exception/code/ReviewErrorCode.java index 1408d0e..ab8ae77 100644 --- a/src/main/java/com/example/umc9th/domain/review/exception/code/ReviewErrorCode.java +++ b/src/main/java/com/example/umc9th/domain/review/exception/code/ReviewErrorCode.java @@ -11,7 +11,9 @@ public enum ReviewErrorCode implements BaseErrorCode { INVALID_TYPE(HttpStatus.BAD_REQUEST, "REVIEW400_1", "type 값이 올바르지 않습니다. [restaurant|rating|both|all]"), MISSING_QUERY(HttpStatus.BAD_REQUEST, "REVIEW400_2", "해당 type에 필요한 query 값이 없습니다."), INVALID_BOTH_QUERY(HttpStatus.BAD_REQUEST, "REVIEW400_3", "both 타입의 query는 '이름&숫자' 형식이어야 합니다."), - INVALID_RATING(HttpStatus.BAD_REQUEST, "REVIEW400_4", "rating은 0.0 ~ 5.0 사이의 숫자여야 합니다."),; + INVALID_RATING(HttpStatus.BAD_REQUEST, "REVIEW400_4", "rating은 0.0 ~ 5.0 사이의 숫자여야 합니다."), + NOT_FOUND(HttpStatus.NOT_FOUND, "REVIEW404_1", "해당 리뷰를 찾을 수 없습니다."), + MY_REVIEW_NOT_FOUND(HttpStatus.NOT_FOUND, "REVIEW404_2", "해당 회원의 리뷰를 찾을 수 없습니다."); private final HttpStatus status; private final String code; diff --git a/src/main/java/com/example/umc9th/domain/review/exception/code/ReviewSuccessCode.java b/src/main/java/com/example/umc9th/domain/review/exception/code/ReviewSuccessCode.java index 7dce9f7..b5fb583 100644 --- a/src/main/java/com/example/umc9th/domain/review/exception/code/ReviewSuccessCode.java +++ b/src/main/java/com/example/umc9th/domain/review/exception/code/ReviewSuccessCode.java @@ -9,17 +9,9 @@ @AllArgsConstructor public enum ReviewSuccessCode implements BaseSuccessCode { - REVIEW_CREATED( - HttpStatus.CREATED, - "REVIEW201_1", - "리뷰가 성공적으로 등록되었습니다." - ), - - MY_REVIEWS_FOUND( - HttpStatus.OK, - "REVIEW200_1", - "내 리뷰 목록을 성공적으로 조회했습니다." - ); + REVIEW_CREATED(HttpStatus.CREATED, "REVIEW201_1", "리뷰가 성공적으로 등록되었습니다."), + MY_REVIEWS_FOUND(HttpStatus.OK,"REVIEW200_1","내 리뷰 목록을 성공적으로 조회했습니다."), + FOUND(HttpStatus.OK, "REVIEW_200_2", "리뷰를 성공적으로 찾았습니다."); private final HttpStatus status; private final String code; diff --git a/src/main/java/com/example/umc9th/domain/review/repository/ReviewRepository.java b/src/main/java/com/example/umc9th/domain/review/repository/ReviewRepository.java index 64d965b..b6175d1 100644 --- a/src/main/java/com/example/umc9th/domain/review/repository/ReviewRepository.java +++ b/src/main/java/com/example/umc9th/domain/review/repository/ReviewRepository.java @@ -3,6 +3,8 @@ import com.example.umc9th.domain.member.entity.Member; import com.example.umc9th.domain.restaurant.entity.Restaurant; import com.example.umc9th.domain.review.entity.Review; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; @@ -13,5 +15,7 @@ public interface ReviewRepository extends JpaRepository, ReviewQue List findByRestaurant(Restaurant restaurant); // 특정 회원이 작성한 리뷰 조회 - List findByMember(Member member, Pageable pageable); + Page findByMember(Member member, PageRequest pageRequest); + + Page findAllByRestaurant(Restaurant restaurantName, PageRequest pageRequest); } diff --git a/src/main/java/com/example/umc9th/domain/review/service/query/ReviewQueryService.java b/src/main/java/com/example/umc9th/domain/review/service/query/ReviewQueryService.java index 98ae711..b18291e 100644 --- a/src/main/java/com/example/umc9th/domain/review/service/query/ReviewQueryService.java +++ b/src/main/java/com/example/umc9th/domain/review/service/query/ReviewQueryService.java @@ -1,9 +1,15 @@ package com.example.umc9th.domain.review.service.query; +import com.example.umc9th.domain.review.dto.res.ReviewResDTO; import com.example.umc9th.domain.review.entity.Review; import java.util.List; public interface ReviewQueryService { List searchMyReviews(Long memberId, String type, String query); + + ReviewResDTO.ReviewPreViewListDTO findReview(String restaurantName, Integer page); + + // 내가 작성한 리뷰 목록 + ReviewResDTO.MyReviewListDTO findMyReviews(Long memberId, Integer page); } diff --git a/src/main/java/com/example/umc9th/domain/review/service/query/ReviewQueryServiceImpl.java b/src/main/java/com/example/umc9th/domain/review/service/query/ReviewQueryServiceImpl.java index 2fe7e9e..34ce9e3 100644 --- a/src/main/java/com/example/umc9th/domain/review/service/query/ReviewQueryServiceImpl.java +++ b/src/main/java/com/example/umc9th/domain/review/service/query/ReviewQueryServiceImpl.java @@ -1,5 +1,15 @@ package com.example.umc9th.domain.review.service.query; +import com.example.umc9th.domain.member.entity.Member; +import com.example.umc9th.domain.member.exception.MemberException; +import com.example.umc9th.domain.member.exception.code.MemberErrorCode; +import com.example.umc9th.domain.member.repository.MemberRepository; +import com.example.umc9th.domain.restaurant.entity.Restaurant; +import com.example.umc9th.domain.restaurant.exception.RestaurantException; +import com.example.umc9th.domain.restaurant.exception.code.RestaurantErrorCode; +import com.example.umc9th.domain.restaurant.repository.RestaurantRepository; +import com.example.umc9th.domain.review.converter.ReviewConverter; +import com.example.umc9th.domain.review.dto.res.ReviewResDTO; import com.example.umc9th.domain.review.entity.QReview; import com.example.umc9th.domain.review.entity.Review; import com.example.umc9th.domain.review.exception.ReviewException; @@ -8,6 +18,8 @@ import com.querydsl.core.BooleanBuilder; import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; import org.springframework.stereotype.Service; import java.util.List; @@ -17,8 +29,11 @@ @Transactional public class ReviewQueryServiceImpl implements ReviewQueryService { private final ReviewRepository reviewRepository; + private final RestaurantRepository restaurantRepository; + private final MemberRepository memberRepository; + @Override public List searchMyReviews(Long memberId, String type, String query){ String safeType = (type == null) ? "all" : type.trim(); String safeQuery = (query == null) ? "" : query.trim(); @@ -68,6 +83,36 @@ public List searchMyReviews(Long memberId, String type, String query){ return reviewList; } + @Override + public ReviewResDTO.ReviewPreViewListDTO findReview(String restaurantName, Integer page){ + + // 가게를 가져온다 (가게 존재 여부 검증) + Restaurant restaurant = restaurantRepository.findByRestaurantName(restaurantName) + // 없으면 예외 터뜨리기 + .orElseThrow(() -> new RestaurantException(RestaurantErrorCode.NOT_FOUND)); + PageRequest pageRequest = PageRequest.of(page, 5); + Page result = reviewRepository.findAllByRestaurant(restaurant, pageRequest); + return ReviewConverter.toReviewPreViewListDTO(result); + } + + // 내가 작성한 리뷰 목록 페이징 + @Override + public ReviewResDTO.MyReviewListDTO findMyReviews(Long memberId, Integer page) { + + Member member = memberRepository.findById(memberId) + .orElseThrow(() -> new MemberException(MemberErrorCode.NOT_FOUND)); + + // 프론트는 1 이상 전달 PageRequest는 0부터 + int pageIndex = page - 1; + int size = 10; // 한 페이지당 10개 + + PageRequest pageRequest = PageRequest.of(pageIndex, size); + + Page result = reviewRepository.findByMember(member, pageRequest); + + return ReviewConverter.toMyReviewPageDTO(result); + } + private void applyRatingFilterOrThrow(BooleanBuilder builder, QReview review, String ratingText) { if (ratingText == null || ratingText.isEmpty()) return; diff --git a/src/main/java/com/example/umc9th/global/annotation/ValidPage.java b/src/main/java/com/example/umc9th/global/annotation/ValidPage.java new file mode 100644 index 0000000..2918a09 --- /dev/null +++ b/src/main/java/com/example/umc9th/global/annotation/ValidPage.java @@ -0,0 +1,22 @@ +// package 예시: com.example.umc9th.global.validation + +package com.example.umc9th.global.annotation; + +import com.example.umc9th.global.validator.PageValidator; +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +import java.lang.annotation.*; + +@Documented +@Constraint(validatedBy = PageValidator .class) +@Target({ElementType.PARAMETER, ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface ValidPage { + + String message() default "page는 1 이상의 값이어야 합니다."; + + Class[] groups() default {}; + + Class[] payload() default {}; +} diff --git a/src/main/java/com/example/umc9th/global/config/JpaConfig.java b/src/main/java/com/example/umc9th/global/config/JpaConfig.java deleted file mode 100644 index b256807..0000000 --- a/src/main/java/com/example/umc9th/global/config/JpaConfig.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.example.umc9th.global.config; - -import org.springframework.context.annotation.Configuration; -import org.springframework.data.jpa.repository.config.EnableJpaAuditing; - -@Configuration -@EnableJpaAuditing -public class JpaConfig { -} diff --git a/src/main/java/com/example/umc9th/global/validator/PageValidator.java b/src/main/java/com/example/umc9th/global/validator/PageValidator.java new file mode 100644 index 0000000..a0efa45 --- /dev/null +++ b/src/main/java/com/example/umc9th/global/validator/PageValidator.java @@ -0,0 +1,22 @@ +package com.example.umc9th.global.validator; + +import com.example.umc9th.global.annotation.ValidPage; +import com.example.umc9th.global.apiPayload.code.GeneralErrorCode; +import com.example.umc9th.global.apiPayload.exception.GeneralException; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; + +public class PageValidator implements ConstraintValidator { + + @Override + public boolean isValid(Integer value, ConstraintValidatorContext context) { + + // null이거나 1 미만이면 바로 GeneralException 던짐 + if (value == null || value < 1) { + throw new GeneralException(GeneralErrorCode.VALID_FAIL); + } + + // 여기까지 왔으면 유효 + return true; + } +}