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
@@ -1,6 +1,7 @@
package com.jiwon.mylog.domain.post.controller;

import com.jiwon.mylog.domain.post.service.PostService;
import com.jiwon.mylog.global.security.auth.annotation.AllUser;
import com.jiwon.mylog.global.security.auth.annotation.LoginUser;
import com.jiwon.mylog.domain.post.dto.request.PostRequest;
import com.jiwon.mylog.domain.post.dto.response.PostDetailResponse;
Expand Down Expand Up @@ -103,8 +104,9 @@ public ResponseEntity<PageResponse> getAllNotices(
}
)
public ResponseEntity<PostDetailResponse> getPost(
@AllUser String userKey,
@PathVariable("postId") Long postId) {
PostDetailResponse response = postService.getPost(postId);
PostDetailResponse response = postService.getPost(postId, userKey);
return new ResponseEntity<>(response, HttpStatus.OK);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@ public static PostDetailResponse fromPost(Post post) {
.build();
}

public void setViews(int views) {
this.views = views;
}

public void setTagsAndComments(List<TagResponse> tags, List<CommentResponse> comments) {
this.tags = tags;
this.comments = comments;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,18 @@
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;

@Repository
public interface PostRepository extends JpaRepository<Post, Long>, PostRepositoryCustom {

@Modifying
@Query("update Post p set p.views = :view where p.id = :postId")
void updatePostView(@Param("postId") Long postId, @Param("view") int view);

@Query(value = "select p from Post p where p.deletedAt is null and p.isNotice = true order by p.createdAt desc",
countQuery = "select count(p) from Post p where p.deletedAt is null and p.isNotice = true")
Page<Post> findAllNotice(Pageable pageable);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
public class PostService {

private final ApplicationEventPublisher eventPublisher;
private final PostViewService postViewService;
private final TagService tagService;
private final PostRepository postRepository;
private final UserRepository userRepository;
Expand Down Expand Up @@ -167,11 +168,18 @@ public PageResponse getAllNotices(Pageable pageable) {
condition = "#postId != null && #postId > 0"
)
@Transactional(readOnly = true)
public PostDetailResponse getPost(Long postId) {
public PostDetailResponse getPostContent(Long postId) {
return postRepository.findPostDetail(postId)
.orElseThrow(() -> new NotFoundException(ErrorCode.NOT_FOUND_POST));
}

public PostDetailResponse getPost(Long postId, String userKey) {
PostDetailResponse post = getPostContent(postId);
postViewService.incrementPostView(post.getPostId(), post.getViews(), userKey);
post.setViews(postViewService.getPostView(post.getPostId(), post.getViews()));
return post;
}

@Cacheable(value = "post::main",
key = "'page:' + #pageable.pageNumber + ':size:' + #pageable.pageSize",
condition = "#pageable != null")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.jiwon.mylog.domain.post.service;

import com.jiwon.mylog.global.redis.RedisUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

@RequiredArgsConstructor
@Service
public class PostViewService {

private final RedisUtil redisUtil;
private static final String VIEW_KEY_PREFIX = "post:view:";
private static final String VIEW_COUNT_KEY_PREFIX = "post:view:count";

public void incrementPostView(Long postId, int view, String userKey) {
String existKey = VIEW_KEY_PREFIX + postId;
String countKey = VIEW_COUNT_KEY_PREFIX;

boolean exist = redisUtil.existPostViewUser(existKey, userKey);
if (!exist) {
redisUtil.addPostViewUser(existKey, userKey);
redisUtil.increasePostView(countKey, postId.toString(), String.valueOf(view));
}
}

public int getPostView(Long postId, int view) {
return redisUtil.getPostView(VIEW_COUNT_KEY_PREFIX, postId.toString(), view);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.jiwon.mylog.global.common.config;

import com.jiwon.mylog.global.security.auth.resolver.AllUserArgumentResolver;
import com.jiwon.mylog.global.security.auth.resolver.LoginUserArgumentResolver;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
Expand All @@ -13,9 +14,11 @@
public class WebConfig implements WebMvcConfigurer {

private final LoginUserArgumentResolver loginUserArgumentResolver;
private final AllUserArgumentResolver allUserArgumentResolver;

@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(loginUserArgumentResolver);
resolvers.add(allUserArgumentResolver);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ public void verifyEmailCode(String email, String code) {

@Transactional
public void sendCodeMail(String email) {
if (redisUtil.existData(email)) {
if (redisUtil.existEmailData(email)) {
redisUtil.deleteData(email);
}

Expand Down
38 changes: 37 additions & 1 deletion src/main/java/com/jiwon/mylog/global/redis/RedisUtil.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package com.jiwon.mylog.global.redis;

import java.time.Duration;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
Expand All @@ -15,7 +19,7 @@ public String getData(String key) {
return redisTemplate.opsForValue().get(key);
}

public boolean existData(String key) {
public boolean existEmailData(String key) {
return Boolean.TRUE.equals(redisTemplate.hasKey(key));
}

Expand All @@ -26,4 +30,36 @@ public void setDataExpire(String key, String value, long duration) {
public void deleteData(String key) {
redisTemplate.delete(key);
}

/**
* 조회수 관련
*/
public boolean existPostViewUser(String key, String value) {
return redisTemplate.opsForSet().isMember(key, value);
}

public void addPostViewUser(String key, String value) {
redisTemplate.opsForSet().add(key, value);
redisTemplate.expire(key, Duration.ofHours(12L));
}

public Long increasePostView(String key, String hashKey, String view) {
if (!redisTemplate.opsForHash().hasKey(key, hashKey)) {
redisTemplate.opsForHash().put(key, hashKey, view);
}
return redisTemplate.opsForHash().increment(key, hashKey, 1);
}

public int getPostView(String key, String hashKey, int view) {
String value = (String) redisTemplate.opsForHash().get(key, hashKey);
return value != null ? Integer.parseInt(value) : view;
}

public Map<Long, Integer> getAllPostView(String key) {
return redisTemplate.opsForHash().entries(key).entrySet().stream()
.collect(Collectors.toMap(
e -> Long.parseLong(e.getKey().toString()),
e -> Integer.parseInt(e.getValue().toString())
));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.jiwon.mylog.global.schedular;

import com.jiwon.mylog.domain.post.repository.PostRepository;
import com.jiwon.mylog.global.redis.RedisUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import java.util.Map;

@Slf4j
@RequiredArgsConstructor
@Component
public class PostViewScheduler {

private final PostRepository postRepository;
private final RedisUtil redisUtil;
private static final String REDIS_KEY = "post:view:count";

@Scheduled(fixedRate = 10 * 60 * 1000L)
@Transactional
public void syncPostViewToDB() {
Map<Long, Integer> postCounts = redisUtil.getAllPostView(REDIS_KEY);
postCounts.forEach((postId, view) -> {
try {
postRepository.updatePostView(postId, view);
} catch (Exception e) {
log.error("조회수 동기화 실패, 게시글 {}: {}", postId, e.getMessage());
}
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.jiwon.mylog.global.security.auth.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface AllUser {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package com.jiwon.mylog.global.security.auth.resolver;

import com.jiwon.mylog.global.security.auth.annotation.AllUser;
import com.jiwon.mylog.global.security.jwt.JwtService;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;

import java.util.List;

@RequiredArgsConstructor
@Component
public class AllUserArgumentResolver implements HandlerMethodArgumentResolver {

private final JwtService jwtService;
private final List<String> headers = List.of(
"X-Forwarded-For", "Proxy-Client-IP", "WL-Proxy-Client-IP",
"HTTP_CLIENT_IP", "HTTP_X_FORWARDED_FOR");

@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.getParameterAnnotation(AllUser.class) != null &&
parameter.getParameterType().equals(String.class);
}

@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {

HttpServletRequest httpServletRequest = webRequest.getNativeRequest(HttpServletRequest.class);
String header = httpServletRequest.getHeader(HttpHeaders.AUTHORIZATION);
String token = jwtService.getAccessToken(header);

if (token == null || !jwtService.validateToken(token)) {
return getClientIP(httpServletRequest);
}

return jwtService.getUserId(token).toString();
}

private String getClientIP(HttpServletRequest request) {
String clientIP = null;

for(String header: headers) {
clientIP = request.getHeader(header);
if (!(clientIP == null || clientIP.isEmpty() || "unknown".equalsIgnoreCase(clientIP))) {
break;
}
}

if (clientIP == null) {
clientIP = request.getRemoteAddr();
}
if (clientIP != null || clientIP.contains(",")) {
clientIP = clientIP.split(",")[0].trim();
}

return clientIP;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ void createPost() {
PostDetailResponse post = postService.createPost(userId, postRequest, false);
Long postId = post.getPostId();

postService.getPost(postId);
postService.getPost(postId, "dummy");
postService.getUserPosts(userId, pageable); // 캐시 다시 생성
postService.getPostsByCategoryAndTags(userId, categoryId, List.of(), pageable); // 캐시 다시 생성

Expand Down Expand Up @@ -88,7 +88,7 @@ void updatePost() {

// when
PostDetailResponse post = postService.updatePost(userId, postId, postRequest, false);
postService.getPost(postId);
postService.getPost(postId, "dummy");
postService.getUserPosts(userId, pageable); // 캐시 다시 생성
postService.getPostsByCategoryAndTags(userId, categoryId, List.of(), pageable); // 캐시 다시 생성

Expand All @@ -113,11 +113,11 @@ void deletePost() {
.willReturn(Optional.empty());

// when
postService.getPost(postId);
postService.getPost(postId, "dummy");
postService.deletePost(userId, postId);

// then
assertThrows(NotFoundException.class, () -> postService.getPost(postId));
assertThrows(NotFoundException.class, () -> postService.getPost(postId, "dummy"));
verify(postRepository, times(2)).findPostDetail(postId);
}

Expand All @@ -131,10 +131,10 @@ void getPost() {
.willReturn(Optional.of(post));

// when & then
PostDetailResponse firstPost = postService.getPost(postId);
PostDetailResponse firstPost = postService.getPost(postId, "dummy");
verify(postRepository, times(1)).findPostDetail(postId);

PostDetailResponse secondPost = postService.getPost(postId);
PostDetailResponse secondPost = postService.getPost(postId, "dummy");
verify(postRepository, times(1)).findPostDetail(postId);

assertThat(firstPost).isEqualTo(secondPost);
Expand Down