Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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 @@ -8,6 +8,8 @@
import dev.woori.woorilog.global.response.ApiResponseUtil;
import dev.woori.woorilog.global.response.BaseResponse;
import dev.woori.woorilog.global.response.SuccessCode;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
Expand Down Expand Up @@ -46,9 +48,11 @@ public ResponseEntity<BaseResponse<?>> createBlog(
@GetMapping("/blog/{postId}")
public ResponseEntity<BaseResponse<?>> getBlogInfo(
@UserId Optional<Long> userId,
@PathVariable("postId") Long postId
@PathVariable("postId") Long postId,
HttpServletRequest request,
HttpServletResponse response
) {
BlogDetailInfoRes res = blogService.getBlogInfo(userId, postId);
BlogDetailInfoRes res = blogService.getBlogInfo(userId, postId, request, response);
return ApiResponseUtil.success(SuccessCode.OK, res);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ public record BlogBasicInfoDto(
Category category,
List<String> tags,
List<ProgressDto> progresses,
Long viewCount,
LocalDateTime createdAt,
LocalDateTime updatedAt
) {
Expand All @@ -36,6 +37,7 @@ public static BlogBasicInfoDto create(Blog blog) {
.category(blog.getCategory())
.tags(blog.getTags())
.progresses(toProgressDtoList(blog.getProgresses()))
.viewCount(blog.getViewCount())
.createdAt(blog.getCreatedAt())
.updatedAt(blog.getUpdatedAt())
.build();
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/dev/woori/woorilog/domain/blog/dto/BlogDto.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ public record BlogDto(
Category category,
String document,
List<ProgressDto> progresses,
Long viewCount,
LocalDateTime createdAt,
LocalDateTime updatedAt
) {
Expand All @@ -28,6 +29,7 @@ public static BlogDto create(
.category(blog.getCategory())
.document(blog.getDocument())
.progresses(progressToDto(blog.getProgresses()))
.viewCount(blog.getViewCount())
.createdAt(blog.getCreatedAt())
.updatedAt(blog.getUpdatedAt())
.build();
Expand Down
4 changes: 4 additions & 0 deletions src/main/java/dev/woori/woorilog/domain/blog/entity/Blog.java
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ public class Blog extends BaseEntity {
@CollectionTable(name = "tags", joinColumns = @JoinColumn(name = "blog_id"))
private List<String> tags;

@Column(nullable = false)
@Builder.Default
private Long viewCount = 0L;

@OneToMany(
mappedBy = "blog",
fetch = FetchType.LAZY,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
import dev.woori.woorilog.domain.blog.entity.Blog;
import dev.woori.woorilog.domain.project.entity.Project;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
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;
Expand All @@ -29,19 +29,31 @@ public interface BlogRepository extends JpaRepository<Blog, Long> {
@Query("SELECT b FROM Blog b JOIN FETCH b.member JOIN FETCH b.project LEFT JOIN FETCH b.progresses WHERE b.id = :id")
Optional<Blog> findBlogByIdWithDetails(@Param("id") Long id);

@Query("SELECT DISTINCT b FROM Blog b " +
@Query("SELECT b FROM Blog b LEFT JOIN FETCH b.tags WHERE b.id = :id")
void findBlogByIdWithTags(@Param("id") Long id);

@Query("SELECT b.id FROM Blog b " +
"WHERE b.category != 'CHECKPOINT' "
)
Page<Long> findBlogIds(Pageable pageable);

@Query("SELECT DISTINCT b " +
"FROM Blog b " +
"LEFT JOIN FETCH b.progresses " +
"JOIN FETCH b.member " +
"JOIN FETCH b.project " +
"WHERE b.category != 'CHECKPOINT' " +
"ORDER BY b.createdAt DESC "
)
List<Blog> findTopOrderByCreatedAtDesc(Pageable pageable);
"WHERE b.id IN :ids " +
"ORDER BY b.createdAt DESC")
List<Blog> findBlogsWithDetailsByIds(@Param("ids") List<Long> ids);

@Query("SELECT b.project.id as projectId, COUNT(b.id) as blogCount " +
"FROM Blog b " +
"WHERE b.project IN :projects AND b.category != dev.woori.woorilog.domain.blog.enums.Category.CHECKPOINT " +
"GROUP BY b.project.id"
)
List<ProjectBlogCount> findBlogCountsByProjects(@Param("projects") List<Project> projects);

@Modifying(clearAutomatically = true)
@Query("UPDATE Blog b SET b.viewCount = b.viewCount + 1 WHERE b.id = :id")
void increaseViewCount(@Param("id") Long id);
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,13 @@
import dev.woori.woorilog.domain.project.entity.ProjectMember;
import dev.woori.woorilog.domain.project.repository.ProjectMemberRepository;
import dev.woori.woorilog.global.cache.CacheNames;
import dev.woori.woorilog.global.util.CookieUtils;
import jakarta.persistence.EntityNotFoundException;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.domain.Page;
Expand All @@ -26,11 +31,13 @@
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Arrays;
import java.util.List;
import java.util.Optional;

import static dev.woori.woorilog.global.response.error.ErrorMessage.*;

@Slf4j
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
Expand Down Expand Up @@ -73,14 +80,18 @@ public Long createBlog(Long projectId, Long userId, BlogCreateOrUpdateReq reques
* 글을 열람하기 위한 글과 작성자, 작성 프로젝트 응답을 생성합니다.
* 하위 DTO를 생성하고 조립한 응답을 생성해 리턴합니다.
*
* @param postId 열람할 글 id
* @param blogId 열람할 글 id
* @return BlogDetailInfoRes: 열람할 글, 작성자, 작성 프로젝트의 정보
*/
@Transactional
public BlogDetailInfoRes getBlogInfo(Optional<Long> memberId, Long postId) {
Blog blog = blogRepository.findBlogByIdWithDetails(postId)
public BlogDetailInfoRes getBlogInfo(Optional<Long> memberId, Long blogId, HttpServletRequest request, HttpServletResponse response) {
increaseViewCount(blogId, request, response);

Blog blog = blogRepository.findBlogByIdWithDetails(blogId)
.orElseThrow(() -> new EntityNotFoundException(BLOG_NOT_FOUND));

blogRepository.findBlogByIdWithTags(blogId);

Project project = blog.getProject();
Member author = blog.getMember();
boolean isAuthor = checkAuthor(memberId, author.getId());
Expand All @@ -100,15 +111,19 @@ public BlogDetailInfoRes getBlogInfo(Optional<Long> memberId, Long postId) {
*/
@Cacheable(value = CacheNames.HOME_BLOGS)
public BlogHomeRes getBlogBasicInfos(int page) {
Page<Blog> blogPage = blogRepository.findAll(
PageRequest.of(page - 1, BLOG_PAGE_SIZE, Sort.by(SORT_CRITERIA).descending())
PageRequest pageRequest = PageRequest.of(
page - 1, BLOG_PAGE_SIZE, Sort.by(SORT_CRITERIA).descending()
);

List<BlogBasicInfoDto> blogBasicInfoDtoList = blogPage.stream()
Page<Long> blogIds = blogRepository.findBlogIds(pageRequest);
List<Blog> blogsWithDetailsByIds = blogRepository.findBlogsWithDetailsByIds(blogIds.getContent());


List<BlogBasicInfoDto> blogBasicInfoDtoList = blogsWithDetailsByIds.stream()
.map(BlogBasicInfoDto::create)
.toList();

return BlogHomeRes.of(blogPage.getNumber() + 1, blogPage.getTotalPages(), blogBasicInfoDtoList);
return BlogHomeRes.of(blogIds.getNumber() + 1, blogIds.getTotalPages(), blogBasicInfoDtoList);
}

/**
Expand Down Expand Up @@ -141,6 +156,29 @@ public void deleteBlog(Long userId, Long blogId) {
blogRepository.delete(blog);
}

private void increaseViewCount(Long blogId, HttpServletRequest request, HttpServletResponse response) {
Optional<Cookie> optionalCookie = CookieUtils.getCookie(request, CookieUtils.VIEW_COOKIE_NAME);
if (optionalCookie.isEmpty()) {
// 새로운 쿠키 추가
blogRepository.increaseViewCount(blogId);
Cookie viewCookie = CookieUtils.createViewCookie(CookieUtils.VIEW_COOKIE_NAME, String.valueOf(blogId));
log.info("[ADD COOKIE] : {} {}", request.getLocalName(), viewCookie.getValue());
response.addCookie(viewCookie);
} else {
// 쿠키 업데이트
Cookie originalCookie = optionalCookie.get();
String originalValue = CookieUtils.getDecodedCookieValue(originalCookie);
String additionalValue = CookieUtils.getCookieValue(String.valueOf(blogId));
// 조회하지 않은 게시물의 경우 조회수 + 1
if (CookieUtils.isContainedValue(originalValue, additionalValue)) {
blogRepository.increaseViewCount(blogId);
Cookie updateCookie = CookieUtils.updateCookie(originalCookie, additionalValue);
log.info("[UPDATE COOKIE] : {} {}", request.getLocalName(), updateCookie.getValue());
response.addCookie(updateCookie);
}
}
}

/**
* 블로그 글 id를 통해 블로그 글을 가져오고 글의 작성자인지 확인하는 메서드
* @param userId 사용자 id
Expand Down
64 changes: 64 additions & 0 deletions src/main/java/dev/woori/woorilog/global/util/CookieUtils.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package dev.woori.woorilog.global.util;

import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;

import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Optional;

public class CookieUtils {

private static final int COOKIE_MAX_AGE = 24 * 60 * 60; // 24시간
private static final int MAX_COOKIE_SIZE = 3000;
private static final String NAME_PREFIX = "[";
private static final String NAME_POSTFIX = "]";

public static final String VIEW_COOKIE_NAME = "postView";

public static Cookie createViewCookie(String name, String value) {
String cookieValue = getCookieValue(value);
Cookie cookie = new Cookie(name, URLEncoder.encode(cookieValue, StandardCharsets.UTF_8));
cookie.setMaxAge(COOKIE_MAX_AGE);
cookie.setPath("/");
cookie.setHttpOnly(true);
return cookie;
}

public static String getCookieValue(String value) {
return NAME_PREFIX + value + NAME_POSTFIX;
}

public static String getDecodedCookieValue(Cookie cookie) {
return URLDecoder.decode(cookie.getValue(), StandardCharsets.UTF_8);
}

public static Cookie updateCookie(Cookie oldCookie, String newValue) {
String decodedValue = getDecodedCookieValue(oldCookie);
String tempValue = decodedValue + "_" + newValue;
// 쿠키 오버플로우 방지
String updateValue = tempValue.length() <= MAX_COOKIE_SIZE ?
URLEncoder.encode(tempValue, StandardCharsets.UTF_8) : URLEncoder.encode(newValue, StandardCharsets.UTF_8);

oldCookie.setValue(updateValue);
oldCookie.setMaxAge(COOKIE_MAX_AGE);
return oldCookie;
}

public static boolean isContainedValue(String originalValue, String newValue) {
return Arrays.stream(originalValue.split("_")).noneMatch(newValue::equals);
}

// 쿠키 조회
public static Optional<Cookie> getCookie(HttpServletRequest request, String cookieName) {
Cookie[] cookies = request.getCookies();
if (cookies == null) {
return Optional.empty();
}
return Arrays.stream(cookies)
.filter(cookie -> cookieName.equals(cookie.getName()))
.findFirst();
}
}