From 5b1e4be46704d7c0b1ba69be0f06890c8c9118bb Mon Sep 17 00:00:00 2001 From: JinYoung Shin Date: Sun, 10 May 2026 21:48:57 +0900 Subject: [PATCH 1/8] =?UTF-8?q?feat=20:=20MissionReqDTO=20=EB=82=B4=20Crea?= =?UTF-8?q?teMission=20=EB=A0=88=EC=BD=94=EB=93=9C=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../umc10th/domain/mission/dto/MissionReqDTO.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Jinyong/src/main/java/com/example/umc10th/domain/mission/dto/MissionReqDTO.java b/Jinyong/src/main/java/com/example/umc10th/domain/mission/dto/MissionReqDTO.java index e33508b9..22461e17 100644 --- a/Jinyong/src/main/java/com/example/umc10th/domain/mission/dto/MissionReqDTO.java +++ b/Jinyong/src/main/java/com/example/umc10th/domain/mission/dto/MissionReqDTO.java @@ -1,4 +1,13 @@ package com.example.umc10th.domain.mission.dto; +import java.time.LocalDate; + public class MissionReqDTO { + + // 가게 미션 생성 + public record CreateMission( + String title, + LocalDate deadline, + Integer reward + ) {} } From f3d178d972260bb0974856f144c4d2ee130219f2 Mon Sep 17 00:00:00 2001 From: JinYoung Shin Date: Sun, 10 May 2026 22:02:57 +0900 Subject: [PATCH 2/8] =?UTF-8?q?feat=20:=20MissionResDTO=20=EB=82=B4=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=95=20=EC=9D=91=EB=8B=B5=EC=9D=84=20?= =?UTF-8?q?=EC=9C=84=ED=95=9C=20Pagination=20=EB=A0=88=EC=BD=94=EB=93=9C?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../umc10th/domain/mission/dto/MissionResDTO.java | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/Jinyong/src/main/java/com/example/umc10th/domain/mission/dto/MissionResDTO.java b/Jinyong/src/main/java/com/example/umc10th/domain/mission/dto/MissionResDTO.java index 8d6ee79e..d581eb32 100644 --- a/Jinyong/src/main/java/com/example/umc10th/domain/mission/dto/MissionResDTO.java +++ b/Jinyong/src/main/java/com/example/umc10th/domain/mission/dto/MissionResDTO.java @@ -3,9 +3,11 @@ import lombok.Builder; import java.time.LocalDate; +import java.util.List; public class MissionResDTO { + // 가게 내 미션 조회 @Builder public record MissionInfo( Long id, @@ -13,6 +15,13 @@ public record MissionInfo( String title, LocalDate deadline, Integer reward - ) { - } + ) {} + + // 페이지네이션 툴 + @Builder + public record Pagination( + List data, + Integer pageNumber, + Integer pageSize + ){} } \ No newline at end of file From 2640c2971090527fec8b1cf140995feb791526bb Mon Sep 17 00:00:00 2001 From: JinYoung Shin Date: Tue, 12 May 2026 17:41:09 +0900 Subject: [PATCH 3/8] =?UTF-8?q?feat:=20RequestBody=20=EA=B2=80=EC=A6=9D=20?= =?UTF-8?q?=EC=98=88=EC=99=B8=20=EC=B2=98=EB=A6=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/mission/dto/MissionReqDTO.java | 12 ++++++++++- .../handler/GeneralExceptionAdvice.java | 21 +++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/Jinyong/src/main/java/com/example/umc10th/domain/mission/dto/MissionReqDTO.java b/Jinyong/src/main/java/com/example/umc10th/domain/mission/dto/MissionReqDTO.java index 22461e17..22959316 100644 --- a/Jinyong/src/main/java/com/example/umc10th/domain/mission/dto/MissionReqDTO.java +++ b/Jinyong/src/main/java/com/example/umc10th/domain/mission/dto/MissionReqDTO.java @@ -1,13 +1,23 @@ package com.example.umc10th.domain.mission.dto; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.PositiveOrZero; + import java.time.LocalDate; public class MissionReqDTO { // 가게 미션 생성 public record CreateMission( + @NotBlank(message = "미션 제목은 필수입니다.") String title, + + @NotNull(message = "마감기한은 필수입니다.") LocalDate deadline, + + @NotNull(message = "보상 포인트는 필수입니다.") + @PositiveOrZero(message = "보상 포인트는 0 이상이어야 합니다.") Integer reward ) {} -} +} \ No newline at end of file diff --git a/Jinyong/src/main/java/com/example/umc10th/global/apiPayload/handler/GeneralExceptionAdvice.java b/Jinyong/src/main/java/com/example/umc10th/global/apiPayload/handler/GeneralExceptionAdvice.java index e8fbbd3e..fe6c6723 100644 --- a/Jinyong/src/main/java/com/example/umc10th/global/apiPayload/handler/GeneralExceptionAdvice.java +++ b/Jinyong/src/main/java/com/example/umc10th/global/apiPayload/handler/GeneralExceptionAdvice.java @@ -5,9 +5,13 @@ import com.example.umc10th.global.apiPayload.code.GeneralErrorCode; import com.example.umc10th.global.apiPayload.exception.ProjectException; 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; +import java.util.HashMap; +import java.util.Map; + @RestControllerAdvice public class GeneralExceptionAdvice { @@ -21,6 +25,23 @@ public ResponseEntity> handleMemberException( .body(ApiResponse.onFailure(errorCode, null)); } + // @Valid 검증 실패 예외 처리 + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity>> handleMethodArgumentNotValidException( + MethodArgumentNotValidException e + ) { + Map errors = new HashMap<>(); + + e.getBindingResult().getFieldErrors().forEach(error -> + errors.put(error.getField(), error.getDefaultMessage()) + ); + + BaseErrorCode code = GeneralErrorCode.BAD_REQUEST; + + return ResponseEntity.status(code.getStatus()) + .body(ApiResponse.onFailure(code, errors)); + } + // 그 외의 정의되지 않은 모든 예외 처리 @ExceptionHandler(Exception.class) public ResponseEntity> handleException( From b8b1f3679731caa52f8aa7b103e9549ed3004678 Mon Sep 17 00:00:00 2001 From: JinYoung Shin Date: Tue, 12 May 2026 17:41:59 +0900 Subject: [PATCH 4/8] =?UTF-8?q?feat:=20=EA=B0=80=EA=B2=8C=EB=B3=84=20?= =?UTF-8?q?=EB=AF=B8=EC=85=98=20=EC=A1=B0=ED=9A=8C=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=EB=84=A4=EC=9D=B4=EC=85=98=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mission/code/MissionSuccessCode.java | 5 +- .../mission/controller/MissionController.java | 46 ++++++-- .../mission/converter/MissionConverter.java | 57 ++++++++- .../domain/mission/dto/MissionResDTO.java | 16 ++- .../mission/repository/MissionRepository.java | 8 +- .../mission/service/MissionService.java | 108 ++++++++++++++++-- 6 files changed, 217 insertions(+), 23 deletions(-) diff --git a/Jinyong/src/main/java/com/example/umc10th/domain/mission/code/MissionSuccessCode.java b/Jinyong/src/main/java/com/example/umc10th/domain/mission/code/MissionSuccessCode.java index bf61f50c..0413dbcb 100644 --- a/Jinyong/src/main/java/com/example/umc10th/domain/mission/code/MissionSuccessCode.java +++ b/Jinyong/src/main/java/com/example/umc10th/domain/mission/code/MissionSuccessCode.java @@ -9,8 +9,11 @@ @AllArgsConstructor public enum MissionSuccessCode implements BaseSuccessCode { - OK(HttpStatus.OK, + CREATED(HttpStatus.OK, "MISSION200_1", + "성공적으로 미션을 생성했습니다."), + OK(HttpStatus.OK, + "MISSION200_2", "성공적으로 미션을 조회했습니다."); private final HttpStatus status; diff --git a/Jinyong/src/main/java/com/example/umc10th/domain/mission/controller/MissionController.java b/Jinyong/src/main/java/com/example/umc10th/domain/mission/controller/MissionController.java index 5c232fd2..5a4a4186 100644 --- a/Jinyong/src/main/java/com/example/umc10th/domain/mission/controller/MissionController.java +++ b/Jinyong/src/main/java/com/example/umc10th/domain/mission/controller/MissionController.java @@ -1,15 +1,15 @@ package com.example.umc10th.domain.mission.controller; import com.example.umc10th.domain.mission.code.MissionSuccessCode; +import com.example.umc10th.domain.mission.dto.MissionReqDTO; import com.example.umc10th.domain.mission.dto.MissionResDTO; import com.example.umc10th.domain.mission.service.MissionService; import com.example.umc10th.global.apiPayload.ApiResponse; import com.example.umc10th.global.apiPayload.code.BaseSuccessCode; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; -import java.util.List; - @RestController @RequiredArgsConstructor @RequestMapping("/api") @@ -17,11 +17,43 @@ public class MissionController { private final MissionService missionService; - @GetMapping("/v1/missions") - public ApiResponse> getMissionList( - @RequestParam Long storeId + // 가게 미션 생성(POST) + @PostMapping("/v1/stores/{storeId}/missions") + public ApiResponse createMission( + @PathVariable Long storeId, + @RequestBody @Valid MissionReqDTO.CreateMission dto + ) { + missionService.createMission(storeId, dto); + + BaseSuccessCode code = MissionSuccessCode.CREATED; + return ApiResponse.onSuccess(code, null); + } + + // 가게 내 미션 목록 조회 조회(GET) + @GetMapping("/v1/stores/{storeId}/missions") + public ApiResponse> getMissionList( + @PathVariable Long storeId, + @RequestParam Integer pageSize, + @RequestParam Integer pageNumber, + @RequestParam(required = false) String sort ) { BaseSuccessCode code = MissionSuccessCode.OK; - return ApiResponse.onSuccess(code, missionService.getMissionList(storeId)); + return ApiResponse.onSuccess(code, missionService.getMissionList(storeId, pageSize, pageNumber, sort)); + } + + // 내가 진행 중인 미션 조회 + @GetMapping("/v1/users/{memberId}/missions/ongoing") + public ApiResponse> getMyOngoingMissions( + @PathVariable Long memberId, + @RequestParam Integer pageSize, + @RequestParam Integer pageNumber, + @RequestParam(required = false) String sort + ) { + BaseSuccessCode code = MissionSuccessCode.OK; + + return ApiResponse.onSuccess( + code, + missionService.getMyOngoingMissions(memberId, pageSize, pageNumber, sort) + ); } -} \ No newline at end of file +} diff --git a/Jinyong/src/main/java/com/example/umc10th/domain/mission/converter/MissionConverter.java b/Jinyong/src/main/java/com/example/umc10th/domain/mission/converter/MissionConverter.java index 60da9f6f..1140acd9 100644 --- a/Jinyong/src/main/java/com/example/umc10th/domain/mission/converter/MissionConverter.java +++ b/Jinyong/src/main/java/com/example/umc10th/domain/mission/converter/MissionConverter.java @@ -1,17 +1,68 @@ package com.example.umc10th.domain.mission.converter; +import com.example.umc10th.domain.mission.dto.MissionReqDTO; import com.example.umc10th.domain.mission.dto.MissionResDTO; import com.example.umc10th.domain.mission.entity.Mission; +import com.example.umc10th.domain.mission.entity.mapping.MemberMission; +import com.example.umc10th.domain.store.entity.Store; + +import java.util.List; public class MissionConverter { - public static MissionResDTO.MissionInfo toMissionInfo(Mission mission) { - return MissionResDTO.MissionInfo.builder() + // 가게 미션 생성 + public static Mission toMission( + Store store, + MissionReqDTO.CreateMission dto + ) { + return Mission.builder() + .store(store) + .title(dto.title()) + .reward(dto.reward()) + .deadline(dto.deadline()) + .build(); + } + + // 가격 내 미션 조회 + public static MissionResDTO.GetMission toGetMission( + Mission mission + ){ + return MissionResDTO.GetMission.builder() .id(mission.getId()) .storeId(mission.getStore().getId()) .title(mission.getTitle()) + .reward(mission.getReward()) + .deadline(mission.getDeadline()) + .build(); + } + + // 내가 진행 중인 미션 조회 + public static MissionResDTO.MyOngoingMission toMyOngoingMission( + MemberMission memberMission + ) { + Mission mission = memberMission.getMission(); + + return MissionResDTO.MyOngoingMission.builder() + .memberMissionId(memberMission.getId()) + .missionId(mission.getId()) + .storeId(mission.getStore().getId()) + .title(mission.getTitle()) .deadline(mission.getDeadline()) .reward(mission.getReward()) + .status(memberMission.getStatus()) + .build(); + } + + // 페이지네이션 툴 생성 + public static MissionResDTO.Pagination toPagination( + List data, + Integer pageNumber, + Integer pageSize + ){ + return MissionResDTO.Pagination.builder() + .data(data) + .pageNumber(pageNumber) + .pageSize(pageSize) .build(); } -} \ No newline at end of file +} diff --git a/Jinyong/src/main/java/com/example/umc10th/domain/mission/dto/MissionResDTO.java b/Jinyong/src/main/java/com/example/umc10th/domain/mission/dto/MissionResDTO.java index d581eb32..d81c1236 100644 --- a/Jinyong/src/main/java/com/example/umc10th/domain/mission/dto/MissionResDTO.java +++ b/Jinyong/src/main/java/com/example/umc10th/domain/mission/dto/MissionResDTO.java @@ -1,5 +1,6 @@ package com.example.umc10th.domain.mission.dto; +import com.example.umc10th.domain.mission.entity.MemberMissionStatus; import lombok.Builder; import java.time.LocalDate; @@ -9,7 +10,7 @@ public class MissionResDTO { // 가게 내 미션 조회 @Builder - public record MissionInfo( + public record GetMission( Long id, Long storeId, String title, @@ -17,6 +18,18 @@ public record MissionInfo( Integer reward ) {} + // 내가 진행 중인 미션 조회 + @Builder + public record MyOngoingMission( + Long memberMissionId, + Long missionId, + Long storeId, + String title, + LocalDate deadline, + Integer reward, + MemberMissionStatus status + ) {} + // 페이지네이션 툴 @Builder public record Pagination( @@ -24,4 +37,5 @@ public record Pagination( Integer pageNumber, Integer pageSize ){} + } \ No newline at end of file diff --git a/Jinyong/src/main/java/com/example/umc10th/domain/mission/repository/MissionRepository.java b/Jinyong/src/main/java/com/example/umc10th/domain/mission/repository/MissionRepository.java index 5a5eceec..49df0c5d 100644 --- a/Jinyong/src/main/java/com/example/umc10th/domain/mission/repository/MissionRepository.java +++ b/Jinyong/src/main/java/com/example/umc10th/domain/mission/repository/MissionRepository.java @@ -1,14 +1,14 @@ package com.example.umc10th.domain.mission.repository; import com.example.umc10th.domain.mission.entity.Mission; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; -import java.util.List; - public interface MissionRepository extends JpaRepository { @Query("SELECT m FROM Mission m WHERE m.store.id = :storeId") - List findMissionByStoreId(@Param("storeId") Long storeId); -} \ No newline at end of file + Page findMissionByStoreId(@Param("storeId") Long storeId, Pageable pageable); +} diff --git a/Jinyong/src/main/java/com/example/umc10th/domain/mission/service/MissionService.java b/Jinyong/src/main/java/com/example/umc10th/domain/mission/service/MissionService.java index f2f1ea94..5f4e3e16 100644 --- a/Jinyong/src/main/java/com/example/umc10th/domain/mission/service/MissionService.java +++ b/Jinyong/src/main/java/com/example/umc10th/domain/mission/service/MissionService.java @@ -1,22 +1,116 @@ package com.example.umc10th.domain.mission.service; import com.example.umc10th.domain.mission.converter.MissionConverter; +import com.example.umc10th.domain.mission.dto.MissionReqDTO; import com.example.umc10th.domain.mission.dto.MissionResDTO; +import com.example.umc10th.domain.mission.entity.MemberMissionStatus; +import com.example.umc10th.domain.mission.entity.Mission; +import com.example.umc10th.domain.mission.entity.mapping.MemberMission; import com.example.umc10th.domain.mission.repository.MissionRepository; +import com.example.umc10th.domain.store.entity.Store; +import com.example.umc10th.domain.store.exception.StoreException; +import com.example.umc10th.domain.store.exception.code.StoreErrorCode; +import com.example.umc10th.domain.store.repository.StoreRepository; +import jakarta.transaction.Transactional; 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 java.util.List; +import com.example.umc10th.domain.mission.repository.MemberMissionRepository; @Service @RequiredArgsConstructor public class MissionService { private final MissionRepository missionRepository; + private final StoreRepository storeRepository; + private final MemberMissionRepository memberMissionRepository; + + @Transactional + public void createMission( + Long storeId, + MissionReqDTO.CreateMission dto + ) { + Store store = storeRepository.findById(storeId) + .orElseThrow(() -> new StoreException(StoreErrorCode.STORE_NOT_FOUND)); + + Mission mission = MissionConverter.toMission(store, dto); + missionRepository.save(mission); + } + + @Transactional + public MissionResDTO.Pagination getMissionList( + Long storeId, + Integer pageNumber, + Integer pageSize, + String sort + ) { + storeRepository.findById(storeId) + .orElseThrow(() -> new StoreException(StoreErrorCode.STORE_NOT_FOUND)); + + return getMissions(storeId, pageSize, pageNumber, sort); + } + + // 내가 진행 중인 미션 조회 + @Transactional + public MissionResDTO.Pagination getMyOngoingMissions( + Long memberId, + Integer pageSize, + Integer pageNumber, + String sort + ) { + Sort sortInfo; + + if (sort != null) { + sortInfo = Sort.by(sort).descending(); + } else { + sortInfo = Sort.by("id").descending(); + } + + PageRequest pageRequest = PageRequest.of(pageNumber, pageSize, sortInfo); + + Page memberMissionPage = + memberMissionRepository.findMyOngoingMissions( + memberId, + MemberMissionStatus.ONGOING, + pageRequest + ); + + return MissionConverter.toPagination( + memberMissionPage.map(MissionConverter::toMyOngoingMission).toList(), + memberMissionPage.getNumber(), + memberMissionPage.getSize() + ); + } + + // 가게 내 미션들 조회 + public MissionResDTO.Pagination getMissions( + Long storeId, + Integer pageSize, + Integer pageNumber, + String sort + ) { + + // 정렬 정보 생성 + Sort sortInfo; + if (sort != null) { + sortInfo = Sort.by(sort); + } else { + sortInfo = Sort.by("id").descending(); + } + + // 페이지 정보들을 PageRequest로 만들기 + PageRequest pageRequest = PageRequest.of(pageNumber, pageSize, sortInfo); + + // 가게 내 미션들 조회 + Page missionList = missionRepository.findMissionByStoreId(storeId, pageRequest); - public List getMissionList(Long storeId) { - return missionRepository.findMissionByStoreId(storeId).stream() - .map(MissionConverter::toMissionInfo) - .toList(); + // 미션들 응답 DTO로 포장하기 + return MissionConverter.toPagination( + missionList.map(MissionConverter::toGetMission).toList(), + missionList.getNumber(), + missionList.getSize() + ); } -} \ No newline at end of file +} From 71cc4a90326559176900d677c0f8529248fc6e9e Mon Sep 17 00:00:00 2001 From: JinYoung Shin Date: Tue, 12 May 2026 17:43:32 +0900 Subject: [PATCH 5/8] =?UTF-8?q?feat:=20=EC=A7=84=ED=96=89=20=EC=A4=91?= =?UTF-8?q?=EC=9D=B8=20=EB=AF=B8=EC=85=98=20=EC=A1=B0=ED=9A=8C=20=EC=BF=BC?= =?UTF-8?q?=EB=A6=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/MemberMissionRepository.java | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/Jinyong/src/main/java/com/example/umc10th/domain/mission/repository/MemberMissionRepository.java b/Jinyong/src/main/java/com/example/umc10th/domain/mission/repository/MemberMissionRepository.java index 26230097..1b9103c2 100644 --- a/Jinyong/src/main/java/com/example/umc10th/domain/mission/repository/MemberMissionRepository.java +++ b/Jinyong/src/main/java/com/example/umc10th/domain/mission/repository/MemberMissionRepository.java @@ -1,7 +1,21 @@ package com.example.umc10th.domain.mission.repository; +import com.example.umc10th.domain.mission.entity.MemberMissionStatus; import com.example.umc10th.domain.mission.entity.mapping.MemberMission; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; -public interface MemberMissionRepository extends JpaRepository { -} +public interface MemberMissionRepository extends JpaRepository { + + @Query("SELECT mm FROM MemberMission mm " + + "WHERE mm.member.id = :memberId " + + "AND mm.status = :status") + Page findMyOngoingMissions( + @Param("memberId") Long memberId, + @Param("status") MemberMissionStatus status, + Pageable pageable + ); +} \ No newline at end of file From 13f40d452807b415faff4a500eb01d6612de6c4b Mon Sep 17 00:00:00 2001 From: JinYoung Shin Date: Tue, 12 May 2026 17:46:44 +0900 Subject: [PATCH 6/8] =?UTF-8?q?feat:=20=EB=82=B4=EA=B0=80=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1=ED=95=9C=20=EB=A6=AC=EB=B7=B0=20=EC=BB=A4=EC=84=9C=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=EB=84=A4=EC=9D=B4=EC=85=98=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../review/controller/ReviewController.java | 17 +++ .../review/converter/ReviewConverter.java | 28 +++++ .../domain/review/dto/ReviewResDTO.java | 22 ++++ .../review/repository/ReviewRepository.java | 48 ++++++++- .../domain/review/service/ReviewService.java | 101 ++++++++++++++++++ 5 files changed, 215 insertions(+), 1 deletion(-) diff --git a/Jinyong/src/main/java/com/example/umc10th/domain/review/controller/ReviewController.java b/Jinyong/src/main/java/com/example/umc10th/domain/review/controller/ReviewController.java index dc48f9ff..62b34462 100644 --- a/Jinyong/src/main/java/com/example/umc10th/domain/review/controller/ReviewController.java +++ b/Jinyong/src/main/java/com/example/umc10th/domain/review/controller/ReviewController.java @@ -17,9 +17,26 @@ public class ReviewController { private final ReviewService reviewService; + // 리뷰 전체 조회 API @GetMapping("/v1/reviews") public ApiResponse> getReviewList() { BaseSuccessCode code = ReviewSuccessCode.OK; return ApiResponse.onSuccess(code, reviewService.getReviewList()); } + + // 내가 작성한 리뷰 조회 API - 커서 기반 페이지네이션 + @GetMapping("/v1/users/{memberId}/reviews") + public ApiResponse> getMyReviews( + @PathVariable Long memberId, + @RequestParam Integer pageSize, + @RequestParam String cursor, + @RequestParam String query + ) { + BaseSuccessCode code = ReviewSuccessCode.OK; + + return ApiResponse.onSuccess( + code, + reviewService.getMyReviews(memberId, pageSize, cursor, query) + ); + } } \ No newline at end of file diff --git a/Jinyong/src/main/java/com/example/umc10th/domain/review/converter/ReviewConverter.java b/Jinyong/src/main/java/com/example/umc10th/domain/review/converter/ReviewConverter.java index fe2f34ac..a3a6b1fa 100644 --- a/Jinyong/src/main/java/com/example/umc10th/domain/review/converter/ReviewConverter.java +++ b/Jinyong/src/main/java/com/example/umc10th/domain/review/converter/ReviewConverter.java @@ -1,8 +1,11 @@ package com.example.umc10th.domain.review.converter; +import com.example.umc10th.domain.mission.dto.MissionResDTO; import com.example.umc10th.domain.review.dto.ReviewResDTO; import com.example.umc10th.domain.review.entity.Review; +import java.util.List; + public class ReviewConverter { public static ReviewResDTO.ReviewInfo toReviewInfo(Review review) { @@ -15,4 +18,29 @@ public static ReviewResDTO.ReviewInfo toReviewInfo(Review review) { .score(review.getScore()) .build(); } + + public static ReviewResDTO.MyReview toMyReview(Review review) { + return ReviewResDTO.MyReview.builder() + .reviewId(review.getId()) + .userId(review.getMember().getId()) + .storeId(review.getStore().getId()) + .userMissionId(review.getMemberMission().getId()) + .content(review.getContent()) + .score(review.getScore()) + .build(); + } + + public static ReviewResDTO.CursorPagination toCursorPagination( + List data, + Boolean hasNext, + String nextCursor, + Integer pageSize + ) { + return ReviewResDTO.CursorPagination.builder() + .data(data) + .hasNext(hasNext) + .nextCursor(nextCursor) + .pageSize(pageSize) + .build(); + } } \ No newline at end of file diff --git a/Jinyong/src/main/java/com/example/umc10th/domain/review/dto/ReviewResDTO.java b/Jinyong/src/main/java/com/example/umc10th/domain/review/dto/ReviewResDTO.java index 8d5a18db..4a52b829 100644 --- a/Jinyong/src/main/java/com/example/umc10th/domain/review/dto/ReviewResDTO.java +++ b/Jinyong/src/main/java/com/example/umc10th/domain/review/dto/ReviewResDTO.java @@ -2,6 +2,8 @@ import lombok.Builder; +import java.util.List; + public class ReviewResDTO { @Builder @@ -13,4 +15,24 @@ public record ReviewInfo( String content, Integer score ) {} + + // 내가 작성한 리뷰 조회 + @Builder + public record MyReview( + Long reviewId, + Long userId, + Long storeId, + Long userMissionId, + String content, + Integer score + ) {} + + // 커서 기반 페이지네이션 + @Builder + public record CursorPagination( + List data, + Boolean hasNext, + String nextCursor, + Integer pageSize + ) {} } \ No newline at end of file diff --git a/Jinyong/src/main/java/com/example/umc10th/domain/review/repository/ReviewRepository.java b/Jinyong/src/main/java/com/example/umc10th/domain/review/repository/ReviewRepository.java index d754a107..3550f3f8 100644 --- a/Jinyong/src/main/java/com/example/umc10th/domain/review/repository/ReviewRepository.java +++ b/Jinyong/src/main/java/com/example/umc10th/domain/review/repository/ReviewRepository.java @@ -1,7 +1,53 @@ package com.example.umc10th.domain.review.repository; import com.example.umc10th.domain.review.entity.Review; +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 ReviewRepository extends JpaRepository { -} + + // ID 순 첫 조회 + @Query("SELECT r FROM Review r " + + "WHERE r.member.id = :memberId " + + "ORDER BY r.id DESC") + Slice findMyReviewsByIdFirst( + @Param("memberId") Long memberId, + Pageable pageable + ); + + // ID 순 커서 조회 + @Query("SELECT r FROM Review r " + + "WHERE r.member.id = :memberId " + + "AND r.id < :cursorId " + + "ORDER BY r.id DESC") + Slice findMyReviewsByIdCursor( + @Param("memberId") Long memberId, + @Param("cursorId") Long cursorId, + Pageable pageable + ); + + // 별점 순 첫 조회 + @Query("SELECT r FROM Review r " + + "WHERE r.member.id = :memberId " + + "ORDER BY r.score DESC, r.id DESC") + Slice findMyReviewsByScoreFirst( + @Param("memberId") Long memberId, + Pageable pageable + ); + + // 별점 순 커서 조회 + @Query("SELECT r FROM Review r " + + "WHERE r.member.id = :memberId " + + "AND (r.score < :cursorScore " + + "OR (r.score = :cursorScore AND r.id < :cursorId)) " + + "ORDER BY r.score DESC, r.id DESC") + Slice findMyReviewsByScoreCursor( + @Param("memberId") Long memberId, + @Param("cursorScore") Integer cursorScore, + @Param("cursorId") Long cursorId, + Pageable pageable + ); +} \ No newline at end of file diff --git a/Jinyong/src/main/java/com/example/umc10th/domain/review/service/ReviewService.java b/Jinyong/src/main/java/com/example/umc10th/domain/review/service/ReviewService.java index 7959b683..ec4d09a6 100644 --- a/Jinyong/src/main/java/com/example/umc10th/domain/review/service/ReviewService.java +++ b/Jinyong/src/main/java/com/example/umc10th/domain/review/service/ReviewService.java @@ -2,8 +2,12 @@ import com.example.umc10th.domain.review.converter.ReviewConverter; import com.example.umc10th.domain.review.dto.ReviewResDTO; +import com.example.umc10th.domain.review.entity.Review; import com.example.umc10th.domain.review.repository.ReviewRepository; +import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Slice; import org.springframework.stereotype.Service; import java.util.List; @@ -19,4 +23,101 @@ public List getReviewList() { .map(ReviewConverter::toReviewInfo) .toList(); } + + // 가게 내 미션들 조회 + @Transactional + public ReviewResDTO.CursorPagination getMyReviews( + Long memberId, + Integer pageSize, + String cursor, + String query + ) { + // 페이지 정보들을 PageRequest로 만들기 + PageRequest pageRequest = PageRequest.of(0, pageSize); + + Slice reviewList; + String nextCursor; + + // 커서가 있는 경우 + if (!cursor.equals("-1")) { + + String[] cursorSplit = cursor.split(":"); + + switch (query.toLowerCase()) { + + // ID 순 조회 + case "id" -> { + Long idCursor = Long.parseLong(cursorSplit[0]); + + reviewList = reviewRepository.findMyReviewsByIdCursor( + memberId, + idCursor, + pageRequest + ); + } + + // 별점 순 조회 + case "score" -> { + Integer scoreCursor = Integer.parseInt(cursorSplit[0]); + Long idCursor = Long.parseLong(cursorSplit[1]); + + // 실제 DB 조회 + reviewList = reviewRepository.findMyReviewsByScoreCursor( + memberId, + scoreCursor, + idCursor, + pageRequest + ); + } + + // 예외 처리 + default -> throw new IllegalArgumentException("query는 id 또는 score만 가능합니다."); + } + + } else { // cursor = -1인 경우의 시작 (첫 조회) + // 커서 없이 첫 조회 + switch (query.toLowerCase()) { + + // ID 순 첫 조회 + case "id" -> reviewList = reviewRepository.findMyReviewsByIdFirst( + memberId, + pageRequest + ); + + // 별점 순 첫 조회 + case "score" -> reviewList = reviewRepository.findMyReviewsByScoreFirst( + memberId, + pageRequest + ); + + default -> throw new IllegalArgumentException("query는 id 또는 score만 가능합니다."); + } + } + + // 다음 커서 계산 + if (reviewList.getContent().isEmpty()) { + nextCursor = null; // 조회결과가 비어 있으면 다음 커서를 만들 수 없음 + } else { + Review lastReview = reviewList.getContent() + .get(reviewList.getContent().size() - 1); // 마지막 데이터 꺼내오기 + + + // ID 기준이면 다음 커서는 마지막 리뷰의 ID + if (query.equals("id")) { + nextCursor = String.valueOf(lastReview.getId()); + } else { // 별점 기준이면 다음 커서는 score:id 형태 + nextCursor = lastReview.getScore() + ":" + lastReview.getId(); + } + } + + // 리뷰 응답 DTO로 포장하기 + return ReviewConverter.toCursorPagination( + reviewList.getContent().stream() + .map(ReviewConverter::toMyReview) + .toList(), + reviewList.hasNext(), + nextCursor, + reviewList.getSize() + ); + } } \ No newline at end of file From bb39467ddb4fb8ab51ac21608175e0d0ae604ded Mon Sep 17 00:00:00 2001 From: JinYoung Shin Date: Tue, 12 May 2026 17:47:01 +0900 Subject: [PATCH 7/8] =?UTF-8?q?fix:=20=EA=B0=80=EA=B2=8C=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EC=8B=A4=ED=8C=A8=20=EC=97=90=EB=9F=AC=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../umc10th/domain/store/exception/code/StoreErrorCode.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Jinyong/src/main/java/com/example/umc10th/domain/store/exception/code/StoreErrorCode.java b/Jinyong/src/main/java/com/example/umc10th/domain/store/exception/code/StoreErrorCode.java index 335dbbb8..91e8b1ec 100644 --- a/Jinyong/src/main/java/com/example/umc10th/domain/store/exception/code/StoreErrorCode.java +++ b/Jinyong/src/main/java/com/example/umc10th/domain/store/exception/code/StoreErrorCode.java @@ -10,10 +10,10 @@ public enum StoreErrorCode implements BaseErrorCode { STORE_NOT_FOUND(HttpStatus.NOT_FOUND, - "STORE404", + "STORE404_1", "해당 가게를 찾을 수 없습니다."), REGION_NOT_FOUND(HttpStatus.NOT_FOUND, - "REGION404", + "REGION404_1", "해당 지역을 찾을 수 없습니다."); private final HttpStatus httpStatus; From 8dceb60320c8d526befc633fb6076c941ea0a7f3 Mon Sep 17 00:00:00 2001 From: JinYoung Shin Date: Tue, 12 May 2026 19:46:55 +0900 Subject: [PATCH 8/8] =?UTF-8?q?docs:=207=EC=A3=BC=EC=B0=A8=20=ED=95=B5?= =?UTF-8?q?=EC=8B=AC=20=ED=82=A4=EC=9B=8C=EB=93=9C=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...heckout_at_2026-04-30__1_57__Changes_1.xml | 4 - .../shelved.patch" | 206 ------------------ .idea/workspace.xml | 149 ++++++++----- Jinyong/keyword_summary/ch07.md | 111 ++++++++++ 4 files changed, 205 insertions(+), 265 deletions(-) delete mode 100644 .idea/shelf/Uncommitted_changes_before_Checkout_at_2026-04-30__1_57__Changes_1.xml delete mode 100644 ".idea/shelf/Uncommitted_changes_before_Checkout_at_2026-04-30_\354\230\244\355\233\204_1_57_[Changes]1/shelved.patch" create mode 100644 Jinyong/keyword_summary/ch07.md diff --git a/.idea/shelf/Uncommitted_changes_before_Checkout_at_2026-04-30__1_57__Changes_1.xml b/.idea/shelf/Uncommitted_changes_before_Checkout_at_2026-04-30__1_57__Changes_1.xml deleted file mode 100644 index 2646ec10..00000000 --- a/.idea/shelf/Uncommitted_changes_before_Checkout_at_2026-04-30__1_57__Changes_1.xml +++ /dev/null @@ -1,4 +0,0 @@ - - \ No newline at end of file diff --git "a/.idea/shelf/Uncommitted_changes_before_Checkout_at_2026-04-30_\354\230\244\355\233\204_1_57_[Changes]1/shelved.patch" "b/.idea/shelf/Uncommitted_changes_before_Checkout_at_2026-04-30_\354\230\244\355\233\204_1_57_[Changes]1/shelved.patch" deleted file mode 100644 index e4e70c9a..00000000 --- "a/.idea/shelf/Uncommitted_changes_before_Checkout_at_2026-04-30_\354\230\244\355\233\204_1_57_[Changes]1/shelved.patch" +++ /dev/null @@ -1,206 +0,0 @@ -Index: Jinyong/src/main/java/com/example/umc10th/domain/member/service/MemberService.java -IDEA additional info: -Subsystem: com.intellij.openapi.diff.impl.patch.BaseRevisionTextPatchEP -<+>package com.example.umc10th.domain.member.service;\r\n\r\npublic class MemberService {\r\n}\r\n -Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP -<+>UTF-8 -=================================================================== -diff --git a/Jinyong/src/main/java/com/example/umc10th/domain/member/service/MemberService.java b/Jinyong/src/main/java/com/example/umc10th/domain/member/service/MemberService.java ---- a/Jinyong/src/main/java/com/example/umc10th/domain/member/service/MemberService.java (revision fdc37513896120be201c90f5927b89f8037b3aa3) -+++ b/Jinyong/src/main/java/com/example/umc10th/domain/member/service/MemberService.java (date 1777524542470) -@@ -1,4 +1,30 @@ - package com.example.umc10th.domain.member.service; - -+import com.example.umc10th.domain.member.code.MemberErrorCode; -+import com.example.umc10th.domain.member.dto.MemberReqDTO; -+import com.example.umc10th.domain.member.dto.MemberResDTO; -+import com.example.umc10th.domain.member.entity.Member; -+import com.example.umc10th.domain.member.exception.MemberException; -+import com.example.umc10th.domain.member.repository.MemberRepository; -+ - public class MemberService { -+ -+ private final MemberRepository memberRepository; -+ -+ public MemberService(MemberRepository memberRepository) { -+ this.memberRepository = memberRepository; -+ } -+ -+ public MemberResDTO.GetInfo getInfo( -+ MemberReqDTO.GetInfo dto -+ ) { -+ // DTO에서 유저 ID를 추출 -+ Long memebrId = dto.id(); -+ // DB에서 해당 유저 ID로 데이터 조회 -+ Member member = memberRepository.findById(memberId) -+ .orElseThrow(() -> new MemberException(MemberErrorCode.MEMBER_NOT_FOUND)); -+ } -+ -+ public Object getInfo(MemberReqDTO.GetInfo dto) { -+ } - } -Index: Jinyong/src/main/java/com/example/umc10th/domain/member/converter/MemberConverter.java -IDEA additional info: -Subsystem: com.intellij.openapi.diff.impl.patch.BaseRevisionTextPatchEP -<+>package com.example.umc10th.domain.member.converter;\r\n\r\npublic class MemberConverter {\r\n}\r\n -Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP -<+>UTF-8 -=================================================================== -diff --git a/Jinyong/src/main/java/com/example/umc10th/domain/member/converter/MemberConverter.java b/Jinyong/src/main/java/com/example/umc10th/domain/member/converter/MemberConverter.java ---- a/Jinyong/src/main/java/com/example/umc10th/domain/member/converter/MemberConverter.java (revision fdc37513896120be201c90f5927b89f8037b3aa3) -+++ b/Jinyong/src/main/java/com/example/umc10th/domain/member/converter/MemberConverter.java (date 1777523938149) -@@ -1,4 +1,20 @@ - package com.example.umc10th.domain.member.converter; - -+import com.example.umc10th.domain.member.dto.MemberResDTO; -+import com.example.umc10th.domain.member.entity.Member; -+ - public class MemberConverter { --} -+ -+ // 마이페이지 정보 조회를 위한 DTO 변환 -+ public static MemberResDTO.GetInfo toGetInfo(Member member) { -+ -+ // 빌더 패턴을 사용하여 엔티티의 데이터를 DTO에 매핑합니다. -+ return MemberResDTO.GetInfo.builder() -+ .email(member.getEmail()) -+ .name(member.getName()) -+ .point(member.getPoint()) -+ .phoneNumber(member.getPhoneNumber()) -+ .profileUrl(member.getProfileUrl()) -+ .build(); -+ } -+} -\ No newline at end of file -Index: Jinyong/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java -IDEA additional info: -Subsystem: com.intellij.openapi.diff.impl.patch.BaseRevisionTextPatchEP -<+>package com.example.umc10th.domain.member.controller;\r\n\r\nimport com.example.umc10th.domain.member.dto.MemberReqDTO;\r\nimport com.example.umc10th.domain.member.dto.MemberResDTO;\r\nimport com.example.umc10th.global.apiPayload.ApiResponse;\r\nimport org.springframework.web.bind.annotation.*;\r\n\r\n@RestController\r\n@RequestMapping(\"/api\")\r\npublic class MemberController {\r\n\r\n // 마이페이지\r\n @PostMapping(\"/v1/users/me\")\r\n public ApiResponse getInfo(@RequestBody MemberReqDTO.GetInfo dto) {\r\n\r\n\r\n MemberResDTO.GetInfo result = MemberResDTO.GetInfo.builder()\r\n .name(\"철수\")\r\n .profileUrl(\"https://example.com/profile.jpg\")\r\n .email(\"patrick@hansung.ac.kr\")\r\n .phoneNumber(\"010-1234-5678\")\r\n .point(22500)\r\n .build();\r\n\r\n return ApiResponse.onSuccess(result);\r\n }\r\n} -Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP -<+>UTF-8 -=================================================================== -diff --git a/Jinyong/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java b/Jinyong/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java ---- a/Jinyong/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java (revision fdc37513896120be201c90f5927b89f8037b3aa3) -+++ b/Jinyong/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java (date 1777524675505) -@@ -1,27 +1,27 @@ - package com.example.umc10th.domain.member.controller; - -+import com.example.umc10th.domain.member.code.MemberSuccessCode; - import com.example.umc10th.domain.member.dto.MemberReqDTO; - import com.example.umc10th.domain.member.dto.MemberResDTO; -+import com.example.umc10th.domain.member.service.MemberService; - import com.example.umc10th.global.apiPayload.ApiResponse; -+import com.example.umc10th.global.apiPayload.code.BaseSuccessCode; -+import lombok.RequiredArgsConstructor; - import org.springframework.web.bind.annotation.*; - - @RestController -+@RequiredArgsConstructor - @RequestMapping("/api") - public class MemberController { - -+ private final MemberService memberService; -+ - // 마이페이지 - @PostMapping("/v1/users/me") -- public ApiResponse getInfo(@RequestBody MemberReqDTO.GetInfo dto) { -- -- -- MemberResDTO.GetInfo result = MemberResDTO.GetInfo.builder() -- .name("철수") -- .profileUrl("https://example.com/profile.jpg") -- .email("patrick@hansung.ac.kr") -- .phoneNumber("010-1234-5678") -- .point(22500) -- .build(); -- -- return ApiResponse.onSuccess(result); -+ public ApiResponse getInfo( -+ @RequestBody MemberReqDTO.GetInfo dto -+ ){ -+ BaseSuccessCode code = MemberSuccessCode.OK; -+ return ApiResponse.onSuccess(code, memberService.getInfo(dto)); - } - } -\ No newline at end of file -Index: Jinyong/src/main/java/com/example/umc10th/domain/member/repository/MemberRepository.java -IDEA additional info: -Subsystem: com.intellij.openapi.diff.impl.patch.BaseRevisionTextPatchEP -<+>package com.example.umc10th.domain.member.repository;\r\n\r\npublic class MemberRepository {\r\n}\r\n -Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP -<+>UTF-8 -=================================================================== -diff --git a/Jinyong/src/main/java/com/example/umc10th/domain/member/repository/MemberRepository.java b/Jinyong/src/main/java/com/example/umc10th/domain/member/repository/MemberRepository.java ---- a/Jinyong/src/main/java/com/example/umc10th/domain/member/repository/MemberRepository.java (revision fdc37513896120be201c90f5927b89f8037b3aa3) -+++ b/Jinyong/src/main/java/com/example/umc10th/domain/member/repository/MemberRepository.java (date 1777524765052) -@@ -1,4 +1,9 @@ - package com.example.umc10th.domain.member.repository; - --public class MemberRepository { --} -+import com.example.umc10th.domain.member.entity.Member; -+import org.springframework.data.jpa.repository.JpaRepository; -+ -+// JpaRepository<엔티티 타입, ID 타입>을 상속받아야 findById를 사용할 수 있어요! -+public interface MemberRepository extends JpaRepository { -+ -+} -\ No newline at end of file -Index: Jinyong/src/main/java/com/example/umc10th/domain/member/dto/MemberReqDTO.java -IDEA additional info: -Subsystem: com.intellij.openapi.diff.impl.patch.BaseRevisionTextPatchEP -<+>package com.example.umc10th.domain.member.dto;\r\n\r\npublic class MemberReqDTO {\r\n\r\n public record GetInfo(\r\n Long id\r\n ) {}\r\n} -Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP -<+>UTF-8 -=================================================================== -diff --git a/Jinyong/src/main/java/com/example/umc10th/domain/member/dto/MemberReqDTO.java b/Jinyong/src/main/java/com/example/umc10th/domain/member/dto/MemberReqDTO.java ---- a/Jinyong/src/main/java/com/example/umc10th/domain/member/dto/MemberReqDTO.java (revision fdc37513896120be201c90f5927b89f8037b3aa3) -+++ b/Jinyong/src/main/java/com/example/umc10th/domain/member/dto/MemberReqDTO.java (date 1777386765775) -@@ -5,4 +5,4 @@ - public record GetInfo( - Long id - ) {} --} -\ No newline at end of file -+} -Index: Jinyong/src/main/java/com/example/umc10th/domain/member/code/MemberErrorCode.java -=================================================================== -diff --git a/Jinyong/src/main/java/com/example/umc10th/domain/member/code/MemberErrorCode.java b/Jinyong/src/main/java/com/example/umc10th/domain/member/code/MemberErrorCode.java -new file mode 100644 ---- /dev/null (date 1777523463270) -+++ b/Jinyong/src/main/java/com/example/umc10th/domain/member/code/MemberErrorCode.java (date 1777523463270) -@@ -0,0 +1,4 @@ -+package com.example.umc10th.domain.member.code; -+ -+public enum MemberErrorCode { -+} -Index: .idea/workspace.xml -IDEA additional info: -Subsystem: com.intellij.openapi.diff.impl.patch.BaseRevisionTextPatchEP -<+>\r\n\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n diff --git a/Jinyong/keyword_summary/ch07.md b/Jinyong/keyword_summary/ch07.md new file mode 100644 index 00000000..ae10a571 --- /dev/null +++ b/Jinyong/keyword_summary/ch07.md @@ -0,0 +1,111 @@ +- Page와 Slice + + Page와 Slice는 Spring Data JPA에서 페이징 처리를 할 때 사용하는 반환 타입이다. + + 여기서 **페이징**이란? → 전체 데이터를 한번에 가져오는 게 아니라, 사용자가 요청한 만큼만 나누어 가져오는 방식이다. + + ex) 리뷰가 10000개인데 한번에 가져오면 무거움 → 10개씩 나눠서 조회하는 방식 + + Slice는 다음 또는 이전 Slice가 있는지 알 수 있는 데이터 조각이고, Page는 전체 페이지 수나 전체 데이터 수 같은 추가 정보를 포함 + + Page는 전체 데이터 개수, 전체 페이지 수, 현재 페이지 번호, 다음 페이지 존재 여부 등 많은 것을 한꺼번에 제공한다. → 전체 조회 시 유리 + + Slice는 전체 개수까진 알 필요 없고, 다음 페이지가 있는지 여부 정도만 알려준다. + + ex) 무한 스크롤에서는 굳이 전체 페이지가 필요하지 않다. + + → Page보다 가볍게 사용 가능하다. + + 차이점 (표) + + | 구분 | Page | Slice | + | --- | --- | --- | + | 전체 데이터 개수 | 알 수 있음 | 알 수 없음 | + | 전체 페이지 수 | 알 수 있음 | 알 수 없음 | + | 다음 페이지 여부 | 알 수 있음 | 알 수 있음 | + | count 쿼리 | 보통 실행됨 | 보통 필요 없음 | + | 적합한 상황 | 게시판, 관리자 페이지 | 무한 스크롤, 더보기 버튼 | + + Page는 전체 페이지 수와 전체 데이터 개수가 필요한 경우에 사용하고, Slice는 다음 데이터가 있는지만 알면 되는 무한 스크롤 방식에 적합하다. + +- Java stream API + + Java stream API는 컬렉션, 배열 같은 데이터 흐름처럼 처리할 수 있게 해주는 기능이다. + + ex) List에 여러 개의 데이터가 있을 때, 반복문을 작성하지 않고도 filter, map, collect 같이 메서드를 이용해 데이터를 다룰 수 있다. + + **why? 왜 사용할까?** + + 1. 조건에 맞는 데이터만 걸러내기 위해서 + 2. 객체에서 필요한 값만 뽑아낼려고 + 3. 데이터를 다른 형태로 바꾸기 위해서 + 4. 결과를 다시 List로 만들기 위해 + + 기존 for문, if문으로 쓰던 방식에서 반복문을 줄이고, 데이터 처리 의도를 더 명확하게 표현하기 위해서 쓴다 + + Stream API는 Spring Boot에서 **조회한 Entity 목록을 Response DTO 목록으로 바꿀 때** 많이 사용되기 때문에 Spring Boot에서 자주 사용된다. + + 한줄 요약 + + : Stream API는 컬렉션 데이터를 반복문 없이 깔끔하게 처리하기 위한 Java 기능입니다. 특히 `filter`, `map`, `toList`를 많이 사용하며, Spring Boot에서는 Entity를 DTO로 변환할 때 자주 사용된다. + +- 객체 그래프 탐색 + + 객체 그래프 탐색이란 객체가 가진 연관관계를 따라가며 다른 객체에 접근하는 것을 말한다. + + (객체 지향 언어에서 참조를 사용하여 연관된 객체를 타고 들어가 데이터를 조회하는 방식) + 예를 들어 `Review`가 `Member`와 연결되어 있다고 가정할 때, + + ```java + Review review = reviewRepository.findById(1L).get(); + + String nickname = review.getMember().getNickname(); + ``` + + 여기서 review.getMember()를 통해 Review 객체에서 연결된 Member 객체로 이동한다. + 이처럼 객체의 연관관계를 따라가며 접근하는 것을 객체 그래프 탐색이라고 볼 수 있다. + + 객체 그래프 탐색은 JPA의 핵심 장점 중 하나이다. + + JPA는 DB 테이블을 Java 객체처럼 다룰 수 있게 해주는데, SQL 직접 작성 시 필요한 조인(Join)의 제약에서 벗어나, 논리적인 도메인 모델 구조에 따라 데이터를 조회 가능하게 해준다. + + 주의할 점으로는, 지연 로딩과 N + 1문제가 있는데, 연관 객체에 접근하다가 추가 쿼리가 발생할 수 있는 것을 알아야 한다. + + ex) 리뷰 목록 조회 후 리뷰 작성자 닉네임을 가져올려고 하는 때에, + + 리뷰 목록 조회 1번 + + 리뷰 10개의 작성자 조회 10번 + = 총 11번 쿼리 ⇒ 이런 문제 발생 할 수도 있음 + + 해결 방향으로는, 연관된 객체를 언제 함께 가져올지 신경 써야 한다. + + `@EntityGraph`를 사용하여 조회 시 연관 엔티티를 함께 가져오는 방식으로 사용할 수 있다. + + 한줄요약 + + :객체 그래프 탐색은 객체의 연관관계를 따라 다른 객체에 접근하는 방식이다. JPA에서는 매우 자연스러운 방식이지만, 연관 객체 접근 시 추가 쿼리가 발생할 수 있으므로 N+1 문제를 주의해야 한다. + +- @Valid vs @Validated + + `@Valid`와 `@Validated`는 모두 **요청 데이터의 유효성 검증**을 위해 사용한다. + + ex) 회원가입 요청에서 이메일이 비어 있거나 형식이 잘못된 경우, 컨트롤러까지 들어온 데이터를 검증해서 잘못된 요청을 막을 수 있다.(DB까지 가지 않고 controller에서 조기 진압) + + `@Valid`는 필드, 메서드 파라미터, 반환값 등에 붙여서 해당 객체와 내부 속성에 정의된 제약 조건을 검증하게 해준다. + + 즉, `@Valid`는 주로 **Request Body DTO 검증**에 많이 사용된다. + + `@Validated`는 Spring 기반 메서드 검증을 활성화하거나, 검증 그룹을 지정할 때 사용할 수 있다. + + `@RequestParam`이나 `@PathVariable`에 붙은 `@Min`, `@NotNull` 같은 검증을 제대로 적용하려면 컨트롤러 클래스에 `@Validated`를 붙이는 경우가 많다. + + @Valid와 @Validated 차이점(표) + + | 구분 | @Valid | @Validated | + | --- | --- | --- | + | 주 사용 위치 | RequestBody DTO 검증 | 클래스, 메서드, 파라미터 검증 | + | 검증 그룹 | 기본적으로 그룹 지정 어려움 | 그룹 지정 가능 | + | 자주 쓰는 상황 | `@RequestBody @Valid DTO` | `@RequestParam`, `@PathVariable`, Service 메서드 검증 | + + `@Valid`는 DTO 내부 필드 검증에 주로 사용되고, `@Validated`는 메서드 파라미터 검증이나 검증 그룹이 필요할 때 사용한다. \ No newline at end of file