From 6fb353cdd169f2db3e1746f0faeba4ce1a7547ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=B0=EC=84=9D=ED=98=84?= Date: Wed, 6 Aug 2025 02:53:22 +0900 Subject: [PATCH] [FIX] like,delete logic --- .../post/controller/LikeController.java | 11 ++--- .../entity/repository/PostLikeRepository.java | 5 ++- .../loop/domain/post/service/LikeService.java | 42 ++++++++++++------- .../loop/domain/post/service/PostService.java | 22 +++++----- .../loop/global/config/SecurityConfig.java | 11 +++-- 5 files changed, 53 insertions(+), 38 deletions(-) diff --git a/src/main/java/server/loop/domain/post/controller/LikeController.java b/src/main/java/server/loop/domain/post/controller/LikeController.java index 6b5c691..4d0dcc3 100644 --- a/src/main/java/server/loop/domain/post/controller/LikeController.java +++ b/src/main/java/server/loop/domain/post/controller/LikeController.java @@ -29,13 +29,10 @@ public class LikeController { @PostMapping(value = "/posts/{postId}/like", produces = "application/json") public ResponseEntity toggleLike(@PathVariable Long postId, @AuthenticationPrincipal UserDetails userDetails) { - likeService.toggleLike(postId, userDetails.getUsername()); - Post post = postRepository.findById(postId) - .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 게시글입니다.")); - boolean likedByUser = post.getLikes().stream() - .anyMatch(like -> like.getUser().getEmail().equals(userDetails.getUsername())); - - return ResponseEntity.ok(new PostLikeResponseDto(likedByUser, post.getLikes().size())); + // ⭐ 수정 사항: 서비스에서 직접 DTO를 반환하도록 변경 + // 이렇게 하면 컨트롤러가 불필요하게 DB를 다시 조회하는 것을 방지합니다. + PostLikeResponseDto responseDto = likeService.toggleLike(postId, userDetails.getUsername()); + return ResponseEntity.ok(responseDto); } @Operation(summary = "전날 작성된 글 중 좋아요 Top 5", description = "전날 작성된 게시글 중 좋아요가 가장 많은 게시글 5개를 반환합니다.") diff --git a/src/main/java/server/loop/domain/post/entity/repository/PostLikeRepository.java b/src/main/java/server/loop/domain/post/entity/repository/PostLikeRepository.java index 8e5fe33..f9fee9d 100644 --- a/src/main/java/server/loop/domain/post/entity/repository/PostLikeRepository.java +++ b/src/main/java/server/loop/domain/post/entity/repository/PostLikeRepository.java @@ -38,6 +38,9 @@ List findTopPostsCreatedInPeriodOrderByLikesNative( @Param("start") LocalDateTime start, @Param("end") LocalDateTime end ); + // ⭐ 추가: 특정 게시글의 좋아요 개수 조회 + long countByPost(Post post); - + // ⭐ 추가: 특정 사용자의 모든 좋아요 삭제 + void deleteByUser(User user); } \ No newline at end of file diff --git a/src/main/java/server/loop/domain/post/service/LikeService.java b/src/main/java/server/loop/domain/post/service/LikeService.java index cdadf00..a81ed79 100644 --- a/src/main/java/server/loop/domain/post/service/LikeService.java +++ b/src/main/java/server/loop/domain/post/service/LikeService.java @@ -4,6 +4,7 @@ import org.springframework.data.domain.PageRequest; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import server.loop.domain.post.dto.post.res.PostLikeResponseDto; import server.loop.domain.post.dto.post.res.PostResponseDto; import server.loop.domain.post.dto.post.res.TopLikedPostResponseDto; import server.loop.domain.post.entity.Post; @@ -17,6 +18,7 @@ import java.time.LocalDateTime; import java.time.LocalTime; import java.util.List; +import java.util.Optional; import java.util.stream.Collectors; @Service @@ -28,27 +30,37 @@ public class LikeService { private final UserRepository userRepository; private final PostRepository postRepository; - public String toggleLike(Long postId, String email) { + public PostLikeResponseDto toggleLike(Long postId, String email) { User user = userRepository.findByEmail(email) .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 사용자입니다.")); Post post = postRepository.findById(postId) .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 게시글입니다.")); - // 이미 좋아요를 눌렀는지 확인 - return postLikeRepository.findByUserAndPost(user, post) - .map(like -> { - // 이미 좋아요를 눌렀다면 -> 좋아요 취소 - postLikeRepository.delete(like); - return "좋아요를 취소했습니다."; - }) - .orElseGet(() -> { - // 좋아요를 누르지 않았다면 -> 좋아요 추가 - PostLike like = new PostLike(user); - like.setPost(post); // 연관관계 편의 메서드 사용 - postLikeRepository.save(like); - return "좋아요를 눌렀습니다."; - }); + // Optional을 활용하여 좋아요 존재 여부 확인 + Optional existingLike = postLikeRepository.findByUserAndPost(user, post); + + if (existingLike.isPresent()) { + // 이미 좋아요를 눌렀다면 -> 좋아요 취소 + postLikeRepository.delete(existingLike.get()); + } else { + // 좋아요를 누르지 않았다면 -> 좋아요 추가 + PostLike like = new PostLike(user); + like.setPost(post); + postLikeRepository.save(like); + } + + // 최신 좋아요 상태를 다시 확인 + boolean likedByUser = postLikeRepository.findByUserAndPost(user, post).isPresent(); + // 최신 좋아요 수 + long likeCount = postLikeRepository.countByPost(post); // long 타입으로 받기 + + // DTO에 int 타입이 필요하다면 캐스팅 + int likeCountAsInt = (int) likeCount; + + return new PostLikeResponseDto(likedByUser, likeCountAsInt); } + + public List getYesterdayTop5LikedPosts() { LocalDate yesterday = LocalDate.now().minusDays(1); LocalDateTime start = yesterday.atStartOfDay(); diff --git a/src/main/java/server/loop/domain/post/service/PostService.java b/src/main/java/server/loop/domain/post/service/PostService.java index 777798f..7c11af0 100644 --- a/src/main/java/server/loop/domain/post/service/PostService.java +++ b/src/main/java/server/loop/domain/post/service/PostService.java @@ -125,25 +125,23 @@ public Long updatePost(Long postId, PostUpdateRequestDto requestDto, List new IllegalArgumentException("존재하지 않는 게시글입니다.")); - // 2. 작성자 본인 확인 - // 현재 로그인된 사용자의 이메일과 게시글 작성자의 이메일을 비교 - if (!post.getAuthor().getEmail().equals(email)) { + // 2. 로그인한 사용자 정보 조회 + User user = userRepository.findByEmail(email) + .orElseThrow(() -> new IllegalArgumentException("사용자를 찾을 수 없습니다.")); + + // 3. ⭐ 권한 확인: 게시글 작성자와 로그인 사용자가 일치하는지 확인 + if (!post.getAuthor().equals(user)) { + // 작성자가 아닐 경우 AccessDeniedException 발생 throw new AccessDeniedException("게시글을 삭제할 권한이 없습니다."); } - // 3. (선택 사항) S3 이미지 파일 삭제 - // 게시글에 연결된 이미지가 있다면, S3에서 먼저 삭제 - post.getImages().forEach(img -> s3UploadService.deleteImageFromS3(img.getImageUrl())); - - // 4. 데이터베이스에서 게시글을 영구 삭제 - // post 객체로 삭제해도 되고, post.getId()로 삭제해도 됩니다. - postRepository.delete(post); // 또는 postRepository.deleteById(postId); + // 4. 게시글 삭제 로직 (여기서 외래 키에 따른 이미지 삭제 등) + postRepository.delete(post); } } \ No newline at end of file diff --git a/src/main/java/server/loop/global/config/SecurityConfig.java b/src/main/java/server/loop/global/config/SecurityConfig.java index b44daea..d097d3d 100644 --- a/src/main/java/server/loop/global/config/SecurityConfig.java +++ b/src/main/java/server/loop/global/config/SecurityConfig.java @@ -38,8 +38,11 @@ public PasswordEncoder passwordEncoder() { @Bean public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration configuration = new CorsConfiguration(); - configuration.setAllowedOriginPatterns(List.of( - "https://*.vercel.app", // Vercel 모든 배포 환경 허용 + // 로컬 환경과 배포 환경 모두 명시 + configuration.setAllowedOrigins(List.of( + "http://localhost:5173", // 로컬 프론트엔드 개발 환경 + "http://localhost:8080", // 로컬 백엔드 서버(테스트용) + "https://*.vercel.app", // Vercel 배포 환경 "https://loop.o-r.kr", // 커스텀 도메인 "https://www.loop.o-r.kr" // www 서브도메인 )); @@ -52,7 +55,7 @@ public CorsConfigurationSource corsConfigurationSource() { return source; } - // OPTIONS 요청 전역 허용 + // OPTIONS 요청 전역 허용은 이미 잘 설정되어 있으므로 유지하면 됩니다. @Bean public WebSecurityCustomizer webSecurityCustomizer() { return (web) -> web.ignoring().requestMatchers(HttpMethod.OPTIONS, "/**"); @@ -65,6 +68,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .cors(cors -> cors.configurationSource(corsConfigurationSource())) .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(auth -> auth + .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll() // <-- 추가 .requestMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll() .requestMatchers(HttpMethod.POST, "/api/users/signup").permitAll() .requestMatchers(HttpMethod.POST, "/api/users/login").permitAll() @@ -89,4 +93,5 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti return http.build(); } + }