diff --git a/src/main/java/com/be/sportizebe/domain/chat/controller/NoteController.java b/src/main/java/com/be/sportizebe/domain/chat/controller/NoteController.java index 58c8562..35465aa 100644 --- a/src/main/java/com/be/sportizebe/domain/chat/controller/NoteController.java +++ b/src/main/java/com/be/sportizebe/domain/chat/controller/NoteController.java @@ -6,9 +6,9 @@ import com.be.sportizebe.domain.post.entity.Post; import com.be.sportizebe.domain.post.exception.PostErrorCode; import com.be.sportizebe.domain.post.repository.PostRepository; -import com.be.sportizebe.domain.user.entity.User; import com.be.sportizebe.global.exception.CustomException; import com.be.sportizebe.global.response.BaseResponse; +import com.be.sportizebe.global.cache.dto.UserAuthInfo; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; @@ -33,12 +33,12 @@ public class NoteController { @Operation(summary = "1대1 채팅방 생성", description = "게시글 작성자와 1대1 쪽지 채팅방을 생성합니다. 채팅방 이름은 게시글 제목으로 설정됩니다. 이미 채팅방이 존재하면 기존 채팅방을 반환합니다.") public ResponseEntity> createChatRoom( @Parameter(description = "게시글 ID") @PathVariable Long postId, - @AuthenticationPrincipal User user) { + @AuthenticationPrincipal UserAuthInfo userAuthInfo) { Post post = postRepository.findById(postId) .orElseThrow(() -> new CustomException(PostErrorCode.POST_NOT_FOUND)); - ChatRoom room = chatRoomService.createNote(post, user); + ChatRoom room = chatRoomService.createNote(post, userAuthInfo.getId()); return ResponseEntity.status(HttpStatus.CREATED) .body(BaseResponse.success("채팅방 생성 성공", ChatRoomResponse.from(room))); @@ -47,9 +47,9 @@ public ResponseEntity> createChatRoom( @GetMapping("/rooms") @Operation(summary = "내 쪽지 채팅방 목록 조회", description = "현재 사용자가 참여한 모든 쪽지 채팅방 목록을 조회합니다.") public ResponseEntity>> getMyChatRooms( - @AuthenticationPrincipal User user) { + @AuthenticationPrincipal UserAuthInfo userAuthInfo) { - List rooms = chatRoomService.findMyNoteRooms(user).stream() + List rooms = chatRoomService.findMyNoteRooms(userAuthInfo.getId()).stream() .map(ChatRoomResponse::from) .toList(); diff --git a/src/main/java/com/be/sportizebe/domain/chat/repository/ChatRoomRepository.java b/src/main/java/com/be/sportizebe/domain/chat/repository/ChatRoomRepository.java index 1fc70ea..9d6b7e2 100644 --- a/src/main/java/com/be/sportizebe/domain/chat/repository/ChatRoomRepository.java +++ b/src/main/java/com/be/sportizebe/domain/chat/repository/ChatRoomRepository.java @@ -2,7 +2,6 @@ import com.be.sportizebe.domain.chat.entity.ChatRoom; import com.be.sportizebe.domain.post.entity.Post; -import com.be.sportizebe.domain.user.entity.User; import org.springframework.data.jpa.repository.JpaRepository; import java.util.List; @@ -11,8 +10,8 @@ public interface ChatRoomRepository extends JpaRepository { // 특정 게시글에 대해 두 사용자 간의 1대1 채팅방이 이미 존재하는지 확인 - Optional findByPostAndGuestUser(Post post, User guestUser); + Optional findByPostAndGuestUserId(Post post, Long guestUserId); // 사용자가 참여한 1대1 채팅방 목록 조회 (게시글 작성자 또는 채팅 요청자로 참여) - List findByPost_UserOrGuestUser(User postUser, User guestUser); + List findByPost_UserIdOrGuestUserId(Long postUserId, Long guestUserId); } diff --git a/src/main/java/com/be/sportizebe/domain/chat/service/ChatRoomService.java b/src/main/java/com/be/sportizebe/domain/chat/service/ChatRoomService.java index 2363281..10fc9d9 100644 --- a/src/main/java/com/be/sportizebe/domain/chat/service/ChatRoomService.java +++ b/src/main/java/com/be/sportizebe/domain/chat/service/ChatRoomService.java @@ -6,6 +6,8 @@ import com.be.sportizebe.domain.club.entity.Club; import com.be.sportizebe.domain.post.entity.Post; import com.be.sportizebe.domain.user.entity.User; +import com.be.sportizebe.domain.user.exception.UserErrorCode; +import com.be.sportizebe.domain.user.repository.UserRepository; import com.be.sportizebe.global.exception.CustomException; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -18,6 +20,7 @@ @Transactional(readOnly = true) public class ChatRoomService { private final ChatRoomRepository chatRoomRepository; + private final UserRepository userRepository; @Transactional public ChatRoom createGroup(Club club) { @@ -29,17 +32,19 @@ public ChatRoom createGroup(Club club) { } @Transactional - public ChatRoom createNote(Post post, User guestUser) { + public ChatRoom createNote(Post post, Long guestUserId) { User hostUser = post.getUser(); // 게시글 등록자 // 자기 자신에게 채팅 불가 - if (hostUser.getId() == guestUser.getId()) { + if (hostUser.getId() == guestUserId) { throw new CustomException(ChatErrorCode.SELF_CHAT_NOT_ALLOWED); } // 이미 존재하는 채팅방이 있으면 반환 - return chatRoomRepository.findByPostAndGuestUser(post, guestUser) + return chatRoomRepository.findByPostAndGuestUserId(post, guestUserId) .orElseGet(() -> { + User guestUser = userRepository.findById(guestUserId) + .orElseThrow(() -> new CustomException(UserErrorCode.USER_NOT_FOUND)); ChatRoom room = ChatRoom.builder() .chatRoomType(ChatRoom.ChatRoomType.NOTE) .post(post) @@ -50,8 +55,8 @@ public ChatRoom createNote(Post post, User guestUser) { } // 사용자가 참여한 1대1 채팅방 목록 조회 - public List findMyNoteRooms(User user) { - return chatRoomRepository.findByPost_UserOrGuestUser(user, user); + public List findMyNoteRooms(Long userId) { + return chatRoomRepository.findByPost_UserIdOrGuestUserId(userId, userId); } public List findAll() { diff --git a/src/main/java/com/be/sportizebe/domain/club/controller/ClubController.java b/src/main/java/com/be/sportizebe/domain/club/controller/ClubController.java index a7776b5..1bdf12e 100644 --- a/src/main/java/com/be/sportizebe/domain/club/controller/ClubController.java +++ b/src/main/java/com/be/sportizebe/domain/club/controller/ClubController.java @@ -5,8 +5,8 @@ import com.be.sportizebe.domain.club.dto.response.ClubImageResponse; import com.be.sportizebe.domain.club.dto.response.ClubResponse; import com.be.sportizebe.domain.club.service.ClubServiceImpl; -import com.be.sportizebe.domain.user.entity.User; import com.be.sportizebe.global.response.BaseResponse; +import com.be.sportizebe.global.cache.dto.UserAuthInfo; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; @@ -32,8 +32,8 @@ public class ClubController { public ResponseEntity> createClub( @RequestPart("request") @Valid ClubCreateRequest request, @RequestPart(value = "image", required = false) MultipartFile image, - @AuthenticationPrincipal User user) { - ClubResponse response = clubService.createClub(request, image, user); + @AuthenticationPrincipal UserAuthInfo userAuthInfo) { + ClubResponse response = clubService.createClub(request, image, userAuthInfo.getId()); return ResponseEntity.status(HttpStatus.CREATED) .body(BaseResponse.success("동호회 생성 성공", response)); } @@ -43,8 +43,8 @@ public ResponseEntity> createClub( public ResponseEntity> updateClub( @PathVariable Long clubId, @RequestBody @Valid ClubUpdateRequest request, - @AuthenticationPrincipal User user) { - ClubResponse response = clubService.updateClub(clubId, request, user); + @AuthenticationPrincipal UserAuthInfo userAuthInfo) { + ClubResponse response = clubService.updateClub(clubId, request, userAuthInfo.getId()); return ResponseEntity.ok(BaseResponse.success("동호회 수정 성공", response)); } @@ -53,8 +53,8 @@ public ResponseEntity> updateClub( public ResponseEntity> updateClubImage( @Parameter(description = "동호회 ID") @PathVariable Long clubId, @RequestPart("image") MultipartFile image, - @AuthenticationPrincipal User user) { - ClubImageResponse response = clubService.updateClubImage(clubId, image, user); + @AuthenticationPrincipal UserAuthInfo userAuthInfo) { + ClubImageResponse response = clubService.updateClubImage(clubId, image, userAuthInfo.getId()); return ResponseEntity.ok(BaseResponse.success("동호회 사진 수정 성공", response)); } } diff --git a/src/main/java/com/be/sportizebe/domain/club/service/ClubService.java b/src/main/java/com/be/sportizebe/domain/club/service/ClubService.java index 4ac4c0e..5ad3028 100644 --- a/src/main/java/com/be/sportizebe/domain/club/service/ClubService.java +++ b/src/main/java/com/be/sportizebe/domain/club/service/ClubService.java @@ -4,13 +4,12 @@ import com.be.sportizebe.domain.club.dto.request.ClubUpdateRequest; import com.be.sportizebe.domain.club.dto.response.ClubImageResponse; import com.be.sportizebe.domain.club.dto.response.ClubResponse; -import com.be.sportizebe.domain.user.entity.User; import org.springframework.web.multipart.MultipartFile; public interface ClubService { - ClubResponse createClub(ClubCreateRequest request, MultipartFile image, User user); // 동호회 생성 + ClubResponse createClub(ClubCreateRequest request, MultipartFile image, Long userId); // 동호회 생성 - ClubResponse updateClub(Long clubId, ClubUpdateRequest request, User user); // 동호회 수정 + ClubResponse updateClub(Long clubId, ClubUpdateRequest request, Long userId); // 동호회 수정 - ClubImageResponse updateClubImage(Long clubId, MultipartFile image, User user); // 동호회 사진 수정 + ClubImageResponse updateClubImage(Long clubId, MultipartFile image, Long userId); // 동호회 사진 수정 } diff --git a/src/main/java/com/be/sportizebe/domain/club/service/ClubServiceImpl.java b/src/main/java/com/be/sportizebe/domain/club/service/ClubServiceImpl.java index 49de51c..529f9c6 100644 --- a/src/main/java/com/be/sportizebe/domain/club/service/ClubServiceImpl.java +++ b/src/main/java/com/be/sportizebe/domain/club/service/ClubServiceImpl.java @@ -11,6 +11,8 @@ import com.be.sportizebe.domain.club.repository.ClubMemberRepository; import com.be.sportizebe.domain.club.repository.ClubRepository; import com.be.sportizebe.domain.user.entity.User; +import com.be.sportizebe.domain.user.exception.UserErrorCode; +import com.be.sportizebe.domain.user.repository.UserRepository; import com.be.sportizebe.global.exception.CustomException; import com.be.sportizebe.global.s3.enums.PathName; import com.be.sportizebe.global.s3.service.S3Service; @@ -27,11 +29,15 @@ public class ClubServiceImpl implements ClubService { private final ClubRepository clubRepository; private final ChatRoomService chatRoomService; private final ClubMemberRepository clubMemberRepository; + private final UserRepository userRepository; private final S3Service s3Service; @Override @Transactional - public ClubResponse createClub(ClubCreateRequest request, MultipartFile image, User user) { + public ClubResponse createClub(ClubCreateRequest request, MultipartFile image, Long userId) { + User user = userRepository.findById(userId) + .orElseThrow(() -> new CustomException(UserErrorCode.USER_NOT_FOUND)); + if (clubRepository.existsByName(request.name())) { throw new CustomException(ClubErrorCode.CLUB_NAME_DUPLICATED); } @@ -62,12 +68,12 @@ public ClubResponse createClub(ClubCreateRequest request, MultipartFile image, U @Override @Transactional - public ClubResponse updateClub(Long clubId, ClubUpdateRequest request, User user) { + public ClubResponse updateClub(Long clubId, ClubUpdateRequest request, Long userId) { Club club = clubRepository.findById(clubId) .orElseThrow(() -> new CustomException(ClubErrorCode.CLUB_NOT_FOUND)); // 동호회 방장만 수정 가능하도록 검증 - if (club.getLeader().getId() != user.getId()) { + if (club.getLeader().getId() != userId) { throw new CustomException(ClubErrorCode.CLUB_UPDATE_DENIED); } @@ -86,12 +92,12 @@ public ClubResponse updateClub(Long clubId, ClubUpdateRequest request, User user @Override @Transactional - public ClubImageResponse updateClubImage(Long clubId, MultipartFile image, User user) { + public ClubImageResponse updateClubImage(Long clubId, MultipartFile image, Long userId) { Club club = clubRepository.findById(clubId) .orElseThrow(() -> new CustomException(ClubErrorCode.CLUB_NOT_FOUND)); // 동호회 방장만 수정 가능하도록 검증 - if (club.getLeader().getId() != user.getId()) { + if (club.getLeader().getId() != userId) { throw new CustomException(ClubErrorCode.CLUB_UPDATE_DENIED); } diff --git a/src/main/java/com/be/sportizebe/domain/comment/controller/CommentController.java b/src/main/java/com/be/sportizebe/domain/comment/controller/CommentController.java index e473077..bc61f13 100644 --- a/src/main/java/com/be/sportizebe/domain/comment/controller/CommentController.java +++ b/src/main/java/com/be/sportizebe/domain/comment/controller/CommentController.java @@ -4,8 +4,8 @@ import com.be.sportizebe.domain.comment.dto.response.CommentListResponse; import com.be.sportizebe.domain.comment.dto.response.CommentResponse; import com.be.sportizebe.domain.comment.service.CommentService; -import com.be.sportizebe.domain.user.entity.User; import com.be.sportizebe.global.response.BaseResponse; +import com.be.sportizebe.global.cache.dto.UserAuthInfo; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; @@ -16,8 +16,6 @@ import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; -import java.util.List; - @RestController @RequiredArgsConstructor @RequestMapping("/api/posts/{postId}/comments") @@ -31,8 +29,8 @@ public class CommentController { public ResponseEntity> createComment( @Parameter(description = "게시글 ID") @PathVariable Long postId, @RequestBody @Valid CreateCommentRequest request, - @AuthenticationPrincipal User user) { - CommentResponse response = commentService.createComment(postId, request, user); + @AuthenticationPrincipal UserAuthInfo userAuthInfo) { + CommentResponse response = commentService.createComment(postId, request, userAuthInfo.getId()); return ResponseEntity.status(HttpStatus.CREATED) .body(BaseResponse.success("댓글 생성 성공", response)); } @@ -50,8 +48,8 @@ public ResponseEntity> getComments( public ResponseEntity> deleteComment( @Parameter(description = "게시글 ID") @PathVariable Long postId, @Parameter(description = "댓글 ID") @PathVariable Long commentId, - @AuthenticationPrincipal User user) { - commentService.deleteComment(postId, commentId, user); + @AuthenticationPrincipal UserAuthInfo userAuthInfo) { + commentService.deleteComment(postId, commentId, userAuthInfo.getId()); return ResponseEntity.ok(BaseResponse.success("댓글 삭제 성공", null)); } diff --git a/src/main/java/com/be/sportizebe/domain/comment/service/CommentService.java b/src/main/java/com/be/sportizebe/domain/comment/service/CommentService.java index af3a052..b6b2c0e 100644 --- a/src/main/java/com/be/sportizebe/domain/comment/service/CommentService.java +++ b/src/main/java/com/be/sportizebe/domain/comment/service/CommentService.java @@ -3,20 +3,17 @@ import com.be.sportizebe.domain.comment.dto.request.CreateCommentRequest; import com.be.sportizebe.domain.comment.dto.response.CommentListResponse; import com.be.sportizebe.domain.comment.dto.response.CommentResponse; -import com.be.sportizebe.domain.user.entity.User; - -import java.util.List; public interface CommentService { // 댓글 생성 (대댓글 포함) - CommentResponse createComment(Long postId, CreateCommentRequest request, User user); + CommentResponse createComment(Long postId, CreateCommentRequest request, Long userId); // 게시글의 댓글 목록 조회 CommentListResponse getCommentsByPostId(Long postId); // 댓글 삭제 - void deleteComment(Long postId, Long commentId, User user); + void deleteComment(Long postId, Long commentId, Long userId); // 게시글의 댓글 수 조회 long getCommentCount(Long postId); diff --git a/src/main/java/com/be/sportizebe/domain/comment/service/CommentServiceImpl.java b/src/main/java/com/be/sportizebe/domain/comment/service/CommentServiceImpl.java index a51b6e7..81a4347 100644 --- a/src/main/java/com/be/sportizebe/domain/comment/service/CommentServiceImpl.java +++ b/src/main/java/com/be/sportizebe/domain/comment/service/CommentServiceImpl.java @@ -10,6 +10,8 @@ import com.be.sportizebe.domain.post.exception.PostErrorCode; import com.be.sportizebe.domain.post.repository.PostRepository; import com.be.sportizebe.domain.user.entity.User; +import com.be.sportizebe.domain.user.exception.UserErrorCode; +import com.be.sportizebe.domain.user.repository.UserRepository; import com.be.sportizebe.global.exception.CustomException; import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; @@ -27,15 +29,20 @@ public class CommentServiceImpl implements CommentService { private final CommentRepository commentRepository; private final PostRepository postRepository; + private final UserRepository userRepository; @Override @CacheEvict(cacheNames = {"commentList", "commentCount"}, key = "#postId") @Transactional - public CommentResponse createComment(Long postId, CreateCommentRequest request, User user) { + public CommentResponse createComment(Long postId, CreateCommentRequest request, Long userId) { // 게시글 조회 Post post = postRepository.findById(postId) .orElseThrow(() -> new CustomException(PostErrorCode.POST_NOT_FOUND)); + // 사용자 조회 + User user = userRepository.findById(userId) + .orElseThrow(() -> new CustomException(UserErrorCode.USER_NOT_FOUND)); + // 부모 댓글 조회 (대댓글인 경우) Comment parent = null; // null로 초기화 if (request.parentId() != null) { // parentId가 null이 아니면 대댓글이기 때문에 부모 댓글 조회로 검증 필요함 @@ -79,12 +86,12 @@ public CommentListResponse getCommentsByPostId(Long postId) { @Override @CacheEvict(cacheNames = {"commentList", "commentCount"}, key = "#postId") @Transactional - public void deleteComment(Long postId, Long commentId, User user) { + public void deleteComment(Long postId, Long commentId, Long userId) { Comment comment = commentRepository.findById(commentId) .orElseThrow(() -> new CustomException(CommentErrorCode.COMMENT_NOT_FOUND)); // 작성자 확인 - if (comment.getUser().getId() != user.getId()) { + if (comment.getUser().getId() != userId) { throw new CustomException(CommentErrorCode.COMMENT_DELETE_DENIED); } diff --git a/src/main/java/com/be/sportizebe/domain/like/controller/LikeController.java b/src/main/java/com/be/sportizebe/domain/like/controller/LikeController.java index 7df4106..68865a1 100644 --- a/src/main/java/com/be/sportizebe/domain/like/controller/LikeController.java +++ b/src/main/java/com/be/sportizebe/domain/like/controller/LikeController.java @@ -3,8 +3,8 @@ import com.be.sportizebe.domain.like.dto.response.LikeResponse; import com.be.sportizebe.domain.like.entity.LikeTargetType; import com.be.sportizebe.domain.like.service.LikeService; -import com.be.sportizebe.domain.user.entity.User; import com.be.sportizebe.global.response.BaseResponse; +import com.be.sportizebe.global.cache.dto.UserAuthInfo; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; @@ -24,27 +24,27 @@ public class LikeController { @PostMapping("/posts/{postId}") @Operation(summary = "게시글 좋아요 토글", description = "게시글 좋아요 추가/취소") public ResponseEntity> togglePostLike( - @AuthenticationPrincipal User user, + @AuthenticationPrincipal UserAuthInfo userAuthInfo, @Parameter(description = "게시글 ID") @PathVariable Long postId) { - LikeResponse response = likeService.toggleLike(user, LikeTargetType.POST, postId); + LikeResponse response = likeService.toggleLike(userAuthInfo.getId(), LikeTargetType.POST, postId); return ResponseEntity.ok(BaseResponse.success("좋아요 처리 완료", response)); } @PostMapping("/comments/{commentId}") @Operation(summary = "댓글 좋아요 토글", description = "댓글 좋아요 추가/취소") public ResponseEntity> toggleCommentLike( - @AuthenticationPrincipal User user, + @AuthenticationPrincipal UserAuthInfo userAuthInfo, @Parameter(description = "댓글 ID") @PathVariable Long commentId) { - LikeResponse response = likeService.toggleLike(user, LikeTargetType.COMMENT, commentId); + LikeResponse response = likeService.toggleLike(userAuthInfo.getId(), LikeTargetType.COMMENT, commentId); return ResponseEntity.ok(BaseResponse.success("좋아요 처리 완료", response)); } @GetMapping("/posts/{postId}") @Operation(summary = "게시글 좋아요 수 조회", description = "게시글의 좋아요 수와 본인 좋아요 여부 조회") public ResponseEntity> getPostLikeStatus( - @AuthenticationPrincipal User user, + @AuthenticationPrincipal UserAuthInfo userAuthInfo, @Parameter(description = "게시글 ID") @PathVariable Long postId) { - boolean liked = likeService.isLiked(user, LikeTargetType.POST, postId); + boolean liked = likeService.isLiked(userAuthInfo.getId(), LikeTargetType.POST, postId); long likeCount = likeService.getLikeCount(LikeTargetType.POST, postId); LikeResponse response = LikeResponse.of(liked, LikeTargetType.POST, postId, likeCount); return ResponseEntity.ok(BaseResponse.success(response)); @@ -53,9 +53,9 @@ public ResponseEntity> getPostLikeStatus( @GetMapping("/comments/{commentId}") @Operation(summary = "댓글 좋아요 수 조회", description = "댓글의 좋아요 수와 본인 좋아요 여부 조회") public ResponseEntity> getCommentLikeStatus( - @AuthenticationPrincipal User user, + @AuthenticationPrincipal UserAuthInfo userAuthInfo, @Parameter(description = "댓글 ID") @PathVariable Long commentId) { - boolean liked = likeService.isLiked(user, LikeTargetType.COMMENT, commentId); + boolean liked = likeService.isLiked(userAuthInfo.getId(), LikeTargetType.COMMENT, commentId); long likeCount = likeService.getLikeCount(LikeTargetType.COMMENT, commentId); LikeResponse response = LikeResponse.of(liked, LikeTargetType.COMMENT, commentId, likeCount); return ResponseEntity.ok(BaseResponse.success(response)); diff --git a/src/main/java/com/be/sportizebe/domain/like/repository/LikeRepository.java b/src/main/java/com/be/sportizebe/domain/like/repository/LikeRepository.java index 70a0959..a86f36c 100644 --- a/src/main/java/com/be/sportizebe/domain/like/repository/LikeRepository.java +++ b/src/main/java/com/be/sportizebe/domain/like/repository/LikeRepository.java @@ -2,7 +2,6 @@ import com.be.sportizebe.domain.like.entity.Like; import com.be.sportizebe.domain.like.entity.LikeTargetType; -import com.be.sportizebe.domain.user.entity.User; import org.springframework.data.jpa.repository.JpaRepository; import java.util.Optional; @@ -10,15 +9,14 @@ public interface LikeRepository extends JpaRepository { // 특정 사용자가 특정 대상에 좋아요 했는지 확인 - // npe를 방지하기 위해 Optional로 null이 올 수 있는 값을 감쌈(참조시 npe 발생x) - Optional findByUserAndTargetTypeAndTargetId(User user, LikeTargetType targetType, Long targetId); + Optional findByUserIdAndTargetTypeAndTargetId(Long userId, LikeTargetType targetType, Long targetId); // 특정 사용자가 특정 대상에 좋아요 했는지 여부 - boolean existsByUserAndTargetTypeAndTargetId(User user, LikeTargetType targetType, Long targetId); + boolean existsByUserIdAndTargetTypeAndTargetId(Long userId, LikeTargetType targetType, Long targetId); // 특정 대상의 좋아요 수 long countByTargetTypeAndTargetId(LikeTargetType targetType, Long targetId); // 특정 사용자의 특정 대상 좋아요 삭제 - void deleteByUserAndTargetTypeAndTargetId(User user, LikeTargetType targetType, Long targetId); + void deleteByUserIdAndTargetTypeAndTargetId(Long userId, LikeTargetType targetType, Long targetId); } \ No newline at end of file diff --git a/src/main/java/com/be/sportizebe/domain/like/service/LikeService.java b/src/main/java/com/be/sportizebe/domain/like/service/LikeService.java index 50f2535..7db7c2a 100644 --- a/src/main/java/com/be/sportizebe/domain/like/service/LikeService.java +++ b/src/main/java/com/be/sportizebe/domain/like/service/LikeService.java @@ -2,15 +2,14 @@ import com.be.sportizebe.domain.like.dto.response.LikeResponse; import com.be.sportizebe.domain.like.entity.LikeTargetType; -import com.be.sportizebe.domain.user.entity.User; public interface LikeService { // 좋아요 토글 (좋아요 추가/취소) - LikeResponse toggleLike(User user, LikeTargetType targetType, Long targetId); + LikeResponse toggleLike(Long userId, LikeTargetType targetType, Long targetId); // 좋아요 여부 확인 - boolean isLiked(User user, LikeTargetType targetType, Long targetId); + boolean isLiked(Long userId, LikeTargetType targetType, Long targetId); // 좋아요 수 조회 long getLikeCount(LikeTargetType targetType, Long targetId); diff --git a/src/main/java/com/be/sportizebe/domain/like/service/LikeServiceImpl.java b/src/main/java/com/be/sportizebe/domain/like/service/LikeServiceImpl.java index ed6cd91..90983d7 100644 --- a/src/main/java/com/be/sportizebe/domain/like/service/LikeServiceImpl.java +++ b/src/main/java/com/be/sportizebe/domain/like/service/LikeServiceImpl.java @@ -5,39 +5,41 @@ import com.be.sportizebe.domain.like.entity.LikeTargetType; import com.be.sportizebe.domain.like.repository.LikeRepository; import com.be.sportizebe.domain.user.entity.User; +import com.be.sportizebe.domain.user.exception.UserErrorCode; +import com.be.sportizebe.domain.user.repository.UserRepository; +import com.be.sportizebe.global.exception.CustomException; import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.Cacheable; import org.springframework.cache.annotation.Caching; -import org.springframework.dao.DataIntegrityViolationException; -import org.springframework.security.core.parameters.P; import org.springframework.stereotype.Service; -import java.util.Optional; - @Slf4j @Service @RequiredArgsConstructor public class LikeServiceImpl implements LikeService { private final LikeRepository likeRepository; + private final UserRepository userRepository; @Override @Transactional @Caching(evict = { @CacheEvict(cacheNames = "likeCount", key = "#targetType + ':' + #targetId"), - @CacheEvict(cacheNames = "likeStatus", key = "#user.id + ':' + #targetType + ':' + #targetId") + @CacheEvict(cacheNames = "likeStatus", key = "#userId + ':' + #targetType + ':' + #targetId") }) - public LikeResponse toggleLike(User user, LikeTargetType targetType, Long targetId) { + public LikeResponse toggleLike(Long userId, LikeTargetType targetType, Long targetId) { boolean liked = false; // 좋아요 여부 변수 - if(likeRepository.existsByUserAndTargetTypeAndTargetId(user, targetType, targetId)) { - likeRepository.deleteByUserAndTargetTypeAndTargetId(user, targetType, targetId); + if(likeRepository.existsByUserIdAndTargetTypeAndTargetId(userId, targetType, targetId)) { + likeRepository.deleteByUserIdAndTargetTypeAndTargetId(userId, targetType, targetId); } else { // 좋아요 추가 + User user = userRepository.findById(userId) + .orElseThrow(() -> new CustomException(UserErrorCode.USER_NOT_FOUND)); Like like = Like.builder() .user(user) .targetType(targetType) @@ -53,9 +55,9 @@ public LikeResponse toggleLike(User user, LikeTargetType targetType, Long target } @Override - @Cacheable(cacheNames = "likeStatus", key = "#user.id + ':' + #targetType + ':' + #targetId") - public boolean isLiked(User user, LikeTargetType targetType, Long targetId) { - return likeRepository.existsByUserAndTargetTypeAndTargetId(user, targetType, targetId); + @Cacheable(cacheNames = "likeStatus", key = "#userId + ':' + #targetType + ':' + #targetId") + public boolean isLiked(Long userId, LikeTargetType targetType, Long targetId) { + return likeRepository.existsByUserIdAndTargetTypeAndTargetId(userId, targetType, targetId); } @Override diff --git a/src/main/java/com/be/sportizebe/domain/post/controller/PostController.java b/src/main/java/com/be/sportizebe/domain/post/controller/PostController.java index 3cacfeb..4143c4b 100644 --- a/src/main/java/com/be/sportizebe/domain/post/controller/PostController.java +++ b/src/main/java/com/be/sportizebe/domain/post/controller/PostController.java @@ -6,14 +6,13 @@ import com.be.sportizebe.domain.post.dto.response.PostResponse; import com.be.sportizebe.domain.post.entity.PostProperty; import com.be.sportizebe.domain.post.service.PostService; -import com.be.sportizebe.domain.user.entity.User; import com.be.sportizebe.global.response.BaseResponse; +import com.be.sportizebe.global.cache.dto.UserAuthInfo; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.data.web.PageableDefault; @@ -37,9 +36,9 @@ public class PostController { public ResponseEntity> createPost( @Parameter(description = "게시판 종류 (SOCCER, BASKETBALL, FREE)") @PathVariable PostProperty property, @RequestPart("request") @Valid CreatePostRequest request, - @RequestPart(value = "image", required = false) MultipartFile image, // JSON 아님 - @AuthenticationPrincipal User user) { - PostResponse response = postService.createPost(property, request, image, user); + @RequestPart(value = "image", required = false) MultipartFile image, + @AuthenticationPrincipal UserAuthInfo userAuthInfo) { + PostResponse response = postService.createPost(property, request, image, userAuthInfo.getId()); return ResponseEntity.status(HttpStatus.CREATED) .body(BaseResponse.success("게시글 생성 성공", response)); } @@ -49,8 +48,8 @@ public ResponseEntity> createPost( public ResponseEntity> updatePost( @Parameter(description = "게시글 ID") @PathVariable Long postId, @RequestBody @Valid UpdatePostRequest request, - @AuthenticationPrincipal User user) { - PostResponse response = postService.updatePost(postId, request, user); + @AuthenticationPrincipal UserAuthInfo userAuthInfo) { + PostResponse response = postService.updatePost(postId, request, userAuthInfo.getId()); return ResponseEntity.ok(BaseResponse.success("게시글 수정 성공", response)); } @@ -58,8 +57,8 @@ public ResponseEntity> updatePost( @Operation(summary = "게시글 삭제", description = "게시글 삭제 (작성자만 가능)") public ResponseEntity> deletePost( @Parameter(description = "게시글 ID") @PathVariable Long postId, - @AuthenticationPrincipal User user) { - postService.deletePost(postId, user); + @AuthenticationPrincipal UserAuthInfo userAuthInfo) { + postService.deletePost(postId, userAuthInfo.getId()); return ResponseEntity.ok(BaseResponse.success("게시글 삭제 성공", null)); } diff --git a/src/main/java/com/be/sportizebe/domain/post/service/PostService.java b/src/main/java/com/be/sportizebe/domain/post/service/PostService.java index 6191f2f..06e680c 100644 --- a/src/main/java/com/be/sportizebe/domain/post/service/PostService.java +++ b/src/main/java/com/be/sportizebe/domain/post/service/PostService.java @@ -5,17 +5,15 @@ import com.be.sportizebe.domain.post.dto.response.PostPageResponse; import com.be.sportizebe.domain.post.dto.response.PostResponse; import com.be.sportizebe.domain.post.entity.PostProperty; -import com.be.sportizebe.domain.user.entity.User; -import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.web.multipart.MultipartFile; public interface PostService { - PostResponse createPost(PostProperty property, CreatePostRequest request, MultipartFile image, User user); // 게시글 생성 + PostResponse createPost(PostProperty property, CreatePostRequest request, MultipartFile image, Long userId); // 게시글 생성 - PostResponse updatePost(Long postId, UpdatePostRequest request, User user); // 게시글 수정 + PostResponse updatePost(Long postId, UpdatePostRequest request, Long userId); // 게시글 수정 - void deletePost(Long postId, User user); // 게시글 삭제 + void deletePost(Long postId, Long userId); // 게시글 삭제 PostPageResponse getPosts(PostProperty property, Pageable pageable); // 게시글 목록 조회 } diff --git a/src/main/java/com/be/sportizebe/domain/post/service/PostServiceImpl.java b/src/main/java/com/be/sportizebe/domain/post/service/PostServiceImpl.java index f77e0b0..ba77c07 100644 --- a/src/main/java/com/be/sportizebe/domain/post/service/PostServiceImpl.java +++ b/src/main/java/com/be/sportizebe/domain/post/service/PostServiceImpl.java @@ -9,6 +9,8 @@ import com.be.sportizebe.domain.post.exception.PostErrorCode; import com.be.sportizebe.domain.post.repository.PostRepository; import com.be.sportizebe.domain.user.entity.User; +import com.be.sportizebe.domain.user.exception.UserErrorCode; +import com.be.sportizebe.domain.user.repository.UserRepository; import com.be.sportizebe.global.exception.CustomException; import com.be.sportizebe.global.s3.enums.PathName; import com.be.sportizebe.global.s3.service.S3Service; @@ -28,12 +30,16 @@ public class PostServiceImpl implements PostService { private final PostRepository postRepository; + private final UserRepository userRepository; private final S3Service s3Service; @Override @CacheEvict(cacheNames = "postList", allEntries = true) @Transactional - public PostResponse createPost(PostProperty property, CreatePostRequest request, MultipartFile image, User user) { + public PostResponse createPost(PostProperty property, CreatePostRequest request, MultipartFile image, Long userId) { + User user = userRepository.findById(userId) + .orElseThrow(() -> new CustomException(UserErrorCode.USER_NOT_FOUND)); + // 이미지가 있으면 S3에 업로드 String imgUrl = null; if (image != null && !image.isEmpty()) { @@ -49,12 +55,12 @@ public PostResponse createPost(PostProperty property, CreatePostRequest request, @Override @CacheEvict(cacheNames = "postList", allEntries = true) @Transactional - public void deletePost(Long postId, User user) { + public void deletePost(Long postId, Long userId) { Post post = postRepository.findById(postId) .orElseThrow(() -> new CustomException(PostErrorCode.POST_NOT_FOUND)); // 작성자 확인 - if (post.getUser().getId() != user.getId()) { + if (post.getUser().getId() != userId) { throw new CustomException(PostErrorCode.POST_DELETE_DENIED); } @@ -64,12 +70,12 @@ public void deletePost(Long postId, User user) { @Override @CacheEvict(cacheNames = "postList", allEntries = true) @Transactional - public PostResponse updatePost(Long postId, UpdatePostRequest request, User user) { + public PostResponse updatePost(Long postId, UpdatePostRequest request, Long userId) { Post post = postRepository.findById(postId) .orElseThrow(() -> new CustomException(PostErrorCode.POST_NOT_FOUND)); // 작성자 확인 - if (post.getUser().getId() != user.getId()) { + if (post.getUser().getId() != userId) { throw new CustomException(PostErrorCode.POST_UPDATE_DENIED); } diff --git a/src/main/java/com/be/sportizebe/domain/user/controller/UserController.java b/src/main/java/com/be/sportizebe/domain/user/controller/UserController.java index 103bf41..414c5ab 100644 --- a/src/main/java/com/be/sportizebe/domain/user/controller/UserController.java +++ b/src/main/java/com/be/sportizebe/domain/user/controller/UserController.java @@ -1,13 +1,11 @@ package com.be.sportizebe.domain.user.controller; -import com.be.sportizebe.domain.auth.exception.AuthErrorCode; import com.be.sportizebe.domain.user.dto.request.SignUpRequest; import com.be.sportizebe.domain.user.dto.response.ProfileImageResponse; import com.be.sportizebe.domain.user.dto.response.SignUpResponse; -import com.be.sportizebe.domain.user.entity.User; import com.be.sportizebe.domain.user.service.UserServiceImpl; -import com.be.sportizebe.global.exception.CustomException; import com.be.sportizebe.global.response.BaseResponse; +import com.be.sportizebe.global.cache.dto.UserAuthInfo; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; @@ -38,10 +36,10 @@ public ResponseEntity> signUp(@RequestBody @Valid S @PostMapping(value = "/profile-image", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) @Operation(summary = "프로필 사진 업로드", description = "사용자 프로필 사진을 업로드합니다. (최대 5MB, jpg/jpeg/png/gif/webp 지원)") public ResponseEntity> uploadProfileImage( - @AuthenticationPrincipal User user, + @AuthenticationPrincipal UserAuthInfo userAuthInfo, @RequestPart("file") MultipartFile file ) { - ProfileImageResponse response = userService.uploadProfileImage(user.getId(), file); + ProfileImageResponse response = userService.uploadProfileImage(userAuthInfo.getId(), file); return ResponseEntity.ok(BaseResponse.success("프로필 사진 업로드 성공", response)); } } \ No newline at end of file diff --git a/src/main/java/com/be/sportizebe/domain/user/service/UserService.java b/src/main/java/com/be/sportizebe/domain/user/service/UserService.java index 6efa18e..691fc28 100644 --- a/src/main/java/com/be/sportizebe/domain/user/service/UserService.java +++ b/src/main/java/com/be/sportizebe/domain/user/service/UserService.java @@ -12,5 +12,4 @@ public interface UserService { // 프로필 사진 업로드 ProfileImageResponse uploadProfileImage(Long userId, MultipartFile file); - } diff --git a/src/main/java/com/be/sportizebe/global/cache/dto/UserAuthInfo.java b/src/main/java/com/be/sportizebe/global/cache/dto/UserAuthInfo.java new file mode 100644 index 0000000..249434e --- /dev/null +++ b/src/main/java/com/be/sportizebe/global/cache/dto/UserAuthInfo.java @@ -0,0 +1,32 @@ +package com.be.sportizebe.global.cache.dto; + +import com.be.sportizebe.domain.user.entity.Role; +import com.be.sportizebe.domain.user.entity.User; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.io.Serializable; + +/** + * JWT 인증 필터에서 사용할 캐시용 사용자 정보 DTO + * User 엔티티의 연관관계(posts 등)로 인한 직렬화 문제를 방지 + */ +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class UserAuthInfo implements Serializable { // Serializable : 직렬화 가능하다라는 마커 표시 + private Long id; + private String username; + private String nickname; + private Role role; + + public static UserAuthInfo from(User user) { + return new UserAuthInfo( + user.getId(), + user.getUsername(), + user.getNickname(), + user.getRole() + ); + } +} \ No newline at end of file diff --git a/src/main/java/com/be/sportizebe/global/cache/service/UserCacheService.java b/src/main/java/com/be/sportizebe/global/cache/service/UserCacheService.java new file mode 100644 index 0000000..8cd78b1 --- /dev/null +++ b/src/main/java/com/be/sportizebe/global/cache/service/UserCacheService.java @@ -0,0 +1,42 @@ +package com.be.sportizebe.global.cache.service; + +import com.be.sportizebe.global.cache.dto.UserAuthInfo; +import com.be.sportizebe.domain.user.repository.UserRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Service; + +/** + * JWT 인증 필터에서 사용할 사용자 정보 캐시 서비스 + * Filter는 Spring AOP 프록시 범위 밖이므로 별도 서비스로 분리 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class UserCacheService { + + private final UserRepository userRepository; + + /** + * userId로 사용자 인증 정보 조회 (캐시 적용) + * 캐시 TTL: 5분 (RedisCacheConfig에서 설정) + */ + @Cacheable(cacheNames = "userAuthInfo", key = "#userId", unless = "#result == null") + public UserAuthInfo findUserAuthInfoById(Long userId) { + log.debug("DB에서 사용자 정보 조회: userId={}", userId); + return userRepository.findById(userId) + .map(UserAuthInfo::from) + .orElse(null); + } + + /** + * 사용자 정보 변경 시 캐시 무효화 + * 비밀번호 변경, 역할 변경, 회원 탈퇴 등에서 호출 + */ + @CacheEvict(cacheNames = "userAuthInfo", key = "#userId") + public void evictUserAuthInfo(Long userId) { + log.debug("사용자 캐시 무효화: userId={}", userId); + } +} \ No newline at end of file diff --git a/src/main/java/com/be/sportizebe/global/config/RedisCacheConfig.java b/src/main/java/com/be/sportizebe/global/config/RedisCacheConfig.java index 7bc269e..0140a25 100644 --- a/src/main/java/com/be/sportizebe/global/config/RedisCacheConfig.java +++ b/src/main/java/com/be/sportizebe/global/config/RedisCacheConfig.java @@ -2,6 +2,7 @@ import com.be.sportizebe.domain.comment.dto.response.CommentListResponse; import com.be.sportizebe.domain.post.dto.response.PostPageResponse; +import com.be.sportizebe.global.cache.dto.UserAuthInfo; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.json.JsonMapper; @@ -58,6 +59,11 @@ public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) new Jackson2JsonRedisSerializer<>(Boolean.class); likeStatusSerializer.setObjectMapper(objectMapper); + // userAuthInfo 캐시는 UserAuthInfo 타입으로 역직렬화 + Jackson2JsonRedisSerializer userAuthInfoSerializer = + new Jackson2JsonRedisSerializer<>(UserAuthInfo.class); + userAuthInfoSerializer.setObjectMapper(objectMapper); + // 기본 캐시 설정 // TTL: 5분, Serializer: Object 기준 @@ -115,6 +121,17 @@ public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) ) .entryTtl(Duration.ofSeconds(15)) ); + + // JWT 인증 필터에서 사용하는 사용자 인증 정보 캐시 (TTL: 5분) + cacheConfigs.put( + "userAuthInfo", + RedisCacheConfiguration.defaultCacheConfig() + .serializeValuesWith( + RedisSerializationContext.SerializationPair.fromSerializer(userAuthInfoSerializer) + ) + .entryTtl(Duration.ofMinutes(5)) + ); + return RedisCacheManager.builder(redisConnectionFactory) .cacheDefaults(defaultConfig) .withInitialCacheConfigurations(cacheConfigs) diff --git a/src/main/java/com/be/sportizebe/global/jwt/JwtAuthenticationFilter.java b/src/main/java/com/be/sportizebe/global/jwt/JwtAuthenticationFilter.java index d0e7ff2..b078721 100644 --- a/src/main/java/com/be/sportizebe/global/jwt/JwtAuthenticationFilter.java +++ b/src/main/java/com/be/sportizebe/global/jwt/JwtAuthenticationFilter.java @@ -1,8 +1,8 @@ package com.be.sportizebe.global.jwt; -import com.be.sportizebe.domain.user.entity.User; -import com.be.sportizebe.domain.user.repository.UserRepository; import com.be.sportizebe.global.exception.CustomException; +import com.be.sportizebe.global.cache.dto.UserAuthInfo; +import com.be.sportizebe.global.cache.service.UserCacheService; import io.jsonwebtoken.JwtException; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; @@ -27,7 +27,7 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { private static final String BEARER_PREFIX = "Bearer "; private final JwtProvider jwtProvider; - private final UserRepository userRepository; + private final UserCacheService userCacheService; @Override protected void doFilterInternal( @@ -39,15 +39,14 @@ protected void doFilterInternal( // TODO: AccessToken과 RefreshToken 구분해서 검증하는 로직 필요함 if (token != null && jwtProvider.validateToken(token)) { Long userId = Long.parseLong(jwtProvider.extractSocialId(token)); - User user = userRepository.findById(userId) - .orElse(null); + UserAuthInfo userAuthInfo = userCacheService.findUserAuthInfoById(userId); - if (user != null) { + if (userAuthInfo != null) { UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( - user, // User 엔티티를 직접 principal로 설정 + userAuthInfo, // 캐시된 UserAuthInfo를 principal로 설정 null, - List.of(new SimpleGrantedAuthority(user.getRole().name()))); + List.of(new SimpleGrantedAuthority(userAuthInfo.getRole().name()))); SecurityContextHolder.getContext().setAuthentication(authentication); log.debug("SecurityContext에 '{}' 인증 정보를 저장했습니다.", userId);