📚 관련 독서/스프링 부트 3 백엔드 개발자 되기 - 자바 편

[SpringBoot] 07장 블로그 화면 구성하기 (타임리프)

imname1am 2024. 2. 8. 15:31
반응형

📗 타임리프 ⭐

📍 템플릿 엔진 개념잡기

템플릿 엔진
 : 스프링 서버에서 데이터를 넘겨받아 HTML에 데이터를 넣어 동적 웹 페이지를 만들어주는 도구

 

 

타임리프 표현식

  • ${..}   : 변수의 값 표현식
  • #{..}   : 속성 파일 값 표현식
  • @{..}  : 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 조건이 false일 때만 표시 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)

implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'

 

 

 

① 타임리프용 컨트롤러 작성하기 (ExampleController.java)

- /thymeleaf/example GET 요청이 오면 특정 데이터를 뷰(HTML)에 넘겨주는 모델 객체에 추가하는 컨트롤러 메소드

@Controller
public class ExampleController {

    @GetMapping("/thymeleaf/example")
    public String thymeleafExample(Model model) {   // 뷰로 데이터 넘겨주는 모델 객체
        Person exPerson = new Person();
        exPerson.setId(1L);
        exPerson.setName("홍길동");
        exPerson.setAge(11);
        exPerson.setHobbies(List.of("운동", "독서"));

        model.addAttribute("person", exPerson); // Person 객체 처장
        model.addAttribute("today", LocalDate.now());
        
        return "example";   // example.html이라는 뷰 조회
    }

    @Setter
    @Getter
    class Person {
        private Long id;
        private String name;
        private int age;
        private List<String> hobbies;
    }
}

 

@Controller

: 반환값으로 view 찾아 보여주는 애너테이션

 

 

Model 객체

: 뷰 쪽으로 값을 넘겨주는 객체 (컨트롤러와 뷰의 중간다리 역할. 컨트롤러에서 데이터를 설정하고, 모델은 뷰에서 사용할 수 있게 키에 맞는 데이터를 전달하는 역할)

 

 

addAttribute()

: 모델에 값 저장

 

 

② HTML 뷰 만들고 테스트하기

- 뷰 파일 생성 위치 : [src/main/resources/templates/example.html]

<!DOCTYPE html>
<html xmlns:th="http://thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>타임리프 익히기</h1>
<p th:text="${#temporals.format(today, 'yyyy-MM-dd')}"></p>
<div th:object="${person}"> <!-- person을 선택한 객체로 지정 -->
    <p th:text="|이름 : *{name}|"></p>    <!-- 하위 태그라 * 사용해 부모 태그에 적용한 객체 값에 접근 가능 -->
    <p th:text="|나이 : *{age}|"></p>
    <p>취미</p>
    <ul th:each="hobby : *{hobbies}">   <!-- hobbies 갯수만큼 반복 -->
        <li th:text="${hobby}"></li>
        <!-- 반복 대상이 운동이라면, '대표 취미'라는 표시 추가 -->
        <span th:if="${hobby == '운동'}">(대표 취미)</span>
    </ul>
</div>
<a th:href="@{/api/articles/{id}(id=${person.id})}">글 보기</a>
</body>
</html>

 

 

- [결과] http://localhost:8080/thymeleaf/example 로 접속해 확인한다.

 

 

📗 글 목록 뷰 구현하기

① 컨트롤러 메서드 작성하기

1) 뷰에 데이터 전달하기 위한 객체(dto) 생성하기

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

 

 

2) 컨트롤러 패키지에 /articles GET 요청 처리할 코드 작성 (블로그 글 전체 리스트 담은 뷰 반환)

@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);  // 1. 블로그 글 리스트 저장

        return "articleList";   // 2. articleList.html이라는 뷰 조회
    }
}

 

 

② HTML 뷰 만들고 테스트하기

- resource/templates/articleList.html 파일 생성해 해당 내용 입력

<!DOCTYPE html>
<html xmlns:th="http://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}">    <!-- 1. article 개수만큼 반복-->
        <div class="card">
            <div class="card-header" th:text="${item.id}">  <!-- 2. 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 href="#" class="btn btn-primary">보러 가기</a>
            </div>
        </div>
    </div>
</div>
</body>
</html>

 

 

- [결과]  http://localhost:8080/articles 에 접속해 결과 확인

 

 

📗 글 뷰 구현하기

📍 엔티티에 생성, 수정 시간 추가하기

1. Article 엔티티(dto)에 생성, 수정 시간 추가

@CreatedDate    // 엔티티 생성 시 생성 시간 저장
@Column(name = "created_at")
private LocalDateTime createdAt;

@LastModifiedDate   // 엔티티 수정 시 수정 시간 저장
@Column(name = "updated_at")
private LocalDateTime updatedAt;

 

 

2. data.sql에서 실행할 때마다 created_at, updated_at이 변경되도록 저장

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. SpringBootBlogApplication.java 파일에서 created_at, updated_at 자동으로 업데이트하기 위한 애너테이션 추가

@EnableJpaAuditing  // created_at, updated_at 자동 업데이트

 

 

① 컨트롤러 메서드 작성하기

1) 뷰에 데이터 전달하기 위한 객체(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();
    }
}

 

 

2) BlogViewController.java getArticle() 메소드 추가

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

 

 

② HTML 뷰 만들고 테스트하기

1) 글 상세 화면 article.html 작성하기

<!DOCTYPE html>
<html xmlns:th="http://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, '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" 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 수정

<a th:href="@{/articles/{id}(id=${item.id})}" class="btn btn-primary">보러 가기</a>

 

 

[결과]

 

 

📗 삭제 기능 추가하기

① 삭제 코드 작성하기 (js)

1) 자바스크립트로 삭제 코드 작성하기

▷ src/main/resources/static/js/article.js

// 삭제 기능
const deleteBtn = document.getElementById('delete-btn');

if(deleteBtn) {
    deleteBtn.addEventListener('click', event => {
        let id = document.getElementById('article-id').value;

        fetch(`/api/articles/${id}`, {
            method: 'DELETE'
        })
        .then(() => {
            alert('삭제 완료');
            location.replace('/articles');
        });
    });
}

➔ id가 delete-btn인 엘리먼트를 찾아 엘리먼트에서 클릭 이벤트 발생 시 fetch() 메소드 통해 /api/articles/DELETE 요청 보내는 역할 수행

➔ then() : fetch()가 잘 완료되면 연이어 실행되는 메소드

➔ location.replace() : 사용자의 웹 브라우저 화면을 현재 주소를 기반해 옮겨주는 역할

 

 

2) article.html에서 삭제 기능 article.js가 동작하도록 js코드 import

 

 

② 실행 테스트하기

- http://localhost:8080/article/1에 접속한 다음 [삭제]-[확인] 누르고 삭제 결과 확인

location.replace() 덕분에 글 목록 페이지로 이동

 

 

 

📗 수정/생성 기능 추가하기

쿼리 파라미터
: HTTP 요청에서 URL 끝에 ?로 시작하는 키 값으로 이뤄진 문자열. &로 구분

 

① 수정/생성 뷰 컨트롤러 작성하기 (BlogViewController.java)

// 수정 화면 보여주기
@GetMapping("/new-article")
// 1. id 키를 가진 쿼리 파라미터 값을 id 변수에 매핑 (id는 없을 수도 있음)
public String newArticle(@RequestParam(required = false) Long id, Model model) {
    if(id == null) {    // 2. id가 없으면 생성
        model.addAttribute("article", new ArticleViewResponse());
    }
    else {  // 3. id가 있으면 수정
        Article article = blogService.findById(id);
        model.addAttribute("article", new ArticleViewResponse(article));
    }

    return "newArticle";
}

 

 

② 수정/생성 뷰 만들기

1) 컨트롤러 메소드에서 반환하는 newArticle.html 뷰 구현

<!DOCTYPE html>
<html xmlns:th="http://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>
                <!-- 블로그 글 id 정보 저장 (수정용) -->
                <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>  <!-- article.js 파일 추가-->
</body>
</html>

 

 

2) 실제 수정,생성 기능 위한 API 구현 (article.js)

// 수정 기능
const modifyBtn = document.getElementById('modify-btn');

if(modifyBtn) {
    modifyBtn.addEventListener('click', event=> {
       let params = new URLSearchParams(location.search);
       let id = params.get('id');

       fetch(`/api/articles/${id}`, {
          method : 'PUT',
          headers: {  // headers에 요청 형식 지정
              "Content-Type": "application/json",
          },
          body: JSON.stringify({    // body에 HTML에 입력한 데이터를 JSON 형식으로 바꿔 보냄
              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>

 

 

[결과] http://localhost:8080/articles/1에 접속해 [수정] 버튼 눌러 내용 수정하기

 

 

 

📗 생성 기능 추가하기

자바스크립트로 등록(생성) 코드 작성하기 (article.js)

// 등록(생성) 기능
const createBtn = document.getElementById("create-btn");

if(createBtn) {
    createBtn.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");
        });
    });
}

 

 

articleList.html 파일 수정해 id가 create-btn인 [등록] 버튼 추가

 

 

[결과] http://localhost:8080/articles에 접속해 [글 등록] 버튼 눌러 확인하기

 

 

 

 

(해당 글 내용은 📗 스프링 부트 3 백엔드 개발자 되기 - 자바 편을 읽고 정리한 내용입니다.)

반응형