diff --git a/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/inquiry/controller/InquiryController.java b/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/inquiry/controller/InquiryController.java index abfbccc..65e2c3b 100644 --- a/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/inquiry/controller/InquiryController.java +++ b/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/inquiry/controller/InquiryController.java @@ -7,11 +7,14 @@ import com.umc.jaengchalttak.global.apiPayload.ApiResponse; import com.umc.jaengchalttak.global.apiPayload.code.BaseSuccessCode; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; import org.springframework.web.bind.annotation.*; import java.time.LocalDateTime; import java.util.List; +@Tag(name = "1:1 문의 API", description = "1:1 문의 관련 API입니다.") @RestController @RequestMapping("/api/inquiry") public class InquiryController { @@ -56,7 +59,7 @@ public ApiResponse getInquiryInfo(@PathVariable Long inquiryI @Operation(summary = "1대1 문의 제출", description = "유저가 새로운 1대1 문의를 작성합니다.") @PostMapping - public ApiResponse submitInquiry(@RequestBody SubmitInquiryReqDTO request) { + public ApiResponse submitInquiry(@Valid @RequestBody SubmitInquiryReqDTO request) { BaseSuccessCode code = InquirySuccessCode.SUBMIT_INQUIRY_CREATED; return ApiResponse.onSuccess(code, "1대1 문의 제출 성공!"); } diff --git a/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/inquiry/dto/request/SubmitInquiryReqDTO.java b/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/inquiry/dto/request/SubmitInquiryReqDTO.java index 53aa091..7570e2f 100644 --- a/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/inquiry/dto/request/SubmitInquiryReqDTO.java +++ b/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/inquiry/dto/request/SubmitInquiryReqDTO.java @@ -1,11 +1,22 @@ package com.umc.jaengchalttak.domain.inquiry.dto.request; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; import java.util.List; public record SubmitInquiryReqDTO( + @NotBlank(message = "문의 제목은 필수입니다.") + @Size(max = 50, message = "문의 제목은 50자 이내여야 합니다.") String inquiryTitle, + + @NotBlank(message = "문의 내용은 필수입니다.") + @Size(max = 500, message = "문의 내용은 500자 이내여야 합니다.") String inquiryContent, + + @NotBlank(message = "문의 타입은 필수입니다.") + @Size(max = 20, message = "문의 타입은 20자 이내여야 합니다.") String inquiryType, + List photo ) { diff --git a/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/inquiry/entity/Inquiry.java b/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/inquiry/entity/Inquiry.java index 27ac517..bc39adc 100644 --- a/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/inquiry/entity/Inquiry.java +++ b/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/inquiry/entity/Inquiry.java @@ -46,6 +46,7 @@ public class Inquiry { // ====== 연관관계 매핑 ====== @OneToMany(mappedBy = "inquiry", cascade = CascadeType.ALL, orphanRemoval = true) + @Builder.Default private List inquiryPhotos = new ArrayList<>(); } \ No newline at end of file diff --git a/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/mission/controller/MissionController.java b/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/mission/controller/MissionController.java index a8bce4b..b209b34 100644 --- a/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/mission/controller/MissionController.java +++ b/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/mission/controller/MissionController.java @@ -5,19 +5,19 @@ import com.umc.jaengchalttak.domain.mission.dto.response.MissionsProgressResDTO; import com.umc.jaengchalttak.domain.mission.dto.response.MyMissionResDTO; import com.umc.jaengchalttak.domain.mission.payload.code.MissionSuccessCode; -import com.umc.jaengchalttak.domain.mission.service.MissionService; import com.umc.jaengchalttak.domain.mission.service.UserMissionService; import com.umc.jaengchalttak.domain.user.enums.Address; import com.umc.jaengchalttak.global.apiPayload.ApiResponse; import com.umc.jaengchalttak.global.apiPayload.code.BaseSuccessCode; +import com.umc.jaengchalttak.global.dto.OffsetPagination; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; import jakarta.validation.constraints.Min; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; -import java.time.LocalDateTime; -import java.util.List; - +@Tag(name = "미션 API", description = "미션 관련 API입니다.") @RestController @RequiredArgsConstructor @RequestMapping("/api/mission") @@ -43,11 +43,23 @@ public ApiResponse getMyMissionList(@RequestParam("userId") Lon @Operation(summary = "진행 상태별 내 미션 조회", description = "유저가 진행 중이거나 이미 완료한 미션 목록을 필터링하여 조회합니다.") @GetMapping("/me") - public ApiResponse> getMyMissionsByProgress(@RequestParam("userId") Long userId, - @RequestParam("isProgress") boolean isProgress, - @Min(value = 1, message = "페이지 번호는 1 이상이어야 합니다.") - @RequestParam(value = "page", defaultValue = "1") int page) { - List result = userMissionService.getUserMissionsByProgress(userId, isProgress, page); + public ApiResponse> getMyMissionsByProgress( + @RequestParam Long userId, + @RequestParam boolean isProgress, + + @RequestParam(defaultValue = "3") + @Min(value = 1, message = "페이지 크기는 1 이상이어야 합니다.") + Integer pageSize, + + @RequestParam(defaultValue = "0") + @Min(value = 0, message = "페이지 번호는 0 이상이어야 합니다.") + Integer pageNumber, + + @RequestParam(required = false) + String sort + ) { + OffsetPagination result = + userMissionService.getUserMissionsByProgress(userId, isProgress, pageSize, pageNumber, sort); BaseSuccessCode code = MissionSuccessCode.MISSION_BY_PROGRESS_OK; return ApiResponse.onSuccess(code, result); @@ -56,7 +68,7 @@ public ApiResponse> getMyMissionsByProgress(@Reques @Operation(summary = "미션 도전하기", description = "특정 미션을 수행 목록에 추가하고 진행 상태로 변경합니다.") @PostMapping - public ApiResponse startMission(@RequestBody StartMissionReqDTO request) { + public ApiResponse startMission(@Valid @RequestBody StartMissionReqDTO request) { BaseSuccessCode code = MissionSuccessCode.START_MISSION_CREATED; return ApiResponse.onSuccess(code, "미션 진행 시작!"); } @@ -64,7 +76,7 @@ public ApiResponse startMission(@RequestBody StartMissionReqDTO request) @Operation(summary = "미션 완료 인증", description = "진행 중인 미션에 대해 완료 증빙을 제출하고 성공 처리를 요청합니다.") @PostMapping("/success") - public ApiResponse completeMission(@RequestBody CompleteMissionReqDTO request) { + public ApiResponse completeMission(@Valid @RequestBody CompleteMissionReqDTO request) { BaseSuccessCode code = MissionSuccessCode.COMPLETE_MISSION_CREATED; return ApiResponse.onSuccess(code, "미션 수행 완료!"); } diff --git a/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/mission/converter/MissionConverter.java b/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/mission/converter/MissionConverter.java index 8512460..cb3a3e3 100644 --- a/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/mission/converter/MissionConverter.java +++ b/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/mission/converter/MissionConverter.java @@ -23,6 +23,7 @@ public static MissionsProgressResDTO toMissionsProgressResDTO(UserMission userMi return MissionsProgressResDTO.builder() .storeId(userMission.getMission().getStore().getId()) .storeName(userMission.getMission().getStore().getStoreName()) + .userMissionId(userMission.getId()) .missionName(userMission.getMission().getMissionName()) .missionPoint(userMission.getMission().getMissionPoint()) .missionAmount(userMission.getMission().getMissionAmount()) diff --git a/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/mission/dto/request/CompleteMissionReqDTO.java b/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/mission/dto/request/CompleteMissionReqDTO.java index e0d14a8..0541d3e 100644 --- a/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/mission/dto/request/CompleteMissionReqDTO.java +++ b/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/mission/dto/request/CompleteMissionReqDTO.java @@ -1,7 +1,14 @@ package com.umc.jaengchalttak.domain.mission.dto.request; +import jakarta.validation.constraints.NotNull; + public record CompleteMissionReqDTO( + @NotNull(message = "사용자 ID는 필수입니다.") Long userId, + + @NotNull(message = "미션 ID는 필수입니다.") Long missionId, + + @NotNull(message = "가게 ID는 필수입니다.") Long storeId ) { } diff --git a/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/mission/dto/request/StartMissionReqDTO.java b/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/mission/dto/request/StartMissionReqDTO.java index 27653ee..f1c9b67 100644 --- a/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/mission/dto/request/StartMissionReqDTO.java +++ b/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/mission/dto/request/StartMissionReqDTO.java @@ -1,6 +1,11 @@ package com.umc.jaengchalttak.domain.mission.dto.request; +import jakarta.validation.constraints.NotNull; + public record StartMissionReqDTO( + @NotNull(message = "사용자 ID는 필수입니다.") Long userId, + + @NotNull(message = "미션 ID는 필수입니다.") Long missionId ) { } diff --git a/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/mission/dto/response/MissionsProgressResDTO.java b/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/mission/dto/response/MissionsProgressResDTO.java index 34d4a21..5a38696 100644 --- a/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/mission/dto/response/MissionsProgressResDTO.java +++ b/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/mission/dto/response/MissionsProgressResDTO.java @@ -6,6 +6,7 @@ public record MissionsProgressResDTO( Long storeId, String storeName, + Long userMissionId, String missionName, Integer missionPoint, Integer missionAmount, diff --git a/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/mission/entity/Mission.java b/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/mission/entity/Mission.java index eacaf8a..bd3327e 100644 --- a/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/mission/entity/Mission.java +++ b/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/mission/entity/Mission.java @@ -2,6 +2,7 @@ import com.umc.jaengchalttak.domain.store.entity.Store; import jakarta.persistence.*; +import jakarta.validation.constraints.Min; import lombok.*; import java.time.LocalDate; import java.util.ArrayList; @@ -23,9 +24,11 @@ public class Mission { @Column(nullable = false, length = 50) private String missionName; + @Min(value = 1, message = "미션 포인트는 1점 이상이어야 합니다.") @Column(nullable = false) private Integer missionPoint; + @Min(value = 1, message = "미션 금액은 1원 이상이어야 합니다.") @Column(nullable = false) private Integer missionAmount; @@ -41,4 +44,6 @@ public class Mission { @OneToMany(mappedBy = "mission", cascade = CascadeType.REMOVE) private List userMissionList = new ArrayList<>(); + + } \ No newline at end of file diff --git a/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/mission/entity/UserMission.java b/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/mission/entity/UserMission.java index 30fac3c..3963138 100644 --- a/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/mission/entity/UserMission.java +++ b/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/mission/entity/UserMission.java @@ -18,20 +18,23 @@ indexes = { @Index(name = "idx_user_mission_user_progress", columnList = "user_id, is_progress") } -)@Getter +) +@Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor @Builder -@IdClass(UserMissionId.class) // 복합 키 매핑 @EntityListeners(AuditingEntityListener.class) public class UserMission { @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "user_mission_id") + private Long id; + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "user_id", nullable = false) private User user; - @Id @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "mission_id", nullable = false) private Mission mission; diff --git a/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/mission/entity/UserMissionId.java b/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/mission/entity/UserMissionId.java deleted file mode 100644 index 1331789..0000000 --- a/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/mission/entity/UserMissionId.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.umc.jaengchalttak.domain.mission.entity; - -import lombok.*; -import java.io.Serializable; - -@Getter -@NoArgsConstructor -@AllArgsConstructor -@EqualsAndHashCode -public class UserMissionId implements Serializable { - private Long user; // User 엔티티의 @Id 필드명과 일치 - private Long mission; // Mission 엔티티의 @Id 필드명과 일치 -} diff --git a/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/mission/repository/MissionRepository.java b/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/mission/repository/MissionRepository.java index 3101f0f..4c3148e 100644 --- a/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/mission/repository/MissionRepository.java +++ b/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/mission/repository/MissionRepository.java @@ -1,9 +1,14 @@ package com.umc.jaengchalttak.domain.mission.repository; import com.umc.jaengchalttak.domain.mission.entity.Mission; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; @Repository public interface MissionRepository extends JpaRepository { + Page findAllByStoreId(Long storeId, Pageable pageable); } diff --git a/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/mission/repository/UserMissionRepository.java b/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/mission/repository/UserMissionRepository.java index 85f1c01..546961e 100644 --- a/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/mission/repository/UserMissionRepository.java +++ b/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/mission/repository/UserMissionRepository.java @@ -1,10 +1,10 @@ package com.umc.jaengchalttak.domain.mission.repository; import com.umc.jaengchalttak.domain.mission.entity.UserMission; -import com.umc.jaengchalttak.domain.mission.entity.UserMissionId; import com.umc.jaengchalttak.domain.mission.enums.ProgressStatus; import com.umc.jaengchalttak.domain.user.enums.Address; 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; @@ -12,7 +12,7 @@ import org.springframework.stereotype.Repository; @Repository -public interface UserMissionRepository extends JpaRepository { +public interface UserMissionRepository extends JpaRepository { // 특정 사용자의 상태별(진행 중/완료 등) 미션 내역을 페이징하여 조회 // 사용자 미션, 미션, 가게를 가져옴 diff --git a/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/mission/service/UserMissionService.java b/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/mission/service/UserMissionService.java index dcd2133..4b79b4f 100644 --- a/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/mission/service/UserMissionService.java +++ b/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/mission/service/UserMissionService.java @@ -11,6 +11,8 @@ import com.umc.jaengchalttak.domain.user.payload.UserException; import com.umc.jaengchalttak.domain.user.payload.code.UserErrorCode; import com.umc.jaengchalttak.domain.user.repository.UserRepository; +import com.umc.jaengchalttak.global.converter.GlobalConverter; +import com.umc.jaengchalttak.global.dto.OffsetPagination; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; @@ -19,8 +21,6 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.List; - @Service @RequiredArgsConstructor public class UserMissionService { @@ -30,10 +30,13 @@ public class UserMissionService { private static final int PAGE_SIZE = 3; - // 사용자 지역에서 현재 도전할 수 있는 새로운 미션 목록을 조회 (홈 화면) @Transactional(readOnly = true) - public MyMissionResDTO getAvailableMissionsByAddress(Long userId, Address address, int page) { + public MyMissionResDTO getAvailableMissionsByAddress( + Long userId, + Address address, + int page + ) { User user = userRepository.findByIdWithServiceUseAllow(userId) .orElseThrow(() -> new UserException(UserErrorCode.USER_NOT_FOUND)); @@ -58,30 +61,32 @@ public MyMissionResDTO getAvailableMissionsByAddress(Long userId, Address addres // 진행 중/진행 완료 상태인 내 미션 조회 @Transactional(readOnly = true) - public List getUserMissionsByProgress (Long userId, boolean isProgress, int page) { - // userId 존재하는지 확인 - if (!userRepository.existsById(userId)) { - throw new UserException(UserErrorCode.USER_NOT_FOUND); - } - + public OffsetPagination getUserMissionsByProgress ( + Long userId, + boolean isProgress, + Integer pageSize, + Integer pageNumber, + String sort + ) { // true면 진행 완료, false면 진행 중 ProgressStatus status = isProgress ? ProgressStatus.COMPLETED : ProgressStatus.IN_PROGRESS; - // mission.id 기준으로 내림차순 정렬 - Pageable pageable = PageRequest.of( - page - 1, - PAGE_SIZE, - Sort.by(Sort.Direction.DESC, "mission.id") - ); + Sort sortInfo = (sort != null && !sort.isBlank()) ? + Sort.by(sort) : + Sort.by("id").descending(); + + PageRequest pageRequest = PageRequest.of(pageNumber, pageSize, sortInfo); // JPQL로 조회 Page userMissionList = userMissionRepository. - findByUserAndProgressWithMissionAndStore(userId, status, pageable); + findByUserAndProgressWithMissionAndStore(userId, status, pageRequest); - // converter가 엔티티 리스트를 DTO로 변환 - return userMissionList.stream() - .map(MissionConverter::toMissionsProgressResDTO) - .toList(); + + return GlobalConverter.toOffsetPagination( + userMissionList.map(MissionConverter::toMissionsProgressResDTO).toList(), + userMissionList.getNumber(), + userMissionList.getSize() + ); } diff --git a/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/store/controller/ReviewController.java b/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/store/controller/ReviewController.java index 9fcdac4..f6aa41f 100644 --- a/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/store/controller/ReviewController.java +++ b/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/store/controller/ReviewController.java @@ -3,18 +3,24 @@ import com.umc.jaengchalttak.domain.store.dto.request.CommentReqDTO; import com.umc.jaengchalttak.domain.store.dto.request.StoreReviewReqDTO; import com.umc.jaengchalttak.domain.store.dto.response.StoreReviewListResDTO; +import com.umc.jaengchalttak.domain.store.enums.QueryType; import com.umc.jaengchalttak.domain.store.payload.code.StoreSuccessCode; import com.umc.jaengchalttak.domain.store.service.ReviewService; import com.umc.jaengchalttak.global.apiPayload.ApiResponse; import com.umc.jaengchalttak.global.apiPayload.code.BaseSuccessCode; +import com.umc.jaengchalttak.global.dto.CursorPagination; +import com.umc.jaengchalttak.global.dto.OffsetPagination; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; +import jakarta.validation.constraints.Min; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; import java.time.LocalDateTime; import java.util.List; +@Tag(name = "가게 리뷰 API", description = "가게 리뷰 및 사장님 답글 관련 API입니다.") @RestController @RequiredArgsConstructor @RequestMapping("/api/store/review") @@ -31,10 +37,10 @@ public ApiResponse> getStoreReviewList(@RequestParam StoreReviewListResDTO.builder() .userId(1L) .userName("홍길동") - .reviewStar(5) + .reviewId(101L) + .reviewStar(5.0) .reviewContent("커피가 정말 맛있어요!") .reviewCreatedAt(LocalDateTime.now()) - .reviewSavePath(List.of("경로1", "경로2")) .commentId(101L) .commentContent("감사합니다 😊") .commentCreateAt(LocalDateTime.now()) @@ -46,6 +52,33 @@ public ApiResponse> getStoreReviewList(@RequestParam } + @Operation( + summary = "내 리뷰 목록 보기", + description = "특정 가게에 작성된 리뷰와 사장님 답글 목록 중 내 리뷰를 페이징하여 조회합니다.") + @GetMapping("/me") + public ApiResponse> getMyStoreReview( + @RequestParam Long userId, + @RequestParam Long storeId, + + @RequestParam(defaultValue = "3") + @Min(value = 1, message = "페이지 크기는 1 이상이어야 합니다.") + Integer pageSize, + + @RequestParam(required = false) + String cursor, + + @RequestParam + QueryType query + ) { + CursorPagination result = + reviewService.getMyReviewList(userId, storeId, pageSize, cursor, query); + + BaseSuccessCode code = StoreSuccessCode.MY_REVIEW_LIST_OK; + return ApiResponse.onSuccess(code, result); + } + + + @Operation(summary = "가게 리뷰 작성", description = "유저가 방문한 가게에 대해 별점과 사진을 포함한 리뷰를 작성합니다.") @PostMapping public ApiResponse writerReview(@Valid @RequestBody StoreReviewReqDTO request) { @@ -58,7 +91,7 @@ public ApiResponse writerReview(@Valid @RequestBody StoreReviewReqDTO re @Operation(summary = "사장님 댓글 작성", description = "가게 주인이 유저의 리뷰에 대해 답글(댓글)을 작성합니다.") @PostMapping("/comment") - public ApiResponse writerComment(@RequestBody CommentReqDTO request) { + public ApiResponse writerComment(@Valid @RequestBody CommentReqDTO request) { BaseSuccessCode code = StoreSuccessCode.COMMENT_CREATED; return ApiResponse.onSuccess(code, "댓글 작성 완료!"); } diff --git a/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/store/controller/StoreController.java b/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/store/controller/StoreController.java index 69ae3e5..132d54b 100644 --- a/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/store/controller/StoreController.java +++ b/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/store/controller/StoreController.java @@ -7,12 +7,14 @@ import com.umc.jaengchalttak.global.apiPayload.ApiResponse; import com.umc.jaengchalttak.global.apiPayload.code.BaseSuccessCode; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.web.bind.annotation.*; import java.util.List; import static com.umc.jaengchalttak.domain.user.enums.Address.TEHERAN_RO; +@Tag(name = "가게 API", description = "가게 관련 API입니다.") @RestController @RequestMapping("/api/store") public class StoreController { diff --git a/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/store/controller/StoreMissionController.java b/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/store/controller/StoreMissionController.java new file mode 100644 index 0000000..fff1350 --- /dev/null +++ b/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/store/controller/StoreMissionController.java @@ -0,0 +1,45 @@ +package com.umc.jaengchalttak.domain.store.controller; + +import com.umc.jaengchalttak.domain.store.dto.request.CreateStoreMissionReqDTO; +import com.umc.jaengchalttak.domain.store.dto.response.GetStoreMissionResDTO; +import com.umc.jaengchalttak.domain.store.payload.code.StoreSuccessCode; +import com.umc.jaengchalttak.domain.store.service.StoreMissionService; +import com.umc.jaengchalttak.global.apiPayload.ApiResponse; +import com.umc.jaengchalttak.global.apiPayload.code.BaseSuccessCode; +import com.umc.jaengchalttak.global.dto.OffsetPagination; +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.web.bind.annotation.*; + +@Tag(name = "가게 미션 API", description = "가게 미션 관련 API입니다.") +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/store/mission") +public class StoreMissionController { + + private final StoreMissionService storeMissionService; + + @Operation(summary = "가게 미션 생성", description = "가게 ID를 통해 해당 가게의 미션을 생성합니다.") + @PostMapping("/{storeId}") + public ApiResponse createStoreMission(@PathVariable Long storeId, + @Valid @RequestBody CreateStoreMissionReqDTO request) { + storeMissionService.createMission(storeId, request); + BaseSuccessCode code = StoreSuccessCode.STORE_MISSION_CREATED_OK; + return ApiResponse.onSuccess(code, null); + } + + @Operation(summary = "가게 미션 조회", description = "가게 내의 미션들을 페이징하여 조회합니다.") + @GetMapping + public ApiResponse> getStoreMissions(@RequestParam Long storeId, + @RequestParam Integer pageSize, + @RequestParam Integer pageNumber, + @RequestParam(required = false) String sort) { + OffsetPagination result = + storeMissionService.getMissions(storeId, pageSize, pageNumber, sort); + BaseSuccessCode code = StoreSuccessCode.STORE_MISSION_OK; + return ApiResponse.onSuccess(code, result); + } + +} diff --git a/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/store/converter/StoreMissionConverter.java b/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/store/converter/StoreMissionConverter.java new file mode 100644 index 0000000..4933027 --- /dev/null +++ b/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/store/converter/StoreMissionConverter.java @@ -0,0 +1,35 @@ +package com.umc.jaengchalttak.domain.store.converter; + +import com.umc.jaengchalttak.domain.mission.entity.Mission; +import com.umc.jaengchalttak.domain.store.dto.request.CreateStoreMissionReqDTO; +import com.umc.jaengchalttak.domain.store.dto.response.GetStoreMissionResDTO; +import com.umc.jaengchalttak.domain.store.entity.Store; +import com.umc.jaengchalttak.global.apiPayload.code.GeneralErrorCode; +import com.umc.jaengchalttak.global.apiPayload.exception.ProjectException; + +public class StoreMissionConverter { + + // 객체 생성하면 예외 + private StoreMissionConverter() { + throw new ProjectException(GeneralErrorCode.UTILITY_CLASS_INSTANTIATION); + } + + public static Mission toMission(Store store, CreateStoreMissionReqDTO request) { + return Mission.builder() + .missionName(request.missionName()) + .missionPoint(request.missionPoint()) + .missionAmount(request.missionAmount()) + .missionDate(request.missionDate()) + .store(store) + .build(); + } + + public static GetStoreMissionResDTO toGetStoreMissionResDTO(Mission mission) { + return GetStoreMissionResDTO.builder() + .missionId(mission.getId()) + .missionAmount(mission.getMissionAmount()) + .missionPoint(mission.getMissionPoint()) + .build(); + } + +} diff --git a/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/store/converter/StoreReviewConverter.java b/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/store/converter/StoreReviewConverter.java index e781118..54dd400 100644 --- a/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/store/converter/StoreReviewConverter.java +++ b/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/store/converter/StoreReviewConverter.java @@ -1,6 +1,8 @@ package com.umc.jaengchalttak.domain.store.converter; import com.umc.jaengchalttak.domain.store.dto.request.StoreReviewReqDTO; +import com.umc.jaengchalttak.domain.store.dto.response.StoreReviewListResDTO; +import com.umc.jaengchalttak.domain.store.entity.OwnerComment; import com.umc.jaengchalttak.domain.store.entity.Store; import com.umc.jaengchalttak.domain.store.entity.StoreReview; import com.umc.jaengchalttak.domain.user.entity.User; @@ -14,7 +16,23 @@ private StoreReviewConverter() { throw new ProjectException(GeneralErrorCode.UTILITY_CLASS_INSTANTIATION); } - public static StoreReview toEntity(StoreReviewReqDTO request, User user, Store store) { + public static StoreReviewListResDTO toStoreReviewListResDTO(StoreReview storeReview) { + OwnerComment ownerComment = storeReview.getOwnerComment(); + + return StoreReviewListResDTO.builder() + .userId(storeReview.getUser().getId()) + .userName(storeReview.getUser().getName()) + .reviewId(storeReview.getId()) + .reviewStar(storeReview.getReviewStar()) + .reviewContent(storeReview.getReviewContent()) + .reviewCreatedAt(storeReview.getCreatedAt()) + .commentId(ownerComment != null ? ownerComment.getId() : null) + .commentContent(ownerComment != null ? ownerComment.getCommentContent() : null) + .commentCreateAt(ownerComment != null ? ownerComment.getCreatedAt() : null) + .build(); + } + + public static StoreReview toStoreReview(StoreReviewReqDTO request, User user, Store store) { return StoreReview.builder() .user(user) .store(store) diff --git a/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/store/dto/request/CommentReqDTO.java b/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/store/dto/request/CommentReqDTO.java index 0c07a39..45956d2 100644 --- a/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/store/dto/request/CommentReqDTO.java +++ b/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/store/dto/request/CommentReqDTO.java @@ -1,6 +1,14 @@ package com.umc.jaengchalttak.domain.store.dto.request; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; + public record CommentReqDTO( + @NotNull(message = "리뷰 ID는 필수입니다.") Long reviewId, + + @NotBlank(message = "댓글 내용은 필수입니다.") + @Size(max = 500, message = "댓글 내용은 500자 이내여야 합니다.") String commentContent ) { } diff --git a/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/store/dto/request/CreateStoreMissionReqDTO.java b/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/store/dto/request/CreateStoreMissionReqDTO.java new file mode 100644 index 0000000..489a6f3 --- /dev/null +++ b/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/store/dto/request/CreateStoreMissionReqDTO.java @@ -0,0 +1,23 @@ +package com.umc.jaengchalttak.domain.store.dto.request; + +import jakarta.validation.constraints.*; + +import java.time.LocalDate; + +public record CreateStoreMissionReqDTO( + @NotBlank(message = "미션 이름은 반드시 존재해야 합니다.") // 문자열은 NotBlank + @Size(max = 50, message = "미션 이름은 50자를 넘기면 안됩니다.") + String missionName, + + @NotNull(message = "미션 날짜는 필수입니다.") + @FutureOrPresent(message = "미션 날짜는 과거일 수 없습니다.") + LocalDate missionDate, + + @NotNull(message = "포인트는 필수입니다.") + @Min(value = 1, message = "포인트는 1점 이상이어야 합니다.") + Integer missionPoint, + + @NotNull(message = "미션 금액은 필수입니다.") + @Min(value = 1, message = "미션 금액은 1원 이상이어야 합니다.") + Integer missionAmount +) { } \ No newline at end of file diff --git a/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/store/dto/response/GetStoreMissionResDTO.java b/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/store/dto/response/GetStoreMissionResDTO.java new file mode 100644 index 0000000..cb055ae --- /dev/null +++ b/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/store/dto/response/GetStoreMissionResDTO.java @@ -0,0 +1,10 @@ +package com.umc.jaengchalttak.domain.store.dto.response; + +import lombok.Builder; + +@Builder +public record GetStoreMissionResDTO( + Long missionId, + Integer missionAmount, + Integer missionPoint +) { } diff --git a/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/store/dto/response/StoreReviewListResDTO.java b/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/store/dto/response/StoreReviewListResDTO.java index c6a610a..67bf402 100644 --- a/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/store/dto/response/StoreReviewListResDTO.java +++ b/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/store/dto/response/StoreReviewListResDTO.java @@ -10,10 +10,10 @@ public record StoreReviewListResDTO( Long userId, String userName, - Integer reviewStar, + Long reviewId, + Double reviewStar, String reviewContent, LocalDateTime reviewCreatedAt, - List reviewSavePath, Long commentId, String commentContent, diff --git a/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/store/entity/Store.java b/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/store/entity/Store.java index c3b4c93..025e85e 100644 --- a/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/store/entity/Store.java +++ b/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/store/entity/Store.java @@ -45,12 +45,15 @@ public class Store { // ====== 연관관계 매핑 ====== @OneToMany(mappedBy = "store", cascade = CascadeType.REMOVE) + @Builder.Default private List missionList = new ArrayList<>(); @OneToMany(mappedBy = "store", cascade = CascadeType.REMOVE) + @Builder.Default private List storePhotos = new ArrayList<>(); @OneToMany(mappedBy = "store", cascade = CascadeType.REMOVE) + @Builder.Default private List storeReviews = new ArrayList<>(); } \ No newline at end of file diff --git a/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/store/entity/StoreReview.java b/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/store/entity/StoreReview.java index 554e180..49d0228 100644 --- a/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/store/entity/StoreReview.java +++ b/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/store/entity/StoreReview.java @@ -17,6 +17,7 @@ @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor +@Builder @EntityListeners(AuditingEntityListener.class) public class StoreReview { @@ -46,18 +47,9 @@ public class StoreReview { private Store store; @OneToMany(mappedBy = "storeReview", cascade = CascadeType.REMOVE) + @Builder.Default private List reviewPhotos = new ArrayList<>(); @OneToOne(mappedBy = "storeReview", cascade = CascadeType.REMOVE) private OwnerComment ownerComment; - - @Builder - private StoreReview(User user, Store store, Double reviewStar, String reviewContent) { - this.user = user; - this.store = store; - this.reviewStar = reviewStar; - this.reviewContent = reviewContent; - } - - } \ No newline at end of file diff --git a/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/store/enums/QueryType.java b/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/store/enums/QueryType.java new file mode 100644 index 0000000..be4fbb7 --- /dev/null +++ b/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/store/enums/QueryType.java @@ -0,0 +1,15 @@ +package com.umc.jaengchalttak.domain.store.enums; + +import lombok.Getter; + +@Getter +public enum QueryType { + ID("id"), + STAR("reviewStar"); + + private final String name; + + QueryType(String name) { + this.name = name; + } +} diff --git a/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/store/payload/code/StoreSuccessCode.java b/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/store/payload/code/StoreSuccessCode.java index 82a22df..d5a5dc6 100644 --- a/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/store/payload/code/StoreSuccessCode.java +++ b/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/store/payload/code/StoreSuccessCode.java @@ -19,7 +19,15 @@ public enum StoreSuccessCode implements BaseSuccessCode { REVIEW_LIST_OK(HttpStatus.OK, "STORE200_3", "성공적으로 가게의 리뷰 목록을 조회했습니다."), - + MY_REVIEW_LIST_OK(HttpStatus.OK, + "STORE200_4", + "성공적으로 나의 가게의 리뷰 목록을 조회했습니다."), + STORE_MISSION_CREATED_OK(HttpStatus.OK, + "STORE200_5", + "성공적으로 가게 미션을 생성했습니다."), + STORE_MISSION_OK(HttpStatus.OK, + "STORE200_6", + "성공적으로 가게 미션을 조회했습니다."), // ====== 201 ====== REVIEW_CREATED(HttpStatus.CREATED, "STORE201_1", diff --git a/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/store/repository/StoreReviewRepository.java b/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/store/repository/StoreReviewRepository.java index 673e3a2..f12120d 100644 --- a/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/store/repository/StoreReviewRepository.java +++ b/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/store/repository/StoreReviewRepository.java @@ -1,7 +1,40 @@ package com.umc.jaengchalttak.domain.store.repository; import com.umc.jaengchalttak.domain.store.entity.StoreReview; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; public interface StoreReviewRepository extends JpaRepository { + + // ID 기준 페이징 + @Query("SELECT r FROM StoreReview r " + + "JOIN FETCH r.user u JOIN FETCH r.store s " + + "WHERE r.user.id = :userId AND r.store.id = :storeId " + + "AND (:cursorId IS NULL OR r.id < :cursorId) " + + "ORDER BY r.id DESC") + Slice findReviewsByIdCursor( + @Param("userId") Long userId, + @Param("storeId") Long storeId, + @Param("cursorId") Long cursorId, + Pageable pageable + ); + + // 별점 기준 페이징 (별점이 같을 경우 ID를 비교) + @Query("SELECT r FROM StoreReview r " + + "JOIN FETCH r.user u JOIN FETCH r.store s " + + "WHERE r.user.id = :userId AND r.store.id = :storeId " + + "AND (:cursorStar IS NULL OR " + + " r.reviewStar < :cursorStar OR " + + " (r.reviewStar = :cursorStar AND r.id < :cursorId)) " + + "ORDER BY r.reviewStar DESC, r.id DESC") + Slice findReviewsByStarCursor( + @Param("userId") Long userId, + @Param("storeId") Long storeId, + @Param("cursorStar") Double cursorStar, + @Param("cursorId") Long cursorId, + Pageable pageable + ); } diff --git a/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/store/service/ReviewService.java b/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/store/service/ReviewService.java index 2d0aa1a..fc3d16b 100644 --- a/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/store/service/ReviewService.java +++ b/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/store/service/ReviewService.java @@ -2,8 +2,10 @@ import com.umc.jaengchalttak.domain.store.converter.StoreReviewConverter; import com.umc.jaengchalttak.domain.store.dto.request.StoreReviewReqDTO; +import com.umc.jaengchalttak.domain.store.dto.response.StoreReviewListResDTO; import com.umc.jaengchalttak.domain.store.entity.Store; import com.umc.jaengchalttak.domain.store.entity.StoreReview; +import com.umc.jaengchalttak.domain.store.enums.QueryType; import com.umc.jaengchalttak.domain.store.payload.StoreException; import com.umc.jaengchalttak.domain.store.payload.code.StoreErrorCode; import com.umc.jaengchalttak.domain.store.repository.StoreRepository; @@ -12,9 +14,14 @@ import com.umc.jaengchalttak.domain.user.payload.UserException; import com.umc.jaengchalttak.domain.user.payload.code.UserErrorCode; import com.umc.jaengchalttak.domain.user.repository.UserRepository; -import jakarta.transaction.Transactional; +import com.umc.jaengchalttak.global.converter.GlobalConverter; +import com.umc.jaengchalttak.global.dto.CursorPagination; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.*; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; @Service @RequiredArgsConstructor @@ -24,6 +31,56 @@ public class ReviewService { private final StoreRepository storeRepository; private final StoreReviewRepository storeReviewRepository; + @Transactional(readOnly = true) + public CursorPagination getMyReviewList( + Long userId, + Long storeId, + Integer pageSize, + String cursor, + QueryType query + ) { + Pageable pageable = PageRequest.of(0, pageSize); + Slice storeReviewSlice; + + if (query == QueryType.ID) { + Long cursorId = (cursor != null) ? Long.parseLong(cursor) : null; + storeReviewSlice = storeReviewRepository.findReviewsByIdCursor(userId, storeId, cursorId, pageable); + } else { + // 정렬 기준인 별점이 동일할 경우를 대비하여 [별점:ID] 형태의 복합 커서를 분리하여 동점자 처리 + Double cursorStar = null; + Long cursorId = null; + if (cursor != null) { + String[] parts = cursor.split(":"); + cursorStar = Double.parseDouble(parts[0]); + cursorId = Long.parseLong(parts[1]); + } + storeReviewSlice = storeReviewRepository.findReviewsByStarCursor(userId, storeId, cursorStar, cursorId, pageable); + } + + List content = storeReviewSlice.getContent(); + + // nextCursor 생성 + String nextCursor = null; + if (!content.isEmpty()) { + StoreReview lastReview = content.getLast(); + nextCursor = (query == QueryType.ID) + ? String.valueOf(lastReview.getId()) + : lastReview.getReviewStar() + ":" + lastReview.getId(); + } + + List dtoList = content.stream() + .map(StoreReviewConverter::toStoreReviewListResDTO) + .toList(); + + return GlobalConverter.toCursorPagination( + dtoList, + storeReviewSlice.hasNext(), + nextCursor, + pageSize + ); + } + + @Transactional public void createReview(StoreReviewReqDTO request) { User user = userRepository.findById(request.userId()) @@ -32,7 +89,7 @@ public void createReview(StoreReviewReqDTO request) { .orElseThrow(() -> new StoreException(StoreErrorCode.STORE_NOT_FOUND)); // Converter를 통해 entity로 변환 - StoreReview review = StoreReviewConverter.toEntity(request, user, store); + StoreReview review = StoreReviewConverter.toStoreReview(request, user, store); storeReviewRepository.save(review); } diff --git a/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/store/service/StoreMissionService.java b/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/store/service/StoreMissionService.java new file mode 100644 index 0000000..853c2c7 --- /dev/null +++ b/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/store/service/StoreMissionService.java @@ -0,0 +1,60 @@ +package com.umc.jaengchalttak.domain.store.service; + +import com.umc.jaengchalttak.domain.mission.entity.Mission; +import com.umc.jaengchalttak.domain.mission.repository.MissionRepository; +import com.umc.jaengchalttak.domain.store.converter.StoreMissionConverter; +import com.umc.jaengchalttak.domain.store.dto.request.CreateStoreMissionReqDTO; +import com.umc.jaengchalttak.domain.store.dto.response.GetStoreMissionResDTO; +import com.umc.jaengchalttak.domain.store.entity.Store; +import com.umc.jaengchalttak.domain.store.payload.StoreException; +import com.umc.jaengchalttak.domain.store.payload.code.StoreErrorCode; +import com.umc.jaengchalttak.domain.store.repository.StoreRepository; +import com.umc.jaengchalttak.global.converter.GlobalConverter; +import com.umc.jaengchalttak.global.dto.OffsetPagination; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class StoreMissionService { + + private final StoreRepository storeRepository; + private final MissionRepository missionRepository; + + @Transactional + public void createMission(Long storeId, CreateStoreMissionReqDTO request) { + Store store = storeRepository.findById(storeId) + .orElseThrow(() -> new StoreException(StoreErrorCode.STORE_NOT_FOUND)); + + Mission mission = StoreMissionConverter.toMission(store, request); + missionRepository.save(mission); + } + + @Transactional(readOnly = true) + public OffsetPagination getMissions( + Long storeId, + Integer pageSize, + Integer pageNumber, + String sort + ) { + Sort sortInfo = (sort != null && !sort.isBlank()) ? + Sort.by(sort) : + Sort.by("id").descending(); + + PageRequest pageRequest = PageRequest.of(pageNumber, pageSize, sortInfo); + + Page missionList = missionRepository.findAllByStoreId(storeId, pageRequest); + + return GlobalConverter.toOffsetPagination( + missionList.map(StoreMissionConverter::toGetStoreMissionResDTO).toList(), + missionList.getNumber(), + missionList.getSize() + ); + } + + +} diff --git a/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/user/controller/AuthController.java b/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/user/controller/AuthController.java index 4fd32e8..0f29632 100644 --- a/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/user/controller/AuthController.java +++ b/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/user/controller/AuthController.java @@ -8,9 +8,11 @@ import com.umc.jaengchalttak.domain.user.payload.code.UserSuccessCode; import com.umc.jaengchalttak.domain.user.payload.UserException; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import org.springframework.web.bind.annotation.*; +@Tag(name = "인증 API", description = "로그인, 회원가입 등 인증 관련 API입니다.") @RestController @RequestMapping("/api/auth") public class AuthController { @@ -23,7 +25,7 @@ public ApiResponse test() { @Operation(summary = "일반 회원가입", description = "새로운 유저 정보를 등록하여 회원가입을 진행합니다.") @PostMapping("/signup") - public ApiResponse signUpUser(@RequestBody UserInfoDTO request) { + public ApiResponse signUpUser(@Valid @RequestBody UserInfoDTO request) { BaseSuccessCode code = UserSuccessCode.USER_CREATED; return ApiResponse.onSuccess(code, "회원가입 성공!"); } diff --git a/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/user/controller/UserController.java b/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/user/controller/UserController.java index e6308d6..6f71702 100644 --- a/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/user/controller/UserController.java +++ b/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/user/controller/UserController.java @@ -7,10 +7,13 @@ import com.umc.jaengchalttak.global.apiPayload.code.BaseSuccessCode; import com.umc.jaengchalttak.domain.user.payload.code.UserSuccessCode; 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.web.bind.annotation.*; +@Tag(name = "사용자 API", description = "마이페이지, 알림 설정 등 사용자 관련 API입니다.") @RestController @RequiredArgsConstructor @RequestMapping("/api/user") @@ -42,7 +45,7 @@ public ApiResponse getPointInfo(@PathVariable Long userId) { @Operation(summary = "알림 설정 업데이트", description = "개별 알림 수신 여부를 수정합니다.") @PutMapping("/alarm") public ApiResponse updateAlarm( - @RequestBody UserAlarmDTO request + @Valid @RequestBody UserAlarmDTO request ) { // 임시값 삽입, Service 완성 시 삭제 예정 UserAlarmDTO.alarmResDTO result = UserAlarmDTO.alarmResDTO.builder() @@ -59,7 +62,7 @@ public ApiResponse updateAlarm( @Operation(summary = "유저 닉네임 변경", description = "유저의 서비스 내 활동명을 변경합니다.") @PatchMapping("/name") public ApiResponse changeUserName( - @RequestBody UserInfoDTO.userNameUpdateDTO name) { + @Valid @RequestBody UserInfoDTO.userNameUpdateDTO name) { // 임시값 삽입, Service 완성 시 삭제 예정 UserInfoDTO.userNameUpdateDTO result = UserInfoDTO.userNameUpdateDTO.builder() diff --git a/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/user/dto/UserAlarmDTO.java b/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/user/dto/UserAlarmDTO.java index 3b3399e..43a750e 100644 --- a/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/user/dto/UserAlarmDTO.java +++ b/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/user/dto/UserAlarmDTO.java @@ -1,11 +1,19 @@ package com.umc.jaengchalttak.domain.user.dto; +import jakarta.validation.constraints.NotNull; import lombok.Builder; public record UserAlarmDTO( + @NotNull(message = "사용자 ID는 필수입니다.") Long userId, + + @NotNull(message = "새로운 이벤트 알림 설정은 필수입니다.") Boolean newEvent, - Boolean review_answer, + + @NotNull(message = "리뷰 답글 알림 설정은 필수입니다.") + Boolean reviewAnswer, + + @NotNull(message = "문의 알림 설정은 필수입니다.") Boolean inquiry ) { @Builder diff --git a/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/user/dto/UserInfoDTO.java b/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/user/dto/UserInfoDTO.java index cce192b..a098774 100644 --- a/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/user/dto/UserInfoDTO.java +++ b/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/user/dto/UserInfoDTO.java @@ -3,6 +3,10 @@ import com.umc.jaengchalttak.domain.user.enums.Address; import com.umc.jaengchalttak.domain.user.enums.Gender; import com.umc.jaengchalttak.domain.user.enums.ServiceUseTitle; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Past; +import jakarta.validation.constraints.Size; import lombok.Builder; import java.time.LocalDate; @@ -11,15 +15,28 @@ @Builder public record UserInfoDTO( Map serviceUseAllow, // enum 기반 동의 여부 + + @NotBlank(message = "이름은 필수입니다.") + @Size(max = 45, message = "이름은 45자 이내여야 합니다.") String name, + Gender gender, + + @NotNull(message = "생년월일은 필수입니다.") + @Past(message = "생년월일은 과거 날짜여야 합니다.") LocalDate birthday, + + @NotBlank(message = "주소는 필수입니다.") String address, + Integer phoneNumber, + Integer point ) { @Builder public static record userNameUpdateDTO( + @NotBlank(message = "변경할 이름은 필수입니다.") + @Size(max = 45, message = "이름은 45자 이내여야 합니다.") String name ) {} } \ No newline at end of file diff --git a/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/user/entity/User.java b/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/user/entity/User.java index 2eef52a..bd49c4f 100644 --- a/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/user/entity/User.java +++ b/jaengchalttak/src/main/java/com/umc/jaengchalttak/domain/user/entity/User.java @@ -58,18 +58,23 @@ public class User extends BaseEntity { // ====== 연관관계 매핑 ====== @OneToMany(mappedBy = "user", cascade = CascadeType.REMOVE) + @Builder.Default private List userMissionList = new ArrayList<>(); @OneToMany(mappedBy = "user", cascade = CascadeType.REMOVE) + @Builder.Default private List favoriteFoodList = new ArrayList<>(); @OneToMany(mappedBy = "user", cascade = CascadeType.REMOVE) + @Builder.Default private List inquiryList = new ArrayList<>(); @OneToMany(mappedBy = "user", cascade = CascadeType.REMOVE) + @Builder.Default private List serviceUseAllows = new ArrayList<>(); @OneToMany(mappedBy = "user", cascade = CascadeType.REMOVE) + @Builder.Default private List storeReviewList = new ArrayList<>(); } diff --git a/jaengchalttak/src/main/java/com/umc/jaengchalttak/global/apiPayload/code/GeneralErrorCode.java b/jaengchalttak/src/main/java/com/umc/jaengchalttak/global/apiPayload/code/GeneralErrorCode.java index 776cf06..16d0655 100644 --- a/jaengchalttak/src/main/java/com/umc/jaengchalttak/global/apiPayload/code/GeneralErrorCode.java +++ b/jaengchalttak/src/main/java/com/umc/jaengchalttak/global/apiPayload/code/GeneralErrorCode.java @@ -10,18 +10,26 @@ public enum GeneralErrorCode implements BaseErrorCode { BAD_REQUEST(HttpStatus.BAD_REQUEST, "COMMON400_1", "잘못된 요청입니다."), - INVALID_INPUT_VALUE(HttpStatus.BAD_REQUEST, + INVALID_REQUEST_BODY(HttpStatus.BAD_REQUEST, "COMMON400_2", - "입력값이 올바르지 않습니다."), + "요청 본문 값이 올바르지 않습니다."), + + INVALID_REQUEST_PARAMETER(HttpStatus.BAD_REQUEST, + "COMMON400_3", + "요청 파라미터 값이 올바르지 않습니다."), + UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "COMMON401_1", "인증되지 않았습니다."), + FORBIDDEN(HttpStatus.FORBIDDEN, "COMMON403_1", "접근이 금지되었습니다."), + NOT_FOUND(HttpStatus.NOT_FOUND, "COMMON404_1", "해당 리소스를 찾을 수 없습니다."), + INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "COMMON500_1", "서버에서 에러가 발생했습니다."), diff --git a/jaengchalttak/src/main/java/com/umc/jaengchalttak/global/apiPayload/handler/GeneralExceptionAdvice.java b/jaengchalttak/src/main/java/com/umc/jaengchalttak/global/apiPayload/handler/GeneralExceptionAdvice.java index 07b75ed..d191bd9 100644 --- a/jaengchalttak/src/main/java/com/umc/jaengchalttak/global/apiPayload/handler/GeneralExceptionAdvice.java +++ b/jaengchalttak/src/main/java/com/umc/jaengchalttak/global/apiPayload/handler/GeneralExceptionAdvice.java @@ -7,7 +7,9 @@ import jakarta.validation.ConstraintViolation; import jakarta.validation.ConstraintViolationException; import lombok.extern.slf4j.Slf4j; +import org.springframework.context.support.DefaultMessageSourceResolvable; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; @@ -47,20 +49,43 @@ public ResponseEntity> handleException( ); } - // 유효성 검증 관련 예외 - @ExceptionHandler(ConstraintViolationException.class) - public ResponseEntity> handleConstraintViolationException(ConstraintViolationException e) { - String errorMessage = e.getConstraintViolations().stream() - .map(ConstraintViolation::getMessage) + // 요청 본문 검증 실패 + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity> handleMethodArgumentNotValidException( + MethodArgumentNotValidException e + ) { + + String errorMessage = e.getBindingResult() + .getFieldErrors() + .stream() .findFirst() - .orElse("유효성 검증에 실패했습니다."); + .map(DefaultMessageSourceResolvable::getDefaultMessage) + .orElse("요청 본문 값이 올바르지 않습니다."); - BaseErrorCode errorCode = GeneralErrorCode.INVALID_INPUT_VALUE; + return ResponseEntity.badRequest() + .body(ApiResponse.onFailure( + GeneralErrorCode.INVALID_REQUEST_BODY, + errorMessage + )); + } - return ResponseEntity.status(errorCode.getStatus()) + // 파라미터 검증 실패 + @ExceptionHandler(ConstraintViolationException.class) + public ResponseEntity> handleConstraintViolationException( + ConstraintViolationException e + ) { + + String errorMessage = e.getConstraintViolations() + .stream() + .findFirst() + .map(ConstraintViolation::getMessage) + .orElse("요청 파라미터 값이 올바르지 않습니다."); + + return ResponseEntity.badRequest() .body(ApiResponse.onFailure( - errorCode, + GeneralErrorCode.INVALID_REQUEST_PARAMETER, errorMessage )); } + } diff --git a/jaengchalttak/src/main/java/com/umc/jaengchalttak/global/converter/GlobalConverter.java b/jaengchalttak/src/main/java/com/umc/jaengchalttak/global/converter/GlobalConverter.java new file mode 100644 index 0000000..8494953 --- /dev/null +++ b/jaengchalttak/src/main/java/com/umc/jaengchalttak/global/converter/GlobalConverter.java @@ -0,0 +1,42 @@ +package com.umc.jaengchalttak.global.converter; + +import com.umc.jaengchalttak.global.apiPayload.code.GeneralErrorCode; +import com.umc.jaengchalttak.global.apiPayload.exception.ProjectException; +import com.umc.jaengchalttak.global.dto.CursorPagination; +import com.umc.jaengchalttak.global.dto.OffsetPagination; + +import java.util.List; + +public class GlobalConverter { + + private GlobalConverter() { + throw new ProjectException(GeneralErrorCode.UTILITY_CLASS_INSTANTIATION); + } + + public static OffsetPagination toOffsetPagination( + List data, + Integer pageNumber, + Integer pageSize + ) { + return OffsetPagination.builder() + .data(data) + .pageNumber(pageNumber) + .pageSize(pageSize) + .build(); + } + + public static CursorPagination toCursorPagination( + List data, + Boolean hasNext, + String nextCursor, + Integer pageSize + ) { + return CursorPagination.builder() + .data(data) + .hasNext(hasNext) + .nextCursor(nextCursor) + .pageSize(pageSize) + .build(); + } + +} diff --git a/jaengchalttak/src/main/java/com/umc/jaengchalttak/global/dto/CursorPagination.java b/jaengchalttak/src/main/java/com/umc/jaengchalttak/global/dto/CursorPagination.java new file mode 100644 index 0000000..bf85cbf --- /dev/null +++ b/jaengchalttak/src/main/java/com/umc/jaengchalttak/global/dto/CursorPagination.java @@ -0,0 +1,13 @@ +package com.umc.jaengchalttak.global.dto; + +import lombok.Builder; + +import java.util.List; + +@Builder +public record CursorPagination ( + List data, + Boolean hasNext, + String nextCursor, + Integer pageSize +) {} diff --git a/jaengchalttak/src/main/java/com/umc/jaengchalttak/global/dto/OffsetPagination.java b/jaengchalttak/src/main/java/com/umc/jaengchalttak/global/dto/OffsetPagination.java new file mode 100644 index 0000000..9eac19c --- /dev/null +++ b/jaengchalttak/src/main/java/com/umc/jaengchalttak/global/dto/OffsetPagination.java @@ -0,0 +1,12 @@ +package com.umc.jaengchalttak.global.dto; + +import lombok.Builder; + +import java.util.List; + +@Builder +public record OffsetPagination ( + List data, + Integer pageNumber, + Integer pageSize +) { }