이번에는 Thymeleaf
를 이용하여 SpringBoot 프로젝트 실습한 내용을 기록해봅니다.
페이징 처리하는데 Tymeleaf는 어떻게 처리하는지를 중점으로 정리해봤습니다.
Gradle
, MySQL
resources/templates
바로 아래에 만들어서 사용한다.(설정가능)@Controller
로 사용한다.spring.thymeleaf:
를 cache: false
해주어야 한다.build.gradle
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
// thymeleaf 설정
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.thymeleaf.extras:thymeleaf-extras-java8time'
compileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
runtimeOnly 'com.h2database:h2'
runtimeOnly 'mysql:mysql-connector-java'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
application.yml
server:
port: 8090
## cache가 되어 있으면 제대로 안나오는 경우가 있어 false 처리
spring:
thymeleaf:
cache: false
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/{database}
username: root
password: {root password}
jpa:
open-in-view: true
hibernate:
ddl-auto: update
naming:
physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
use-new-id-generator-mappings: false
show-sql: true
properties:
hibernate.format_sql: true
dialect: org.hibernate.dialect.MySQL5InnoDBDialect
logging:
level:
org.hibernate.SQL: debug
Board
@Entity
@Getter
@Setter
@ToString
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "board")
public class Board {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
private String content;
}
BoardRepository
public interface BoardRepository extends JpaRepository<Board, Long> {
// 검색 키워드
Page<Board> findByTitleContaining(String searchKeyword, Pageable pageable);
}
BoardService
@Service
@RequiredArgsConstructor
public class BoardService {
private final BoardRepository boardRepository;
// 글 작성 처리
public void write(Board board) {
boardRepository.save(board);
}
// 게시글 리스트 처리
public Page<Board> list(Pageable pageable) {
return boardRepository.findAll(pageable);
}
// 특정 게시글 상세보기
public Board view(Long id) {
return boardRepository.findById(id)
.orElseThrow(() -> new RuntimeException("없는 id입니다."));
}
// 특정 게시글 삭제
public void deleteById(Long id) {
boardRepository.deleteById(id);
}
public Page<Board> searchList(String searchKeyword, Pageable pageable) {
return boardRepository.findByTitleContaining(searchKeyword, pageable);
}
}
BoardController
// 주의! : Pageble -> import org.springframework.data.domain.Pageable;
@Slf4j
@Controller
@RequestMapping("/board")
@RequiredArgsConstructor
public class BoardController {
private final BoardService boardService;
@GetMapping("/write")
public String write() {
return "board/write";
}
@PostMapping("/writedo")
public String writedo(Board board, Model model) {
boardService.write(board);
model.addAttribute("message", "글 작성이 완료되었습니다.");
model.addAttribute("searchUrl", "/board/list");
return "board/message";
}
@GetMapping("/list")
public String list(Model model,
@PageableDefault(page = 0, size = 10, sort = "id", direction = Sort.Direction.DESC) Pageable pageable,
String searchKeyword
) {
Page<Board> list = null;
if (searchKeyword == null) {
list = boardService.list(pageable);
} else {
list = boardService.searchList(searchKeyword, pageable);
}
int nowPage = list.getPageable().getPageNumber() + 1;
int startPage = Math.max(nowPage - 4, 1);
int endPage = Math.min(nowPage + 5, list.getTotalPages());
model.addAttribute("list", list);
model.addAttribute("nowPage", nowPage);
model.addAttribute("startPage", startPage);
model.addAttribute("endPage", endPage);
return "board/list";
}
@GetMapping("/view/{id}")
public String view(Model model, @PathVariable("id") Long id) {
model.addAttribute("board", boardService.view(id));
return "board/view";
}
@GetMapping("/delete/{id}")
public String delete(@PathVariable("id") Long id) {
boardService.deleteById(id);
return "redirect:/board/list";
}
@GetMapping("/modify/{id}")
public String modify(@PathVariable("id") Long id,
Model model) {
model.addAttribute("board", boardService.view(id));
return "board/modify";
}
@PostMapping("/update/{id}")
public String update(@PathVariable("id") Long id, Board board) {
Board boardTemp = boardService.view(id);
boardTemp.setTitle(board.getTitle());
boardTemp.setContent(board.getContent());
boardService.write(boardTemp);
return "redirect:/board/list";
}
}
게시판 dumy 데이터는 TestCode로 넣었습니다.
@SpringBootTest
class BoardRepositoryTest {
@Autowired
BoardRepository boardRepository;
@BeforeEach
void beforeEach() {
boardRepository.deleteAll();
}
// Long id도 초기화 -> application.yml 내 ddl-auto: create
@DisplayName("add 테스트")
@Test
public void addTest() {
IntStream.rangeClosed(1,140).forEach(i -> {
Board board = Board.builder()
.title("board_dsg_title" + i)
.content("board_dsg_content" + i)
.build();
boardRepository.save(board);
});
}
}
board/list.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>게시물 리스트</title>
</head>
<style>
.layout {
width: 500px;
margin: 0 auto;
margin-top: 40px;
}
.layout input {
width: 100%;
box-sizing: border-box;
}
.layout textarea {
width: 100%;
margin-top: 10px;
min-height: 300px;
}
</style>
<body>
<div class="layout">
<table>
<thead>
<tr>
<th>글번호</th>
<th>제목</th>
</tr>
</thead>
<tbody>
<tr th:each="board : ${list}">
<td th:text="${board.id}"></td>
<td>
<a th:text="${board.title}" th:href="@{/board/view/{id}(id=${board.id})}"></a>
</td>
</tr>
</tbody>
</table>
<th:block th:each="page : ${#numbers.sequence(startPage, endPage)}">
<a th:if="${page != nowPage}" th:href="@{/board/list(page = ${page - 1}, searchKeyword = ${param.searchKeyword})}" th:text="${page}"></a>
<strong th:if="${page == nowPage}" th:text="${page}" style="color: red"></strong>
</th:block>
<form th:action="@{/board/list}" method="get">
<input style="width: 130px;" type="text" name="searchKeyword">
<button type="submit">검색</button>
</form>
</div>
</body>
</html>
board/write.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>게시물 작성폼</title>
</head>
<style>
.layout {
width: 500px;
margin: 0 auto;
}
.layout input {
width: 100%;
box-sizing: border-box;
}
.layout textarea {
width: 100%;
margin-top: 10px;
min-height: 300px;
}
</style>
<body>
<div class="layout">
<form action="/board/writedo" method="post">
<input name="title" type="text"> <br>
<textarea name="content"></textarea> <br>
<button type="submit">작성</button>
</form>
</div>
</body>
</html>
board/view.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>게시물 상세 페이지</title>
</head>
<style>
.layout {
width: 500px;
margin: 0 auto;
margin-top: 40px;
}
.layout input {
width: 100%;
box-sizing: border-box;
}
.layout textarea {
width: 100%;
margin-top: 10px;
min-height: 300px;
}
</style>
<body>
<h1 th:text="${board.title}">제목입니다.</h1>
<p th:text="${board.content}">내용이 들어간 부분입니다.</p>
<a th:href="@{/board/delete/{id}(id=${board.id})}">글 삭제</a>
<a th:href="@{/board/modify/{id}(id=${board.id})}">글 수정</a>
</body>
</html>
board/modfiy.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>게시물 수정폼</title>
</head>
<style>
.layout {
width: 500px;
margin: 0 auto;
}
.layout input {
width: 100%;
box-sizing: border-box;
}
.layout textarea {
width: 100%;
margin-top: 10px;
min-height: 300px;
}
</style>
<body>
<div class="layout">
<form th:action="@{/board/update/{id}(id = ${board.id})}" method="post">
<input name="title" type="text" th:value="${board.title}"> <br>
<textarea name="content" th:text="${board.content}"></textarea> <br>
<button type="submit">수정</button>
</form>
</div>
</body>
</html>
board/message.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>message</title>
</head>
<script th:inline="javascript">
var message = [[${message}]];
alert(message);
location.replace([[${searchUrl}]]);
</script>
<body>
</body>
</html>
소스출처 : https://github.com/mooh2jj/springboot-board-example.git