From 837f9463584949422a4fc820f73271d19e9ca77f Mon Sep 17 00:00:00 2001 From: minju Date: Wed, 29 Apr 2026 14:43:35 +0900 Subject: [PATCH 01/10] =?UTF-8?q?feat:=205=EC=A3=BC=EC=B0=A8=20=EB=AF=B8?= =?UTF-8?q?=EC=85=98=20=EA=B3=B5=ED=86=B5=20=EC=9D=91=EB=8B=B5/=EC=97=90?= =?UTF-8?q?=EB=9F=AC=20=ED=95=B8=EB=93=A4=EB=A7=81=20=EB=B0=8F=20Controlle?= =?UTF-8?q?r=20DTO=20=EC=9E=91=EC=84=B1=20#11?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bean/umc10th/.gitignore | 3 +- .../member/controller/MemberController.java | 34 +++++++------ .../domain/member/dto/MemberReqDto.java | 6 +++ .../domain/member/dto/MemberResDto.java | 9 ++++ .../member/exception/MemberException.java | 11 ++-- .../exception/code/MemberErrorCode.java | 18 +++++-- .../exception/code/MemberSuccessCode.java | 27 ++++++++++ .../mission/controller/MissionController.java | 11 ++-- .../mission/exception/MissionException.java | 11 ++-- .../exception/code/MissionErrorCode.java | 20 ++++++-- .../review/controller/ReviewController.java | 11 ++-- .../review/exception/ReviewException.java | 11 ++-- .../exception/code/ReviewErrorCode.java | 22 ++++++-- .../store/controller/StoreController.java | 7 +-- .../store/exception/StoreException.java | 11 ++-- .../store/exception/code/StoreErrorCode.java | 18 +++++-- .../test/controller/TestController.java | 20 ++++++++ .../umc10th/domain/test/dto/TestResDto.java | 10 ++++ .../global/apiPayload/ApiResponse.java | 50 +++++++++++++++++++ .../global/apiPayload/code/BaseCode.java | 9 ++++ .../global/apiPayload/code/BaseErrorCode.java | 4 ++ .../apiPayload/code/BaseSuccessCode.java | 4 ++ .../global/apiPayload/code/dto/ReasonDto.java | 11 ++++ .../apiPayload/code/status/ErrorStatus.java | 29 +++++++++++ .../apiPayload/code/status/SuccessStatus.java | 27 ++++++++++ .../exception/GeneralException.java | 21 ++++++++ .../exception/GeneralExceptionHandler.java | 46 +++++++++++++++++ 27 files changed, 385 insertions(+), 76 deletions(-) create mode 100644 bean/umc10th/src/main/java/com/example/umc10th/domain/member/exception/code/MemberSuccessCode.java create mode 100644 bean/umc10th/src/main/java/com/example/umc10th/domain/test/controller/TestController.java create mode 100644 bean/umc10th/src/main/java/com/example/umc10th/domain/test/dto/TestResDto.java create mode 100644 bean/umc10th/src/main/java/com/example/umc10th/global/apiPayload/ApiResponse.java create mode 100644 bean/umc10th/src/main/java/com/example/umc10th/global/apiPayload/code/BaseCode.java create mode 100644 bean/umc10th/src/main/java/com/example/umc10th/global/apiPayload/code/BaseErrorCode.java create mode 100644 bean/umc10th/src/main/java/com/example/umc10th/global/apiPayload/code/BaseSuccessCode.java create mode 100644 bean/umc10th/src/main/java/com/example/umc10th/global/apiPayload/code/dto/ReasonDto.java create mode 100644 bean/umc10th/src/main/java/com/example/umc10th/global/apiPayload/code/status/ErrorStatus.java create mode 100644 bean/umc10th/src/main/java/com/example/umc10th/global/apiPayload/code/status/SuccessStatus.java create mode 100644 bean/umc10th/src/main/java/com/example/umc10th/global/apiPayload/exception/GeneralException.java create mode 100644 bean/umc10th/src/main/java/com/example/umc10th/global/apiPayload/exception/GeneralExceptionHandler.java diff --git a/bean/umc10th/.gitignore b/bean/umc10th/.gitignore index ac2055b..5cbf6be 100644 --- a/bean/umc10th/.gitignore +++ b/bean/umc10th/.gitignore @@ -1,7 +1,6 @@ HELP.md .gradle -.gradle-user-home/ -.gradle-user-home-2/ +.gradle-user-home*/ build/ !gradle/wrapper/gradle-wrapper.jar !**/src/main/**/build/ diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java index a223397..13c0241 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java @@ -2,31 +2,37 @@ 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.domain.member.exception.MemberException; +import com.example.umc10th.domain.member.exception.code.MemberErrorCode; +import com.example.umc10th.domain.member.exception.code.MemberSuccessCode; +import com.example.umc10th.global.apiPayload.ApiResponse; import jakarta.validation.Valid; -import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController -@RequestMapping("/api/members") -@RequiredArgsConstructor +@RequestMapping("/api/v1/users") public class MemberController { - private final MemberService memberService; + @PostMapping("/me") + public ResponseEntity> getMyPage( + @Valid @RequestBody MemberReqDto.GetMyPageRequest request + ) { + if (request.id() <= 0) { + throw new MemberException(MemberErrorCode.MEMBER_NOT_FOUND); + } - @GetMapping("/{memberId}") - public ResponseEntity getMember(@PathVariable Long memberId) { - return ResponseEntity.ok(memberService.getMember(memberId)); - } + MemberResDto.GetMyPageResponse result = new MemberResDto.GetMyPageResponse( + "bean", + "https://example.com/profile/bean.png", + "bean@umc.com", + null, + 2500 + ); - @PostMapping - public ResponseEntity createMember(@Valid @RequestBody MemberReqDto.Create request) { - return ResponseEntity.ok(memberService.createMember(request)); + return ResponseEntity.ok(ApiResponse.of(MemberSuccessCode.GET_MY_PAGE_SUCCESS, result)); } } \ No newline at end of file diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/member/dto/MemberReqDto.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/member/dto/MemberReqDto.java index 71fad6a..adfafa5 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/domain/member/dto/MemberReqDto.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/member/dto/MemberReqDto.java @@ -3,6 +3,7 @@ import com.example.umc10th.domain.member.enums.Gender; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Positive; import jakarta.validation.constraints.Size; import java.time.LocalDate; @@ -16,4 +17,9 @@ public record Create( @NotBlank @Size(max = 20) String nickname ) { } + + public record GetMyPageRequest( + @NotNull @Positive Long id + ) { + } } \ No newline at end of file diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/member/dto/MemberResDto.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/member/dto/MemberResDto.java index 26d1817..8ba3e08 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/domain/member/dto/MemberResDto.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/member/dto/MemberResDto.java @@ -14,4 +14,13 @@ public record MemberInfo( String nickname ) { } + + public record GetMyPageResponse( + String name, + String profileUrl, + String email, + String phoneNumber, + Integer point + ) { + } } \ No newline at end of file diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/member/exception/MemberException.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/member/exception/MemberException.java index be0bbb2..5e0d1f9 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/domain/member/exception/MemberException.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/member/exception/MemberException.java @@ -1,15 +1,10 @@ package com.example.umc10th.domain.member.exception; import com.example.umc10th.domain.member.exception.code.MemberErrorCode; -import lombok.Getter; - -@Getter -public class MemberException extends RuntimeException { - - private final MemberErrorCode errorCode; +import com.example.umc10th.global.apiPayload.exception.GeneralException; +public class MemberException extends GeneralException { public MemberException(MemberErrorCode errorCode) { - super(errorCode.getMessage()); - this.errorCode = errorCode; + super(errorCode); } } \ No newline at end of file diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/member/exception/code/MemberErrorCode.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/member/exception/code/MemberErrorCode.java index 8d59239..371c66d 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/domain/member/exception/code/MemberErrorCode.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/member/exception/code/MemberErrorCode.java @@ -1,15 +1,27 @@ package com.example.umc10th.domain.member.exception.code; +import com.example.umc10th.global.apiPayload.code.BaseErrorCode; +import com.example.umc10th.global.apiPayload.code.dto.ReasonDto; import lombok.Getter; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; @Getter @RequiredArgsConstructor -public enum MemberErrorCode { - MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "MEMBER404", "����ڸ� ã�� �� �����ϴ�."); +public enum MemberErrorCode implements BaseErrorCode { + MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "MEMBER404", "사용자를 찾을 수 없습니다."); - private final HttpStatus status; + private final HttpStatus httpStatus; private final String code; private final String message; + + @Override + public ReasonDto getReason() { + return new ReasonDto(httpStatus, false, code, message); + } + + @Override + public ReasonDto getReasonHttpStatus() { + return getReason(); + } } \ No newline at end of file diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/member/exception/code/MemberSuccessCode.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/member/exception/code/MemberSuccessCode.java new file mode 100644 index 0000000..cba0595 --- /dev/null +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/member/exception/code/MemberSuccessCode.java @@ -0,0 +1,27 @@ +package com.example.umc10th.domain.member.exception.code; + +import com.example.umc10th.global.apiPayload.code.BaseSuccessCode; +import com.example.umc10th.global.apiPayload.code.dto.ReasonDto; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; + +@Getter +@RequiredArgsConstructor +public enum MemberSuccessCode implements BaseSuccessCode { + GET_MY_PAGE_SUCCESS(HttpStatus.OK, "MEMBER2001", "성공적으로 사용자 정보를 조회했습니다."); + + private final HttpStatus httpStatus; + private final String code; + private final String message; + + @Override + public ReasonDto getReason() { + return new ReasonDto(httpStatus, true, code, message); + } + + @Override + public ReasonDto getReasonHttpStatus() { + return getReason(); + } +} \ No newline at end of file diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/controller/MissionController.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/controller/MissionController.java index f26e710..79c6c87 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/controller/MissionController.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/controller/MissionController.java @@ -3,6 +3,7 @@ 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 jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; @@ -21,12 +22,12 @@ public class MissionController { private final MissionService missionService; @GetMapping("/{missionId}") - public ResponseEntity getMission(@PathVariable Long missionId) { - return ResponseEntity.ok(missionService.getMission(missionId)); + public ResponseEntity> getMission(@PathVariable Long missionId) { + return ResponseEntity.ok(ApiResponse.onSuccess(missionService.getMission(missionId))); } @PostMapping - public ResponseEntity createMission(@Valid @RequestBody MissionReqDto.Create request) { - return ResponseEntity.ok(missionService.createMission(request)); + public ResponseEntity> createMission(@Valid @RequestBody MissionReqDto.Create request) { + return ResponseEntity.ok(ApiResponse.onSuccess(missionService.createMission(request))); } -} \ No newline at end of file +} diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/exception/MissionException.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/exception/MissionException.java index 26a61fe..41fdeaa 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/exception/MissionException.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/exception/MissionException.java @@ -1,15 +1,10 @@ package com.example.umc10th.domain.mission.exception; import com.example.umc10th.domain.mission.exception.code.MissionErrorCode; -import lombok.Getter; - -@Getter -public class MissionException extends RuntimeException { - - private final MissionErrorCode errorCode; +import com.example.umc10th.global.apiPayload.exception.GeneralException; +public class MissionException extends GeneralException { public MissionException(MissionErrorCode errorCode) { - super(errorCode.getMessage()); - this.errorCode = errorCode; + super(errorCode); } } \ No newline at end of file diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/exception/code/MissionErrorCode.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/exception/code/MissionErrorCode.java index 4d71999..120f51f 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/exception/code/MissionErrorCode.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/exception/code/MissionErrorCode.java @@ -1,16 +1,28 @@ package com.example.umc10th.domain.mission.exception.code; +import com.example.umc10th.global.apiPayload.code.BaseErrorCode; +import com.example.umc10th.global.apiPayload.code.dto.ReasonDto; import lombok.Getter; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; @Getter @RequiredArgsConstructor -public enum MissionErrorCode { - MISSION_NOT_FOUND(HttpStatus.NOT_FOUND, "MISSION404", "�̼��� ã�� �� �����ϴ�."), - STORE_NOT_FOUND(HttpStatus.NOT_FOUND, "STORE404", "���Ը� ã�� �� �����ϴ�."); +public enum MissionErrorCode implements BaseErrorCode { + MISSION_NOT_FOUND(HttpStatus.NOT_FOUND, "MISSION404", "미션을 찾을 수 없습니다."), + STORE_NOT_FOUND(HttpStatus.NOT_FOUND, "STORE404", "가게를 찾을 수 없습니다."); - private final HttpStatus status; + private final HttpStatus httpStatus; private final String code; private final String message; + + @Override + public ReasonDto getReason() { + return new ReasonDto(httpStatus, false, code, message); + } + + @Override + public ReasonDto getReasonHttpStatus() { + return getReason(); + } } \ No newline at end of file diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/review/controller/ReviewController.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/review/controller/ReviewController.java index f3cfdfd..57cfa3e 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/domain/review/controller/ReviewController.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/review/controller/ReviewController.java @@ -3,6 +3,7 @@ import com.example.umc10th.domain.review.dto.ReviewReqDto; import com.example.umc10th.domain.review.dto.ReviewResDto; import com.example.umc10th.domain.review.service.ReviewService; +import com.example.umc10th.global.apiPayload.ApiResponse; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; @@ -21,12 +22,12 @@ public class ReviewController { private final ReviewService reviewService; @GetMapping("/{reviewId}") - public ResponseEntity getReview(@PathVariable Long reviewId) { - return ResponseEntity.ok(reviewService.getReview(reviewId)); + public ResponseEntity> getReview(@PathVariable Long reviewId) { + return ResponseEntity.ok(ApiResponse.onSuccess(reviewService.getReview(reviewId))); } @PostMapping - public ResponseEntity createReview(@Valid @RequestBody ReviewReqDto.Create request) { - return ResponseEntity.ok(reviewService.createReview(request)); + public ResponseEntity> createReview(@Valid @RequestBody ReviewReqDto.Create request) { + return ResponseEntity.ok(ApiResponse.onSuccess(reviewService.createReview(request))); } -} \ No newline at end of file +} diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/review/exception/ReviewException.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/review/exception/ReviewException.java index e4d509f..10f3491 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/domain/review/exception/ReviewException.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/review/exception/ReviewException.java @@ -1,15 +1,10 @@ package com.example.umc10th.domain.review.exception; import com.example.umc10th.domain.review.exception.code.ReviewErrorCode; -import lombok.Getter; - -@Getter -public class ReviewException extends RuntimeException { - - private final ReviewErrorCode errorCode; +import com.example.umc10th.global.apiPayload.exception.GeneralException; +public class ReviewException extends GeneralException { public ReviewException(ReviewErrorCode errorCode) { - super(errorCode.getMessage()); - this.errorCode = errorCode; + super(errorCode); } } \ No newline at end of file diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/review/exception/code/ReviewErrorCode.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/review/exception/code/ReviewErrorCode.java index 84140b6..b435640 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/domain/review/exception/code/ReviewErrorCode.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/review/exception/code/ReviewErrorCode.java @@ -1,17 +1,29 @@ package com.example.umc10th.domain.review.exception.code; +import com.example.umc10th.global.apiPayload.code.BaseErrorCode; +import com.example.umc10th.global.apiPayload.code.dto.ReasonDto; import lombok.Getter; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; @Getter @RequiredArgsConstructor -public enum ReviewErrorCode { - REVIEW_NOT_FOUND(HttpStatus.NOT_FOUND, "REVIEW404", "���並 ã�� �� �����ϴ�."), - MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "MEMBER404", "����ڸ� ã�� �� �����ϴ�."), - STORE_NOT_FOUND(HttpStatus.NOT_FOUND, "STORE404", "���Ը� ã�� �� �����ϴ�."); +public enum ReviewErrorCode implements BaseErrorCode { + REVIEW_NOT_FOUND(HttpStatus.NOT_FOUND, "REVIEW404", "리뷰를 찾을 수 없습니다."), + MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "MEMBER404", "사용자를 찾을 수 없습니다."), + STORE_NOT_FOUND(HttpStatus.NOT_FOUND, "STORE404", "가게를 찾을 수 없습니다."); - private final HttpStatus status; + private final HttpStatus httpStatus; private final String code; private final String message; + + @Override + public ReasonDto getReason() { + return new ReasonDto(httpStatus, false, code, message); + } + + @Override + public ReasonDto getReasonHttpStatus() { + return getReason(); + } } \ No newline at end of file diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/store/controller/StoreController.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/store/controller/StoreController.java index 22afbfa..ed3fdb5 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/domain/store/controller/StoreController.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/store/controller/StoreController.java @@ -2,6 +2,7 @@ import com.example.umc10th.domain.store.dto.StoreResDto; import com.example.umc10th.domain.store.service.StoreService; +import com.example.umc10th.global.apiPayload.ApiResponse; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; @@ -17,7 +18,7 @@ public class StoreController { private final StoreService storeService; @GetMapping("/{storeId}") - public ResponseEntity getStore(@PathVariable Long storeId) { - return ResponseEntity.ok(storeService.getStore(storeId)); + public ResponseEntity> getStore(@PathVariable Long storeId) { + return ResponseEntity.ok(ApiResponse.onSuccess(storeService.getStore(storeId))); } -} \ No newline at end of file +} diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/store/exception/StoreException.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/store/exception/StoreException.java index 3d33f25..6574d25 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/domain/store/exception/StoreException.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/store/exception/StoreException.java @@ -1,15 +1,10 @@ package com.example.umc10th.domain.store.exception; import com.example.umc10th.domain.store.exception.code.StoreErrorCode; -import lombok.Getter; - -@Getter -public class StoreException extends RuntimeException { - - private final StoreErrorCode errorCode; +import com.example.umc10th.global.apiPayload.exception.GeneralException; +public class StoreException extends GeneralException { public StoreException(StoreErrorCode errorCode) { - super(errorCode.getMessage()); - this.errorCode = errorCode; + super(errorCode); } } \ No newline at end of file diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/store/exception/code/StoreErrorCode.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/store/exception/code/StoreErrorCode.java index dffd006..87e78c3 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/domain/store/exception/code/StoreErrorCode.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/store/exception/code/StoreErrorCode.java @@ -1,15 +1,27 @@ package com.example.umc10th.domain.store.exception.code; +import com.example.umc10th.global.apiPayload.code.BaseErrorCode; +import com.example.umc10th.global.apiPayload.code.dto.ReasonDto; import lombok.Getter; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; @Getter @RequiredArgsConstructor -public enum StoreErrorCode { - STORE_NOT_FOUND(HttpStatus.NOT_FOUND, "STORE404", "Store not found."); +public enum StoreErrorCode implements BaseErrorCode { + STORE_NOT_FOUND(HttpStatus.NOT_FOUND, "STORE404", "가게를 찾을 수 없습니다."); - private final HttpStatus status; + private final HttpStatus httpStatus; private final String code; private final String message; + + @Override + public ReasonDto getReason() { + return new ReasonDto(httpStatus, false, code, message); + } + + @Override + public ReasonDto getReasonHttpStatus() { + return getReason(); + } } \ No newline at end of file diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/test/controller/TestController.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/test/controller/TestController.java new file mode 100644 index 0000000..7fcc448 --- /dev/null +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/test/controller/TestController.java @@ -0,0 +1,20 @@ +package com.example.umc10th.domain.test.controller; + +import com.example.umc10th.domain.test.dto.TestResDto; +import com.example.umc10th.global.apiPayload.ApiResponse; +import com.example.umc10th.global.apiPayload.code.status.SuccessStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/v1/test") +public class TestController { + + @GetMapping("/health") + public ResponseEntity> health() { + TestResDto.HealthResponse result = new TestResDto.HealthResponse("UP", "Test API is working."); + return ResponseEntity.ok(ApiResponse.of(SuccessStatus.OK, result)); + } +} \ No newline at end of file diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/test/dto/TestResDto.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/test/dto/TestResDto.java new file mode 100644 index 0000000..bd56486 --- /dev/null +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/test/dto/TestResDto.java @@ -0,0 +1,10 @@ +package com.example.umc10th.domain.test.dto; + +public class TestResDto { + + public record HealthResponse( + String status, + String message + ) { + } +} \ No newline at end of file diff --git a/bean/umc10th/src/main/java/com/example/umc10th/global/apiPayload/ApiResponse.java b/bean/umc10th/src/main/java/com/example/umc10th/global/apiPayload/ApiResponse.java new file mode 100644 index 0000000..7912d32 --- /dev/null +++ b/bean/umc10th/src/main/java/com/example/umc10th/global/apiPayload/ApiResponse.java @@ -0,0 +1,50 @@ +package com.example.umc10th.global.apiPayload; + +import com.example.umc10th.global.apiPayload.code.BaseErrorCode; +import com.example.umc10th.global.apiPayload.code.BaseSuccessCode; +import com.example.umc10th.global.apiPayload.code.status.SuccessStatus; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +@JsonPropertyOrder({"isSuccess", "code", "message", "result"}) +public class ApiResponse { + + @JsonProperty("isSuccess") + private final Boolean isSuccess; + private final String code; + private final String message; + + @JsonInclude(JsonInclude.Include.NON_NULL) + private T result; + + public static ApiResponse onSuccess(T result) { + return of(SuccessStatus.OK, result); + } + + public static ApiResponse of(BaseSuccessCode code, T result) { + return new ApiResponse<>( + true, + code.getReason().code(), + code.getReason().message(), + result + ); + } + + public static ApiResponse onFailure(BaseErrorCode code, T result) { + return new ApiResponse<>( + false, + code.getReason().code(), + code.getReason().message(), + result + ); + } + + public static ApiResponse onFailure(String code, String message, T result) { + return new ApiResponse<>(false, code, message, result); + } +} diff --git a/bean/umc10th/src/main/java/com/example/umc10th/global/apiPayload/code/BaseCode.java b/bean/umc10th/src/main/java/com/example/umc10th/global/apiPayload/code/BaseCode.java new file mode 100644 index 0000000..07573cd --- /dev/null +++ b/bean/umc10th/src/main/java/com/example/umc10th/global/apiPayload/code/BaseCode.java @@ -0,0 +1,9 @@ +package com.example.umc10th.global.apiPayload.code; + +import com.example.umc10th.global.apiPayload.code.dto.ReasonDto; + +public interface BaseCode { + ReasonDto getReason(); + + ReasonDto getReasonHttpStatus(); +} \ No newline at end of file diff --git a/bean/umc10th/src/main/java/com/example/umc10th/global/apiPayload/code/BaseErrorCode.java b/bean/umc10th/src/main/java/com/example/umc10th/global/apiPayload/code/BaseErrorCode.java new file mode 100644 index 0000000..da096a5 --- /dev/null +++ b/bean/umc10th/src/main/java/com/example/umc10th/global/apiPayload/code/BaseErrorCode.java @@ -0,0 +1,4 @@ +package com.example.umc10th.global.apiPayload.code; + +public interface BaseErrorCode extends BaseCode { +} \ No newline at end of file diff --git a/bean/umc10th/src/main/java/com/example/umc10th/global/apiPayload/code/BaseSuccessCode.java b/bean/umc10th/src/main/java/com/example/umc10th/global/apiPayload/code/BaseSuccessCode.java new file mode 100644 index 0000000..35b18d3 --- /dev/null +++ b/bean/umc10th/src/main/java/com/example/umc10th/global/apiPayload/code/BaseSuccessCode.java @@ -0,0 +1,4 @@ +package com.example.umc10th.global.apiPayload.code; + +public interface BaseSuccessCode extends BaseCode { +} \ No newline at end of file diff --git a/bean/umc10th/src/main/java/com/example/umc10th/global/apiPayload/code/dto/ReasonDto.java b/bean/umc10th/src/main/java/com/example/umc10th/global/apiPayload/code/dto/ReasonDto.java new file mode 100644 index 0000000..92b3568 --- /dev/null +++ b/bean/umc10th/src/main/java/com/example/umc10th/global/apiPayload/code/dto/ReasonDto.java @@ -0,0 +1,11 @@ +package com.example.umc10th.global.apiPayload.code.dto; + +import org.springframework.http.HttpStatus; + +public record ReasonDto( + HttpStatus httpStatus, + boolean isSuccess, + String code, + String message +) { +} \ No newline at end of file diff --git a/bean/umc10th/src/main/java/com/example/umc10th/global/apiPayload/code/status/ErrorStatus.java b/bean/umc10th/src/main/java/com/example/umc10th/global/apiPayload/code/status/ErrorStatus.java new file mode 100644 index 0000000..202e2eb --- /dev/null +++ b/bean/umc10th/src/main/java/com/example/umc10th/global/apiPayload/code/status/ErrorStatus.java @@ -0,0 +1,29 @@ +package com.example.umc10th.global.apiPayload.code.status; + +import com.example.umc10th.global.apiPayload.code.BaseErrorCode; +import com.example.umc10th.global.apiPayload.code.dto.ReasonDto; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; + +@Getter +@RequiredArgsConstructor +public enum ErrorStatus implements BaseErrorCode { + INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "COMMON500", "Server error."), + BAD_REQUEST(HttpStatus.BAD_REQUEST, "COMMON400", "Bad request."), + VALIDATION_ERROR(HttpStatus.BAD_REQUEST, "COMMON4001", "Validation failed."); + + private final HttpStatus httpStatus; + private final String code; + private final String message; + + @Override + public ReasonDto getReason() { + return new ReasonDto(httpStatus, false, code, message); + } + + @Override + public ReasonDto getReasonHttpStatus() { + return getReason(); + } +} \ No newline at end of file diff --git a/bean/umc10th/src/main/java/com/example/umc10th/global/apiPayload/code/status/SuccessStatus.java b/bean/umc10th/src/main/java/com/example/umc10th/global/apiPayload/code/status/SuccessStatus.java new file mode 100644 index 0000000..75662f9 --- /dev/null +++ b/bean/umc10th/src/main/java/com/example/umc10th/global/apiPayload/code/status/SuccessStatus.java @@ -0,0 +1,27 @@ +package com.example.umc10th.global.apiPayload.code.status; + +import com.example.umc10th.global.apiPayload.code.BaseSuccessCode; +import com.example.umc10th.global.apiPayload.code.dto.ReasonDto; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; + +@Getter +@RequiredArgsConstructor +public enum SuccessStatus implements BaseSuccessCode { + OK(HttpStatus.OK, "COMMON200", "Success"); + + private final HttpStatus httpStatus; + private final String code; + private final String message; + + @Override + public ReasonDto getReason() { + return new ReasonDto(httpStatus, true, code, message); + } + + @Override + public ReasonDto getReasonHttpStatus() { + return getReason(); + } +} \ No newline at end of file diff --git a/bean/umc10th/src/main/java/com/example/umc10th/global/apiPayload/exception/GeneralException.java b/bean/umc10th/src/main/java/com/example/umc10th/global/apiPayload/exception/GeneralException.java new file mode 100644 index 0000000..2021214 --- /dev/null +++ b/bean/umc10th/src/main/java/com/example/umc10th/global/apiPayload/exception/GeneralException.java @@ -0,0 +1,21 @@ +package com.example.umc10th.global.apiPayload.exception; + +import com.example.umc10th.global.apiPayload.code.BaseErrorCode; +import com.example.umc10th.global.apiPayload.code.dto.ReasonDto; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public class GeneralException extends RuntimeException { + + private final BaseErrorCode code; + + public ReasonDto getErrorReason() { + return code.getReason(); + } + + public ReasonDto getErrorReasonHttpStatus() { + return code.getReasonHttpStatus(); + } +} \ No newline at end of file diff --git a/bean/umc10th/src/main/java/com/example/umc10th/global/apiPayload/exception/GeneralExceptionHandler.java b/bean/umc10th/src/main/java/com/example/umc10th/global/apiPayload/exception/GeneralExceptionHandler.java new file mode 100644 index 0000000..b997923 --- /dev/null +++ b/bean/umc10th/src/main/java/com/example/umc10th/global/apiPayload/exception/GeneralExceptionHandler.java @@ -0,0 +1,46 @@ +package com.example.umc10th.global.apiPayload.exception; + +import com.example.umc10th.global.apiPayload.ApiResponse; +import com.example.umc10th.global.apiPayload.code.dto.ReasonDto; +import com.example.umc10th.global.apiPayload.code.status.ErrorStatus; +import java.util.LinkedHashMap; +import java.util.Map; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.FieldError; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@RestControllerAdvice +public class GeneralExceptionHandler { + + @ExceptionHandler(GeneralException.class) + public ResponseEntity> handleGeneralException(GeneralException exception) { + ReasonDto reason = exception.getErrorReasonHttpStatus(); + return ResponseEntity + .status(reason.httpStatus()) + .body(ApiResponse.onFailure(reason.code(), reason.message(), null)); + } + + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity> handleValidationException(MethodArgumentNotValidException exception) { + Map errors = new LinkedHashMap<>(); + + for (FieldError fieldError : exception.getBindingResult().getFieldErrors()) { + errors.put(fieldError.getField(), fieldError.getDefaultMessage()); + } + + ErrorStatus status = ErrorStatus.VALIDATION_ERROR; + return ResponseEntity + .status(status.getHttpStatus()) + .body(ApiResponse.onFailure(status.getCode(), status.getMessage(), errors)); + } + + @ExceptionHandler(Exception.class) + public ResponseEntity> handleException(Exception exception) { + ErrorStatus status = ErrorStatus.INTERNAL_SERVER_ERROR; + return ResponseEntity + .status(status.getHttpStatus()) + .body(ApiResponse.onFailure(status.getCode(), status.getMessage(), exception.getMessage())); + } +} \ No newline at end of file From 66e7f9f0094b99900bdf59938bcd50d8bdd5bdd7 Mon Sep 17 00:00:00 2001 From: minju Date: Sun, 10 May 2026 23:57:26 +0900 Subject: [PATCH 02/10] =?UTF-8?q?feat:=206=EC=A3=BC=EC=B0=A8=20=EB=AF=B8?= =?UTF-8?q?=EC=85=98=20=EC=97=94=ED=8B=B0=ED=8B=B0=20=EB=A7=A4=ED=95=91=20?= =?UTF-8?q?=EB=B0=8F=20=EC=84=9C=EB=B9=84=EC=8A=A4=20=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../member/controller/MemberController.java | 21 +++---- .../member/converter/MemberConverter.java | 12 +++- .../exception/code/MemberErrorCode.java | 2 +- .../exception/code/MemberSuccessCode.java | 2 +- .../domain/member/service/MemberService.java | 4 +- .../member/service/MemberServiceImpl.java | 18 +++++- .../mission/controller/MissionController.java | 26 +++++++- .../mission/converter/MissionConverter.java | 60 ++++++++++++++++++- .../domain/mission/dto/MissionResDto.java | 44 +++++++++++++- .../exception/code/MissionErrorCode.java | 4 +- .../repository/MemberMissionRepository.java | 31 ++++++++++ .../mission/repository/MissionRepository.java | 26 ++++++++ .../mission/service/MissionService.java | 7 ++- .../mission/service/MissionServiceImpl.java | 31 +++++++++- .../exception/code/ReviewErrorCode.java | 6 +- .../store/exception/code/StoreErrorCode.java | 2 +- 16 files changed, 266 insertions(+), 30 deletions(-) diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java index 13c0241..06db769 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java @@ -2,11 +2,11 @@ import com.example.umc10th.domain.member.dto.MemberReqDto; import com.example.umc10th.domain.member.dto.MemberResDto; -import com.example.umc10th.domain.member.exception.MemberException; -import com.example.umc10th.domain.member.exception.code.MemberErrorCode; import com.example.umc10th.domain.member.exception.code.MemberSuccessCode; +import com.example.umc10th.domain.member.service.MemberService; import com.example.umc10th.global.apiPayload.ApiResponse; import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -15,24 +15,17 @@ @RestController @RequestMapping("/api/v1/users") +@RequiredArgsConstructor public class MemberController { + private final MemberService memberService; + @PostMapping("/me") public ResponseEntity> getMyPage( @Valid @RequestBody MemberReqDto.GetMyPageRequest request ) { - if (request.id() <= 0) { - throw new MemberException(MemberErrorCode.MEMBER_NOT_FOUND); - } - - MemberResDto.GetMyPageResponse result = new MemberResDto.GetMyPageResponse( - "bean", - "https://example.com/profile/bean.png", - "bean@umc.com", - null, - 2500 - ); + MemberResDto.GetMyPageResponse result = memberService.getMyPage(request.id()); return ResponseEntity.ok(ApiResponse.of(MemberSuccessCode.GET_MY_PAGE_SUCCESS, result)); } -} \ No newline at end of file +} diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/member/converter/MemberConverter.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/member/converter/MemberConverter.java index 479a2d4..edc3bf4 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/domain/member/converter/MemberConverter.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/member/converter/MemberConverter.java @@ -29,4 +29,14 @@ public static MemberResDto.MemberInfo toMemberInfo(Member member) { member.getNickname() ); } -} \ No newline at end of file + + public static MemberResDto.GetMyPageResponse toMyPageResponse(Member member, Integer point) { + return new MemberResDto.GetMyPageResponse( + member.getName(), + null, + null, + null, + point + ); + } +} diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/member/exception/code/MemberErrorCode.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/member/exception/code/MemberErrorCode.java index 371c66d..cc28733 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/domain/member/exception/code/MemberErrorCode.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/member/exception/code/MemberErrorCode.java @@ -9,7 +9,7 @@ @Getter @RequiredArgsConstructor public enum MemberErrorCode implements BaseErrorCode { - MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "MEMBER404", "사용자를 찾을 수 없습니다."); + MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "MEMBER404", "User not found."); private final HttpStatus httpStatus; private final String code; diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/member/exception/code/MemberSuccessCode.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/member/exception/code/MemberSuccessCode.java index cba0595..11a1c29 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/domain/member/exception/code/MemberSuccessCode.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/member/exception/code/MemberSuccessCode.java @@ -9,7 +9,7 @@ @Getter @RequiredArgsConstructor public enum MemberSuccessCode implements BaseSuccessCode { - GET_MY_PAGE_SUCCESS(HttpStatus.OK, "MEMBER2001", "성공적으로 사용자 정보를 조회했습니다."); + GET_MY_PAGE_SUCCESS(HttpStatus.OK, "MEMBER2001", "Successfully retrieved user information."); private final HttpStatus httpStatus; private final String code; diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/member/service/MemberService.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/member/service/MemberService.java index 86dbcad..970c6a8 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/domain/member/service/MemberService.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/member/service/MemberService.java @@ -7,4 +7,6 @@ public interface MemberService { MemberResDto.MemberInfo getMember(Long memberId); MemberResDto.MemberInfo createMember(MemberReqDto.Create request); -} \ No newline at end of file + + MemberResDto.GetMyPageResponse getMyPage(Long memberId); +} diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/member/service/MemberServiceImpl.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/member/service/MemberServiceImpl.java index 76794fe..552b8ea 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/domain/member/service/MemberServiceImpl.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/member/service/MemberServiceImpl.java @@ -7,6 +7,8 @@ import com.example.umc10th.domain.member.exception.MemberException; import com.example.umc10th.domain.member.exception.code.MemberErrorCode; import com.example.umc10th.domain.member.repository.MemberRepository; +import com.example.umc10th.domain.mission.enums.MemberMissionStatus; +import com.example.umc10th.domain.mission.repository.MemberMissionRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -17,6 +19,7 @@ public class MemberServiceImpl implements MemberService { private final MemberRepository memberRepository; + private final MemberMissionRepository memberMissionRepository; @Override public MemberResDto.MemberInfo getMember(Long memberId) { @@ -32,4 +35,17 @@ public MemberResDto.MemberInfo createMember(MemberReqDto.Create request) { Member savedMember = memberRepository.save(MemberConverter.toEntity(request)); return MemberConverter.toMemberInfo(savedMember); } -} \ No newline at end of file + + @Override + public MemberResDto.GetMyPageResponse getMyPage(Long memberId) { + Member member = memberRepository.findById(memberId) + .orElseThrow(() -> new MemberException(MemberErrorCode.MEMBER_NOT_FOUND)); + + Integer totalPoint = memberMissionRepository.sumRewardPointByMemberIdAndStatus( + memberId, + MemberMissionStatus.COMPLETE + ); + + return MemberConverter.toMyPageResponse(member, totalPoint); + } +} diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/controller/MissionController.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/controller/MissionController.java index 79c6c87..6774cc3 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/controller/MissionController.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/controller/MissionController.java @@ -2,21 +2,27 @@ import com.example.umc10th.domain.mission.dto.MissionReqDto; import com.example.umc10th.domain.mission.dto.MissionResDto; +import com.example.umc10th.domain.mission.enums.MemberMissionStatus; import com.example.umc10th.domain.mission.service.MissionService; import com.example.umc10th.global.apiPayload.ApiResponse; import jakarta.validation.Valid; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController -@RequestMapping("/api/missions") +@RequestMapping("/api/v1/missions") @RequiredArgsConstructor +@Validated public class MissionController { private final MissionService missionService; @@ -30,4 +36,22 @@ public ResponseEntity> getMission(@PathVa public ResponseEntity> createMission(@Valid @RequestBody MissionReqDto.Create request) { return ResponseEntity.ok(ApiResponse.onSuccess(missionService.createMission(request))); } + + @GetMapping("/members/{memberId}") + public ResponseEntity> getMemberMissions( + @PathVariable Long memberId, + @RequestParam MemberMissionStatus status, + @RequestParam(defaultValue = "1") @Min(1) Integer page + ) { + return ResponseEntity.ok(ApiResponse.onSuccess(missionService.getMemberMissions(memberId, status, page))); + } + + @GetMapping("/home") + public ResponseEntity> getHomeMissions( + @RequestParam @NotNull Long regionId, + @RequestParam(required = false) Long memberId, + @RequestParam(defaultValue = "1") @Min(1) Integer page + ) { + return ResponseEntity.ok(ApiResponse.onSuccess(missionService.getHomeMissions(regionId, memberId, page))); + } } diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/converter/MissionConverter.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/converter/MissionConverter.java index c3c7f68..4baf8a4 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/converter/MissionConverter.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/converter/MissionConverter.java @@ -2,8 +2,11 @@ import com.example.umc10th.domain.mission.dto.MissionReqDto; import com.example.umc10th.domain.mission.dto.MissionResDto; +import com.example.umc10th.domain.mission.entity.MemberMission; import com.example.umc10th.domain.mission.entity.Mission; +import org.springframework.data.domain.Page; import com.example.umc10th.domain.store.entity.Store; +import java.util.List; public final class MissionConverter { @@ -26,4 +29,59 @@ public static MissionResDto.MissionInfo toMissionInfo(Mission mission) { mission.getRewardPoint() ); } -} \ No newline at end of file + + public static MissionResDto.MemberMissionPageItem toMemberMissionPageItem(MemberMission memberMission) { + Mission mission = memberMission.getMission(); + + return new MissionResDto.MemberMissionPageItem( + memberMission.getId(), + mission.getId(), + mission.getStore().getName(), + mission.getContent(), + mission.getRewardPoint(), + memberMission.getStatus() + ); + } + + public static MissionResDto.HomeMissionPageItem toHomeMissionPageItem(Mission mission) { + return new MissionResDto.HomeMissionPageItem( + mission.getId(), + mission.getStore().getName(), + mission.getContent(), + mission.getRewardPoint(), + mission.getStore().getScore() + ); + } + + public static MissionResDto.MemberMissionPageResponse toMemberMissionPageResponse(Page page) { + List missions = page.getContent() + .stream() + .map(MissionConverter::toMemberMissionPageItem) + .toList(); + + return new MissionResDto.MemberMissionPageResponse( + missions, + missions.size(), + page.getTotalPages(), + page.getTotalElements(), + page.isFirst(), + page.isLast() + ); + } + + public static MissionResDto.HomeMissionPageResponse toHomeMissionPageResponse(Page page) { + List missions = page.getContent() + .stream() + .map(MissionConverter::toHomeMissionPageItem) + .toList(); + + return new MissionResDto.HomeMissionPageResponse( + missions, + missions.size(), + page.getTotalPages(), + page.getTotalElements(), + page.isFirst(), + page.isLast() + ); + } +} diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/dto/MissionResDto.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/dto/MissionResDto.java index fc37061..dce8553 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/dto/MissionResDto.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/dto/MissionResDto.java @@ -1,5 +1,8 @@ package com.example.umc10th.domain.mission.dto; +import com.example.umc10th.domain.mission.enums.MemberMissionStatus; +import java.util.List; + public class MissionResDto { public record MissionInfo( @@ -9,4 +12,43 @@ public record MissionInfo( int rewardPoint ) { } -} \ No newline at end of file + + public record MemberMissionPageItem( + Long memberMissionId, + Long missionId, + String storeName, + String missionContent, + Integer rewardPoint, + MemberMissionStatus status + ) { + } + + public record HomeMissionPageItem( + Long missionId, + String storeName, + String missionContent, + Integer rewardPoint, + Float storeScore + ) { + } + + public record MemberMissionPageResponse( + List missions, + Integer listSize, + Integer totalPage, + Long totalElements, + Boolean isFirst, + Boolean isLast + ) { + } + + public record HomeMissionPageResponse( + List missions, + Integer listSize, + Integer totalPage, + Long totalElements, + Boolean isFirst, + Boolean isLast + ) { + } +} diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/exception/code/MissionErrorCode.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/exception/code/MissionErrorCode.java index 120f51f..f3087ba 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/exception/code/MissionErrorCode.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/exception/code/MissionErrorCode.java @@ -9,8 +9,8 @@ @Getter @RequiredArgsConstructor public enum MissionErrorCode implements BaseErrorCode { - MISSION_NOT_FOUND(HttpStatus.NOT_FOUND, "MISSION404", "미션을 찾을 수 없습니다."), - STORE_NOT_FOUND(HttpStatus.NOT_FOUND, "STORE404", "가게를 찾을 수 없습니다."); + MISSION_NOT_FOUND(HttpStatus.NOT_FOUND, "MISSION404", "Mission not found."), + STORE_NOT_FOUND(HttpStatus.NOT_FOUND, "STORE404", "Store not found."); private final HttpStatus httpStatus; private final String code; diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/repository/MemberMissionRepository.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/repository/MemberMissionRepository.java index d9d5164..28bd763 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/repository/MemberMissionRepository.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/repository/MemberMissionRepository.java @@ -1,7 +1,38 @@ package com.example.umc10th.domain.mission.repository; import com.example.umc10th.domain.mission.entity.MemberMission; +import com.example.umc10th.domain.mission.enums.MemberMissionStatus; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.EntityGraph; 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 { + + @EntityGraph(attributePaths = {"mission", "mission.store"}) + @Query(""" + select mm + from MemberMission mm + where mm.member.id = :memberId + and mm.status = :status + order by mm.id desc + """) + Page findMemberMissionsByStatus( + @Param("memberId") Long memberId, + @Param("status") MemberMissionStatus status, + Pageable pageable + ); + + @Query(""" + select coalesce(sum(mm.mission.rewardPoint), 0) + from MemberMission mm + where mm.member.id = :memberId + and mm.status = :status + """) + Integer sumRewardPointByMemberIdAndStatus( + @Param("memberId") Long memberId, + @Param("status") MemberMissionStatus status + ); } diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/repository/MissionRepository.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/repository/MissionRepository.java index 71f7528..73cdabf 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/repository/MissionRepository.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/repository/MissionRepository.java @@ -1,7 +1,33 @@ 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.EntityGraph; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; public interface MissionRepository extends JpaRepository { + + @EntityGraph(attributePaths = {"store"}) + @Query(""" + select m + from Mission m + where m.store.region.id = :regionId + and ( + :memberId is null + or m.id not in ( + select mm.mission.id + from MemberMission mm + where mm.member.id = :memberId + ) + ) + order by m.id desc + """) + Page findChallengeableMissionsByRegion( + @Param("regionId") Long regionId, + @Param("memberId") Long memberId, + Pageable pageable + ); } diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/service/MissionService.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/service/MissionService.java index 594e659..a4d76d9 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/service/MissionService.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/service/MissionService.java @@ -2,9 +2,14 @@ import com.example.umc10th.domain.mission.dto.MissionReqDto; import com.example.umc10th.domain.mission.dto.MissionResDto; +import com.example.umc10th.domain.mission.enums.MemberMissionStatus; public interface MissionService { MissionResDto.MissionInfo getMission(Long missionId); MissionResDto.MissionInfo createMission(MissionReqDto.Create request); -} \ No newline at end of file + + MissionResDto.MemberMissionPageResponse getMemberMissions(Long memberId, MemberMissionStatus status, Integer page); + + MissionResDto.HomeMissionPageResponse getHomeMissions(Long regionId, Long memberId, Integer page); +} diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/service/MissionServiceImpl.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/service/MissionServiceImpl.java index 42873ba..602cee6 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/service/MissionServiceImpl.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/service/MissionServiceImpl.java @@ -3,13 +3,18 @@ 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.MemberMission; import com.example.umc10th.domain.mission.entity.Mission; +import com.example.umc10th.domain.mission.enums.MemberMissionStatus; import com.example.umc10th.domain.store.entity.Store; import com.example.umc10th.domain.mission.exception.MissionException; import com.example.umc10th.domain.mission.exception.code.MissionErrorCode; +import com.example.umc10th.domain.mission.repository.MemberMissionRepository; import com.example.umc10th.domain.mission.repository.MissionRepository; import com.example.umc10th.domain.store.repository.StoreRepository; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -19,6 +24,7 @@ public class MissionServiceImpl implements MissionService { private final MissionRepository missionRepository; + private final MemberMissionRepository memberMissionRepository; private final StoreRepository storeRepository; @Override @@ -38,4 +44,27 @@ public MissionResDto.MissionInfo createMission(MissionReqDto.Create request) { Mission savedMission = missionRepository.save(MissionConverter.toEntity(request, store)); return MissionConverter.toMissionInfo(savedMission); } -} \ No newline at end of file + + @Override + public MissionResDto.MemberMissionPageResponse getMemberMissions(Long memberId, MemberMissionStatus status, Integer page) { + int pageNumber = Math.max(page, 1) - 1; + PageRequest pageRequest = PageRequest.of(pageNumber, 10); + + Page memberMissionPage = memberMissionRepository.findMemberMissionsByStatus( + memberId, + status, + pageRequest + ); + + return MissionConverter.toMemberMissionPageResponse(memberMissionPage); + } + + @Override + public MissionResDto.HomeMissionPageResponse getHomeMissions(Long regionId, Long memberId, Integer page) { + int pageNumber = Math.max(page, 1) - 1; + PageRequest pageRequest = PageRequest.of(pageNumber, 10); + + Page missionPage = missionRepository.findChallengeableMissionsByRegion(regionId, memberId, pageRequest); + return MissionConverter.toHomeMissionPageResponse(missionPage); + } +} diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/review/exception/code/ReviewErrorCode.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/review/exception/code/ReviewErrorCode.java index b435640..115b466 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/domain/review/exception/code/ReviewErrorCode.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/review/exception/code/ReviewErrorCode.java @@ -9,9 +9,9 @@ @Getter @RequiredArgsConstructor public enum ReviewErrorCode implements BaseErrorCode { - REVIEW_NOT_FOUND(HttpStatus.NOT_FOUND, "REVIEW404", "리뷰를 찾을 수 없습니다."), - MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "MEMBER404", "사용자를 찾을 수 없습니다."), - STORE_NOT_FOUND(HttpStatus.NOT_FOUND, "STORE404", "가게를 찾을 수 없습니다."); + REVIEW_NOT_FOUND(HttpStatus.NOT_FOUND, "REVIEW404", "Review not found."), + MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "MEMBER404", "User not found."), + STORE_NOT_FOUND(HttpStatus.NOT_FOUND, "STORE404", "Store not found."); private final HttpStatus httpStatus; private final String code; diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/store/exception/code/StoreErrorCode.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/store/exception/code/StoreErrorCode.java index 87e78c3..bcac3ca 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/domain/store/exception/code/StoreErrorCode.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/store/exception/code/StoreErrorCode.java @@ -9,7 +9,7 @@ @Getter @RequiredArgsConstructor public enum StoreErrorCode implements BaseErrorCode { - STORE_NOT_FOUND(HttpStatus.NOT_FOUND, "STORE404", "가게를 찾을 수 없습니다."); + STORE_NOT_FOUND(HttpStatus.NOT_FOUND, "STORE404", "Store not found."); private final HttpStatus httpStatus; private final String code; From 94ac033c14ddee0d5b9dc7b8035d12a12d3401be Mon Sep 17 00:00:00 2001 From: minju Date: Wed, 13 May 2026 16:43:01 +0900 Subject: [PATCH 03/10] =?UTF-8?q?feat:=207=EC=A3=BC=EC=B0=A8=20=EB=AF=B8?= =?UTF-8?q?=EC=85=98=20=EC=98=A4=ED=94=84=EC=85=8B=20=EB=B0=8F=20=EC=BB=A4?= =?UTF-8?q?=EC=84=9C=20=ED=8E=98=EC=9D=B4=EC=A7=95=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mission/controller/MissionController.java | 7 ++ .../domain/mission/dto/MissionReqDto.java | 10 ++- .../mission/service/MissionService.java | 2 + .../mission/service/MissionServiceImpl.java | 14 ++++ .../review/controller/ReviewController.java | 14 ++++ .../review/converter/ReviewConverter.java | 30 +++++++- .../domain/review/dto/ReviewReqDto.java | 18 ++++- .../domain/review/dto/ReviewResDto.java | 28 +++++++- .../exception/code/ReviewErrorCode.java | 5 +- .../review/repository/ReviewRepository.java | 60 +++++++++++++++- .../domain/review/service/ReviewService.java | 6 +- .../review/service/ReviewServiceImpl.java | 69 ++++++++++++++++++- ...ndler.java => GeneralExceptionAdvice.java} | 24 ++++++- 13 files changed, 275 insertions(+), 12 deletions(-) rename bean/umc10th/src/main/java/com/example/umc10th/global/apiPayload/exception/{GeneralExceptionHandler.java => GeneralExceptionAdvice.java} (62%) diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/controller/MissionController.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/controller/MissionController.java index 6774cc3..3f7649e 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/controller/MissionController.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/controller/MissionController.java @@ -37,6 +37,13 @@ public ResponseEntity> createMission(@Val return ResponseEntity.ok(ApiResponse.onSuccess(missionService.createMission(request))); } + @PostMapping("/my/challenging") + public ResponseEntity> getMyChallengingMissions( + @Valid @RequestBody MissionReqDto.MyChallengingMissionsRequest request + ) { + return ResponseEntity.ok(ApiResponse.onSuccess(missionService.getMyChallengingMissions(request))); + } + @GetMapping("/members/{memberId}") public ResponseEntity> getMemberMissions( @PathVariable Long memberId, diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/dto/MissionReqDto.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/dto/MissionReqDto.java index 5c39322..73c0fd4 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/dto/MissionReqDto.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/dto/MissionReqDto.java @@ -3,6 +3,7 @@ import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Positive; public class MissionReqDto { @@ -12,4 +13,11 @@ public record Create( @Min(0) int rewardPoint ) { } -} \ No newline at end of file + + public record MyChallengingMissionsRequest( + @NotNull @Positive Long memberId, + @NotNull @Min(1) Integer page, + @NotNull @Min(1) Integer size + ) { + } +} diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/service/MissionService.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/service/MissionService.java index a4d76d9..dcf7c7f 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/service/MissionService.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/service/MissionService.java @@ -12,4 +12,6 @@ public interface MissionService { MissionResDto.MemberMissionPageResponse getMemberMissions(Long memberId, MemberMissionStatus status, Integer page); MissionResDto.HomeMissionPageResponse getHomeMissions(Long regionId, Long memberId, Integer page); + + MissionResDto.MemberMissionPageResponse getMyChallengingMissions(MissionReqDto.MyChallengingMissionsRequest request); } diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/service/MissionServiceImpl.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/service/MissionServiceImpl.java index 602cee6..a22361f 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/service/MissionServiceImpl.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/service/MissionServiceImpl.java @@ -67,4 +67,18 @@ public MissionResDto.HomeMissionPageResponse getHomeMissions(Long regionId, Long Page missionPage = missionRepository.findChallengeableMissionsByRegion(regionId, memberId, pageRequest); return MissionConverter.toHomeMissionPageResponse(missionPage); } + + @Override + public MissionResDto.MemberMissionPageResponse getMyChallengingMissions(MissionReqDto.MyChallengingMissionsRequest request) { + int pageNumber = request.page() - 1; + PageRequest pageRequest = PageRequest.of(pageNumber, request.size()); + + Page memberMissionPage = memberMissionRepository.findMemberMissionsByStatus( + request.memberId(), + MemberMissionStatus.CHALLENGING, + pageRequest + ); + + return MissionConverter.toMemberMissionPageResponse(memberMissionPage); + } } diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/review/controller/ReviewController.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/review/controller/ReviewController.java index 57cfa3e..033d245 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/domain/review/controller/ReviewController.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/review/controller/ReviewController.java @@ -30,4 +30,18 @@ public ResponseEntity> getReview(@PathVaria public ResponseEntity> createReview(@Valid @RequestBody ReviewReqDto.Create request) { return ResponseEntity.ok(ApiResponse.onSuccess(reviewService.createReview(request))); } + + @PostMapping("/my/id-cursor") + public ResponseEntity> getMyReviewsByIdCursor( + @Valid @RequestBody ReviewReqDto.MyReviewsByIdCursorRequest request + ) { + return ResponseEntity.ok(ApiResponse.onSuccess(reviewService.getMyReviewsByIdCursor(request))); + } + + @PostMapping("/my/score-cursor") + public ResponseEntity> getMyReviewsByScoreCursor( + @Valid @RequestBody ReviewReqDto.MyReviewsByScoreCursorRequest request + ) { + return ResponseEntity.ok(ApiResponse.onSuccess(reviewService.getMyReviewsByScoreCursor(request))); + } } diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/review/converter/ReviewConverter.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/review/converter/ReviewConverter.java index 99f6080..528a334 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/domain/review/converter/ReviewConverter.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/review/converter/ReviewConverter.java @@ -5,6 +5,7 @@ import com.example.umc10th.domain.review.dto.ReviewReqDto; import com.example.umc10th.domain.review.dto.ReviewResDto; import com.example.umc10th.domain.review.entity.Review; +import java.util.List; public final class ReviewConverter { @@ -29,4 +30,31 @@ public static ReviewResDto.ReviewInfo toReviewInfo(Review review) { review.getScore() ); } -} \ No newline at end of file + + public static ReviewResDto.MyReviewItem toMyReviewItem(Review review) { + return new ReviewResDto.MyReviewItem( + review.getId(), + review.getStore().getId(), + review.getStore().getName(), + review.getContent(), + review.getScore() + ); + } + + public static ReviewResDto.MyReviewsByIdCursorResponse toMyReviewsByIdCursorResponse( + List reviews, + Long nextCursorId, + boolean hasNext + ) { + return new ReviewResDto.MyReviewsByIdCursorResponse(reviews, nextCursorId, hasNext); + } + + public static ReviewResDto.MyReviewsByScoreCursorResponse toMyReviewsByScoreCursorResponse( + List reviews, + Float nextCursorScore, + Long nextCursorId, + boolean hasNext + ) { + return new ReviewResDto.MyReviewsByScoreCursorResponse(reviews, nextCursorScore, nextCursorId, hasNext); + } +} diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/review/dto/ReviewReqDto.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/review/dto/ReviewReqDto.java index d9b49b7..45754f5 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/domain/review/dto/ReviewReqDto.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/review/dto/ReviewReqDto.java @@ -4,6 +4,7 @@ import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Positive; public class ReviewReqDto { @@ -14,4 +15,19 @@ public record Create( @Min(0) @Max(5) float score ) { } -} \ No newline at end of file + + public record MyReviewsByIdCursorRequest( + @NotNull @Positive Long memberId, + @Positive Long cursorId, + @NotNull @Min(1) @Max(50) Integer size + ) { + } + + public record MyReviewsByScoreCursorRequest( + @NotNull @Positive Long memberId, + @Min(0) @Max(5) Float cursorScore, + @Positive Long cursorId, + @NotNull @Min(1) @Max(50) Integer size + ) { + } +} diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/review/dto/ReviewResDto.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/review/dto/ReviewResDto.java index 5ca1e5d..1832470 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/domain/review/dto/ReviewResDto.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/review/dto/ReviewResDto.java @@ -1,5 +1,7 @@ package com.example.umc10th.domain.review.dto; +import java.util.List; + public class ReviewResDto { public record ReviewInfo( @@ -10,4 +12,28 @@ public record ReviewInfo( float score ) { } -} \ No newline at end of file + + public record MyReviewItem( + Long reviewId, + Long storeId, + String storeName, + String content, + Float score + ) { + } + + public record MyReviewsByIdCursorResponse( + List reviews, + Long nextCursorId, + Boolean hasNext + ) { + } + + public record MyReviewsByScoreCursorResponse( + List reviews, + Float nextCursorScore, + Long nextCursorId, + Boolean hasNext + ) { + } +} diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/review/exception/code/ReviewErrorCode.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/review/exception/code/ReviewErrorCode.java index 115b466..0f63a8b 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/domain/review/exception/code/ReviewErrorCode.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/review/exception/code/ReviewErrorCode.java @@ -11,7 +11,8 @@ public enum ReviewErrorCode implements BaseErrorCode { REVIEW_NOT_FOUND(HttpStatus.NOT_FOUND, "REVIEW404", "Review not found."), MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "MEMBER404", "User not found."), - STORE_NOT_FOUND(HttpStatus.NOT_FOUND, "STORE404", "Store not found."); + STORE_NOT_FOUND(HttpStatus.NOT_FOUND, "STORE404", "Store not found."), + INVALID_CURSOR(HttpStatus.BAD_REQUEST, "REVIEW4001", "Invalid cursor request."); private final HttpStatus httpStatus; private final String code; @@ -26,4 +27,4 @@ public ReasonDto getReason() { public ReasonDto getReasonHttpStatus() { return getReason(); } -} \ No newline at end of file +} diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/review/repository/ReviewRepository.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/review/repository/ReviewRepository.java index 73627b1..7f27ce1 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/domain/review/repository/ReviewRepository.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/review/repository/ReviewRepository.java @@ -1,7 +1,65 @@ package com.example.umc10th.domain.review.repository; import com.example.umc10th.domain.review.entity.Review; +import java.util.List; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.EntityGraph; 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 { -} \ No newline at end of file + + @EntityGraph(attributePaths = {"store"}) + @Query(""" + select r + from Review r + where r.member.id = :memberId + order by r.id desc + """) + List findMyReviewsByIdCursorFirst( + @Param("memberId") Long memberId, + Pageable pageable + ); + + @EntityGraph(attributePaths = {"store"}) + @Query(""" + select r + from Review r + where r.member.id = :memberId + and r.id < :cursorId + order by r.id desc + """) + List findMyReviewsByIdCursorNext( + @Param("memberId") Long memberId, + @Param("cursorId") Long cursorId, + Pageable pageable + ); + + @EntityGraph(attributePaths = {"store"}) + @Query(""" + select r + from Review r + where r.member.id = :memberId + order by r.score desc, r.id desc + """) + List findMyReviewsByScoreCursorFirst( + @Param("memberId") Long memberId, + Pageable pageable + ); + + @EntityGraph(attributePaths = {"store"}) + @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 + """) + List findMyReviewsByScoreCursorNext( + @Param("memberId") Long memberId, + @Param("cursorScore") Float cursorScore, + @Param("cursorId") Long cursorId, + Pageable pageable + ); +} diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/review/service/ReviewService.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/review/service/ReviewService.java index 0d29ce7..aab5d6e 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/domain/review/service/ReviewService.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/review/service/ReviewService.java @@ -7,4 +7,8 @@ public interface ReviewService { ReviewResDto.ReviewInfo getReview(Long reviewId); ReviewResDto.ReviewInfo createReview(ReviewReqDto.Create request); -} \ No newline at end of file + + ReviewResDto.MyReviewsByIdCursorResponse getMyReviewsByIdCursor(ReviewReqDto.MyReviewsByIdCursorRequest request); + + ReviewResDto.MyReviewsByScoreCursorResponse getMyReviewsByScoreCursor(ReviewReqDto.MyReviewsByScoreCursorRequest request); +} diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/review/service/ReviewServiceImpl.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/review/service/ReviewServiceImpl.java index 1cb21bb..60b2b8d 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/domain/review/service/ReviewServiceImpl.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/review/service/ReviewServiceImpl.java @@ -11,7 +11,9 @@ import com.example.umc10th.domain.review.exception.ReviewException; import com.example.umc10th.domain.review.exception.code.ReviewErrorCode; import com.example.umc10th.domain.review.repository.ReviewRepository; +import java.util.List; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.PageRequest; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -44,4 +46,69 @@ public ReviewResDto.ReviewInfo createReview(ReviewReqDto.Create request) { Review savedReview = reviewRepository.save(ReviewConverter.toEntity(request, member, store)); return ReviewConverter.toReviewInfo(savedReview); } -} \ No newline at end of file + + @Override + public ReviewResDto.MyReviewsByIdCursorResponse getMyReviewsByIdCursor(ReviewReqDto.MyReviewsByIdCursorRequest request) { + memberRepository.findById(request.memberId()) + .orElseThrow(() -> new ReviewException(ReviewErrorCode.MEMBER_NOT_FOUND)); + + int fetchSize = request.size() + 1; + PageRequest pageRequest = PageRequest.of(0, fetchSize); + + List reviews = request.cursorId() == null + ? reviewRepository.findMyReviewsByIdCursorFirst(request.memberId(), pageRequest) + : reviewRepository.findMyReviewsByIdCursorNext(request.memberId(), request.cursorId(), pageRequest); + + boolean hasNext = reviews.size() > request.size(); + List content = hasNext ? reviews.subList(0, request.size()) : reviews; + + List items = content.stream() + .map(ReviewConverter::toMyReviewItem) + .toList(); + + Long nextCursorId = hasNext ? content.get(content.size() - 1).getId() : null; + return ReviewConverter.toMyReviewsByIdCursorResponse(items, nextCursorId, hasNext); + } + + @Override + public ReviewResDto.MyReviewsByScoreCursorResponse getMyReviewsByScoreCursor(ReviewReqDto.MyReviewsByScoreCursorRequest request) { + memberRepository.findById(request.memberId()) + .orElseThrow(() -> new ReviewException(ReviewErrorCode.MEMBER_NOT_FOUND)); + + boolean bothCursorProvided = request.cursorScore() != null && request.cursorId() != null; + boolean bothCursorEmpty = request.cursorScore() == null && request.cursorId() == null; + if (!bothCursorProvided && !bothCursorEmpty) { + throw new ReviewException(ReviewErrorCode.INVALID_CURSOR); + } + + int fetchSize = request.size() + 1; + PageRequest pageRequest = PageRequest.of(0, fetchSize); + + List reviews = bothCursorEmpty + ? reviewRepository.findMyReviewsByScoreCursorFirst(request.memberId(), pageRequest) + : reviewRepository.findMyReviewsByScoreCursorNext( + request.memberId(), + request.cursorScore(), + request.cursorId(), + pageRequest + ); + + boolean hasNext = reviews.size() > request.size(); + List content = hasNext ? reviews.subList(0, request.size()) : reviews; + + List items = content.stream() + .map(ReviewConverter::toMyReviewItem) + .toList(); + + Float nextCursorScore = null; + Long nextCursorId = null; + + if (hasNext) { + Review lastReview = content.get(content.size() - 1); + nextCursorScore = lastReview.getScore(); + nextCursorId = lastReview.getId(); + } + + return ReviewConverter.toMyReviewsByScoreCursorResponse(items, nextCursorScore, nextCursorId, hasNext); + } +} diff --git a/bean/umc10th/src/main/java/com/example/umc10th/global/apiPayload/exception/GeneralExceptionHandler.java b/bean/umc10th/src/main/java/com/example/umc10th/global/apiPayload/exception/GeneralExceptionAdvice.java similarity index 62% rename from bean/umc10th/src/main/java/com/example/umc10th/global/apiPayload/exception/GeneralExceptionHandler.java rename to bean/umc10th/src/main/java/com/example/umc10th/global/apiPayload/exception/GeneralExceptionAdvice.java index b997923..9ff7c5a 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/global/apiPayload/exception/GeneralExceptionHandler.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/global/apiPayload/exception/GeneralExceptionAdvice.java @@ -3,16 +3,18 @@ import com.example.umc10th.global.apiPayload.ApiResponse; import com.example.umc10th.global.apiPayload.code.dto.ReasonDto; import com.example.umc10th.global.apiPayload.code.status.ErrorStatus; +import jakarta.validation.ConstraintViolationException; import java.util.LinkedHashMap; import java.util.Map; import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.validation.FieldError; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; @RestControllerAdvice -public class GeneralExceptionHandler { +public class GeneralExceptionAdvice { @ExceptionHandler(GeneralException.class) public ResponseEntity> handleGeneralException(GeneralException exception) { @@ -23,7 +25,7 @@ public ResponseEntity> handleGeneralException(GeneralExcepti } @ExceptionHandler(MethodArgumentNotValidException.class) - public ResponseEntity> handleValidationException(MethodArgumentNotValidException exception) { + public ResponseEntity> handleMethodArgumentNotValidException(MethodArgumentNotValidException exception) { Map errors = new LinkedHashMap<>(); for (FieldError fieldError : exception.getBindingResult().getFieldErrors()) { @@ -36,6 +38,22 @@ public ResponseEntity> handleValidationException(MethodArgum .body(ApiResponse.onFailure(status.getCode(), status.getMessage(), errors)); } + @ExceptionHandler(ConstraintViolationException.class) + public ResponseEntity> handleConstraintViolationException(ConstraintViolationException exception) { + ErrorStatus status = ErrorStatus.BAD_REQUEST; + return ResponseEntity + .status(status.getHttpStatus()) + .body(ApiResponse.onFailure(status.getCode(), status.getMessage(), exception.getMessage())); + } + + @ExceptionHandler(HttpMessageNotReadableException.class) + public ResponseEntity> handleHttpMessageNotReadableException(HttpMessageNotReadableException exception) { + ErrorStatus status = ErrorStatus.BAD_REQUEST; + return ResponseEntity + .status(status.getHttpStatus()) + .body(ApiResponse.onFailure(status.getCode(), status.getMessage(), "Request body is invalid.")); + } + @ExceptionHandler(Exception.class) public ResponseEntity> handleException(Exception exception) { ErrorStatus status = ErrorStatus.INTERNAL_SERVER_ERROR; @@ -43,4 +61,4 @@ public ResponseEntity> handleException(Exception exception) .status(status.getHttpStatus()) .body(ApiResponse.onFailure(status.getCode(), status.getMessage(), exception.getMessage())); } -} \ No newline at end of file +} From 03e17715b940eba45cc1f114a7c0dd350c435568 Mon Sep 17 00:00:00 2001 From: minju Date: Wed, 20 May 2026 17:36:38 +0900 Subject: [PATCH 04/10] =?UTF-8?q?feat:=20Spring=20Security=20=EB=B0=8F=20?= =?UTF-8?q?=ED=9A=8C=EC=9B=90=EA=B0=80=EC=9E=85=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bean/umc10th/build.gradle | 1 + .../member/controller/MemberController.java | 8 +++ .../member/converter/MemberConverter.java | 29 ++++++++++- .../domain/member/dto/MemberReqDto.java | 17 ++++++- .../domain/member/dto/MemberResDto.java | 11 +++- .../umc10th/domain/member/entity/Member.java | 8 ++- .../exception/code/MemberErrorCode.java | 6 ++- .../exception/code/MemberSuccessCode.java | 3 +- .../member/repository/MemberRepository.java | 6 ++- .../domain/member/service/MemberService.java | 2 + .../member/service/MemberServiceImpl.java | 39 ++++++++++++++ .../apiPayload/code/status/ErrorStatus.java | 6 ++- .../umc10th/global/config/SecurityConfig.java | 51 +++++++++++++++++++ .../umc10th/global/config/SwaggerConfig.java | 7 ++- .../security/CustomAccessDeniedHandler.java | 37 ++++++++++++++ .../CustomAuthenticationEntryPoint.java | 37 ++++++++++++++ .../security/CustomUserDetailsService.java | 32 ++++++++++++ 17 files changed, 286 insertions(+), 14 deletions(-) create mode 100644 bean/umc10th/src/main/java/com/example/umc10th/global/config/SecurityConfig.java create mode 100644 bean/umc10th/src/main/java/com/example/umc10th/global/security/CustomAccessDeniedHandler.java create mode 100644 bean/umc10th/src/main/java/com/example/umc10th/global/security/CustomAuthenticationEntryPoint.java create mode 100644 bean/umc10th/src/main/java/com/example/umc10th/global/security/CustomUserDetailsService.java diff --git a/bean/umc10th/build.gradle b/bean/umc10th/build.gradle index 43915b1..90fd4b8 100644 --- a/bean/umc10th/build.gradle +++ b/bean/umc10th/build.gradle @@ -14,6 +14,7 @@ repositories { dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-validation' compileOnly 'org.projectlombok:lombok' diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java index 06db769..ad87a8d 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java @@ -20,6 +20,14 @@ public class MemberController { private final MemberService memberService; + @PostMapping("/signup") + public ResponseEntity> signUp( + @Valid @RequestBody MemberReqDto.SignUpRequest request + ) { + MemberResDto.SignUpResponse result = memberService.signUp(request); + return ResponseEntity.ok(ApiResponse.of(MemberSuccessCode.SIGN_UP_SUCCESS, result)); + } + @PostMapping("/me") public ResponseEntity> getMyPage( @Valid @RequestBody MemberReqDto.GetMyPageRequest request diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/member/converter/MemberConverter.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/member/converter/MemberConverter.java index edc3bf4..ba46479 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/domain/member/converter/MemberConverter.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/member/converter/MemberConverter.java @@ -3,6 +3,8 @@ 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.store.entity.FoodCategory; +import java.util.List; public final class MemberConverter { @@ -19,6 +21,18 @@ public static Member toEntity(MemberReqDto.Create request) { .build(); } + public static Member toEntity(MemberReqDto.SignUpRequest request, String encodedPassword) { + return Member.builder() + .name(request.name()) + .gender(request.gender()) + .birthDate(request.birthDate()) + .address(request.address()) + .nickname(request.nickname()) + .email(request.email()) + .password(encodedPassword) + .build(); + } + public static MemberResDto.MemberInfo toMemberInfo(Member member) { return new MemberResDto.MemberInfo( member.getId(), @@ -34,9 +48,22 @@ public static MemberResDto.GetMyPageResponse toMyPageResponse(Member member, Int return new MemberResDto.GetMyPageResponse( member.getName(), null, - null, + member.getEmail(), null, point ); } + + public static MemberResDto.SignUpResponse toSignUpResponse(Member member, List categories) { + List preferCategoryIds = categories.stream() + .map(FoodCategory::getId) + .toList(); + + return new MemberResDto.SignUpResponse( + member.getId(), + member.getEmail(), + member.getNickname(), + preferCategoryIds + ); + } } diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/member/dto/MemberReqDto.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/member/dto/MemberReqDto.java index adfafa5..9ea545a 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/domain/member/dto/MemberReqDto.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/member/dto/MemberReqDto.java @@ -1,11 +1,14 @@ package com.example.umc10th.domain.member.dto; import com.example.umc10th.domain.member.enums.Gender; +import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Positive; import jakarta.validation.constraints.Size; import java.time.LocalDate; +import java.util.List; public class MemberReqDto { @@ -22,4 +25,16 @@ public record GetMyPageRequest( @NotNull @Positive Long id ) { } -} \ No newline at end of file + + public record SignUpRequest( + @NotBlank @Size(max = 20) String name, + @NotNull Gender gender, + @NotNull LocalDate birthDate, + @NotBlank @Size(max = 255) String address, + @NotBlank @Size(max = 20) String nickname, + @NotBlank @Email @Size(max = 100) String email, + @NotBlank @Size(min = 8, max = 30) String password, + @NotEmpty List<@NotNull @Positive Long> preferCategoryIds + ) { + } +} diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/member/dto/MemberResDto.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/member/dto/MemberResDto.java index 8ba3e08..d90a9d0 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/domain/member/dto/MemberResDto.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/member/dto/MemberResDto.java @@ -2,6 +2,7 @@ import com.example.umc10th.domain.member.enums.Gender; import java.time.LocalDate; +import java.util.List; public class MemberResDto { @@ -23,4 +24,12 @@ public record GetMyPageResponse( Integer point ) { } -} \ No newline at end of file + + public record SignUpResponse( + Long memberId, + String email, + String nickname, + List preferCategoryIds + ) { + } +} diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/member/entity/Member.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/member/entity/Member.java index 92e388f..1d8303d 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/domain/member/entity/Member.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/member/entity/Member.java @@ -53,6 +53,12 @@ public class Member { @Column(name = "nickname", nullable = false, length = 20) private String nickname; + @Column(name = "email", unique = true, length = 100) + private String email; + + @Column(name = "password", length = 255) + private String password; + @Column(name = "created_at", nullable = false) private LocalDateTime createdAt; @@ -85,4 +91,4 @@ protected void onCreate() { protected void onUpdate() { this.updatedAt = LocalDateTime.now(); } -} \ No newline at end of file +} diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/member/exception/code/MemberErrorCode.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/member/exception/code/MemberErrorCode.java index cc28733..b949c6b 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/domain/member/exception/code/MemberErrorCode.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/member/exception/code/MemberErrorCode.java @@ -9,7 +9,9 @@ @Getter @RequiredArgsConstructor public enum MemberErrorCode implements BaseErrorCode { - MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "MEMBER404", "User not found."); + MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "MEMBER404", "User not found."), + EMAIL_ALREADY_EXISTS(HttpStatus.BAD_REQUEST, "MEMBER4001", "Email already exists."), + FOOD_CATEGORY_NOT_FOUND(HttpStatus.BAD_REQUEST, "MEMBER4002", "Food category not found."); private final HttpStatus httpStatus; private final String code; @@ -24,4 +26,4 @@ public ReasonDto getReason() { public ReasonDto getReasonHttpStatus() { return getReason(); } -} \ No newline at end of file +} diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/member/exception/code/MemberSuccessCode.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/member/exception/code/MemberSuccessCode.java index 11a1c29..a6916d7 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/domain/member/exception/code/MemberSuccessCode.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/member/exception/code/MemberSuccessCode.java @@ -9,6 +9,7 @@ @Getter @RequiredArgsConstructor public enum MemberSuccessCode implements BaseSuccessCode { + SIGN_UP_SUCCESS(HttpStatus.OK, "MEMBER2000", "Sign-up completed successfully."), GET_MY_PAGE_SUCCESS(HttpStatus.OK, "MEMBER2001", "Successfully retrieved user information."); private final HttpStatus httpStatus; @@ -24,4 +25,4 @@ public ReasonDto getReason() { public ReasonDto getReasonHttpStatus() { return getReason(); } -} \ No newline at end of file +} diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/member/repository/MemberRepository.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/member/repository/MemberRepository.java index e878bbb..33d6a76 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/domain/member/repository/MemberRepository.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/member/repository/MemberRepository.java @@ -1,7 +1,11 @@ package com.example.umc10th.domain.member.repository; import com.example.umc10th.domain.member.entity.Member; +import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; public interface MemberRepository extends JpaRepository { -} \ No newline at end of file + Optional findByEmail(String email); + + boolean existsByEmail(String email); +} diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/member/service/MemberService.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/member/service/MemberService.java index 970c6a8..52148aa 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/domain/member/service/MemberService.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/member/service/MemberService.java @@ -9,4 +9,6 @@ public interface MemberService { MemberResDto.MemberInfo createMember(MemberReqDto.Create request); MemberResDto.GetMyPageResponse getMyPage(Long memberId); + + MemberResDto.SignUpResponse signUp(MemberReqDto.SignUpRequest request); } diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/member/service/MemberServiceImpl.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/member/service/MemberServiceImpl.java index 552b8ea..29e1a61 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/domain/member/service/MemberServiceImpl.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/member/service/MemberServiceImpl.java @@ -4,12 +4,18 @@ 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.entity.MemberPrefer; import com.example.umc10th.domain.member.exception.MemberException; import com.example.umc10th.domain.member.exception.code.MemberErrorCode; +import com.example.umc10th.domain.member.repository.MemberPreferRepository; import com.example.umc10th.domain.member.repository.MemberRepository; import com.example.umc10th.domain.mission.enums.MemberMissionStatus; import com.example.umc10th.domain.mission.repository.MemberMissionRepository; +import com.example.umc10th.domain.store.entity.FoodCategory; +import com.example.umc10th.domain.store.repository.FoodCategoryRepository; +import java.util.List; import lombok.RequiredArgsConstructor; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -20,6 +26,9 @@ public class MemberServiceImpl implements MemberService { private final MemberRepository memberRepository; private final MemberMissionRepository memberMissionRepository; + private final MemberPreferRepository memberPreferRepository; + private final FoodCategoryRepository foodCategoryRepository; + private final PasswordEncoder passwordEncoder; @Override public MemberResDto.MemberInfo getMember(Long memberId) { @@ -48,4 +57,34 @@ public MemberResDto.GetMyPageResponse getMyPage(Long memberId) { return MemberConverter.toMyPageResponse(member, totalPoint); } + + @Override + @Transactional + public MemberResDto.SignUpResponse signUp(MemberReqDto.SignUpRequest request) { + if (memberRepository.existsByEmail(request.email())) { + throw new MemberException(MemberErrorCode.EMAIL_ALREADY_EXISTS); + } + + List distinctCategoryIds = request.preferCategoryIds().stream() + .distinct() + .toList(); + + List categories = foodCategoryRepository.findAllById(distinctCategoryIds); + if (categories.size() != distinctCategoryIds.size()) { + throw new MemberException(MemberErrorCode.FOOD_CATEGORY_NOT_FOUND); + } + + String encodedPassword = passwordEncoder.encode(request.password()); + Member member = memberRepository.save(MemberConverter.toEntity(request, encodedPassword)); + + List memberPrefers = categories.stream() + .map(category -> MemberPrefer.builder() + .member(member) + .category(category) + .build()) + .toList(); + memberPreferRepository.saveAll(memberPrefers); + + return MemberConverter.toSignUpResponse(member, categories); + } } diff --git a/bean/umc10th/src/main/java/com/example/umc10th/global/apiPayload/code/status/ErrorStatus.java b/bean/umc10th/src/main/java/com/example/umc10th/global/apiPayload/code/status/ErrorStatus.java index 202e2eb..ccee76a 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/global/apiPayload/code/status/ErrorStatus.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/global/apiPayload/code/status/ErrorStatus.java @@ -11,7 +11,9 @@ public enum ErrorStatus implements BaseErrorCode { INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "COMMON500", "Server error."), BAD_REQUEST(HttpStatus.BAD_REQUEST, "COMMON400", "Bad request."), - VALIDATION_ERROR(HttpStatus.BAD_REQUEST, "COMMON4001", "Validation failed."); + VALIDATION_ERROR(HttpStatus.BAD_REQUEST, "COMMON4001", "Validation failed."), + UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "COMMON401", "Authentication is required."), + FORBIDDEN(HttpStatus.FORBIDDEN, "COMMON403", "Access is denied."); private final HttpStatus httpStatus; private final String code; @@ -26,4 +28,4 @@ public ReasonDto getReason() { public ReasonDto getReasonHttpStatus() { return getReason(); } -} \ No newline at end of file +} diff --git a/bean/umc10th/src/main/java/com/example/umc10th/global/config/SecurityConfig.java b/bean/umc10th/src/main/java/com/example/umc10th/global/config/SecurityConfig.java new file mode 100644 index 0000000..806b6f1 --- /dev/null +++ b/bean/umc10th/src/main/java/com/example/umc10th/global/config/SecurityConfig.java @@ -0,0 +1,51 @@ +package com.example.umc10th.global.config; + +import com.example.umc10th.global.security.CustomAccessDeniedHandler; +import com.example.umc10th.global.security.CustomAuthenticationEntryPoint; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; + +@Configuration +@RequiredArgsConstructor +public class SecurityConfig { + + private final CustomAuthenticationEntryPoint customAuthenticationEntryPoint; + private final CustomAccessDeniedHandler customAccessDeniedHandler; + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http + .csrf(AbstractHttpConfigurer::disable) + .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)) + .authorizeHttpRequests(auth -> auth + .requestMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll() + .requestMatchers("/api/v1/users/signup").permitAll() + .requestMatchers("/login", "/error").permitAll() + .anyRequest().authenticated() + ) + .formLogin(form -> form + .usernameParameter("email") + .passwordParameter("password") + ) + .httpBasic(Customizer.withDefaults()) + .exceptionHandling(exception -> exception + .authenticationEntryPoint(customAuthenticationEntryPoint) + .accessDeniedHandler(customAccessDeniedHandler) + ); + + return http.build(); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} diff --git a/bean/umc10th/src/main/java/com/example/umc10th/global/config/SwaggerConfig.java b/bean/umc10th/src/main/java/com/example/umc10th/global/config/SwaggerConfig.java index b0ab9b2..5c36f98 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/global/config/SwaggerConfig.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/global/config/SwaggerConfig.java @@ -16,15 +16,14 @@ public class SwaggerConfig { public OpenAPI swagger() { Info info = new Info().title("UMC10th").description("10th Swagger").version("0.0.1"); - String securityScheme = "JWT TOKEN"; + String securityScheme = "basicAuth"; SecurityRequirement securityRequirement = new SecurityRequirement().addList(securityScheme); Components components = new Components() .addSecuritySchemes(securityScheme, new SecurityScheme() .name(securityScheme) .type(SecurityScheme.Type.HTTP) - .scheme("Bearer") - .bearerFormat("JWT")); + .scheme("basic")); return new OpenAPI() .info(info) @@ -32,4 +31,4 @@ public OpenAPI swagger() { .addSecurityItem(securityRequirement) .components(components); } -} \ No newline at end of file +} diff --git a/bean/umc10th/src/main/java/com/example/umc10th/global/security/CustomAccessDeniedHandler.java b/bean/umc10th/src/main/java/com/example/umc10th/global/security/CustomAccessDeniedHandler.java new file mode 100644 index 0000000..ceb14c9 --- /dev/null +++ b/bean/umc10th/src/main/java/com/example/umc10th/global/security/CustomAccessDeniedHandler.java @@ -0,0 +1,37 @@ +package com.example.umc10th.global.security; + +import com.example.umc10th.global.apiPayload.ApiResponse; +import com.example.umc10th.global.apiPayload.code.status.ErrorStatus; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.http.MediaType; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +@Component +@RequiredArgsConstructor +public class CustomAccessDeniedHandler implements AccessDeniedHandler { + + private final ObjectMapper objectMapper; + + @Override + public void handle( + HttpServletRequest request, + HttpServletResponse response, + AccessDeniedException accessDeniedException + ) throws IOException { + ErrorStatus status = ErrorStatus.FORBIDDEN; + response.setStatus(status.getHttpStatus().value()); + response.setCharacterEncoding(StandardCharsets.UTF_8.name()); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.getWriter().write(objectMapper.writeValueAsString( + ApiResponse.onFailure(status.getCode(), status.getMessage(), null) + )); + } +} diff --git a/bean/umc10th/src/main/java/com/example/umc10th/global/security/CustomAuthenticationEntryPoint.java b/bean/umc10th/src/main/java/com/example/umc10th/global/security/CustomAuthenticationEntryPoint.java new file mode 100644 index 0000000..fbc5489 --- /dev/null +++ b/bean/umc10th/src/main/java/com/example/umc10th/global/security/CustomAuthenticationEntryPoint.java @@ -0,0 +1,37 @@ +package com.example.umc10th.global.security; + +import com.example.umc10th.global.apiPayload.ApiResponse; +import com.example.umc10th.global.apiPayload.code.status.ErrorStatus; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.http.MediaType; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +@Component +@RequiredArgsConstructor +public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint { + + private final ObjectMapper objectMapper; + + @Override + public void commence( + HttpServletRequest request, + HttpServletResponse response, + AuthenticationException authException + ) throws IOException { + ErrorStatus status = ErrorStatus.UNAUTHORIZED; + response.setStatus(status.getHttpStatus().value()); + response.setCharacterEncoding(StandardCharsets.UTF_8.name()); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.getWriter().write(objectMapper.writeValueAsString( + ApiResponse.onFailure(status.getCode(), status.getMessage(), null) + )); + } +} diff --git a/bean/umc10th/src/main/java/com/example/umc10th/global/security/CustomUserDetailsService.java b/bean/umc10th/src/main/java/com/example/umc10th/global/security/CustomUserDetailsService.java new file mode 100644 index 0000000..449144d --- /dev/null +++ b/bean/umc10th/src/main/java/com/example/umc10th/global/security/CustomUserDetailsService.java @@ -0,0 +1,32 @@ +package com.example.umc10th.global.security; + +import com.example.umc10th.domain.member.entity.Member; +import com.example.umc10th.domain.member.repository.MemberRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class CustomUserDetailsService implements UserDetailsService { + + private final MemberRepository memberRepository; + + @Override + public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { + Member member = memberRepository.findByEmail(email) + .orElseThrow(() -> new UsernameNotFoundException("User not found")); + + return new User( + member.getEmail(), + member.getPassword(), + List.of(new SimpleGrantedAuthority("ROLE_USER")) + ); + } +} From 3d42277a7e1e2e45a437402b83d6d836c5bc6696 Mon Sep 17 00:00:00 2001 From: minju Date: Wed, 27 May 2026 16:57:18 +0900 Subject: [PATCH 05/10] =?UTF-8?q?feat:=20JWT=20=EC=9D=B8=EC=A6=9D=20?= =?UTF-8?q?=EA=B8=B0=EB=B0=98=20=ED=9A=8C=EC=9B=90=20API=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../member/controller/MemberController.java | 26 +++- .../member/converter/MemberConverter.java | 30 +++- .../domain/member/dto/MemberReqDto.java | 6 + .../domain/member/dto/MemberResDto.java | 15 +- .../exception/code/MemberErrorCode.java | 3 +- .../exception/code/MemberSuccessCode.java | 1 + .../domain/member/service/MemberService.java | 5 + .../member/service/MemberServiceImpl.java | 25 +++- .../umc10th/global/config/SecurityConfig.java | 22 +-- .../umc10th/global/config/SwaggerConfig.java | 5 +- .../umc10th/global/security/AuthMember.java | 32 ++++ .../security/CustomUserDetailsService.java | 10 +- .../security/jwt/JwtAuthenticationFilter.java | 61 ++++++++ .../global/security/jwt/JwtTokenProvider.java | 138 ++++++++++++++++++ .../src/main/resources/application.yml | 6 +- 15 files changed, 351 insertions(+), 34 deletions(-) create mode 100644 bean/umc10th/src/main/java/com/example/umc10th/global/security/AuthMember.java create mode 100644 bean/umc10th/src/main/java/com/example/umc10th/global/security/jwt/JwtAuthenticationFilter.java create mode 100644 bean/umc10th/src/main/java/com/example/umc10th/global/security/jwt/JwtTokenProvider.java diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java index ad87a8d..925d836 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java @@ -5,22 +5,23 @@ import com.example.umc10th.domain.member.exception.code.MemberSuccessCode; import com.example.umc10th.domain.member.service.MemberService; import com.example.umc10th.global.apiPayload.ApiResponse; +import com.example.umc10th.global.security.AuthMember; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController -@RequestMapping("/api/v1/users") @RequiredArgsConstructor public class MemberController { private final MemberService memberService; - @PostMapping("/signup") + @PostMapping("/api/v1/users/signup") public ResponseEntity> signUp( @Valid @RequestBody MemberReqDto.SignUpRequest request ) { @@ -28,7 +29,15 @@ public ResponseEntity> signUp( return ResponseEntity.ok(ApiResponse.of(MemberSuccessCode.SIGN_UP_SUCCESS, result)); } - @PostMapping("/me") + @PostMapping("/api/v1/users/login") + public ResponseEntity> login( + @Valid @RequestBody MemberReqDto.LoginRequest request + ) { + MemberResDto.LoginResponse result = memberService.login(request); + return ResponseEntity.ok(ApiResponse.of(MemberSuccessCode.LOGIN_SUCCESS, result)); + } + + @PostMapping("/api/v1/users/me") public ResponseEntity> getMyPage( @Valid @RequestBody MemberReqDto.GetMyPageRequest request ) { @@ -36,4 +45,13 @@ public ResponseEntity> getMyPage( return ResponseEntity.ok(ApiResponse.of(MemberSuccessCode.GET_MY_PAGE_SUCCESS, result)); } + + @GetMapping("/api/v2/users/me") + public ResponseEntity> getMyPageV2( + @AuthenticationPrincipal AuthMember authMember + ) { + MemberResDto.GetMyPageResponse result = memberService.getMyPage(authMember.getMember()); + + return ResponseEntity.ok(ApiResponse.of(MemberSuccessCode.GET_MY_PAGE_SUCCESS, result)); + } } diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/member/converter/MemberConverter.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/member/converter/MemberConverter.java index ba46479..2833fa2 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/domain/member/converter/MemberConverter.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/member/converter/MemberConverter.java @@ -4,6 +4,7 @@ import com.example.umc10th.domain.member.dto.MemberResDto; import com.example.umc10th.domain.member.entity.Member; import com.example.umc10th.domain.store.entity.FoodCategory; +import com.example.umc10th.global.security.jwt.JwtTokenProvider; import java.util.List; public final class MemberConverter { @@ -47,14 +48,18 @@ public static MemberResDto.MemberInfo toMemberInfo(Member member) { public static MemberResDto.GetMyPageResponse toMyPageResponse(Member member, Integer point) { return new MemberResDto.GetMyPageResponse( member.getName(), - null, + "", member.getEmail(), null, - point + point == null ? 0 : point ); } - public static MemberResDto.SignUpResponse toSignUpResponse(Member member, List categories) { + public static MemberResDto.SignUpResponse toSignUpResponse( + Member member, + List categories, + JwtTokenProvider.TokenInfo tokenInfo + ) { List preferCategoryIds = categories.stream() .map(FoodCategory::getId) .toList(); @@ -63,7 +68,24 @@ public static MemberResDto.SignUpResponse toSignUpResponse(Member member, List preferCategoryIds ) { } + + public record LoginRequest( + @NotBlank @Email @Size(max = 100) String email, + @NotBlank String password + ) { + } } diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/member/dto/MemberResDto.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/member/dto/MemberResDto.java index d90a9d0..89c3fd6 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/domain/member/dto/MemberResDto.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/member/dto/MemberResDto.java @@ -29,7 +29,20 @@ public record SignUpResponse( Long memberId, String email, String nickname, - List preferCategoryIds + List preferCategoryIds, + String accessToken, + String tokenType, + Long expiresIn + ) { + } + + public record LoginResponse( + Long memberId, + String email, + String nickname, + String accessToken, + String tokenType, + Long expiresIn ) { } } diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/member/exception/code/MemberErrorCode.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/member/exception/code/MemberErrorCode.java index b949c6b..d7a40a8 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/domain/member/exception/code/MemberErrorCode.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/member/exception/code/MemberErrorCode.java @@ -11,7 +11,8 @@ public enum MemberErrorCode implements BaseErrorCode { MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "MEMBER404", "User not found."), EMAIL_ALREADY_EXISTS(HttpStatus.BAD_REQUEST, "MEMBER4001", "Email already exists."), - FOOD_CATEGORY_NOT_FOUND(HttpStatus.BAD_REQUEST, "MEMBER4002", "Food category not found."); + FOOD_CATEGORY_NOT_FOUND(HttpStatus.BAD_REQUEST, "MEMBER4002", "Food category not found."), + INVALID_PASSWORD(HttpStatus.UNAUTHORIZED, "MEMBER4010", "Password does not match."); private final HttpStatus httpStatus; private final String code; diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/member/exception/code/MemberSuccessCode.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/member/exception/code/MemberSuccessCode.java index a6916d7..7b04aad 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/domain/member/exception/code/MemberSuccessCode.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/member/exception/code/MemberSuccessCode.java @@ -10,6 +10,7 @@ @RequiredArgsConstructor public enum MemberSuccessCode implements BaseSuccessCode { SIGN_UP_SUCCESS(HttpStatus.OK, "MEMBER2000", "Sign-up completed successfully."), + LOGIN_SUCCESS(HttpStatus.OK, "MEMBER2002", "Login completed successfully."), GET_MY_PAGE_SUCCESS(HttpStatus.OK, "MEMBER2001", "Successfully retrieved user information."); private final HttpStatus httpStatus; diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/member/service/MemberService.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/member/service/MemberService.java index 52148aa..87ef9ce 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/domain/member/service/MemberService.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/member/service/MemberService.java @@ -2,6 +2,7 @@ import com.example.umc10th.domain.member.dto.MemberReqDto; import com.example.umc10th.domain.member.dto.MemberResDto; +import com.example.umc10th.domain.member.entity.Member; public interface MemberService { MemberResDto.MemberInfo getMember(Long memberId); @@ -10,5 +11,9 @@ public interface MemberService { MemberResDto.GetMyPageResponse getMyPage(Long memberId); + MemberResDto.GetMyPageResponse getMyPage(Member member); + MemberResDto.SignUpResponse signUp(MemberReqDto.SignUpRequest request); + + MemberResDto.LoginResponse login(MemberReqDto.LoginRequest request); } diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/member/service/MemberServiceImpl.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/member/service/MemberServiceImpl.java index 29e1a61..acab56b 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/domain/member/service/MemberServiceImpl.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/member/service/MemberServiceImpl.java @@ -13,6 +13,7 @@ import com.example.umc10th.domain.mission.repository.MemberMissionRepository; import com.example.umc10th.domain.store.entity.FoodCategory; import com.example.umc10th.domain.store.repository.FoodCategoryRepository; +import com.example.umc10th.global.security.jwt.JwtTokenProvider; import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.security.crypto.password.PasswordEncoder; @@ -29,6 +30,7 @@ public class MemberServiceImpl implements MemberService { private final MemberPreferRepository memberPreferRepository; private final FoodCategoryRepository foodCategoryRepository; private final PasswordEncoder passwordEncoder; + private final JwtTokenProvider jwtTokenProvider; @Override public MemberResDto.MemberInfo getMember(Long memberId) { @@ -50,8 +52,13 @@ public MemberResDto.GetMyPageResponse getMyPage(Long memberId) { Member member = memberRepository.findById(memberId) .orElseThrow(() -> new MemberException(MemberErrorCode.MEMBER_NOT_FOUND)); + return getMyPage(member); + } + + @Override + public MemberResDto.GetMyPageResponse getMyPage(Member member) { Integer totalPoint = memberMissionRepository.sumRewardPointByMemberIdAndStatus( - memberId, + member.getId(), MemberMissionStatus.COMPLETE ); @@ -85,6 +92,20 @@ public MemberResDto.SignUpResponse signUp(MemberReqDto.SignUpRequest request) { .toList(); memberPreferRepository.saveAll(memberPrefers); - return MemberConverter.toSignUpResponse(member, categories); + JwtTokenProvider.TokenInfo tokenInfo = jwtTokenProvider.generateAccessToken(member); + return MemberConverter.toSignUpResponse(member, categories, tokenInfo); + } + + @Override + public MemberResDto.LoginResponse login(MemberReqDto.LoginRequest request) { + Member member = memberRepository.findByEmail(request.email()) + .orElseThrow(() -> new MemberException(MemberErrorCode.MEMBER_NOT_FOUND)); + + if (!passwordEncoder.matches(request.password(), member.getPassword())) { + throw new MemberException(MemberErrorCode.INVALID_PASSWORD); + } + + JwtTokenProvider.TokenInfo tokenInfo = jwtTokenProvider.generateAccessToken(member); + return MemberConverter.toLoginResponse(member, tokenInfo); } } diff --git a/bean/umc10th/src/main/java/com/example/umc10th/global/config/SecurityConfig.java b/bean/umc10th/src/main/java/com/example/umc10th/global/config/SecurityConfig.java index 806b6f1..99e6af3 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/global/config/SecurityConfig.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/global/config/SecurityConfig.java @@ -2,16 +2,18 @@ import com.example.umc10th.global.security.CustomAccessDeniedHandler; import com.example.umc10th.global.security.CustomAuthenticationEntryPoint; +import com.example.umc10th.global.security.jwt.JwtAuthenticationFilter; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.security.config.Customizer; +import org.springframework.http.HttpMethod; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; @Configuration @RequiredArgsConstructor @@ -19,27 +21,27 @@ public class SecurityConfig { private final CustomAuthenticationEntryPoint customAuthenticationEntryPoint; private final CustomAccessDeniedHandler customAccessDeniedHandler; + private final JwtAuthenticationFilter jwtAuthenticationFilter; @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .csrf(AbstractHttpConfigurer::disable) - .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)) + // JWT는 매 요청의 Authorization 헤더로 인증하므로 서버 세션을 만들지 않는다. + .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(auth -> auth .requestMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll() - .requestMatchers("/api/v1/users/signup").permitAll() - .requestMatchers("/login", "/error").permitAll() + .requestMatchers(HttpMethod.POST, "/api/v1/users/signup", "/api/v1/users/login").permitAll() + .requestMatchers("/error").permitAll() .anyRequest().authenticated() ) - .formLogin(form -> form - .usernameParameter("email") - .passwordParameter("password") - ) - .httpBasic(Customizer.withDefaults()) + .formLogin(AbstractHttpConfigurer::disable) + .httpBasic(AbstractHttpConfigurer::disable) .exceptionHandling(exception -> exception .authenticationEntryPoint(customAuthenticationEntryPoint) .accessDeniedHandler(customAccessDeniedHandler) - ); + ) + .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); return http.build(); } diff --git a/bean/umc10th/src/main/java/com/example/umc10th/global/config/SwaggerConfig.java b/bean/umc10th/src/main/java/com/example/umc10th/global/config/SwaggerConfig.java index 5c36f98..e1bdb78 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/global/config/SwaggerConfig.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/global/config/SwaggerConfig.java @@ -16,14 +16,15 @@ public class SwaggerConfig { public OpenAPI swagger() { Info info = new Info().title("UMC10th").description("10th Swagger").version("0.0.1"); - String securityScheme = "basicAuth"; + String securityScheme = "bearerAuth"; SecurityRequirement securityRequirement = new SecurityRequirement().addList(securityScheme); Components components = new Components() .addSecuritySchemes(securityScheme, new SecurityScheme() .name(securityScheme) .type(SecurityScheme.Type.HTTP) - .scheme("basic")); + .scheme("bearer") + .bearerFormat("JWT")); return new OpenAPI() .info(info) diff --git a/bean/umc10th/src/main/java/com/example/umc10th/global/security/AuthMember.java b/bean/umc10th/src/main/java/com/example/umc10th/global/security/AuthMember.java new file mode 100644 index 0000000..392b3b1 --- /dev/null +++ b/bean/umc10th/src/main/java/com/example/umc10th/global/security/AuthMember.java @@ -0,0 +1,32 @@ +package com.example.umc10th.global.security; + +import com.example.umc10th.domain.member.entity.Member; +import java.util.Collection; +import java.util.List; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +@Getter +@RequiredArgsConstructor +public class AuthMember implements UserDetails { + + private final Member member; + + @Override + public Collection getAuthorities() { + return List.of(new SimpleGrantedAuthority("ROLE_USER")); + } + + @Override + public String getPassword() { + return member.getPassword(); + } + + @Override + public String getUsername() { + return member.getEmail(); + } +} diff --git a/bean/umc10th/src/main/java/com/example/umc10th/global/security/CustomUserDetailsService.java b/bean/umc10th/src/main/java/com/example/umc10th/global/security/CustomUserDetailsService.java index 449144d..555af30 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/global/security/CustomUserDetailsService.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/global/security/CustomUserDetailsService.java @@ -3,15 +3,11 @@ import com.example.umc10th.domain.member.entity.Member; import com.example.umc10th.domain.member.repository.MemberRepository; import lombok.RequiredArgsConstructor; -import org.springframework.security.core.authority.SimpleGrantedAuthority; -import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; -import java.util.List; - @Service @RequiredArgsConstructor public class CustomUserDetailsService implements UserDetailsService { @@ -23,10 +19,6 @@ public UserDetails loadUserByUsername(String email) throws UsernameNotFoundExcep Member member = memberRepository.findByEmail(email) .orElseThrow(() -> new UsernameNotFoundException("User not found")); - return new User( - member.getEmail(), - member.getPassword(), - List.of(new SimpleGrantedAuthority("ROLE_USER")) - ); + return new AuthMember(member); } } diff --git a/bean/umc10th/src/main/java/com/example/umc10th/global/security/jwt/JwtAuthenticationFilter.java b/bean/umc10th/src/main/java/com/example/umc10th/global/security/jwt/JwtAuthenticationFilter.java new file mode 100644 index 0000000..2f69269 --- /dev/null +++ b/bean/umc10th/src/main/java/com/example/umc10th/global/security/jwt/JwtAuthenticationFilter.java @@ -0,0 +1,61 @@ +package com.example.umc10th.global.security.jwt; + +import com.example.umc10th.global.security.CustomUserDetailsService; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import lombok.RequiredArgsConstructor; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; +import org.springframework.web.filter.OncePerRequestFilter; + +@Component +@RequiredArgsConstructor +public class JwtAuthenticationFilter extends OncePerRequestFilter { + + private static final String AUTHORIZATION_HEADER = "Authorization"; + private static final String BEARER_PREFIX = "Bearer "; + + private final JwtTokenProvider jwtTokenProvider; + private final CustomUserDetailsService customUserDetailsService; + + @Override + protected void doFilterInternal( + HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain + ) throws ServletException, IOException { + String token = resolveToken(request); + + if (StringUtils.hasText(token) + && jwtTokenProvider.validateToken(token) + && SecurityContextHolder.getContext().getAuthentication() == null) { + String email = jwtTokenProvider.getSubject(token); + UserDetails userDetails = customUserDetailsService.loadUserByUsername(email); + + UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( + userDetails, + null, + userDetails.getAuthorities() + ); + authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + SecurityContextHolder.getContext().setAuthentication(authentication); + } + + filterChain.doFilter(request, response); + } + + private String resolveToken(HttpServletRequest request) { + String bearerToken = request.getHeader(AUTHORIZATION_HEADER); + if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(BEARER_PREFIX)) { + return bearerToken.substring(BEARER_PREFIX.length()); + } + return null; + } +} diff --git a/bean/umc10th/src/main/java/com/example/umc10th/global/security/jwt/JwtTokenProvider.java b/bean/umc10th/src/main/java/com/example/umc10th/global/security/jwt/JwtTokenProvider.java new file mode 100644 index 0000000..6384f2d --- /dev/null +++ b/bean/umc10th/src/main/java/com/example/umc10th/global/security/jwt/JwtTokenProvider.java @@ -0,0 +1,138 @@ +package com.example.umc10th.global.security.jwt; + +import com.example.umc10th.domain.member.entity.Member; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.time.Instant; +import java.util.Base64; +import java.util.LinkedHashMap; +import java.util.Map; +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +/** + * 외부 라이브러리 없이 HMAC-SHA256 기반 JWT access token을 발급하고 검증합니다. + */ +@Component +@RequiredArgsConstructor +public class JwtTokenProvider { + + private static final String HMAC_ALGORITHM = "HmacSHA256"; + private static final String TOKEN_TYPE = "Bearer"; + + private final ObjectMapper objectMapper; + + @Value("${jwt.secret}") + private String secret; + + @Value("${jwt.access-token-expiration-millis}") + private long accessTokenExpirationMillis; + + /** + * 인증 성공 또는 회원가입 직후 클라이언트가 Authorization 헤더에 담아 사용할 토큰을 만듭니다. + */ + public TokenInfo generateAccessToken(Member member) { + Instant now = Instant.now(); + Instant expiresAt = now.plusMillis(accessTokenExpirationMillis); + + Map header = new LinkedHashMap<>(); + header.put("alg", "HS256"); + header.put("typ", "JWT"); + + Map payload = new LinkedHashMap<>(); + payload.put("sub", member.getEmail()); + payload.put("memberId", member.getId()); + payload.put("iat", now.getEpochSecond()); + payload.put("exp", expiresAt.getEpochSecond()); + + String unsignedToken = base64UrlEncode(writeJson(header)) + + "." + + base64UrlEncode(writeJson(payload)); + String signature = base64UrlEncode(sign(unsignedToken)); + + return new TokenInfo(unsignedToken + "." + signature, TOKEN_TYPE, accessTokenExpirationMillis); + } + + /** + * 서명과 만료 시간을 함께 검증해 필터에서 신뢰 가능한 토큰만 SecurityContext에 올립니다. + */ + public boolean validateToken(String token) { + try { + Map claims = parseAndVerify(token); + Number expiresAt = (Number) claims.get("exp"); + return expiresAt != null && Instant.now().getEpochSecond() < expiresAt.longValue(); + } catch (RuntimeException exception) { + return false; + } + } + + public String getSubject(String token) { + Object subject = parseAndVerify(token).get("sub"); + if (subject == null) { + throw new IllegalArgumentException("JWT subject is missing."); + } + return subject.toString(); + } + + private Map parseAndVerify(String token) { + String[] parts = token.split("\\."); + if (parts.length != 3) { + throw new IllegalArgumentException("JWT must have 3 parts."); + } + + String unsignedToken = parts[0] + "." + parts[1]; + byte[] expectedSignature = sign(unsignedToken); + byte[] actualSignature = base64UrlDecode(parts[2]); + if (!MessageDigest.isEqual(expectedSignature, actualSignature)) { + throw new IllegalArgumentException("JWT signature is invalid."); + } + + try { + return objectMapper.readValue( + base64UrlDecode(parts[1]), + new TypeReference>() { + } + ); + } catch (Exception exception) { + throw new IllegalArgumentException("JWT payload is invalid.", exception); + } + } + + private byte[] writeJson(Map value) { + try { + return objectMapper.writeValueAsBytes(value); + } catch (Exception exception) { + throw new IllegalStateException("Failed to write JWT json.", exception); + } + } + + private byte[] sign(String value) { + try { + Mac mac = Mac.getInstance(HMAC_ALGORITHM); + mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), HMAC_ALGORITHM)); + return mac.doFinal(value.getBytes(StandardCharsets.UTF_8)); + } catch (Exception exception) { + throw new IllegalStateException("Failed to sign JWT.", exception); + } + } + + private String base64UrlEncode(byte[] value) { + return Base64.getUrlEncoder().withoutPadding().encodeToString(value); + } + + private byte[] base64UrlDecode(String value) { + return Base64.getUrlDecoder().decode(value); + } + + public record TokenInfo( + String accessToken, + String tokenType, + Long expiresIn + ) { + } +} diff --git a/bean/umc10th/src/main/resources/application.yml b/bean/umc10th/src/main/resources/application.yml index d5eb749..562ba83 100644 --- a/bean/umc10th/src/main/resources/application.yml +++ b/bean/umc10th/src/main/resources/application.yml @@ -16,4 +16,8 @@ spring: ddl-auto: update properties: hibernate: - format_sql: true \ No newline at end of file + format_sql: true + +jwt: + secret: ${JWT_SECRET:umc10th-jwt-secret-key-for-local-development-needs-at-least-32-bytes} + access-token-expiration-millis: ${JWT_ACCESS_TOKEN_EXPIRATION_MILLIS:3600000} From 98b55be2986ad956c5dae72f1a2aa05fdd27dab9 Mon Sep 17 00:00:00 2001 From: minju Date: Wed, 27 May 2026 17:05:17 +0900 Subject: [PATCH 06/10] =?UTF-8?q?refactor:=20=EB=A6=AC=EB=B7=B0=20?= =?UTF-8?q?=ED=94=BC=EB=93=9C=EB=B0=B1=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../member/controller/MemberController.java | 12 ++++++++ .../domain/member/dto/MemberReqDto.java | 29 +++++++++++++++++++ .../exception/code/MemberErrorCode.java | 8 ++--- .../exception/code/MissionErrorCode.java | 5 ++-- .../mission/service/MissionServiceImpl.java | 6 ++-- .../exception/code/ReviewErrorCode.java | 6 ++-- .../review/service/ReviewServiceImpl.java | 16 ++++++---- .../store/exception/code/StoreErrorCode.java | 4 +-- 8 files changed, 65 insertions(+), 21 deletions(-) diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java index 925d836..dabe97a 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java @@ -6,6 +6,9 @@ import com.example.umc10th.domain.member.service.MemberService; import com.example.umc10th.global.apiPayload.ApiResponse; import com.example.umc10th.global.security.AuthMember; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; @@ -17,10 +20,12 @@ @RestController @RequiredArgsConstructor +@Tag(name = "Member", description = "회원 API") public class MemberController { private final MemberService memberService; + @Operation(summary = "회원가입", description = "회원 정보를 저장하고 JWT access token을 발급합니다.") @PostMapping("/api/v1/users/signup") public ResponseEntity> signUp( @Valid @RequestBody MemberReqDto.SignUpRequest request @@ -29,6 +34,7 @@ public ResponseEntity> signUp( return ResponseEntity.ok(ApiResponse.of(MemberSuccessCode.SIGN_UP_SUCCESS, result)); } + @Operation(summary = "로그인", description = "이메일과 비밀번호를 검증하고 JWT access token을 발급합니다.") @PostMapping("/api/v1/users/login") public ResponseEntity> login( @Valid @RequestBody MemberReqDto.LoginRequest request @@ -37,6 +43,7 @@ public ResponseEntity> login( return ResponseEntity.ok(ApiResponse.of(MemberSuccessCode.LOGIN_SUCCESS, result)); } + @Operation(summary = "마이페이지 조회(v1)", description = "요청 body의 회원 ID로 마이페이지 정보를 조회합니다.") @PostMapping("/api/v1/users/me") public ResponseEntity> getMyPage( @Valid @RequestBody MemberReqDto.GetMyPageRequest request @@ -46,6 +53,11 @@ public ResponseEntity> getMyPage( return ResponseEntity.ok(ApiResponse.of(MemberSuccessCode.GET_MY_PAGE_SUCCESS, result)); } + @Operation( + summary = "마이페이지 조회(v2)", + description = "Authorization 헤더의 Bearer 토큰으로 인증된 회원의 마이페이지 정보를 조회합니다.", + security = @SecurityRequirement(name = "bearerAuth") + ) @GetMapping("/api/v2/users/me") public ResponseEntity> getMyPageV2( @AuthenticationPrincipal AuthMember authMember diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/member/dto/MemberReqDto.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/member/dto/MemberReqDto.java index 0d237b2..e70f096 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/domain/member/dto/MemberReqDto.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/member/dto/MemberReqDto.java @@ -1,6 +1,7 @@ package com.example.umc10th.domain.member.dto; import com.example.umc10th.domain.member.enums.Gender; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotEmpty; @@ -13,33 +14,61 @@ public class MemberReqDto { public record Create( + @Schema(description = "회원 이름", example = "김철수") @NotBlank @Size(max = 20) String name, + + @Schema(description = "성별", example = "MALE", allowableValues = {"MALE", "FEMALE", "NONE"}) @NotNull Gender gender, + + @Schema(description = "생년월일", example = "2000-03-24") @NotNull LocalDate birthDate, + + @Schema(description = "주소", example = "서울특별시 성동구 왕십리로 222") @NotBlank @Size(max = 255) String address, + + @Schema(description = "닉네임", example = "bean") @NotBlank @Size(max = 20) String nickname ) { } public record GetMyPageRequest( + @Schema(description = "회원 ID", example = "1") @NotNull @Positive Long id ) { } public record SignUpRequest( + @Schema(description = "회원 이름", example = "김철수") @NotBlank @Size(max = 20) String name, + + @Schema(description = "성별", example = "MALE", allowableValues = {"MALE", "FEMALE", "NONE"}) @NotNull Gender gender, + + @Schema(description = "생년월일", example = "2000-03-24") @NotNull LocalDate birthDate, + + @Schema(description = "주소", example = "서울특별시 성동구 왕십리로 222") @NotBlank @Size(max = 255) String address, + + @Schema(description = "닉네임", example = "bean") @NotBlank @Size(max = 20) String nickname, + + @Schema(description = "이메일", example = "bean@example.com") @NotBlank @Email @Size(max = 100) String email, + + @Schema(description = "비밀번호", example = "password123") @NotBlank @Size(min = 8, max = 30) String password, + + @Schema(description = "선호 음식 카테고리 ID 목록", example = "[1, 2]") @NotEmpty List<@NotNull @Positive Long> preferCategoryIds ) { } public record LoginRequest( + @Schema(description = "이메일", example = "bean@example.com") @NotBlank @Email @Size(max = 100) String email, + + @Schema(description = "비밀번호", example = "password123") @NotBlank String password ) { } diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/member/exception/code/MemberErrorCode.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/member/exception/code/MemberErrorCode.java index d7a40a8..c0b8737 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/domain/member/exception/code/MemberErrorCode.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/member/exception/code/MemberErrorCode.java @@ -9,10 +9,10 @@ @Getter @RequiredArgsConstructor public enum MemberErrorCode implements BaseErrorCode { - MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "MEMBER404", "User not found."), - EMAIL_ALREADY_EXISTS(HttpStatus.BAD_REQUEST, "MEMBER4001", "Email already exists."), - FOOD_CATEGORY_NOT_FOUND(HttpStatus.BAD_REQUEST, "MEMBER4002", "Food category not found."), - INVALID_PASSWORD(HttpStatus.UNAUTHORIZED, "MEMBER4010", "Password does not match."); + MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "MEMBER404_1", "User not found."), + EMAIL_ALREADY_EXISTS(HttpStatus.BAD_REQUEST, "MEMBER400_1", "Email already exists."), + FOOD_CATEGORY_NOT_FOUND(HttpStatus.BAD_REQUEST, "MEMBER400_2", "Food category not found."), + INVALID_PASSWORD(HttpStatus.UNAUTHORIZED, "MEMBER401_1", "Password does not match."); private final HttpStatus httpStatus; private final String code; diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/exception/code/MissionErrorCode.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/exception/code/MissionErrorCode.java index f3087ba..2d4757d 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/exception/code/MissionErrorCode.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/exception/code/MissionErrorCode.java @@ -9,8 +9,7 @@ @Getter @RequiredArgsConstructor public enum MissionErrorCode implements BaseErrorCode { - MISSION_NOT_FOUND(HttpStatus.NOT_FOUND, "MISSION404", "Mission not found."), - STORE_NOT_FOUND(HttpStatus.NOT_FOUND, "STORE404", "Store not found."); + MISSION_NOT_FOUND(HttpStatus.NOT_FOUND, "MISSION404_1", "Mission not found."); private final HttpStatus httpStatus; private final String code; @@ -25,4 +24,4 @@ public ReasonDto getReason() { public ReasonDto getReasonHttpStatus() { return getReason(); } -} \ No newline at end of file +} diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/service/MissionServiceImpl.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/service/MissionServiceImpl.java index a22361f..ac2cce5 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/service/MissionServiceImpl.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/service/MissionServiceImpl.java @@ -6,11 +6,13 @@ import com.example.umc10th.domain.mission.entity.MemberMission; import com.example.umc10th.domain.mission.entity.Mission; import com.example.umc10th.domain.mission.enums.MemberMissionStatus; -import com.example.umc10th.domain.store.entity.Store; import com.example.umc10th.domain.mission.exception.MissionException; import com.example.umc10th.domain.mission.exception.code.MissionErrorCode; import com.example.umc10th.domain.mission.repository.MemberMissionRepository; 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 lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; @@ -39,7 +41,7 @@ public MissionResDto.MissionInfo getMission(Long missionId) { @Transactional public MissionResDto.MissionInfo createMission(MissionReqDto.Create request) { Store store = storeRepository.findById(request.storeId()) - .orElseThrow(() -> new MissionException(MissionErrorCode.STORE_NOT_FOUND)); + .orElseThrow(() -> new StoreException(StoreErrorCode.STORE_NOT_FOUND)); Mission savedMission = missionRepository.save(MissionConverter.toEntity(request, store)); return MissionConverter.toMissionInfo(savedMission); diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/review/exception/code/ReviewErrorCode.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/review/exception/code/ReviewErrorCode.java index 0f63a8b..9b6eb24 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/domain/review/exception/code/ReviewErrorCode.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/review/exception/code/ReviewErrorCode.java @@ -9,10 +9,8 @@ @Getter @RequiredArgsConstructor public enum ReviewErrorCode implements BaseErrorCode { - REVIEW_NOT_FOUND(HttpStatus.NOT_FOUND, "REVIEW404", "Review not found."), - MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "MEMBER404", "User not found."), - STORE_NOT_FOUND(HttpStatus.NOT_FOUND, "STORE404", "Store not found."), - INVALID_CURSOR(HttpStatus.BAD_REQUEST, "REVIEW4001", "Invalid cursor request."); + REVIEW_NOT_FOUND(HttpStatus.NOT_FOUND, "REVIEW404_1", "Review not found."), + INVALID_CURSOR(HttpStatus.BAD_REQUEST, "REVIEW400_1", "Invalid cursor request."); private final HttpStatus httpStatus; private final String code; diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/review/service/ReviewServiceImpl.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/review/service/ReviewServiceImpl.java index 60b2b8d..a4627e5 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/domain/review/service/ReviewServiceImpl.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/review/service/ReviewServiceImpl.java @@ -1,9 +1,9 @@ package com.example.umc10th.domain.review.service; import com.example.umc10th.domain.member.entity.Member; +import com.example.umc10th.domain.member.exception.MemberException; +import com.example.umc10th.domain.member.exception.code.MemberErrorCode; import com.example.umc10th.domain.member.repository.MemberRepository; -import com.example.umc10th.domain.store.entity.Store; -import com.example.umc10th.domain.store.repository.StoreRepository; import com.example.umc10th.domain.review.converter.ReviewConverter; import com.example.umc10th.domain.review.dto.ReviewReqDto; import com.example.umc10th.domain.review.dto.ReviewResDto; @@ -11,6 +11,10 @@ import com.example.umc10th.domain.review.exception.ReviewException; import com.example.umc10th.domain.review.exception.code.ReviewErrorCode; import com.example.umc10th.domain.review.repository.ReviewRepository; +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 java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.PageRequest; @@ -38,10 +42,10 @@ public ReviewResDto.ReviewInfo getReview(Long reviewId) { @Transactional public ReviewResDto.ReviewInfo createReview(ReviewReqDto.Create request) { Member member = memberRepository.findById(request.memberId()) - .orElseThrow(() -> new ReviewException(ReviewErrorCode.MEMBER_NOT_FOUND)); + .orElseThrow(() -> new MemberException(MemberErrorCode.MEMBER_NOT_FOUND)); Store store = storeRepository.findById(request.storeId()) - .orElseThrow(() -> new ReviewException(ReviewErrorCode.STORE_NOT_FOUND)); + .orElseThrow(() -> new StoreException(StoreErrorCode.STORE_NOT_FOUND)); Review savedReview = reviewRepository.save(ReviewConverter.toEntity(request, member, store)); return ReviewConverter.toReviewInfo(savedReview); @@ -50,7 +54,7 @@ public ReviewResDto.ReviewInfo createReview(ReviewReqDto.Create request) { @Override public ReviewResDto.MyReviewsByIdCursorResponse getMyReviewsByIdCursor(ReviewReqDto.MyReviewsByIdCursorRequest request) { memberRepository.findById(request.memberId()) - .orElseThrow(() -> new ReviewException(ReviewErrorCode.MEMBER_NOT_FOUND)); + .orElseThrow(() -> new MemberException(MemberErrorCode.MEMBER_NOT_FOUND)); int fetchSize = request.size() + 1; PageRequest pageRequest = PageRequest.of(0, fetchSize); @@ -73,7 +77,7 @@ public ReviewResDto.MyReviewsByIdCursorResponse getMyReviewsByIdCursor(ReviewReq @Override public ReviewResDto.MyReviewsByScoreCursorResponse getMyReviewsByScoreCursor(ReviewReqDto.MyReviewsByScoreCursorRequest request) { memberRepository.findById(request.memberId()) - .orElseThrow(() -> new ReviewException(ReviewErrorCode.MEMBER_NOT_FOUND)); + .orElseThrow(() -> new MemberException(MemberErrorCode.MEMBER_NOT_FOUND)); boolean bothCursorProvided = request.cursorScore() != null && request.cursorId() != null; boolean bothCursorEmpty = request.cursorScore() == null && request.cursorId() == null; diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/store/exception/code/StoreErrorCode.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/store/exception/code/StoreErrorCode.java index bcac3ca..3a7a23c 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/domain/store/exception/code/StoreErrorCode.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/store/exception/code/StoreErrorCode.java @@ -9,7 +9,7 @@ @Getter @RequiredArgsConstructor public enum StoreErrorCode implements BaseErrorCode { - STORE_NOT_FOUND(HttpStatus.NOT_FOUND, "STORE404", "Store not found."); + STORE_NOT_FOUND(HttpStatus.NOT_FOUND, "STORE404_1", "Store not found."); private final HttpStatus httpStatus; private final String code; @@ -24,4 +24,4 @@ public ReasonDto getReason() { public ReasonDto getReasonHttpStatus() { return getReason(); } -} \ No newline at end of file +} From 11ab2eed940b83ad31e92c828c3c408df19a2356 Mon Sep 17 00:00:00 2001 From: minju Date: Wed, 27 May 2026 17:18:40 +0900 Subject: [PATCH 07/10] =?UTF-8?q?refactor:=20=EB=AF=B8=EC=85=98=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=95=20=ED=94=BC=EB=93=9C=EB=B0=B1=20?= =?UTF-8?q?=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mission/controller/MissionController.java | 6 +-- .../mission/converter/MissionConverter.java | 43 +++++++++---------- .../domain/mission/dto/MissionResDto.java | 19 ++------ .../repository/MemberMissionRepository.java | 4 +- .../mission/repository/MissionRepository.java | 9 ++-- .../mission/service/MissionService.java | 16 +++++-- .../mission/service/MissionServiceImpl.java | 36 ++++++++++------ 7 files changed, 72 insertions(+), 61 deletions(-) diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/controller/MissionController.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/controller/MissionController.java index 3f7649e..d14d7f3 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/controller/MissionController.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/controller/MissionController.java @@ -38,14 +38,14 @@ public ResponseEntity> createMission(@Val } @PostMapping("/my/challenging") - public ResponseEntity> getMyChallengingMissions( + public ResponseEntity>> getMyChallengingMissions( @Valid @RequestBody MissionReqDto.MyChallengingMissionsRequest request ) { return ResponseEntity.ok(ApiResponse.onSuccess(missionService.getMyChallengingMissions(request))); } @GetMapping("/members/{memberId}") - public ResponseEntity> getMemberMissions( + public ResponseEntity>> getMemberMissions( @PathVariable Long memberId, @RequestParam MemberMissionStatus status, @RequestParam(defaultValue = "1") @Min(1) Integer page @@ -54,7 +54,7 @@ public ResponseEntity> getM } @GetMapping("/home") - public ResponseEntity> getHomeMissions( + public ResponseEntity>> getHomeMissions( @RequestParam @NotNull Long regionId, @RequestParam(required = false) Long memberId, @RequestParam(defaultValue = "1") @Min(1) Integer page diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/converter/MissionConverter.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/converter/MissionConverter.java index 4baf8a4..d4300da 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/converter/MissionConverter.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/converter/MissionConverter.java @@ -4,9 +4,10 @@ import com.example.umc10th.domain.mission.dto.MissionResDto; import com.example.umc10th.domain.mission.entity.MemberMission; import com.example.umc10th.domain.mission.entity.Mission; -import org.springframework.data.domain.Page; import com.example.umc10th.domain.store.entity.Store; import java.util.List; +import java.util.function.Function; +import org.springframework.data.domain.Slice; public final class MissionConverter { @@ -53,35 +54,33 @@ public static MissionResDto.HomeMissionPageItem toHomeMissionPageItem(Mission mi ); } - public static MissionResDto.MemberMissionPageResponse toMemberMissionPageResponse(Page page) { - List missions = page.getContent() - .stream() - .map(MissionConverter::toMemberMissionPageItem) - .toList(); + public static MissionResDto.MissionPageResponse toMemberMissionPageResponse( + Slice slice + ) { + return toMissionPageResponse(slice, MissionConverter::toMemberMissionPageItem); + } - return new MissionResDto.MemberMissionPageResponse( - missions, - missions.size(), - page.getTotalPages(), - page.getTotalElements(), - page.isFirst(), - page.isLast() - ); + public static MissionResDto.MissionPageResponse toHomeMissionPageResponse( + Slice slice + ) { + return toMissionPageResponse(slice, MissionConverter::toHomeMissionPageItem); } - public static MissionResDto.HomeMissionPageResponse toHomeMissionPageResponse(Page page) { - List missions = page.getContent() + private static MissionResDto.MissionPageResponse toMissionPageResponse( + Slice slice, + Function mapper + ) { + List missions = slice.getContent() .stream() - .map(MissionConverter::toHomeMissionPageItem) + .map(mapper) .toList(); - return new MissionResDto.HomeMissionPageResponse( + return new MissionResDto.MissionPageResponse<>( missions, missions.size(), - page.getTotalPages(), - page.getTotalElements(), - page.isFirst(), - page.isLast() + slice.isFirst(), + slice.isLast(), + slice.hasNext() ); } } diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/dto/MissionResDto.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/dto/MissionResDto.java index dce8553..228327b 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/dto/MissionResDto.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/dto/MissionResDto.java @@ -32,23 +32,12 @@ public record HomeMissionPageItem( ) { } - public record MemberMissionPageResponse( - List missions, + public record MissionPageResponse( + List missions, Integer listSize, - Integer totalPage, - Long totalElements, Boolean isFirst, - Boolean isLast - ) { - } - - public record HomeMissionPageResponse( - List missions, - Integer listSize, - Integer totalPage, - Long totalElements, - Boolean isFirst, - Boolean isLast + Boolean isLast, + Boolean hasNext ) { } } diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/repository/MemberMissionRepository.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/repository/MemberMissionRepository.java index 28bd763..ce744a8 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/repository/MemberMissionRepository.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/repository/MemberMissionRepository.java @@ -2,8 +2,8 @@ import com.example.umc10th.domain.mission.entity.MemberMission; import com.example.umc10th.domain.mission.enums.MemberMissionStatus; -import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; import org.springframework.data.jpa.repository.EntityGraph; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; @@ -19,7 +19,7 @@ public interface MemberMissionRepository extends JpaRepository findMemberMissionsByStatus( + Slice findMemberMissionsByStatus( @Param("memberId") Long memberId, @Param("status") MemberMissionStatus status, Pageable pageable diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/repository/MissionRepository.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/repository/MissionRepository.java index 73cdabf..cbb623b 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/repository/MissionRepository.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/repository/MissionRepository.java @@ -1,8 +1,8 @@ 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.domain.Slice; import org.springframework.data.jpa.repository.EntityGraph; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; @@ -17,15 +17,16 @@ public interface MissionRepository extends JpaRepository { where m.store.region.id = :regionId and ( :memberId is null - or m.id not in ( - select mm.mission.id + or not exists ( + select 1 from MemberMission mm where mm.member.id = :memberId + and mm.mission.id = m.id ) ) order by m.id desc """) - Page findChallengeableMissionsByRegion( + Slice findChallengeableMissionsByRegion( @Param("regionId") Long regionId, @Param("memberId") Long memberId, Pageable pageable diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/service/MissionService.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/service/MissionService.java index dcf7c7f..af1453b 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/service/MissionService.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/service/MissionService.java @@ -9,9 +9,19 @@ public interface MissionService { MissionResDto.MissionInfo createMission(MissionReqDto.Create request); - MissionResDto.MemberMissionPageResponse getMemberMissions(Long memberId, MemberMissionStatus status, Integer page); + MissionResDto.MissionPageResponse getMemberMissions( + Long memberId, + MemberMissionStatus status, + Integer page + ); - MissionResDto.HomeMissionPageResponse getHomeMissions(Long regionId, Long memberId, Integer page); + MissionResDto.MissionPageResponse getHomeMissions( + Long regionId, + Long memberId, + Integer page + ); - MissionResDto.MemberMissionPageResponse getMyChallengingMissions(MissionReqDto.MyChallengingMissionsRequest request); + MissionResDto.MissionPageResponse getMyChallengingMissions( + MissionReqDto.MyChallengingMissionsRequest request + ); } diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/service/MissionServiceImpl.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/service/MissionServiceImpl.java index ac2cce5..5eba914 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/service/MissionServiceImpl.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/service/MissionServiceImpl.java @@ -15,8 +15,8 @@ import com.example.umc10th.domain.store.exception.code.StoreErrorCode; import com.example.umc10th.domain.store.repository.StoreRepository; import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Slice; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -25,6 +25,8 @@ @Transactional(readOnly = true) public class MissionServiceImpl implements MissionService { + private static final int DEFAULT_PAGE_SIZE = 10; + private final MissionRepository missionRepository; private final MemberMissionRepository memberMissionRepository; private final StoreRepository storeRepository; @@ -48,39 +50,49 @@ public MissionResDto.MissionInfo createMission(MissionReqDto.Create request) { } @Override - public MissionResDto.MemberMissionPageResponse getMemberMissions(Long memberId, MemberMissionStatus status, Integer page) { + public MissionResDto.MissionPageResponse getMemberMissions( + Long memberId, + MemberMissionStatus status, + Integer page + ) { int pageNumber = Math.max(page, 1) - 1; - PageRequest pageRequest = PageRequest.of(pageNumber, 10); + PageRequest pageRequest = PageRequest.of(pageNumber, DEFAULT_PAGE_SIZE); - Page memberMissionPage = memberMissionRepository.findMemberMissionsByStatus( + Slice memberMissionSlice = memberMissionRepository.findMemberMissionsByStatus( memberId, status, pageRequest ); - return MissionConverter.toMemberMissionPageResponse(memberMissionPage); + return MissionConverter.toMemberMissionPageResponse(memberMissionSlice); } @Override - public MissionResDto.HomeMissionPageResponse getHomeMissions(Long regionId, Long memberId, Integer page) { + public MissionResDto.MissionPageResponse getHomeMissions( + Long regionId, + Long memberId, + Integer page + ) { int pageNumber = Math.max(page, 1) - 1; - PageRequest pageRequest = PageRequest.of(pageNumber, 10); + PageRequest pageRequest = PageRequest.of(pageNumber, DEFAULT_PAGE_SIZE); - Page missionPage = missionRepository.findChallengeableMissionsByRegion(regionId, memberId, pageRequest); - return MissionConverter.toHomeMissionPageResponse(missionPage); + Slice missionSlice = missionRepository.findChallengeableMissionsByRegion(regionId, memberId, pageRequest); + return MissionConverter.toHomeMissionPageResponse(missionSlice); } @Override - public MissionResDto.MemberMissionPageResponse getMyChallengingMissions(MissionReqDto.MyChallengingMissionsRequest request) { + public MissionResDto.MissionPageResponse getMyChallengingMissions( + MissionReqDto.MyChallengingMissionsRequest request + ) { int pageNumber = request.page() - 1; PageRequest pageRequest = PageRequest.of(pageNumber, request.size()); - Page memberMissionPage = memberMissionRepository.findMemberMissionsByStatus( + Slice memberMissionSlice = memberMissionRepository.findMemberMissionsByStatus( request.memberId(), MemberMissionStatus.CHALLENGING, pageRequest ); - return MissionConverter.toMemberMissionPageResponse(memberMissionPage); + return MissionConverter.toMemberMissionPageResponse(memberMissionSlice); } } From 8864af649802b6835003f7f0294df6c56955204d Mon Sep 17 00:00:00 2001 From: minju Date: Wed, 27 May 2026 17:26:38 +0900 Subject: [PATCH 08/10] =?UTF-8?q?refactor:=20=EB=A6=AC=EB=B7=B0=20?= =?UTF-8?q?=EC=BB=A4=EC=84=9C=20=EC=A1=B0=ED=9A=8C=20=ED=94=BC=EB=93=9C?= =?UTF-8?q?=EB=B0=B1=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mission/service/MissionServiceImpl.java | 2 +- .../review/converter/ReviewConverter.java | 39 ++++++++++---- .../review/repository/ReviewRepository.java | 10 ++-- .../review/service/ReviewServiceImpl.java | 52 +++++-------------- 4 files changed, 50 insertions(+), 53 deletions(-) diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/service/MissionServiceImpl.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/service/MissionServiceImpl.java index 5eba914..607b710 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/service/MissionServiceImpl.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/service/MissionServiceImpl.java @@ -84,7 +84,7 @@ public MissionResDto.MissionPageResponse getH public MissionResDto.MissionPageResponse getMyChallengingMissions( MissionReqDto.MyChallengingMissionsRequest request ) { - int pageNumber = request.page() - 1; + int pageNumber = Math.max(request.page(), 1) - 1; PageRequest pageRequest = PageRequest.of(pageNumber, request.size()); Slice memberMissionSlice = memberMissionRepository.findMemberMissionsByStatus( diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/review/converter/ReviewConverter.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/review/converter/ReviewConverter.java index 528a334..2739b84 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/domain/review/converter/ReviewConverter.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/review/converter/ReviewConverter.java @@ -6,6 +6,7 @@ import com.example.umc10th.domain.review.dto.ReviewResDto; import com.example.umc10th.domain.review.entity.Review; import java.util.List; +import org.springframework.data.domain.Slice; public final class ReviewConverter { @@ -42,19 +43,39 @@ public static ReviewResDto.MyReviewItem toMyReviewItem(Review review) { } public static ReviewResDto.MyReviewsByIdCursorResponse toMyReviewsByIdCursorResponse( - List reviews, - Long nextCursorId, - boolean hasNext + Slice slice ) { - return new ReviewResDto.MyReviewsByIdCursorResponse(reviews, nextCursorId, hasNext); + List reviews = slice.getContent(); + List items = reviews.stream() + .map(ReviewConverter::toMyReviewItem) + .toList(); + + Long nextCursorId = slice.hasNext() + ? reviews.get(reviews.size() - 1).getId() + : null; + + return new ReviewResDto.MyReviewsByIdCursorResponse(items, nextCursorId, slice.hasNext()); } public static ReviewResDto.MyReviewsByScoreCursorResponse toMyReviewsByScoreCursorResponse( - List reviews, - Float nextCursorScore, - Long nextCursorId, - boolean hasNext + Slice slice ) { - return new ReviewResDto.MyReviewsByScoreCursorResponse(reviews, nextCursorScore, nextCursorId, hasNext); + List reviews = slice.getContent(); + List items = reviews.stream() + .map(ReviewConverter::toMyReviewItem) + .toList(); + + Review lastReview = slice.hasNext() + ? reviews.get(reviews.size() - 1) + : null; + Float nextCursorScore = lastReview == null ? null : lastReview.getScore(); + Long nextCursorId = lastReview == null ? null : lastReview.getId(); + + return new ReviewResDto.MyReviewsByScoreCursorResponse( + items, + nextCursorScore, + nextCursorId, + slice.hasNext() + ); } } diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/review/repository/ReviewRepository.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/review/repository/ReviewRepository.java index 7f27ce1..c55b23d 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/domain/review/repository/ReviewRepository.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/review/repository/ReviewRepository.java @@ -1,8 +1,8 @@ package com.example.umc10th.domain.review.repository; import com.example.umc10th.domain.review.entity.Review; -import java.util.List; import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; import org.springframework.data.jpa.repository.EntityGraph; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; @@ -17,7 +17,7 @@ public interface ReviewRepository extends JpaRepository { where r.member.id = :memberId order by r.id desc """) - List findMyReviewsByIdCursorFirst( + Slice findMyReviewsByIdCursorFirst( @Param("memberId") Long memberId, Pageable pageable ); @@ -30,7 +30,7 @@ List findMyReviewsByIdCursorFirst( and r.id < :cursorId order by r.id desc """) - List findMyReviewsByIdCursorNext( + Slice findMyReviewsByIdCursorNext( @Param("memberId") Long memberId, @Param("cursorId") Long cursorId, Pageable pageable @@ -43,7 +43,7 @@ List findMyReviewsByIdCursorNext( where r.member.id = :memberId order by r.score desc, r.id desc """) - List findMyReviewsByScoreCursorFirst( + Slice findMyReviewsByScoreCursorFirst( @Param("memberId") Long memberId, Pageable pageable ); @@ -56,7 +56,7 @@ List findMyReviewsByScoreCursorFirst( and (r.score < :cursorScore or (r.score = :cursorScore and r.id < :cursorId)) order by r.score desc, r.id desc """) - List findMyReviewsByScoreCursorNext( + Slice findMyReviewsByScoreCursorNext( @Param("memberId") Long memberId, @Param("cursorScore") Float cursorScore, @Param("cursorId") Long cursorId, diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/review/service/ReviewServiceImpl.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/review/service/ReviewServiceImpl.java index a4627e5..2d05cf4 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/domain/review/service/ReviewServiceImpl.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/review/service/ReviewServiceImpl.java @@ -15,9 +15,9 @@ 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 java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Slice; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -41,8 +41,7 @@ public ReviewResDto.ReviewInfo getReview(Long reviewId) { @Override @Transactional public ReviewResDto.ReviewInfo createReview(ReviewReqDto.Create request) { - Member member = memberRepository.findById(request.memberId()) - .orElseThrow(() -> new MemberException(MemberErrorCode.MEMBER_NOT_FOUND)); + Member member = getMember(request.memberId()); Store store = storeRepository.findById(request.storeId()) .orElseThrow(() -> new StoreException(StoreErrorCode.STORE_NOT_FOUND)); @@ -53,31 +52,20 @@ public ReviewResDto.ReviewInfo createReview(ReviewReqDto.Create request) { @Override public ReviewResDto.MyReviewsByIdCursorResponse getMyReviewsByIdCursor(ReviewReqDto.MyReviewsByIdCursorRequest request) { - memberRepository.findById(request.memberId()) - .orElseThrow(() -> new MemberException(MemberErrorCode.MEMBER_NOT_FOUND)); + getMember(request.memberId()); - int fetchSize = request.size() + 1; - PageRequest pageRequest = PageRequest.of(0, fetchSize); + PageRequest pageRequest = PageRequest.of(0, request.size()); - List reviews = request.cursorId() == null + Slice reviewSlice = request.cursorId() == null ? reviewRepository.findMyReviewsByIdCursorFirst(request.memberId(), pageRequest) : reviewRepository.findMyReviewsByIdCursorNext(request.memberId(), request.cursorId(), pageRequest); - boolean hasNext = reviews.size() > request.size(); - List content = hasNext ? reviews.subList(0, request.size()) : reviews; - - List items = content.stream() - .map(ReviewConverter::toMyReviewItem) - .toList(); - - Long nextCursorId = hasNext ? content.get(content.size() - 1).getId() : null; - return ReviewConverter.toMyReviewsByIdCursorResponse(items, nextCursorId, hasNext); + return ReviewConverter.toMyReviewsByIdCursorResponse(reviewSlice); } @Override public ReviewResDto.MyReviewsByScoreCursorResponse getMyReviewsByScoreCursor(ReviewReqDto.MyReviewsByScoreCursorRequest request) { - memberRepository.findById(request.memberId()) - .orElseThrow(() -> new MemberException(MemberErrorCode.MEMBER_NOT_FOUND)); + getMember(request.memberId()); boolean bothCursorProvided = request.cursorScore() != null && request.cursorId() != null; boolean bothCursorEmpty = request.cursorScore() == null && request.cursorId() == null; @@ -85,10 +73,9 @@ public ReviewResDto.MyReviewsByScoreCursorResponse getMyReviewsByScoreCursor(Rev throw new ReviewException(ReviewErrorCode.INVALID_CURSOR); } - int fetchSize = request.size() + 1; - PageRequest pageRequest = PageRequest.of(0, fetchSize); + PageRequest pageRequest = PageRequest.of(0, request.size()); - List reviews = bothCursorEmpty + Slice reviewSlice = bothCursorEmpty ? reviewRepository.findMyReviewsByScoreCursorFirst(request.memberId(), pageRequest) : reviewRepository.findMyReviewsByScoreCursorNext( request.memberId(), @@ -97,22 +84,11 @@ public ReviewResDto.MyReviewsByScoreCursorResponse getMyReviewsByScoreCursor(Rev pageRequest ); - boolean hasNext = reviews.size() > request.size(); - List content = hasNext ? reviews.subList(0, request.size()) : reviews; - - List items = content.stream() - .map(ReviewConverter::toMyReviewItem) - .toList(); - - Float nextCursorScore = null; - Long nextCursorId = null; - - if (hasNext) { - Review lastReview = content.get(content.size() - 1); - nextCursorScore = lastReview.getScore(); - nextCursorId = lastReview.getId(); - } + return ReviewConverter.toMyReviewsByScoreCursorResponse(reviewSlice); + } - return ReviewConverter.toMyReviewsByScoreCursorResponse(items, nextCursorScore, nextCursorId, hasNext); + private Member getMember(Long memberId) { + return memberRepository.findById(memberId) + .orElseThrow(() -> new MemberException(MemberErrorCode.MEMBER_NOT_FOUND)); } } From dc75590f99ece3fc384d828fc837ba593f2052cf Mon Sep 17 00:00:00 2001 From: minju Date: Wed, 27 May 2026 17:41:45 +0900 Subject: [PATCH 09/10] =?UTF-8?q?feat:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20=EC=95=BD=EA=B4=80=20=EB=8F=99=EC=9D=98=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../member/converter/MemberConverter.java | 5 +++ .../domain/member/dto/MemberReqDto.java | 37 +++++++++++++++++-- .../umc10th/domain/member/entity/Member.java | 15 ++++++++ .../exception/code/MemberErrorCode.java | 1 + .../member/service/MemberServiceImpl.java | 13 +++++++ 5 files changed, 67 insertions(+), 4 deletions(-) diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/member/converter/MemberConverter.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/member/converter/MemberConverter.java index 2833fa2..4fa7047 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/domain/member/converter/MemberConverter.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/member/converter/MemberConverter.java @@ -31,6 +31,11 @@ public static Member toEntity(MemberReqDto.SignUpRequest request, String encoded .nickname(request.nickname()) .email(request.email()) .password(encodedPassword) + .ageOver14Agreed(Boolean.TRUE.equals(request.termsAgreement().ageOver14Agreed())) + .serviceTermsAgreed(Boolean.TRUE.equals(request.termsAgreement().serviceTermsAgreed())) + .privacyPolicyAgreed(Boolean.TRUE.equals(request.termsAgreement().privacyPolicyAgreed())) + .locationAgreed(Boolean.TRUE.equals(request.termsAgreement().locationAgreed())) + .marketingAgreed(Boolean.TRUE.equals(request.termsAgreement().marketingAgreed())) .build(); } diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/member/dto/MemberReqDto.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/member/dto/MemberReqDto.java index e70f096..0be2b90 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/domain/member/dto/MemberReqDto.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/member/dto/MemberReqDto.java @@ -2,10 +2,12 @@ import com.example.umc10th.domain.member.enums.Gender; import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; import jakarta.validation.constraints.Positive; import jakarta.validation.constraints.Size; import java.time.LocalDate; @@ -56,11 +58,20 @@ public record SignUpRequest( @Schema(description = "이메일", example = "bean@example.com") @NotBlank @Email @Size(max = 100) String email, - @Schema(description = "비밀번호", example = "password123") - @NotBlank @Size(min = 8, max = 30) String password, + @Schema(description = "비밀번호", example = "Password1!") + @NotBlank + @Size(min = 8, max = 30) + @Pattern( + regexp = "^(?=.*[A-Za-z])(?=.*\\d)(?=.*[!@#$%^&*()_+=\\-{}\\[\\]:;\"'<>,.?/]).+$", + message = "비밀번호는 영문, 숫자, 특수문자를 각각 1개 이상 포함해야 합니다." + ) + String password, @Schema(description = "선호 음식 카테고리 ID 목록", example = "[1, 2]") - @NotEmpty List<@NotNull @Positive Long> preferCategoryIds + @NotEmpty List<@NotNull @Positive Long> preferCategoryIds, + + @Schema(description = "약관 동의 정보") + @Valid @NotNull TermsAgreement termsAgreement ) { } @@ -68,8 +79,26 @@ public record LoginRequest( @Schema(description = "이메일", example = "bean@example.com") @NotBlank @Email @Size(max = 100) String email, - @Schema(description = "비밀번호", example = "password123") + @Schema(description = "비밀번호", example = "Password1!") @NotBlank String password ) { } + + public record TermsAgreement( + @Schema(description = "만 14세 이상 동의 여부", example = "true") + @NotNull Boolean ageOver14Agreed, + + @Schema(description = "서비스 이용약관 필수 동의 여부", example = "true") + @NotNull Boolean serviceTermsAgreed, + + @Schema(description = "개인정보 처리방침 필수 동의 여부", example = "true") + @NotNull Boolean privacyPolicyAgreed, + + @Schema(description = "위치정보 제공 선택 동의 여부", example = "false") + @NotNull Boolean locationAgreed, + + @Schema(description = "마케팅 수신 선택 동의 여부", example = "false") + @NotNull Boolean marketingAgreed + ) { + } } diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/member/entity/Member.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/member/entity/Member.java index 1d8303d..e4e7cfe 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/domain/member/entity/Member.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/member/entity/Member.java @@ -59,6 +59,21 @@ public class Member { @Column(name = "password", length = 255) private String password; + @Column(name = "age_over_14_agreed", nullable = false) + private boolean ageOver14Agreed; + + @Column(name = "service_terms_agreed", nullable = false) + private boolean serviceTermsAgreed; + + @Column(name = "privacy_policy_agreed", nullable = false) + private boolean privacyPolicyAgreed; + + @Column(name = "location_agreed", nullable = false) + private boolean locationAgreed; + + @Column(name = "marketing_agreed", nullable = false) + private boolean marketingAgreed; + @Column(name = "created_at", nullable = false) private LocalDateTime createdAt; diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/member/exception/code/MemberErrorCode.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/member/exception/code/MemberErrorCode.java index c0b8737..cc7eeb0 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/domain/member/exception/code/MemberErrorCode.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/member/exception/code/MemberErrorCode.java @@ -12,6 +12,7 @@ public enum MemberErrorCode implements BaseErrorCode { MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "MEMBER404_1", "User not found."), EMAIL_ALREADY_EXISTS(HttpStatus.BAD_REQUEST, "MEMBER400_1", "Email already exists."), FOOD_CATEGORY_NOT_FOUND(HttpStatus.BAD_REQUEST, "MEMBER400_2", "Food category not found."), + REQUIRED_TERMS_NOT_AGREED(HttpStatus.BAD_REQUEST, "MEMBER400_3", "Required terms must be agreed."), INVALID_PASSWORD(HttpStatus.UNAUTHORIZED, "MEMBER401_1", "Password does not match."); private final HttpStatus httpStatus; diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/member/service/MemberServiceImpl.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/member/service/MemberServiceImpl.java index acab56b..a926315 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/domain/member/service/MemberServiceImpl.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/member/service/MemberServiceImpl.java @@ -68,6 +68,8 @@ public MemberResDto.GetMyPageResponse getMyPage(Member member) { @Override @Transactional public MemberResDto.SignUpResponse signUp(MemberReqDto.SignUpRequest request) { + validateRequiredTerms(request.termsAgreement()); + if (memberRepository.existsByEmail(request.email())) { throw new MemberException(MemberErrorCode.EMAIL_ALREADY_EXISTS); } @@ -108,4 +110,15 @@ public MemberResDto.LoginResponse login(MemberReqDto.LoginRequest request) { JwtTokenProvider.TokenInfo tokenInfo = jwtTokenProvider.generateAccessToken(member); return MemberConverter.toLoginResponse(member, tokenInfo); } + + private void validateRequiredTerms(MemberReqDto.TermsAgreement termsAgreement) { + boolean requiredTermsAgreed = termsAgreement != null + && Boolean.TRUE.equals(termsAgreement.ageOver14Agreed()) + && Boolean.TRUE.equals(termsAgreement.serviceTermsAgreed()) + && Boolean.TRUE.equals(termsAgreement.privacyPolicyAgreed()); + + if (!requiredTermsAgreed) { + throw new MemberException(MemberErrorCode.REQUIRED_TERMS_NOT_AGREED); + } + } } From bc7331ad145603701294d1d4655062ce6329ec5d Mon Sep 17 00:00:00 2001 From: minju Date: Wed, 27 May 2026 17:58:55 +0900 Subject: [PATCH 10/10] =?UTF-8?q?fix:=20ObjectMapper=20Bean=20=EB=93=B1?= =?UTF-8?q?=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../umc10th/global/config/JacksonConfig.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 bean/umc10th/src/main/java/com/example/umc10th/global/config/JacksonConfig.java diff --git a/bean/umc10th/src/main/java/com/example/umc10th/global/config/JacksonConfig.java b/bean/umc10th/src/main/java/com/example/umc10th/global/config/JacksonConfig.java new file mode 100644 index 0000000..793a076 --- /dev/null +++ b/bean/umc10th/src/main/java/com/example/umc10th/global/config/JacksonConfig.java @@ -0,0 +1,14 @@ +package com.example.umc10th.global.config; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class JacksonConfig { + + @Bean + public ObjectMapper objectMapper() { + return new ObjectMapper(); + } +}