diff --git a/backend-board/src/main/java/com/backendboard/domain/post/controller/PostController.java b/backend-board/src/main/java/com/backendboard/domain/post/controller/PostController.java index ad17270..ca69cac 100644 --- a/backend-board/src/main/java/com/backendboard/domain/post/controller/PostController.java +++ b/backend-board/src/main/java/com/backendboard/domain/post/controller/PostController.java @@ -98,8 +98,13 @@ public ResponseEntity updatePost( content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class))), }) @GetMapping("/{postId}") - public ResponseEntity readPost(@PathVariable Long postId) { - PostReadResponse response = postService.getPost(postId); + public ResponseEntity readPost(@PathVariable Long postId, + @AuthenticationPrincipal CustomUserDetails customUserDetails) { + Long currentAuthUserId = null; + if (customUserDetails != null) { + currentAuthUserId = customUserDetails.getId(); + } + PostReadResponse response = postService.getPost(postId, currentAuthUserId); return ResponseEntity.status(HttpStatus.OK).body(response); } diff --git a/backend-board/src/main/java/com/backendboard/domain/post/dto/PostReadResponse.java b/backend-board/src/main/java/com/backendboard/domain/post/dto/PostReadResponse.java index 7dfb3ec..216a453 100644 --- a/backend-board/src/main/java/com/backendboard/domain/post/dto/PostReadResponse.java +++ b/backend-board/src/main/java/com/backendboard/domain/post/dto/PostReadResponse.java @@ -57,14 +57,14 @@ private PostReadResponse(Long id, String author, String title, String content, L @Builder - public static PostReadResponse toDto(Post post, String author, List images) { + public static PostReadResponse toDto(Post post, String author, List images, Long viewCount) { return PostReadResponse.builder() .id(post.getId()) .author(author) .title(post.getTitle()) .content(post.getContent()) .likeCount(post.getLikeCount()) - .viewCount(post.getViewCount()) + .viewCount(viewCount) .images(images.stream().map(PostImageReadResponse::toDto).toList()) .createdDate(post.getCreatedDate()) .lastModifiedDate(post.getLastModifiedDate()) diff --git a/backend-board/src/main/java/com/backendboard/domain/post/respository/ViewCountRedisRepository.java b/backend-board/src/main/java/com/backendboard/domain/post/respository/ViewCountRedisRepository.java new file mode 100644 index 0000000..f68e84c --- /dev/null +++ b/backend-board/src/main/java/com/backendboard/domain/post/respository/ViewCountRedisRepository.java @@ -0,0 +1,13 @@ +package com.backendboard.domain.post.respository; + +import java.util.Map; + +public interface ViewCountRedisRepository { + void delete(); + + Map getEntries(); + + void incrementCount(String postId); + + Long getIncrementCount(String postId); +} diff --git a/backend-board/src/main/java/com/backendboard/domain/post/respository/ViewCountRedisRepositoryImpl.java b/backend-board/src/main/java/com/backendboard/domain/post/respository/ViewCountRedisRepositoryImpl.java new file mode 100644 index 0000000..93303b0 --- /dev/null +++ b/backend-board/src/main/java/com/backendboard/domain/post/respository/ViewCountRedisRepositoryImpl.java @@ -0,0 +1,39 @@ +package com.backendboard.domain.post.respository; + +import java.util.Map; +import java.util.Optional; + +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Repository; + +import lombok.RequiredArgsConstructor; + +@Repository +@RequiredArgsConstructor +public class ViewCountRedisRepositoryImpl implements ViewCountRedisRepository { + private static final String KEY = "post:view:count"; + + private final RedisTemplate redisTemplate; + + @Override + public void delete() { + redisTemplate.delete(KEY); + } + + @Override + public Map getEntries() { + return redisTemplate.opsForHash().entries(KEY); + } + + @Override + public void incrementCount(String postId) { + redisTemplate.opsForHash().increment(KEY, postId, 1); + } + + @Override + public Long getIncrementCount(String postId) { + return Optional.ofNullable(redisTemplate.opsForHash().get(KEY, postId)) + .map(value -> ((Number)value).longValue()) + .orElse(0L); + } +} diff --git a/backend-board/src/main/java/com/backendboard/domain/post/scheduler/PostScheduler.java b/backend-board/src/main/java/com/backendboard/domain/post/scheduler/PostScheduler.java index 949b5cf..b5114dd 100644 --- a/backend-board/src/main/java/com/backendboard/domain/post/scheduler/PostScheduler.java +++ b/backend-board/src/main/java/com/backendboard/domain/post/scheduler/PostScheduler.java @@ -8,6 +8,7 @@ import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; +import com.backendboard.domain.post.respository.ViewCountRedisRepository; import com.backendboard.domain.postlike.repository.PostLikeRedisRepository; import lombok.RequiredArgsConstructor; @@ -18,11 +19,13 @@ @RequiredArgsConstructor public class PostScheduler { private static final String UPDATE_LIKE_COUNT_SQL = "UPDATE post SET like_count = ? WHERE id = ?"; + private static final String UPDATE_VIEW_COUNT_SQL = "UPDATE post SET view_count = view_count + ? WHERE id = ?"; private final PostLikeRedisRepository postLikeRedisRepository; + private final ViewCountRedisRepository viewCountRedisRepository; private final JdbcTemplate jdbcTemplate; - @Scheduled(fixedDelay = 300_000) + @Scheduled(fixedDelay = 180_000) public void syncLikeCount() { Map entries = postLikeRedisRepository.getEntries(); List batchArgs = new ArrayList<>(); @@ -36,4 +39,19 @@ public void syncLikeCount() { jdbcTemplate.batchUpdate(UPDATE_LIKE_COUNT_SQL, batchArgs); postLikeRedisRepository.delete(); } + + @Scheduled(fixedDelay = 60_000) + public void syncViewCount() { + Map entries = viewCountRedisRepository.getEntries(); + List batchArgs = new ArrayList<>(); + + for (Map.Entry entry : entries.entrySet()) { + String postId = (String)entry.getKey(); + long viewCount = ((Number)entry.getValue()).longValue(); + batchArgs.add(new Object[] {viewCount, Long.valueOf(postId)}); + } + + jdbcTemplate.batchUpdate(UPDATE_VIEW_COUNT_SQL, batchArgs); + viewCountRedisRepository.delete(); + } } diff --git a/backend-board/src/main/java/com/backendboard/domain/post/service/PostService.java b/backend-board/src/main/java/com/backendboard/domain/post/service/PostService.java index ac7d42f..050e1ba 100644 --- a/backend-board/src/main/java/com/backendboard/domain/post/service/PostService.java +++ b/backend-board/src/main/java/com/backendboard/domain/post/service/PostService.java @@ -15,7 +15,7 @@ public interface PostService { PostUpdateResponse updatePost(PostUpdateRequest request, Long postId, Long authUserId); - PostReadResponse getPost(Long postId); + PostReadResponse getPost(Long postId, Long currentAuthUserId); void deletePost(Long postId, Long authUserId); diff --git a/backend-board/src/main/java/com/backendboard/domain/post/service/PostServiceImpl.java b/backend-board/src/main/java/com/backendboard/domain/post/service/PostServiceImpl.java index 84ac2be..0d92b7b 100644 --- a/backend-board/src/main/java/com/backendboard/domain/post/service/PostServiceImpl.java +++ b/backend-board/src/main/java/com/backendboard/domain/post/service/PostServiceImpl.java @@ -19,6 +19,7 @@ import com.backendboard.domain.post.dto.PostUpdateResponse; import com.backendboard.domain.post.entity.Post; import com.backendboard.domain.post.respository.PostRepository; +import com.backendboard.domain.post.respository.ViewCountRedisRepository; import com.backendboard.domain.postimage.entity.PostImage; import com.backendboard.domain.postimage.repository.PostImageRepository; import com.backendboard.domain.user.entity.User; @@ -35,6 +36,7 @@ public class PostServiceImpl implements PostService { private final PostRepository postRepository; private final UserRepository userRepository; private final PostImageRepository postImageRepository; + private final ViewCountRedisRepository viewCountRedisRepository; @Transactional @Override @@ -73,13 +75,23 @@ public PostUpdateResponse updatePost(PostUpdateRequest request, Long postId, Lon } @Override - public PostReadResponse getPost(Long postId) { + public PostReadResponse getPost(Long postId, Long currentAuthUserId) { Post post = postRepository.findById(postId).orElseThrow(() -> new CustomException(CustomError.POST_NOT_FOUND)); User user = userRepository.findById(post.getUserId()) .orElseThrow(() -> new CustomException(CustomError.USER_NOT_FOUND)); + if (user.getAuthUser().getId() != currentAuthUserId) { + incrementViewCount(postId); + } + + Long viewCount = post.getViewCount() + viewCountRedisRepository.getIncrementCount(postId.toString()); List images = postImageRepository.findByPostId(postId); - return PostReadResponse.toDto(post, user.getNickname(), images); + return PostReadResponse.toDto(post, user.getNickname(), images, viewCount); + } + + private void incrementViewCount(Long postId) { + String postIdKey = postId.toString(); + viewCountRedisRepository.incrementCount(postIdKey); } @Transactional diff --git a/backend-board/src/main/java/com/backendboard/domain/postlike/repository/PostLikeRedisRepository.java b/backend-board/src/main/java/com/backendboard/domain/postlike/repository/PostLikeRedisRepository.java index 1505103..9330c43 100644 --- a/backend-board/src/main/java/com/backendboard/domain/postlike/repository/PostLikeRedisRepository.java +++ b/backend-board/src/main/java/com/backendboard/domain/postlike/repository/PostLikeRedisRepository.java @@ -10,4 +10,6 @@ public interface PostLikeRedisRepository { void delete(); Map getEntries(); + + void incrementCount(String postId, Long delta); } diff --git a/backend-board/src/main/java/com/backendboard/domain/postlike/repository/PostLikeRedisRepositoryImpl.java b/backend-board/src/main/java/com/backendboard/domain/postlike/repository/PostLikeRedisRepositoryImpl.java index 0c22309..dfa61e1 100644 --- a/backend-board/src/main/java/com/backendboard/domain/postlike/repository/PostLikeRedisRepositoryImpl.java +++ b/backend-board/src/main/java/com/backendboard/domain/postlike/repository/PostLikeRedisRepositoryImpl.java @@ -7,9 +7,7 @@ import org.springframework.stereotype.Repository; import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -@Slf4j @Repository @RequiredArgsConstructor public class PostLikeRedisRepositoryImpl implements PostLikeRedisRepository { @@ -38,4 +36,9 @@ public void delete() { public Map getEntries() { return redisTemplate.opsForHash().entries(KEY); } + + @Override + public void incrementCount(String postId, Long delta) { + redisTemplate.opsForHash().increment(KEY, postId, delta); + } } diff --git a/backend-board/src/main/java/com/backendboard/domain/postlike/service/PostLikeServiceImpl.java b/backend-board/src/main/java/com/backendboard/domain/postlike/service/PostLikeServiceImpl.java index 0f5d823..d445843 100644 --- a/backend-board/src/main/java/com/backendboard/domain/postlike/service/PostLikeServiceImpl.java +++ b/backend-board/src/main/java/com/backendboard/domain/postlike/service/PostLikeServiceImpl.java @@ -26,11 +26,15 @@ public class PostLikeServiceImpl implements PostLikeService { public PostLikeStatusResponse toggleLike(Long authUserId, Long postId) { User user = userRepository.getByAuthUserId(authUserId); boolean isLiked = postLikeRepository.deleteByUserIdAndPostId(user.getId(), postId) == 0; + String postIdKey = postId.toString(); + long delta = -1L; if (isLiked) { PostLike postLike = PostLike.create(user.getId(), postId); postLikeRepository.save(postLike); + delta = 1L; } + postLikeRedisRepository.incrementCount(postIdKey, delta); return PostLikeStatusResponse.toDto(isLiked); } diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx index 9531a49..d8b0398 100644 --- a/frontend/src/main.jsx +++ b/frontend/src/main.jsx @@ -1,10 +1,9 @@ import {createRoot} from 'react-dom/client' import './index.css' import App from './App.jsx' -import {StrictMode} from "react"; createRoot(document.getElementById('root')).render( - - - , + // + + //, ) diff --git a/frontend/src/pages/post/PostDetail.jsx b/frontend/src/pages/post/PostDetail.jsx index 0e6bc93..24cdbd6 100644 --- a/frontend/src/pages/post/PostDetail.jsx +++ b/frontend/src/pages/post/PostDetail.jsx @@ -35,35 +35,42 @@ const PostDetail = () => { setIsLoggedIn(!!user && !!accessToken); }, []); + // 1. 게시글 상세(조회수 포함) - 한 번만 호출 useEffect(() => { api.get(`/posts/${id}`) .then(res => { if (res.status !== 200) throw new Error('게시글을 불러올 수 없습니다.'); - return res.data; - }) - .then(data => { - setPost(data); - api.get(`/post-likes/${id}/count`).then(res => { - if (res.status !== 200) { - throw new Error('좋아요 상태를 불러올 수 없습니다.'); - } - setLikeCount(res.data.count); - }) - - // 로그인한 경우 좋아요 상태 확인 - if (isLoggedIn) { - api.get(`/post-likes/${id}/status`).then(res => { - if (res.status !== 200) { - throw new Error('좋아요 상태를 불러올 수 없습니다.'); - } - setIsLiked(res.data.liked); - }) - // 좋아요 상태 확인 로직 추가 - } + setPost(res.data); }) .catch(err => setError(err.message)) .finally(() => setLoading(false)); + }, [id]); + + // 2. 좋아요 개수는 id만 의존 (로그인 상관없이 보여주려면) + useEffect(() => { + api.get(`/post-likes/${id}/count`) + .then(res => { + if (res.status !== 200) + throw new Error('좋아요 개수를 불러올 수 없습니다.'); + setLikeCount(res.data.count); + }) + .catch(err => setError(err.message)); + }, [id]); + + // 3. 좋아요 상태는 로그인 상태와 id에 따라 호출 + useEffect(() => { + if (isLoggedIn) { + api.get(`/post-likes/${id}/status`) + .then(res => { + if (res.status !== 200) + throw new Error('좋아요 상태를 불러올 수 없습니다.'); + setIsLiked(res.data.liked); + }) + .catch(err => setError(err.message)); + } else { + setIsLiked(false); // 로그아웃 시 상태 초기화 + } }, [id, isLoggedIn]); // 댓글 불러오기