조찬국 2024. 3. 3. 19:14
728x90

참고 저서: https://goldenrabbit.co.kr/product/springboot3java/

 

[되기] 스프링 부트 3 백엔드 개발자 되기(자바 편) - 골든래빗

자바 백엔드 개발자가 되고 싶다면 자바 그다음에 꼭 보세요! 입문자에게 백엔드 개발의 필수 지식을 학습 로드맵 중심으로 설명합니다. 스프링 부트 3 개발에 꼭 필요한 JPA ORM, OAuth2 인증, AWS 배

goldenrabbit.co.kr

 

이번 시간에는 블로그화면을 구성해 보겠다. (js나 html에 대한 설명은 생략했습니다.)
(템플릿 엔진에 대해 모르면 아래의 웹MVC 포스팅을 보고 오셔야합니다.)

https://changuk0308.tistory.com/3

 

[SpringBoot-스프링 입문] 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술: 2. 스프링 웹 개발 기초

강의 출처:https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%EC%9E%85%EB%AC%B8-%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8 [지금 무료] 스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술 강의 - 인프런

changuk0308.tistory.com

1.타임리프 템플릿 엔진 동작 확인

ExampleController

package blogExample.blog.controller;

import lombok.Getter;
import lombok.Setter;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

import java.time.LocalDate;
import java.util.List;

@Controller
public class ExampleController {

    @GetMapping("/thymeleaf/example")
    public String thymeleafExample(Model model) {
        Person examplePerson = new Person();
        examplePerson.setId(1L);
        examplePerson.setName("홍길동");
        examplePerson.setAge(11);
        examplePerson.setHobbies(List.of("운동", "독서"));

        model.addAttribute("person", examplePerson);
        model.addAttribute("today", LocalDate.now());

        return "example";
    }

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

example.html

<!DOCTYPE html>
<html xmlns:th="http://www.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}">
    <p th:text="|이름 : *{name}|"></p>
    <p th:text="|나이 : *{age}|"></p>
    <p>취미</p>
    <ul th:each="hobby : *{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>

결과

2. 블로그 글 목록 뷰 구현하기

1. 뷰에게 데이터를 전달할 객체 생성

ArticleListViewResponseDto 패키지에 추가

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

BlogViewController

@RequiredArgsConstructor
@Controller //MVC형태로 만들 예정이니
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"; //뷰 반환

    }

/articles GET 요청을 처리할 코드를 작성한다. 여기서는 블로그 글 전체 리스트를 담은 뷰를 반환.

articleList.htmlresource/templates에 추가

<!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">
    <button type="button" id="create-btn"
            th:onclick="|location.href='@{/new-article}'|"
            class="btn btn-secondary btn-sm mb-3">글 등록</button>
    <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-text" th:text="${item.content}"></p>
                <a th:href="@{/articles/{id}(id=${item.id})}" class="btn btn-primary">보러가기</a>
            </div>
        </div>
        <br>
    </div>
    <button type="button" class="btn btn-secondary" onclick="location.href='/logout'">로그아웃</button>
</div>

<script src="/js/article.js"></script>
</body>

결과

2. 생성시각,수정 시각 나타내기

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

BlogViewControllergetArticle() 메서드를 추가

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

    return "article";
}

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">
    <!-- My Blog 클릭 시 /article로 이동하는 링크 추가 -->
    <a href="/articles" style="text-decoration: none; color: inherit;">
        <h1 class="mb-3">My Blog</h1>
    </a>
    <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">
                    <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>

<script src="/js/article.js"></script>
</body>
</html>

main함수가 위치한 클래스에 다음과 같은 어노테이션 입력

import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

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

JpaAuditingConfiguration 파일이 등록되지 않기 때문에, 테스트시 createdAt이나 updatedAt 에 등록, 수정 시간이 아닌 null 값이 들어갈 수 있다.

Article 도메인 수정

package blogExample.blog.domain;

import jakarta.persistence.*;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.springframework.cglib.core.Local;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;

import java.time.LocalDateTime;

import org.springframework.data.jpa.domain.support.AuditingEntityListener;
@EntityListeners(AuditingEntityListener.class)
@Entity //빈으로 등록
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)//기본생성자를 protected로 생성하는 롬복
public class Article {

    //기본키
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name="id",updatable = false)
    private Long id;

    @Column(name="title",nullable = false)
    private String title;

    @Column(name="content",nullable = false)
    private String content;

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

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


    //setter대신 생성자로 값 설정
    @Builder //★★Builder 패턴으로 생성자 객체 정의★★★
    public Article(String title,String content)
    {
        this.title=title;
        this.content=content;
    }
    /**
     *    ★Builder 패턴:
     *
     *    기존:
     *    new Article("abc","def");
     *
     *    builder 패턴:
     *    Article.builder()
     *           .title("abc")
     *           .content("def")
     *           .build();
     *
     *    파라미터 삽입시 위치에 상관없이 주입가능
     *    (메서드가 아니라 객체에서만 사용가능)
     */

//    //기본 생성자 (롬복으로 설정
//    JPA에서는 기본 생성자를 통해 접근하기에
//    protected Article(){
//
//    }

    /**
     * 수정 메서드 작성(값을 재세팅 하기 위해서)
     */
    public void update(String title,String content)
    {
        this.title=title;
        this.content=content;
    }

}

결과

3. 삭제 기능 추가하기

삭제 코드를 작성. 삭제 코드는 자바스크립트로 작성했다.
(만약 디렉터리를 생성하면서 파일을 한 번에 만들고 싶다면 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');
            });
    });
}
// 수정 기능
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: 'PATCH',
            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');
            });
    });
}

결과

5. 블로그 글 수정 및 생성

settings->java Compiler에서 밑에 보면 Addtional command line parameters가 존재하는데 저 부분에서 -(하이픈)을 추가하기 위해 저렇게 -parameters라 설정하자.

BlogViewControllernewArticle() 메서드를 추가


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

수정/생성 뷰 만들기

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">
    <!-- My Blog 클릭 시 /article로 이동하는 링크 추가 -->
    <a href="/articles" style="text-decoration: none; color: inherit;">
        <h1 class="mb-3">My Blog</h1>
    </a>
    <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>
                <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>

생성 결과

수정 결과

6. 핵심 요약

1) @Controller 는 반환값으로 뷰를 찾아 보여주는 애너테이션이다.
2) 템플릿 엔진 은 데이터를 넘겨받아 HTML에 데이터를 넣어 동적인 웹페에지를 만들어주는 도구이다.
3) 타임리프는 스프링의 대표적인 템플릿 엔진입니다. 컨트롤러 모델Model 을 통해 데이터를 설정하면, 모델은 뷰에서 사용할 수 있게 데이터를 전달해준다.

타임리프 표현식

표현식설명

${...} 변수의 값 표현식
#{...} 속성 파일 값 표현식
@{...} URL 표현식
*{...} 선택한 변수의 표현식 th:object에서 선택한 객체에 접근

타임리프 문법

표현식설명예제

th:text 텍스트를 표현할 때 사용 th:text=${person.name}
th:each 컬렉션을 반복할 때 사용 th:each="person : ${person}"
th:if 조건이 true일 때만 사용 th:if="${person.age} >= 20"
th:unless 조건이 false일 때만 사용 th:unless="${person.age} >= 20"
th:href 이동경로 th:href="@{/person(id=${person.id})}"
th:with 변숫값으로 지정 th:with="name = ${person.name}"
th:object 선택한 객체로 지정 th:object="${person}"
728x90