diff --git a/src/main/java/com/example/feeda/domain/post/controller/PostController.java b/src/main/java/com/example/feeda/domain/post/controller/PostController.java index 67986b9..565dec6 100644 --- a/src/main/java/com/example/feeda/domain/post/controller/PostController.java +++ b/src/main/java/com/example/feeda/domain/post/controller/PostController.java @@ -2,23 +2,31 @@ import com.example.feeda.domain.post.dto.PostRequestDto; import com.example.feeda.domain.post.dto.PostResponseDto; -import com.example.feeda.domain.post.entity.Post; import com.example.feeda.domain.post.service.PostService; +import com.example.feeda.security.jwt.JwtPayload; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; import lombok.RequiredArgsConstructor; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; -import org.springframework.data.jpa.repository.config.EnableJpaAuditing; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; -import org.springframework.web.server.ResponseStatusException; - -import java.util.List; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; @RestController +@Validated @RequestMapping("/api/posts") @RequiredArgsConstructor public class PostController { @@ -26,44 +34,57 @@ public class PostController { private final PostService postService; @PostMapping - public ResponseEntity createPost(@RequestBody PostRequestDto requestDto) { + public ResponseEntity createPost(@RequestBody PostRequestDto requestDto, + @AuthenticationPrincipal JwtPayload jwtPayload) { - PostResponseDto post = postService.createPost(requestDto.getTitle(), requestDto.getContent(), requestDto.getCategory()); + PostResponseDto post = postService.createPost(requestDto, jwtPayload); - return new ResponseEntity<>(post, - HttpStatus.CREATED); + return new ResponseEntity<>(post, HttpStatus.CREATED); } @GetMapping("/{id}") - public ResponseEntity findPostById(@PathVariable Long id) { + public ResponseEntity findPostById(@PathVariable @NotNull Long id) { return new ResponseEntity<>(postService.findPostById(id), HttpStatus.OK); } @GetMapping public ResponseEntity> findAllPost( - @RequestParam(defaultValue = "1") int page, - @RequestParam(defaultValue = "10") int size, - @RequestParam(defaultValue = "") String keyword + @RequestParam(defaultValue = "1") @Min(1) int page, + @RequestParam(defaultValue = "10") @Min(1) int size, + @RequestParam(defaultValue = "") String keyword ) { - if (page < 1 || size < 1) { - throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "page 와 size 는 1 이상의 값이어야 합니다."); - } + Pageable pageable = PageRequest.of(page - 1, size, Sort.Direction.DESC, "updatedAt"); return new ResponseEntity<>(postService.findAll(pageable, keyword), HttpStatus.OK); } + @GetMapping("/followings") + public ResponseEntity> findFollowingAllPost( + @RequestParam(defaultValue = "1") @Min(1) int page, + @RequestParam(defaultValue = "10") @Min(1) int size, + @AuthenticationPrincipal JwtPayload jwtPayload + ) { + Pageable pageable = PageRequest.of(page - 1, size); + + return new ResponseEntity<>(postService.findFollowingAllPost(pageable, jwtPayload), + HttpStatus.OK); + } + @PutMapping("/{id}") - public ResponseEntity updatePost( - @PathVariable Long id, - @RequestBody PostRequestDto requestDto) { - Post post = postService.updatePost(id, requestDto); + public ResponseEntity updatePost( + @PathVariable @NotNull Long id, + @RequestBody PostRequestDto requestDto, @AuthenticationPrincipal JwtPayload jwtPayload) { + + PostResponseDto post = postService.updatePost(id, requestDto, jwtPayload); return new ResponseEntity<>(post, HttpStatus.OK); } @DeleteMapping("/{id}") - public ResponseEntity deletePost(@PathVariable Long id) { - postService.deletePost(id); + public ResponseEntity deletePost(@PathVariable @NotNull Long id, + @AuthenticationPrincipal JwtPayload jwtPayload) { + + postService.deletePost(id, jwtPayload); return new ResponseEntity<>(HttpStatus.OK); } } diff --git a/src/main/java/com/example/feeda/domain/post/entity/Post.java b/src/main/java/com/example/feeda/domain/post/entity/Post.java index 87dbbfb..f1367d6 100644 --- a/src/main/java/com/example/feeda/domain/post/entity/Post.java +++ b/src/main/java/com/example/feeda/domain/post/entity/Post.java @@ -1,7 +1,14 @@ package com.example.feeda.domain.post.entity; import com.example.feeda.domain.profile.entity.Profile; -import jakarta.persistence.*; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; import lombok.Getter; @@ -10,7 +17,7 @@ @Getter @Entity @Table(name = "posts") -public class Post extends BaseEntity{ +public class Post extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -28,24 +35,25 @@ public class Post extends BaseEntity{ @Column(length = 50) private String category; - public void update(String title, String content, String category) { - this.title = title; - this.content = content; - this.category = category; - } + @ManyToOne + @JoinColumn(name = "profile_id") + private Profile profile; - public Post(String title, String content, String category) { + public Post(String title, String content, String category, Profile profile) { this.title = title; this.content = content; this.category = category; + this.profile = profile; } protected Post() { } - @ManyToOne - @JoinColumn(name = "profile_id") - private Profile profile; + public void update(String title, String content, String category) { + this.title = title; + this.content = content; + this.category = category; + } } diff --git a/src/main/java/com/example/feeda/domain/post/repository/PostRepository.java b/src/main/java/com/example/feeda/domain/post/repository/PostRepository.java index bd62d79..4d0ec21 100644 --- a/src/main/java/com/example/feeda/domain/post/repository/PostRepository.java +++ b/src/main/java/com/example/feeda/domain/post/repository/PostRepository.java @@ -1,14 +1,14 @@ package com.example.feeda.domain.post.repository; import com.example.feeda.domain.post.entity.Post; +import java.util.List; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.http.HttpStatus; -import org.springframework.web.server.ResponseStatusException; public interface PostRepository extends JpaRepository { Page findAllByTitleContaining(String title, Pageable pageable); + Page findAllByProfile_IdIn(List followingProfileIds, Pageable pageable); } diff --git a/src/main/java/com/example/feeda/domain/post/service/PostService.java b/src/main/java/com/example/feeda/domain/post/service/PostService.java index 3e340c7..40c1f9e 100644 --- a/src/main/java/com/example/feeda/domain/post/service/PostService.java +++ b/src/main/java/com/example/feeda/domain/post/service/PostService.java @@ -2,22 +2,23 @@ import com.example.feeda.domain.post.dto.PostRequestDto; import com.example.feeda.domain.post.dto.PostResponseDto; -import com.example.feeda.domain.post.entity.Post; +import com.example.feeda.security.jwt.JwtPayload; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; -import java.util.List; - public interface PostService { - PostResponseDto createPost(String title, String content, String category); + PostResponseDto createPost(PostRequestDto postRequestDto, + JwtPayload jwtPayload); PostResponseDto findPostById(Long id); Page findAll(Pageable pageable, String keyword); - Post updatePost(Long id, PostRequestDto requestDto); + Page findFollowingAllPost(Pageable pageable, JwtPayload jwtPayload); + + PostResponseDto updatePost(Long id, PostRequestDto requestDto, JwtPayload jwtPayload); - void deletePost(Long id); + void deletePost(Long id, JwtPayload jwtPayload); } diff --git a/src/main/java/com/example/feeda/domain/post/service/PostServiceImpl.java b/src/main/java/com/example/feeda/domain/post/service/PostServiceImpl.java index ff3a9b3..7fa17a7 100644 --- a/src/main/java/com/example/feeda/domain/post/service/PostServiceImpl.java +++ b/src/main/java/com/example/feeda/domain/post/service/PostServiceImpl.java @@ -1,36 +1,54 @@ package com.example.feeda.domain.post.service; +import com.example.feeda.domain.follow.entity.Follows; +import com.example.feeda.domain.follow.repository.FollowsRepository; import com.example.feeda.domain.post.dto.PostRequestDto; import com.example.feeda.domain.post.dto.PostResponseDto; import com.example.feeda.domain.post.entity.Post; import com.example.feeda.domain.post.repository.PostRepository; +import com.example.feeda.domain.profile.entity.Profile; +import com.example.feeda.domain.profile.repository.ProfileRepository; +import com.example.feeda.security.jwt.JwtPayload; +import java.util.List; +import java.util.Optional; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import org.springframework.web.server.ResponseStatusException; -import java.util.List; -import java.util.Optional; - @Service @RequiredArgsConstructor public class PostServiceImpl implements PostService { private final PostRepository postRepository; + private final ProfileRepository profileRepository; + private final FollowsRepository followsRepository; @Override - public PostResponseDto createPost(String title, String content, String category) { - Post post = new Post(title, content, category); + public PostResponseDto createPost(PostRequestDto postRequestDto, JwtPayload jwtPayload) { + + Profile profile = profileRepository.findById(jwtPayload.getProfileId()) + .orElseThrow( + () -> new ResponseStatusException(HttpStatus.NOT_FOUND, "존재하지 않는 프로필입니다.")); + + Post post = new Post(postRequestDto.getTitle(), postRequestDto.getContent(), + postRequestDto.getCategory(), profile); + Post savedPost = postRepository.save(post); + return new PostResponseDto(savedPost.getId(), - savedPost.getTitle(), - savedPost.getContent(), - savedPost.getCategory()); + savedPost.getTitle(), + savedPost.getContent(), + savedPost.getCategory()); } @Override + @Transactional(readOnly = true) public PostResponseDto findPostById(Long id) { Optional optionalPost = postRepository.findById(id); @@ -40,29 +58,66 @@ public PostResponseDto findPostById(Long id) { Post findPost = optionalPost.get(); - return new PostResponseDto(id, findPost.getTitle(), findPost.getContent(), findPost.getCategory()); + return new PostResponseDto(id, findPost.getTitle(), findPost.getContent(), + findPost.getCategory()); } @Override + @Transactional(readOnly = true) public Page findAll(Pageable pageable, String keyword) { - return postRepository.findAllByTitleContaining(keyword, pageable).map(PostResponseDto::toDto); + + return postRepository.findAllByTitleContaining(keyword, pageable) + .map(PostResponseDto::toDto); } @Override - public Post updatePost(Long id, PostRequestDto requestDto) { + @Transactional(readOnly = true) + public Page findFollowingAllPost(Pageable pageable, JwtPayload jwtPayload) { + + Page followings = followsRepository.findAllByFollowers_Id( + jwtPayload.getProfileId(), pageable); + + List followingProfileIds = followings.stream() + .map(following -> following.getFollowings().getId()) + .toList(); + + Pageable newPageable = PageRequest.of(pageable.getPageNumber(), pageable.getPageSize(), + Sort.Direction.DESC, "updatedAt"); + + return postRepository.findAllByProfile_IdIn(followingProfileIds, newPageable) + .map(PostResponseDto::toDto); + } + + @Override + @Transactional + public PostResponseDto updatePost(Long id, PostRequestDto requestDto, JwtPayload jwtPayload) { + Post findPost = postRepository.findById(id) - .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "존재하지 않는 게시글")); + .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "존재하지 않는 게시글")); + if (!findPost.getProfile().getId().equals(jwtPayload.getProfileId())) { + throw new ResponseStatusException(HttpStatus.FORBIDDEN, "권한이 없습니다."); + } findPost.update(requestDto.getTitle(), requestDto.getCategory(), requestDto.getCategory()); - postRepository.save(findPost); + Post savedPost = postRepository.save(findPost); - return findPost; + return new PostResponseDto(savedPost.getId(), + savedPost.getTitle(), + savedPost.getContent(), + savedPost.getCategory()); } @Override - public void deletePost(Long id) { - Post findPost = postRepository.findById(id).orElseThrow( () -> new ResponseStatusException(HttpStatus.NOT_FOUND, "존재하지 않는 게시글")); + @Transactional + public void deletePost(Long id, JwtPayload jwtPayload) { + + Post findPost = postRepository.findById(id) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "존재하지 않는 게시글")); + + if (!findPost.getProfile().getId().equals(jwtPayload.getProfileId())) { + throw new ResponseStatusException(HttpStatus.FORBIDDEN, "권한이 없습니다."); + } postRepository.delete(findPost); }