스프링 부트 게시판 프로젝트 - 9 | 게시글 등록, 조회, 상세 조회 기능 개발

seren-dev·2022년 8월 29일
0

홈 화면 수정



게시글 등록 및 조회 기능 개발

Board 엔티티 수정

registerDate 타입을 LocalDateTime으로 하면 게시글 조회 화면에서
년-월-일T시:분:초 라고 나타나기 때문에 DateTimeFormatter를 사용해서 registerDate를 String 타입으로 저장한다.

package hello.board.entity;

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

import javax.persistence.*;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Board {

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

    private String title;
    private String content;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id")
    private User user;

    private String registerDate;

    @Builder
    public Board(String title, String content, User user, LocalDateTime registerDate) {
        this.title = title;
        this.content = content;
        this.user = user;
        this.registerDate = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").format(registerDate);
    }

    //==생성 메서드==//
    public static Board createBoard(String title, String content, User user) {
        return Board.builder()
                .title(title).content(content).user(user)
                .registerDate(LocalDateTime.now())
                .build();
    }

    //==비즈니스 메서드==//
    public void update(String title, String content) {
        this.title = title;
        this.content = content;
    }
}
  • 생성자 메서드에 LocalDateTime을 보내면, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").format(registerDate);를 통해 포맷한 문자열을 registerDate에 저장한다.

ddl도 수정한다.

create table board (
       board_id bigint not null auto_increment,
        title varchar(255),
        content text,
        user_id bigint,
        register_date varchar(20),
        primary key (board_id),
        constraint fk_user_board
        foreign key (user_id)
        references users (user_id) on update cascade
    ) engine=InnoDB default charset=utf8mb4;
  • register_date varchar(20)

게시글 등록용 폼 객체

PostForm

package hello.board.controller.board;

import lombok.Getter;
import lombok.Setter;

import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;

@Getter @Setter
public class PostForm {

    @NotBlank(message = "제목을 입력해주세요.")
    private String title;

    private String content;
}

게시판 컨트롤러

BoardController

package hello.board.controller.board;

import hello.board.controller.SessionConst;
import hello.board.entity.Board;
import hello.board.entity.User;
import hello.board.service.BoardService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;

import javax.validation.Valid;

@Controller
@Slf4j
@RequiredArgsConstructor
@RequestMapping("/board")
public class BoardController {

    private final BoardService boardService;

    @GetMapping
    public String postList(Model model) {
        model.addAttribute("list", boardService.findAll());
        return "board/postList";
    }

    @GetMapping("/{postId}")
    public String postView(@PathVariable Long postId, Model model) {
        log.info("postView");

        Board post = boardService.findOne(postId).orElseThrow();
        model.addAttribute("post", post);

        return "board/post";
    }

    @GetMapping("/register")
    public String registerForm(@ModelAttribute PostForm postForm) {
        return "board/registerForm";
    }

    @PostMapping("/register")
    public String register(@Valid @ModelAttribute PostForm postForm, BindingResult bindingResult, @SessionAttribute(name = SessionConst.LOGIN_USER, required = false) User loginUser) {

        if (bindingResult.hasErrors()) {
            log.info("errors = {}", bindingResult);
            return "board/registerForm";
        }

        boardService.register(postForm.getTitle(), postForm.getContent(), loginUser.getId());

        return "redirect:/board";
    }
}
  • postList(): 게시글 전체 조회
    • boardService.findAll()의 반환 값인 List<Board>model에 담아서 뷰 템플릿으로 전달한다.
  • postView(): 게시글 상세 조회
    • URL로 받은 postId로 게시글을 찾아 model에 담아서 뷰 템플릿에 전달한다.
  • registerForm(): 게시글 등록 폼 화면
  • register(): 게시글 등록
    • @Valid 어노테이션으로 PostForm 객체를 검증한다.
    • 검증에 실패하면, 에러 메시지와 함께 게시글 등록 폼 화면을 다시 보여준다.
    • @SessionAttribute로 현재 로그인한 회원(loginUser)의 정보를 얻어 id 값을 찾고, boardService.register()를 호출한다.

문제점

게시글의 내용(content)은 아무것도 입력하지 않아도 된다. 하지만, 내용에 아무것도 입력하지 않으면
BoardControllerregister() 메서드의
boardService.register(postForm.getTitle(), postForm.getContent(), loginUser.getId()); 에서 NPE가 발생한다.

해결

PostForm@NotNull 어노테이션을 추가한다.

package hello.board.controller.board;

import lombok.Getter;
import lombok.Setter;

import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;

@Getter @Setter
public class PostForm {

    @NotBlank(message = "제목을 입력해주세요.")
    private String title;

    @NotNull
    private String content;
}

게시글 조회 뷰 템플릿

board/postList.html

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="utf-8">
    <link th:href="@{/css/bootstrap.min.css}"
          href="../css/bootstrap.min.css" rel="stylesheet">
</head>
<body>

    <div id="wrapper">
        <div class="container">
            <div class="col-md-12">
                <div class="py-5 text-center">
                    <h2>게시판</h2>
                </div>
                <button class="btn btn-dark"
                        th:onclick="|location.href='@{/}'|" type="button">
                    홈 화면
                </button>

                <button class="btn btn-primary"
                        th:onclick="|location.href='@{/board/register}'|" type="button">
                    게시글 작성
                </button>

                <hr class="my-4">

                <table class="table">
                    <thead>
                    <tr>
                        <th width="10%">게시글번호</th>
                        <th width="">제목</th>
                        <th width="20%">작성자</th>
                        <th width="20%">작성일</th>
                    </tr>
                    </thead>
                    <tbody>
                    <tr th:each="post : ${list}">
                        <td>
                            <a th:href="@{/board/{postId}(postId=${post.id})}"
                            th:text="${post.id}">게시글 번호</a>
                        </td>
                        <td>
                            <a th:href="@{/board/{postId}(postId=${post.id})}"
                            th:text="${post.title}">제목</a>
                        </td>
                        <td th:text="${post.user.loginId}">작성자</td>
                        <td th:text="${post.registerDate}">작성일</td>
                    </tr>
                    </tbody>
                </table>
                <hr class="my-4">

            </div>
        </div>
    </div>

</body>

게시글 등록 폼 뷰 템플릿

board/registerForm.html

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="utf-8">
    <link th:href="@{/css/bootstrap.min.css}"
          href="../css/bootstrap.min.css" rel="stylesheet">
    <style>
    .field-error {
    border-color: #dc3545;
    color: #dc3545;
    }
    </style>
</head>
<body>
<div class="container">
    <div class="py-5 text-center">
        <h2>게시글 등록</h2>
    </div>
    <form th:action th:object="${postForm}" method="post">
        <div>
            <label for="title">제목</label>
            <input type="text" id="title" th:field="*{title}" class="form-control"
                   th:errorclass="field-error">
            <div class="field-error" th:errors="*{title}" />
        </div>
        <div class="mb-3">
            <label for="content">내용</label>
            <textarea class="form-control" rows="5"
                      id="content" name="content" th:value="*{content}"></textarea>
        </div>
        <hr class="my-4">

        <button class="btn btn-primary btn-lg" type="submit">
            작성
        </button>
        <button class="btn btn-secondary btn-lg"
                th:onclick="|location.href='@{/board}'|"
                type="button">
            취소
        </button>

    </form>

</div>
</body>
  • 내용(content)을 적는 칸에는 input 태그 대신 textarea 태그를 사용하여, 여러 줄의 텍스트를 입력할 수 있는 입력 칸을 만든다.

게시글 상세 조회 뷰 템플릿

board/post.html

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="utf-8">
    <link th:href="@{/css/bootstrap.min.css}"
          href="../css/bootstrap.min.css" rel="stylesheet">
    <style>
    .container{
        max-width: 700px;
    }
    .field-error {
    border-color: #dc3545;
    color: #dc3545;
    }
    </style>
</head>
<body>
<div class="container">
    <div class="py-5 text-center">
        <h2>게시글</h2>
    </div>

    <button class="btn btn-dark pull-right"
            onclick="location.href='board/postList.html'"
            th:onclick="|location.href='@{/board}'|"
            type="button">목록으로
    </button>

    <div>
        <label for="postId">게시판번호</label>
        <input type="text" id="postId" name="postId" class="form-control"
               value="1" th:value="${post.id}" readonly>
    </div>

    <div>
        <label for="postTitle">제목</label>
        <input type="text" id="postTitle" name="postTitle" class="form-control"
               value="제목" th:value="${post.title}" readonly>
    </div>
    <div>
        <label for="postContent">내용</label>
        <textarea class="form-control" rows="5"
                  id="postContent" name="postContent"
                  th:value="${post.content}" th:text="${post.content}" readonly></textarea>
    </div>
    <div>
        <label for="writer">작성자</label>
        <input type="text" id="writer" name="writer" class="form-control"
               value="작성자" th:value="${post.user.loginId}" readonly>
    </div>
    <div>
        <label for="registerDate">작성일</label>
        <input type="text" id="registerDate" name="registerDate" class="form-control"
               value="작성자" th:value="${post.registerDate}" readonly>
    </div>
    <hr class="my-4">

    <button class="btn btn-primary"
            th:onclick="|location.href='@{/board/{postId}/edit(postId=${post.id})}'|"
            type="button">수정</button>

    <button class="btn btn-danger"
            th:onclick="|location.href='@{/board/{postId}/delete(postId=${post.id})}'|"
            type="button">삭제</button>


</div> <!-- /container -->

</body>
  • 내용(content)을 적는 칸에는 input 태그 대신 textarea 태그를 사용하여, 여러 줄의 텍스트를 입력할 수 있는 입력 칸을 만든다.
    • 주의할 점: textarea 태그는 th:text를 사용해야지 넘겨받은 변수내용을 표기할 수 있다. th:value만 사용한다면 값이 표기되지 않는다.
  • input 태그와 textarea 태그의 속성으로 readonly를 사용하여 수정이 불가능하도록 한다.

게시글 등록 화면

게시글 조회 화면

게시글 상세 조회 화면

0개의 댓글