[내일배움캠프 Spring 4기] 45일차 TIL - Spring 뉴스 피드 프로젝트 | 게시물 기능 구현

서예진·2024년 2월 13일
0

목차📕

  1. 게시물 작성 기능 구현
  2. 게시물 조회 기능 구현
  3. 게시물 수정 기능 구현
  4. 게시물 삭제 기능 구현
  5. pull 받은 후 구현

1. 게시물 작성 기능 구현

  • entity > TimeEntity
    • TimeEntity

      package com.sparta.newsfeed.entity;
      
      import jakarta.persistence.EntityListeners;
      import jakarta.persistence.MappedSuperclass;
      import lombok.Getter;
      import org.springframework.data.annotation.CreatedDate;
      import org.springframework.data.annotation.LastModifiedDate;
      import org.springframework.data.jpa.domain.support.AuditingEntityListener;
      
      import java.time.LocalDateTime;
      
      @Getter
      @MappedSuperclass
      @EntityListeners(AuditingEntityListener.class)
      public class TimeEntity {
          @CreatedDate
          private LocalDateTime createdDate;
      
          @LastModifiedDate
          private LocalDateTime modifiedDate;
      }
    • @MappedSuperClass

      JPA Entity 클래스들이 BaseTimeEntity를 상속할 경우 이 클래스의 변수인 createdDate와 modifiedDate도 칼럼으로 인식하게한다.

    • @EntityListners(AuditingEntityListener.class)

      BaseTimeEntity 클래스에 Auditing 기능을 포함시킨다.

    • @CreatedDate

      Entity가 생성되면, createdDate에 저장 시간이 자동으로 저장된다.

    • @LastModifiedDate

      조회한 Entity의 값을 변경할 때 시간이 자동 저장된다.

      이렇게 만들어준 BaseTimeEntity를 Entity인 Posts에 상속해주고, JPA Auditing을 활성화 할 수 있도록 Application 클래스에 활성화 어노테이션을 추가해주면, 자동으로 데이터베이스에 생성시간과 수정시간을 포함하여 저장할 수 있다.

  • entity > Post

    package com.sparta.newsfeed.entity;
    
    import com.sparta.newsfeed.dto.PostRequsetDto;
    import com.sparta.newsfeed.dto.PostResponseDto;
    import jakarta.persistence.*;
    import lombok.Getter;
    import lombok.NoArgsConstructor;
    
    import java.time.LocalDateTime;
    
    @Entity
    @Getter
    @NoArgsConstructor
    @Table(name = "posts")
    public class Post extends TimeEntity{
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long id;
    
        @Column(nullable = false)
        private String title;
    
        @Column(nullable = false)
        private String content;
    
        public Post(PostRequsetDto requsetDto){
            this.title = requsetDto.getTitle();
            this.content = requsetDto.getContent();
        }
    
    }
  • controller > PostController

    package com.sparta.newsfeed.controller;
    
    import com.sparta.newsfeed.dto.PostRequsetDto;
    import com.sparta.newsfeed.dto.PostResponseDto;
    import com.sparta.newsfeed.service.PostService;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestBody;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    @RestController
    @RequestMapping("/api/posts")
    public class PostController {
    
        private final PostService postService;
        public PostController(PostService postService) {
            this.postService = postService;
        }
    
        //게시물 생성
        @PostMapping("/create")
        public PostResponseDto createPost(@RequestBody PostRequsetDto requsetDto){
            return postService.createPost(requsetDto);
        }
    
    }
  • dto > PostRequestDto

    package com.sparta.newsfeed.dto;
    
    import lombok.Getter;
    
    @Getter
    public class PostRequsetDto {
        private String title;
        private String content;
    }
  • dto > PostResponseDto

    package com.sparta.newsfeed.dto;
    
    import com.sparta.newsfeed.entity.Post;
    import lombok.Getter;
    import lombok.Setter;
    import org.springframework.cglib.core.Local;
    
    import java.time.LocalDateTime;
    
    @Getter
    @Setter
    public class PostResponseDto {
        private Long id;
        private String title;
        private String content;
        private LocalDateTime createdDate;
        private LocalDateTime modifiedDate;
    
        public PostResponseDto(Post post){
            this.id = post.getId();
            this.title = post.getTitle();
            this.content= post.getContent();
            this.createdDate = post.getCreatedDate();
            this.modifiedDate = post.getModifiedDate();
        }
    }
  • service > PostService

    package com.sparta.newsfeed.service;
    
    import com.sparta.newsfeed.dto.PostRequsetDto;
    import com.sparta.newsfeed.dto.PostResponseDto;
    import com.sparta.newsfeed.entity.Post;
    import com.sparta.newsfeed.repository.PostRepository;
    import org.springframework.stereotype.Service;
    
    @Service
    public class PostService {
    
        private final PostRepository postRepository;
        public PostService(PostRepository postRepository) {
            this.postRepository = postRepository;
        }
    
        //게시물 생성
        public PostResponseDto createPost(PostRequsetDto requsetDto) {
            Post post = new Post(requsetDto);
            postRepository.save(post);
            return new PostResponseDto(post);
        }
    }
  • repository > PostRepository

    package com.sparta.newsfeed.repository;
    
        import com.sparta.newsfeed.entity.Post;
        import org.springframework.data.jpa.repository.JpaRepository;
    
        public interface PostRepository extends 	JpaRepository<Post, Long> {
        }

오류

  • 게시물 작성 기능을 postman에서 test할 때 401이 뜸

시도

  • build.gradle에서 security 주석처리 => 해결
  • null인 점을 안보여주고 싶어서 아래 코드를 responsedto에 추가
@JsonInclude(JsonInclude.Include.NON_NULL) // -> JSON으로 받아올 때 null인 것들을 빼고 받아옴

알게된 점

  • Spring Security는 자동으로 웹 보안이 적용된다.

2. 게시물 조회 기능 구현

[게시물 1건 조회 기능]

  • controller > PostController
    //게시물 1건 조회
        @GetMapping("/{postid}")
        public PostResponseDto getOnePost(@PathVariable Long postid){
            return postService.getOnePost(postid);
        }
  • service > PostService
    public PostResponseDto getOnePost(Long postid) {
            Post post = postRepository.findById(postid).orElseThrow(() -> new IllegalArgumentException("존재하지 않는 id 입니다."));
            return  new PostResponseDto(post);
        }

[게시물 전체 조회 기능]

  • PostController

    //게시물 전체 조회
        @GetMapping
        public List<PostResponseDto> getAllPost(){
            return postService.getAllPost();
        }
  • PostService

    //게시물 전체 조회
        public List<PostResponseDto> getAllPost() {
            List<Post> postList = postRepository.findAll(Sort.by(Sort.Direction.DESC, "modifiedDate"));
            List<PostResponseDto> responseDtoList = new ArrayList<>();
    
            for(Post post : postList){
                responseDtoList.add(new PostResponseDto(post));
            }
    
            return responseDtoList;
        }

3. 게시물 수정 기능 구현

  • controller > PostController
    //게시물 수정
        @PutMapping("/{postid}")
        public PostResponseDto updatePost(@PathVariable Long postid, @RequestBody PostRequsetDto requsetDto){
            return postService.updatePost(postid, requsetDto);
        }
  • service > PostService
    //게시물 수정
        @Transactional
        public PostResponseDto updatePost(Long postid, PostRequsetDto requsetDto) {
            Post post = postRepository.findById(postid).orElseThrow(() -> new IllegalArgumentException("존재하지 않는 id 입니다."));
            post.update(requsetDto);
            return new PostResponseDto(post);
        }
  • entity > Post
    public void update(PostRequsetDto requsetDto) {
            this.title = requsetDto.getTitle();
            this.content = requsetDto.getContent();
        }

4. 게시물 삭제 기능 구현

  • PostController
    //게시물 삭제
        @DeleteMapping("/{postid}")
        public void deletePost(@PathVariable Long postid){
            postService.deletePost(postid);
        }
  • PostService
    //게시물 삭제
        public void deletePost(Long postid) {
            Post post = postRepository.findById(postid).orElseThrow(() -> new IllegalArgumentException("존재하지 않는 id 입니다."));
            postRepository.delete(post);
        }

5. pull 받은 후 구현

💡 로그인된 유저만 게시물 생성, 수정, 삭제할 수 있게 수정

💡 게시물 삭제 기능에서 댓글 먼저 삭제 후 게시물 삭제되게 수정

  • Post

    package com.sparta.newsfeed.entity;
    
    import com.sparta.newsfeed.dto.PostRequsetDto;
    import com.sparta.newsfeed.dto.PostResponseDto;
    import com.sparta.newsfeed.user.entity.User;
    import com.sparta.newsfeed.user.security.UserDetailsImpl;
    import jakarta.persistence.*;
    import lombok.Getter;
    import lombok.NoArgsConstructor;
    import org.springframework.security.core.userdetails.UserDetails;
    
    import java.time.LocalDateTime;
    
    @Entity
    @Getter
    @NoArgsConstructor
    @Table(name = "posts")
    public class Post extends TimeEntity{
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long id;
    
        @Column(nullable = false)
        private String title;
    
        @Column(nullable = false)
        private String content;
    
        @Column(nullable = false)
        private String username;
    
        @ManyToOne
        @JoinColumn(name = "user_id")
        private User user;
    
        public Post(PostRequsetDto requsetDto, UserDetailsImpl userDetails){
            this.title = requsetDto.getTitle();
            this.content = requsetDto.getContent();
            this.user = userDetails.getUser();
            this.username = userDetails.getUsername();
        }
    
        public void update(PostRequsetDto requsetDto) {
            this.title = requsetDto.getTitle();
            this.content = requsetDto.getContent();
        }
    }
  • PostResponseDto

    package com.sparta.newsfeed.dto;
    
    import com.sparta.newsfeed.entity.Post;
    import lombok.Getter;
    import lombok.Setter;
    import org.springframework.cglib.core.Local;
    
    import java.time.LocalDateTime;
    
    @Getter
    @Setter
    public class PostResponseDto {
        private Long id;
        private String title;
        private String content;
        private String username;
        private LocalDateTime createdDate;
        private LocalDateTime modifiedDate;
    
        public PostResponseDto(Post post){
            this.id = post.getId();
            this.title = post.getTitle();
            this.content= post.getContent();
            this.username = post.getUsername();
            this.createdDate = post.getCreatedDate();
            this.modifiedDate = post.getModifiedDate();
        }
    }
  • PostController

    package com.sparta.newsfeed.controller;
    
    import com.sparta.newsfeed.dto.PostRequsetDto;
    import com.sparta.newsfeed.dto.PostResponseDto;
    import com.sparta.newsfeed.service.PostService;
    import com.sparta.newsfeed.user.security.UserDetailsImpl;
    import org.springframework.http.ResponseEntity;
    import org.springframework.security.core.annotation.AuthenticationPrincipal;
    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.*;
    
    import java.util.List;
    
    @Controller
    @RequestMapping("/api/posts")
    public class PostController {
    
        private final PostService postService;
        public PostController(PostService postService) {
            this.postService = postService;
        }
    
        //게시물 생성
        @PostMapping("/create")
        public String createPost(@RequestBody PostRequsetDto requsetDto, @AuthenticationPrincipal UserDetailsImpl userDetails){
            postService.createPost(requsetDto, userDetails);
            return "redirect:/api/posts";
        }
    
        //작성자의 전체 게시물 조회
        @ResponseBody
        @GetMapping("/userId/{userId}")
        public List<PostResponseDto> getPostsByUserId(@PathVariable Long userId, @AuthenticationPrincipal UserDetailsImpl userDetails){
            return postService.getPostsByUserId(userId, userDetails);
        }
    
        //게시물 1건 조회
        @ResponseBody
        @GetMapping("/postId/{postId}")
        public PostResponseDto getOnePost(@PathVariable Long postId){
            return postService.getOnePost(postId);
        }
    
        //게시물 전체 조회
        @ResponseBody
        @GetMapping
        public List<PostResponseDto> getAllPost(){
            return postService.getAllPost();
        }
    
        //게시물 수정
        @PutMapping("/{postid}")
        public String updatePost(@PathVariable Long postid, @RequestBody PostRequsetDto requsetDto, @AuthenticationPrincipal UserDetailsImpl userDetails){
            postService.updatePost(postid, requsetDto, userDetails);
            return "redirect:/api/posts";
        }
    
        //게시물 삭제
        @DeleteMapping("/{postid}")
        public String deletePost(@PathVariable Long postid, @AuthenticationPrincipal UserDetailsImpl userDetails){
            postService.deletePost(postid, userDetails);
            return "redirect:/api/posts";
        }
    }
  • PosetService

    package com.sparta.newsfeed.service;
    
    import com.sparta.newsfeed.dto.PostRequsetDto;
    import com.sparta.newsfeed.dto.PostResponseDto;
    import com.sparta.newsfeed.entity.Post;
    import com.sparta.newsfeed.repository.PostRepository;
    import com.sparta.newsfeed.user.entity.User;
    import com.sparta.newsfeed.user.security.UserDetailsImpl;
    import org.springframework.data.domain.Sort;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Transactional;
    
    import java.util.ArrayList;
    import java.util.List;
    import java.util.concurrent.RejectedExecutionException;
    
    @Service
    public class PostService {
    
        private final PostRepository postRepository;
        private final CommentService commentService;
        public PostService(PostRepository postRepository, CommentService commentService) {
            this.postRepository = postRepository;
            this.commentService = commentService;
        }
    
        //게시물 생성
        public PostResponseDto createPost(PostRequsetDto requsetDto, UserDetailsImpl userDetails) {
            Post post = new Post(requsetDto, userDetails);
            postRepository.save(post);
            return new PostResponseDto(post);
        }
    
        //게시물 1건 조회
        public PostResponseDto getOnePost(Long postid) {
            Post post = postRepository.findById(postid).orElseThrow(() -> new IllegalArgumentException("존재하지 않는 id 입니다."));
            return new PostResponseDto(post);
        }
    
        //게시물 전체 조회
        public List<PostResponseDto> getAllPost() {
            List<Post> postList = postRepository.findAll(Sort.by(Sort.Direction.DESC, "modifiedDate"));
            List<PostResponseDto> responseDtoList = new ArrayList<>();
    
            for(Post post : postList){
                responseDtoList.add(new PostResponseDto(post));
            }
    
            return responseDtoList;
        }
        //게시물 수정
        @Transactional
        public PostResponseDto updatePost(Long postid, PostRequsetDto requsetDto, UserDetailsImpl userDetails) {
            Post post = postRepository.findById(postid).orElseThrow(() -> new IllegalArgumentException("존재하지 않는 id 입니다."));
    
            if(!userDetails.getUsername().equals(post.getUsername())){
                throw new RejectedExecutionException("게시물의 작성자만 수정이 가능합니다.");
            }
            post.update(requsetDto);
            return new PostResponseDto(post);
        }
    
        //게시물 삭제
        @Transactional
        public void deletePost(Long postid, UserDetailsImpl userDetails) {
            Post post = postRepository.findById(postid).orElseThrow(() -> new IllegalArgumentException("존재하지 않는 id 입니다."));
    
            if(!userDetails.getUsername().equals(post.getUsername())){
                throw new RejectedExecutionException("게시물의 작성자만 삭제가 가능합니다.");
            }
            commentService.deleteCommentByPostId(postid);
            postRepository.delete(post);
        }
    
        //작성자의 전체 게시물 조회
        public List<PostResponseDto> getPostsByUserId(Long userId, UserDetailsImpl userDetails) {
            if(!userId.equals(userDetails.getUser().getId())){
                throw new RejectedExecutionException("가입된 유저가 아닙니다.");
            }
            List<Post> posts = postRepository.findByUserId(userId);
            List<PostResponseDto> responseDtoList = new ArrayList<>();
    
            for (Post post : posts) {
                responseDtoList.add(new PostResponseDto(post));
            }
    
            return responseDtoList;
        }
    }
  • PostRepository

    package com.sparta.newsfeed.repository;
    
    import com.sparta.newsfeed.entity.Post;
    import org.springframework.data.jpa.repository.JpaRepository;
    
    import java.util.List;
    import java.util.Optional;
    
    public interface PostRepository extends JpaRepository<Post, Long> {
        List<Post> findByUserId(Long userId);
    }

오류

  • 게시물 삭제 기능에서만 아래 오류가 뜸
    'InvalidDataAccessApiUsageException: No EntityManager with actual transaction available for current thread'

시도

  • PostService에서 게시물 삭제 메서드에 @Transactional을 달았더니 해결됨

알게된 점

  • 스프링 어플리케이션에서 데이터베이스 트랜잭션을 관리할 때 @Transactional 애노테이션을 사용하여 트랜잭션을 정의해야 한다.

profile
안녕하세요

0개의 댓글