본문 바로가기

Programing/Spring

[Spring Boot] 간단한 CRUD 게시판 만들기

게시글 목록, 작성, 수정, 삭제만 가능한 간단한 게시판을 만들어보자.

 

1. Spring Initializr 를 이용하여 Project 생성하기

 

Spring Initializr 사이트 바로가기

 

원하는 개발 환경을 설정하여 파일로 다운로드할 수 있다. 아래 링크에서 설정한 내용을 확인할 수 있다.

설정한 환경 확인하기!!

 

 


2. 압축 해제

원하는 경로에 압축을 해제한다.

 


3. 프로젝트 Import

Package Explorer 창에서 마우스 우클릭 후 [Import] 를 선택한다.

 

Maven > Existing Maven Projects > [Next] 버튼 클릭

 

[Browse..] 버튼 클릭 > simpleboard 폴더 선택(압축 해제한 폴더를 선택하면 된다) > [폴더 선택] 버튼 클릭

 

Root Directory가 설정되고 Projects에 항목이 추가되면 [Finish] 버튼을 클릭하여 Import 한다.

 

왼쪽 상단 Package Explorer에 프로젝트가 추가되었고, 오른쪽 하단에 프로젝트 관련 빌드 중인 것을 확인할 수 있다.

 


4. Package 구성

simpleboard 프로젝트의 Package를 생성한다.

 

 


5. 도메인 매핑하기

도메인 매핑은 JPA를 사용하여 DB와 도메인 클래스를 연결시켜주는 작업이다. 도메인 클래스를 생성하여 H2 DB에 매핑하도록 한다.

 

5-1. DB에서 도메인을 활용하여 Repository까지의 데이터 흐름

※ 이번에는 User domain은 사용하지 않는다.

JPA를 활용한 H2 DB 매핑 구성도

 

5-2. Board 클래스 생성

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
package com.acma.board.domain;
 
import java.time.LocalDateTime;
 
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
 
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
 
@Getter
@Setter
@NoArgsConstructor
@Entity
@Table
public class Board {
    
    /*
     * @GeneratedValue(strategy = GenerationType.IDENTITY)
     * - 기본 키가 자동으로 할당되도록 설정하는 어노테이션이다.
    * - 기본 키 할당 전략을 선택할 수 있는데, 키 생성을 데이터베이스에 위임하는 IDENTITY 전략 사용
     */
    @Id
    @Column
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long idx;
    
    @Column
    private String title;
    
    @Column
    private String content;
    
    @Column
    private LocalDateTime createdDate;
    
    @Column
    private LocalDateTime updatedDate;
    
    @Builder
    public Board(Long idx, String title, String content, LocalDateTime createdDate, 
                    LocalDateTime, updatedDate) {
        this.idx = idx;
        this.title = title;
        this.content = content;
        this.createdDate = createdDate;
        this.updatedDate = updatedDate;
    }
}

 


6. Repository Interface 생성

1
2
3
4
5
6
7
8
package com.acma.board.repository;
 
import org.springframework.data.jpa.repository.JpaRepository;
 
import com.acma.board.domain.Board;
 
public interface BoardRepository extends JpaRepository<Board, Long> {
}

 


7. 도메인 테스트하기

- 스프링부트에서 지원하는 @DataJpaTest를 사용하여 도메인을 테스트한다.

- @DataJpaTest는 JPA에 대한 테스트를 지원하는 어노테이션으로 테스트 시 실행된 변경사항이 실제 DB에 반영되지 않는다. 이는 테스트를 수행하고 다시 테스트 이전의 데이터로 롤백하기 때문이다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package com.acma.board;
 
import java.time.LocalDateTime;
 
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.test.context.junit4.SpringRunner;
 
import com.acma.board.domain.Board;
import com.acma.board.repository.BoardRepository;
 
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
 
@RunWith(SpringRunner.class)
@DataJpaTest
public class JpaMappingTest {
    
    private final String title = "테스트";
    private final String content = "내용";
    
    @Autowired
    private BoardRepository boardRepository;
    
    @Before
    public void init() {
        boardRepository.save(Board.builder()
                .title(title)
                .content(content)
                .createdDate(LocalDateTime.now())
                .updatedDate(LocalDateTime.now()).build());
    }
    
    @Test
    public void test() {
        Board board = boardRepository.getOne((long1);
        assertThat(board.getTitle(), is(title));
        assertThat(board.getContent(), is(content));
    }
}
더보기

◈ Line 18 : @RunWith 어노테이션을 사용하면 JUnit에 내장된 러너를 사용하는 대신 어노테이션에 정의된 클래스를 호출한다. 또한 JUnit의 확장 기능을 지정하여 각 테스트 시 독립적인 애플리케이션 컨텍스트(빈의 생성과 관계 설정 같은 제어를 담당하는 IOC 객체를 빈 팩토리라 부르며 이러한 빈 팩토리를 더 확장한 개념이 애플리케이션 컨텍스트이다)를 보장한다.

 Line 19 : 스프링 부트에서 JPA 테스트를 위한 전용 어노테이션이다. 첫 설계 시 엔티티 간의 관계 설정 및 기능 테스트를 가능하게 도와준다. 테스트가 끝날 때마다 자동 롤백을 해주어 편리한 JPA 테스트가 가능하다.

 

 Line 28 : 각 테스트가 실행되기 전에 실행될 메서드를 선언한다.

 

 Line 37 : 실제 테스트가 진행될 메서드를 선언한다.

 

JUnit 테스트 성공.

 


8. BoardService 생성

8-1. BoardService Interface 생성

1
2
3
4
5
6
7
8
9
10
11
package com.acma.board.service;
 
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
 
import com.acma.board.domain.Board;
 
public interface BoardService {
    Page<Board> findBoardList(Pageable pageable);
    Board findBoardByIdx(Long idx);
}

 

8-2. BoardService Class 생성

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package com.acma.board.service;
 
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.stereotype.Service;
 
import com.acma.board.domain.Board;
import com.acma.board.repository.BoardRepository;
 
@Service
public class BoardServiceImpl implements BoardService {
 
    @Autowired
    private BoardRepository boardRepository;
    
    @Override
    public Page<Board> findBoardList(Pageable pageable) {
        pageable = PageRequest.of(pageable.getPageNumber() <= 0 ? 0 : pageable.getPageNumber() - 1,
            pageable.getPageSize());
        return boardRepository.findAll(pageable);
    }
 
    @Override
    public Board findBoardByIdx(Long idx) {
        return boardRepository.findById(idx).orElse(new Board());
    }
}
더보기

 Line 19 : pageable로 넘어온 pageNumber 객체가 0 이하일 때 0으로 초기화한다. 기본 페이지 크기인 10으로 새로운 PageRequest 객체를 만들어 페이징 처리된 게시글 리스트를 반환한다.

 

 Line 26 : board의 idx 값을 사용하여 board 객체를 반환한다.

 


9. BoardController 생성

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
package com.acma.board.controller;
 
import java.time.LocalDateTime;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Pageable;
import org.springframework.data.web.PageableDefault;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
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 com.acma.board.domain.Board;
import com.acma.board.repository.BoardRepository;
import com.acma.board.service.BoardService;
 
@Controller
@RequestMapping("/board")
public class BoardController {
 
    @Autowired
    private BoardService boardService;
    
    @Autowired
    private BoardRepository boardRepository;
    
    /*
     * 게시글 목록
     */
    @GetMapping("/list")
    public String list(@PageableDefault Pageable pageable, Model model) {
        model.addAttribute("boardList", boardService.findBoardList(pageable));
        return "/board/list";
    }
    
    /*
     * 게시글 상세 및 등록 폼 호출
     */
    @GetMapping({"""/"})
    public String board(@RequestParam(value = "idx", defaultValue = "0") Long idx, Model model) {
        model.addAttribute("board", boardService.findBoardByIdx(idx));
        return "/board/form";
    }
    
    /*
     * 게시글 생성
     */
    @PostMapping
    public ResponseEntity<?> postBoard(@RequestBody Board board) {
        board.setCreatedDate(LocalDateTime.now());
        board.setUpdatedDate(LocalDateTime.now());
        boardRepository.save(board);
        
        return new ResponseEntity<>("{}", HttpStatus.CREATED);
    }
    
    /*
     * 게시글 수정
     */
    @PutMapping("/{idx}")
    public ResponseEntity<?> putBoard(@PathVariable("idx") Long idx, @RequestBody Board board) {
        Board updateBoard = boardRepository.getOne(idx);
        updateBoard.setTitle(board.getTitle());
        updateBoard.setContent(board.getContent());
        updateBoard.setUpdatedDate(LocalDateTime.now());
        boardRepository.save(updateBoard);
        
        return new ResponseEntity<>("{}", HttpStatus.OK);
    }
    
    /*
     * 게시글 삭제
     */
    @DeleteMapping("/{idx}")
    public ResponseEntity<?> deleteBoard(@PathVariable("idx") Long idx) {
        boardRepository.deleteById(idx);
        return new ResponseEntity<>("{}", HttpStatus.OK);
    }
}
더보기

◈ Line 39 : @PageableDefault 어노테이션의 파라미터인 size, sort, direction 등을 사용하여 페이징 처리에 대한 규약을 정의할 수 있다.

 

 Line 47 : 매핑 경로를 중괄호를 사용하여 여러 개를 받는다.

 

 Line 48 : @RequestParam 어노테이션을 사용하여 idx 파라미터를 필수로 받는다. 만약 바인딩할 값이 없으면 기본 값 "0"으로 설정된다. findBoardByIdx(idx)로 조회 시 idx 값을 "0"으로 조회하면 board 값은 null 값으로 반환된다.

 


10. CommandLineRunner를 사용하여 DB에 데이터 넣기

- CommandLineRunner는 애플리케이션 구동 후 특정 코드를 실행시키고 싶을 때 직접 구현하는 인터페이스이다.

- 애플리케이션을 구동하여 목록을 볼 수 있도록 게시글 100개를 생성하는 코드를 작성한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package com.acma.board;
 
import java.time.LocalDateTime;
import java.util.stream.IntStream;
 
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
 
import com.acma.board.domain.Board;
import com.acma.board.repository.BoardRepository;
 
@SpringBootApplication
public class SimpleboardApplication {
 
    public static void main(String[] args) {
        SpringApplication.run(SimpleboardApplication.class, args);
    }
    
    @Bean
    public CommandLineRunner runner(BoardRepository boardRepository) throws Exception {
        return (args) -> {
            IntStream.rangeClosed(1100).forEach(index ->
                boardRepository.save(Board.builder()
                        .title("게시글" + index)
                        .content("내용" + index)
                        .createdDate(LocalDateTime.now())
                        .updatedDate(LocalDateTime.now()).build()));
        };
    }
}
더보기

◈ Line 22 : 스프링은 빈(Bean)으로 생성된 메서드에 파라미터로 DI(Dependency Injection, 스프링의 주요 특성 중 하나로 주로 의존 관계 주입이라고 한다. 또는 의존 관계를 주입하는게 아니라 단지 객체의 레퍼런스를 전달하여 참조시킨다는 의미로 의존 관계 설정이라고도 한다)시키는 메커니즘이 존재한다. 생성자를 통해 의존성을 주입시키는 방법과 유사하다. 이를 이용하여 CommandLineRunner를 빈으로 등록한 후 UserRepository를 주입받는다.

Line 23 : 자바 8 람다 표현식을 사용하여 깔끔하게 코드를 구현하였다.

 

 Line 24 : 페이징 처리 테스트를 위해서 Board 객체를 빌더 패턴(Builder Pattern, 객체의 생성 과정과 표현 방법을 분리하여 객체를 단계별 동일한 생성 절차로 복잡한 객체로 만드는 패턴이다)을 사용하여 생성한 후 주입받은 BoardRepository를 사용하여 Board 객체를 저장한다. 이때 IntStream의 rangeClosed를 사용하여 index 순서대로 Board 객체 100개를 생성하여 저장한다.

 


11. 게시글 리스트 기능 만들기

- 뷰를 구성하는 데 다양한 서버 사이드 템플릿 엔진을 사용할 수 있다. 

- 서버 사이드 템플릿이란 미리 정의된 HTML에 데이터를 반영하여 뷰를 만드는 작업을 서버에서 진행하고 클라이언트에 전달하는 방식을 말한다. 흔히 사용하는 JSP, 타임리프(Thymeleaf) 등이 서버 사이드 템플릿 엔진이며 스프링 부트 2.0 버전에서 지원하는 템플릿 엔진은 타임리프(Thymeleaf), 프리마커(Freemarker), 무스타치(Mustache), 그루비 템플릿(Groovy Templates) 등이 있다.

 

11-1. 뷰 페이지 작성을 위한 구조 생성

 

뷰 페이지 작성은 src/main/resources/templates 하위 폴더를 사용한다.

 

src/main/resources/templates 폴더에 하위 폴더를 생성한다.

 

11-2. Head 정보를 설정할 HTML 생성

- src/main/resources/templates/fragments/config.html

1
2
3
4
5
6
7
8
9
10
11
12
13
<!DOCTYPE html>
<html lang="ko" xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" 
        xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
    <!-- HTML Head Tag 정보 Set -->
    <head th:fragment="config">
        <!-- Meta Set -->    
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <!-- Custom styles for this template-->
        <link rel='stylesheet' th:href='@{/webjars/bootstrap/3.4.1/css/bootstrap.min.css}'>
        <!-- Page level plugin CSS-->
        <th:block th:fragment="contentsCss"></th:block>
    </head>
</html>
더보기

◈ Line 2 : th는 기존의 html을 효과적으로 대체하는 네임스페이스이다. th:test 프로퍼티와 함께 사용하면 내부에 표현된 #{...} 구문을 실제값으로 대체한다.

 Line 9 : @{...}는 타임리프의 기본 링크 표현 구문이다. server-relative URL 방식으로 동일 서버 내의 다른 컨텍스트로 연결해주는 방식으로 서버의 루트 경로를 기준으로 구문에서 경로를 탐색하여 href의 URL을 대체한다.

 

11-3. 게시판의 Header 구조를 위한 HTML 생성

- src/main/resources/templates/fragments/header.html

1
2
3
4
5
6
7
8
9
<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
    <!-- HTML Body Tag 내 Header 구성 -->
    <header th:fragment="header">
        <nav class="navbar navbar-inverse">
            <a class="navbar-brand" href="/board/list">Simple Board</a>
        </nav>
    </header>
</html>

 

11-4. 게시판에서 Javascript 사용을 위한 HTML 생성

- src/main/resources/templates/fragments/script.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!DOCTYPE html>
<html lang="ko" xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" 
        xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
    <!-- HTML Script 정보 Set -->
    <th:block th:fragment="script">
        <!-- Bootstrap/Jquery core JavaScript-->
        <script src="/webjars/jquery/3.3.1/jquery.min.js"></script>
        <script src="/webjars/bootstrap/3.4.1/js/bootstrap.min.js"></script>
        <script src="/webjars/jquery-easing/1.4.1/jquery.easing.min.js"></script>
        <!-- Page level plugin JavaScript-->
        <th:block layout:fragment="contentsScript"></th:block>
        <!-- Custom scripts for this pages-->
        <th:block layout:fragment="customScript"></th:block>
    </th:block>
</html>

 

11-5. 게시판 HTML의 기본 구조 생성

- src/main/resources/templates/layout/default.html

- src/main/resources/templates/board 패키지 내에 생성할 게시판 뷰의 기본 구조가 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!DOCTYPE html>
<html lang="ko" xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" 
        xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
 
    <head>
        <title layout:title-pattern="$LAYOUT_TITLE : $CONTENT_TITLE">게시판</title>
        <th:block th:replace="fragments/config :: config"></th:block>
    </head>
    
    <body>
        <header th:replace="fragments/header :: header"></header>
        
        <div id="wrapper" class="wrapper">
            <div layout:fragment="content">
            </div>
        </div>
        
        <th:block th:replace="fragments/script :: script"></th:block>
    </body>
    
</html>

 

11-6. 게시판 리스트 HTML 생성

- src/main/resources/templates/board/list.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org" 
        xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" 
        layout:decorator="layout/default">
 
<head>
    <title>List</title>
    
    <!-- Page level plugin CSS-->
    <th:block layout:fragment="contentsCss">
    </th:block>
</head>
 
<body>
    <!-- header를 작성하지 않아도 header가 이 위치에 구성된다. -->
 
    <div class="container" layout:fragment="content">
    
        <div class="page-header">
            <h1>게시글 목록</h1>
        </div>
        <div class="pull-right" style="width:100px;margin:10px 0;">
            <a href="/board" class="btn btn-primary btn-block">등록</a>
        </div>
        <br/>
        <br/>
        <br/>
        
        <div class="container-fluid">
            <div class="row">
                <table class="table table-hover">
                    <thead>
                        <tr class="warning">
                            <th class="col-sm-1">No</th>
                            <th class="col-sm-5">제목</th>
                            <th class="col-sm-3">작성 날짜</th>
                            <th class="col-sm-3">수정 날짜</th>
                        </tr>
                    </thead>
                    <tbody>
                        <tr th:each="board : ${boardList}">
                            <td th:text="${board.idx}"></td>
                            <td>
                                <a th:href="'/board?idx='+${board.idx}" th:text="${board.title}"></a>
                            
</td>
                            <td th:text="${#temporals.format(board.createdDate, 'yyyy-MM-dd HH:mm:dd')}">
                            </td>
                            <td th:text="${#temporals.format(board.updatedDate, 'yyyy-MM-dd HH:mm:dd')}">
                            </td>
                        </tr>
                    </tbody>
                </table>
            </div>
            
            <!-- pagination -->
            <nav class="navbar" aria-label="Page navigation" style="text-align: center;">
                <ul class="pagination" 
                    th:with="startNumber=${T(Math).floor(boardList.number/10)} * 10 + 1,
                            endNumber=(${boardList.totalPages} > ${startNumber} + 9) ? 
                                        ${startNumber} + 9 : ${boardList.totalPages}"
>
                    
                    <li th:style="${boardList.first} ? 'display:none'">
                        <a href="/board/list?page=1">&laquo;</a>
                    
</li>
                    <li th:style="${boardList.first} ? 'display:none'">
                        <a th:href="@{/board/list(page=${boardList.number})}">&lsaquo;</a>
                    </li>
                    <li th:each="page : ${#numbers.sequence(startNumber, endNumber)}"
                        th:class="(${page} == ${boardList.number} + 1) ? 'active'">
                        <a th:href="@{/board/list(page=${page})}" th:text="${page}">
                            <span class="sr-only"></span>
                        </a>
                    </li>
                    <li th:style="${boardList.last} ? 'display:none'">
                        <a th:href="@{/board/list(page=${boardList.number} + 2)}">&rsaquo;</a>
                    </li>
                    <li th:style="${boardList.last} ? 'display:none'">
                        <a th:href="@{/board/list(page=${boardList.totalPages})}">&raquo;</a>
                    </li>
                </ul>
            </nav>
        </div>
    </div>
    
    <!-- 이 위치에 Bootstrap/Jquery core JavaScript가 구성된다. -->
    
    <!-- Page level plugin JavaScript-->
    <th:block layout:fragment="contentsScript">
    </th:block>
    <!-- Custom scripts for this pages-->
    <th:block layout:fragment="customScript">
    </th:block>
</body>
</html>
더보기

◈ Line 41 : th:each는 반복 구문으로 ${boardList}에 담긴 리스트를 Board 객체로 순차 처리한다. Board 객체에 담긴 get*() 메서드를 board.*로 접근할 수 있다. board.idx, board.title과 같이 사용할 수 있는 이유는 Board 객체에 getIdx()와 getTitle() 메서드가 정의되어 있기 때문이다.

 Line 44, 45 : temporals의 format 함수를 사용하여 날짜 포맷 변환을 수행한다. 포매팅 없이 그대로 날짜를 출력하면 LocalDateTime의 기본형인 ISO 방식으로 출련된다. temporals를 사용할 수 있게 해주는 thymeleaf-extras-java8time 의존성은 spring-boot-starter-thymeleaf 스타터에 포함되어 있다.

[참조] http://www.thymeleaf.org/documentation.html

 

Line 54 : th:with 구문을 사용하여 ul 태그 안에서 사용할 변수를 정의한다. startNumber와 endNumber 변수로 페이지의 처음과 끝을 동적으로 계산하여 초기화한다. 변수 계산 로직은 기본 10페이지 단위로 처리한다.

 

Line 57, 58, 67, 70 : pageable 객체에는 편리하게도 해당 페이지가 처음인지(isFirst) 마지막인지(isLast)에 대한 데이터(불린형)를 제공한다. 이를 사용하여 이전/다음 페이지의 노출 여부를 결정한다.

 

Line 61 : 각 페이지 버튼은 th:each를 사용하여 startNumber부터 endNumber까지를 출력시킨다. pageable은 현재 페이지를 알려주는 number 객체가 0부터 시작한다. 그래서 ${boardList.number} + 1로 비교하여 현재 페이지 번호일 경우 class에 현재 페이지임을 보여주는 'active' 프로퍼티를 추가한다.

 


12. 게시판 목록 확인

 

서버 구동 후 게시글 목록을 확인한다.

 


13. 게시글 작성/상세 화면 만들기

- src/main/resources/templates/board/form.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org" 
        xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" 
        layout:decorator="layout/default">
 
<head>
    <title>Form</title>
    <th:block layout:fragment="contentsCss">
    </th:block>
</head>
 
<body>
 
    <div class="container" layout:fragment="content">
        <div class="page-header">
            <!--
                <h1 th:if="!${board?.idx}">게시글 등록</h1>
                <h1 th:if="${board?.idx}">게시글 상세</h1>
            -->
            <h1 th:text="${board?.idx} ? '게시글 상세' : '게시글 등록'"></h1>
        </div>
        <br/>
        <input id="board_idx" type="hidden" th:value="${board?.idx}" />
        <input id="board_create_date" type="hidden" th:value="${board?.createdDate}" />
        <table class="table">
            <tr>
                <th style="padding:13px 0 0 15px;">생성 날짜</th>
                <td>
                    <input type="text" class="form-control input-sm" readonly
                           th:value="${board?.createdDate} ? 
                                 ${#temporals.format(board.createdDate, 'yyyy-MM-dd HH:mm')} :
                                 ${board?.createdDate}"
 />
                </td>
            </tr>
            <tr>
                <th style="padding:13px 0 0 15px;">제목</th>
                <td>
                    <input id="board_title" type="text" class="form-control input-sm" 
                           th:value="${board?.title}" />
                </td>
            </tr>
            <tr>
                <th style="padding:13px 0 0 15px;">내용</th>
                <td>
                    <textarea id="board_content" class="form-control input-sm" 
                             maxlength="140" rows="70" style="height: 200px;" 
                            th:text="${board?.content}" ></textarea>
                    <span class="help-block"></span>
                </td>
            </tr>
        </table>
        
        <!-- 목록으로 -->
        <div class="pull-left">
            <a href="/board/list" class="btn btn-success">목록으로</a>
        </div>
        
        <!-- 저장/수정/삭제 버튼 -->
        <div class="pull-right">
            <button th:if="!${board?.idx}" type="button" class="btn btn-primary" id="insert">
                저장
            </button>
            <button th:if="${board?.idx}" type="button" class="btn btn-info" id="update">
                수정
            </button>
            <button th:if="${board?.idx}" type="button" class="btn btn-danger" id="delete">
                삭제
            </button>
        </div>
    </div>
    
    <th:block layout:fragment="contentsScript">
    </th:block>
    
    <th:block layout:fragment="customScript">
        <!-- 신규 등록 -->
        <script th:if="!${board?.idx}">
            // 저장버튼
            $("#insert").click(function() {
                var jsonData = JSON.stringify({
                    title : $("#board_title").val(),
                    content : $("#board_content").val()
                });
                $.ajax({
                    url : "http://localhost:8080/board/",
                    type : "POST",
                    data : jsonData,
                    contentType : "application/json",
                    dataType : "json",
                    success : function() {
                        alert("저장 성공!");
                        location.href = "/board/list";
                    },
                    error : function() {
                        alert("저장 실패!");
                    }
                });
            });
        </script>
        
        <!-- 수정/삭제 -->
        <script th:if="${board?.idx}">
            // 수정버튼
            $("#update").click(function() {
                var jsonData = JSON.stringify({
                    title : $("#board_title").val(),
                    content : $("#board_content").val()
                });
                $.ajax({
                    url : "http://localhost:8080/board/" + $("#board_idx").val(),
                    type : "PUT",
                    data : jsonData,
                    contentType : "application/json",
                    dataType : "json",
                    success : function() {
                        alert("수정 성공!");
                        location.href = "/board/list";
                    },
                    error : function() {
                        alert("수정 실패!");
                    }
                });
            });
            
            // 삭제버튼
            $("#delete").click(function() {
                $.ajax({
                    url : "http://localhost:8080/board/" + $("#board_idx").val(),
                    type : "DELETE",
                    success : function() {
                        alert("삭제 성공!");
                        location.href = "/board/list";
                    },
                    error : function() {
                        alert("삭제 실패!");
                    }
                });
            });
        </script>
    </th:block>
</body>
</html>

 


14. 게시글 등록/수정/삭제 확인

14-1. 게시글 등록

게시글 등록 화면

 

제목, 내용 입력 > 저장 버튼 클릭

 

게시글이 등록 된 것을 확인할 수 있다.

14-2. 게시글 수정

게시글 상세 화면

 

제목, 내용 수정 > 수정 버튼 클릭

 

게시글이 수정 된 것을 확인할 수 있다.

14-3. 게시글 삭제

삭제 버튼 클릭

 

게시글이 삭제 된 것을 확인할 수 있다.


15. application.properties 설정

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#Encoding UTF-8
spring.http.encoding.charset=UTF-8
spring.http.encoding.enabled=true
spring.http.encoding.force=true
 
#DataSource(mem : memory, file : file)
#spring.datasource.url=jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
#spring.datasource.url=jdbc:h2:mem:testdb;
spring.datasource.url=jdbc:h2:file:~/tmp/testdb;DB_CLOSE_ON_EXIT=TRUE;FILE_LOCK=NO
spring.datasource.platform=h2
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
 
#JPA
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.jpa.hibernate.ddl-auto=update
 
#H2
spring.h2.console.enabled=true
spring.h2.console.path=/console
 
#PORT
server.port=8080

 

 

 

 

# 정말 간단한 게시판을 Spring Boot, Thymeleaf 로 만들어 보았다. 자바부터 Thymeleaf 까지 처음 사용하는 api 들이 많았던 만큼... 개발은 정말 어려운 것 같다.