타임리프는 템플릿 엔진이다.
- 템플릿 엔진: 스프링 서버에서 데이터를 받아 우리가 보는 웹페이지, 즉 HTML 상에 그 데이터를 넣어 보여주는 도구이다.
<h1 text=${이름}>
<p text=${나이}>
h1태그에 ${이름}이 text 어트리뷰트로 할당되어있다.(p태그도 마찬가지)
-> 이름, 나이라는 키로 데이터를 템플릿 엔진에 넘겨주고 템플릿 엔진은 이를 받아 HTML에 값을 적용한다.
값이 달라질때마다 그때그때 화면에 반영하여 동적인 웹페이지를 만들 수 있다.{ 이름: "홍길동" 나이: 11 }
- 템플릿 엔진의 종류: JSP, 타임리프, 프리마커 등
| 표현식 | 설명 |
|---|---|
| ${...} | 변수의 값 표현식 |
| #{...} | 속성 파일 값 표현식 |
| @{...} | URL 표현식 |
| *{...} | 선택한 변수의 표현식. th:object에서 선택한 객체에 접근 |
| 표현식 | 설명 | 예제 |
|---|---|---|
| th:text | 텍스트를 표현할 때 사용 | th:text=${person.name} |
| th:each | 컬렉션을 반복할 때 사용 | th:each="person:${persons}" |
| th:if | 조건이 true인 때만 표시 | th:if="${person.age}>=20" |
| th:unless | 조건이 flase인 때만 표시 | th:unless="${person.age}>=20" |
| th:href | 이동 경로 | th:href="@{persons(id=${person.id})}" |
| th:with | 빈솟값으로 지정 | th:with="name=${person.name}" |
| th:object | 선택한 객체로 지정 | th:object="${person}" |
build.gradle 파일에 의존성을 추가한다.
dependencies { ... implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' }
@Getter
public class ArticleListViewResponse {
private final Long id;
private final String title;
private final String content;
public ArticleListViewResponse(Article article){
this.id = article.getId();
this.title = article.getTitle();
this.content = article.getContent();
}
}
뷰에게 전달할 데이터(dto): ArticleListViewResponse.java를 생성한다.
controller패키지에 BlogViewController.java를 만든다.
@RequiredArgsConstructor
@Controller
public class BlogViewController {
private final BlogService blogService;
@GetMapping("/articles")
public String getArticles(Model model){
List<ArticleListViewResponse> articles = blogService.findAll().stream()
.map(ArticleListViewResponse::new)
.toList();
model.addAttribute("articles", articles);
return "articleList";
}
}
1) addAttribute() 메소드를 사용해 모델에 값을 저장함("articles"키에 블로그 글 리스트를 저장)
2) 반환값인 "articleList"는 resource/templates/articleList.html 을 찾도록 뷰의 이름을 적은 것이다.
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset = "UTF-8">
<title>블로그 글 목록</title>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css">
</head>
<body>
<div class="p-5 mb-5 text-center</> bg-light">
<h1 class="mb-3">My Blog</h1>
<h4 class="mb-3">블로그에 오신 것을 환영합니다.</h4>
</div>
<div class="container">
<div class="row-6" th:each="item: ${articles}">
<div class="card">
<div class="card-header" th:text="${item.id}">
</div>
<div class="card-body">
<h5 class="card-title" th:text="${item.title}"></h5>
<p class="card-test" th:text="${item.content}"></p>
<a href="#" class="btn btn-primary">보러 가기</a>
</div>
</div>
</br>
</div>
</div>
</body>
</html>
1) th:each로 "articles"키에 담긴 데이터 개수만큼 반복한다.
2) th:text는 반복 대상 객체의 id, "text"를 출력한다.
엔티티에 생성 시간과 수정시간을 추가하여 글이 언제 생성되었는지 뷰에서 확인한다.
@EntityListeners(AuditingEntityListener.class)
...
public class Article {
...
@CreatedDate
@Column(name = "created_at")
private LocalDateTime createdAt;
@LastModifiedDate
@Column(name = "updated_at")
private LocalDateTime updatedAt;
}
1) @CreatedDate: 엔티티가 생성될 때 생성 시간을 created_at 컬럼에 저장
2) @LastModifiedDate: 엔티티가 구정될 때 마지막으로 수정된 시간을 updated_at 컬럼에 저장
3) @EntityListeners(AuditingEntityListener.class): 엔티티의 생성 및 수정 시간을 자동으로 감시하고 기록
data.sql은 created_at과 updated_at을 바꾸지 않는다.
최초 파일 생성에도 이 값을 수정하도록 data.sql을 수정한다.
INSERT INTO article (title, content, created_at, updated_at) VALUES ('제목 1', '내용 1', NOW(), NOW())
INSERT INTO article (title, content, created_at, updated_at) VALUES ('제목 2', '내용 2', NOW(), NOW())
INSERT INTO article (title, content, created_at, updated_at) VALUES ('제목 3', '내용 3', NOW(), NOW())
@EnableJpaAuditing 추가 : created_at, updated_at 자동으로 업데이트
@EnableJpaAuditing
@SpringBootApplication
public class blogApplication {
public static void main(String [] args){
SpringApplication.run(blogApplication.class, args);
}
}
dto에서 ArticleViewResponse.java 생성
@NoArgsConstructor
@Getter
public class ArticleViewResponse {
private Long id;
private String title;
private String content;
private LocalDateTime createdAt;
public ArticleViewResponse(Article article){
this.id = article.getId();
this.title = article.getTitle();
this.content = article.getContent();
this.createdAt = article.getCreatedAt();
}
}
블로그 글 반환할 메소드 추가
@GetMapping("/articles/{id}")
public String getArticle(@PathVariable Long id, Model model){
Article article = blogService.findById(id);
model.addAttribute("article", new ArticleViewResponse(article));
return "article";
}
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset = "UTF-8">
<title>블로그 글</title>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css">
</head>
<body>
<div class="p-5 mb-5 text-center</> bg-light">
<h1 class="mb-3">My Blog</h1>
<h4 class="mb-3">블로그에 오신 것을 환영합니다.</h4>
</div>
<div class="container mt-5">
<div class="row">
<div class="col-lg-8">
<article>
<header class="mb-4">
<h1 class="fw-bolder mb-1" th:text="${article.title}"></h1>
<div class = "text-muted fst-italic mb-2" th:text="|Posted on ${#temporals.format(article.createdAt, 'yyy-MM-dd HH:mm')}|"></div>
</header>
<section class="mb-5">
<p class="fs-5 mb-4" th:text="${article.content}"></p>
</section>
<button type="button" class="btn btn-primary btn-sm">수정</button>
<button type="button" class="btn btn-secondary btn-sm">삭제</button>
</article>
</div>
</div>
</div>
</body>
</html>
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset = "UTF-8">
<title>블로그 글 목록</title>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css">
</head>
<body>
...
<div class="card-body">
<h5 class="card-title" th:text="${item.title}"></h5>
<p class="card-test" th:text="${item.content}"></p>
<a th:href="@{/articles/{id}(id=${item.id})}" class="btn btn-primary">보러 가기</a>
</div>
</div>
</br>
</div>
</div>
</body>
</html>
main/resources/static/js 에 article.js생성
const deleteButton = document.getElementById('delete-btn');
if (deleteButton) {
deleteButton.addEventListener('click', event => {
let id = document.getElementById('article-id').value;
fetch(`/api/articles/${id}`, {
method: 'DELETE'
})
.then(()=>{
alert('삭제가 완료되었습니다.');
location.replace('/articles');
});
});
}
HTML에서 id를 delete-btn으로 설정한 엘리멘트를 찾아 그 엘리멘트에서 클릭 이벤트가 발생하면 fetch()를 통해 /api/articles/DELETE 요청을 보내는 역할을 한다.
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
...
<div class="container mt-5">
<div class="row">
<div class="col-lg-8">
<article>
<!---블로그 글 ID 추가-->
<input type="hidden" id="article-id" th:value="${article.id}">
<header class="mb-4">
<h1 class="fw-bolder mb-1" th:text="${article.title}"></h1>
<div class = "text-muted fst-italic mb-2" th:text="|Posted on ${#temporals.format(article.createdAt, 'yyy-MM-dd HH:mm')}|"></div>
</header>
<section class="mb-5">
<p class="fs-5 mb-4" th:text="${article.content}"></p>
</section>
<button type="button" class="btn btn-primary btn-sm">수정</button>
<!--삭제버튼에 id 추가-->
<button type="button" id="delete-btn" class="btn btn-secondary btn-sm">삭제</button>
</article>
</div>
</div>
</div>
<script src="/js/article.js"></script> <!---article.js 추가-->
</body>
</html>
@GetMapping("/new-article")
public String newArticle(@RequestParam(required = false) Long id, Model model){
if(id==null){
model.addAttribute("article", new ArticleViewResponse());
} else{
Article article = blogService.findById(id);
model.addAttribute("article", new ArticleViewResponse(article));
}
return "newArticle";
}
1) id가 없는 경우: 기본 생성자를 이용해 ArticleViewResponse 객체를 만든다.
2) id가 있는 경우: 기존값을 가져오는 findById()메소드를 호출한다.
<head>
<meta charset = "UTF-8">
<title>블로그 글</title>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css">
</head>
<body>
<div class="p-5 mb-5 text-center</> bg-light">
<h1 class="mb-3">My Blog</h1>
<h4 class="mb-3">블로그에 오신 것을 환영합니다.</h4>
</div>
<div class="container mt-5">
<div class="row">
<div class="col-lg-8">
<article>
<input type="hidden" id="article-id" th:value="${article.id}">
<header class="mb-4">
<input type = "text" class="form-control" placeholder="제목" id="title" th:value="${article.title}"></input>
</header>
<section class="mb-5">
<textarea class = 'form-control h-25' rows="10" placeholder="내용" id="content" th:text="${article.content}"></textarea>
</section>
<button th:if="${article.id} != null" type="button" id="modify-btn" class="btn btn-primary btn-sm">수정</button>
<button th:if="${article.id} == null" type="button" id="create-btn" class="btn btn-primary btn-sm">등록</button>
</article>
</div>
</div>
</div>
<script src="/js/article.js"></script>
</body>
</html>
실제 생성, 수정을 위한 api 구현
const modifyButton = document.getElementById('modify-btn');
if(modifyButton){
modifyButton.addEventListener('click', event => {
let params = new URLSearchParams(location.search);
let id = params.get('id');
fetch(`/api/articles/${id}`, {
method: 'PUT',
headers:{
"Content-Type": "application/json",
},
body: JSON.stringify({
title: document.getElementById('title').value,
content: document.getElementById('content').value
})
})
.then(() => {
alert('수정이 완료되었습니다.');
location.replace(`/articles/${id}`);
})
})
}
수정 버튼에 id값과 클릭이벤트를 추가한다.
<button type="button" id="modify-btn" th:onclick="|location.href='@{/new-article?id={articleId}(articleId=${article.id})}'|"
class="btn btn-primary btn-sm">수정</button>
<button type="button" id="delete-btn" class="btn btn-secondary btn-sm">삭제</button>
글생성 코드 추가
const createButton = document.getElementById("create-btn");
if(createButton){
createButton.addEventListener("click", (event)=>{
fetch("/api/articles", {
method: "POST",
headers:{
"Content-Type": "application/json",
},
body: JSON.stringify({
title: document.getElementById("title").value,
content:document.getElementById("content").value,
}),
}).then(() => {
alert("등록 완료되었습니다.");
location.replace("/articles");
});
});
}
id가 create-btn인 버튼을 추가
<div class="container">
<button type="button" id="create-btn"
th:onclick="|location.href='@{/new-article}'|"
class="btn btn-secondary btn-sm mb-3">글등록</button>
...
</div>