Skip to content

Commit

Permalink
Merge pull request #48 from IT-Cotato/feat/47-post
Browse files Browse the repository at this point in the history
[FEATURE] 게시글 스크랩 추가/삭제 & 마이페이지 스크랩 게시글 조회
  • Loading branch information
u-genuine authored Jan 26, 2025
2 parents 406b727 + 2dfb429 commit d9dd678
Show file tree
Hide file tree
Showing 12 changed files with 261 additions and 20 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ public ResponseEntity<DataResponse<PostDetailResponse>> findPostDetail(
}

@DeleteMapping("/{postId}")
@Operation(summary = "게시글 삭제", description = "postId를 통해 게시글 삭제")
@Operation(summary = "게시글 삭제", description = "(현재 유저가 작성한 게시글일 경우) 게시글을 삭제합니다.")
public ResponseEntity<DataResponse<PostDeleteResponse>> deletePost(
@PathVariable Long postId
) {
Expand Down Expand Up @@ -122,13 +122,45 @@ public ResponseEntity<DataResponse<MyPostResponse>> findMyPosts(
)
);
}

@PostMapping("/{postId}/likes")
@PostMapping("/{postId}/likes")
@Operation(summary = "게시글 좋아요", description = "게시글 좋아요")
public ResponseEntity<DataResponse<Void>> likePost(
@PathVariable Long postId
) {
postService.likePost(postId);
return ResponseEntity.ok(DataResponse.ok());
}

@PostMapping("/{postId}/scrap")
@Operation(summary = "게시글 스크랩", description = "게시글을 스크랩합니다.")
public ResponseEntity<DataResponse<Void>> scrapPost(
@PathVariable Long postId
){
postService.scrapPost(postId);
return ResponseEntity.ok(DataResponse.ok());
}

@DeleteMapping("/{postId}/scrap")
@Operation(summary = "게시글 스크랩 취소", description = "게시글 스크랩을 해제합니다.")
public ResponseEntity<DataResponse<Void>> unscrapPost(
@PathVariable Long postId
){
postService.unscrapPost(postId);
return ResponseEntity.ok(DataResponse.ok());
}

@GetMapping("/my/scrap")
@Operation(summary = "[마이페이지] 스크랩한 게시글 조회", description = "현재 사용자가 스크랩한 게시글을 최신순으로 조회합니다.")
public ResponseEntity<DataResponse<MyPostResponse>> findMyScrapedPosts(
@RequestParam(required = false, defaultValue = "0") int page
){
return ResponseEntity.ok(DataResponse.from(
MyPostResponse.from(
postService.findUserScrapedPosts(page)
)
)
);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -45,5 +45,6 @@ public Boolean validatePostAuthor(Long postId, Long userId) {
PostDto post = postFinder.findPost(postId);

return post.userId().equals(userId);

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@
import com.cotato.kampus.domain.common.application.ApiUserResolver;
import com.cotato.kampus.domain.post.dao.PostPhotoRepository;
import com.cotato.kampus.domain.post.dao.PostRepository;
import com.cotato.kampus.domain.post.dao.PostScrapRepository;
import com.cotato.kampus.domain.post.domain.Post;
import com.cotato.kampus.domain.post.domain.PostPhoto;
import com.cotato.kampus.domain.post.domain.PostScrap;
import com.cotato.kampus.domain.post.dto.MyPostWithPhoto;
import com.cotato.kampus.domain.post.dto.PostDto;
import com.cotato.kampus.domain.post.dto.PostWithPhotos;
Expand All @@ -37,18 +39,13 @@ public class PostFinder {
public static final String SORT_PROPERTY = "createdTime";
private final ApiUserResolver apiUserResolver;
private final BoardFinder boardFinder;
private final PostScrapRepository postScrapRepository;

public Post getPost(Long postId) {
return postRepository.findById(postId)
.orElseThrow(() -> new AppException(ErrorCode.POST_NOT_FOUND));
}

public Long getAuthorId(Long postId) {
Post post = postRepository.findById(postId)
.orElseThrow(() -> new AppException(ErrorCode.POST_NOT_FOUND));

return post.getUserId();
}

public Slice<PostWithPhotos> findPosts(Long boardId, int page) {
// 1. Post 리스트를 Slice로 조회
Expand Down Expand Up @@ -89,4 +86,25 @@ public Slice<MyPostWithPhoto> findUserPosts(int page){
return MyPostWithPhoto.from(post, boardName, postPhoto);
});
}

public Slice<MyPostWithPhoto> findUserScrapedPosts(int page){

Long userId = apiUserResolver.getUserId();
CustomPageRequest customPageRequest = new CustomPageRequest(page, PAGE_SIZE, Sort.Direction.DESC);

// 스크랩된 포스트만 조회
Slice<PostScrap> postScraps = postScrapRepository.findAllByUserId(userId, customPageRequest.of(SORT_PROPERTY));

// 스크랩된 포스트에 해당하는 Post를 찾아서 반환
return postScraps.map(postScrap -> {
Post post = getPost(postScrap.getPostId());

String boardName = boardFinder.findBoard(post.getBoardId()).getBoardName();

PostPhoto postPhoto = postPhotoRepository.findFirstByPostIdOrderByCreatedTimeAsc(post.getId())
.orElse(null);

return MyPostWithPhoto.from(post, boardName, postPhoto);
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.cotato.kampus.domain.post.application;

import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import com.cotato.kampus.domain.post.dao.PostScrapRepository;
import com.cotato.kampus.domain.post.domain.PostScrap;
import com.cotato.kampus.global.error.ErrorCode;
import com.cotato.kampus.global.error.exception.AppException;

import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;

@Component
@Transactional(readOnly = true)
@RequiredArgsConstructor(access = AccessLevel.PROTECTED)
public class PostScrapUpdater {

private final PostScrapRepository postScrapRepository;

public void append(Long postId, Long userId) {
PostScrap postScrap = PostScrap.builder()
.postId(postId)
.userId(userId)
.build();

postScrapRepository.save(postScrap);
}

public void delete(Long postId, Long userId) {
PostScrap postScrap = postScrapRepository.findByUserIdAndPostId(userId, postId)
.orElseThrow(() -> new AppException(ErrorCode.POST_SCRAP_NOT_EXIST));

postScrapRepository.delete(postScrap);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.cotato.kampus.domain.post.application;

import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import com.cotato.kampus.domain.post.dao.PostScrapRepository;
import com.cotato.kampus.global.error.ErrorCode;
import com.cotato.kampus.global.error.exception.AppException;

import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;

@Component
@Transactional(readOnly = true)
@RequiredArgsConstructor(access = AccessLevel.PROTECTED)
public class PostScrapValidator {

private final PostScrapRepository postScrapRepository;
private final PostAuthorResolver postAuthorResolver;

public void validatePostScrap(Long postId, Long userId){
Long authorId = postAuthorResolver.getAuthorId(postId);

// 본인 게시글 또는 이미 스크랩한 게시글은 예외처리
if(userId.equals(authorId)){
throw new AppException(ErrorCode.POST_SCRAP_FORBIDDEN);
}

if(postScrapRepository.existsByUserIdAndPostId(userId, postId)){
throw new AppException(ErrorCode.POST_SCRAP_DUPLICATED);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,16 +31,20 @@ public class PostService {
private final PostFinder postFinder;
private final PostUpdater postUpdater;

private final PostAuthorResolver postAuthorResolver;
private static final String POST_IMAGE_FOLDER = "post";

private final PostImageAppender postImageAppender;
private final PostImageFinder postImageFinder;
private final PostImageUpdater postImageUpdater;

private final PostAuthorResolver postAuthorResolver;
private final PostScrapUpdater postScrapUpdater;
private final ApiUserResolver apiUserResolver;
private final S3Uploader s3Uploader;

private final UserValidator userValidator;
private final ImageValidator imageValidator;
private final PostScrapValidator postScrapValidator;
private final UserValidator userValidator;
private final PostLikeAppender postLikeAppender;
private final PostLikeValidator postLikeValidator;
Expand Down Expand Up @@ -79,7 +83,10 @@ public Long createPost(
@Transactional
public Long deletePost(Long postId) {
// 작성자 검증: 현재 사용자가 게시글 작성자인지 확인
userValidator.validatePostAuthor(postId);
Long userId = apiUserResolver.getUserId();
Long authorId = postAuthorResolver.getAuthorId(postId);

userValidator.validatePostAuthor(authorId, userId);

// 게시글 삭제
postDeleter.delete(postId);
Expand Down Expand Up @@ -110,7 +117,7 @@ public void updatePost(Long postId, String title, String content, PostCategory p
List<MultipartFile> images) throws ImageException {
// 1. Post Author 검증
Long userId = apiUserResolver.getUserId();
postAuthorResolver.validatePostAuthor(postId, userId);
userValidator.validatePostAuthor(postId, userId);

// 2. Post 업데이트
postUpdater.updatePost(postId, title, content, postCategory, anonymity);
Expand All @@ -137,4 +144,33 @@ public void likePost(Long postId) {
public Slice<MyPostWithPhoto> findUserPosts(int page) {
return postFinder.findUserPosts(page);
}

@Transactional
public void scrapPost(Long postId){
// 스크랩 가능 여부 검증
Long userId = apiUserResolver.getUserId();
postScrapValidator.validatePostScrap(postId, userId);

// 게시글 스크랩 수 추가
postUpdater.increaseScraps(postId);

// 스크랩 데이터 추가
postScrapUpdater.append(postId, userId);
}

@Transactional
public void unscrapPost(Long postId){
// 유저 조회
Long userId = apiUserResolver.getUserId();

// 게시글 스크랩 수 감소
postUpdater.decreaseScraps(postId);

// 스크랩 데이터 삭제
postScrapUpdater.delete(postId, userId);
}

public Slice<MyPostWithPhoto> findUserScrapedPosts(int page){
return postFinder.findUserScrapedPosts(page);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,22 @@ public Long increaseNextAnonymousNumber(Long postId) {

return currentAnonymousNumber;
}

@Transactional
public void increaseScraps(Long postId){

Post post = postFinder.getPost(postId);
post.increaseScraps();

postRepository.save(post);
}

@Transactional
public void decreaseScraps(Long postId){

Post post = postFinder.getPost(postId);
post.decreaseScraps();

postRepository.save(post);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.cotato.kampus.domain.post.dao;

import java.util.Optional;

import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
import org.springframework.data.jpa.repository.JpaRepository;

import com.cotato.kampus.domain.post.domain.PostScrap;

public interface PostScrapRepository extends JpaRepository<PostScrap, Long> {

boolean existsByUserIdAndPostId(Long userId, Long postId);

Optional<PostScrap> findByUserIdAndPostId(Long userId, Long postId);

Slice<PostScrap> findAllByUserId(Long userId, Pageable pageable);
}
13 changes: 11 additions & 2 deletions src/main/java/com/cotato/kampus/domain/post/domain/Post.java
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ public class Post extends BaseTimeEntity {
@Column(name = "post_status", nullable = false)
private PostStatus postStatus;

@Enumerated(EnumType.STRING)
@Column(name = "post_category", nullable = false)
private PostCategory postCategory;

Expand Down Expand Up @@ -85,10 +86,18 @@ public void update(String title, String content, PostCategory postCategory, Anon
this.content = content;
this.postCategory = postCategory;
this.anonymity = anonymity;
}

public void increaseNextAnonymousNumber(){
this.nextAnonymousNumber++;
}

public void increaseScraps() {
this.scraps++;
}

public void increaseNextAnonymousNumber() {
this.nextAnonymousNumber++;
public void decreaseScraps() {
this.scraps--;
}

public void increaseLikes() {
Expand Down
38 changes: 38 additions & 0 deletions src/main/java/com/cotato/kampus/domain/post/domain/PostScrap.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.cotato.kampus.domain.post.domain;

import com.cotato.kampus.domain.common.domain.BaseTimeEntity;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Entity
@Table(name = "post_scrap")
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class PostScrap extends BaseTimeEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "post_scrap_id")
private Long id;

@Column(name = "user_id", nullable = false)
private Long userId;

@Column(name = "post_id", nullable = false)
private Long postId;

@Builder
public PostScrap(Long userId, Long postId) {
this.userId = userId;
this.postId = postId;
}
}
Loading

0 comments on commit d9dd678

Please sign in to comment.