diff --git a/src/main/java/com/mycom/backenddaengplace/auth/config/SecurityConfig.java b/src/main/java/com/mycom/backenddaengplace/auth/config/SecurityConfig.java index 54571dd..29b900b 100644 --- a/src/main/java/com/mycom/backenddaengplace/auth/config/SecurityConfig.java +++ b/src/main/java/com/mycom/backenddaengplace/auth/config/SecurityConfig.java @@ -32,45 +32,24 @@ public class SecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http - // CSRF 비활성화 (REST API 스타일에서는 보통 사용하지 않음) .csrf(AbstractHttpConfigurer::disable) - // 폼 로그인 비활성화 (소셜 로그인만 사용) + .sessionManagement(session -> + session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .formLogin(AbstractHttpConfigurer::disable) - // HTTP 기본 인증 비활성화 .httpBasic(AbstractHttpConfigurer::disable) - // CORS 설정 - .cors(corsCustomizer -> corsCustomizer.configurationSource(request -> { - CorsConfiguration configuration = new CorsConfiguration(); - configuration.setAllowedOrigins(Collections.singletonList("http://localhost:8080")); // React 프론트엔드 URL - configuration.setAllowedMethods(Collections.singletonList("*")); // 모든 HTTP 메서드 허용 - configuration.setAllowCredentials(true); // 쿠키 허용 - configuration.setAllowedHeaders(Collections.singletonList("*")); // 모든 헤더 허용 - configuration.setMaxAge(3600L); // CORS 캐싱 시간 설정 - return configuration; - })) - - // 소셜 로그인 설정 - .oauth2Login(oauth2 -> oauth2 - .userInfoEndpoint(userInfo -> userInfo.userService(customOAuth2UserService)) // 사용자 정보 처리 - .successHandler(customSuccessHandler)) // 로그인 성공 시 처리 - - // JWT 인증 필터 추가 - .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class) - // 로그아웃 필터 추가 - .addFilterBefore(customLogoutFilter, LogoutFilter.class) - - // 권한별 요청 처리 .authorizeHttpRequests(auth -> auth - - // 토큰 재발급, 루트 페이지, 로그인, OAuth2 요청은 인증 없이 접근 가능 - .requestMatchers("/reissue", "/", "/login", "/oauth2/**").permitAll() - - // 나머지 요청은 인증 필요 + .requestMatchers("/login", "/oauth2/**", "/auth/**", "/reissue").permitAll() + // "/" 경로 제외 .anyRequest().authenticated()) - // 세션 정책: Stateless - .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)); + .oauth2Login(oauth2 -> oauth2 + .userInfoEndpoint(userInfo -> + userInfo.userService(customOAuth2UserService)) + .successHandler(customSuccessHandler)) + .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class) // JWT 필터 추가 + .addFilterBefore(customLogoutFilter, LogoutFilter.class); + return http.build(); } diff --git a/src/main/java/com/mycom/backenddaengplace/auth/config/WebConfig.java b/src/main/java/com/mycom/backenddaengplace/auth/config/WebConfig.java index f716ca5..303bad6 100644 --- a/src/main/java/com/mycom/backenddaengplace/auth/config/WebConfig.java +++ b/src/main/java/com/mycom/backenddaengplace/auth/config/WebConfig.java @@ -3,7 +3,6 @@ import com.mycom.backenddaengplace.auth.interceptor.AuthorizationInterceptor; import lombok.RequiredArgsConstructor; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; -import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @RequiredArgsConstructor @@ -19,9 +18,9 @@ public void addInterceptors(InterceptorRegistry registry) { * // (...) * .anyRequest().authenticated()) // 그 외의 요청은 인증 필요 */ - registry.addInterceptor(authorizationInterceptor) // 인터셉터 등록 (여러 인터셉터 등록이 가능하며, 필요 시 순서 조정도 가능) - .addPathPatterns("/**") // 인터셉터 적용 경로 - .excludePathPatterns("/"); // 인터셉터 제외 경로 + registry.addInterceptor(authorizationInterceptor) + .addPathPatterns("/**") // 모든 경로에 적용, '/' 제외 필요 없음 + .excludePathPatterns("/error", "/logout", "/api/login"); // 에러 페이지, 로그아웃, 로그인 API는 제외 } } diff --git a/src/main/java/com/mycom/backenddaengplace/auth/controller/ApiTestController.java b/src/main/java/com/mycom/backenddaengplace/auth/controller/ApiTestController.java deleted file mode 100644 index da52b0d..0000000 --- a/src/main/java/com/mycom/backenddaengplace/auth/controller/ApiTestController.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.mycom.backenddaengplace.auth.controller; - -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -import java.util.HashMap; - -@RestController -@RequestMapping("/api/test") -public class ApiTestController { - - @GetMapping - public HashMap testApi() { - HashMap response = new HashMap<>(); - response.put("message", "success"); - - return response; - } - -} \ No newline at end of file diff --git a/src/main/java/com/mycom/backenddaengplace/auth/controller/HomeController.java b/src/main/java/com/mycom/backenddaengplace/auth/controller/HomeController.java deleted file mode 100644 index 8984d22..0000000 --- a/src/main/java/com/mycom/backenddaengplace/auth/controller/HomeController.java +++ /dev/null @@ -1,46 +0,0 @@ -package com.mycom.backenddaengplace.auth.controller; - -import com.mycom.backenddaengplace.auth.repository.AuthMemberRepository; -import com.mycom.backenddaengplace.member.domain.Member; -import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -import java.util.List; -import java.util.Optional; - -@RestController -@RequestMapping("/users") // API 엔드포인트 기본 경로 설정 -public class HomeController { - - private final AuthMemberRepository authMemberRepository; - - public HomeController(AuthMemberRepository authMemberRepository) { - this.authMemberRepository = authMemberRepository; - } - - // 모든 유저 조회 - @GetMapping - public List getAllUsers() { - return authMemberRepository.findAll(); - } - - // 특정 유저 조회 by ID - @GetMapping("/{id}") - public Member getUserById(@PathVariable Long id) { - Optional user = authMemberRepository.findById(id); - return user.orElseThrow(() -> new RuntimeException("User not found with ID: " + id)); - } - - // 특정 유저 조회 by Email - @GetMapping("/email/{email}") - public Member getUserByEmail(@PathVariable String email) { - Optional user = authMemberRepository.findAll().stream() - .filter(member -> email.equals(member.getEmail())) - .findFirst(); - return user.orElseThrow(() -> new RuntimeException("User not found with email: " + email)); - } - -} diff --git a/src/main/java/com/mycom/backenddaengplace/auth/dto/CustomOAuth2User.java b/src/main/java/com/mycom/backenddaengplace/auth/dto/CustomOAuth2User.java index f57433c..267f5b9 100644 --- a/src/main/java/com/mycom/backenddaengplace/auth/dto/CustomOAuth2User.java +++ b/src/main/java/com/mycom/backenddaengplace/auth/dto/CustomOAuth2User.java @@ -1,6 +1,7 @@ package com.mycom.backenddaengplace.auth.dto; - +import com.mycom.backenddaengplace.member.domain.Member; +import lombok.Getter; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.oauth2.core.user.OAuth2User; @@ -8,42 +9,40 @@ import java.util.Collection; import java.util.Map; +@Getter public class CustomOAuth2User implements OAuth2User { private final UserDTO userDTO; + private final Member member; // Member 추가 - public CustomOAuth2User(UserDTO userDTO) { - + public CustomOAuth2User(UserDTO userDTO, Member member) { // 생성자 수정 this.userDTO = userDTO; + this.member = member; } @Override public Map getAttributes() { - return null; } @Override public Collection getAuthorities() { Collection collection = new ArrayList<>(); - collection.add(new GrantedAuthority() { - @Override - public String getAuthority() { - // 권한이 없으면 기본 ROLE_USER 반환 - return "ROLE_USER"; - } - }); + collection.add(() -> "ROLE_USER"); // 람다식으로 단순화 return collection; } @Override public String getName() { - // userDTO.getName() 대신 nickname을 반환 return userDTO.getNickname() != null ? userDTO.getNickname() : "Unknown User"; } public String getUsername() { - // userDTO.getUsername() 대신 provider + providerId 반환 return userDTO.getProvider() + "_" + userDTO.getProviderId(); } + + // Member 객체를 반환하는 메소드 추가 + public Member getMember() { + return member; + } } \ No newline at end of file diff --git a/src/main/java/com/mycom/backenddaengplace/auth/dto/CustomUserDetails.java b/src/main/java/com/mycom/backenddaengplace/auth/dto/CustomUserDetails.java index 5d53bf6..865f332 100644 --- a/src/main/java/com/mycom/backenddaengplace/auth/dto/CustomUserDetails.java +++ b/src/main/java/com/mycom/backenddaengplace/auth/dto/CustomUserDetails.java @@ -1,6 +1,7 @@ package com.mycom.backenddaengplace.auth.dto; import com.mycom.backenddaengplace.member.domain.Member; +import lombok.Getter; import lombok.RequiredArgsConstructor; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; @@ -8,10 +9,11 @@ import java.util.ArrayList; import java.util.Collection; +@Getter @RequiredArgsConstructor public class CustomUserDetails implements UserDetails { - private final Member Member; + private final Member member; @Override public Collection getAuthorities() { @@ -35,7 +37,7 @@ public String getPassword() { @Override public String getUsername() { // 사용자 이름 대신 providerId를 고유 식별자로 반환 - return Member.getProvider() + "_" + Member.getProviderId(); + return member.getProvider() + "_" + member.getProviderId(); } @Override diff --git a/src/main/java/com/mycom/backenddaengplace/auth/jwt/JWTFilter.java b/src/main/java/com/mycom/backenddaengplace/auth/jwt/JWTFilter.java index dfa08af..f699a55 100644 --- a/src/main/java/com/mycom/backenddaengplace/auth/jwt/JWTFilter.java +++ b/src/main/java/com/mycom/backenddaengplace/auth/jwt/JWTFilter.java @@ -1,6 +1,9 @@ package com.mycom.backenddaengplace.auth.jwt; +import com.mycom.backenddaengplace.auth.dto.CustomOAuth2User; import com.mycom.backenddaengplace.auth.dto.CustomUserDetails; +import com.mycom.backenddaengplace.auth.dto.UserDTO; +import com.mycom.backenddaengplace.auth.repository.AuthMemberRepository; import com.mycom.backenddaengplace.member.domain.Member; import io.jsonwebtoken.ExpiredJwtException; import jakarta.servlet.FilterChain; @@ -25,85 +28,72 @@ public class JWTFilter extends OncePerRequestFilter { private final JWTUtil jwtUtil; + private final AuthMemberRepository authMemberRepository; // 추가 @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { - // 헤더에서 access키에 담긴 토큰을 꺼냄 - // String accessToken = request.getHeader("access"); - // String accessToken = request.getHeader("Authorization"); String accessToken = request.getHeader(HttpHeaders.AUTHORIZATION); log.info("{}{} {}{}Authorization: {}", System.lineSeparator(), request.getMethod(), request.getRequestURI(), System.lineSeparator(), accessToken); - // 토큰이 없다면 다음 필터로 넘김 - if (accessToken == null) { + if (accessToken == null || !accessToken.startsWith("Bearer ")) { filterChain.doFilter(request, response); return; } - // 토큰 만료 여부 확인, 만료시 다음 필터로 넘기지 않음 + accessToken = accessToken.substring(7); // "Bearer " 제거 + try { jwtUtil.isExpired(accessToken); } catch (ExpiredJwtException e) { - - //response body PrintWriter writer = response.getWriter(); writer.print("access token expired"); - - //response status code response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); return; } - // 토큰이 access인지 확인 (발급시 페이로드에 명시) String category = jwtUtil.getCategory(accessToken); - if (!category.equals("access")) { - - //response body PrintWriter writer = response.getWriter(); writer.print("invalid access token"); - - //response status code response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); return; } -// username, role 값을 획득 String username = jwtUtil.getUsername(accessToken); String role = jwtUtil.getRole(accessToken); -// Member 객체 생성 (빌더 패턴 사용) - Member member = Member.builder() - .provider(username) - .providerId(username) - .nickname(username) - .build(); - - -// CustomUserDetails 생성 - /* - * TODO: 인증된 사용자 정보를 만드는 방법 - * 1. User 클래스를 사용하는 방법 - * 2. JPA 엔티티에 UserDetails 인터페이스를 구현한 클래스를 사용하는 방법 - * 3. 커스텀 UserDetails 클래스를 만들어 사용하는 방법 (추천) - */ - // User customUserDetails = new User(Member.getEmail(), Member.getPassword(), List.of(authEntity.getRole())); - CustomUserDetails customUserDetails = new CustomUserDetails(member); - -// Authentication 객체 생성 - Authentication authToken = new UsernamePasswordAuthenticationToken( - customUserDetails, - null, - customUserDetails.getAuthorities() // 기본 권한 ROLE_USER 반환 - ); - -// SecurityContextHolder에 인증 정보 설정 - // SecurityContext, PersistenceContext, XxxContext: Xxx - SecurityContextHolder.getContext().setAuthentication(authToken); - -// 다음 필터로 요청 전달 - filterChain.doFilter(request, response); + // username에서 provider와 providerId 추출 + String[] parts = username.split("_"); + if (parts.length == 2) { + String provider = parts[0]; + String providerId = parts[1]; + + // DB에서 Member 조회 String[] parts = username.split("_"); + Member member = authMemberRepository.findByProviderAndProviderId(provider, providerId); + if (member != null) { + // UserDTO 생성 + UserDTO userDTO = new UserDTO(); + userDTO.setProvider(provider); + userDTO.setProviderId(providerId); + userDTO.setEmail(member.getEmail()); + userDTO.setNickname(member.getNickname()); + userDTO.setProfileImage(member.getProfileImageUrl()); + + // CustomOAuth2User 생성 + CustomOAuth2User customOAuth2User = new CustomOAuth2User(userDTO, member); + + // Authentication 객체 생성 및 SecurityContext에 설정 + Authentication authToken = new UsernamePasswordAuthenticationToken( + customOAuth2User, + null, + customOAuth2User.getAuthorities() + ); + + SecurityContextHolder.getContext().setAuthentication(authToken); + } + } + filterChain.doFilter(request, response); } } \ No newline at end of file diff --git a/src/main/java/com/mycom/backenddaengplace/auth/jwt/JWTUtil.java b/src/main/java/com/mycom/backenddaengplace/auth/jwt/JWTUtil.java index ef0c5fb..2ad8df3 100644 --- a/src/main/java/com/mycom/backenddaengplace/auth/jwt/JWTUtil.java +++ b/src/main/java/com/mycom/backenddaengplace/auth/jwt/JWTUtil.java @@ -14,37 +14,60 @@ public class JWTUtil { private static final int BEARER_PREFIX_LENGTH = 7; + private static final String BEARER_PREFIX = "Bearer "; - private SecretKey secretKey; + private final SecretKey secretKey; - public JWTUtil(@Value("${spring.jwt.secret}")String secret) { + public JWTUtil(@Value("${spring.jwt.secret}") String secret) { + this.secretKey = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), + Jwts.SIG.HS256.key().build().getAlgorithm()); + } - this.secretKey = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), Jwts.SIG.HS256.key().build().getAlgorithm()); + private String removeBearer(String token) { + if (token != null && token.startsWith(BEARER_PREFIX)) { + return token.substring(BEARER_PREFIX_LENGTH); + } + return token; } public String getUsername(String token) { - - return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token.substring(7)).getPayload().get("username", String.class); + return Jwts.parser() + .verifyWith(secretKey) + .build() + .parseSignedClaims(removeBearer(token)) + .getPayload() + .get("username", String.class); } public String getRole(String token) { - - return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(getAccessToken(token)).getPayload().get("role", String.class); + return Jwts.parser() + .verifyWith(secretKey) + .build() + .parseSignedClaims(removeBearer(token)) + .getPayload() + .get("role", String.class); } public String getCategory(String token) { - return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token.substring(7)).getPayload().get("category", String.class); + return Jwts.parser() + .verifyWith(secretKey) + .build() + .parseSignedClaims(removeBearer(token)) + .getPayload() + .get("category", String.class); } - // ERROR: Compact JWT strings may not contain whitespace public Boolean isExpired(String token) { - - return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(getAccessToken(token)).getPayload().getExpiration().before(new Date()); + return Jwts.parser() + .verifyWith(secretKey) + .build() + .parseSignedClaims(removeBearer(token)) + .getPayload() + .getExpiration() + .before(new Date()); } - public String createJwt(String category, String username, String role, Long expiredMs) { - return Jwts.builder() .claim("category", category) .claim("username", username) @@ -54,9 +77,4 @@ public String createJwt(String category, String username, String role, Long expi .signWith(secretKey) .compact(); } - - private static String getAccessToken(String token) { - return token.substring(BEARER_PREFIX_LENGTH); - } - } diff --git a/src/main/java/com/mycom/backenddaengplace/auth/service/CustomOAuth2UserService.java b/src/main/java/com/mycom/backenddaengplace/auth/service/CustomOAuth2UserService.java index c19f4d0..8a21bef 100644 --- a/src/main/java/com/mycom/backenddaengplace/auth/service/CustomOAuth2UserService.java +++ b/src/main/java/com/mycom/backenddaengplace/auth/service/CustomOAuth2UserService.java @@ -28,20 +28,18 @@ public CustomOAuth2UserService(AuthMemberRepository authMemberRepository, AuthMe @Override public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException { - // 소셜 로그인으로 가져온 사용자 정보 OAuth2User oAuth2User = super.loadUser(userRequest); - - // 소셜 로그인 제공자 (google, kakao 등) String registrationId = userRequest.getClientRegistration().getRegistrationId(); OAuth2Response oAuth2Response = getOAuth2Response(registrationId, oAuth2User); // Member 저장 또는 업데이트 Member member = saveOrUpdateUser(oAuth2Response); - // UserDTO 생성 및 반환 + // UserDTO 생성 UserDTO userDTO = createUserDTO(member); - return new CustomOAuth2User(userDTO); + // CustomOAuth2User 생성 시 Member도 함께 전달 + return new CustomOAuth2User(userDTO, member); } private OAuth2Response getOAuth2Response(String registrationId, OAuth2User oAuth2User) { diff --git a/src/main/java/com/mycom/backenddaengplace/review/controller/ReviewController.java b/src/main/java/com/mycom/backenddaengplace/review/controller/ReviewController.java index 56c40fb..9898379 100644 --- a/src/main/java/com/mycom/backenddaengplace/review/controller/ReviewController.java +++ b/src/main/java/com/mycom/backenddaengplace/review/controller/ReviewController.java @@ -1,89 +1,72 @@ package com.mycom.backenddaengplace.review.controller; +import com.mycom.backenddaengplace.auth.dto.CustomOAuth2User; import com.mycom.backenddaengplace.common.dto.ApiResponse; +import com.mycom.backenddaengplace.member.domain.Member; import com.mycom.backenddaengplace.review.dto.request.ReviewRequest; import com.mycom.backenddaengplace.review.dto.response.PopularReviewResponse; import com.mycom.backenddaengplace.review.dto.response.ReviewResponse; -import com.mycom.backenddaengplace.review.dto.response.MemberReviewResponse; import com.mycom.backenddaengplace.review.service.ReviewService; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; - import java.util.List; @RestController @RequestMapping("/reviews") @RequiredArgsConstructor -public class ReviewController { +public class ReviewController { private final ReviewService reviewService; @PostMapping("/{placeId}") public ResponseEntity> createReview( @PathVariable Long placeId, - @Valid @RequestBody ReviewRequest request) { - - ReviewResponse response = reviewService.createReview(placeId, request); - - return ResponseEntity - .status(HttpStatus.CREATED) - .body(ApiResponse.success("리뷰가 성공적으로 등록되었습니다.", response)); + @Valid @RequestBody ReviewRequest request, + @AuthenticationPrincipal CustomOAuth2User customOAuth2User) { + Member member = customOAuth2User.getMember(); + return ResponseEntity.status(HttpStatus.CREATED) + .body(ApiResponse.success("리뷰가 등록되었습니다.", + reviewService.createReview(placeId, request, member.getId()))); } @GetMapping("/places/{placeId}") public ResponseEntity>> getReviews(@PathVariable Long placeId) { - List reviews = reviewService.getReviews(placeId); - - return ResponseEntity.ok(ApiResponse.success("리뷰 목록을 성공적으로 조회했습니다.", reviews)); + return ResponseEntity.ok(ApiResponse.success("리뷰 목록을 조회했습니다.", + reviewService.getReviews(placeId))); } @GetMapping("/{placeId}/and/{reviewId}") public ResponseEntity> getReviewDetail( @PathVariable Long placeId, @PathVariable Long reviewId) { - - ReviewResponse review = reviewService.getReviewDetail(placeId, reviewId); - - return ResponseEntity.ok( - ApiResponse.success("리뷰 상세 정보를 성공적으로 조회했습니다.", review) - ); + return ResponseEntity.ok(ApiResponse.success("리뷰 상세 정보를 조회했습니다.", + reviewService.getReviewDetail(placeId, reviewId))); } - @GetMapping("/members/{memberId}") - public ResponseEntity>> getUserReview(@PathVariable Long memberId) { - List reviews = reviewService.getUserReview(memberId); - - return ResponseEntity.ok(ApiResponse.success("사용자의 리뷰 목록을 성공적으로 조회했습니다.", reviews)); - } - - @DeleteMapping("/{memberId}/{reviewId}") - public ResponseEntity> deleteReview(@PathVariable Long memberId, @PathVariable Long reviewId) { - reviewService.deleteReview(memberId, reviewId); - return ResponseEntity.ok(ApiResponse.success("리뷰가 성공적으로 삭제되었습니다.")); + @DeleteMapping("/{reviewId}") + public ResponseEntity> deleteReview( + @PathVariable Long reviewId, + @AuthenticationPrincipal CustomOAuth2User customOAuth2User) { + reviewService.deleteReview(reviewId, customOAuth2User.getMember()); + return ResponseEntity.ok(ApiResponse.success("리뷰가 삭제되었습니다.")); } @GetMapping("/popular") public ResponseEntity>> getPopularReviews() { - List reviews = reviewService.getPopularReviews(); - return ResponseEntity.ok(ApiResponse.success( - "인기 리뷰 조회에 성공했습니다.", - reviews - )); + return ResponseEntity.ok(ApiResponse.success("인기 리뷰를 조회했습니다.", + reviewService.getPopularReviews())); } - @GetMapping("/{reviewId}") - public ResponseEntity> getReview(@PathVariable Long reviewId) { - MemberReviewResponse review = reviewService.getReview(reviewId); - return ResponseEntity.ok(ApiResponse.success("리뷰 조회에 성공했습니다.", review)); + @PatchMapping("/{reviewId}") + public ResponseEntity> updateReview( + @PathVariable Long reviewId, + @Valid @RequestBody ReviewRequest request, + @AuthenticationPrincipal CustomOAuth2User customOAuth2User) { + reviewService.updateReview(reviewId, request, customOAuth2User.getMember()); + return ResponseEntity.ok(ApiResponse.success("리뷰가 수정되었습니다.")); } - - @PatchMapping("/{memberId}/{reviewId}") - public ResponseEntity> updateReview(@PathVariable Long memberId, @PathVariable Long reviewId, @Valid @RequestBody ReviewRequest request) { - reviewService.updateReview(memberId, reviewId, request); - return ResponseEntity.ok(ApiResponse.success("리뷰가 성공적으로 수정되었습니다.")); - } - } diff --git a/src/main/java/com/mycom/backenddaengplace/review/controller/ReviewLikeController.java b/src/main/java/com/mycom/backenddaengplace/review/controller/ReviewLikeController.java index 64eace7..e7d77d6 100644 --- a/src/main/java/com/mycom/backenddaengplace/review/controller/ReviewLikeController.java +++ b/src/main/java/com/mycom/backenddaengplace/review/controller/ReviewLikeController.java @@ -1,36 +1,37 @@ package com.mycom.backenddaengplace.review.controller; +import com.mycom.backenddaengplace.auth.dto.CustomOAuth2User; import com.mycom.backenddaengplace.common.dto.ApiResponse; +import com.mycom.backenddaengplace.member.domain.Member; import com.mycom.backenddaengplace.review.dto.response.ReviewLikeResponse; import com.mycom.backenddaengplace.review.service.ReviewLikeService; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; @RestController -@RequestMapping("/reviews") +@RequestMapping("/reviews/likes") @RequiredArgsConstructor public class ReviewLikeController { private final ReviewLikeService reviewLikeService; - @PostMapping("/likes/{placeId}/and/{reviewId}/members/{memberId}") + @PostMapping("/{placeId}/and/{reviewId}") public ResponseEntity> createLike( @PathVariable Long placeId, @PathVariable Long reviewId, - @PathVariable Long memberId - ) { - ReviewLikeResponse response = reviewLikeService.createLike(placeId, reviewId, memberId); - return ResponseEntity.ok(ApiResponse.success("좋아요가 등록되었습니다.", response)); + @AuthenticationPrincipal CustomOAuth2User customOAuth2User) { + return ResponseEntity.ok(ApiResponse.success("좋아요가 등록되었습니다.", + reviewLikeService.createLike(placeId, reviewId, customOAuth2User.getMember()))); } - @DeleteMapping("/likes/{placeId}/and/{reviewId}/members/{memberId}") + @DeleteMapping("/{placeId}/and/{reviewId}") public ResponseEntity> deleteLike( @PathVariable Long placeId, @PathVariable Long reviewId, - @PathVariable Long memberId - ) { - ReviewLikeResponse response = reviewLikeService.deleteLike(placeId, reviewId, memberId); - return ResponseEntity.ok(ApiResponse.success("좋아요가 취소되었습니다.", response)); + @AuthenticationPrincipal CustomOAuth2User customOAuth2User) { + return ResponseEntity.ok(ApiResponse.success("좋아요가 취소되었습니다.", + reviewLikeService.deleteLike(placeId, reviewId, customOAuth2User.getMember()))); } } diff --git a/src/main/java/com/mycom/backenddaengplace/review/dto/request/ReviewRequest.java b/src/main/java/com/mycom/backenddaengplace/review/dto/request/ReviewRequest.java index dfad7bd..44cee48 100644 --- a/src/main/java/com/mycom/backenddaengplace/review/dto/request/ReviewRequest.java +++ b/src/main/java/com/mycom/backenddaengplace/review/dto/request/ReviewRequest.java @@ -10,8 +10,6 @@ @Setter @NoArgsConstructor(access = AccessLevel.PROTECTED) public class ReviewRequest { - @NotNull(message = "회원 ID는 필수입니다.") - private Long memberId; @NotNull(message = "별점은 필수입니다.") @DecimalMin(value = "0.0", message = "별점은 0.0 이상이어야 합니다.") diff --git a/src/main/java/com/mycom/backenddaengplace/review/service/ReviewLikeService.java b/src/main/java/com/mycom/backenddaengplace/review/service/ReviewLikeService.java index 74d1a91..d572458 100644 --- a/src/main/java/com/mycom/backenddaengplace/review/service/ReviewLikeService.java +++ b/src/main/java/com/mycom/backenddaengplace/review/service/ReviewLikeService.java @@ -1,12 +1,11 @@ package com.mycom.backenddaengplace.review.service; import com.mycom.backenddaengplace.member.domain.Member; -import com.mycom.backenddaengplace.member.exception.MemberNotFoundException; -import com.mycom.backenddaengplace.member.repository.MemberRepository; import com.mycom.backenddaengplace.review.domain.Review; import com.mycom.backenddaengplace.review.domain.ReviewLike; import com.mycom.backenddaengplace.review.dto.response.ReviewLikeResponse; import com.mycom.backenddaengplace.review.exception.ReviewException; +import com.mycom.backenddaengplace.review.exception.ReviewNotFoundException; import com.mycom.backenddaengplace.review.repository.ReviewLikeRepository; import com.mycom.backenddaengplace.review.repository.ReviewRepository; import lombok.RequiredArgsConstructor; @@ -14,57 +13,43 @@ import org.springframework.transaction.annotation.Transactional; @Service -@RequiredArgsConstructor @Transactional(readOnly = true) +@RequiredArgsConstructor public class ReviewLikeService { - private final ReviewLikeRepository reviewLikeRepository; private final ReviewRepository reviewRepository; - private final MemberRepository memberRepository; @Transactional - public ReviewLikeResponse createLike(Long placeId, Long reviewId, Long memberId) { + public ReviewLikeResponse createLike(Long placeId, Long reviewId, Member member) { Review review = findReviewAndValidate(placeId, reviewId); - Member member = findMember(memberId); - validateNotAlreadyLiked(review, member); ReviewLike reviewLike = reviewLikeRepository.save(new ReviewLike(review, member)); - long likeCount = reviewLikeRepository.countByReview(review); - - return ReviewLikeResponse.from(reviewId, likeCount, true); + return ReviewLikeResponse.from(reviewId, + reviewLikeRepository.countByReview(review), true); } @Transactional - public ReviewLikeResponse deleteLike(Long placeId, Long reviewId, Long memberId) { + public ReviewLikeResponse deleteLike(Long placeId, Long reviewId, Member member) { Review review = findReviewAndValidate(placeId, reviewId); - Member member = findMember(memberId); - ReviewLike reviewLike = reviewLikeRepository.findByReviewAndMember(review, member) - .orElseThrow(() -> ReviewException.notFound(placeId, reviewId)); + .orElseThrow(() -> new ReviewNotFoundException(placeId, reviewId)); reviewLikeRepository.delete(reviewLike); - long likeCount = reviewLikeRepository.countByReview(review); - - return ReviewLikeResponse.from(reviewId, likeCount, false); + return ReviewLikeResponse.from(reviewId, + reviewLikeRepository.countByReview(review), false); } private Review findReviewAndValidate(Long placeId, Long reviewId) { Review review = reviewRepository.findById(reviewId) - .orElseThrow(() -> ReviewException.notFound(placeId, reviewId)); + .orElseThrow(() -> new ReviewNotFoundException(placeId, reviewId)); if (!review.getPlace().getId().equals(placeId)) { - throw ReviewException.notFound(placeId, reviewId); + throw new ReviewNotFoundException(placeId, reviewId); } - return review; } - private Member findMember(Long memberId) { - return memberRepository.findById(memberId) - .orElseThrow(() -> new MemberNotFoundException(memberId)); - } - private void validateNotAlreadyLiked(Review review, Member member) { if (reviewLikeRepository.existsByReviewAndMember(review, member)) { throw ReviewException.alreadyLiked(review.getId()); diff --git a/src/main/java/com/mycom/backenddaengplace/review/service/ReviewService.java b/src/main/java/com/mycom/backenddaengplace/review/service/ReviewService.java index 03e42b2..97620b3 100644 --- a/src/main/java/com/mycom/backenddaengplace/review/service/ReviewService.java +++ b/src/main/java/com/mycom/backenddaengplace/review/service/ReviewService.java @@ -10,9 +10,7 @@ import com.mycom.backenddaengplace.review.dto.request.ReviewRequest; import com.mycom.backenddaengplace.review.dto.response.PopularReviewResponse; import com.mycom.backenddaengplace.review.dto.response.ReviewResponse; -import com.mycom.backenddaengplace.review.dto.response.MemberReviewResponse; import com.mycom.backenddaengplace.review.exception.ReviewAlreadyExistsException; -import com.mycom.backenddaengplace.review.exception.ReviewException; import com.mycom.backenddaengplace.review.exception.ReviewNotFoundException; import com.mycom.backenddaengplace.review.exception.ReviewNotOwnedException; import com.mycom.backenddaengplace.review.repository.ReviewQueryRepository; @@ -25,31 +23,26 @@ import java.util.stream.Collectors; @Service -@RequiredArgsConstructor @Transactional(readOnly = true) +@RequiredArgsConstructor public class ReviewService { - private final ReviewRepository reviewRepository; private final PlaceRepository placeRepository; - private final MemberRepository memberRepository; private final ReviewQueryRepository reviewQueryRepository; + private final MemberRepository memberRepository; @Transactional - public ReviewResponse createReview(Long placeId, ReviewRequest request) { - // 회원 조회 - Member member = memberRepository.findById(request.getMemberId()) - .orElseThrow(() -> new MemberNotFoundException(request.getMemberId())); - - // 장소 조회 + public ReviewResponse createReview(Long placeId, ReviewRequest request, Long memberId) { Place place = placeRepository.findById(placeId) .orElseThrow(() -> new PlaceNotFoundException(placeId)); - // 리뷰 중복 검사 + Member member = memberRepository.findById(memberId) + .orElseThrow(() -> new MemberNotFoundException(memberId)); + if (reviewRepository.existsByMemberAndPlace(member, place)) { - throw new ReviewAlreadyExistsException(request.getMemberId(), placeId); + throw new ReviewAlreadyExistsException(member.getId(), placeId); } - // 리뷰 생성 Review review = Review.builder() .member(member) .place(place) @@ -58,83 +51,50 @@ public ReviewResponse createReview(Long placeId, ReviewRequest request) { .traitTag(request.getTraitTag()) .build(); - review = reviewRepository.save(review); - - return ReviewResponse.from(review); + return ReviewResponse.from(reviewRepository.save(review)); } public List getReviews(Long placeId) { if (!placeRepository.existsById(placeId)) { throw new PlaceNotFoundException(placeId); } - - List reviews = reviewRepository.findByPlaceId(placeId); - return reviews.stream() + return reviewRepository.findByPlaceId(placeId).stream() .map(ReviewResponse::from) .collect(Collectors.toList()); } public ReviewResponse getReviewDetail(Long placeId, Long reviewId) { - // 장소 존재 여부 확인 if (!placeRepository.existsById(placeId)) { throw new PlaceNotFoundException(placeId); } - - // 리뷰 조회 - Review review = reviewRepository.findByIdAndPlaceId(reviewId, placeId) - .orElseThrow(() -> new ReviewNotFoundException(reviewId, placeId)); - - return ReviewResponse.from(review); - } - - public List getUserReview(Long memberId) { - if (!memberRepository.existsById(memberId)) { - throw new MemberNotFoundException(memberId); - } - - List reviews = reviewRepository.findByMemberId(memberId); - return reviews.stream() - .map(MemberReviewResponse::from) - .collect(Collectors.toList()); + return ReviewResponse.from(reviewRepository.findByIdAndPlaceId(reviewId, placeId) + .orElseThrow(() -> new ReviewNotFoundException(reviewId, placeId))); } @Transactional - public void deleteReview(Long memberId, Long reviewId) { - if (!memberRepository.existsById(memberId)) { - throw new MemberNotFoundException(memberId); - } + public void deleteReview(Long reviewId, Member member) { + Review review = reviewRepository.findById(reviewId) + .orElseThrow(() -> new ReviewNotFoundException(reviewId, null)); - Review review = reviewRepository.findByIdAndMemberId(reviewId, memberId); - if (review == null) { - throw new ReviewNotOwnedException(memberId, reviewId); + if (!review.getMember().equals(member)) { + throw new ReviewNotOwnedException(member.getId(), reviewId); } - reviewRepository.delete(review); - } - public List getPopularReviews() { - List reviews = reviewQueryRepository.findPopularReviews(); - return reviews.stream() + return reviewQueryRepository.findPopularReviews().stream() .map(PopularReviewResponse::from) .collect(Collectors.toList()); } - public MemberReviewResponse getReview(Long reviewId) { - Review review = reviewRepository.findById(reviewId) - .orElseThrow(() -> ReviewException.notFound(reviewId)); - - return MemberReviewResponse.from(review); - } - @Transactional - public void updateReview(Long memberId, Long reviewId, ReviewRequest request) { + public void updateReview(Long reviewId, ReviewRequest request, Member member) { Review review = reviewRepository.findById(reviewId) .orElseThrow(() -> new ReviewNotFoundException(reviewId, null)); - if (!review.getMember().getId().equals(memberId)) { - throw new ReviewNotOwnedException(memberId, reviewId); + if (!review.getMember().equals(member)) { + throw new ReviewNotOwnedException(member.getId(), reviewId); } review.update(request.getContent(), request.getRating(), request.getTraitTag()); } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 7e0ebb4..bf1314e 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,6 +1,6 @@ spring: profiles: - active: dev, email, security + active: dev, security, email config: import: - application-email.yml