diff --git a/src/main/java/com/umc/training/domain/member/entity/repository/MemberMissionRepository.java b/src/main/java/com/umc/training/domain/member/entity/repository/MemberMissionRepository.java new file mode 100644 index 0000000..c48874d --- /dev/null +++ b/src/main/java/com/umc/training/domain/member/entity/repository/MemberMissionRepository.java @@ -0,0 +1,16 @@ +package com.umc.training.domain.member.entity.repository; + +import com.umc.training.domain.member.entity.MemberMission; +import com.umc.training.domain.mission.entity.enums.MissionStatus; + +import org.springframework.data.domain.Pageable; +import org.springframework.data.repository.Repository; + +import java.util.List; + +public interface MemberMissionRepository extends Repository { + + MemberMission save(MemberMission memberMission); + + List findByMemberIdAndStatus(Long memberId, MissionStatus status, Pageable pageable); +} diff --git a/src/main/java/com/umc/training/domain/member/entity/repository/MemberRepository.java b/src/main/java/com/umc/training/domain/member/entity/repository/MemberRepository.java index 5efd549..f5cc44b 100644 --- a/src/main/java/com/umc/training/domain/member/entity/repository/MemberRepository.java +++ b/src/main/java/com/umc/training/domain/member/entity/repository/MemberRepository.java @@ -3,10 +3,16 @@ import com.umc.training.domain.member.entity.Member; import org.springframework.data.repository.Repository; +import java.util.Optional; + public interface MemberRepository extends Repository { // 미션 2 Member findById(Member member); + Optional findById(Long id); + Integer countById(Long id); + + boolean existsById(Long id); } diff --git a/src/main/java/com/umc/training/domain/mission/controller/MissionController.java b/src/main/java/com/umc/training/domain/mission/controller/MissionController.java new file mode 100644 index 0000000..a37c32b --- /dev/null +++ b/src/main/java/com/umc/training/domain/mission/controller/MissionController.java @@ -0,0 +1,53 @@ +package com.umc.training.domain.mission.controller; + +import com.umc.training.domain.mission.dto.response.MemberMissionResponseDTO; +import com.umc.training.domain.mission.dto.response.MissionResponseDTO; +import com.umc.training.domain.mission.service.MissionService; +import com.umc.training.global.apiPayload.ApiResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/missions") +public class MissionController implements MissionControllerDocs { + + private final MissionService missionService; + + @Override + @PostMapping("/{missionId}/user/{userId}/challenge") + public ApiResponse challengeMission( + @PathVariable("missionId") Long missionId, + @PathVariable("userId") Long userId) { + + missionService.challengeMission(missionId, userId); + return ApiResponse.onSuccess(null); + } + + @Override + @GetMapping("/store/{storeId}") + public ApiResponse> getStoreMissions( + @PathVariable("storeId") Long storeId, + @RequestParam("page") int page, + @RequestParam("size") int size) { + + return ApiResponse.onSuccess(missionService.getStoreMissions(storeId, page, size)); + } + + @Override + @GetMapping("/user/{userId}/in-progress") + public ApiResponse> getMyInProgressMissions( + @PathVariable("userId") Long userId, + @RequestParam("page") int page, + @RequestParam("size") int size) { + + return ApiResponse.onSuccess(missionService.getMyInProgressMissions(userId, page, size)); + } +} diff --git a/src/main/java/com/umc/training/domain/mission/controller/MissionControllerDocs.java b/src/main/java/com/umc/training/domain/mission/controller/MissionControllerDocs.java new file mode 100644 index 0000000..a489189 --- /dev/null +++ b/src/main/java/com/umc/training/domain/mission/controller/MissionControllerDocs.java @@ -0,0 +1,68 @@ +package com.umc.training.domain.mission.controller; + +import com.umc.training.domain.mission.dto.response.MemberMissionResponseDTO; +import com.umc.training.domain.mission.dto.response.MissionResponseDTO; +import com.umc.training.global.apiPayload.ApiResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; + +import java.util.List; + +@Tag(name = "미션", description = "미션 관련 API") +public interface MissionControllerDocs { + + @Operation( + summary = "미션 도전하기", + description = "특정 미션을 사용자의 진행 중인 미션 목록에 추가합니다." + ) + @ApiResponses({ + @io.swagger.v3.oas.annotations.responses.ApiResponse( + responseCode = "200", + description = "미션 도전 성공", + content = @Content(schema = @Schema(implementation = ApiResponse.class)) + ) + }) + ApiResponse challengeMission( + @Parameter(description = "미션 ID", required = true) Long missionId, + @Parameter(description = "사용자 ID", required = true) Long userId + ); + + @Operation( + summary = "특정 가게의 미션 목록 조회", + description = "페이징을 적용하여 특정 가게의 모든 미션을 조회합니다." + ) + @ApiResponses({ + @io.swagger.v3.oas.annotations.responses.ApiResponse( + responseCode = "200", + description = "조회 성공", + content = @Content(schema = @Schema(implementation = ApiResponse.class)) + ) + }) + ApiResponse> getStoreMissions( + @Parameter(description = "가게 ID", required = true) Long storeId, + @Parameter(description = "페이지 번호 (0부터 시작)", required = true) int page, + @Parameter(description = "페이지 크기", required = true) int size + ); + + @Operation( + summary = "내가 진행중인 미션 목록 조회", + description = "페이징을 적용하여 사용자가 진행 중인 모든 미션을 조회합니다." + ) + @ApiResponses({ + @io.swagger.v3.oas.annotations.responses.ApiResponse( + responseCode = "200", + description = "조회 성공", + content = @Content(schema = @Schema(implementation = ApiResponse.class)) + ) + }) + ApiResponse> getMyInProgressMissions( + @Parameter(description = "사용자 ID", required = true) Long userId, + @Parameter(description = "페이지 번호 (0부터 시작)", required = true) int page, + @Parameter(description = "페이지 크기", required = true) int size + ); +} + diff --git a/src/main/java/com/umc/training/domain/mission/dto/response/MemberMissionResponseDTO.java b/src/main/java/com/umc/training/domain/mission/dto/response/MemberMissionResponseDTO.java new file mode 100644 index 0000000..49beb3b --- /dev/null +++ b/src/main/java/com/umc/training/domain/mission/dto/response/MemberMissionResponseDTO.java @@ -0,0 +1,31 @@ +package com.umc.training.domain.mission.dto.response; + +import com.umc.training.domain.member.entity.MemberMission; +import com.umc.training.domain.mission.entity.enums.MissionStatus; + +import java.time.LocalDate; + +public record MemberMissionResponseDTO( + Long id, + Long missionId, + Long storeId, + String storeName, + LocalDate deadline, + MissionStatus status, + String missionContent, + LocalDate createdAt +) { + public MemberMissionResponseDTO(MemberMission memberMission) { + this( + memberMission.getId(), + memberMission.getMission().getId(), + memberMission.getStore().getId(), + memberMission.getStore().getName(), + memberMission.getDeadline(), + memberMission.getStatus(), + memberMission.getMissionContent(), + memberMission.getCreatedAt() + ); + } +} + diff --git a/src/main/java/com/umc/training/domain/mission/dto/response/MissionResponseDTO.java b/src/main/java/com/umc/training/domain/mission/dto/response/MissionResponseDTO.java new file mode 100644 index 0000000..8107597 --- /dev/null +++ b/src/main/java/com/umc/training/domain/mission/dto/response/MissionResponseDTO.java @@ -0,0 +1,22 @@ +package com.umc.training.domain.mission.dto.response; + +import com.umc.training.domain.mission.entity.Mission; + +import java.time.LocalDate; + +public record MissionResponseDTO( + Long id, + Integer reward, + LocalDate deadline, + String missionSpec +) { + public MissionResponseDTO(Mission mission) { + this( + mission.getId(), + mission.getReward(), + mission.getDeadline(), + mission.getMissionSpec() + ); + } +} + diff --git a/src/main/java/com/umc/training/domain/mission/entity/repository/MissionRepository.java b/src/main/java/com/umc/training/domain/mission/entity/repository/MissionRepository.java index 476aa05..5e2ff01 100644 --- a/src/main/java/com/umc/training/domain/mission/entity/repository/MissionRepository.java +++ b/src/main/java/com/umc/training/domain/mission/entity/repository/MissionRepository.java @@ -2,7 +2,6 @@ import com.umc.training.domain.member.entity.Member; import com.umc.training.domain.member.entity.MemberMission; -import com.umc.training.domain.member.entity.enums.MemberStatus; import com.umc.training.domain.mission.entity.Mission; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -10,50 +9,51 @@ import org.springframework.data.repository.Repository; import org.springframework.data.repository.query.Param; -public interface MissionRepository extends Repository { +import java.util.List; +import java.util.Optional; +public interface MissionRepository extends Repository { - // 미션 3 - @Query( - value = "SELECT m " + - "FROM MemberMission m " + - "left join fetch Mission " + - "where m.member.id = :member " + - "AND m.status IN (" + - "com.umc.training.domain.mission.entity.enums.MissionStatus.IN_PROGRESS, " + - "com.umc.training.domain.mission.entity.enums.MissionStatus.IN_THE_WORKS" + - ") " + - "order by m.createdAt DESC", - countQuery = "SELECT COUNT(m) " + - "FROM MemberMission m " + - "WHERE m.member = :member " + - "AND m.status IN (" + - "com.umc.training.domain.mission.entity.enums.MissionStatus.IN_PROGRESS, " + - "com.umc.training.domain.mission.entity.enums.MissionStatus.IN_THE_WORKS" + - ")" - - ) - Page findMissionInProgressOrCompletedByMember( - @Param("member") Member member, Pageable pageable); - - - @Query( - value = "SELECT m " + - "FROM MemberMission m " + - "left join fetch Store s " + - "left join fetch Mission " + - "WHERE s.region.id = :region_id " + - "AND m.member.id IS NULL " + - "AND m.status = com.umc.training.domain.mission.entity.enums.MissionStatus.IN_THE_WORKS " + - "ORDER BY m.createdAt DESC", - - countQuery = "SELECT COUNT(m) " + - "FROM MemberMission m " + - "LEFT JOIN m.store s " + - "WHERE s.region.id = :region_id " + - "AND m.member IS NULL " + - "AND m.status = com.umc.training.domain.mission.entity.enums.MissionStatus.IN_THE_WORKS" - ) - Page findChallengingMissionByMember( - @Param("region_id") Long region_id, Pageable pageable); + Optional findById(Long id); + + List findByStoreId(Long storeId, Pageable pageable); + + // 미션 3 + @Query(value = "SELECT m " + + "FROM MemberMission m " + + "left join fetch Mission " + + "where m.member.id = :member " + + "AND m.status IN (" + + "com.umc.training.domain.mission.entity.enums.MissionStatus.IN_PROGRESS, " + + "com.umc.training.domain.mission.entity.enums.MissionStatus.IN_THE_WORKS" + + ") " + + "order by m.createdAt DESC", countQuery = "SELECT COUNT(m) " + + "FROM MemberMission m " + + "WHERE m.member = :member " + + "AND m.status IN (" + + "com.umc.training.domain.mission.entity.enums.MissionStatus.IN_PROGRESS, " + + "com.umc.training.domain.mission.entity.enums.MissionStatus.IN_THE_WORKS" + + ")" + + ) + Page findMissionInProgressOrCompletedByMember( + @Param("member") Member member, Pageable pageable); + + @Query(value = "SELECT m " + + "FROM MemberMission m " + + "left join fetch Store s " + + "left join fetch Mission " + + "WHERE s.region.id = :region_id " + + "AND m.member.id IS NULL " + + "AND m.status = com.umc.training.domain.mission.entity.enums.MissionStatus.IN_THE_WORKS " + + "ORDER BY m.createdAt DESC", + + countQuery = "SELECT COUNT(m) " + + "FROM MemberMission m " + + "LEFT JOIN m.store s " + + "WHERE s.region.id = :region_id " + + "AND m.member IS NULL " + + "AND m.status = com.umc.training.domain.mission.entity.enums.MissionStatus.IN_THE_WORKS") + Page findChallengingMissionByMember( + @Param("region_id") Long region_id, Pageable pageable); } diff --git a/src/main/java/com/umc/training/domain/mission/exception/MissionException.java b/src/main/java/com/umc/training/domain/mission/exception/MissionException.java new file mode 100644 index 0000000..cd02bd4 --- /dev/null +++ b/src/main/java/com/umc/training/domain/mission/exception/MissionException.java @@ -0,0 +1,12 @@ +package com.umc.training.domain.mission.exception; + +import com.umc.training.domain.mission.exception.code.MissionErrorCode; +import com.umc.training.global.apiPayload.exception.GeneralException; + +public class MissionException extends GeneralException { + + public MissionException(MissionErrorCode code) { + super(code); + } +} + diff --git a/src/main/java/com/umc/training/domain/mission/exception/code/MissionErrorCode.java b/src/main/java/com/umc/training/domain/mission/exception/code/MissionErrorCode.java new file mode 100644 index 0000000..13202d1 --- /dev/null +++ b/src/main/java/com/umc/training/domain/mission/exception/code/MissionErrorCode.java @@ -0,0 +1,18 @@ +package com.umc.training.domain.mission.exception.code; + +import com.umc.training.global.apiPayload.code.BaseErrorCode; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor +public enum MissionErrorCode implements BaseErrorCode { + + MISSION_NOT_FOUND(HttpStatus.NOT_FOUND, "MISSION_NOT_FOUND_4001", "Mission not found."); + + private final HttpStatus status; + private final String message; + private final String code; +} + diff --git a/src/main/java/com/umc/training/domain/mission/service/MissionService.java b/src/main/java/com/umc/training/domain/mission/service/MissionService.java new file mode 100644 index 0000000..2638f66 --- /dev/null +++ b/src/main/java/com/umc/training/domain/mission/service/MissionService.java @@ -0,0 +1,91 @@ +package com.umc.training.domain.mission.service; + +import com.umc.training.domain.member.entity.Member; +import com.umc.training.domain.member.entity.MemberMission; +import com.umc.training.domain.member.entity.exception.code.MemberBaseCode; +import com.umc.training.domain.member.entity.exception.code.MemberException; +import com.umc.training.domain.member.entity.repository.MemberMissionRepository; +import com.umc.training.domain.member.entity.repository.MemberRepository; +import com.umc.training.domain.mission.entity.Mission; +import com.umc.training.domain.mission.entity.enums.MissionStatus; +import com.umc.training.domain.mission.exception.MissionException; +import com.umc.training.domain.mission.exception.code.MissionErrorCode; +import com.umc.training.domain.mission.dto.response.MemberMissionResponseDTO; +import com.umc.training.domain.mission.dto.response.MissionResponseDTO; +import com.umc.training.domain.mission.entity.repository.MissionRepository; +import com.umc.training.domain.store.entity.Store; +import com.umc.training.domain.store.exception.StoreException; +import com.umc.training.domain.store.exception.code.StoreErrorCode; +import com.umc.training.domain.store.repository.StoreRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import org.springframework.data.domain.PageRequest; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDate; +import java.util.List; + +@Slf4j +@Service +@RequiredArgsConstructor +@Transactional +public class MissionService { + + private final MemberMissionRepository memberMissionRepository; + private final MemberRepository memberRepository; + private final MissionRepository missionRepository; + private final StoreRepository storeRepository; + + public void challengeMission(Long missionId, Long userId) { + + Member member = memberRepository.findById(userId) + .orElseThrow(() -> new MemberException(MemberBaseCode.MEMBER_NOT_FOUND)); + + Mission mission = missionRepository.findById(missionId) + .orElseThrow(() -> new MissionException(MissionErrorCode.MISSION_NOT_FOUND)); + + Store store = mission.getStore(); + + MemberMission memberMission = MemberMission.builder() + .member(member) + .mission(mission) + .store(store) + .deadline(mission.getDeadline()) + .status(MissionStatus.IN_PROGRESS) + .missionContent(mission.getMissionSpec()) + .createdAt(LocalDate.now()) + .build(); + + memberMissionRepository.save(memberMission); + } + + @Transactional(readOnly = true) + public List getStoreMissions(Long storeId, int page, int size) { + + if (!storeRepository.existsById(storeId)) { + throw new StoreException(StoreErrorCode.STORE_NOT_FOUND); + } + + List missions = missionRepository.findByStoreId(storeId, PageRequest.of(page, size)); + + return missions.stream() + .map(MissionResponseDTO::new) + .toList(); + } + + @Transactional(readOnly = true) + public List getMyInProgressMissions(Long userId, int page, int size) { + if (!memberRepository.existsById(userId)) { + throw new MemberException(MemberBaseCode.MEMBER_NOT_FOUND); + } + + List memberMissions = memberMissionRepository.findByMemberIdAndStatus( + userId, MissionStatus.IN_PROGRESS, PageRequest.of(page, size)); + + return memberMissions.stream() + .map(MemberMissionResponseDTO::new) + .toList(); + } +} diff --git a/src/main/java/com/umc/training/domain/region/exception/RegionException.java b/src/main/java/com/umc/training/domain/region/exception/RegionException.java new file mode 100644 index 0000000..320726c --- /dev/null +++ b/src/main/java/com/umc/training/domain/region/exception/RegionException.java @@ -0,0 +1,12 @@ +package com.umc.training.domain.region.exception; + +import com.umc.training.domain.region.exception.code.RegionErrorCode; +import com.umc.training.global.apiPayload.exception.GeneralException; + +public class RegionException extends GeneralException { + + public RegionException(RegionErrorCode code) { + super(code); + } +} + diff --git a/src/main/java/com/umc/training/domain/region/exception/code/RegionErrorCode.java b/src/main/java/com/umc/training/domain/region/exception/code/RegionErrorCode.java new file mode 100644 index 0000000..36bbe1c --- /dev/null +++ b/src/main/java/com/umc/training/domain/region/exception/code/RegionErrorCode.java @@ -0,0 +1,18 @@ +package com.umc.training.domain.region.exception.code; + +import com.umc.training.global.apiPayload.code.BaseErrorCode; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor +public enum RegionErrorCode implements BaseErrorCode { + + REGION_NOT_FOUND(HttpStatus.NOT_FOUND, "REGION_NOT_FOUND_4001", "Region not found."); + + private final HttpStatus status; + private final String message; + private final String code; +} + diff --git a/src/main/java/com/umc/training/domain/region/repository/RegionRepository.java b/src/main/java/com/umc/training/domain/region/repository/RegionRepository.java new file mode 100644 index 0000000..979a520 --- /dev/null +++ b/src/main/java/com/umc/training/domain/region/repository/RegionRepository.java @@ -0,0 +1,16 @@ +package com.umc.training.domain.region.repository; + +import com.umc.training.domain.region.entity.Region; +import org.springframework.data.repository.Repository; + +import java.util.Optional; + +public interface RegionRepository extends Repository { + + Optional findById(Long id); + + boolean existsById(Long id); + + Region save(Region region); +} + diff --git a/src/main/java/com/umc/training/domain/review/ReviewController.java b/src/main/java/com/umc/training/domain/review/ReviewController.java deleted file mode 100644 index 3328ae1..0000000 --- a/src/main/java/com/umc/training/domain/review/ReviewController.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.umc.training.domain.review; - -import com.umc.training.domain.review.dto.response.ReviewResponseDTO; -import com.umc.training.global.apiPayload.ApiResponse; -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 java.util.List; - -@RestController -@RequiredArgsConstructor -@RequestMapping("/api/reviews") -public class ReviewController { - - private final ReviewService reviewService; - - @GetMapping - public ApiResponse> getMyReview( - @RequestParam("userId") Long userId, - @RequestParam("query") String query, - @RequestParam("type") String type - ) { - - return ApiResponse.onSuccess(reviewService.getMyReview(userId, query, type)); - } - -} diff --git a/src/main/java/com/umc/training/domain/review/ReviewService.java b/src/main/java/com/umc/training/domain/review/ReviewService.java deleted file mode 100644 index c55a38e..0000000 --- a/src/main/java/com/umc/training/domain/review/ReviewService.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.umc.training.domain.review; - -import com.umc.training.domain.member.entity.exception.code.MemberBaseCode; -import com.umc.training.domain.member.entity.exception.code.MemberException; -import com.umc.training.domain.member.entity.repository.MemberRepository; -import com.umc.training.domain.review.dto.response.ReviewResponseDTO; -import com.umc.training.domain.review.entity.Review; -import com.umc.training.domain.review.repository.ReviewRepository; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.util.List; - -@Slf4j -@Service -@Transactional(readOnly = true) -@RequiredArgsConstructor -public class ReviewService { - - private final ReviewRepository reviewRepository; - private final MemberRepository memberRepository; - - public List getMyReview(Long userId, String query, String type) { - - log.info("memberCount : {}", memberRepository.countById(userId)); - if(memberRepository.countById(userId) == 0) { - throw new MemberException(MemberBaseCode.MEMBER_NOT_FOUND); - } - - List reviewList = reviewRepository.findByUserId(userId, query, type); - - return reviewList.stream() - .map(ReviewResponseDTO::new) - .toList(); - - } - -} diff --git a/src/main/java/com/umc/training/domain/review/controller/ReviewController.java b/src/main/java/com/umc/training/domain/review/controller/ReviewController.java new file mode 100644 index 0000000..d0f6d83 --- /dev/null +++ b/src/main/java/com/umc/training/domain/review/controller/ReviewController.java @@ -0,0 +1,57 @@ +package com.umc.training.domain.review.controller; + +import com.umc.training.domain.review.service.ReviewService; +import com.umc.training.domain.review.dto.request.StoreAddReviewRequestDTO; +import com.umc.training.domain.review.dto.response.ReviewResponseDTO; +import com.umc.training.global.apiPayload.ApiResponse; + +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/reviews") +public class ReviewController implements ReviewControllerDocs { + + private final ReviewService reviewService; + + @Override + @GetMapping + public ApiResponse> getMyReview( + @RequestParam("userId") Long userId, + @RequestParam("query") String query, + @RequestParam("type") String type) { + + return ApiResponse.onSuccess(reviewService.getMyReview(userId, query, type)); + } + + @Override + @GetMapping("/user/{userId}") + public ApiResponse> getMyReviewList( + @PathVariable("userId") Long userId, + @RequestParam("page") int page, + @RequestParam("size") int size) { + + return ApiResponse.onSuccess(reviewService.getMyReviewList(userId, page, size)); + } + + @Override + @PostMapping("/store/{storeId}/user/{userId}") + public ApiResponse addReview( + @PathVariable("storeId") Long storeId, + @PathVariable("userId") Long userId, + @RequestBody StoreAddReviewRequestDTO request) { + + reviewService.addReview(storeId, userId, request); + return ApiResponse.onSuccess(null); + } + +} diff --git a/src/main/java/com/umc/training/domain/review/controller/ReviewControllerDocs.java b/src/main/java/com/umc/training/domain/review/controller/ReviewControllerDocs.java new file mode 100644 index 0000000..a900bbd --- /dev/null +++ b/src/main/java/com/umc/training/domain/review/controller/ReviewControllerDocs.java @@ -0,0 +1,73 @@ +package com.umc.training.domain.review.controller; + +import com.umc.training.domain.review.dto.request.StoreAddReviewRequestDTO; +import com.umc.training.domain.review.dto.response.ReviewResponseDTO; +import com.umc.training.global.apiPayload.ApiResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; + +import java.util.List; + +@Tag(name = "리뷰", description = "리뷰 관련 API") +public interface ReviewControllerDocs { + + @Operation( + summary = "내 리뷰 조회 (필터링)", + description = "사용자 ID, 쿼리, 타입을 기반으로 리뷰를 필터링하여 조회합니다." + ) + @ApiResponses({ + @io.swagger.v3.oas.annotations.responses.ApiResponse( + responseCode = "200", + description = "조회 성공", + content = @Content(schema = @Schema(implementation = ApiResponse.class)) + ) + }) + ApiResponse> getMyReview( + @Parameter(description = "사용자 ID", required = true) Long userId, + @Parameter(description = "검색 쿼리", required = true) String query, + @Parameter(description = "필터 타입 (star 또는 store)", required = true) String type + ); + + @Operation( + summary = "내가 작성한 리뷰 목록 조회", + description = "페이징을 적용하여 사용자가 작성한 모든 리뷰를 조회합니다." + ) + @ApiResponses({ + @io.swagger.v3.oas.annotations.responses.ApiResponse( + responseCode = "200", + description = "조회 성공", + content = @Content(schema = @Schema(implementation = ApiResponse.class)) + ) + }) + ApiResponse> getMyReviewList( + @Parameter(description = "사용자 ID", required = true) Long userId, + @Parameter(description = "페이지 번호 (0부터 시작)", required = true) int page, + @Parameter(description = "페이지 크기", required = true) int size + ); + + @Operation( + summary = "가게에 리뷰 추가", + description = "특정 가게에 리뷰를 작성합니다." + ) + @ApiResponses({ + @io.swagger.v3.oas.annotations.responses.ApiResponse( + responseCode = "200", + description = "리뷰 작성 성공", + content = @Content(schema = @Schema(implementation = ApiResponse.class)) + ) + }) + ApiResponse addReview( + @Parameter(description = "가게 ID", required = true) Long storeId, + @Parameter(description = "사용자 ID", required = true) Long userId, + @io.swagger.v3.oas.annotations.parameters.RequestBody( + description = "리뷰 작성 요청", + required = true, + content = @Content(schema = @Schema(implementation = StoreAddReviewRequestDTO.class)) + ) StoreAddReviewRequestDTO request + ); +} + diff --git a/src/main/java/com/umc/training/domain/review/dto/request/StoreAddReviewRequestDTO.java b/src/main/java/com/umc/training/domain/review/dto/request/StoreAddReviewRequestDTO.java new file mode 100644 index 0000000..fffcbde --- /dev/null +++ b/src/main/java/com/umc/training/domain/review/dto/request/StoreAddReviewRequestDTO.java @@ -0,0 +1,7 @@ +package com.umc.training.domain.review.dto.request; + +public record StoreAddReviewRequestDTO( + String contents, + Float score) { + +} diff --git a/src/main/java/com/umc/training/domain/review/repository/ReviewRepositoryCustom.java b/src/main/java/com/umc/training/domain/review/repository/ReviewRepositoryCustom.java index 5e11d99..f93c43c 100644 --- a/src/main/java/com/umc/training/domain/review/repository/ReviewRepositoryCustom.java +++ b/src/main/java/com/umc/training/domain/review/repository/ReviewRepositoryCustom.java @@ -2,9 +2,13 @@ import com.umc.training.domain.review.entity.Review; +import org.springframework.data.domain.Pageable; + import java.util.List; public interface ReviewRepositoryCustom { List findByUserId(Long userId, String query, String type); + + List findAllByMemberId(Long memberId, Pageable pageable); } diff --git a/src/main/java/com/umc/training/domain/review/repository/ReviewRepositoryCustomImpl.java b/src/main/java/com/umc/training/domain/review/repository/ReviewRepositoryCustomImpl.java index 0c8660f..2fd4b5e 100644 --- a/src/main/java/com/umc/training/domain/review/repository/ReviewRepositoryCustomImpl.java +++ b/src/main/java/com/umc/training/domain/review/repository/ReviewRepositoryCustomImpl.java @@ -5,6 +5,7 @@ import com.umc.training.domain.review.entity.Review; import jakarta.persistence.EntityManager; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; import java.util.List; @@ -37,6 +38,16 @@ public List findByUserId(Long userId, String query, String type) { .fetch(); } - + public List findAllByMemberId(Long memberId, Pageable pageable) { + JPAQueryFactory queryFactory = new JPAQueryFactory(em); + + return queryFactory + .selectFrom(review) + .where(review.member.id.eq(memberId)) + .orderBy(review.id.desc()) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + } } diff --git a/src/main/java/com/umc/training/domain/review/service/ReviewService.java b/src/main/java/com/umc/training/domain/review/service/ReviewService.java new file mode 100644 index 0000000..2eaf433 --- /dev/null +++ b/src/main/java/com/umc/training/domain/review/service/ReviewService.java @@ -0,0 +1,83 @@ +package com.umc.training.domain.review.service; + +import com.umc.training.domain.member.entity.Member; +import com.umc.training.domain.member.entity.exception.code.MemberBaseCode; +import com.umc.training.domain.member.entity.exception.code.MemberException; +import com.umc.training.domain.member.entity.repository.MemberRepository; +import com.umc.training.domain.review.dto.request.StoreAddReviewRequestDTO; +import com.umc.training.domain.review.dto.response.ReviewResponseDTO; +import com.umc.training.domain.review.entity.Review; +import com.umc.training.domain.review.repository.ReviewRepository; +import com.umc.training.domain.store.entity.Store; +import com.umc.training.domain.store.exception.StoreException; +import com.umc.training.domain.store.exception.code.StoreErrorCode; +import com.umc.training.domain.store.repository.StoreRepository; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import org.springframework.data.domain.PageRequest; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Slf4j +@Service +@RequiredArgsConstructor +public class ReviewService { + + private final ReviewRepository reviewRepository; + private final MemberRepository memberRepository; + private final StoreRepository storeRepository; + + @Transactional(readOnly = true) + public List getMyReview(Long userId, String query, String type) { + + log.info("memberCount : {}", memberRepository.countById(userId)); + + if (memberRepository.countById(userId) == 0) { + throw new MemberException(MemberBaseCode.MEMBER_NOT_FOUND); + } + + List reviewList = reviewRepository.findByUserId(userId, query, type); + + return reviewList.stream() + .map(ReviewResponseDTO::new) + .toList(); + + } + + @Transactional(readOnly = true) + public List getMyReviewList(Long userId, int page, int size) { + if (!memberRepository.existsById(userId)) { + throw new MemberException(MemberBaseCode.MEMBER_NOT_FOUND); + } + + List reviewList = reviewRepository.findAllByMemberId(userId, PageRequest.of(page, size)); + + return reviewList.stream() + .map(ReviewResponseDTO::new) + .toList(); + } + + @Transactional + public void addReview(Long storeId, Long userId, StoreAddReviewRequestDTO request) { + + Member member = memberRepository.findById(userId) + .orElseThrow(() -> new MemberException(MemberBaseCode.MEMBER_NOT_FOUND)); + + Store store = storeRepository.findById(storeId) + .orElseThrow(() -> new StoreException(StoreErrorCode.STORE_NOT_FOUND)); + + Review review = Review.builder() + .member(member) + .store(store) + .contents(request.contents()) + .score(request.score()) + .build(); + + reviewRepository.save(review); + } + +} diff --git a/src/main/java/com/umc/training/domain/store/controller/StoreController.java b/src/main/java/com/umc/training/domain/store/controller/StoreController.java new file mode 100644 index 0000000..30eb438 --- /dev/null +++ b/src/main/java/com/umc/training/domain/store/controller/StoreController.java @@ -0,0 +1,29 @@ +package com.umc.training.domain.store.controller; + +import com.umc.training.domain.store.dto.request.StoreAddRequestDTO; +import com.umc.training.domain.store.service.StoreService; +import com.umc.training.global.apiPayload.ApiResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/stores") +public class StoreController implements StoreControllerDocs { + + private final StoreService storeService; + + @Override + @PostMapping("/region/{regionId}") + public ApiResponse addStore( + @PathVariable("regionId") Long regionId, + @RequestBody StoreAddRequestDTO request) { + + storeService.addStore(regionId, request); + return ApiResponse.onSuccess(null); + } +} diff --git a/src/main/java/com/umc/training/domain/store/controller/StoreControllerDocs.java b/src/main/java/com/umc/training/domain/store/controller/StoreControllerDocs.java new file mode 100644 index 0000000..94acc72 --- /dev/null +++ b/src/main/java/com/umc/training/domain/store/controller/StoreControllerDocs.java @@ -0,0 +1,35 @@ +package com.umc.training.domain.store.controller; + +import com.umc.training.domain.store.dto.request.StoreAddRequestDTO; +import com.umc.training.global.apiPayload.ApiResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; + +@Tag(name = "가게", description = "가게 관련 API") +public interface StoreControllerDocs { + + @Operation( + summary = "특정 지역에 가게 추가", + description = "지역 ID를 받아 해당 지역에 새로운 가게를 등록합니다." + ) + @ApiResponses({ + @io.swagger.v3.oas.annotations.responses.ApiResponse( + responseCode = "200", + description = "가게 등록 성공", + content = @Content(schema = @Schema(implementation = ApiResponse.class)) + ) + }) + ApiResponse addStore( + @Parameter(description = "지역 ID", required = true) Long regionId, + @io.swagger.v3.oas.annotations.parameters.RequestBody( + description = "가게 등록 요청", + required = true, + content = @Content(schema = @Schema(implementation = StoreAddRequestDTO.class)) + ) StoreAddRequestDTO request + ); +} + diff --git a/src/main/java/com/umc/training/domain/store/dto/request/StoreAddRequestDTO.java b/src/main/java/com/umc/training/domain/store/dto/request/StoreAddRequestDTO.java new file mode 100644 index 0000000..4fcbd57 --- /dev/null +++ b/src/main/java/com/umc/training/domain/store/dto/request/StoreAddRequestDTO.java @@ -0,0 +1,6 @@ +package com.umc.training.domain.store.dto.request; + +public record StoreAddRequestDTO( + String name, + String address) { +} diff --git a/src/main/java/com/umc/training/domain/store/entity/Store.java b/src/main/java/com/umc/training/domain/store/entity/Store.java index 60745ce..e545969 100644 --- a/src/main/java/com/umc/training/domain/store/entity/Store.java +++ b/src/main/java/com/umc/training/domain/store/entity/Store.java @@ -29,7 +29,8 @@ public class Store extends BaseEntity { private String address; @Column(name = "score", nullable = false) - private Float score; + @Builder.Default + private Float score = 0.0f; @OneToMany(mappedBy = "store", cascade = CascadeType.ALL) private List missionList = new ArrayList<>(); diff --git a/src/main/java/com/umc/training/domain/store/exception/StoreException.java b/src/main/java/com/umc/training/domain/store/exception/StoreException.java new file mode 100644 index 0000000..be5b760 --- /dev/null +++ b/src/main/java/com/umc/training/domain/store/exception/StoreException.java @@ -0,0 +1,12 @@ +package com.umc.training.domain.store.exception; + +import com.umc.training.domain.store.exception.code.StoreErrorCode; +import com.umc.training.global.apiPayload.exception.GeneralException; + +public class StoreException extends GeneralException { + + public StoreException(StoreErrorCode code) { + super(code); + } + +} diff --git a/src/main/java/com/umc/training/domain/store/exception/code/StoreErrorCode.java b/src/main/java/com/umc/training/domain/store/exception/code/StoreErrorCode.java new file mode 100644 index 0000000..3774c25 --- /dev/null +++ b/src/main/java/com/umc/training/domain/store/exception/code/StoreErrorCode.java @@ -0,0 +1,19 @@ +package com.umc.training.domain.store.exception.code; + +import com.umc.training.global.apiPayload.code.BaseErrorCode; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor +public enum StoreErrorCode implements BaseErrorCode { + + STORE_NOT_FOUND(HttpStatus.NOT_FOUND, "STORE_NOT_FOUND_4001", "Store not found."); + + private final HttpStatus status; + private final String message; + private final String code; + +} diff --git a/src/main/java/com/umc/training/domain/store/repository/StoreRepository.java b/src/main/java/com/umc/training/domain/store/repository/StoreRepository.java new file mode 100644 index 0000000..1bc1874 --- /dev/null +++ b/src/main/java/com/umc/training/domain/store/repository/StoreRepository.java @@ -0,0 +1,17 @@ +package com.umc.training.domain.store.repository; + +import org.springframework.data.repository.Repository; + +import com.umc.training.domain.store.entity.Store; + +import java.util.Optional; + +public interface StoreRepository extends Repository { + + boolean existsById(Long id); + + Optional findById(Long id); + + Store save(Store store); + +} diff --git a/src/main/java/com/umc/training/domain/store/service/StoreService.java b/src/main/java/com/umc/training/domain/store/service/StoreService.java new file mode 100644 index 0000000..a06d4f5 --- /dev/null +++ b/src/main/java/com/umc/training/domain/store/service/StoreService.java @@ -0,0 +1,42 @@ +package com.umc.training.domain.store.service; + +import com.umc.training.domain.region.entity.Region; +import com.umc.training.domain.region.exception.RegionException; +import com.umc.training.domain.region.exception.code.RegionErrorCode; +import com.umc.training.domain.region.repository.RegionRepository; +import com.umc.training.domain.store.dto.request.StoreAddRequestDTO; +import com.umc.training.domain.store.entity.Store; +import com.umc.training.domain.store.repository.StoreRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@Service +@RequiredArgsConstructor +@Transactional +public class StoreService { + + private final StoreRepository storeRepository; + private final RegionRepository regionRepository; + + public void addStore(Long regionId, StoreAddRequestDTO request) { + + if (!regionRepository.existsById(regionId)) { + throw new RegionException(RegionErrorCode.REGION_NOT_FOUND); + } + + Region region = regionRepository.findById(regionId) + .orElseThrow(() -> new RegionException(RegionErrorCode.REGION_NOT_FOUND)); + + // 가게 생성 및 저장 + Store store = Store.builder() + .region(region) + .name(request.name()) + .address(request.address()) + .build(); + + storeRepository.save(store); + } +} diff --git a/src/main/java/com/umc/training/global/annotation/PageNumber.java b/src/main/java/com/umc/training/global/annotation/PageNumber.java new file mode 100644 index 0000000..64d4317 --- /dev/null +++ b/src/main/java/com/umc/training/global/annotation/PageNumber.java @@ -0,0 +1,19 @@ +package com.umc.training.global.annotation; + +import com.umc.training.global.validator.PageNumberValidator; +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +import java.lang.annotation.*; + +@Documented +@Constraint(validatedBy = PageNumberValidator.class) +@Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER }) +@Retention(RetentionPolicy.RUNTIME) +public @interface PageNumber { + String message() default "페이지 번호는 0 이상이어야 합니다."; + + Class[] groups() default {}; + + Class[] payload() default {}; +} diff --git a/src/main/java/com/umc/training/global/validator/PageNumberValidator.java b/src/main/java/com/umc/training/global/validator/PageNumberValidator.java new file mode 100644 index 0000000..4dccf48 --- /dev/null +++ b/src/main/java/com/umc/training/global/validator/PageNumberValidator.java @@ -0,0 +1,28 @@ +package com.umc.training.global.validator; + +import com.umc.training.global.annotation.PageNumber; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import org.springframework.stereotype.Component; + +@Component +public class PageNumberValidator implements ConstraintValidator { + + @Override + public void initialize(PageNumber constraintAnnotation) { + } + + @Override + public boolean isValid(Integer value, ConstraintValidatorContext context) { + + boolean isValid = value >= 0; + + if (!isValid) { + context.disableDefaultConstraintViolation(); + context.buildConstraintViolationWithTemplate("페이지 번호는 0 이상이어야 합니다.") + .addConstraintViolation(); + } + + return isValid; + } +}