From 0e4f71937b23cf804a0d0920a969313746219e96 Mon Sep 17 00:00:00 2001 From: seoyoon lee Date: Fri, 8 May 2026 11:53:51 +0900 Subject: [PATCH 01/13] =?UTF-8?q?refactor:=205=EC=A3=BC=EC=B0=A8=20-=20@Sc?= =?UTF-8?q?hema,=20@Valid=20=EC=B6=94=EA=B0=80=20#21?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- leeseo/umc10th/build.gradle | 3 + .../member/controller/MemberController.java | 5 +- .../controller/MemberControllerDocs.java | 7 +- .../domain/member/dto/MemberReqDto.java | 27 +++++--- .../review/controller/ReviewController.java | 5 +- .../domain/review/dto/ReviewReqDto.java | 18 +++-- .../exception/code/ReviewSuccessCode.java | 6 +- .../apiPayload/code/GeneralErrorCode.java | 6 +- .../handler/GeneralExceptionAdvice.java | 66 +++++++++++++++++++ 9 files changed, 118 insertions(+), 25 deletions(-) create mode 100644 leeseo/umc10th/src/main/java/com/example/umc10th/global/apiPayload/handler/GeneralExceptionAdvice.java diff --git a/leeseo/umc10th/build.gradle b/leeseo/umc10th/build.gradle index 93fc11f..fba59ff 100644 --- a/leeseo/umc10th/build.gradle +++ b/leeseo/umc10th/build.gradle @@ -32,6 +32,9 @@ dependencies { // Swagger implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:3.0.1' implementation 'org.springdoc:springdoc-openapi-starter-webmvc-api:3.0.1' + + // Validation + implementation 'org.springframework.boot:spring-boot-starter-validation' } tasks.named('test') { diff --git a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java index 67f9411..6c73c24 100644 --- a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java +++ b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java @@ -7,6 +7,7 @@ import com.example.umc10th.domain.member.service.MemberService; import com.example.umc10th.global.apiPayload.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; @@ -36,7 +37,7 @@ public ApiResponse getProfile( @PatchMapping("/me/profile") public ApiResponse updateProfile( - @RequestBody MemberReqDto.Profile dto + @RequestBody @Valid MemberReqDto.Profile dto ) { memberService.updateProfile(dto); return ApiResponse.onSuccess(MemberSuccessCode.PROFILE_PATCH_OK, null); @@ -44,7 +45,7 @@ public ApiResponse updateProfile( @PatchMapping("/me/nickname") public ApiResponse updateNickname( - @RequestBody MemberReqDto.Nickname dto + @RequestBody @Valid MemberReqDto.Nickname dto ) { memberService.updateNickname(dto); return ApiResponse.onSuccess(MemberSuccessCode.NICKNAME_PATCH_OK, null); diff --git a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/member/controller/MemberControllerDocs.java b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/member/controller/MemberControllerDocs.java index 45ee6b1..28022fa 100644 --- a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/member/controller/MemberControllerDocs.java +++ b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/member/controller/MemberControllerDocs.java @@ -4,7 +4,7 @@ import com.example.umc10th.domain.member.dto.MemberResDto; import com.example.umc10th.global.apiPayload.ApiResponse; import io.swagger.v3.oas.annotations.Operation; -import org.springframework.web.bind.annotation.PatchMapping; +import jakarta.validation.Valid; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestParam; @@ -31,15 +31,14 @@ public ApiResponse getProfile( description = "회원 프로필 정보를 저장합니다." ) public ApiResponse updateProfile( - @RequestBody MemberReqDto.Profile dto + @RequestBody @Valid MemberReqDto.Profile dto ); @Operation( summary = "닉네임 수정", description = "닉네임을 수정합니다." ) - @PatchMapping("/me/nickname") public ApiResponse updateNickname( - @RequestBody MemberReqDto.Nickname dto + @RequestBody @Valid MemberReqDto.Nickname dto ); } diff --git a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/member/dto/MemberReqDto.java b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/member/dto/MemberReqDto.java index c2ee939..daf2e1a 100644 --- a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/member/dto/MemberReqDto.java +++ b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/member/dto/MemberReqDto.java @@ -3,6 +3,9 @@ import com.example.umc10th.domain.member.enums.FoodType; import com.example.umc10th.domain.member.enums.Gender; import com.example.umc10th.domain.mission.enums.Address; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; import java.time.LocalDate; import java.util.List; @@ -13,23 +16,29 @@ public record TermList ( List terms ) {} + @Schema(name = "Term", description = "약관별 동의 여부를 저장합니다.") public record Term ( Long termId, Boolean agreed ) {} + @Schema(name = "Profile", description = "수정할 프로필 정보를 저장합합니다.") public record Profile ( - Long id, - String nickname, - Gender gender, - LocalDate birth, - Address address, - String fullAddress, - List food + @NotNull(message = "사용자 ID는 필수입니다.") + Long id, + String nickname, + Gender gender, + LocalDate birth, + Address address, + String fullAddress, + List food ) {} + @Schema(name = "Nickname", description = "수정할 닉네임을 저장합합니다.") public record Nickname ( - Long id, - String nickname + @NotNull(message = "사용자 ID는 필수입니다.") + Long id, + @NotBlank(message = "닉네임 입력은 필수입니다.") + String nickname ) {} } diff --git a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/review/controller/ReviewController.java b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/review/controller/ReviewController.java index 7fcc152..094a8b7 100644 --- a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/review/controller/ReviewController.java +++ b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/review/controller/ReviewController.java @@ -7,6 +7,7 @@ import com.example.umc10th.domain.review.service.ReviewService; import com.example.umc10th.global.apiPayload.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; @@ -23,7 +24,7 @@ public class ReviewController implements ReviewControllerDocs{ @PostMapping("/stores/{storeId}/reviews") public ApiResponse saveReview( @PathVariable Long storeId, - @RequestBody ReviewReqDto.Review dto, + @RequestBody @Valid ReviewReqDto.Review dto, @RequestParam Long memberId ) { reviewService.saveReview(storeId, memberId, dto); @@ -40,7 +41,7 @@ public ApiResponse> getReviewList( @GetMapping("/stores/{storeId}/reviews/photos") public ApiResponse> getReviewPhotoList( - @PathVariable Long storeId + @PathVariable @Valid Long storeId ) { List response = reviewService.getReviewPhotoList(storeId); return ApiResponse.onSuccess(ReviewSuccessCode.REVIEW_PHOTO_LIST_GET_OK, response); diff --git a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/review/dto/ReviewReqDto.java b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/review/dto/ReviewReqDto.java index 0c1edd1..a8afe30 100644 --- a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/review/dto/ReviewReqDto.java +++ b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/review/dto/ReviewReqDto.java @@ -1,16 +1,26 @@ package com.example.umc10th.domain.review.dto; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + import java.util.List; public class ReviewReqDto { + @Schema(name = "Review", description = "등록할 리뷰 정보를 저장합합니다.") public record Review( - Long rate, - String content, - List reviewPhotoList + @NotNull + Long rate, + @NotBlank + String content, + @NotNull + List reviewPhotoList ) {} + @Schema(name = "ReviewPhoto", description = "등록할 리뷰 사진들을 저장합합니다.") public record ReviewPhoto( - String reviewPhoto + @NotBlank + String reviewPhoto ) {} } diff --git a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/review/exception/code/ReviewSuccessCode.java b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/review/exception/code/ReviewSuccessCode.java index 387dc3e..75cb716 100644 --- a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/review/exception/code/ReviewSuccessCode.java +++ b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/review/exception/code/ReviewSuccessCode.java @@ -9,15 +9,15 @@ @RequiredArgsConstructor public enum ReviewSuccessCode implements BaseSuccessCode { REVIEW_POST_OK(HttpStatus.OK, - "REVIEW200_1", + "REVIEW201_1", "리뷰가 작성되었습니다."), REVIEW_LIST_GET_OK(HttpStatus.OK, - "REVIEW200_2", + "REVIEW200_1", "리뷰 목록이 조회되었습니다."), REVIEW_PHOTO_LIST_GET_OK(HttpStatus.OK, - "REVIEW200_3", + "REVIEW200_2", "리뷰 사진 목록이 조회되었습니다."); private final HttpStatus status; diff --git a/leeseo/umc10th/src/main/java/com/example/umc10th/global/apiPayload/code/GeneralErrorCode.java b/leeseo/umc10th/src/main/java/com/example/umc10th/global/apiPayload/code/GeneralErrorCode.java index 6e030e6..411526c 100644 --- a/leeseo/umc10th/src/main/java/com/example/umc10th/global/apiPayload/code/GeneralErrorCode.java +++ b/leeseo/umc10th/src/main/java/com/example/umc10th/global/apiPayload/code/GeneralErrorCode.java @@ -22,7 +22,11 @@ public enum GeneralErrorCode implements BaseErrorCode{ NOT_FOUND(HttpStatus.NOT_FOUND, "COMMON404_1", - "해당 리소스를 찾을 수 없습니다."); + "해당 리소스를 찾을 수 없습니다."), + + INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, + "COMMON500_1", + "서버가 응답하지 않습니다."); private final HttpStatus status; private final String code; diff --git a/leeseo/umc10th/src/main/java/com/example/umc10th/global/apiPayload/handler/GeneralExceptionAdvice.java b/leeseo/umc10th/src/main/java/com/example/umc10th/global/apiPayload/handler/GeneralExceptionAdvice.java new file mode 100644 index 0000000..4ac3ada --- /dev/null +++ b/leeseo/umc10th/src/main/java/com/example/umc10th/global/apiPayload/handler/GeneralExceptionAdvice.java @@ -0,0 +1,66 @@ +package com.example.umc10th.global.apiPayload.handler; + +import com.example.umc10th.global.apiPayload.ApiResponse; +import com.example.umc10th.global.apiPayload.code.BaseErrorCode; +import com.example.umc10th.global.apiPayload.code.GeneralErrorCode; +import com.example.umc10th.global.apiPayload.exception.BaseException; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import java.util.HashMap; +import java.util.Map; + +@RestControllerAdvice +public class GeneralExceptionAdvice { + + // 애플리케이션에서 발생하는 커스텀 예외를 처리 + @ExceptionHandler(BaseException.class) + public ResponseEntity> handleException( + BaseException ex + ) { + + return ResponseEntity.status(ex.getErrorCode().getStatus()) + .body(ApiResponse.onFailure( + ex.getErrorCode(), + null + ) + ); + } + + // Valid 오류 + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity>> handleValidationException( + MethodArgumentNotValidException e + ) { + + Map errors = new HashMap<>(); + + e.getBindingResult() + .getFieldErrors() + .forEach(error -> + errors.put( + error.getField(), + error.getDefaultMessage() + ) + ); + BaseErrorCode code = GeneralErrorCode.BAD_REQUEST; + return ResponseEntity.badRequest().body(ApiResponse.onFailure(code, errors)); + } + + // 그 외의 정의되지 않은 모든 예외 처리 + @ExceptionHandler(Exception.class) + public ResponseEntity> handleException( + Exception ex + ) { + + BaseErrorCode code = GeneralErrorCode.INTERNAL_SERVER_ERROR; + return ResponseEntity.status(code.getStatus()) + .body(ApiResponse.onFailure( + code, + ex.getMessage() + ) + ); + } +} From 91dba6b381e9fc6e4283643e6b375a6ddae1a510 Mon Sep 17 00:00:00 2001 From: seoyoon lee Date: Fri, 8 May 2026 12:34:17 +0900 Subject: [PATCH 02/13] =?UTF-8?q?refactor:=206=EC=A3=BC=EC=B0=A8=20-=20?= =?UTF-8?q?=EA=B3=B5=ED=86=B5=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1,=20N+1=20=EB=AC=B8=EC=A0=9C=20=EB=B0=A9=EC=A7=80=20#2?= =?UTF-8?q?1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../member/repository/MemberRepository.java | 8 ++++++++ .../domain/member/service/MemberService.java | 16 ++++++++++------ .../domain/mission/service/MissionService.java | 12 ++++++++---- .../review/repository/ReviewRepository.java | 6 ++++++ .../domain/review/service/ReviewService.java | 13 ++++++++----- 5 files changed, 40 insertions(+), 15 deletions(-) diff --git a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/member/repository/MemberRepository.java b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/member/repository/MemberRepository.java index b60402f..7357c02 100644 --- a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/member/repository/MemberRepository.java +++ b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/member/repository/MemberRepository.java @@ -2,9 +2,17 @@ import com.example.umc10th.domain.member.entity.Member; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; import java.util.Optional; public interface MemberRepository extends JpaRepository { Optional findMemberById(Long id); + @Query(""" + select m from Member m + join fetch m.memberFoodList mf + join fetch mf.food + where m.id = :id + """) + Optional findMemberWithFoods(Long id); } diff --git a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/member/service/MemberService.java b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/member/service/MemberService.java index b9a2fed..af33653 100644 --- a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/member/service/MemberService.java +++ b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/member/service/MemberService.java @@ -27,15 +27,14 @@ public class MemberService { private final FoodRepository foodRepository; public MemberResDto.Profile getProfile(Long id) { - Member member = memberRepository.findMemberById(id) + Member memberWithFood = memberRepository.findMemberWithFoods(id) .orElseThrow(() -> new MemberException(MemberErrorCode.MEMBER_NOT_FOUND)); - return MemberConverter.toProfile(member); + return MemberConverter.toProfile(memberWithFood); } @Transactional public void updateProfile(MemberReqDto.Profile dto) { - Member member = memberRepository.findMemberById(dto.id()) - .orElseThrow(() -> new MemberException(MemberErrorCode.MEMBER_NOT_FOUND)); + Member member = getMemberById(dto.id()); if (dto.nickname() != null) { member.updateNickname(dto.nickname()); @@ -71,11 +70,16 @@ public void updateProfile(MemberReqDto.Profile dto) { @Transactional public void updateNickname(MemberReqDto.Nickname dto) { - Member member = memberRepository.findMemberById(dto.id()) - .orElseThrow(() -> new MemberException(MemberErrorCode.MEMBER_NOT_FOUND)); + Member member = getMemberById(dto.id()); if (dto.nickname() != null) { member.updateNickname(dto.nickname()); } } + + private Member getMemberById(Long id) { + Member member = memberRepository.findMemberById(id) + .orElseThrow(() -> new MemberException(MemberErrorCode.MEMBER_NOT_FOUND)); + return member; + } } diff --git a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/mission/service/MissionService.java b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/mission/service/MissionService.java index 41ea1cc..d8c9cc6 100644 --- a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/mission/service/MissionService.java +++ b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/mission/service/MissionService.java @@ -35,12 +35,17 @@ public class MissionService { private final MissionRepository missionRepository; public Achievement getMissionAchievement(Address address, Long memberId) { - Member member = memberRepository.findMemberById(memberId) - .orElseThrow(() -> new MemberException(MemberErrorCode.MEMBER_NOT_FOUND)); + Member member = getMemberById(memberId); return memberMissionRepository.getTotalCountAndSuccessCount(address, member); } + private Member getMemberById(Long memberId) { + Member member = memberRepository.findMemberById(memberId) + .orElseThrow(() -> new MemberException(MemberErrorCode.MEMBER_NOT_FOUND)); + return member; + } + public Slice getMissionList(Address address, Long cursor) { Pageable pageable = PageRequest.of(0, 10); return missionRepository.getMissionByAddress(address, cursor, pageable); @@ -48,8 +53,7 @@ public Slice getMissionList(Address address, Long cursor) { @Transactional public void saveMemberMission(Long missionId, Long memberId) { - Member member = memberRepository.findMemberById(memberId) - .orElseThrow(() -> new MemberException(MemberErrorCode.MEMBER_NOT_FOUND)); + Member member = getMemberById(memberId); Mission mission = missionRepository.findMissionById(missionId) .orElseThrow(() -> new MissionException(MissionErrorCode.MISSION_NOT_FOUND)); MemberMission memberMission = MissionConverter.toMemberMission(member, mission); diff --git a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/review/repository/ReviewRepository.java b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/review/repository/ReviewRepository.java index fc85243..d906fc7 100644 --- a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/review/repository/ReviewRepository.java +++ b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/review/repository/ReviewRepository.java @@ -3,9 +3,15 @@ import com.example.umc10th.domain.mission.entity.Store; import com.example.umc10th.domain.review.entity.Review; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; import java.util.List; public interface ReviewRepository extends JpaRepository { + @Query(""" + select distinct r from Review r + left join fetch r.reviewPhotoList rp + where r.store = :store + """) List getReviewsByStore(Store store); } diff --git a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/review/service/ReviewService.java b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/review/service/ReviewService.java index 15dec97..b6ef26d 100644 --- a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/review/service/ReviewService.java +++ b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/review/service/ReviewService.java @@ -35,22 +35,25 @@ public class ReviewService { public void saveReview(Long storeId, Long memberId, ReviewReqDto.Review dto) { Member member = memberRepository.findMemberById(memberId) .orElseThrow(() -> new MemberException(MemberErrorCode.MEMBER_NOT_FOUND)); - Store store = storeRepository.findStoreById(storeId) - .orElseThrow(() -> new StoreException(StoreErrorCode.STORE_NOT_FOUND)); + Store store = getStoreById(storeId); Review review = ReviewConverter.toReview(member, store, dto); reviewRepository.save(review); } public List getReviewList(Long storeId) { - Store store = storeRepository.findStoreById(storeId) - .orElseThrow(() -> new StoreException(StoreErrorCode.STORE_NOT_FOUND)); + Store store = getStoreById(storeId); List reviewList = reviewRepository.getReviewsByStore(store); return reviewList.stream().map(ReviewConverter::toReviewInfo).toList(); } - public List getReviewPhotoList(Long storeId) { + private Store getStoreById(Long storeId) { Store store = storeRepository.findStoreById(storeId) .orElseThrow(() -> new StoreException(StoreErrorCode.STORE_NOT_FOUND)); + return store; + } + + public List getReviewPhotoList(Long storeId) { + getStoreById(storeId); List reviewPhotoList = reviewPhotoRepository.getReviewPhotoByStore(storeId); return reviewPhotoList.stream().map(ReviewConverter::toReviewPhotoUrl).toList(); } From 402d009ba49d326b8a5090ac1eede564186545fc Mon Sep 17 00:00:00 2001 From: seoyoon lee Date: Fri, 8 May 2026 13:55:59 +0900 Subject: [PATCH 03/13] =?UTF-8?q?feat:=20=EB=AF=B8=EC=85=98=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=ED=95=98=EA=B8=B0=20=ED=8E=98=EC=9D=B4=EC=A7=95=20(of?= =?UTF-8?q?fset=20=EA=B8=B0=EB=B0=98)=20#21?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mission/controller/MissionController.java | 22 +++---- .../controller/MissionControllerDocs.java | 20 +++---- .../mission/converter/MissionConverter.java | 16 ++++++ .../domain/mission/dto/MissionReqDto.java | 9 +++ .../domain/mission/dto/MissionResDto.java | 15 +++++ .../repository/MemberMissionRepository.java | 6 +- .../mission/repository/MissionRepository.java | 8 +-- .../mission/service/MissionService.java | 57 ++++++++++++++----- 8 files changed, 112 insertions(+), 41 deletions(-) create mode 100644 leeseo/umc10th/src/main/java/com/example/umc10th/domain/mission/dto/MissionResDto.java diff --git a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/mission/controller/MissionController.java b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/mission/controller/MissionController.java index e1f3999..4d5432e 100644 --- a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/mission/controller/MissionController.java +++ b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/mission/controller/MissionController.java @@ -1,15 +1,13 @@ package com.example.umc10th.domain.mission.controller; -import com.example.umc10th.domain.mission.dto.Achievement; -import com.example.umc10th.domain.mission.dto.MemberMissionInfo; -import com.example.umc10th.domain.mission.dto.MissionInfo; -import com.example.umc10th.domain.mission.dto.OwnerNumber; +import com.example.umc10th.domain.mission.dto.*; import com.example.umc10th.domain.mission.enums.Address; import com.example.umc10th.domain.mission.enums.Status; import com.example.umc10th.domain.mission.exception.code.MissionSuccessCode; import com.example.umc10th.domain.mission.service.MissionService; import com.example.umc10th.global.apiPayload.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.persistence.criteria.CriteriaBuilder; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Slice; import org.springframework.web.bind.annotation.*; @@ -32,11 +30,13 @@ public ApiResponse getMissionAchievement( } @GetMapping("/missions") - public ApiResponse> getMissionList( + public ApiResponse> getMissionList( @RequestParam Address location, - @RequestParam Long cursor + @RequestParam Integer pageSize, + @RequestParam Integer pageNumber, + @RequestParam (required = false) String sort ) { - Slice response = missionService.getMissionList(location, cursor); + MissionResDto.Pagination response = missionService.getMissionList(location, pageSize, pageNumber, sort); return ApiResponse.onSuccess(MissionSuccessCode.MISSION_LIST_GET_OK, response); } @@ -50,12 +50,14 @@ public ApiResponse saveMemberMission( } @GetMapping("/members/me/missions") - public ApiResponse> getMyMissions( + public ApiResponse> getMyMissions( @RequestParam Status status, @RequestParam Long memberId, - @RequestParam Long cursor + @RequestParam Integer pageSize, + @RequestParam Integer pageNumber, + @RequestParam (required = false) String sort ) { - Slice response = missionService.getMyMissionList(status, memberId, cursor); + MissionResDto.Pagination response = missionService.getMyMissionList(status, memberId, pageSize, pageNumber, sort); return ApiResponse.onSuccess(MissionSuccessCode.MY_MISSION_LIST_GET_OK, response); } diff --git a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/mission/controller/MissionControllerDocs.java b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/mission/controller/MissionControllerDocs.java index 5492650..0cfa2f5 100644 --- a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/mission/controller/MissionControllerDocs.java +++ b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/mission/controller/MissionControllerDocs.java @@ -1,14 +1,10 @@ package com.example.umc10th.domain.mission.controller; -import com.example.umc10th.domain.mission.dto.Achievement; -import com.example.umc10th.domain.mission.dto.MemberMissionInfo; -import com.example.umc10th.domain.mission.dto.MissionInfo; -import com.example.umc10th.domain.mission.dto.OwnerNumber; +import com.example.umc10th.domain.mission.dto.*; import com.example.umc10th.domain.mission.enums.Address; import com.example.umc10th.domain.mission.enums.Status; import com.example.umc10th.global.apiPayload.ApiResponse; import io.swagger.v3.oas.annotations.Operation; -import org.springframework.data.domain.Slice; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestParam; @@ -27,9 +23,11 @@ public ApiResponse getMissionAchievement( summary = "지역별 미션 조회", description = "해당 지역의 미션 목록을 조회합니다." ) - public ApiResponse> getMissionList( + public ApiResponse> getMissionList( @RequestParam Address location, - @RequestParam Long cursor + @RequestParam Integer pageSize, + @RequestParam Integer pageNumber, + @RequestParam (required = false) String sort ); @Operation( @@ -45,10 +43,12 @@ public ApiResponse saveMemberMission( summary = "진행중/진행완료 미션 조회", description = "내 미션 목록을 진행 상태별로 조회합니다." ) - public ApiResponse> getMyMissions( + public ApiResponse> getMyMissions( @RequestParam Status status, - @RequestParam Long missionId, - @RequestParam Long cursor + @RequestParam Long memberId, + @RequestParam Integer pageSize, + @RequestParam Integer pageNumber, + @RequestParam (required = false) String sort ); @Operation( diff --git a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/mission/converter/MissionConverter.java b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/mission/converter/MissionConverter.java index ada0bf5..df94b10 100644 --- a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/mission/converter/MissionConverter.java +++ b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/mission/converter/MissionConverter.java @@ -1,10 +1,14 @@ package com.example.umc10th.domain.mission.converter; +import com.example.umc10th.domain.member.dto.MemberResDto; import com.example.umc10th.domain.member.entity.Member; +import com.example.umc10th.domain.mission.dto.MissionResDto; import com.example.umc10th.domain.mission.dto.OwnerNumber; import com.example.umc10th.domain.mission.entity.Mission; import com.example.umc10th.domain.mission.entity.mapping.MemberMission; +import java.util.List; + public class MissionConverter { public static MemberMission toMemberMission( Member member, @@ -23,4 +27,16 @@ public static OwnerNumber toOwnerNumber ( .ownerNumber(mission.getSuccessNumber()) .build(); } + + public static MissionResDto.Pagination toPagination( + List data, + Integer pageNumber, + Integer pageSize + ) { + return MissionResDto.Pagination.builder() + .data(data) + .pageNumber(pageNumber) + .pageSize(pageSize) + .build(); + } } diff --git a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/mission/dto/MissionReqDto.java b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/mission/dto/MissionReqDto.java index a688215..280bf67 100644 --- a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/mission/dto/MissionReqDto.java +++ b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/mission/dto/MissionReqDto.java @@ -1,4 +1,13 @@ package com.example.umc10th.domain.mission.dto; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; + public class MissionReqDto { + + @Schema(name = "MemberID", description = "회원 아이디를 입력합니다.") + public record MemberId( + @NotNull + Long id + ){} } diff --git a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/mission/dto/MissionResDto.java b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/mission/dto/MissionResDto.java new file mode 100644 index 0000000..1cd694a --- /dev/null +++ b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/mission/dto/MissionResDto.java @@ -0,0 +1,15 @@ +package com.example.umc10th.domain.mission.dto; + +import lombok.Builder; + +import java.util.List; + +public class MissionResDto { + + @Builder + public record Pagination( + List data, + Integer pageNumber, + Integer pageSize + ){} +} diff --git a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/mission/repository/MemberMissionRepository.java b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/mission/repository/MemberMissionRepository.java index e4b1d88..613ae48 100644 --- a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/mission/repository/MemberMissionRepository.java +++ b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/mission/repository/MemberMissionRepository.java @@ -7,6 +7,8 @@ import com.example.umc10th.domain.mission.entity.mapping.MemberMission; import com.example.umc10th.domain.mission.enums.Address; import com.example.umc10th.domain.mission.enums.Status; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; import org.springframework.data.jpa.repository.JpaRepository; @@ -38,8 +40,6 @@ public interface MemberMissionRepository extends JpaRepository getMissionByStatus(Status status, Long cursor, Pageable pageable,Long memberId); + Page getMissionByStatus(Status status, Long memberId, PageRequest pageRequest); } diff --git a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/mission/repository/MissionRepository.java b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/mission/repository/MissionRepository.java index 8f63f9e..6c5bd08 100644 --- a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/mission/repository/MissionRepository.java +++ b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/mission/repository/MissionRepository.java @@ -3,10 +3,10 @@ import com.example.umc10th.domain.mission.dto.MissionInfo; import com.example.umc10th.domain.mission.entity.Mission; import com.example.umc10th.domain.mission.enums.Address; -import org.springframework.data.domain.Slice; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; -import org.springframework.data.domain.Pageable; import java.util.Optional; @@ -21,8 +21,6 @@ public interface MissionRepository extends JpaRepository { FROM Mission m JOIN m.store s WHERE s.address = :address - AND (:cursor IS NULL OR m.id < :cursor) - ORDER BY m.id DESC """) - Slice getMissionByAddress(Address address, Long cursor, Pageable pageable); + Page getMissionByAddress(Address address, PageRequest pageRequest); } diff --git a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/mission/service/MissionService.java b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/mission/service/MissionService.java index d8c9cc6..626fbbe 100644 --- a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/mission/service/MissionService.java +++ b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/mission/service/MissionService.java @@ -5,10 +5,7 @@ import com.example.umc10th.domain.member.exception.code.MemberErrorCode; import com.example.umc10th.domain.member.repository.MemberRepository; import com.example.umc10th.domain.mission.converter.MissionConverter; -import com.example.umc10th.domain.mission.dto.Achievement; -import com.example.umc10th.domain.mission.dto.MemberMissionInfo; -import com.example.umc10th.domain.mission.dto.MissionInfo; -import com.example.umc10th.domain.mission.dto.OwnerNumber; +import com.example.umc10th.domain.mission.dto.*; import com.example.umc10th.domain.mission.entity.Mission; import com.example.umc10th.domain.mission.entity.mapping.MemberMission; import com.example.umc10th.domain.mission.enums.Address; @@ -17,11 +14,10 @@ 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.global.apiPayload.ApiResponse; import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Slice; +import org.springframework.data.domain.*; import org.springframework.stereotype.Service; -import org.springframework.data.domain.Pageable; import org.springframework.transaction.annotation.Transactional; import java.time.LocalDate; @@ -46,9 +42,26 @@ private Member getMemberById(Long memberId) { return member; } - public Slice getMissionList(Address address, Long cursor) { - Pageable pageable = PageRequest.of(0, 10); - return missionRepository.getMissionByAddress(address, cursor, pageable); + public MissionResDto.Pagination getMissionList( + Address address, + Integer pageSize, + Integer pageNumber, + String sort + ) { + Sort sortInfo; + if (sort != null){ + sortInfo = Sort.by(sort); + } else { + sortInfo = Sort.by("id").descending(); + } + + PageRequest pageRequest = PageRequest.of(pageNumber, pageSize, sortInfo); + Page missionList = missionRepository.getMissionByAddress(address, pageRequest); + return MissionConverter.toPagination( + missionList.stream().toList(), + missionList.getNumber(), + missionList.getSize() + ); } @Transactional @@ -60,9 +73,27 @@ public void saveMemberMission(Long missionId, Long memberId) { memberMissionRepository.save(memberMission); } - public Slice getMyMissionList(Status status, Long memberId, Long cursor) { - Pageable pageable = PageRequest.of(0, 10); - return memberMissionRepository.getMissionByStatus(status, cursor, pageable, memberId); + public MissionResDto.Pagination getMyMissionList( + Status status, + Long memberId, + Integer pageSize, + Integer pageNumber, + String sort + ) { + Sort sortInfo; + if (sort != null){ + sortInfo = Sort.by(sort); + } else { + sortInfo = Sort.by("id").descending(); + } + + PageRequest pageRequest = PageRequest.of(pageNumber, pageSize, sortInfo); + Page missionList = memberMissionRepository.getMissionByStatus(status, memberId, pageRequest); + return MissionConverter.toPagination( + missionList.stream().toList(), + missionList.getNumber(), + missionList.getSize() + ); } @Transactional From 3c5f5f0437a5253ce29e248939a63e9d73caab6b Mon Sep 17 00:00:00 2001 From: seoyoon lee Date: Fri, 8 May 2026 14:40:07 +0900 Subject: [PATCH 04/13] =?UTF-8?q?feat:=20=EB=A6=AC=EB=B7=B0=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=ED=95=98=EA=B8=B0=20=ED=8E=98=EC=9D=B4=EC=A7=95=20(cu?= =?UTF-8?q?rsor=20=EA=B8=B0=EB=B0=98)=20#21?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../review/controller/ReviewController.java | 10 +++- .../controller/ReviewControllerDocs.java | 10 +++- .../review/converter/ReviewConverter.java | 17 ++++++ .../domain/review/dto/ReviewResDto.java | 16 ++++++ .../review/exception/ReviewException.java | 10 +++- .../exception/code/ReviewErrorCode.java | 16 +++++- .../review/repository/ReviewRepository.java | 10 ++++ .../domain/review/service/ReviewService.java | 57 +++++++++++++++++-- 8 files changed, 132 insertions(+), 14 deletions(-) create mode 100644 leeseo/umc10th/src/main/java/com/example/umc10th/domain/review/dto/ReviewResDto.java diff --git a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/review/controller/ReviewController.java b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/review/controller/ReviewController.java index 094a8b7..3429a3b 100644 --- a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/review/controller/ReviewController.java +++ b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/review/controller/ReviewController.java @@ -3,6 +3,7 @@ import com.example.umc10th.domain.review.dto.ReviewInfo; import com.example.umc10th.domain.review.dto.ReviewPhotoUrl; import com.example.umc10th.domain.review.dto.ReviewReqDto; +import com.example.umc10th.domain.review.dto.ReviewResDto; import com.example.umc10th.domain.review.exception.code.ReviewSuccessCode; import com.example.umc10th.domain.review.service.ReviewService; import com.example.umc10th.global.apiPayload.ApiResponse; @@ -32,10 +33,13 @@ public ApiResponse saveReview( } @GetMapping("/stores/{storeId}/reviews") - public ApiResponse> getReviewList( - @PathVariable Long storeId + public ApiResponse> getReviewList( + @PathVariable Long storeId, + @RequestParam Integer pageSize, + @RequestParam String cursor, + @RequestParam String query ) { - List response = reviewService.getReviewList(storeId); + ReviewResDto.Pagination response = reviewService.getReviewList(storeId, pageSize, cursor, query); return ApiResponse.onSuccess(ReviewSuccessCode.REVIEW_LIST_GET_OK, response); } diff --git a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/review/controller/ReviewControllerDocs.java b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/review/controller/ReviewControllerDocs.java index 4fccb18..e71ac09 100644 --- a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/review/controller/ReviewControllerDocs.java +++ b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/review/controller/ReviewControllerDocs.java @@ -3,6 +3,7 @@ import com.example.umc10th.domain.review.dto.ReviewInfo; import com.example.umc10th.domain.review.dto.ReviewPhotoUrl; import com.example.umc10th.domain.review.dto.ReviewReqDto; +import com.example.umc10th.domain.review.dto.ReviewResDto; import com.example.umc10th.global.apiPayload.ApiResponse; import io.swagger.v3.oas.annotations.Operation; import org.springframework.web.bind.annotation.PathVariable; @@ -27,9 +28,12 @@ public ApiResponse saveReview( summary = "리뷰 목록 조회", description = "리뷰 목록을 조회합니다." ) - public ApiResponse> getReviewList( - @PathVariable Long storeId - ); + public ApiResponse> getReviewList( + @PathVariable Long storeId, + @RequestParam Integer pageSize, + @RequestParam String cursor, + @RequestParam String query + ) ; @Operation( summary = "리뷰 사진 목록 조회", diff --git a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/review/converter/ReviewConverter.java b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/review/converter/ReviewConverter.java index ee38765..b5e42e1 100644 --- a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/review/converter/ReviewConverter.java +++ b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/review/converter/ReviewConverter.java @@ -5,9 +5,12 @@ import com.example.umc10th.domain.review.dto.ReviewInfo; import com.example.umc10th.domain.review.dto.ReviewPhotoUrl; 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 com.example.umc10th.domain.review.entity.ReviewPhoto; +import java.util.List; + public class ReviewConverter { public static Review toReview( @@ -42,4 +45,18 @@ public static ReviewPhotoUrl toReviewPhotoUrl( .reviewPhoto(reviewPhoto.getImageUrl()) .build(); } + + public static ReviewResDto.Pagination toPagination( + List data, + boolean hasNext, + String nextCursor, + Integer pageSize + ) { + return ReviewResDto.Pagination.builder() + .data(data) + .hasNext(hasNext) + .pageSize(pageSize) + .nextCursor(nextCursor) + .build(); + } } diff --git a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/review/dto/ReviewResDto.java b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/review/dto/ReviewResDto.java new file mode 100644 index 0000000..fd77259 --- /dev/null +++ b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/review/dto/ReviewResDto.java @@ -0,0 +1,16 @@ +package com.example.umc10th.domain.review.dto; + +import lombok.Builder; + +import java.util.List; + +public class ReviewResDto { + + @Builder + public record Pagination( + List data, + Boolean hasNext, + String nextCursor, + Integer pageSize + ){} +} diff --git a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/review/exception/ReviewException.java b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/review/exception/ReviewException.java index f55abb1..ca5c935 100644 --- a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/review/exception/ReviewException.java +++ b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/review/exception/ReviewException.java @@ -1,7 +1,11 @@ package com.example.umc10th.domain.review.exception; -public class ReviewException extends RuntimeException { - public ReviewException(String message) { - super(message); +import com.example.umc10th.global.apiPayload.code.BaseErrorCode; +import com.example.umc10th.global.apiPayload.exception.BaseException; + +public class ReviewException extends BaseException { + + public ReviewException(BaseErrorCode errorCode) { + super(errorCode); } } diff --git a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/review/exception/code/ReviewErrorCode.java b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/review/exception/code/ReviewErrorCode.java index c3ff795..6053ac8 100644 --- a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/review/exception/code/ReviewErrorCode.java +++ b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/review/exception/code/ReviewErrorCode.java @@ -1,4 +1,18 @@ package com.example.umc10th.domain.review.exception.code; -public enum ReviewErrorCode { +import com.example.umc10th.global.apiPayload.code.BaseErrorCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; + +@Getter +@RequiredArgsConstructor +public enum ReviewErrorCode implements BaseErrorCode { + INVALID_QUERY(HttpStatus.BAD_REQUEST, + "REVIEW400_1", + "잘못된 쿼리 입니다."); + + private final HttpStatus status; + private final String code; + private final String message; } diff --git a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/review/repository/ReviewRepository.java b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/review/repository/ReviewRepository.java index d906fc7..71db765 100644 --- a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/review/repository/ReviewRepository.java +++ b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/review/repository/ReviewRepository.java @@ -2,6 +2,8 @@ import com.example.umc10th.domain.mission.entity.Store; import com.example.umc10th.domain.review.entity.Review; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Slice; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; @@ -14,4 +16,12 @@ public interface ReviewRepository extends JpaRepository { where r.store = :store """) List getReviewsByStore(Store store); + + Store store(Store store); + + Slice findReviewsByStore_IdAndIdLessThanOrderByIdDesc(Long storeId, Long idCursor, PageRequest pageRequest); + Slice findReviewsByStore_IdOrderByIdDesc(Long storeId, PageRequest pageRequest); + + Slice findReviewsByStore_IdAndRateLessThanOrderByRateDesc(Long storeId, Long idCursor, PageRequest pageRequest); + Slice findReviewsByStore_IdOrderByRateDesc(Long storeId, PageRequest pageRequest); } diff --git a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/review/service/ReviewService.java b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/review/service/ReviewService.java index b6ef26d..12ff17b 100644 --- a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/review/service/ReviewService.java +++ b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/review/service/ReviewService.java @@ -12,11 +12,16 @@ import com.example.umc10th.domain.review.dto.ReviewInfo; import com.example.umc10th.domain.review.dto.ReviewPhotoUrl; 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 com.example.umc10th.domain.review.entity.ReviewPhoto; +import com.example.umc10th.domain.review.exception.ReviewException; +import com.example.umc10th.domain.review.exception.code.ReviewErrorCode; import com.example.umc10th.domain.review.repository.ReviewPhotoRepository; import com.example.umc10th.domain.review.repository.ReviewRepository; 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; @@ -40,10 +45,54 @@ public void saveReview(Long storeId, Long memberId, ReviewReqDto.Review dto) { reviewRepository.save(review); } - public List getReviewList(Long storeId) { - Store store = getStoreById(storeId); - List reviewList = reviewRepository.getReviewsByStore(store); - return reviewList.stream().map(ReviewConverter::toReviewInfo).toList(); + public ReviewResDto.Pagination getReviewList(Long storeId, Integer pageSize, String cursor, String query) { + PageRequest pageRequest = PageRequest.of(0, pageSize); + if (cursor != null) cursor = "-1"; + + long idCursor; + Slice reviewList; + String nextCursor; + + if (!cursor.equals("-1")){ + String[] cursorSplit = cursor.split(":"); + switch (query.toLowerCase()){ + case "id": + idCursor = Long.parseLong(cursorSplit[1]); + reviewList = reviewRepository.findReviewsByStore_IdAndIdLessThanOrderByIdDesc(storeId, idCursor, pageRequest); + break; + case "rate": + idCursor = Long.parseLong(cursorSplit[1]); + reviewList = reviewRepository.findReviewsByStore_IdAndRateLessThanOrderByRateDesc(storeId, idCursor, pageRequest); + break; + default: + throw new ReviewException(ReviewErrorCode.INVALID_QUERY); + } + } else { + switch (query.toLowerCase()){ + case "id": + reviewList = reviewRepository.findReviewsByStore_IdOrderByIdDesc(storeId, pageRequest); + break; + case "rate": + reviewList = reviewRepository.findReviewsByStore_IdOrderByRateDesc(storeId, pageRequest); + break; + default: + throw new ReviewException(ReviewErrorCode.INVALID_QUERY); + } + } + nextCursor = "-1"; + + if (!reviewList.isEmpty()) { + Long lastId = reviewList.getContent() + .get(reviewList.getNumberOfElements() - 1) + .getId(); + + nextCursor = lastId + ":" + lastId; + } + return ReviewConverter.toPagination( + reviewList.map(ReviewConverter::toReviewInfo).toList(), + reviewList.hasNext(), + nextCursor, + reviewList.getSize()); } private Store getStoreById(Long storeId) { From 4994afdcb96550dd5d78be9df83db369302f3a8d Mon Sep 17 00:00:00 2001 From: seoyoon lee Date: Fri, 8 May 2026 17:07:23 +0900 Subject: [PATCH 05/13] =?UTF-8?q?refactor:=20=EB=A6=AC=EB=B7=B0=20?= =?UTF-8?q?=EB=B3=84=EC=A0=90=EC=88=9C=20=EC=A1=B0=ED=9A=8C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95,=20=EB=A6=AC=EB=B7=B0=20=EC=82=AC=EC=A7=84=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=95=20=EA=B5=AC=ED=98=84=20#21?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../review/controller/ReviewController.java | 11 ++- .../controller/ReviewControllerDocs.java | 7 +- .../review/converter/ReviewConverter.java | 3 + .../umc10th/domain/review/dto/ReviewInfo.java | 1 + .../domain/review/dto/ReviewPhotoUrl.java | 2 + .../repository/ReviewPhotoRepository.java | 29 ++++++ .../review/repository/ReviewRepository.java | 21 +++- .../domain/review/service/ReviewService.java | 97 +++++++++++++++---- 8 files changed, 145 insertions(+), 26 deletions(-) diff --git a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/review/controller/ReviewController.java b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/review/controller/ReviewController.java index 3429a3b..b11203f 100644 --- a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/review/controller/ReviewController.java +++ b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/review/controller/ReviewController.java @@ -36,7 +36,7 @@ public ApiResponse saveReview( public ApiResponse> getReviewList( @PathVariable Long storeId, @RequestParam Integer pageSize, - @RequestParam String cursor, + @RequestParam (required = false) String cursor, @RequestParam String query ) { ReviewResDto.Pagination response = reviewService.getReviewList(storeId, pageSize, cursor, query); @@ -44,10 +44,13 @@ public ApiResponse> getReviewList( } @GetMapping("/stores/{storeId}/reviews/photos") - public ApiResponse> getReviewPhotoList( - @PathVariable @Valid Long storeId + public ApiResponse> getReviewPhotoList( + @PathVariable @Valid Long storeId, + @RequestParam Integer pageSize, + @RequestParam (required = false) String cursor, + @RequestParam String query ) { - List response = reviewService.getReviewPhotoList(storeId); + ReviewResDto.Pagination response = reviewService.getReviewPhotoList(storeId, pageSize, cursor, query); return ApiResponse.onSuccess(ReviewSuccessCode.REVIEW_PHOTO_LIST_GET_OK, response); } diff --git a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/review/controller/ReviewControllerDocs.java b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/review/controller/ReviewControllerDocs.java index e71ac09..3d72714 100644 --- a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/review/controller/ReviewControllerDocs.java +++ b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/review/controller/ReviewControllerDocs.java @@ -39,7 +39,10 @@ public ApiResponse> getReviewList( summary = "리뷰 사진 목록 조회", description = "리뷰 사진 목록을 조회합니다." ) - public ApiResponse> getReviewPhotoList( - @PathVariable Long storeId + public ApiResponse> getReviewPhotoList( + @PathVariable Long storeId, + @RequestParam Integer pageSize, + @RequestParam String cursor, + @RequestParam String query ); } diff --git a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/review/converter/ReviewConverter.java b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/review/converter/ReviewConverter.java index b5e42e1..359bb32 100644 --- a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/review/converter/ReviewConverter.java +++ b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/review/converter/ReviewConverter.java @@ -30,6 +30,7 @@ public static ReviewInfo toReviewInfo( Review review ) { return ReviewInfo.builder() + .reviewId(review.getId()) .rate(review.getRate()) .content(review.getContent()) .reviewerNickname(review.getMember().getNickname()) @@ -42,6 +43,8 @@ public static ReviewPhotoUrl toReviewPhotoUrl( ReviewPhoto reviewPhoto ) { return ReviewPhotoUrl.builder() + .reviewPhotoId(reviewPhoto.getId()) + .reviewRate(reviewPhoto.getReview().getRate()) .reviewPhoto(reviewPhoto.getImageUrl()) .build(); } diff --git a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/review/dto/ReviewInfo.java b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/review/dto/ReviewInfo.java index 3aa43f5..9275035 100644 --- a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/review/dto/ReviewInfo.java +++ b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/review/dto/ReviewInfo.java @@ -7,6 +7,7 @@ @Builder public record ReviewInfo( + Long reviewId, double rate, String content, List reviewPhotoList, diff --git a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/review/dto/ReviewPhotoUrl.java b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/review/dto/ReviewPhotoUrl.java index b4e7134..169d88c 100644 --- a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/review/dto/ReviewPhotoUrl.java +++ b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/review/dto/ReviewPhotoUrl.java @@ -4,5 +4,7 @@ @Builder public record ReviewPhotoUrl( + Long reviewPhotoId, + Double reviewRate, String reviewPhoto ) {} diff --git a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/review/repository/ReviewPhotoRepository.java b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/review/repository/ReviewPhotoRepository.java index 79baebd..b46997d 100644 --- a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/review/repository/ReviewPhotoRepository.java +++ b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/review/repository/ReviewPhotoRepository.java @@ -1,6 +1,9 @@ package com.example.umc10th.domain.review.repository; +import com.example.umc10th.domain.review.entity.Review; import com.example.umc10th.domain.review.entity.ReviewPhoto; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Slice; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; @@ -15,4 +18,30 @@ public interface ReviewPhotoRepository extends JpaRepository WHERE s.id = :storeId """) List getReviewPhotoByStore(Long storeId); + + Slice findReviewPhotosByStore_IdAndIdLessThanOrderByIdDesc(Long storeId, Long idCursor, PageRequest pageRequest); + Slice findReviewPhotosByStore_IdOrderByIdDesc(Long storeId, PageRequest pageRequest); + + @Query(""" + SELECT rp + FROM ReviewPhoto rp + JOIN rp.store s + JOIN rp.review r + WHERE s.id = :storeId + AND ( + r.rate < :rateCursor + OR (r.rate = :rateCursor AND rp.id < :idCursor) + ) + ORDER BY r.rate DESC, rp.id DESC + """) + Slice findReviewPhotosByRateCursor(Long storeId, Double rateCursor, Long idCursor, PageRequest pageRequest); + @Query(""" + SELECT rp + FROM ReviewPhoto rp + JOIN rp.store s + JOIN rp.review r + WHERE s.id = :storeId + ORDER BY r.rate DESC, rp.id DESC + """) + Slice findReviewPhotosOrderByRate(Long storeId, PageRequest pageRequest); } diff --git a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/review/repository/ReviewRepository.java b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/review/repository/ReviewRepository.java index 71db765..2c8486f 100644 --- a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/review/repository/ReviewRepository.java +++ b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/review/repository/ReviewRepository.java @@ -22,6 +22,23 @@ public interface ReviewRepository extends JpaRepository { Slice findReviewsByStore_IdAndIdLessThanOrderByIdDesc(Long storeId, Long idCursor, PageRequest pageRequest); Slice findReviewsByStore_IdOrderByIdDesc(Long storeId, PageRequest pageRequest); - Slice findReviewsByStore_IdAndRateLessThanOrderByRateDesc(Long storeId, Long idCursor, PageRequest pageRequest); - Slice findReviewsByStore_IdOrderByRateDesc(Long storeId, PageRequest pageRequest); + @Query(""" + SELECT r + FROM Review r + WHERE r.store.id = :storeId + AND ( + r.rate < :rateCursor + OR (r.rate = :rateCursor AND r.id < :idCursor) + ) + ORDER BY r.rate DESC, r.id DESC + """) + Slice findReviewsByRateCursor(Long storeId, Double rateCursor, Long idCursor, PageRequest pageRequest); + + @Query(""" + SELECT r + FROM Review r + WHERE r.store.id = :storeId + ORDER BY r.rate DESC, r.id DESC + """) + Slice findReviewsOrderByRate(Long storeId, PageRequest pageRequest); } diff --git a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/review/service/ReviewService.java b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/review/service/ReviewService.java index 12ff17b..dd849dd 100644 --- a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/review/service/ReviewService.java +++ b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/review/service/ReviewService.java @@ -40,16 +40,16 @@ public class ReviewService { public void saveReview(Long storeId, Long memberId, ReviewReqDto.Review dto) { Member member = memberRepository.findMemberById(memberId) .orElseThrow(() -> new MemberException(MemberErrorCode.MEMBER_NOT_FOUND)); - Store store = getStoreById(storeId); + Store store = storeRepository.findStoreById(storeId).orElseThrow(() -> new StoreException(StoreErrorCode.STORE_NOT_FOUND)); Review review = ReviewConverter.toReview(member, store, dto); reviewRepository.save(review); } public ReviewResDto.Pagination getReviewList(Long storeId, Integer pageSize, String cursor, String query) { + if (cursor == null) cursor = "-1"; PageRequest pageRequest = PageRequest.of(0, pageSize); - if (cursor != null) cursor = "-1"; - long idCursor; + double rateCursor; Slice reviewList; String nextCursor; @@ -61,8 +61,9 @@ public ReviewResDto.Pagination getReviewList(Long storeId, Integer p reviewList = reviewRepository.findReviewsByStore_IdAndIdLessThanOrderByIdDesc(storeId, idCursor, pageRequest); break; case "rate": + rateCursor = Double.parseDouble(cursorSplit[0]); idCursor = Long.parseLong(cursorSplit[1]); - reviewList = reviewRepository.findReviewsByStore_IdAndRateLessThanOrderByRateDesc(storeId, idCursor, pageRequest); + reviewList = reviewRepository.findReviewsByRateCursor(storeId, rateCursor, idCursor, pageRequest); break; default: throw new ReviewException(ReviewErrorCode.INVALID_QUERY); @@ -73,7 +74,7 @@ public ReviewResDto.Pagination getReviewList(Long storeId, Integer p reviewList = reviewRepository.findReviewsByStore_IdOrderByIdDesc(storeId, pageRequest); break; case "rate": - reviewList = reviewRepository.findReviewsByStore_IdOrderByRateDesc(storeId, pageRequest); + reviewList = reviewRepository.findReviewsOrderByRate(storeId, pageRequest); break; default: throw new ReviewException(ReviewErrorCode.INVALID_QUERY); @@ -82,12 +83,22 @@ public ReviewResDto.Pagination getReviewList(Long storeId, Integer p nextCursor = "-1"; if (!reviewList.isEmpty()) { - Long lastId = reviewList.getContent() - .get(reviewList.getNumberOfElements() - 1) - .getId(); - nextCursor = lastId + ":" + lastId; + Review lastReview = reviewList.getContent() + .get(reviewList.getNumberOfElements() - 1); + + switch (query.toLowerCase()) { + + case "id": + nextCursor = lastReview.getId() + ":" + lastReview.getId(); + break; + + case "rate": + nextCursor = lastReview.getRate() + ":" + lastReview.getId(); + break; + } } + return ReviewConverter.toPagination( reviewList.map(ReviewConverter::toReviewInfo).toList(), reviewList.hasNext(), @@ -95,15 +106,65 @@ public ReviewResDto.Pagination getReviewList(Long storeId, Integer p reviewList.getSize()); } - private Store getStoreById(Long storeId) { - Store store = storeRepository.findStoreById(storeId) - .orElseThrow(() -> new StoreException(StoreErrorCode.STORE_NOT_FOUND)); - return store; - } + public ReviewResDto.Pagination getReviewPhotoList(Long storeId, Integer pageSize, String cursor, String query) { + if (cursor == null) cursor = "-1"; + PageRequest pageRequest = PageRequest.of(0, pageSize); + + long idCursor; + double rateCursor; + Slice reviewPhotoList; + String nextCursor; + + if (!cursor.equals("-1")){ + String[] cursorSplit = cursor.split(":"); + switch (query.toLowerCase()){ + case "id": + idCursor = Long.parseLong(cursorSplit[1]); + reviewPhotoList = reviewPhotoRepository.findReviewPhotosByStore_IdAndIdLessThanOrderByIdDesc(storeId, idCursor, pageRequest); + break; + case "rate": + rateCursor = Double.parseDouble(cursorSplit[0]); + idCursor = Long.parseLong(cursorSplit[1]); + reviewPhotoList = reviewPhotoRepository.findReviewPhotosByRateCursor(storeId, rateCursor, idCursor, pageRequest); + break; + default: + throw new ReviewException(ReviewErrorCode.INVALID_QUERY); + } + } else { + switch (query.toLowerCase()){ + case "id": + reviewPhotoList = reviewPhotoRepository.findReviewPhotosByStore_IdOrderByIdDesc(storeId, pageRequest); + break; + case "rate": + reviewPhotoList = reviewPhotoRepository.findReviewPhotosOrderByRate(storeId, pageRequest); + break; + default: + throw new ReviewException(ReviewErrorCode.INVALID_QUERY); + } + } + nextCursor = "-1"; - public List getReviewPhotoList(Long storeId) { - getStoreById(storeId); - List reviewPhotoList = reviewPhotoRepository.getReviewPhotoByStore(storeId); - return reviewPhotoList.stream().map(ReviewConverter::toReviewPhotoUrl).toList(); + if (!reviewPhotoList.isEmpty()) { + + ReviewPhoto lastReviewPhoto = reviewPhotoList.getContent() + .get(reviewPhotoList.getNumberOfElements() - 1); + + switch (query.toLowerCase()) { + + case "id": + nextCursor = lastReviewPhoto.getId() + ":" + lastReviewPhoto.getId(); + break; + + case "rate": + nextCursor = lastReviewPhoto.getReview().getRate() + ":" + lastReviewPhoto.getId(); + break; + } + } + + return ReviewConverter.toPagination( + reviewPhotoList.map(ReviewConverter::toReviewPhotoUrl).toList(), + reviewPhotoList.hasNext(), + nextCursor, + reviewPhotoList.getSize()); } } From 4b4d1bc0716140c39c15b19b63b5802060fa647f Mon Sep 17 00:00:00 2001 From: seoyoon lee Date: Sat, 9 May 2026 20:00:40 +0900 Subject: [PATCH 06/13] =?UTF-8?q?refactor:=20cascade=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=20#21?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/example/umc10th/domain/member/entity/Member.java | 6 +++--- .../com/example/umc10th/domain/mission/entity/Mission.java | 2 +- .../com/example/umc10th/domain/mission/entity/Store.java | 4 ++-- .../com/example/umc10th/domain/review/entity/Review.java | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/member/entity/Member.java b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/member/entity/Member.java index 0e12bde..8d08be7 100644 --- a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/member/entity/Member.java +++ b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/member/entity/Member.java @@ -80,13 +80,13 @@ public class Member extends BaseEntity { @Column(name = "is_owner", nullable = false) private Boolean isOwner = false; - @OneToMany(mappedBy = "member") + @OneToMany(mappedBy = "member", cascade = CascadeType.ALL, orphanRemoval = true) private List memberFoodList = new ArrayList<>(); - @OneToMany(mappedBy = "member") + @OneToMany(mappedBy = "member", cascade = CascadeType.ALL, orphanRemoval = true) private List memberTermList = new ArrayList<>(); - @OneToMany(mappedBy = "member") + @OneToMany(mappedBy = "member", cascade = CascadeType.ALL, orphanRemoval = true) private List memberMissionList = new ArrayList<>(); @OneToMany(mappedBy = "member") diff --git a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/mission/entity/Mission.java b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/mission/entity/Mission.java index 49f7067..f956542 100644 --- a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/mission/entity/Mission.java +++ b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/mission/entity/Mission.java @@ -35,7 +35,7 @@ public class Mission { @Column(name = "rewards_point", nullable = false) private Integer rewardsPoint; - @OneToMany(mappedBy = "mission") + @OneToMany(mappedBy = "mission", cascade = CascadeType.ALL, orphanRemoval = true) private List memberMissionList = new ArrayList<>(); @ManyToOne(fetch = FetchType.LAZY) diff --git a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/mission/entity/Store.java b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/mission/entity/Store.java index 7b64918..42b0d85 100644 --- a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/mission/entity/Store.java +++ b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/mission/entity/Store.java @@ -43,9 +43,9 @@ public class Store { @JoinColumn(name = "location_id") private Location location; - @OneToMany(mappedBy = "store") + @OneToMany(mappedBy = "store", cascade = CascadeType.ALL, orphanRemoval = true) private List reviewList = new ArrayList<>(); - @OneToMany(mappedBy = "store") + @OneToMany(mappedBy = "store", cascade = CascadeType.ALL, orphanRemoval = true) private List reviewPhotoList = new ArrayList<>(); } diff --git a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/review/entity/Review.java b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/review/entity/Review.java index b1e431c..4839aad 100644 --- a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/review/entity/Review.java +++ b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/review/entity/Review.java @@ -39,10 +39,10 @@ public class Review extends BaseEntity { @JoinColumn(name = "store_id") private Store store; - @OneToOne(fetch = FetchType.LAZY) + @OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true) @JoinColumn(name = "reply_id") private Reply reply; - @OneToMany(mappedBy = "review") + @OneToMany(mappedBy = "review", cascade = CascadeType.ALL, orphanRemoval = true) List reviewPhotoList = new ArrayList<>(); } From 7a6da963d8412796916a074e2b55a3b2af0d3458 Mon Sep 17 00:00:00 2001 From: seoyoon lee Date: Fri, 15 May 2026 20:43:50 +0900 Subject: [PATCH 07/13] =?UTF-8?q?feat:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20api=20=EA=B5=AC=ED=98=84=20#29?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- leeseo/umc10th/build.gradle | 4 ++ .../auth/controller/AuthController.java | 30 ++++++++++++ .../auth/controller/AuthControllerDocs.java | 19 +++++++ .../domain/auth/converter/AuthConverter.java | 28 +++++++++++ .../umc10th/domain/auth/dto/AuthReqDto.java | 37 ++++++++++++++ .../umc10th/domain/auth/dto/AuthResDto.java | 13 +++++ .../domain/auth/exception/AuthException.java | 10 ++++ .../auth/exception/code/AuthSuccessCode.java | 19 +++++++ .../domain/auth/service/AuthService.java | 49 +++++++++++++++++++ .../umc10th/domain/member/entity/Member.java | 5 ++ .../domain/member/enums/SocialType.java | 2 +- .../exception/code/MemberErrorCode.java | 6 ++- .../member/repository/MemberRepository.java | 1 + .../umc10th/global/config/SecurityConfig.java | 49 +++++++++++++++++++ .../global/security/entity/AuthMember.java | 33 +++++++++++++ .../service/CustomUserDetailsService.java | 28 +++++++++++ 16 files changed, 331 insertions(+), 2 deletions(-) create mode 100644 leeseo/umc10th/src/main/java/com/example/umc10th/domain/auth/controller/AuthController.java create mode 100644 leeseo/umc10th/src/main/java/com/example/umc10th/domain/auth/controller/AuthControllerDocs.java create mode 100644 leeseo/umc10th/src/main/java/com/example/umc10th/domain/auth/converter/AuthConverter.java create mode 100644 leeseo/umc10th/src/main/java/com/example/umc10th/domain/auth/dto/AuthReqDto.java create mode 100644 leeseo/umc10th/src/main/java/com/example/umc10th/domain/auth/dto/AuthResDto.java create mode 100644 leeseo/umc10th/src/main/java/com/example/umc10th/domain/auth/exception/AuthException.java create mode 100644 leeseo/umc10th/src/main/java/com/example/umc10th/domain/auth/exception/code/AuthSuccessCode.java create mode 100644 leeseo/umc10th/src/main/java/com/example/umc10th/domain/auth/service/AuthService.java create mode 100644 leeseo/umc10th/src/main/java/com/example/umc10th/global/config/SecurityConfig.java create mode 100644 leeseo/umc10th/src/main/java/com/example/umc10th/global/security/entity/AuthMember.java create mode 100644 leeseo/umc10th/src/main/java/com/example/umc10th/global/security/service/CustomUserDetailsService.java diff --git a/leeseo/umc10th/build.gradle b/leeseo/umc10th/build.gradle index fba59ff..4810952 100644 --- a/leeseo/umc10th/build.gradle +++ b/leeseo/umc10th/build.gradle @@ -35,6 +35,10 @@ dependencies { // Validation implementation 'org.springframework.boot:spring-boot-starter-validation' + + // Security + implementation 'org.springframework.boot:spring-boot-starter-security' + testImplementation 'org.springframework.security:spring-security-test' } tasks.named('test') { diff --git a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/auth/controller/AuthController.java b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/auth/controller/AuthController.java new file mode 100644 index 0000000..5e30339 --- /dev/null +++ b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/auth/controller/AuthController.java @@ -0,0 +1,30 @@ +package com.example.umc10th.domain.auth.controller; + +import com.example.umc10th.domain.auth.dto.AuthReqDto; +import com.example.umc10th.domain.auth.dto.AuthResDto; +import com.example.umc10th.domain.auth.exception.code.AuthSuccessCode; +import com.example.umc10th.domain.auth.service.AuthService; +import com.example.umc10th.global.apiPayload.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/v1/auth") +@Tag(name = "Auth", description = "인증 관련 API") +public class AuthController implements AuthControllerDocs{ + + private final AuthService authService; + + @PostMapping("/sign-up") + public ApiResponse signUp( + @RequestBody @Valid AuthReqDto.Signup dto + ) { + return ApiResponse.onSuccess(AuthSuccessCode.SIGNUP_OK, authService.signUp(dto)); + } +} diff --git a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/auth/controller/AuthControllerDocs.java b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/auth/controller/AuthControllerDocs.java new file mode 100644 index 0000000..fec6af0 --- /dev/null +++ b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/auth/controller/AuthControllerDocs.java @@ -0,0 +1,19 @@ +package com.example.umc10th.domain.auth.controller; + +import com.example.umc10th.domain.auth.dto.AuthReqDto; +import com.example.umc10th.domain.auth.dto.AuthResDto; +import com.example.umc10th.global.apiPayload.ApiResponse; +import io.swagger.v3.oas.annotations.Operation; +import jakarta.validation.Valid; +import org.springframework.web.bind.annotation.RequestBody; + +public interface AuthControllerDocs { + + @Operation( + summary = "회원가입", + description = "이메일 회원가입 진행" + ) + public ApiResponse signUp( + @RequestBody @Valid AuthReqDto.Signup dto + ); +} diff --git a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/auth/converter/AuthConverter.java b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/auth/converter/AuthConverter.java new file mode 100644 index 0000000..a43fab9 --- /dev/null +++ b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/auth/converter/AuthConverter.java @@ -0,0 +1,28 @@ +package com.example.umc10th.domain.auth.converter; + +import com.example.umc10th.domain.auth.dto.AuthReqDto; +import com.example.umc10th.domain.member.entity.Member; +import com.example.umc10th.domain.member.enums.SocialType; + +import java.util.UUID; + + +public class AuthConverter { + + public static Member toMember( + AuthReqDto.Signup dto, + String salt + ) { + return Member.builder() + .email(dto.email()) + .password(salt) + .nickname(dto.nickname()) + .gender(dto.gender()) + .birth(dto.birth()) + .address(dto.address()) + .fullAddress(dto.fullAddress()) + .socialId("email"+UUID.randomUUID()) + .socialType(SocialType.EMAIL) + .build(); + } +} diff --git a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/auth/dto/AuthReqDto.java b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/auth/dto/AuthReqDto.java new file mode 100644 index 0000000..2b3063c --- /dev/null +++ b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/auth/dto/AuthReqDto.java @@ -0,0 +1,37 @@ +package com.example.umc10th.domain.auth.dto; + +import com.example.umc10th.domain.member.dto.MemberReqDto; +import com.example.umc10th.domain.member.enums.FoodType; +import com.example.umc10th.domain.member.enums.Gender; +import com.example.umc10th.domain.mission.enums.Address; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +import java.time.LocalDate; +import java.util.List; + +public class AuthReqDto { + + @Schema(name = "SignUp", description = "회원가입 정보를 저장합합니다.") + public record Signup ( + @Email + @NotBlank + String email, + @NotBlank + String password, + @NotBlank + String nickname, + @NotNull + Gender gender, + @NotNull + LocalDate birth, + @NotNull + Address address, + @NotBlank + String fullAddress, + @NotNull + List food + ) {} +} diff --git a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/auth/dto/AuthResDto.java b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/auth/dto/AuthResDto.java new file mode 100644 index 0000000..5de5251 --- /dev/null +++ b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/auth/dto/AuthResDto.java @@ -0,0 +1,13 @@ +package com.example.umc10th.domain.auth.dto; + +import lombok.Builder; +import lombok.Getter; + +@Getter +public class AuthResDto { + + @Builder + public record Id( + Long id + ) {} +} diff --git a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/auth/exception/AuthException.java b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/auth/exception/AuthException.java new file mode 100644 index 0000000..9df4eb4 --- /dev/null +++ b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/auth/exception/AuthException.java @@ -0,0 +1,10 @@ +package com.example.umc10th.domain.auth.exception; + +import com.example.umc10th.global.apiPayload.code.BaseErrorCode; +import com.example.umc10th.global.apiPayload.exception.BaseException; + +public class AuthException extends BaseException { + public AuthException(BaseErrorCode errorCode) { + super(errorCode); + } +} diff --git a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/auth/exception/code/AuthSuccessCode.java b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/auth/exception/code/AuthSuccessCode.java new file mode 100644 index 0000000..96149d1 --- /dev/null +++ b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/auth/exception/code/AuthSuccessCode.java @@ -0,0 +1,19 @@ +package com.example.umc10th.domain.auth.exception.code; + +import com.example.umc10th.global.apiPayload.code.BaseSuccessCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; + +@Getter +@RequiredArgsConstructor +public enum AuthSuccessCode implements BaseSuccessCode { + + SIGNUP_OK(HttpStatus.OK, + "AUTH200_1", + "회원가입 되었습니다."); + + private final HttpStatus status; + private final String code; + private final String message; +} diff --git a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/auth/service/AuthService.java b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/auth/service/AuthService.java new file mode 100644 index 0000000..c4bd641 --- /dev/null +++ b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/auth/service/AuthService.java @@ -0,0 +1,49 @@ +package com.example.umc10th.domain.auth.service; + +import com.example.umc10th.domain.auth.converter.AuthConverter; +import com.example.umc10th.domain.auth.dto.AuthReqDto; +import com.example.umc10th.domain.auth.dto.AuthResDto; +import com.example.umc10th.domain.member.converter.MemberConverter; +import com.example.umc10th.domain.member.entity.Food; +import com.example.umc10th.domain.member.entity.Member; +import com.example.umc10th.domain.member.enums.FoodType; +import com.example.umc10th.domain.member.exception.FoodException; +import com.example.umc10th.domain.member.exception.MemberException; +import com.example.umc10th.domain.member.exception.code.FoodErrorCode; +import com.example.umc10th.domain.member.exception.code.MemberErrorCode; +import com.example.umc10th.domain.member.repository.FoodRepository; +import com.example.umc10th.domain.member.repository.MemberRepository; +import com.example.umc10th.domain.member.service.MemberService; +import lombok.RequiredArgsConstructor; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.List; + +@Service +@RequiredArgsConstructor +public class AuthService { + + private final FoodRepository foodRepository; + private final PasswordEncoder passwordEncoder; + private final MemberRepository memberRepository; + + @Transactional + public AuthResDto.Id signUp(AuthReqDto.Signup dto) { + String salt = passwordEncoder.encode(dto.password()); + Member member = AuthConverter.toMember(dto, salt); + List list = new ArrayList<>(); + if (dto.food() != null) { + for (FoodType f : dto.food()) { + Food food = foodRepository.findByName(f) + .orElseThrow(() -> new FoodException(FoodErrorCode.FOOD_TYPE_NOT_FOUND)); + list.add(food); + } + } + memberRepository.save(member); + member.updateFoodList(list); + return AuthResDto.Id.builder().id(member.getId()).build(); + } +} diff --git a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/member/entity/Member.java b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/member/entity/Member.java index 8d08be7..9d42b23 100644 --- a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/member/entity/Member.java +++ b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/member/entity/Member.java @@ -47,6 +47,9 @@ public class Member extends BaseEntity { @Column(name = "email", unique = true) private String email; + @Column(name = "password") + private String password; + @Column(name = "phone_number", unique = true) private String phoneNumber; @@ -80,6 +83,7 @@ public class Member extends BaseEntity { @Column(name = "is_owner", nullable = false) private Boolean isOwner = false; + @Builder.Default @OneToMany(mappedBy = "member", cascade = CascadeType.ALL, orphanRemoval = true) private List memberFoodList = new ArrayList<>(); @@ -113,6 +117,7 @@ public void updateFullAddress(String fullAddress) { } public void updateFoodList(List foodList) { + if (foodList == null) return; List list = new ArrayList<>(); for (Food food : foodList) { list.add(MemberConverter.toMemberFood(food, this)); diff --git a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/member/enums/SocialType.java b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/member/enums/SocialType.java index b14fe2d..54898d4 100644 --- a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/member/enums/SocialType.java +++ b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/member/enums/SocialType.java @@ -1,5 +1,5 @@ package com.example.umc10th.domain.member.enums; public enum SocialType { - KAKAO, GOOGLE, NAVER, APPLE + KAKAO, GOOGLE, NAVER, APPLE, EMAIL } diff --git a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/member/exception/code/MemberErrorCode.java b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/member/exception/code/MemberErrorCode.java index 17d5597..f1ceb9c 100644 --- a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/member/exception/code/MemberErrorCode.java +++ b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/member/exception/code/MemberErrorCode.java @@ -10,7 +10,11 @@ public enum MemberErrorCode implements BaseErrorCode { MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "MEMBER404_1", - "해당 아이디의 회원이 존재하지 않습니다."); + "해당 아이디의 회원이 존재하지 않습니다."), + + BAD_MEMBER_INFO(HttpStatus.BAD_REQUEST, + "MEMBER400_1", + "해당 정보를 저장할 수 없습니다."); private final HttpStatus status; private final String code; diff --git a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/member/repository/MemberRepository.java b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/member/repository/MemberRepository.java index 7357c02..066bc83 100644 --- a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/member/repository/MemberRepository.java +++ b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/member/repository/MemberRepository.java @@ -15,4 +15,5 @@ public interface MemberRepository extends JpaRepository { where m.id = :id """) Optional findMemberWithFoods(Long id); + Optional findByEmail(String email); } diff --git a/leeseo/umc10th/src/main/java/com/example/umc10th/global/config/SecurityConfig.java b/leeseo/umc10th/src/main/java/com/example/umc10th/global/config/SecurityConfig.java new file mode 100644 index 0000000..de3289a --- /dev/null +++ b/leeseo/umc10th/src/main/java/com/example/umc10th/global/config/SecurityConfig.java @@ -0,0 +1,49 @@ +package com.example.umc10th.global.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; + +@EnableWebSecurity +@Configuration +public class SecurityConfig { + + private final String[] allowUris = { + // Swagger 허용 + "/swagger-ui/**", + "/swagger-resources/**", + "/v3/api-docs/**", + "/api/v1/auth/**" + }; + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http + .csrf(AbstractHttpConfigurer::disable) + .authorizeHttpRequests(requests -> requests + .requestMatchers(allowUris).permitAll() + .anyRequest().authenticated() + ) + .formLogin(form -> form + .defaultSuccessUrl("/swagger-ui/index.html", true) + .permitAll() + ) + .logout(logout -> logout + .logoutUrl("/logout") + .logoutSuccessUrl("/login?logout") + .permitAll() + ); + + return http.build(); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} diff --git a/leeseo/umc10th/src/main/java/com/example/umc10th/global/security/entity/AuthMember.java b/leeseo/umc10th/src/main/java/com/example/umc10th/global/security/entity/AuthMember.java new file mode 100644 index 0000000..78b0e18 --- /dev/null +++ b/leeseo/umc10th/src/main/java/com/example/umc10th/global/security/entity/AuthMember.java @@ -0,0 +1,33 @@ +package com.example.umc10th.global.security.entity; + +import com.example.umc10th.domain.member.entity.Member; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.jspecify.annotations.Nullable; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.Collection; +import java.util.List; + +@Getter +@RequiredArgsConstructor +public class AuthMember implements UserDetails { + + private final Member member; + + @Override + public Collection getAuthorities() { + return List.of(); + } + + @Override + public @Nullable String getPassword() { + return member.getPassword(); + } + + @Override + public String getUsername() { + return member.getEmail(); + } +} diff --git a/leeseo/umc10th/src/main/java/com/example/umc10th/global/security/service/CustomUserDetailsService.java b/leeseo/umc10th/src/main/java/com/example/umc10th/global/security/service/CustomUserDetailsService.java new file mode 100644 index 0000000..a4b3470 --- /dev/null +++ b/leeseo/umc10th/src/main/java/com/example/umc10th/global/security/service/CustomUserDetailsService.java @@ -0,0 +1,28 @@ +package com.example.umc10th.global.security.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.global.security.entity.AuthMember; +import lombok.RequiredArgsConstructor; +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; + +@Service +@RequiredArgsConstructor +public class CustomUserDetailsService implements UserDetailsService { + + private final MemberRepository memberRepository; + + @Override + public UserDetails loadUserByUsername( + String username + ) throws UsernameNotFoundException { + Member member = memberRepository.findByEmail(username) + .orElseThrow(() -> new MemberException(MemberErrorCode.MEMBER_NOT_FOUND)); + return new AuthMember(member); + } +} From 893a70ebf6dbf8719524994f7919b3078b528de3 Mon Sep 17 00:00:00 2001 From: seoyoon lee Date: Fri, 15 May 2026 20:56:28 +0900 Subject: [PATCH 08/13] =?UTF-8?q?feat:=20exceptionHandling=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20#29?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../umc10th/global/config/SecurityConfig.java | 16 +++++++++ .../security/handler/CustomAccessDenied.java | 35 +++++++++++++++++++ .../security/handler/CustomEntryPoint.java | 35 +++++++++++++++++++ 3 files changed, 86 insertions(+) create mode 100644 leeseo/umc10th/src/main/java/com/example/umc10th/global/security/handler/CustomAccessDenied.java create mode 100644 leeseo/umc10th/src/main/java/com/example/umc10th/global/security/handler/CustomEntryPoint.java diff --git a/leeseo/umc10th/src/main/java/com/example/umc10th/global/config/SecurityConfig.java b/leeseo/umc10th/src/main/java/com/example/umc10th/global/config/SecurityConfig.java index de3289a..a90b69b 100644 --- a/leeseo/umc10th/src/main/java/com/example/umc10th/global/config/SecurityConfig.java +++ b/leeseo/umc10th/src/main/java/com/example/umc10th/global/config/SecurityConfig.java @@ -1,5 +1,7 @@ package com.example.umc10th.global.config; +import com.example.umc10th.global.security.handler.CustomAccessDenied; +import com.example.umc10th.global.security.handler.CustomEntryPoint; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; @@ -37,6 +39,10 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .logoutUrl("/logout") .logoutSuccessUrl("/login?logout") .permitAll() + ) + .exceptionHandling(exception -> exception + .accessDeniedHandler(customAccessDenied()) + .authenticationEntryPoint(customEntryPoint()) ); return http.build(); @@ -46,4 +52,14 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } + + @Bean + public CustomAccessDenied customAccessDenied() { + return new CustomAccessDenied(); + } + + @Bean + public CustomEntryPoint customEntryPoint() { + return new CustomEntryPoint(); + } } diff --git a/leeseo/umc10th/src/main/java/com/example/umc10th/global/security/handler/CustomAccessDenied.java b/leeseo/umc10th/src/main/java/com/example/umc10th/global/security/handler/CustomAccessDenied.java new file mode 100644 index 0000000..09aa713 --- /dev/null +++ b/leeseo/umc10th/src/main/java/com/example/umc10th/global/security/handler/CustomAccessDenied.java @@ -0,0 +1,35 @@ +package com.example.umc10th.global.security.handler; + +import com.example.umc10th.global.apiPayload.ApiResponse; +import com.example.umc10th.global.apiPayload.code.BaseErrorCode; +import com.example.umc10th.global.apiPayload.code.GeneralErrorCode; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.web.access.AccessDeniedHandler; + +import java.io.IOException; + +public class CustomAccessDenied implements AccessDeniedHandler { + @Override + public void handle( + HttpServletRequest request, + HttpServletResponse response, + AccessDeniedException accessDeniedException + ) throws IOException, ServletException { + ObjectMapper objectMapper = new ObjectMapper(); + BaseErrorCode code = GeneralErrorCode.UNAUTHORIZED; + + // 응답 Content-Type, HTTP 상태코드 정의 + response.setContentType("application/json;charset=UTF-8"); + response.setStatus(code.getStatus().value()); + + // Response Body에 응답 통일한 객체 넣기 + ApiResponse errorResponse = ApiResponse.onFailure(code, null); + + // 실제 Response로 덮어쓰기 + objectMapper.writeValue(response.getOutputStream(), errorResponse); + } +} diff --git a/leeseo/umc10th/src/main/java/com/example/umc10th/global/security/handler/CustomEntryPoint.java b/leeseo/umc10th/src/main/java/com/example/umc10th/global/security/handler/CustomEntryPoint.java new file mode 100644 index 0000000..ff78e01 --- /dev/null +++ b/leeseo/umc10th/src/main/java/com/example/umc10th/global/security/handler/CustomEntryPoint.java @@ -0,0 +1,35 @@ +package com.example.umc10th.global.security.handler; + +import com.example.umc10th.global.apiPayload.ApiResponse; +import com.example.umc10th.global.apiPayload.code.BaseErrorCode; +import com.example.umc10th.global.apiPayload.code.GeneralErrorCode; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; + +import java.io.IOException; + +public class CustomEntryPoint implements AuthenticationEntryPoint { + @Override + public void commence( + HttpServletRequest request, + HttpServletResponse response, + AuthenticationException authException + ) throws IOException, ServletException { + ObjectMapper objectMapper = new ObjectMapper(); + BaseErrorCode code = GeneralErrorCode.UNAUTHORIZED; + + // 응답 Content-Type, HTTP 상태코드 정의 + response.setContentType("application/json;charset=UTF-8"); + response.setStatus(code.getStatus().value()); + + // Response Body에 응답 통일한 객체 넣기 + ApiResponse errorResponse = ApiResponse.onFailure(code, null); + + // 실제 Response로 덮어쓰기 + objectMapper.writeValue(response.getOutputStream(), errorResponse); + } +} From 81f34551d9f622885ff8e5f8af8c779123b4b3f0 Mon Sep 17 00:00:00 2001 From: seoyoon lee Date: Fri, 22 May 2026 21:18:53 +0900 Subject: [PATCH 09/13] =?UTF-8?q?feat:=20JWT=20=ED=86=A0=ED=81=B0=20?= =?UTF-8?q?=EB=B0=A9=EC=8B=9D=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20#36?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- leeseo/umc10th/build.gradle | 6 ++ .../auth/controller/AuthController.java | 7 ++ .../auth/controller/AuthControllerDocs.java | 8 ++ .../domain/auth/converter/AuthConverter.java | 9 ++ .../umc10th/domain/auth/dto/AuthReqDto.java | 10 ++ .../umc10th/domain/auth/dto/AuthResDto.java | 5 + .../auth/exception/code/AuthErrorCode.java | 18 ++++ .../auth/exception/code/AuthSuccessCode.java | 6 +- .../domain/auth/service/AuthService.java | 20 ++++ .../member/controller/MemberController.java | 2 +- .../umc10th/global/config/SecurityConfig.java | 15 +++ .../global/security/filter/JwtAuthFilter.java | 73 +++++++++++++++ .../umc10th/global/security/util/JwtUtil.java | 92 +++++++++++++++++++ .../src/main/resources/application.yml | 8 +- 14 files changed, 276 insertions(+), 3 deletions(-) create mode 100644 leeseo/umc10th/src/main/java/com/example/umc10th/domain/auth/exception/code/AuthErrorCode.java create mode 100644 leeseo/umc10th/src/main/java/com/example/umc10th/global/security/filter/JwtAuthFilter.java create mode 100644 leeseo/umc10th/src/main/java/com/example/umc10th/global/security/util/JwtUtil.java diff --git a/leeseo/umc10th/build.gradle b/leeseo/umc10th/build.gradle index 4810952..d57b051 100644 --- a/leeseo/umc10th/build.gradle +++ b/leeseo/umc10th/build.gradle @@ -39,6 +39,12 @@ dependencies { // Security implementation 'org.springframework.boot:spring-boot-starter-security' testImplementation 'org.springframework.security:spring-security-test' + + // Jwt + implementation 'io.jsonwebtoken:jjwt-api:0.12.3' + implementation 'io.jsonwebtoken:jjwt-impl:0.12.3' + implementation 'io.jsonwebtoken:jjwt-jackson:0.12.3' + implementation 'org.springframework.boot:spring-boot-configuration-processor' } tasks.named('test') { diff --git a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/auth/controller/AuthController.java b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/auth/controller/AuthController.java index 5e30339..25ebc3b 100644 --- a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/auth/controller/AuthController.java +++ b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/auth/controller/AuthController.java @@ -27,4 +27,11 @@ public ApiResponse signUp( ) { return ApiResponse.onSuccess(AuthSuccessCode.SIGNUP_OK, authService.signUp(dto)); } + + @PostMapping("/login") + public ApiResponse login( + @RequestBody @Valid AuthReqDto.Login dto + ) { + return ApiResponse.onSuccess(AuthSuccessCode.LOGIN_OK, authService.login(dto)); + } } diff --git a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/auth/controller/AuthControllerDocs.java b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/auth/controller/AuthControllerDocs.java index fec6af0..b68adba 100644 --- a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/auth/controller/AuthControllerDocs.java +++ b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/auth/controller/AuthControllerDocs.java @@ -16,4 +16,12 @@ public interface AuthControllerDocs { public ApiResponse signUp( @RequestBody @Valid AuthReqDto.Signup dto ); + + @Operation( + summary = "이메일 로그인", + description = "이메일 로그인 진행" + ) + public ApiResponse login( + @RequestBody @Valid AuthReqDto.Login dto + ); } diff --git a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/auth/converter/AuthConverter.java b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/auth/converter/AuthConverter.java index a43fab9..2337a45 100644 --- a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/auth/converter/AuthConverter.java +++ b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/auth/converter/AuthConverter.java @@ -1,6 +1,7 @@ package com.example.umc10th.domain.auth.converter; import com.example.umc10th.domain.auth.dto.AuthReqDto; +import com.example.umc10th.domain.auth.dto.AuthResDto; import com.example.umc10th.domain.member.entity.Member; import com.example.umc10th.domain.member.enums.SocialType; @@ -25,4 +26,12 @@ public static Member toMember( .socialType(SocialType.EMAIL) .build(); } + + public static AuthResDto.AccessToken toAccessToken( + String accessToken + ) { + return AuthResDto.AccessToken.builder() + .accessToken(accessToken) + .build(); + } } diff --git a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/auth/dto/AuthReqDto.java b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/auth/dto/AuthReqDto.java index 2b3063c..00018be 100644 --- a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/auth/dto/AuthReqDto.java +++ b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/auth/dto/AuthReqDto.java @@ -8,6 +8,7 @@ import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; +import lombok.Builder; import java.time.LocalDate; import java.util.List; @@ -34,4 +35,13 @@ public record Signup ( @NotNull List food ) {} + + @Schema(name = "Login", description = "로그인 정보를 저장합니다.") + public record Login ( + @Email + @NotBlank + String email, + @NotBlank + String password + ) {} } diff --git a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/auth/dto/AuthResDto.java b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/auth/dto/AuthResDto.java index 5de5251..97c67a3 100644 --- a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/auth/dto/AuthResDto.java +++ b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/auth/dto/AuthResDto.java @@ -10,4 +10,9 @@ public class AuthResDto { public record Id( Long id ) {} + + @Builder + public record AccessToken( + String accessToken + ) {} } diff --git a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/auth/exception/code/AuthErrorCode.java b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/auth/exception/code/AuthErrorCode.java new file mode 100644 index 0000000..1c30279 --- /dev/null +++ b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/auth/exception/code/AuthErrorCode.java @@ -0,0 +1,18 @@ +package com.example.umc10th.domain.auth.exception.code; + +import com.example.umc10th.global.apiPayload.code.BaseErrorCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; + +@Getter +@RequiredArgsConstructor +public enum AuthErrorCode implements BaseErrorCode { + INVALID_PASSWORD(HttpStatus.BAD_REQUEST, + "AUTH400_1", + "잘못된 비밀번호입니다."); + + private final HttpStatus status; + private final String code; + private final String message; +} diff --git a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/auth/exception/code/AuthSuccessCode.java b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/auth/exception/code/AuthSuccessCode.java index 96149d1..ce93630 100644 --- a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/auth/exception/code/AuthSuccessCode.java +++ b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/auth/exception/code/AuthSuccessCode.java @@ -11,7 +11,11 @@ public enum AuthSuccessCode implements BaseSuccessCode { SIGNUP_OK(HttpStatus.OK, "AUTH200_1", - "회원가입 되었습니다."); + "회원가입 되었습니다."), + + LOGIN_OK(HttpStatus.OK, + "AUTH200_2", + "로그인 되었습니다."); private final HttpStatus status; private final String code; diff --git a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/auth/service/AuthService.java b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/auth/service/AuthService.java index c4bd641..3a73035 100644 --- a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/auth/service/AuthService.java +++ b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/auth/service/AuthService.java @@ -3,6 +3,8 @@ import com.example.umc10th.domain.auth.converter.AuthConverter; import com.example.umc10th.domain.auth.dto.AuthReqDto; import com.example.umc10th.domain.auth.dto.AuthResDto; +import com.example.umc10th.domain.auth.exception.AuthException; +import com.example.umc10th.domain.auth.exception.code.AuthErrorCode; import com.example.umc10th.domain.member.converter.MemberConverter; import com.example.umc10th.domain.member.entity.Food; import com.example.umc10th.domain.member.entity.Member; @@ -14,7 +16,10 @@ import com.example.umc10th.domain.member.repository.FoodRepository; import com.example.umc10th.domain.member.repository.MemberRepository; import com.example.umc10th.domain.member.service.MemberService; +import com.example.umc10th.global.security.entity.AuthMember; +import com.example.umc10th.global.security.util.JwtUtil; import lombok.RequiredArgsConstructor; +import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -29,6 +34,7 @@ public class AuthService { private final FoodRepository foodRepository; private final PasswordEncoder passwordEncoder; private final MemberRepository memberRepository; + private final JwtUtil jwtUtil; @Transactional public AuthResDto.Id signUp(AuthReqDto.Signup dto) { @@ -46,4 +52,18 @@ public AuthResDto.Id signUp(AuthReqDto.Signup dto) { member.updateFoodList(list); return AuthResDto.Id.builder().id(member.getId()).build(); } + + public AuthResDto.AccessToken login(AuthReqDto.Login dto) { + Member member = memberRepository.findByEmail(dto.email()) + .orElseThrow(() -> new MemberException(MemberErrorCode.MEMBER_NOT_FOUND)); + + if (!passwordEncoder.matches(dto.password(), member.getPassword())) { + throw new AuthException(AuthErrorCode.INVALID_PASSWORD); + } + + AuthMember authMember = new AuthMember(member); + String accessToken = jwtUtil.createAccessToken(authMember); + + return AuthConverter.toAccessToken(accessToken); + } } diff --git a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java index 6c73c24..cb7fd1a 100644 --- a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java +++ b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java @@ -13,7 +13,7 @@ @RequiredArgsConstructor @RestController -@RequestMapping("/api/vi/members") +@RequestMapping("/api/v1/members") @Tag(name = "Member", description = "회원 관련 API") public class MemberController implements MemberControllerDocs{ diff --git a/leeseo/umc10th/src/main/java/com/example/umc10th/global/config/SecurityConfig.java b/leeseo/umc10th/src/main/java/com/example/umc10th/global/config/SecurityConfig.java index a90b69b..d0332f5 100644 --- a/leeseo/umc10th/src/main/java/com/example/umc10th/global/config/SecurityConfig.java +++ b/leeseo/umc10th/src/main/java/com/example/umc10th/global/config/SecurityConfig.java @@ -1,7 +1,11 @@ package com.example.umc10th.global.config; +import com.example.umc10th.global.security.filter.JwtAuthFilter; import com.example.umc10th.global.security.handler.CustomAccessDenied; import com.example.umc10th.global.security.handler.CustomEntryPoint; +import com.example.umc10th.global.security.service.CustomUserDetailsService; +import com.example.umc10th.global.security.util.JwtUtil; +import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; @@ -13,8 +17,12 @@ @EnableWebSecurity @Configuration +@RequiredArgsConstructor public class SecurityConfig { + private final JwtUtil jwtUtil; + private final CustomUserDetailsService customUserDetailsService; + private final String[] allowUris = { // Swagger 허용 "/swagger-ui/**", @@ -35,6 +43,8 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .defaultSuccessUrl("/swagger-ui/index.html", true) .permitAll() ) + .sessionManagement(AbstractHttpConfigurer::disable) + .addFilterBefore(jwtAuthFilter(), org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter.class) .logout(logout -> logout .logoutUrl("/logout") .logoutSuccessUrl("/login?logout") @@ -62,4 +72,9 @@ public CustomAccessDenied customAccessDenied() { public CustomEntryPoint customEntryPoint() { return new CustomEntryPoint(); } + + @Bean + public JwtAuthFilter jwtAuthFilter() { + return new JwtAuthFilter(jwtUtil, customUserDetailsService); + } } diff --git a/leeseo/umc10th/src/main/java/com/example/umc10th/global/security/filter/JwtAuthFilter.java b/leeseo/umc10th/src/main/java/com/example/umc10th/global/security/filter/JwtAuthFilter.java new file mode 100644 index 0000000..f31aed0 --- /dev/null +++ b/leeseo/umc10th/src/main/java/com/example/umc10th/global/security/filter/JwtAuthFilter.java @@ -0,0 +1,73 @@ +package com.example.umc10th.global.security.filter; + +import com.example.umc10th.global.apiPayload.ApiResponse; +import com.example.umc10th.global.apiPayload.code.BaseErrorCode; +import com.example.umc10th.global.apiPayload.code.GeneralErrorCode; +import com.example.umc10th.global.security.service.CustomUserDetailsService; +import com.example.umc10th.global.security.util.JwtUtil; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +@RequiredArgsConstructor +public class JwtAuthFilter extends OncePerRequestFilter { + + private final JwtUtil jwtUtil; + private final CustomUserDetailsService customUserDetailsService; + + @Override + protected void doFilterInternal( + @NonNull HttpServletRequest request, + @NonNull HttpServletResponse response, + @NonNull FilterChain filterChain + ) throws ServletException, IOException { + + try { + // 토큰 가져오기 + String token = request.getHeader("Authorization"); + // token이 없거나 Bearer가 아니면 넘기기 + if (token == null || !token.startsWith("Bearer ")) { + filterChain.doFilter(request, response); + return; + } + // Bearer이면 추출 + token = token.replace("Bearer ", ""); + // AccessToken 검증하기: 올바른 토큰이면 + if (jwtUtil.isValid(token)) { + // 토큰에서 이메일 추출 + String email = jwtUtil.getEmail(token); + // 인증 객체 생성: 이메일로 찾아온 뒤, 인증 객체 생성 + UserDetails user = customUserDetailsService.loadUserByUsername(email); + Authentication auth = new UsernamePasswordAuthenticationToken( + user, + null, + user.getAuthorities() + ); + // 인증 완료 후 SecurityContextHolder에 넣기 + SecurityContextHolder.getContext().setAuthentication(auth); + } + filterChain.doFilter(request, response); + } catch (Exception e) { + ObjectMapper mapper = new ObjectMapper(); + BaseErrorCode code = GeneralErrorCode.UNAUTHORIZED; + + response.setContentType("application/json;charset=UTF-8"); + response.setStatus(code.getStatus().value()); + + ApiResponse errorResponse = ApiResponse.onFailure(code,null); + + mapper.writeValue(response.getOutputStream(), errorResponse); + } + } +} diff --git a/leeseo/umc10th/src/main/java/com/example/umc10th/global/security/util/JwtUtil.java b/leeseo/umc10th/src/main/java/com/example/umc10th/global/security/util/JwtUtil.java new file mode 100644 index 0000000..b6d3f11 --- /dev/null +++ b/leeseo/umc10th/src/main/java/com/example/umc10th/global/security/util/JwtUtil.java @@ -0,0 +1,92 @@ +package com.example.umc10th.global.security.util; + +import com.example.umc10th.global.security.entity.AuthMember; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jws; +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.Keys; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.stereotype.Component; + +import javax.crypto.SecretKey; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.time.Instant; +import java.util.Date; +import java.util.stream.Collectors; + +@Component +public class JwtUtil { + private final SecretKey secretKey; + private final Duration accessExpiration; + + public JwtUtil( + @Value("${jwt.token.secretKey}") String secret, + @Value("${jwt.token.expiration.access}") Long accessExpiration + ) { + this.secretKey = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8)); + this.accessExpiration = Duration.ofMillis(accessExpiration); + } + + // AccessToken 생성 + public String createAccessToken(AuthMember member) { + return createToken(member, accessExpiration); + } + + /** 토큰에서 이메일 가져오기 + * + * @param token 유저 정보를 추출할 토큰 + * @return 유저 이메일을 토큰에서 추출합니다 + */ + public String getEmail(String token) { + try { + return getClaims(token).getPayload().getSubject(); // Parsing해서 Subject 가져오기 + } catch (JwtException e) { + return null; + } + } + + /** 토큰 유효성 확인 + * + * @param token 유효한지 확인할 토큰 + * @return True, False 반환합니다 + */ + public boolean isValid(String token) { + try { + getClaims(token); + return true; + } catch (JwtException e) { + return false; + } + } + + // 토큰 생성 + private String createToken(AuthMember member, Duration expiration) { + Instant now = Instant.now(); + + // 인가 정보 + String authorities = member.getAuthorities().stream() + .map(GrantedAuthority::getAuthority) + .collect(Collectors.joining(",")); + + return Jwts.builder() + .subject(member.getUsername()) // User 이메일을 Subject로 + .claim("role", authorities) + .claim("email", member.getUsername()) + .issuedAt(Date.from(now)) // 언제 발급한지 + .expiration(Date.from(now.plus(expiration))) // 언제까지 유효한지 + .signWith(secretKey) // sign할 Key + .compact(); + } + + // 토큰 정보 가져오기 + private Jws getClaims(String token) throws JwtException { + return Jwts.parser() + .verifyWith(secretKey) + .clockSkewSeconds(60) + .build() + .parseSignedClaims(token); + } +} diff --git a/leeseo/umc10th/src/main/resources/application.yml b/leeseo/umc10th/src/main/resources/application.yml index 5678389..aa6bf32 100644 --- a/leeseo/umc10th/src/main/resources/application.yml +++ b/leeseo/umc10th/src/main/resources/application.yml @@ -15,4 +15,10 @@ spring: ddl-auto: update properties: hibernate: - format_sql: true \ No newline at end of file + format_sql: true + +jwt: + token: + secretKey: ${JWT_SECRET_KEY} + expiration: + access: 1800000 # 30분 \ No newline at end of file From 26bc46883ea39821a9a1080cdb9bb6a9e51c9730 Mon Sep 17 00:00:00 2001 From: seoyoon lee Date: Fri, 22 May 2026 21:36:56 +0900 Subject: [PATCH 10/13] =?UTF-8?q?refactor:=20=EB=A7=88=EC=9D=B4=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20API=20=EA=B0=9C=EC=84=A0=20#37?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../member/controller/MemberController.java | 13 +++++++++++-- .../controller/MemberControllerDocs.java | 15 +++++++++++++-- .../member/converter/MemberConverter.java | 2 +- .../domain/member/dto/MemberResDto.java | 3 ++- .../domain/member/service/MemberService.java | 6 ++++++ .../mission/controller/MissionController.java | 18 +++++++++--------- .../controller/MissionControllerDocs.java | 8 +++++--- .../review/controller/ReviewController.java | 8 +++++--- .../controller/ReviewControllerDocs.java | 4 +++- 9 files changed, 55 insertions(+), 22 deletions(-) diff --git a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java index cb7fd1a..a171838 100644 --- a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java +++ b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java @@ -2,13 +2,14 @@ import com.example.umc10th.domain.member.dto.MemberReqDto; import com.example.umc10th.domain.member.dto.MemberResDto; -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 com.example.umc10th.global.security.entity.AuthMember; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; @RequiredArgsConstructor @@ -28,8 +29,16 @@ public ApiResponse saveTermAgreement( } @GetMapping("/me/profile") + public ApiResponse getMyProfile( + @AuthenticationPrincipal AuthMember member + ) { + MemberResDto.Profile response = memberService.getMyProfile(member.getMember()); + return ApiResponse.onSuccess(MemberSuccessCode.PROFILE_GET_OK, response); + } + + @GetMapping("/{id}/profile") public ApiResponse getProfile( - @RequestParam Long id + @PathVariable Long id ) { MemberResDto.Profile response = memberService.getProfile(id); return ApiResponse.onSuccess(MemberSuccessCode.PROFILE_GET_OK, response); diff --git a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/member/controller/MemberControllerDocs.java b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/member/controller/MemberControllerDocs.java index 28022fa..7ca7c90 100644 --- a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/member/controller/MemberControllerDocs.java +++ b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/member/controller/MemberControllerDocs.java @@ -3,8 +3,11 @@ import com.example.umc10th.domain.member.dto.MemberReqDto; import com.example.umc10th.domain.member.dto.MemberResDto; import com.example.umc10th.global.apiPayload.ApiResponse; +import com.example.umc10th.global.security.entity.AuthMember; import io.swagger.v3.oas.annotations.Operation; import jakarta.validation.Valid; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestParam; @@ -19,11 +22,19 @@ public ApiResponse saveTermAgreement( ); @Operation( - summary = "회원정보 조회", + summary = "내 프로필 조회", + description = "내 프로필 정보를 조회합니다." + ) + public ApiResponse getMyProfile( + @AuthenticationPrincipal AuthMember member + ); + + @Operation( + summary = "타인 프로필 조회", description = "회원 프로필 정보를 조회합니다." ) public ApiResponse getProfile( - @RequestParam Long id + @PathVariable Long id ); @Operation( diff --git a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/member/converter/MemberConverter.java b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/member/converter/MemberConverter.java index 7e73c88..2a4fc63 100644 --- a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/member/converter/MemberConverter.java +++ b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/member/converter/MemberConverter.java @@ -17,7 +17,7 @@ public static MemberResDto.Profile toProfile( .address(member.getAddress()) .fullAddress(member.getFullAddress()) .gender(member.getGender()) - .food(member.getMemberFoodList().stream().map(MemberFood::getFood).toList()) + .food(member.getMemberFoodList().stream().map(mf -> mf.getFood().getName()).toList()) .build(); } diff --git a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/member/dto/MemberResDto.java b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/member/dto/MemberResDto.java index 126d6b3..6635d24 100644 --- a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/member/dto/MemberResDto.java +++ b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/member/dto/MemberResDto.java @@ -1,6 +1,7 @@ package com.example.umc10th.domain.member.dto; import com.example.umc10th.domain.member.entity.Food; +import com.example.umc10th.domain.member.enums.FoodType; import com.example.umc10th.domain.member.enums.Gender; import com.example.umc10th.domain.mission.enums.Address; import lombok.Builder; @@ -18,6 +19,6 @@ public record Profile ( LocalDate birth, Address address, String fullAddress, - List food + List food ) {} } diff --git a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/member/service/MemberService.java b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/member/service/MemberService.java index af33653..2816491 100644 --- a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/member/service/MemberService.java +++ b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/member/service/MemberService.java @@ -26,6 +26,12 @@ public class MemberService { private final MemberRepository memberRepository; private final FoodRepository foodRepository; + public MemberResDto.Profile getMyProfile(Member member) { + Member memberWithFood = memberRepository.findMemberWithFoods(member.getId()) + .orElseThrow(() -> new MemberException(MemberErrorCode.MEMBER_NOT_FOUND)); + return MemberConverter.toProfile(memberWithFood); + } + public MemberResDto.Profile getProfile(Long id) { Member memberWithFood = memberRepository.findMemberWithFoods(id) .orElseThrow(() -> new MemberException(MemberErrorCode.MEMBER_NOT_FOUND)); diff --git a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/mission/controller/MissionController.java b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/mission/controller/MissionController.java index 4d5432e..6906ac9 100644 --- a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/mission/controller/MissionController.java +++ b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/mission/controller/MissionController.java @@ -6,10 +6,10 @@ import com.example.umc10th.domain.mission.exception.code.MissionSuccessCode; import com.example.umc10th.domain.mission.service.MissionService; import com.example.umc10th.global.apiPayload.ApiResponse; +import com.example.umc10th.global.security.entity.AuthMember; import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.persistence.criteria.CriteriaBuilder; import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Slice; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; @RequiredArgsConstructor @@ -23,9 +23,9 @@ public class MissionController implements MissionControllerDocs{ @GetMapping("/missions/achievement-rate") public ApiResponse getMissionAchievement( @RequestParam Address location, - @RequestParam Long memberId - ) { - Achievement response = missionService.getMissionAchievement(location, memberId); + @AuthenticationPrincipal AuthMember member + ) { + Achievement response = missionService.getMissionAchievement(location, member.getMember().getId()); return ApiResponse.onSuccess(MissionSuccessCode.ACHIEVEMENT_GET_OK, response); } @@ -43,21 +43,21 @@ public ApiResponse> getMissionList( @PostMapping("/members/me/missions/{missionId}") public ApiResponse saveMemberMission( @PathVariable Long missionId, - @RequestParam Long memberId + @AuthenticationPrincipal AuthMember member ) { - missionService.saveMemberMission(missionId,memberId); + missionService.saveMemberMission(missionId,member.getMember().getId()); return ApiResponse.onSuccess(MissionSuccessCode.MEMBER_MISSION_POST_OK,null); } @GetMapping("/members/me/missions") public ApiResponse> getMyMissions( @RequestParam Status status, - @RequestParam Long memberId, + @AuthenticationPrincipal AuthMember member, @RequestParam Integer pageSize, @RequestParam Integer pageNumber, @RequestParam (required = false) String sort ) { - MissionResDto.Pagination response = missionService.getMyMissionList(status, memberId, pageSize, pageNumber, sort); + MissionResDto.Pagination response = missionService.getMyMissionList(status, member.getMember().getId(), pageSize, pageNumber, sort); return ApiResponse.onSuccess(MissionSuccessCode.MY_MISSION_LIST_GET_OK, response); } diff --git a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/mission/controller/MissionControllerDocs.java b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/mission/controller/MissionControllerDocs.java index 0cfa2f5..037d6b8 100644 --- a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/mission/controller/MissionControllerDocs.java +++ b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/mission/controller/MissionControllerDocs.java @@ -4,7 +4,9 @@ import com.example.umc10th.domain.mission.enums.Address; import com.example.umc10th.domain.mission.enums.Status; import com.example.umc10th.global.apiPayload.ApiResponse; +import com.example.umc10th.global.security.entity.AuthMember; import io.swagger.v3.oas.annotations.Operation; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestParam; @@ -16,7 +18,7 @@ public interface MissionControllerDocs { ) public ApiResponse getMissionAchievement( @RequestParam Address location, - @RequestParam Long memberId + @AuthenticationPrincipal AuthMember member ); @Operation( @@ -36,7 +38,7 @@ public ApiResponse> getMissionList( ) public ApiResponse saveMemberMission( @PathVariable Long missionId, - @RequestParam Long memberId + @AuthenticationPrincipal AuthMember member ); @Operation( @@ -45,7 +47,7 @@ public ApiResponse saveMemberMission( ) public ApiResponse> getMyMissions( @RequestParam Status status, - @RequestParam Long memberId, + @AuthenticationPrincipal AuthMember member, @RequestParam Integer pageSize, @RequestParam Integer pageNumber, @RequestParam (required = false) String sort diff --git a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/review/controller/ReviewController.java b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/review/controller/ReviewController.java index b11203f..dd62a5c 100644 --- a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/review/controller/ReviewController.java +++ b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/review/controller/ReviewController.java @@ -7,9 +7,11 @@ import com.example.umc10th.domain.review.exception.code.ReviewSuccessCode; import com.example.umc10th.domain.review.service.ReviewService; import com.example.umc10th.global.apiPayload.ApiResponse; +import com.example.umc10th.global.security.entity.AuthMember; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; import java.util.List; @@ -26,9 +28,9 @@ public class ReviewController implements ReviewControllerDocs{ public ApiResponse saveReview( @PathVariable Long storeId, @RequestBody @Valid ReviewReqDto.Review dto, - @RequestParam Long memberId - ) { - reviewService.saveReview(storeId, memberId, dto); + @AuthenticationPrincipal AuthMember member + ) { + reviewService.saveReview(storeId, member.getMember().getId(), dto); return ApiResponse.onSuccess(ReviewSuccessCode.REVIEW_POST_OK, null); } diff --git a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/review/controller/ReviewControllerDocs.java b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/review/controller/ReviewControllerDocs.java index 3d72714..1166168 100644 --- a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/review/controller/ReviewControllerDocs.java +++ b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/review/controller/ReviewControllerDocs.java @@ -5,7 +5,9 @@ import com.example.umc10th.domain.review.dto.ReviewReqDto; import com.example.umc10th.domain.review.dto.ReviewResDto; import com.example.umc10th.global.apiPayload.ApiResponse; +import com.example.umc10th.global.security.entity.AuthMember; import io.swagger.v3.oas.annotations.Operation; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestParam; @@ -21,7 +23,7 @@ public interface ReviewControllerDocs { public ApiResponse saveReview( @PathVariable Long storeId, @RequestBody ReviewReqDto.Review dto, - @RequestParam Long memberId + @AuthenticationPrincipal AuthMember member ); @Operation( From 6d411d7cea0d7346048b502ba121adbf87d17103 Mon Sep 17 00:00:00 2001 From: seoyoon lee Date: Fri, 22 May 2026 23:49:55 +0900 Subject: [PATCH 11/13] =?UTF-8?q?feat:=20kakao=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20=EA=B5=AC=ED=98=84=20#36?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- leeseo/umc10th/build.gradle | 3 + .../domain/auth/converter/AuthConverter.java | 12 ++++ .../exception/code/MemberErrorCode.java | 6 +- .../member/repository/MemberRepository.java | 3 + .../umc10th/global/config/SecurityConfig.java | 30 ++++++-- .../umc10th/global/security/dto/KakaoDto.java | 32 +++++++++ .../umc10th/global/security/dto/OAuthDto.java | 10 +++ .../global/security/entity/OAuthMember.java | 37 ++++++++++ .../security/handler/OAuthSuccessHandler.java | 57 +++++++++++++++ .../security/service/CustomOAuthService.java | 71 +++++++++++++++++++ .../service/CustomUserDetailsService.java | 1 - .../src/main/resources/application.yml | 26 ++++++- 12 files changed, 279 insertions(+), 9 deletions(-) create mode 100644 leeseo/umc10th/src/main/java/com/example/umc10th/global/security/dto/KakaoDto.java create mode 100644 leeseo/umc10th/src/main/java/com/example/umc10th/global/security/dto/OAuthDto.java create mode 100644 leeseo/umc10th/src/main/java/com/example/umc10th/global/security/entity/OAuthMember.java create mode 100644 leeseo/umc10th/src/main/java/com/example/umc10th/global/security/handler/OAuthSuccessHandler.java create mode 100644 leeseo/umc10th/src/main/java/com/example/umc10th/global/security/service/CustomOAuthService.java diff --git a/leeseo/umc10th/build.gradle b/leeseo/umc10th/build.gradle index d57b051..d543f5e 100644 --- a/leeseo/umc10th/build.gradle +++ b/leeseo/umc10th/build.gradle @@ -45,6 +45,9 @@ dependencies { implementation 'io.jsonwebtoken:jjwt-impl:0.12.3' implementation 'io.jsonwebtoken:jjwt-jackson:0.12.3' implementation 'org.springframework.boot:spring-boot-configuration-processor' + + // OAuth + implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' } tasks.named('test') { diff --git a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/auth/converter/AuthConverter.java b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/auth/converter/AuthConverter.java index 2337a45..37ccd9e 100644 --- a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/auth/converter/AuthConverter.java +++ b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/auth/converter/AuthConverter.java @@ -4,6 +4,7 @@ import com.example.umc10th.domain.auth.dto.AuthResDto; import com.example.umc10th.domain.member.entity.Member; import com.example.umc10th.domain.member.enums.SocialType; +import com.example.umc10th.global.security.dto.OAuthDto; import java.util.UUID; @@ -27,6 +28,17 @@ public static Member toMember( .build(); } + public static Member toSocialMember( + OAuthDto dto + ) { + return Member.builder() + .email(dto.getSocialEmail()) + .nickname(dto.getName()) + .socialId(dto.getSocialUid()) + .socialType(dto.getSocialType()) + .build(); + } + public static AuthResDto.AccessToken toAccessToken( String accessToken ) { diff --git a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/member/exception/code/MemberErrorCode.java b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/member/exception/code/MemberErrorCode.java index f1ceb9c..94e84b3 100644 --- a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/member/exception/code/MemberErrorCode.java +++ b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/member/exception/code/MemberErrorCode.java @@ -14,7 +14,11 @@ public enum MemberErrorCode implements BaseErrorCode { BAD_MEMBER_INFO(HttpStatus.BAD_REQUEST, "MEMBER400_1", - "해당 정보를 저장할 수 없습니다."); + "해당 정보를 저장할 수 없습니다."), + + NOT_SUPPORT_SOCIAL_PROVIDER(HttpStatus.BAD_REQUEST, + "MEMBER400_2", + "허용하지 않는 소셜 타입입니다."); private final HttpStatus status; private final String code; diff --git a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/member/repository/MemberRepository.java b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/member/repository/MemberRepository.java index 066bc83..c2c893d 100644 --- a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/member/repository/MemberRepository.java +++ b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/member/repository/MemberRepository.java @@ -1,6 +1,7 @@ package com.example.umc10th.domain.member.repository; import com.example.umc10th.domain.member.entity.Member; +import com.example.umc10th.domain.member.enums.SocialType; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; @@ -16,4 +17,6 @@ public interface MemberRepository extends JpaRepository { """) Optional findMemberWithFoods(Long id); Optional findByEmail(String email); + + Optional findBySocialTypeAndSocialId(SocialType socialType, String socialUid); } diff --git a/leeseo/umc10th/src/main/java/com/example/umc10th/global/config/SecurityConfig.java b/leeseo/umc10th/src/main/java/com/example/umc10th/global/config/SecurityConfig.java index d0332f5..25a83dd 100644 --- a/leeseo/umc10th/src/main/java/com/example/umc10th/global/config/SecurityConfig.java +++ b/leeseo/umc10th/src/main/java/com/example/umc10th/global/config/SecurityConfig.java @@ -3,6 +3,8 @@ import com.example.umc10th.global.security.filter.JwtAuthFilter; import com.example.umc10th.global.security.handler.CustomAccessDenied; import com.example.umc10th.global.security.handler.CustomEntryPoint; +import com.example.umc10th.global.security.handler.OAuthSuccessHandler; +import com.example.umc10th.global.security.service.CustomOAuthService; import com.example.umc10th.global.security.service.CustomUserDetailsService; import com.example.umc10th.global.security.util.JwtUtil; import lombok.RequiredArgsConstructor; @@ -28,23 +30,34 @@ public class SecurityConfig { "/swagger-ui/**", "/swagger-resources/**", "/v3/api-docs/**", - "/api/v1/auth/**" + "/api/v1/auth/**", + "/login", + "/oauth/**" }; @Bean - public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + public SecurityFilterChain securityFilterChain(HttpSecurity http, CustomOAuthService customOAuthService) throws Exception { http .csrf(AbstractHttpConfigurer::disable) .authorizeHttpRequests(requests -> requests .requestMatchers(allowUris).permitAll() .anyRequest().authenticated() ) - .formLogin(form -> form - .defaultSuccessUrl("/swagger-ui/index.html", true) - .permitAll() - ) + .formLogin(AbstractHttpConfigurer::disable) .sessionManagement(AbstractHttpConfigurer::disable) .addFilterBefore(jwtAuthFilter(), org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter.class) + .oauth2Login(oauth -> oauth + .authorizationEndpoint(auth -> auth + .baseUri("/oauth/authorization") + ) + .redirectionEndpoint(redirect -> redirect + .baseUri("/oauth/callback/**") + ) + .userInfoEndpoint(userInfo -> { + userInfo.userService(customOAuthService); + }) + .successHandler(oAuthSuccessHandler()) + ) .logout(logout -> logout .logoutUrl("/logout") .logoutSuccessUrl("/login?logout") @@ -77,4 +90,9 @@ public CustomEntryPoint customEntryPoint() { public JwtAuthFilter jwtAuthFilter() { return new JwtAuthFilter(jwtUtil, customUserDetailsService); } + + @Bean + public OAuthSuccessHandler oAuthSuccessHandler() { + return new OAuthSuccessHandler(jwtUtil); + } } diff --git a/leeseo/umc10th/src/main/java/com/example/umc10th/global/security/dto/KakaoDto.java b/leeseo/umc10th/src/main/java/com/example/umc10th/global/security/dto/KakaoDto.java new file mode 100644 index 0000000..0806fef --- /dev/null +++ b/leeseo/umc10th/src/main/java/com/example/umc10th/global/security/dto/KakaoDto.java @@ -0,0 +1,32 @@ +package com.example.umc10th.global.security.dto; + +import com.example.umc10th.domain.member.enums.SocialType; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public class KakaoDto implements OAuthDto{ + + private final String id; + private final String email; + private final String name; + + @Override + public SocialType getSocialType() { + return SocialType.KAKAO; + } + + @Override + public String getSocialUid() { + return id; + } + + @Override + public String getSocialEmail() { + return email; + } + + @Override + public String getName() { + return name; + } +} diff --git a/leeseo/umc10th/src/main/java/com/example/umc10th/global/security/dto/OAuthDto.java b/leeseo/umc10th/src/main/java/com/example/umc10th/global/security/dto/OAuthDto.java new file mode 100644 index 0000000..c7bbb03 --- /dev/null +++ b/leeseo/umc10th/src/main/java/com/example/umc10th/global/security/dto/OAuthDto.java @@ -0,0 +1,10 @@ +package com.example.umc10th.global.security.dto; + +import com.example.umc10th.domain.member.enums.SocialType; + +public interface OAuthDto { + SocialType getSocialType(); + String getSocialUid(); + String getSocialEmail(); + String getName(); +} diff --git a/leeseo/umc10th/src/main/java/com/example/umc10th/global/security/entity/OAuthMember.java b/leeseo/umc10th/src/main/java/com/example/umc10th/global/security/entity/OAuthMember.java new file mode 100644 index 0000000..9fb4231 --- /dev/null +++ b/leeseo/umc10th/src/main/java/com/example/umc10th/global/security/entity/OAuthMember.java @@ -0,0 +1,37 @@ +package com.example.umc10th.global.security.entity; + +import com.example.umc10th.domain.member.entity.Member; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.jspecify.annotations.Nullable; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.oauth2.core.user.OAuth2User; + +import java.util.Collection; +import java.util.List; +import java.util.Map; + +@Getter +@RequiredArgsConstructor +public class OAuthMember implements OAuth2User { + + @Getter + private final Member member; + private final Map attributes; + + @Override + public Map getAttributes() { + return attributes; + } + + @Override + public String getName() { + return this.member.getSocialId(); + } + + @Override + public Collection getAuthorities() { + return List.of(); + } +} diff --git a/leeseo/umc10th/src/main/java/com/example/umc10th/global/security/handler/OAuthSuccessHandler.java b/leeseo/umc10th/src/main/java/com/example/umc10th/global/security/handler/OAuthSuccessHandler.java new file mode 100644 index 0000000..b14a863 --- /dev/null +++ b/leeseo/umc10th/src/main/java/com/example/umc10th/global/security/handler/OAuthSuccessHandler.java @@ -0,0 +1,57 @@ +package com.example.umc10th.global.security.handler; + +import com.example.umc10th.domain.auth.converter.AuthConverter; +import com.example.umc10th.domain.auth.dto.AuthResDto; +import com.example.umc10th.domain.auth.exception.code.AuthSuccessCode; +import com.example.umc10th.domain.member.converter.MemberConverter; +import com.example.umc10th.domain.member.dto.MemberResDto; +import com.example.umc10th.domain.member.exception.code.MemberSuccessCode; +import com.example.umc10th.global.apiPayload.ApiResponse; +import com.example.umc10th.global.apiPayload.code.BaseSuccessCode; +import com.example.umc10th.global.security.entity.AuthMember; +import com.example.umc10th.global.security.entity.OAuthMember; +import com.example.umc10th.global.security.util.JwtUtil; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.authentication.AuthenticationSuccessHandler; + +import java.io.IOException; + +@RequiredArgsConstructor +public class OAuthSuccessHandler implements AuthenticationSuccessHandler { + + private final JwtUtil jwtUtil; + + @Override + public void onAuthenticationSuccess( + HttpServletRequest request, + HttpServletResponse response, + Authentication authentication + ) throws IOException, ServletException { + ObjectMapper objectMapper = new ObjectMapper(); + BaseSuccessCode code = AuthSuccessCode.LOGIN_OK; + + response.setContentType("application/json;charset=UTF-8"); + response.setStatus(code.getStatus().value()); + + // 1. OAuth 멤버 추출 및 토큰 생성 + OAuthMember member = (OAuthMember) authentication.getPrincipal(); + String accessToken = jwtUtil.createAccessToken(new AuthMember(member.getMember())); + + // 2. 💡 [필수 추가] 현재 인증 객체를 SecurityContext에 확실하게 박아줍니다. + // 이렇게 해야 필터 체인이 끝날 때 '인증 안 됨(401)'으로 오해하지 않습니다. + SecurityContextHolder.getContext().setAuthentication(authentication); + + // 3. 응답 출력 + ApiResponse responseBody = ApiResponse.onSuccess( + code, + AuthConverter.toAccessToken(accessToken) + ); + objectMapper.writeValue(response.getOutputStream(), responseBody); + } +} diff --git a/leeseo/umc10th/src/main/java/com/example/umc10th/global/security/service/CustomOAuthService.java b/leeseo/umc10th/src/main/java/com/example/umc10th/global/security/service/CustomOAuthService.java new file mode 100644 index 0000000..4023ff4 --- /dev/null +++ b/leeseo/umc10th/src/main/java/com/example/umc10th/global/security/service/CustomOAuthService.java @@ -0,0 +1,71 @@ +package com.example.umc10th.global.security.service; + +import com.example.umc10th.domain.auth.converter.AuthConverter; +import com.example.umc10th.domain.member.converter.MemberConverter; +import com.example.umc10th.domain.member.entity.Member; +import com.example.umc10th.domain.member.enums.SocialType; +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.global.security.dto.KakaoDto; +import com.example.umc10th.global.security.dto.OAuthDto; +import com.example.umc10th.global.security.entity.OAuthMember; +import lombok.RequiredArgsConstructor; +import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.stereotype.Service; + +import java.util.Map; + +@Service +@RequiredArgsConstructor +public class CustomOAuthService extends DefaultOAuth2UserService { + + private final MemberRepository memberRepository; + + @Override + public OAuth2User loadUser( + OAuth2UserRequest userRequest + ) throws OAuth2AuthenticationException { + // (필수) 인증 서버의 일회성 토큰을 이용해 정보 조회 & 유저 객체 생성 + OAuth2User oAuthMember = super.loadUser(userRequest); + + Map rootAttributes = oAuthMember.getAttributes(); + + // 유저 객체에서 정보 추출 + SocialType providerId; + String socialUid; + Map kakaoAccount = (Map) rootAttributes.get("kakao_account"); + Map profile = (Map) kakaoAccount.get("profile"); + try { + providerId = SocialType.valueOf(userRequest.getClientRegistration().getRegistrationId().toUpperCase()); + socialUid = String.valueOf(rootAttributes.get("id")); + } catch (IllegalArgumentException e) { + throw new MemberException(MemberErrorCode.NOT_SUPPORT_SOCIAL_PROVIDER); + } + + // OAuth 공통 정보 DTO로 매핑 + OAuthDto dto; + switch (providerId) { + case KAKAO -> { + String email = kakaoAccount.get("email").toString(); + String name = profile.get("nickname").toString(); + dto = new KakaoDto(socialUid, email, name); + } + default -> throw new MemberException(MemberErrorCode.NOT_SUPPORT_SOCIAL_PROVIDER); + } + + System.out.println("socialId:" + socialUid); + + // DB 저장: 있다면 그 데이터 가져오고 없으면 새로 저장 + Member member = memberRepository.findBySocialTypeAndSocialId(providerId, socialUid) + .orElseGet(() -> { + Member newMember = AuthConverter.toSocialMember(dto); + memberRepository.save(newMember); + return newMember; + }); + return new OAuthMember(member, oAuthMember.getAttributes()); + } +} diff --git a/leeseo/umc10th/src/main/java/com/example/umc10th/global/security/service/CustomUserDetailsService.java b/leeseo/umc10th/src/main/java/com/example/umc10th/global/security/service/CustomUserDetailsService.java index a4b3470..28e49ef 100644 --- a/leeseo/umc10th/src/main/java/com/example/umc10th/global/security/service/CustomUserDetailsService.java +++ b/leeseo/umc10th/src/main/java/com/example/umc10th/global/security/service/CustomUserDetailsService.java @@ -17,7 +17,6 @@ public class CustomUserDetailsService implements UserDetailsService { private final MemberRepository memberRepository; - @Override public UserDetails loadUserByUsername( String username ) throws UsernameNotFoundException { diff --git a/leeseo/umc10th/src/main/resources/application.yml b/leeseo/umc10th/src/main/resources/application.yml index aa6bf32..3febb84 100644 --- a/leeseo/umc10th/src/main/resources/application.yml +++ b/leeseo/umc10th/src/main/resources/application.yml @@ -7,6 +7,26 @@ spring: username: ${DB_USER} password: ${DB_PW} + security: + oauth2: + client: + registration: + kakao: + client-id: ${KAKAO_REST_API_KEY} + client-secret: ${KAKAO_REST_API_SECRET} + client-authentication-method: client_secret_post + authorization-grant-type: authorization_code + redirect-uri: "{baseUrl}/oauth/callback/{registrationId}" + scope: + - profile_nickname + - account_email + provider: + kakao: + authorization-uri: "https://kauth.kakao.com/oauth/authorize" + token-uri: "https://kauth.kakao.com/oauth/token" + user-info-uri: "https://kapi.kakao.com/v2/user/me" + user-name-attribute: id + jpa: database: mysql database-platform: org.hibernate.dialect.MySQLDialect @@ -21,4 +41,8 @@ jwt: token: secretKey: ${JWT_SECRET_KEY} expiration: - access: 1800000 # 30분 \ No newline at end of file + access: 1800000 # 30분 + +logging: + level: + org.springframework.security: DEBUG \ No newline at end of file From ab36f1d130d2ace04db98720b562b7da4337fa34 Mon Sep 17 00:00:00 2001 From: seoyoon lee Date: Fri, 22 May 2026 21:36:56 +0900 Subject: [PATCH 12/13] =?UTF-8?q?refactor:=20=EB=A7=88=EC=9D=B4=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20API=20=EA=B0=9C=EC=84=A0=20#36?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../member/controller/MemberController.java | 13 +++++++++++-- .../controller/MemberControllerDocs.java | 15 +++++++++++++-- .../member/converter/MemberConverter.java | 2 +- .../domain/member/dto/MemberResDto.java | 3 ++- .../domain/member/service/MemberService.java | 6 ++++++ .../mission/controller/MissionController.java | 18 +++++++++--------- .../controller/MissionControllerDocs.java | 8 +++++--- .../review/controller/ReviewController.java | 8 +++++--- .../controller/ReviewControllerDocs.java | 4 +++- 9 files changed, 55 insertions(+), 22 deletions(-) diff --git a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java index cb7fd1a..a171838 100644 --- a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java +++ b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java @@ -2,13 +2,14 @@ import com.example.umc10th.domain.member.dto.MemberReqDto; import com.example.umc10th.domain.member.dto.MemberResDto; -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 com.example.umc10th.global.security.entity.AuthMember; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; @RequiredArgsConstructor @@ -28,8 +29,16 @@ public ApiResponse saveTermAgreement( } @GetMapping("/me/profile") + public ApiResponse getMyProfile( + @AuthenticationPrincipal AuthMember member + ) { + MemberResDto.Profile response = memberService.getMyProfile(member.getMember()); + return ApiResponse.onSuccess(MemberSuccessCode.PROFILE_GET_OK, response); + } + + @GetMapping("/{id}/profile") public ApiResponse getProfile( - @RequestParam Long id + @PathVariable Long id ) { MemberResDto.Profile response = memberService.getProfile(id); return ApiResponse.onSuccess(MemberSuccessCode.PROFILE_GET_OK, response); diff --git a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/member/controller/MemberControllerDocs.java b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/member/controller/MemberControllerDocs.java index 28022fa..7ca7c90 100644 --- a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/member/controller/MemberControllerDocs.java +++ b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/member/controller/MemberControllerDocs.java @@ -3,8 +3,11 @@ import com.example.umc10th.domain.member.dto.MemberReqDto; import com.example.umc10th.domain.member.dto.MemberResDto; import com.example.umc10th.global.apiPayload.ApiResponse; +import com.example.umc10th.global.security.entity.AuthMember; import io.swagger.v3.oas.annotations.Operation; import jakarta.validation.Valid; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestParam; @@ -19,11 +22,19 @@ public ApiResponse saveTermAgreement( ); @Operation( - summary = "회원정보 조회", + summary = "내 프로필 조회", + description = "내 프로필 정보를 조회합니다." + ) + public ApiResponse getMyProfile( + @AuthenticationPrincipal AuthMember member + ); + + @Operation( + summary = "타인 프로필 조회", description = "회원 프로필 정보를 조회합니다." ) public ApiResponse getProfile( - @RequestParam Long id + @PathVariable Long id ); @Operation( diff --git a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/member/converter/MemberConverter.java b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/member/converter/MemberConverter.java index 7e73c88..2a4fc63 100644 --- a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/member/converter/MemberConverter.java +++ b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/member/converter/MemberConverter.java @@ -17,7 +17,7 @@ public static MemberResDto.Profile toProfile( .address(member.getAddress()) .fullAddress(member.getFullAddress()) .gender(member.getGender()) - .food(member.getMemberFoodList().stream().map(MemberFood::getFood).toList()) + .food(member.getMemberFoodList().stream().map(mf -> mf.getFood().getName()).toList()) .build(); } diff --git a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/member/dto/MemberResDto.java b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/member/dto/MemberResDto.java index 126d6b3..6635d24 100644 --- a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/member/dto/MemberResDto.java +++ b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/member/dto/MemberResDto.java @@ -1,6 +1,7 @@ package com.example.umc10th.domain.member.dto; import com.example.umc10th.domain.member.entity.Food; +import com.example.umc10th.domain.member.enums.FoodType; import com.example.umc10th.domain.member.enums.Gender; import com.example.umc10th.domain.mission.enums.Address; import lombok.Builder; @@ -18,6 +19,6 @@ public record Profile ( LocalDate birth, Address address, String fullAddress, - List food + List food ) {} } diff --git a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/member/service/MemberService.java b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/member/service/MemberService.java index af33653..2816491 100644 --- a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/member/service/MemberService.java +++ b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/member/service/MemberService.java @@ -26,6 +26,12 @@ public class MemberService { private final MemberRepository memberRepository; private final FoodRepository foodRepository; + public MemberResDto.Profile getMyProfile(Member member) { + Member memberWithFood = memberRepository.findMemberWithFoods(member.getId()) + .orElseThrow(() -> new MemberException(MemberErrorCode.MEMBER_NOT_FOUND)); + return MemberConverter.toProfile(memberWithFood); + } + public MemberResDto.Profile getProfile(Long id) { Member memberWithFood = memberRepository.findMemberWithFoods(id) .orElseThrow(() -> new MemberException(MemberErrorCode.MEMBER_NOT_FOUND)); diff --git a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/mission/controller/MissionController.java b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/mission/controller/MissionController.java index 4d5432e..6906ac9 100644 --- a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/mission/controller/MissionController.java +++ b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/mission/controller/MissionController.java @@ -6,10 +6,10 @@ import com.example.umc10th.domain.mission.exception.code.MissionSuccessCode; import com.example.umc10th.domain.mission.service.MissionService; import com.example.umc10th.global.apiPayload.ApiResponse; +import com.example.umc10th.global.security.entity.AuthMember; import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.persistence.criteria.CriteriaBuilder; import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Slice; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; @RequiredArgsConstructor @@ -23,9 +23,9 @@ public class MissionController implements MissionControllerDocs{ @GetMapping("/missions/achievement-rate") public ApiResponse getMissionAchievement( @RequestParam Address location, - @RequestParam Long memberId - ) { - Achievement response = missionService.getMissionAchievement(location, memberId); + @AuthenticationPrincipal AuthMember member + ) { + Achievement response = missionService.getMissionAchievement(location, member.getMember().getId()); return ApiResponse.onSuccess(MissionSuccessCode.ACHIEVEMENT_GET_OK, response); } @@ -43,21 +43,21 @@ public ApiResponse> getMissionList( @PostMapping("/members/me/missions/{missionId}") public ApiResponse saveMemberMission( @PathVariable Long missionId, - @RequestParam Long memberId + @AuthenticationPrincipal AuthMember member ) { - missionService.saveMemberMission(missionId,memberId); + missionService.saveMemberMission(missionId,member.getMember().getId()); return ApiResponse.onSuccess(MissionSuccessCode.MEMBER_MISSION_POST_OK,null); } @GetMapping("/members/me/missions") public ApiResponse> getMyMissions( @RequestParam Status status, - @RequestParam Long memberId, + @AuthenticationPrincipal AuthMember member, @RequestParam Integer pageSize, @RequestParam Integer pageNumber, @RequestParam (required = false) String sort ) { - MissionResDto.Pagination response = missionService.getMyMissionList(status, memberId, pageSize, pageNumber, sort); + MissionResDto.Pagination response = missionService.getMyMissionList(status, member.getMember().getId(), pageSize, pageNumber, sort); return ApiResponse.onSuccess(MissionSuccessCode.MY_MISSION_LIST_GET_OK, response); } diff --git a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/mission/controller/MissionControllerDocs.java b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/mission/controller/MissionControllerDocs.java index 0cfa2f5..037d6b8 100644 --- a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/mission/controller/MissionControllerDocs.java +++ b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/mission/controller/MissionControllerDocs.java @@ -4,7 +4,9 @@ import com.example.umc10th.domain.mission.enums.Address; import com.example.umc10th.domain.mission.enums.Status; import com.example.umc10th.global.apiPayload.ApiResponse; +import com.example.umc10th.global.security.entity.AuthMember; import io.swagger.v3.oas.annotations.Operation; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestParam; @@ -16,7 +18,7 @@ public interface MissionControllerDocs { ) public ApiResponse getMissionAchievement( @RequestParam Address location, - @RequestParam Long memberId + @AuthenticationPrincipal AuthMember member ); @Operation( @@ -36,7 +38,7 @@ public ApiResponse> getMissionList( ) public ApiResponse saveMemberMission( @PathVariable Long missionId, - @RequestParam Long memberId + @AuthenticationPrincipal AuthMember member ); @Operation( @@ -45,7 +47,7 @@ public ApiResponse saveMemberMission( ) public ApiResponse> getMyMissions( @RequestParam Status status, - @RequestParam Long memberId, + @AuthenticationPrincipal AuthMember member, @RequestParam Integer pageSize, @RequestParam Integer pageNumber, @RequestParam (required = false) String sort diff --git a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/review/controller/ReviewController.java b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/review/controller/ReviewController.java index b11203f..dd62a5c 100644 --- a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/review/controller/ReviewController.java +++ b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/review/controller/ReviewController.java @@ -7,9 +7,11 @@ import com.example.umc10th.domain.review.exception.code.ReviewSuccessCode; import com.example.umc10th.domain.review.service.ReviewService; import com.example.umc10th.global.apiPayload.ApiResponse; +import com.example.umc10th.global.security.entity.AuthMember; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; import java.util.List; @@ -26,9 +28,9 @@ public class ReviewController implements ReviewControllerDocs{ public ApiResponse saveReview( @PathVariable Long storeId, @RequestBody @Valid ReviewReqDto.Review dto, - @RequestParam Long memberId - ) { - reviewService.saveReview(storeId, memberId, dto); + @AuthenticationPrincipal AuthMember member + ) { + reviewService.saveReview(storeId, member.getMember().getId(), dto); return ApiResponse.onSuccess(ReviewSuccessCode.REVIEW_POST_OK, null); } diff --git a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/review/controller/ReviewControllerDocs.java b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/review/controller/ReviewControllerDocs.java index 3d72714..1166168 100644 --- a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/review/controller/ReviewControllerDocs.java +++ b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/review/controller/ReviewControllerDocs.java @@ -5,7 +5,9 @@ import com.example.umc10th.domain.review.dto.ReviewReqDto; import com.example.umc10th.domain.review.dto.ReviewResDto; import com.example.umc10th.global.apiPayload.ApiResponse; +import com.example.umc10th.global.security.entity.AuthMember; import io.swagger.v3.oas.annotations.Operation; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestParam; @@ -21,7 +23,7 @@ public interface ReviewControllerDocs { public ApiResponse saveReview( @PathVariable Long storeId, @RequestBody ReviewReqDto.Review dto, - @RequestParam Long memberId + @AuthenticationPrincipal AuthMember member ); @Operation( From 0cdbcfb7afdb0e8df50d817199985d6263e62acb Mon Sep 17 00:00:00 2001 From: seoyoon lee Date: Fri, 22 May 2026 23:49:55 +0900 Subject: [PATCH 13/13] =?UTF-8?q?feat:=20kakao=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20=EA=B5=AC=ED=98=84=20#36?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- leeseo/umc10th/build.gradle | 3 + .../domain/auth/converter/AuthConverter.java | 12 ++++ .../exception/code/MemberErrorCode.java | 6 +- .../member/repository/MemberRepository.java | 3 + .../umc10th/global/config/SecurityConfig.java | 30 ++++++-- .../umc10th/global/security/dto/KakaoDto.java | 32 +++++++++ .../umc10th/global/security/dto/OAuthDto.java | 10 +++ .../global/security/entity/OAuthMember.java | 37 ++++++++++ .../security/handler/OAuthSuccessHandler.java | 57 +++++++++++++++ .../security/service/CustomOAuthService.java | 71 +++++++++++++++++++ .../service/CustomUserDetailsService.java | 1 - .../src/main/resources/application.yml | 26 ++++++- 12 files changed, 279 insertions(+), 9 deletions(-) create mode 100644 leeseo/umc10th/src/main/java/com/example/umc10th/global/security/dto/KakaoDto.java create mode 100644 leeseo/umc10th/src/main/java/com/example/umc10th/global/security/dto/OAuthDto.java create mode 100644 leeseo/umc10th/src/main/java/com/example/umc10th/global/security/entity/OAuthMember.java create mode 100644 leeseo/umc10th/src/main/java/com/example/umc10th/global/security/handler/OAuthSuccessHandler.java create mode 100644 leeseo/umc10th/src/main/java/com/example/umc10th/global/security/service/CustomOAuthService.java diff --git a/leeseo/umc10th/build.gradle b/leeseo/umc10th/build.gradle index d57b051..d543f5e 100644 --- a/leeseo/umc10th/build.gradle +++ b/leeseo/umc10th/build.gradle @@ -45,6 +45,9 @@ dependencies { implementation 'io.jsonwebtoken:jjwt-impl:0.12.3' implementation 'io.jsonwebtoken:jjwt-jackson:0.12.3' implementation 'org.springframework.boot:spring-boot-configuration-processor' + + // OAuth + implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' } tasks.named('test') { diff --git a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/auth/converter/AuthConverter.java b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/auth/converter/AuthConverter.java index 2337a45..37ccd9e 100644 --- a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/auth/converter/AuthConverter.java +++ b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/auth/converter/AuthConverter.java @@ -4,6 +4,7 @@ import com.example.umc10th.domain.auth.dto.AuthResDto; import com.example.umc10th.domain.member.entity.Member; import com.example.umc10th.domain.member.enums.SocialType; +import com.example.umc10th.global.security.dto.OAuthDto; import java.util.UUID; @@ -27,6 +28,17 @@ public static Member toMember( .build(); } + public static Member toSocialMember( + OAuthDto dto + ) { + return Member.builder() + .email(dto.getSocialEmail()) + .nickname(dto.getName()) + .socialId(dto.getSocialUid()) + .socialType(dto.getSocialType()) + .build(); + } + public static AuthResDto.AccessToken toAccessToken( String accessToken ) { diff --git a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/member/exception/code/MemberErrorCode.java b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/member/exception/code/MemberErrorCode.java index f1ceb9c..94e84b3 100644 --- a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/member/exception/code/MemberErrorCode.java +++ b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/member/exception/code/MemberErrorCode.java @@ -14,7 +14,11 @@ public enum MemberErrorCode implements BaseErrorCode { BAD_MEMBER_INFO(HttpStatus.BAD_REQUEST, "MEMBER400_1", - "해당 정보를 저장할 수 없습니다."); + "해당 정보를 저장할 수 없습니다."), + + NOT_SUPPORT_SOCIAL_PROVIDER(HttpStatus.BAD_REQUEST, + "MEMBER400_2", + "허용하지 않는 소셜 타입입니다."); private final HttpStatus status; private final String code; diff --git a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/member/repository/MemberRepository.java b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/member/repository/MemberRepository.java index 066bc83..c2c893d 100644 --- a/leeseo/umc10th/src/main/java/com/example/umc10th/domain/member/repository/MemberRepository.java +++ b/leeseo/umc10th/src/main/java/com/example/umc10th/domain/member/repository/MemberRepository.java @@ -1,6 +1,7 @@ package com.example.umc10th.domain.member.repository; import com.example.umc10th.domain.member.entity.Member; +import com.example.umc10th.domain.member.enums.SocialType; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; @@ -16,4 +17,6 @@ public interface MemberRepository extends JpaRepository { """) Optional findMemberWithFoods(Long id); Optional findByEmail(String email); + + Optional findBySocialTypeAndSocialId(SocialType socialType, String socialUid); } diff --git a/leeseo/umc10th/src/main/java/com/example/umc10th/global/config/SecurityConfig.java b/leeseo/umc10th/src/main/java/com/example/umc10th/global/config/SecurityConfig.java index d0332f5..25a83dd 100644 --- a/leeseo/umc10th/src/main/java/com/example/umc10th/global/config/SecurityConfig.java +++ b/leeseo/umc10th/src/main/java/com/example/umc10th/global/config/SecurityConfig.java @@ -3,6 +3,8 @@ import com.example.umc10th.global.security.filter.JwtAuthFilter; import com.example.umc10th.global.security.handler.CustomAccessDenied; import com.example.umc10th.global.security.handler.CustomEntryPoint; +import com.example.umc10th.global.security.handler.OAuthSuccessHandler; +import com.example.umc10th.global.security.service.CustomOAuthService; import com.example.umc10th.global.security.service.CustomUserDetailsService; import com.example.umc10th.global.security.util.JwtUtil; import lombok.RequiredArgsConstructor; @@ -28,23 +30,34 @@ public class SecurityConfig { "/swagger-ui/**", "/swagger-resources/**", "/v3/api-docs/**", - "/api/v1/auth/**" + "/api/v1/auth/**", + "/login", + "/oauth/**" }; @Bean - public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + public SecurityFilterChain securityFilterChain(HttpSecurity http, CustomOAuthService customOAuthService) throws Exception { http .csrf(AbstractHttpConfigurer::disable) .authorizeHttpRequests(requests -> requests .requestMatchers(allowUris).permitAll() .anyRequest().authenticated() ) - .formLogin(form -> form - .defaultSuccessUrl("/swagger-ui/index.html", true) - .permitAll() - ) + .formLogin(AbstractHttpConfigurer::disable) .sessionManagement(AbstractHttpConfigurer::disable) .addFilterBefore(jwtAuthFilter(), org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter.class) + .oauth2Login(oauth -> oauth + .authorizationEndpoint(auth -> auth + .baseUri("/oauth/authorization") + ) + .redirectionEndpoint(redirect -> redirect + .baseUri("/oauth/callback/**") + ) + .userInfoEndpoint(userInfo -> { + userInfo.userService(customOAuthService); + }) + .successHandler(oAuthSuccessHandler()) + ) .logout(logout -> logout .logoutUrl("/logout") .logoutSuccessUrl("/login?logout") @@ -77,4 +90,9 @@ public CustomEntryPoint customEntryPoint() { public JwtAuthFilter jwtAuthFilter() { return new JwtAuthFilter(jwtUtil, customUserDetailsService); } + + @Bean + public OAuthSuccessHandler oAuthSuccessHandler() { + return new OAuthSuccessHandler(jwtUtil); + } } diff --git a/leeseo/umc10th/src/main/java/com/example/umc10th/global/security/dto/KakaoDto.java b/leeseo/umc10th/src/main/java/com/example/umc10th/global/security/dto/KakaoDto.java new file mode 100644 index 0000000..0806fef --- /dev/null +++ b/leeseo/umc10th/src/main/java/com/example/umc10th/global/security/dto/KakaoDto.java @@ -0,0 +1,32 @@ +package com.example.umc10th.global.security.dto; + +import com.example.umc10th.domain.member.enums.SocialType; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public class KakaoDto implements OAuthDto{ + + private final String id; + private final String email; + private final String name; + + @Override + public SocialType getSocialType() { + return SocialType.KAKAO; + } + + @Override + public String getSocialUid() { + return id; + } + + @Override + public String getSocialEmail() { + return email; + } + + @Override + public String getName() { + return name; + } +} diff --git a/leeseo/umc10th/src/main/java/com/example/umc10th/global/security/dto/OAuthDto.java b/leeseo/umc10th/src/main/java/com/example/umc10th/global/security/dto/OAuthDto.java new file mode 100644 index 0000000..c7bbb03 --- /dev/null +++ b/leeseo/umc10th/src/main/java/com/example/umc10th/global/security/dto/OAuthDto.java @@ -0,0 +1,10 @@ +package com.example.umc10th.global.security.dto; + +import com.example.umc10th.domain.member.enums.SocialType; + +public interface OAuthDto { + SocialType getSocialType(); + String getSocialUid(); + String getSocialEmail(); + String getName(); +} diff --git a/leeseo/umc10th/src/main/java/com/example/umc10th/global/security/entity/OAuthMember.java b/leeseo/umc10th/src/main/java/com/example/umc10th/global/security/entity/OAuthMember.java new file mode 100644 index 0000000..9fb4231 --- /dev/null +++ b/leeseo/umc10th/src/main/java/com/example/umc10th/global/security/entity/OAuthMember.java @@ -0,0 +1,37 @@ +package com.example.umc10th.global.security.entity; + +import com.example.umc10th.domain.member.entity.Member; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.jspecify.annotations.Nullable; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.oauth2.core.user.OAuth2User; + +import java.util.Collection; +import java.util.List; +import java.util.Map; + +@Getter +@RequiredArgsConstructor +public class OAuthMember implements OAuth2User { + + @Getter + private final Member member; + private final Map attributes; + + @Override + public Map getAttributes() { + return attributes; + } + + @Override + public String getName() { + return this.member.getSocialId(); + } + + @Override + public Collection getAuthorities() { + return List.of(); + } +} diff --git a/leeseo/umc10th/src/main/java/com/example/umc10th/global/security/handler/OAuthSuccessHandler.java b/leeseo/umc10th/src/main/java/com/example/umc10th/global/security/handler/OAuthSuccessHandler.java new file mode 100644 index 0000000..b14a863 --- /dev/null +++ b/leeseo/umc10th/src/main/java/com/example/umc10th/global/security/handler/OAuthSuccessHandler.java @@ -0,0 +1,57 @@ +package com.example.umc10th.global.security.handler; + +import com.example.umc10th.domain.auth.converter.AuthConverter; +import com.example.umc10th.domain.auth.dto.AuthResDto; +import com.example.umc10th.domain.auth.exception.code.AuthSuccessCode; +import com.example.umc10th.domain.member.converter.MemberConverter; +import com.example.umc10th.domain.member.dto.MemberResDto; +import com.example.umc10th.domain.member.exception.code.MemberSuccessCode; +import com.example.umc10th.global.apiPayload.ApiResponse; +import com.example.umc10th.global.apiPayload.code.BaseSuccessCode; +import com.example.umc10th.global.security.entity.AuthMember; +import com.example.umc10th.global.security.entity.OAuthMember; +import com.example.umc10th.global.security.util.JwtUtil; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.authentication.AuthenticationSuccessHandler; + +import java.io.IOException; + +@RequiredArgsConstructor +public class OAuthSuccessHandler implements AuthenticationSuccessHandler { + + private final JwtUtil jwtUtil; + + @Override + public void onAuthenticationSuccess( + HttpServletRequest request, + HttpServletResponse response, + Authentication authentication + ) throws IOException, ServletException { + ObjectMapper objectMapper = new ObjectMapper(); + BaseSuccessCode code = AuthSuccessCode.LOGIN_OK; + + response.setContentType("application/json;charset=UTF-8"); + response.setStatus(code.getStatus().value()); + + // 1. OAuth 멤버 추출 및 토큰 생성 + OAuthMember member = (OAuthMember) authentication.getPrincipal(); + String accessToken = jwtUtil.createAccessToken(new AuthMember(member.getMember())); + + // 2. 💡 [필수 추가] 현재 인증 객체를 SecurityContext에 확실하게 박아줍니다. + // 이렇게 해야 필터 체인이 끝날 때 '인증 안 됨(401)'으로 오해하지 않습니다. + SecurityContextHolder.getContext().setAuthentication(authentication); + + // 3. 응답 출력 + ApiResponse responseBody = ApiResponse.onSuccess( + code, + AuthConverter.toAccessToken(accessToken) + ); + objectMapper.writeValue(response.getOutputStream(), responseBody); + } +} diff --git a/leeseo/umc10th/src/main/java/com/example/umc10th/global/security/service/CustomOAuthService.java b/leeseo/umc10th/src/main/java/com/example/umc10th/global/security/service/CustomOAuthService.java new file mode 100644 index 0000000..4023ff4 --- /dev/null +++ b/leeseo/umc10th/src/main/java/com/example/umc10th/global/security/service/CustomOAuthService.java @@ -0,0 +1,71 @@ +package com.example.umc10th.global.security.service; + +import com.example.umc10th.domain.auth.converter.AuthConverter; +import com.example.umc10th.domain.member.converter.MemberConverter; +import com.example.umc10th.domain.member.entity.Member; +import com.example.umc10th.domain.member.enums.SocialType; +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.global.security.dto.KakaoDto; +import com.example.umc10th.global.security.dto.OAuthDto; +import com.example.umc10th.global.security.entity.OAuthMember; +import lombok.RequiredArgsConstructor; +import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.stereotype.Service; + +import java.util.Map; + +@Service +@RequiredArgsConstructor +public class CustomOAuthService extends DefaultOAuth2UserService { + + private final MemberRepository memberRepository; + + @Override + public OAuth2User loadUser( + OAuth2UserRequest userRequest + ) throws OAuth2AuthenticationException { + // (필수) 인증 서버의 일회성 토큰을 이용해 정보 조회 & 유저 객체 생성 + OAuth2User oAuthMember = super.loadUser(userRequest); + + Map rootAttributes = oAuthMember.getAttributes(); + + // 유저 객체에서 정보 추출 + SocialType providerId; + String socialUid; + Map kakaoAccount = (Map) rootAttributes.get("kakao_account"); + Map profile = (Map) kakaoAccount.get("profile"); + try { + providerId = SocialType.valueOf(userRequest.getClientRegistration().getRegistrationId().toUpperCase()); + socialUid = String.valueOf(rootAttributes.get("id")); + } catch (IllegalArgumentException e) { + throw new MemberException(MemberErrorCode.NOT_SUPPORT_SOCIAL_PROVIDER); + } + + // OAuth 공통 정보 DTO로 매핑 + OAuthDto dto; + switch (providerId) { + case KAKAO -> { + String email = kakaoAccount.get("email").toString(); + String name = profile.get("nickname").toString(); + dto = new KakaoDto(socialUid, email, name); + } + default -> throw new MemberException(MemberErrorCode.NOT_SUPPORT_SOCIAL_PROVIDER); + } + + System.out.println("socialId:" + socialUid); + + // DB 저장: 있다면 그 데이터 가져오고 없으면 새로 저장 + Member member = memberRepository.findBySocialTypeAndSocialId(providerId, socialUid) + .orElseGet(() -> { + Member newMember = AuthConverter.toSocialMember(dto); + memberRepository.save(newMember); + return newMember; + }); + return new OAuthMember(member, oAuthMember.getAttributes()); + } +} diff --git a/leeseo/umc10th/src/main/java/com/example/umc10th/global/security/service/CustomUserDetailsService.java b/leeseo/umc10th/src/main/java/com/example/umc10th/global/security/service/CustomUserDetailsService.java index a4b3470..28e49ef 100644 --- a/leeseo/umc10th/src/main/java/com/example/umc10th/global/security/service/CustomUserDetailsService.java +++ b/leeseo/umc10th/src/main/java/com/example/umc10th/global/security/service/CustomUserDetailsService.java @@ -17,7 +17,6 @@ public class CustomUserDetailsService implements UserDetailsService { private final MemberRepository memberRepository; - @Override public UserDetails loadUserByUsername( String username ) throws UsernameNotFoundException { diff --git a/leeseo/umc10th/src/main/resources/application.yml b/leeseo/umc10th/src/main/resources/application.yml index aa6bf32..3febb84 100644 --- a/leeseo/umc10th/src/main/resources/application.yml +++ b/leeseo/umc10th/src/main/resources/application.yml @@ -7,6 +7,26 @@ spring: username: ${DB_USER} password: ${DB_PW} + security: + oauth2: + client: + registration: + kakao: + client-id: ${KAKAO_REST_API_KEY} + client-secret: ${KAKAO_REST_API_SECRET} + client-authentication-method: client_secret_post + authorization-grant-type: authorization_code + redirect-uri: "{baseUrl}/oauth/callback/{registrationId}" + scope: + - profile_nickname + - account_email + provider: + kakao: + authorization-uri: "https://kauth.kakao.com/oauth/authorize" + token-uri: "https://kauth.kakao.com/oauth/token" + user-info-uri: "https://kapi.kakao.com/v2/user/me" + user-name-attribute: id + jpa: database: mysql database-platform: org.hibernate.dialect.MySQLDialect @@ -21,4 +41,8 @@ jwt: token: secretKey: ${JWT_SECRET_KEY} expiration: - access: 1800000 # 30분 \ No newline at end of file + access: 1800000 # 30분 + +logging: + level: + org.springframework.security: DEBUG \ No newline at end of file