블로그 화면 구성하기(뷰)

Sirius·2024년 7월 11일

타임리프

타임리프는 템플릿 엔진이다.

  • 템플릿 엔진: 스프링 서버에서 데이터를 받아 우리가 보는 웹페이지, 즉 HTML 상에 그 데이터를 넣어 보여주는 도구이다.

1) 템플릿 엔진 개념 잡기

<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}"

2) 의존성 추가하기

build.gradle 파일에 의존성을 추가한다.

dependencies {
	...
    implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
}

블로그 글 목록 뷰 구현하기

1) 컨트롤러 메소드 작성하기

1> 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();
    }
}

뷰에게 전달할 데이터(dto): ArticleListViewResponse.java를 생성한다.

2> BlogViewController.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 을 찾도록 뷰의 이름을 적은 것이다.

2) 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"를 출력한다.

블로그 글 뷰 구현하기

1) 엔티티에 생성, 수정 시간 추가

1> Article.java 수정

엔티티에 생성 시간과 수정시간을 추가하여 글이 언제 생성되었는지 뷰에서 확인한다.

@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): 엔티티의 생성 및 수정 시간을 자동으로 감시하고 기록

2> data.sql 수정

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())

3> SpringBootDeveloperApplication.java 수정

@EnableJpaAuditing 추가 : created_at, updated_at 자동으로 업데이트

@EnableJpaAuditing
@SpringBootApplication
public class blogApplication {
    public static void main(String [] args){
        SpringApplication.run(blogApplication.class, args);
    }

}

2) 컨트롤러 메소드 작성

1> ArticleViewResponse.java 생성

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();
    }
}

2> BlogViewController.java에 메소드 추가

블로그 글 반환할 메소드 추가

@GetMapping("/articles/{id}")
    public String getArticle(@PathVariable Long id, Model model){
        Article article = blogService.findById(id);
        model.addAttribute("article", new ArticleViewResponse(article));
        return "article";
    }

3) 뷰 만들기

1> 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>
            <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>

2> 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="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>

삭제기능 추가하기

1) 삭제 기능 코드 작성하기

1> article.js 생성

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 요청을 보내는 역할을 한다.

2> article.html 수정

<!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>

수정/생성 기능 추가하기

1) 수정/생성 뷰 컨트롤러 작성

1> BlogViewController.java 추가

 @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()메소드를 호출한다.

2) 수정/생성 뷰 작성

1> newArticle.html 생성

<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>

2> article.js에 코드 추가

실제 생성, 수정을 위한 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}`);
            })
    })
}

3> article.html수정

수정 버튼에 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>

생성 기능 마무리

1) 생성기능 작성

1> article.js 코드 추가

글생성 코드 추가

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");
            });
        });
}

2> articleList.html 수정

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>

0개의 댓글