<이전 글 참고>
블로그 글 작성 API 구현
글 목록 조회, 전체 조회, 수정, 삭제 API 구현
1. 블로그 글 목록 뷰 구현하기
1-1. 컨트롤러 메서드
요청을 받아 사용자에게 뷰를 보여주려면 뷰 컨트롤러가 필요하다.
앞서 다룬 포스팅은 API를 만들기 위해 컨트롤러 메서드가 데이터를 직렬화한 JSON 문자열을 반환했지만,
뷰 컨트롤러 메서드는 뷰의 이름을 반환하고, 모델 객체에 값을 담는다.
dto / ArticleListViewResponse.java
@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();
}
}
위와 같이 뷰에게 데이터를 전달하기 위한 객체를 생성한다.
controller / BlogViewControllerj.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"; // articleList.html 라는 뷰 조회
}
}
- addAttribute() 메서드
"articles"키에 블로그 글들을, 즉 리스트를 모델에 저장한다. - return "articleList"
resource/templated/articleList.html을 찾도록 뷰의 이름을 적어준다.
1-2. HTML 뷰
resources/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}"> <!-- article 개수만큼 반복 -->
<div class="card">
<div class="card-header" th:text="${item.id}"> <!-- item의 id 출력 -->
</div>
<div class="card-body">
<h5 class="card-title" th:text="${item.title}"></h5>
<p class="card-text" th:text="${item.content}"></p>
</div>
</div>
<br>
</div>
</div>
</body>
모델에 전달한 블로그 글 리스트 개수만큼 반복해 글 정보를 보여주도록 코드를 작성한다.
- th:each
"articles"키에 담긴 데이터의 개수만큼 반복한다 - th:text
반복 대상 객체의 id, "text"를 출력한다.
1-3. 테스트하기
http://localhost:8080/articles
2. 블로그 글 뷰 구현하기
블로그 화면상의 [보러 가기] 버튼을 누르면 블로그 글이 보이도록 블로그 글 뷰를 구현할 것이다.
2-1. 엔티티에 생성, 수정 시간 추가하기
Article.java
@Entity // 엔티티로 지정
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Article {
--- 생략 ---
@CreatedDate // 엔티티가 생성될 때 생성 시간 저장
@Column(name = "created_at")
private LocalDateTime createdAt;
@LastModifiedDate // 엔티티가 수정될 때 수정 시간 저장
@Column(name = "updated_at")
private LocalDateTime updatedAt;
--- 생략 ---
}
엔티티에 생성 시간과 수정 시간을 추가해 글이 언제 생성되었는지 뷰에서 확인할 것이다.
- @CreatedDate
해당 애너테이션을 사용하면 엔티티가 생성될 때 생성 시간을 created_at 컬럼에 저장한다. - @LastModifiedDate
해당 애너테이션을 사용하면 엔티티가 수정될 때 마지막으로 수정된 시간을 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());
엔티티를 생성하면 생성 시간과 수정 시간이 자동으로 저장된다.
하지만 스프링 부트 서버를 실행할 때마다 SQL문으로 데이터를 넣는 data.sql 파일은 created_at과 updated_at을 바꾸지 않는다.
최조 파일 생성에도 이 값을 수정하도록 data.sql 파일을 위와 같이 수정한다.
SpringBootDeveloperApplication.java
@EnableJpaAuditing // created_at, updated_at 자동 업데이트
@SpringBootApplication
public class SpringBootDeveloperApplication {
public static void main(String[] args){
SpringApplication.run(SpringBootDeveloperApplication.class, args);
}
}
서버 실행 클래스에서
@EnableJpaAuditing 애너테이션을 추가하여 created_at와 updated_at을 자동으로 업데이트할 수 있도록 한다.
2-2. 컨트롤러 메서드
dto / ArticleViewResponse.java
뷰에서 사용할 DTO 구현
@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();
}
}
BlogViewController.java
블로그 글을 반환할 컨트롤러 메서드를 작성한다.
@RequiredArgsConstructor
@Controller
public class BlogViewController {
--- 생략 ---
@GetMapping("/articles/{id}")
public String getArticle(@PathVariable Long id, Model model){
Article article = blogService.findById(id);
model.addAttribute("article", new ArticleViewResponse(article));
return "article";
}
}
- getArticle() 메서드
인자 id에 URL로 넘어온 값을 findById() 메서드로 넘겨 글을 조회한다.
화면에서 사용할 모델에 데이터를 저장한 다음, 보여줄 화면의 템플릿 이름을 반환한다.
2-3. HTML 뷰 구현
article.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 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">
<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, 'yyyy-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" 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>
</article>
</div>
</div>
</div>
</body>
- ${#temporals.format()}
날짜 형식을 yyyy-MM-dd HH:mm으로 포매팅한다.
포매팅한 날짜 형식을 | | 기호와 함께 Posted on이라는 텍스트와 붙인다.
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}"> <!-- article 개수만큼 반복 -->
<div class="card">
<div class="card-header" th:text="${item.id}"> <!-- item의 id 출력 -->
</div>
<div class="card-body">
<h5 class="card-title" th:text="${item.title}"></h5>
<p class="card-text" th:text="${item.content}"></p>
<!-- 여기 수정 -->
<a th:href="@{/articles/{id}(id=${item.id})}" class="btn btn-primary">보러가기</a>
</div>
</div>
<br>
</div>
</div>
</body>
href 속성을 th:href 속성으로 변경하고 URL 표현식 @{...}을 사용해
[보러 가기]를 눌렀을 때 주소창의 값을 /articles/{item.id}로 변경해 글 상세 화면으로 이동한다.
2-4. 실행 테스트
http://localhost:8080/articles
보러 가기 버튼을 누르면
/article/1로 이동하며
해당 게시글로 이동하는 것을 확인할 수 있다.
3. 삭제/수정/생성 기능
3-1. 삭제 기능
static/js/articles.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 요청을 보내는 역할을 한다.
3-2. 수정 / 생성 기능
이미 생성한 글을 수정할 때
- 사용자
/new-article?id=123 (id가 123인 글 수정)
글을 생성할 떄는 URL에 별도 쿼리 파라미터가 없는 대신 수정할 때는 id를 쿼리 파라미터에 추가한다.
쿼리 파라미터: '?'로 시작하는 키 값으로 이루어진 문자열이며 '&'으로 구분한다
2. 뷰 컨트롤러
쿼리 파라미터의 id 여부에 따라 [수정]과 [생성] 중 적절한 버튼을 보여준다.
123 id를 가진 엔티티 조회 후 모델에 추가
3. 뷰
화면을 보여준다.
BlogViewController.java
@RequiredArgsConstructor
@Controller
public class BlogViewController {
--- 생략 ---
@GetMapping("/new-article")
// id 키를 가진 쿼리 파라미터의 값을 id 변수에 매핑(id는 없을 수도 있음)
public String newArticle(@RequestParam(required = false) Long id, Model model){
if(id == null){
model.addAttribute("article", new ArticleViewResponse());
}else{ // id가 없으면 수정
Article article = blogService.findById(id);
model.addAttribute("article", new ArticleViewResponse(article));
}
return "newArticle";
}
}
- @RequestParam(required = false)
쿼리 파라미터로 넘어온 id값은 new Article() 메서드의 Long 타입 id 인자에 매핑한다.
이 값은 앞서 언급했듯이 없을 수도 있다. - id 가 있으면 수정 없으면 생성이다.
- id 가 없는 경우 - 생성
기본 생성자를 이용해 ArticleViewResponse 객체를 만든다. - id 가 있는 경우 - 수정
기존 값을 가져오는 findById() 메서드를 호출한다.
- id 가 없는 경우 - 생성
newArticle.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 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}">
</header>
<section class="mb-5">
<textarea class="form-control h-25" rows="10" placeholder="내용" id="content" th:text="${article.content}"></textarea>
</section>
<!-- id가 있을 때는 [수정] 버튼을, 없을 때는 [등록] 버튼이 보이게 함-->
<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>
- 수정할 때는 id가 필요하므로 input 엘리먼트의 tpye을 hidden으로 설정해 엘리먼트를 숨긴다.
- th:value
글의 id를 저장한다. - th:if
id가 있을 때 [수정] 버튼, 없을 때 [등록] 버튼이 나타나도록 한다.
article.js
실제 수정, 생성 기능을 위한 API를 구현한다.
--- 생략 ---
// 수정 기능
.. id가 modify-btn인 엘리먼트 조회
const modifyButton = document.getElementById('modify-btn');
if (modifyButton) {
// 클릭 이벤트가 감지되면 수정 API 요청
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}`);
});
});
}
// 생성 기능
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가 modify-btn인 엘리먼트를 찾고,
그 엘리먼트에서 클릭 이벤트가 발생하면 id가 title, content인 엘리먼트의 값을 가져온다. - fetch() 메서드
수정 API로 /api/articles/PUT 요청을 보낸다.
요청을 보낼 떄는 headers에 요청 형식을 지정하고, body에 HTML에 입력한 데이터를 JSON 형식으로 바꿔 보낸다.
- id가 modify-btn인 엘리먼트를 찾고,
- 생성 기능
- id가 create-btn인 엘리먼트를 찾고,
그 엘리먼트에서 클릭 이벤트가 발생하면 id가 title, content인 엘리먼트의 값을 가져온다. - fetch() 메서드
생성 API로 /api/articles/POST 요청을 보내준다.
- id가 create-btn인 엘리먼트를 찾고,
3-3. 실행 테스트
수정 버튼을 누르고
위와 같이 수정하면
삭제 기능을 누르면
삭제가 이루어지고,
마지막으로 글 등록 버튼을 눌러서
이렇게 작성을 해주면
글 생성까지 원활하게 기능이 작동함을 알 수 있다.
<다음 글 - 로그인/로그아웃, 회원가입 구현하기>
'JAVA > SpringBoot 3' 카테고리의 다른 글
[스프링 부트 3] 스프링 시큐리티 - 2 (로그인/로그아웃, 회원가입 구현하기) (0) | 2024.08.16 |
---|---|
[스프링 부트 3] 스프링 시큐리티 - 1 (스프링 시큐리티, 인증과 인가 ) (0) | 2024.08.14 |
[스프링 부트 3] 템플릿 엔진, 타임리프란? (0) | 2024.08.12 |
[스프링 부트 3] 블로그 만들기 - 2 (글 목록 조회 / 단일 조회 / 삭제 / 수정 API 구현) (0) | 2024.08.08 |
[스프링 부트 3] 블로그 만들기 - 1 (블로그 글 작성 API 구현) (0) | 2024.08.07 |