Server/Spring Boot

[Spring Boot] Toy Project 게시판 만들기 (2) - 생성, 수정

Nellie29 2023. 12. 17. 23:57


1️⃣ Entity 생성

  • DB와 매핑하기 위해서 Entity 계층을 만들어주고 필요한 필드들을 선언

💚 PostEntity

package com.gdsc_teamb.servertoyproject.domain.post.domain;

import com.gdsc_teamb.servertoyproject.domain.user.domain.UserEntity;
import jakarta.persistence.*;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;

import java.time.LocalDateTime;
import java.util.Objects;

@Entity
@Table(name = "Post")
@NoArgsConstructor
@Getter
public class PostEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToOne
    @NotNull
    private UserEntity user;

    @Size(max = 50)
    @NotNull
    private String title;

    @NotNull
    private String content;

    @CreationTimestamp
    private LocalDateTime created_at;

    @UpdateTimestamp
    private LocalDateTime updated_at;

    @Builder
    public PostEntity(UserEntity user, String title, String content) {
        this.user = user;
        this.title = title;
        this.content = content;
    }


    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (!(obj instanceof PostEntity postEntity)) return false;
        if (this.id == null || postEntity.getId() == null) return false;

        return Objects.equals(this.id, postEntity.getId()) &&
                Objects.equals(this.user, postEntity.getUser()) &&
                Objects.equals(this.title, postEntity.getTitle()) &&
                Objects.equals(this.content, postEntity.getContent()) &&
                Objects.equals(this.created_at, postEntity.getCreated_at());
    }

    @Override
    public int hashCode() {
        return Objects.hash(id, user, created_at);
    }
}

2️⃣ Repository 생성

💚 PostRepository

package com.gdsc_teamb.servertoyproject.domain.repository;

import com.gdsc_teamb.servertoyproject.domain.post.domain.PostEntity;
import com.gdsc_teamb.servertoyproject.domain.user.domain.UserEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;

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

// PostEntity 관리하는 JPA 레포
// PostEntity 에 대한 CRUD 작업 제공
@Repository
public interface PostRepository extends JpaRepository<PostEntity, Long> {
    //ID 기준으로 내림차순되게 쿼리 정의함
    //@Query("SELECT p FROM PostEntity p ORDER BY p.id DESC")
    List<PostEntity> findAllByOrderByIdDesc();
    List<PostEntity> findAllByUser(UserEntity user);
}

3️⃣ RequestDto 생성

💚 BoardDto

  • 게시글 제목이 공백이거나 . 50글자를 초과하지 못하도록 함
package com.gdsc_teamb.servertoyproject.dto.boardDto;

import com.gdsc_teamb.servertoyproject.domain.post.domain.PostEntity;
import com.gdsc_teamb.servertoyproject.domain.user.domain.UserEntity;
import lombok.*;
import org.apache.catalina.User;

import java.time.LocalDateTime;

// 게시글의 생성 및 수정에 사용도는 데이터 전송
@Getter
@Setter
@ToString
@NoArgsConstructor
public class BoardDto {
    private String title; // 게시글 제목
    private String content; // 게시글 내용
    private String nickname; // 작성자 이름

    //BoardDto 생성자
    @Builder
    public BoardDto(String title, String content, UserEntity user){
        this.title=title;
        this.content=content;
        this.nickname=user.getNickname();
    }

    // BoardDto 를 PostEntity 로 변환하는 메서드
    // 변환된 PostEntity 가 return
    public PostEntity toEntity(UserEntity user){
        PostEntity build = PostEntity.builder()
                .user(user)
                .title(title)
                .content(content)
                .build();
        return build;
    }

    // 게시글 제목 관련 설정 메서드
    // 제목이 공백이거나 50자 이상을 초과할 경우 IllegalArgumentException 발생됨
    public void setTitle(String title) {
        if (title == null || title.trim().isEmpty()) {
            throw new IllegalArgumentException("게시판 제목은 공백일 수 없습니다.");
        }
        if (title.length() > 50) {
            throw new IllegalArgumentException("게시판 제목은 " + 50 + "자 이하로 입력해야 합니다.");
        }
        this.title = title;
    }
    // 게시글 내용 관련 설정 메서드
    // 내용이 공백일 경우 IllegalArgumentException 발생됨
    public void setContent(String content) {
        if (content == null || content.trim().isEmpty()) {
            throw new IllegalArgumentException("게시판 내용은 공백일 수 없습니다.");
        }
        this.content = content;
    }

}

💚BoardUpdateDto

package com.gdsc_teamb.servertoyproject.dto.boardDto;

import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

//게시글 수정 시 사용
//수정 할 게시글의 제목과 내용 담음
@Getter
@NoArgsConstructor
public class BoardUpdateDto {

    private String title;   // 수정할 게시글 제목
    private String content; // 수정할 게시글 내용


    //BoardUpdateDto 의 생성자
    @Builder
    public BoardUpdateDto(String title, String content){
        this.title=title;
        this.content=content;
    }
}

4️⃣ Service 생성

💚 BoardService

package com.gdsc_teamb.servertoyproject.service;

import com.gdsc_teamb.servertoyproject.domain.post.domain.PostEntity;
import com.gdsc_teamb.servertoyproject.domain.repository.PostRepository;
import com.gdsc_teamb.servertoyproject.domain.user.domain.UserEntity;
import com.gdsc_teamb.servertoyproject.domain.user.domain.UserRepository;
import com.gdsc_teamb.servertoyproject.dto.boardDto.BoardDto;
import com.gdsc_teamb.servertoyproject.dto.boardDto.BoardListDto;
import com.gdsc_teamb.servertoyproject.dto.boardDto.BoardReadDto;
import com.gdsc_teamb.servertoyproject.dto.boardDto.BoardUpdateDto;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.stream.Collectors;

@Service
@RequiredArgsConstructor

public class BoardService {
    private final PostRepository postRepository;
    private final UserRepository userRepository;

    @Transactional
    public ResponseEntity<Object> savePost(BoardDto boardDto) {
        try{
            UserEntity user=userRepository.findByNickname(boardDto.getNickname()).
                    orElseThrow(()->new IllegalArgumentException());
            // PostEntity 생성
            PostEntity postEntity = boardDto.toEntity(user);

            postRepository.save(postEntity); // 저장

            return ResponseEntity.ok(boardDto.toEntity(user).getId());
        }catch(Exception e){
            return ResponseEntity.badRequest().body(e.getMessage());
        }

    }

    @Transactional
    public ResponseEntity<Object> updatePost(Long id, BoardUpdateDto boardUpdateDto) {
        try {
            PostEntity post = postRepository.findById(id)
                    .orElseThrow(() -> new IllegalArgumentException());

            post.update(boardUpdateDto.getTitle(), boardUpdateDto.getContent());

            return ResponseEntity.ok("게시글이 성공적으로 수정되었습니다. id= "+id);
        } catch (Exception e) {
            return ResponseEntity.badRequest().body("게시글 수정에 실패했습니다. " + e.getMessage());
        }
    }


}

📌 savePost()

  • 게시글 작성 메서드
  • boardDto 는 작성할 게시글의 정보를 담음
  • 작성된 게시글의 id 를 return

📌 updatePost()

  • 게시글 수정 메서드
  • id는 수정할 게시글의 id를 담고, boardUpdateDto 는 수정할 내용을 담음
  • 수정된 게시글의 id 를 return

5️⃣ Controller 생성

💚 BoardController

package com.gdsc_teamb.servertoyproject.controller;

import com.gdsc_teamb.servertoyproject.dto.boardDto.BoardDto;
import com.gdsc_teamb.servertoyproject.dto.boardDto.BoardListDto;
import com.gdsc_teamb.servertoyproject.dto.boardDto.BoardReadDto;
import com.gdsc_teamb.servertoyproject.dto.boardDto.BoardUpdateDto;
import com.gdsc_teamb.servertoyproject.service.BoardService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequiredArgsConstructor

public class BoardController {
    private final BoardService boardService;

    // HTTP POST 요청을 통해 새로운 게시글을 생성
    // 매개변수: 새로운 게시글을 생성하기 위한 데이터를 담은 DTO
    // @RequestBody 어노테이션은 JSON 데이터를 BoardDto 객체로 변환
    // 반환 값: 새로 생성된 게시글의 ID를 반환함
    // 반환된 ID는 클라이언트에게 제공 (응답으로 전송) -> 클라이언트가 나중에 수정, 삭제할때 사용
    @PostMapping("/api/boards")
    public ResponseEntity<Object> save(@RequestBody BoardDto boardDto){
        return boardService.save(boardDto);
    }

    // HTTP PUT 요청을 통해 특정 ID의 게시글을 수정
    // 매개변수: 수정할 게시글의 ID, 수정할 내용이 담긴 DTO
    // @PathVariable 을 통해 경로에서 추출된 게시글의 ID를 전달 받고, @RequestBody를 통해 클라이언트로부터 전달된 JSON 데이터를 변환하여 requestDto로 전달
    // 반환 값: 수정이 완료된 게시글의 ID를 반환
    @PutMapping("/api/boards/{id}")
    public ResponseEntity<Object> update(@PathVariable Long id, @RequestBody BoardUpdateDto requestDto){
        return boardService.update(id, requestDto);
    }

}

📌 save()

  • HTTP POST 요청을 통해 새로운 게시글을 생성
  • 매개변수: 새로운 게시글을 생성하기 위한 데이터를 담은 DTO
  • @RequestBody 어노테이션은 JSON 데이터를 BoardDto 객체로 변환
  • 반환 값: 새로 생성된 게시글의 ID를 반환함
  • 반환된 ID는 클라이언트에게 제공 (응답으로 전송) -> 클라이언트가 나중에 수정, 삭제할때 사용

📌 update()

  • HTTP PUT 요청을 통해 특정 ID의 게시글을 수정
  • 매개변수: 수정할 게시글의 ID, 수정할 내용이 담긴 DTO
  • @PathVariable 을 통해 경로에서 추출된 게시글의 ID를 전달 받고, @RequestBody를 통해 클라이언트로부터 전달된 JSON 데이터를 변환하여 requestDto로 전달
  • 반환 값: 수정이 완료된 게시글의 ID를 반환

6️⃣ TestCode

💚 PostRepositoryTest

  • 게시글이 저장되는지, 잘 불러와지는지에 대한 테스트 코드
package com.gdsc_teamb.servertoyproject.domain.repository;

import com.gdsc_teamb.servertoyproject.domain.post.domain.PostEntity;
import com.gdsc_teamb.servertoyproject.domain.user.domain.UserEntity;
import com.gdsc_teamb.servertoyproject.domain.user.domain.UserRepository;
import jakarta.transaction.Transactional;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit.jupiter.SpringExtension;

import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;


@ExtendWith(SpringExtension.class)
@SpringBootTest
@Transactional
class PostRepositoryTest {

    @Autowired
    private PostRepository postRepository;

    @Autowired
    private UserRepository userRepository;

    @Test
    @DisplayName("게시글 저장 & 불러오기")
    public void savePost_Load(){
        final String TITLE = "title-test";
        final String CONTENT = "content-test";

        //Given
        UserEntity savedUser = userRepository.save(UserEntity.builder()
                .email("abc@abc.com")
                .password("password1234")
                .nickname("nickname")
                .phone("01012345678")
                .build());

        PostEntity savedPost = postRepository.save(PostEntity.builder()
                .title(TITLE)
                .content(CONTENT)
                .user(savedUser)
                .build());

        // When
        List<PostEntity> postsList= postRepository.findAll();

        // Then
        PostEntity posts=postsList.get(0);
        assertThat(posts.getTitle()).isEqualTo(savedPost.getTitle());
        assertThat(posts.getContent()).isEqualTo(savedPost.getContent());
        assertThat(posts.getUser().getId()).isEqualTo(savedPost.getUser().getId());

    }


}

💚 BoardControllerTest

  • 게시글 생성, 수정 통신이 잘 이루어지는지에 대한 테스트 코드
  • MocMvc 사용하여 통신
package com.gdsc_teamb.servertoyproject.controller;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.gdsc_teamb.servertoyproject.domain.post.domain.HeartEntity;
import com.gdsc_teamb.servertoyproject.domain.post.domain.PostEntity;
import com.gdsc_teamb.servertoyproject.domain.repository.PostRepository;
import com.gdsc_teamb.servertoyproject.domain.user.domain.UserRepository;
import com.gdsc_teamb.servertoyproject.domain.user.domain.UserEntity;
import com.gdsc_teamb.servertoyproject.dto.boardDto.BoardDto;
import com.gdsc_teamb.servertoyproject.dto.boardDto.BoardUpdateDto;
import jakarta.transaction.Transactional;
import org.junit.jupiter.api.*;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.http.*;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.web.servlet.MockMvc;

import java.util.List;
import java.util.Map;
import java.util.Optional;

import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.test.util.AssertionErrors.fail;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;


@ExtendWith(SpringExtension.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureMockMvc
@Transactional
class BoardControllerTest {
    // 테스트에 사용할 서버의 랜덤 포트 번호를 할당
    @LocalServerPort
    private int port;
    @Autowired
    private PostRepository postRepository;
    @Autowired
    private UserRepository userRepository;
    @Autowired
    private MockMvc mockMvc;

    @Test
    @DisplayName("게시물 등록 테스트")
    public void registerPost() throws Exception{
        final String TITLE = "title-test";
        final String CONTENT = "content-test";

        //Given 등록할 게시글 생성
        UserEntity savedUser = userRepository.save(UserEntity.builder()
                .email("abc@abc.com")
                .password("password1234")
                .nickname("nickname")
                .phone("01012345678")
                .build());

        BoardDto boardDto=BoardDto.builder()
                .title(TITLE)
                .content(CONTENT)
                .user(savedUser)
                .build();

        String url="http://localhost:" + port + "/api/boards";

        // when
        mockMvc.perform(post(url)
                        .contentType(APPLICATION_JSON)
                        .content(new ObjectMapper().writeValueAsString(boardDto)))
                .andExpect(status().isOk())
                .andDo(print());

        // then
        // 저장한 데이터만 조회
        List<PostEntity> savedPosts = postRepository.findAllByUser(savedUser);
        assertThat(savedPosts).hasSize(1); // 사용자에게 속하는 게시글 중에서 검사
        PostEntity postSaved = savedPosts.get(0);

        assertThat(postSaved.getTitle()).isEqualTo(TITLE);
        assertThat(postSaved.getContent()).isEqualTo(CONTENT);
        assertThat(postSaved.getUser().getId()).isEqualTo(savedUser.getId());

    }

    @Test
    @DisplayName("게시물 수정 테스트")
    public void updatePost() throws Exception {
        // Given 등록된 게시물과 수정할 게시물
        UserEntity savedUser = userRepository.save(UserEntity.builder()
                .email("abc@abc.com")
                .password("password1234")
                .nickname("nickname")
                .phone("01012345678")
                .build());

        PostEntity savedPost = postRepository.save(PostEntity.builder()
                .title("title")
                .content("content")
                .user(savedUser)
                .build());

        Long updateId = savedPost.getId();
        String expectedTitle = "title2";
        String expectedContent = "content2";

        BoardUpdateDto boardUpdateDto= BoardUpdateDto.builder()
                .title(expectedTitle)
                .content(expectedContent)
                .build();

        String url="http://localhost:" + port + "/api/boards/"+updateId;

        // when
        mockMvc.perform(put(url)
                        .contentType(APPLICATION_JSON)
                        .content(new ObjectMapper().writeValueAsString(boardUpdateDto)))
                .andExpect(status().isOk());

        // then
        // 저장한 데이터만 조회
        List<PostEntity> savedPosts = postRepository.findAllByUser(savedUser);
        assertThat(savedPosts).hasSize(1); // 사용자에게 속하는 게시글 중에서 검사
        PostEntity postSaved = savedPosts.get(0);

        assertThat(postSaved.getTitle()).isEqualTo(expectedTitle);
        assertThat(postSaved.getContent()).isEqualTo(expectedContent);

    }

}