diff --git a/src/main/java/com/dailyon/snsservice/controller/rest/PostAdminController.java b/src/main/java/com/dailyon/snsservice/controller/rest/PostAdminController.java new file mode 100644 index 0000000..511ce7d --- /dev/null +++ b/src/main/java/com/dailyon/snsservice/controller/rest/PostAdminController.java @@ -0,0 +1,39 @@ +package com.dailyon.snsservice.controller.rest; + +import com.dailyon.snsservice.dto.response.post.PostAdminPageResponse; +import com.dailyon.snsservice.service.post.PostAdminService; +import com.dailyon.snsservice.service.post.PostService; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.web.PageableDefault; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RequestMapping("/admin") +@RestController +@RequiredArgsConstructor +public class PostAdminController { + + private final PostAdminService postAdminService; + + @GetMapping("/posts") + public ResponseEntity getPosts( + @PageableDefault( + page = 0, + size = 5, + sort = {"id"}, + direction = Sort.Direction.ASC) + Pageable pageable) { + PostAdminPageResponse postAdminPageResponse = postAdminService.getPostsForAdmin(pageable); + return ResponseEntity.ok(postAdminPageResponse); + } + + @DeleteMapping("/posts") + public ResponseEntity bulkDeletePosts(@RequestParam(name = "postIds") List postIds) { + postAdminService.softBulkDeleteByIds(postIds); + return ResponseEntity.ok().build(); + } +} diff --git a/src/main/java/com/dailyon/snsservice/dto/response/post/PostAdminPageResponse.java b/src/main/java/com/dailyon/snsservice/dto/response/post/PostAdminPageResponse.java new file mode 100644 index 0000000..c0b377e --- /dev/null +++ b/src/main/java/com/dailyon/snsservice/dto/response/post/PostAdminPageResponse.java @@ -0,0 +1,42 @@ +package com.dailyon.snsservice.dto.response.post; + +import com.dailyon.snsservice.entity.HashTag; +import com.dailyon.snsservice.entity.Post; +import java.util.List; +import java.util.stream.Collectors; +import lombok.*; +import org.springframework.data.domain.Page; + +@Getter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PROTECTED) +public class PostAdminPageResponse { + + private int totalPages; + private long totalElements; + private List posts; + + public static PostAdminPageResponse fromEntity(Page posts) { + return PostAdminPageResponse.builder() + .totalPages(posts.getTotalPages()) + .totalElements(posts.getTotalElements()) + .posts( + posts.getContent().stream() + .map( + post -> + PostAdminResponse.builder() + .id(post.getId()) + .thumbnailImgUrl(post.getPostImage().getThumbnailImgUrl()) + .hashTagNames( + post.getHashTags().stream() + .map(HashTag::getName) + .collect(Collectors.toList())) + .title(post.getTitle()) + .description(post.getDescription()) + .memberNickname(post.getMember().getNickname()) + .build()) + .collect(Collectors.toList())) + .build(); + } +} diff --git a/src/main/java/com/dailyon/snsservice/dto/response/post/PostAdminResponse.java b/src/main/java/com/dailyon/snsservice/dto/response/post/PostAdminResponse.java new file mode 100644 index 0000000..78af9b9 --- /dev/null +++ b/src/main/java/com/dailyon/snsservice/dto/response/post/PostAdminResponse.java @@ -0,0 +1,18 @@ +package com.dailyon.snsservice.dto.response.post; + +import java.util.List; +import lombok.*; + +@Getter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PROTECTED) +public class PostAdminResponse { + + private Long id; + private String thumbnailImgUrl; + private List hashTagNames; + private String title; + private String description; + private String memberNickname; +} diff --git a/src/main/java/com/dailyon/snsservice/exception/common/DomainException.java b/src/main/java/com/dailyon/snsservice/exception/common/DomainException.java index a981f30..7d68b77 100644 --- a/src/main/java/com/dailyon/snsservice/exception/common/DomainException.java +++ b/src/main/java/com/dailyon/snsservice/exception/common/DomainException.java @@ -3,6 +3,7 @@ import java.util.HashMap; import java.util.Map; import lombok.Getter; +import org.springframework.http.HttpStatus; @Getter public abstract class DomainException extends RuntimeException { @@ -12,7 +13,7 @@ public DomainException(String message) { super(message); } - public abstract int getStatusCode(); + public abstract HttpStatus getStatusCode(); public void addValidation(String fieldName, String errorMessage) { validation.put(fieldName, errorMessage); diff --git a/src/main/java/com/dailyon/snsservice/exception/common/EntityNotFoundException.java b/src/main/java/com/dailyon/snsservice/exception/common/EntityNotFoundException.java index ad91be3..bc83d9f 100644 --- a/src/main/java/com/dailyon/snsservice/exception/common/EntityNotFoundException.java +++ b/src/main/java/com/dailyon/snsservice/exception/common/EntityNotFoundException.java @@ -1,5 +1,7 @@ package com.dailyon.snsservice.exception.common; +import org.springframework.http.HttpStatus; + import javax.servlet.http.HttpServletResponse; public class EntityNotFoundException extends DomainException { @@ -9,7 +11,7 @@ public EntityNotFoundException(String message) { } @Override - public int getStatusCode() { - return HttpServletResponse.SC_NOT_FOUND; + public HttpStatus getStatusCode() { + return HttpStatus.NOT_FOUND; } } diff --git a/src/main/java/com/dailyon/snsservice/exceptionhandler/advice/ApiControllerAdvice.java b/src/main/java/com/dailyon/snsservice/exceptionhandler/advice/ApiControllerAdvice.java index a4e5cac..dc2e493 100644 --- a/src/main/java/com/dailyon/snsservice/exceptionhandler/advice/ApiControllerAdvice.java +++ b/src/main/java/com/dailyon/snsservice/exceptionhandler/advice/ApiControllerAdvice.java @@ -2,6 +2,8 @@ import com.dailyon.snsservice.exception.HashTagDuplicatedException; import com.dailyon.snsservice.exception.common.CustomException; +import com.dailyon.snsservice.exception.common.DomainException; +import com.dailyon.snsservice.exception.common.EntityNotFoundException; import com.dailyon.snsservice.exceptionhandler.response.ErrorResponse; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; @@ -41,6 +43,20 @@ public ResponseEntity customException(CustomException e) { return ResponseEntity.status(statusCode).body(errorResponse); } + @ExceptionHandler(DomainException.class) + public ResponseEntity domainException(DomainException e) { + HttpStatus statusCode = e.getStatusCode(); + + ErrorResponse errorResponse = + ErrorResponse.builder() + .code(statusCode) + .message(e.getMessage()) + .validation(e.getValidation()) + .build(); + + return ResponseEntity.status(statusCode).body(errorResponse); + } + @ExceptionHandler(MissingRequestHeaderException.class) public ResponseEntity missingRequestHeaderException( MissingRequestHeaderException e) { diff --git a/src/main/java/com/dailyon/snsservice/repository/post/PostJpaRepository.java b/src/main/java/com/dailyon/snsservice/repository/post/PostJpaRepository.java index e382390..637d734 100644 --- a/src/main/java/com/dailyon/snsservice/repository/post/PostJpaRepository.java +++ b/src/main/java/com/dailyon/snsservice/repository/post/PostJpaRepository.java @@ -56,4 +56,20 @@ int updateCountsById( @Query("select p from Post p where p.id = :id and p.member.id = :memberId") Optional findByIdAndMemberId(Long id, Long memberId); + + @Query( + value = + "select p from Post p " + + "join fetch p.postImage " + + "join fetch p.hashTags " + + "join fetch p.member " + + "where p.isDeleted = false", + countQuery = "select count(p) from Post p where p.isDeleted = false") + Page findAllByIdAscAndIsDeletedFalse(Pageable pageable); + + @Modifying(clearAutomatically = true, flushAutomatically = true) + @Query("update Post p " + + "set p.isDeleted = true " + + "where p.id in :ids") + int softBulkDeleteByIds(List ids); } diff --git a/src/main/java/com/dailyon/snsservice/repository/post/PostRepository.java b/src/main/java/com/dailyon/snsservice/repository/post/PostRepository.java index a98e8a9..7e82af9 100644 --- a/src/main/java/com/dailyon/snsservice/repository/post/PostRepository.java +++ b/src/main/java/com/dailyon/snsservice/repository/post/PostRepository.java @@ -31,4 +31,8 @@ public interface PostRepository { int updateCountsById(Long id, Integer viewCount, Integer likeCount, Integer commentCount); PostDetailResponse findDetailByIdWithIsFollowingAndIsLike(Long id, Long memberId); + + Page findAllByIdAscAndIsDeletedFalse(Pageable pageable); + + int softBulkDeleteByIds(List ids); } diff --git a/src/main/java/com/dailyon/snsservice/repository/post/PostRepositoryImpl.java b/src/main/java/com/dailyon/snsservice/repository/post/PostRepositoryImpl.java index 7cf7707..9effb7e 100644 --- a/src/main/java/com/dailyon/snsservice/repository/post/PostRepositoryImpl.java +++ b/src/main/java/com/dailyon/snsservice/repository/post/PostRepositoryImpl.java @@ -228,67 +228,79 @@ public PostDetailResponse findDetailByIdWithIsFollowingAndIsLike(Long id, Long m followSubQuery.follower.id.eq(memberId)) .exists(); - return query - .transform( - groupBy(post.id) - .as( - new QPostDetailResponse( - post.id, - post.title, - post.description, - post.stature, - post.weight, - postImage.imgUrl, - post.viewCount, - post.likeCount, - post.commentCount, - hasLikedCondition, - post.createdAt, - new QPostDetailMemberResponse( - member.id, - member.nickname, - member.profileImgUrl, - member.code, - isFollowingExpression), - set(new QPostDetailHashTagResponse(hashTag.id, hashTag.name)), - set( - new QPostImageProductDetailResponse( - postImageProductDetail.id, - postImageProductDetail.productId, - postImageProductDetail.productSize, - postImageProductDetail.leftGapPercent, - postImageProductDetail.topGapPercent))))) - .get(id); + PostDetailResponse postDetailResponse = query + .transform( + groupBy(post.id) + .as( + new QPostDetailResponse( + post.id, + post.title, + post.description, + post.stature, + post.weight, + postImage.imgUrl, + post.viewCount, + post.likeCount, + post.commentCount, + hasLikedCondition, + post.createdAt, + new QPostDetailMemberResponse( + member.id, + member.nickname, + member.profileImgUrl, + member.code, + isFollowingExpression), + set(new QPostDetailHashTagResponse(hashTag.id, hashTag.name)), + set( + new QPostImageProductDetailResponse( + postImageProductDetail.id, + postImageProductDetail.productId, + postImageProductDetail.productSize, + postImageProductDetail.leftGapPercent, + postImageProductDetail.topGapPercent))))) + .get(id); + return postDetailResponse; } else { - return query - .transform( - groupBy(post.id) - .as( - new QPostDetailResponse( - post.id, - post.title, - post.description, - post.stature, - post.weight, - postImage.imgUrl, - post.viewCount, - post.likeCount, - post.commentCount, - post.createdAt, - new QPostDetailMemberResponse( - member.id, member.nickname, member.profileImgUrl, member.code), - set(new QPostDetailHashTagResponse(hashTag.id, hashTag.name)), - set( - new QPostImageProductDetailResponse( - postImageProductDetail.id, - postImageProductDetail.productId, - postImageProductDetail.productSize, - postImageProductDetail.leftGapPercent, - postImageProductDetail.topGapPercent))))) - .get(id); + PostDetailResponse postDetailResponse = query + .transform( + groupBy(post.id) + .as( + new QPostDetailResponse( + post.id, + post.title, + post.description, + post.stature, + post.weight, + postImage.imgUrl, + post.viewCount, + post.likeCount, + post.commentCount, + post.createdAt, + new QPostDetailMemberResponse( + member.id, member.nickname, member.profileImgUrl, member.code), + set(new QPostDetailHashTagResponse(hashTag.id, hashTag.name)), + set( + new QPostImageProductDetailResponse( + postImageProductDetail.id, + postImageProductDetail.productId, + postImageProductDetail.productSize, + postImageProductDetail.leftGapPercent, + postImageProductDetail.topGapPercent))))) + .get(id); + return postDetailResponse; } } + @Override + public Page findAllByIdAscAndIsDeletedFalse(Pageable pageable) { + return postJpaRepository.findAllByIdAscAndIsDeletedFalse(pageable); + } + + @Override + public int softBulkDeleteByIds(List ids) { + return postJpaRepository.softBulkDeleteByIds(ids); + } + private Long getMyPageTotalPageCount(Pageable pageable, Long memberId) { return jpaQueryFactory .select(post.count()) diff --git a/src/main/java/com/dailyon/snsservice/service/post/PostAdminService.java b/src/main/java/com/dailyon/snsservice/service/post/PostAdminService.java new file mode 100644 index 0000000..6859758 --- /dev/null +++ b/src/main/java/com/dailyon/snsservice/service/post/PostAdminService.java @@ -0,0 +1,31 @@ +package com.dailyon.snsservice.service.post; + +import com.dailyon.snsservice.dto.response.post.PostAdminPageResponse; +import com.dailyon.snsservice.entity.Post; +import com.dailyon.snsservice.repository.post.PostRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Slf4j +@Service +@Transactional(readOnly = true) +@RequiredArgsConstructor +public class PostAdminService { + + private final PostRepository postRepository; + + public PostAdminPageResponse getPostsForAdmin(Pageable pageable) { + Page posts = postRepository.findAllByIdAscAndIsDeletedFalse(pageable); + return PostAdminPageResponse.fromEntity(posts); + } + + public void softBulkDeleteByIds(List ids) { + postRepository.softBulkDeleteByIds(ids); + } +} diff --git a/src/main/java/com/dailyon/snsservice/service/post/PostService.java b/src/main/java/com/dailyon/snsservice/service/post/PostService.java index 8f4fe04..b7b7e7f 100644 --- a/src/main/java/com/dailyon/snsservice/service/post/PostService.java +++ b/src/main/java/com/dailyon/snsservice/service/post/PostService.java @@ -12,6 +12,7 @@ import com.dailyon.snsservice.dto.response.postimageproductdetail.PostImageProductDetailResponse; import com.dailyon.snsservice.dto.response.postlike.PostLikePageResponse; import com.dailyon.snsservice.entity.*; +import com.dailyon.snsservice.exception.PostEntityNotFoundException; import com.dailyon.snsservice.mapper.hashtag.HashTagMapper; import com.dailyon.snsservice.mapper.post.PostMapper; import com.dailyon.snsservice.mapper.postimage.PostImageMapper; @@ -199,32 +200,33 @@ public OOTDPostPageResponse getMyOOTDPosts(Long memberId, Pageable pageable) { return OOTDPostPageResponse.fromDto(myOOTDPostResponses); } - public OOTDPostPageResponse getMemberOOTDPosts(Long postMemberId, Long memberId, Pageable pageable) { + public OOTDPostPageResponse getMemberOOTDPosts( + Long postMemberId, Long memberId, Pageable pageable) { Page myOOTDPostResponses = - postRepository.findMemberPostsByMemberId(postMemberId, memberId, pageable); + postRepository.findMemberPostsByMemberId(postMemberId, memberId, pageable); myOOTDPostResponses - .getContent() - .forEach( - OOTDPostResponse -> { - try { - PostCountVO dbPostCountVO = - new PostCountVO( - OOTDPostResponse.getViewCount(), - OOTDPostResponse.getLikeCount(), - OOTDPostResponse.getCommentCount()); - - // get count from cache or add all counts to cache - PostCountVO cachedPostCountVO = - postCountRedisRepository.findOrPutPostCountVO( - String.valueOf(OOTDPostResponse.getId()), dbPostCountVO); - - // cache count 값으로 response를 업데이트 - OOTDPostResponse.setViewCount(cachedPostCountVO.getViewCount()); - OOTDPostResponse.setLikeCount(cachedPostCountVO.getLikeCount()); - } catch (JsonProcessingException e) { - throw new RuntimeException(e); - } - }); + .getContent() + .forEach( + OOTDPostResponse -> { + try { + PostCountVO dbPostCountVO = + new PostCountVO( + OOTDPostResponse.getViewCount(), + OOTDPostResponse.getLikeCount(), + OOTDPostResponse.getCommentCount()); + + // get count from cache or add all counts to cache + PostCountVO cachedPostCountVO = + postCountRedisRepository.findOrPutPostCountVO( + String.valueOf(OOTDPostResponse.getId()), dbPostCountVO); + + // cache count 값으로 response를 업데이트 + OOTDPostResponse.setViewCount(cachedPostCountVO.getViewCount()); + OOTDPostResponse.setLikeCount(cachedPostCountVO.getLikeCount()); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + }); return OOTDPostPageResponse.fromDto(myOOTDPostResponses); } @@ -256,20 +258,23 @@ public void addViewCount(Long id) { public PostDetailResponse findDetailByIdWithIsFollowing(Long id, Long memberId) { PostDetailResponse postDetailResponse = postRepository.findDetailByIdWithIsFollowingAndIsLike(id, memberId); + if (postDetailResponse == null) { + throw new PostEntityNotFoundException(); + } List productIds = postDetailResponse.getPostImageProductDetails().stream() .map(PostImageProductDetailResponse::getProductId) .collect(Collectors.toList()); List productInfos; - if(productIds.size() == 1 && Objects.isNull(productIds.get(0))) { + if (productIds.size() == 1 && Objects.isNull(productIds.get(0))) { productInfos = new ArrayList<>(); postDetailResponse.getPostImageProductDetails().clear(); } else { // feign client call productInfos = - Objects.requireNonNull(productServiceClient.getProductInfos(productIds).getBody()) - .getProductInfos(); + Objects.requireNonNull(productServiceClient.getProductInfos(productIds).getBody()) + .getProductInfos(); } List couponsForProduct;