From 8abd930cc56c6c2538906d2edab95992d6d5c25b Mon Sep 17 00:00:00 2001 From: hyedam Date: Tue, 19 May 2026 15:36:04 +0900 Subject: [PATCH] mission/#8 --- build.gradle | 4 ++ .../review/controller/ReviewController.java | 10 +-- .../domain/review/dto/ReviewCursor.java | 14 ++++ .../domain/review/service/ReviewService.java | 48 ++++--------- .../user/controller/UserController.java | 8 ++- .../domain/user/converter/UserConverter.java | 27 ++++++++ .../domain/user/dto/UserReqDTO.java | 12 ++++ .../domain/user/dto/UserResDTO.java | 1 + .../Spring_Boot/domain/user/entity/User.java | 6 ++ .../domain/user/enums/SocialType.java | 2 +- .../user/repository/UserRepository.java | 2 + .../domain/user/security/AuthMember.java | 47 +++++++++++++ .../security/CustomUserDetailsService.java | 23 +++++++ .../domain/user/service/UserService.java | 10 +++ .../global/config/SecurityConfig.java | 64 ++++++++++++++++++ .../Spring_Boot/global/config/WebConfig.java | 21 ++++++ .../ReviewCursorArgumentResolver.java | 67 +++++++++++++++++++ .../global/security/CustomAccessDenied.java | 30 +++++++++ .../global/security/CustomEntryPoint.java | 30 +++++++++ 19 files changed, 381 insertions(+), 45 deletions(-) create mode 100644 src/main/java/com/example/Spring_Boot/domain/review/dto/ReviewCursor.java create mode 100644 src/main/java/com/example/Spring_Boot/domain/user/security/AuthMember.java create mode 100644 src/main/java/com/example/Spring_Boot/domain/user/security/CustomUserDetailsService.java create mode 100644 src/main/java/com/example/Spring_Boot/global/config/SecurityConfig.java create mode 100644 src/main/java/com/example/Spring_Boot/global/config/WebConfig.java create mode 100644 src/main/java/com/example/Spring_Boot/global/resolver/ReviewCursorArgumentResolver.java create mode 100644 src/main/java/com/example/Spring_Boot/global/security/CustomAccessDenied.java create mode 100644 src/main/java/com/example/Spring_Boot/global/security/CustomEntryPoint.java diff --git a/build.gradle b/build.gradle index 7a73c00..657d6ab 100644 --- a/build.gradle +++ b/build.gradle @@ -32,6 +32,10 @@ dependencies { // Swagger implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:3.0.1' implementation 'org.springdoc:springdoc-openapi-starter-webmvc-api:3.0.1' + + // Security + implementation 'org.springframework.boot:spring-boot-starter-security' + testImplementation 'org.springframework.security:spring-security-test' } tasks.named('test') { diff --git a/src/main/java/com/example/Spring_Boot/domain/review/controller/ReviewController.java b/src/main/java/com/example/Spring_Boot/domain/review/controller/ReviewController.java index aa522f8..6ab0e7a 100644 --- a/src/main/java/com/example/Spring_Boot/domain/review/controller/ReviewController.java +++ b/src/main/java/com/example/Spring_Boot/domain/review/controller/ReviewController.java @@ -1,6 +1,7 @@ package com.example.Spring_Boot.domain.review.controller; import com.example.Spring_Boot.domain.mission.dto.MissionResDTO; +import com.example.Spring_Boot.domain.review.dto.ReviewCursor; import com.example.Spring_Boot.domain.review.dto.ReviewReqDTO; import com.example.Spring_Boot.domain.review.dto.ReviewResDTO; import com.example.Spring_Boot.domain.review.service.ReviewService; @@ -17,7 +18,6 @@ public class ReviewController { private final ReviewService reviewService; - // 기존: 리뷰 작성 + 미션 3: @Valid 추가 @PostMapping("/stores/{storeId}/reviews") public ApiResponse createReview( @PathVariable Long storeId, @@ -28,17 +28,13 @@ public ApiResponse createReview( reviewService.createReview(userId, storeId, request)); } - // 미션 2: 내가 생성한 리뷰 조회 (커서 기반) - // GET /api/reviews/my?userId=1&pageSize=10&cursor=-1&query=id - // query: id(ID순) 또는 rating(별점순) @GetMapping("/reviews/my") public ApiResponse> getMyReviews( @RequestParam Long userId, @RequestParam(defaultValue = "10") Integer pageSize, - @RequestParam(defaultValue = "-1") String cursor, - @RequestParam(defaultValue = "id") String query + ReviewCursor reviewCursor ) { return ApiResponse.onSuccess(GeneralSuccessCode.OK, - reviewService.getMyReviews(userId, pageSize, cursor, query)); + reviewService.getMyReviews(userId, pageSize, reviewCursor)); } } \ No newline at end of file diff --git a/src/main/java/com/example/Spring_Boot/domain/review/dto/ReviewCursor.java b/src/main/java/com/example/Spring_Boot/domain/review/dto/ReviewCursor.java new file mode 100644 index 0000000..b78d2ed --- /dev/null +++ b/src/main/java/com/example/Spring_Boot/domain/review/dto/ReviewCursor.java @@ -0,0 +1,14 @@ +package com.example.Spring_Boot.domain.review.dto; + +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class ReviewCursor { + private final String query; + private final String cursor; + private final Long idCursor; + private final Integer ratingCursor; + private final boolean hasCursor; +} \ No newline at end of file diff --git a/src/main/java/com/example/Spring_Boot/domain/review/service/ReviewService.java b/src/main/java/com/example/Spring_Boot/domain/review/service/ReviewService.java index 844e442..af8ba7d 100644 --- a/src/main/java/com/example/Spring_Boot/domain/review/service/ReviewService.java +++ b/src/main/java/com/example/Spring_Boot/domain/review/service/ReviewService.java @@ -3,6 +3,7 @@ import com.example.Spring_Boot.domain.mission.converter.MissionConverter; import com.example.Spring_Boot.domain.mission.dto.MissionResDTO; import com.example.Spring_Boot.domain.review.converter.ReviewConverter; +import com.example.Spring_Boot.domain.review.dto.ReviewCursor; import com.example.Spring_Boot.domain.review.dto.ReviewReqDTO; import com.example.Spring_Boot.domain.review.dto.ReviewResDTO; import com.example.Spring_Boot.domain.review.entity.Review; @@ -28,7 +29,6 @@ public class ReviewService { private final UserRepository userRepository; private final StoreRepository storeRepository; - // 기존: 리뷰 작성 public ReviewResDTO.CreateReviewResultDTO createReview(Long userId, Long storeId, ReviewReqDTO.CreateReviewDTO request) { User user = userRepository.findById(userId) @@ -39,45 +39,27 @@ public ReviewResDTO.CreateReviewResultDTO createReview(Long userId, Long storeId return ReviewConverter.toCreateReviewResultDTO(reviewRepository.save(review)); } - // 미션 2: 내가 생성한 리뷰 조회 (커서 기반) @Transactional(readOnly = true) public MissionResDTO.Pagination getMyReviews( - Long userId, Integer pageSize, String cursor, String query) { + Long userId, Integer pageSize, ReviewCursor reviewCursor) { PageRequest pageRequest = PageRequest.of(0, pageSize); - - long idCursor; Slice reviewList; - String nextCursor; - - // 커서가 있는 경우 - if (!cursor.equals("-1")) { - - // 커서 분리: "id:10" or "rating:4:10" - String[] cursorSplit = cursor.split(":"); - switch (query.toLowerCase()) { - case "id" -> { - // 커서 타입 변환 - idCursor = Long.parseLong(cursorSplit[1]); - // ID 순 조회 - reviewList = reviewRepository.findReviewsByUserIdAndIdLessThanOrderByIdDesc( - userId, idCursor, pageRequest); - } - case "rating" -> { - // 커서 타입 변환 - Integer ratingCursor = Integer.parseInt(cursorSplit[1]); - idCursor = Long.parseLong(cursorSplit[2]); - // 별점 순 조회 (복합 커서) - reviewList = reviewRepository - .findReviewsByUserIdAndRatingLessThanOrUserIdAndRatingEqualsAndIdLessThanOrderByRatingDescIdDesc( - userId, ratingCursor, userId, ratingCursor, idCursor, pageRequest); - } + if (reviewCursor.isHasCursor()) { + switch (reviewCursor.getQuery().toLowerCase()) { + case "id" -> reviewList = reviewRepository + .findReviewsByUserIdAndIdLessThanOrderByIdDesc( + userId, reviewCursor.getIdCursor(), pageRequest); + case "rating" -> reviewList = reviewRepository + .findReviewsByUserIdAndRatingLessThanOrUserIdAndRatingEqualsAndIdLessThanOrderByRatingDescIdDesc( + userId, reviewCursor.getRatingCursor(), + userId, reviewCursor.getRatingCursor(), + reviewCursor.getIdCursor(), pageRequest); default -> throw new ProjectException(GeneralErrorCode.BAD_REQUEST); } } else { - // 커서 없이 조회 - switch (query.toLowerCase()) { + switch (reviewCursor.getQuery().toLowerCase()) { case "id" -> reviewList = reviewRepository .findReviewsByUserIdOrderByIdDesc(userId, pageRequest); case "rating" -> reviewList = reviewRepository @@ -86,18 +68,16 @@ public MissionResDTO.Pagination getMyReviews( } } - // 다음 커서 계산: ID:ID 형태 or rating:rating:ID 형태 Review lastReview = reviewList.getContent().isEmpty() ? null : reviewList.getContent().get(reviewList.getContent().size() - 1); - nextCursor = lastReview == null ? null : switch (query.toLowerCase()) { + String nextCursor = lastReview == null ? null : switch (reviewCursor.getQuery().toLowerCase()) { case "id" -> lastReview.getId() + ":" + lastReview.getId(); case "rating" -> lastReview.getRating() + ":" + lastReview.getRating() + ":" + lastReview.getId(); default -> null; }; List data = reviewList.map(ReviewConverter::toGetReview).toList(); - return MissionConverter.toPagination(data, reviewList.hasNext(), nextCursor, reviewList.getSize()); } } \ No newline at end of file diff --git a/src/main/java/com/example/Spring_Boot/domain/user/controller/UserController.java b/src/main/java/com/example/Spring_Boot/domain/user/controller/UserController.java index 5161065..966c908 100644 --- a/src/main/java/com/example/Spring_Boot/domain/user/controller/UserController.java +++ b/src/main/java/com/example/Spring_Boot/domain/user/controller/UserController.java @@ -5,6 +5,7 @@ import com.example.Spring_Boot.domain.user.service.UserService; import com.example.Spring_Boot.global.apiPayload.ApiResponse; import com.example.Spring_Boot.global.apiPayload.code.GeneralSuccessCode; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; @@ -15,14 +16,15 @@ public class UserController { private final UserService userService; + // Public API - 회원가입 @PostMapping("/users") public ApiResponse join( - @RequestBody UserReqDTO.JoinDTO request + @Valid @RequestBody UserReqDTO.JoinDTO request ) { - return ApiResponse.onSuccess(GeneralSuccessCode.CREATED, null); + return ApiResponse.onSuccess(GeneralSuccessCode.CREATED, userService.join(request)); } - // 화면 3: 마이페이지 + // Private API - 마이페이지 @GetMapping("/users/{userId}") public ApiResponse getMyPage( @PathVariable Long userId diff --git a/src/main/java/com/example/Spring_Boot/domain/user/converter/UserConverter.java b/src/main/java/com/example/Spring_Boot/domain/user/converter/UserConverter.java index 8cb80ef..d8b54a9 100644 --- a/src/main/java/com/example/Spring_Boot/domain/user/converter/UserConverter.java +++ b/src/main/java/com/example/Spring_Boot/domain/user/converter/UserConverter.java @@ -1,10 +1,37 @@ package com.example.Spring_Boot.domain.user.converter; +import com.example.Spring_Boot.domain.mission.enums.Address; +import com.example.Spring_Boot.domain.user.dto.UserReqDTO; import com.example.Spring_Boot.domain.user.dto.UserResDTO; import com.example.Spring_Boot.domain.user.entity.User; +import com.example.Spring_Boot.domain.user.enums.Gender; +import com.example.Spring_Boot.domain.user.enums.SocialType; +import java.time.LocalDate; public class UserConverter { + public static User toUser(UserReqDTO.JoinDTO request, String encodedPassword) { + return User.builder() + .name(request.getName()) + .email(request.getEmail()) + .password(encodedPassword) + .gender(request.getGender() != null ? Gender.valueOf(request.getGender()) : Gender.NONE) + .birth(LocalDate.now()) + .address(Address.SEOUL) + .detailAddress("") + .socialUid("") + .socialType(SocialType.NONE) + .build(); + } + + public static UserResDTO.JoinResultDTO toJoinResultDTO(User user) { + return UserResDTO.JoinResultDTO.builder() + .userId(user.getId()) + .name(user.getName()) + .email(user.getEmail()) + .build(); + } + public static UserResDTO.MyPageDTO toMyPageDTO(User user) { return UserResDTO.MyPageDTO.builder() .userId(user.getId()) diff --git a/src/main/java/com/example/Spring_Boot/domain/user/dto/UserReqDTO.java b/src/main/java/com/example/Spring_Boot/domain/user/dto/UserReqDTO.java index 5f7fb82..24bcbee 100644 --- a/src/main/java/com/example/Spring_Boot/domain/user/dto/UserReqDTO.java +++ b/src/main/java/com/example/Spring_Boot/domain/user/dto/UserReqDTO.java @@ -1,5 +1,7 @@ package com.example.Spring_Boot.domain.user.dto; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; import lombok.Getter; import lombok.NoArgsConstructor; import java.util.List; @@ -9,7 +11,17 @@ public class UserReqDTO { @Getter @NoArgsConstructor public static class JoinDTO { + + @NotBlank(message = "이름은 필수입니다.") private String name; + + @NotBlank(message = "이메일은 필수입니다.") + @Email(message = "이메일 형식이 올바르지 않습니다.") + private String email; + + @NotBlank(message = "비밀번호는 필수입니다.") + private String password; + private String gender; private String birthDate; private String address; diff --git a/src/main/java/com/example/Spring_Boot/domain/user/dto/UserResDTO.java b/src/main/java/com/example/Spring_Boot/domain/user/dto/UserResDTO.java index 44b2026..6a36d2a 100644 --- a/src/main/java/com/example/Spring_Boot/domain/user/dto/UserResDTO.java +++ b/src/main/java/com/example/Spring_Boot/domain/user/dto/UserResDTO.java @@ -14,6 +14,7 @@ public class UserResDTO { public static class JoinResultDTO { private Long userId; private String name; + private String email; } @Getter diff --git a/src/main/java/com/example/Spring_Boot/domain/user/entity/User.java b/src/main/java/com/example/Spring_Boot/domain/user/entity/User.java index 06f6d2e..87bc251 100644 --- a/src/main/java/com/example/Spring_Boot/domain/user/entity/User.java +++ b/src/main/java/com/example/Spring_Boot/domain/user/entity/User.java @@ -29,6 +29,12 @@ public class User extends BaseEntity { @Column(name = "name", nullable = false) private String name; + @Column(name = "email", nullable = false, unique = true) + private String email; + + @Column(name = "password", nullable = false) + private String password; + @Column(name = "gender", nullable = false) @Enumerated(EnumType.STRING) private Gender gender; diff --git a/src/main/java/com/example/Spring_Boot/domain/user/enums/SocialType.java b/src/main/java/com/example/Spring_Boot/domain/user/enums/SocialType.java index 497ef79..b7468e9 100644 --- a/src/main/java/com/example/Spring_Boot/domain/user/enums/SocialType.java +++ b/src/main/java/com/example/Spring_Boot/domain/user/enums/SocialType.java @@ -1,5 +1,5 @@ package com.example.Spring_Boot.domain.user.enums; public enum SocialType { - KAKAO, NAVER, GOOGLE + KAKAO, NAVER, GOOGLE, NONE } \ No newline at end of file diff --git a/src/main/java/com/example/Spring_Boot/domain/user/repository/UserRepository.java b/src/main/java/com/example/Spring_Boot/domain/user/repository/UserRepository.java index 284bbf7..4f4f453 100644 --- a/src/main/java/com/example/Spring_Boot/domain/user/repository/UserRepository.java +++ b/src/main/java/com/example/Spring_Boot/domain/user/repository/UserRepository.java @@ -2,6 +2,8 @@ import com.example.Spring_Boot.domain.user.entity.User; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Optional; public interface UserRepository extends JpaRepository { + Optional findByEmail(String email); } \ No newline at end of file diff --git a/src/main/java/com/example/Spring_Boot/domain/user/security/AuthMember.java b/src/main/java/com/example/Spring_Boot/domain/user/security/AuthMember.java new file mode 100644 index 0000000..d5aef3a --- /dev/null +++ b/src/main/java/com/example/Spring_Boot/domain/user/security/AuthMember.java @@ -0,0 +1,47 @@ +package com.example.Spring_Boot.domain.user.security; + +import com.example.Spring_Boot.domain.user.entity.User; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import java.util.Collection; +import java.util.List; + +public class AuthMember implements UserDetails { + + private final User user; + + public AuthMember(User user) { + this.user = user; + } + + public User getUser() { + return user; + } + + @Override + public Collection getAuthorities() { + return List.of(); + } + + @Override + public String getPassword() { + return user.getPassword(); + } + + @Override + public String getUsername() { + return user.getEmail(); + } + + @Override + public boolean isAccountNonExpired() { return true; } + + @Override + public boolean isAccountNonLocked() { return true; } + + @Override + public boolean isCredentialsNonExpired() { return true; } + + @Override + public boolean isEnabled() { return true; } +} \ No newline at end of file diff --git a/src/main/java/com/example/Spring_Boot/domain/user/security/CustomUserDetailsService.java b/src/main/java/com/example/Spring_Boot/domain/user/security/CustomUserDetailsService.java new file mode 100644 index 0000000..fd0360c --- /dev/null +++ b/src/main/java/com/example/Spring_Boot/domain/user/security/CustomUserDetailsService.java @@ -0,0 +1,23 @@ +package com.example.Spring_Boot.domain.user.security; + +import com.example.Spring_Boot.domain.user.entity.User; +import com.example.Spring_Boot.domain.user.repository.UserRepository; +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 UserRepository userRepository; + + @Override + public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { + User user = userRepository.findByEmail(email) + .orElseThrow(() -> new UsernameNotFoundException("사용자를 찾을 수 없습니다.")); + return new AuthMember(user); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/Spring_Boot/domain/user/service/UserService.java b/src/main/java/com/example/Spring_Boot/domain/user/service/UserService.java index f3ba57d..c959be7 100644 --- a/src/main/java/com/example/Spring_Boot/domain/user/service/UserService.java +++ b/src/main/java/com/example/Spring_Boot/domain/user/service/UserService.java @@ -1,12 +1,14 @@ package com.example.Spring_Boot.domain.user.service; import com.example.Spring_Boot.domain.user.converter.UserConverter; +import com.example.Spring_Boot.domain.user.dto.UserReqDTO; import com.example.Spring_Boot.domain.user.dto.UserResDTO; import com.example.Spring_Boot.domain.user.entity.User; import com.example.Spring_Boot.domain.user.repository.UserRepository; import com.example.Spring_Boot.global.apiPayload.code.GeneralErrorCode; import com.example.Spring_Boot.global.exception.ProjectException; import lombok.RequiredArgsConstructor; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -16,6 +18,14 @@ public class UserService { private final UserRepository userRepository; + private final PasswordEncoder passwordEncoder; + + @Transactional + public UserResDTO.JoinResultDTO join(UserReqDTO.JoinDTO request) { + String encodedPassword = passwordEncoder.encode(request.getPassword()); + User user = UserConverter.toUser(request, encodedPassword); + return UserConverter.toJoinResultDTO(userRepository.save(user)); + } public UserResDTO.MyPageDTO getMyPage(Long userId) { User user = userRepository.findById(userId) diff --git a/src/main/java/com/example/Spring_Boot/global/config/SecurityConfig.java b/src/main/java/com/example/Spring_Boot/global/config/SecurityConfig.java new file mode 100644 index 0000000..c7e56e0 --- /dev/null +++ b/src/main/java/com/example/Spring_Boot/global/config/SecurityConfig.java @@ -0,0 +1,64 @@ +package com.example.Spring_Boot.global.config; + +import com.example.Spring_Boot.global.security.CustomAccessDenied; +import com.example.Spring_Boot.global.security.CustomEntryPoint; +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-ui/**", + "/swagger-resources/**", + "/v3/api-docs/**", + "/auth/users" + }; + + @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() + ) + .exceptionHandling(exception -> exception + .authenticationEntryPoint(customEntryPoint()) + .accessDeniedHandler(customAccessDenied()) + ); + + return http.build(); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + @Bean + public CustomEntryPoint customEntryPoint() { + return new CustomEntryPoint(); + } + + @Bean + public CustomAccessDenied customAccessDenied() { + return new CustomAccessDenied(); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/Spring_Boot/global/config/WebConfig.java b/src/main/java/com/example/Spring_Boot/global/config/WebConfig.java new file mode 100644 index 0000000..af884a8 --- /dev/null +++ b/src/main/java/com/example/Spring_Boot/global/config/WebConfig.java @@ -0,0 +1,21 @@ +package com.example.Spring_Boot.global.config; + +import com.example.Spring_Boot.global.resolver.ReviewCursorArgumentResolver; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import java.util.List; + +@Configuration +@RequiredArgsConstructor +public class WebConfig implements WebMvcConfigurer { + + private final ReviewCursorArgumentResolver reviewCursorArgumentResolver; + + @Override + public void addArgumentResolvers(List resolvers) { + resolvers.add(reviewCursorArgumentResolver); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/Spring_Boot/global/resolver/ReviewCursorArgumentResolver.java b/src/main/java/com/example/Spring_Boot/global/resolver/ReviewCursorArgumentResolver.java new file mode 100644 index 0000000..5968403 --- /dev/null +++ b/src/main/java/com/example/Spring_Boot/global/resolver/ReviewCursorArgumentResolver.java @@ -0,0 +1,67 @@ +package com.example.Spring_Boot.global.resolver; + +import com.example.Spring_Boot.domain.review.dto.ReviewCursor; +import com.example.Spring_Boot.global.apiPayload.code.GeneralErrorCode; +import com.example.Spring_Boot.global.exception.ProjectException; +import org.springframework.core.MethodParameter; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; + +@Component +public class ReviewCursorArgumentResolver implements HandlerMethodArgumentResolver { + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.getParameterType().equals(ReviewCursor.class); + } + + @Override + public Object resolveArgument(MethodParameter parameter, + ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, + WebDataBinderFactory binderFactory) { + + String cursor = webRequest.getParameter("cursor"); + String query = webRequest.getParameter("query"); + + if (cursor == null) cursor = "-1"; + if (query == null) query = "id"; + + if (cursor.equals("-1")) { + return ReviewCursor.builder() + .query(query) + .cursor(cursor) + .hasCursor(false) + .build(); + } + + String[] cursorSplit = cursor.split(":"); + + switch (query.toLowerCase()) { + case "id" -> { + Long idCursor = Long.parseLong(cursorSplit[1]); + return ReviewCursor.builder() + .query(query) + .cursor(cursor) + .idCursor(idCursor) + .hasCursor(true) + .build(); + } + case "rating" -> { + Integer ratingCursor = Integer.parseInt(cursorSplit[1]); + Long idCursor = Long.parseLong(cursorSplit[2]); + return ReviewCursor.builder() + .query(query) + .cursor(cursor) + .ratingCursor(ratingCursor) + .idCursor(idCursor) + .hasCursor(true) + .build(); + } + default -> throw new ProjectException(GeneralErrorCode.BAD_REQUEST); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/example/Spring_Boot/global/security/CustomAccessDenied.java b/src/main/java/com/example/Spring_Boot/global/security/CustomAccessDenied.java new file mode 100644 index 0000000..7b6b8a8 --- /dev/null +++ b/src/main/java/com/example/Spring_Boot/global/security/CustomAccessDenied.java @@ -0,0 +1,30 @@ +package com.example.Spring_Boot.global.security; + +import com.example.Spring_Boot.global.apiPayload.ApiResponse; +import com.example.Spring_Boot.global.apiPayload.code.BaseErrorCode; +import com.example.Spring_Boot.global.apiPayload.code.GeneralErrorCode; +import com.fasterxml.jackson.databind.ObjectMapper; +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 { + ObjectMapper objectMapper = new ObjectMapper(); + BaseErrorCode code = GeneralErrorCode.FORBIDDEN; + + response.setContentType("application/json;charset=UTF-8"); + response.setStatus(code.getStatus().value()); + + ApiResponse errorResponse = ApiResponse.onFailure(code, null); + objectMapper.writeValue(response.getOutputStream(), errorResponse); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/Spring_Boot/global/security/CustomEntryPoint.java b/src/main/java/com/example/Spring_Boot/global/security/CustomEntryPoint.java new file mode 100644 index 0000000..2f3ac60 --- /dev/null +++ b/src/main/java/com/example/Spring_Boot/global/security/CustomEntryPoint.java @@ -0,0 +1,30 @@ +package com.example.Spring_Boot.global.security; + +import com.example.Spring_Boot.global.apiPayload.ApiResponse; +import com.example.Spring_Boot.global.apiPayload.code.BaseErrorCode; +import com.example.Spring_Boot.global.apiPayload.code.GeneralErrorCode; +import com.fasterxml.jackson.databind.ObjectMapper; +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 { + ObjectMapper objectMapper = new ObjectMapper(); + BaseErrorCode code = GeneralErrorCode.UNAUTHORIZED; + + response.setContentType("application/json;charset=UTF-8"); + response.setStatus(code.getStatus().value()); + + ApiResponse errorResponse = ApiResponse.onFailure(code, null); + objectMapper.writeValue(response.getOutputStream(), errorResponse); + } +} \ No newline at end of file