Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,10 @@ public class LikeController {
@PostMapping(value = "/posts/{postId}/like", produces = "application/json")
public ResponseEntity<PostLikeResponseDto> 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개를 반환합니다.")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ List<Post> findTopPostsCreatedInPeriodOrderByLikesNative(
@Param("start") LocalDateTime start,
@Param("end") LocalDateTime end
);
// ⭐ 추가: 특정 게시글의 좋아요 개수 조회
long countByPost(Post post);


// ⭐ 추가: 특정 사용자의 모든 좋아요 삭제
void deleteByUser(User user);
}
42 changes: 27 additions & 15 deletions src/main/java/server/loop/domain/post/service/LikeService.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand All @@ -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<PostLike> 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<TopLikedPostResponseDto> getYesterdayTop5LikedPosts() {
LocalDate yesterday = LocalDate.now().minusDays(1);
LocalDateTime start = yesterday.atStartOfDay();
Expand Down
22 changes: 10 additions & 12 deletions src/main/java/server/loop/domain/post/service/PostService.java
Original file line number Diff line number Diff line change
Expand Up @@ -125,25 +125,23 @@ public Long updatePost(Long postId, PostUpdateRequestDto requestDto, List<Multip
}



// 게시글 삭제
public void deletePost(Long postId, String email) throws AccessDeniedException {
// 1. postId로 게시글을 조회
// 1. 게시글 존재 여부 확인
Post post = postRepository.findById(postId)
.orElseThrow(() -> 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);
}
}
11 changes: 8 additions & 3 deletions src/main/java/server/loop/global/config/SecurityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -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 서브도메인
));
Expand All @@ -52,7 +55,7 @@ public CorsConfigurationSource corsConfigurationSource() {
return source;
}

// OPTIONS 요청 전역 허용
// OPTIONS 요청 전역 허용은 이미 잘 설정되어 있으므로 유지하면 됩니다.
@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
return (web) -> web.ignoring().requestMatchers(HttpMethod.OPTIONS, "/**");
Expand All @@ -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()
Expand All @@ -89,4 +93,5 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
return http.build();
}


}