일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |
- supervised learning
- GPT-4
- 지도학습
- ChatGPT
- LLM
- AI
- regression
- gpt
- Machine Learning
- LG
- Classification
- 분류
- 해커톤
- 티스토리챌린지
- deep learning
- 머신러닝
- LG Aimers 4th
- OpenAI
- LG Aimers
- PCA
- 딥러닝
- 오블완
- 회귀
- Today
- Total
SYDev
[스프링 부트 3 백엔드 개발자 되기] Chapter 7. 블로그 화면 구성하기 본문
7.1. 사전 지식: 타임리프
- 템플릿 엔진: 스프링 서버에서 데이터를 받아 우리가 보는 웹 페이지, HTML 상에 그 데이터를 넣어 보여주는 도구.
템플릿 엔진 개념 잡기
<h1 text = ${이름}>
<p text = ${나이}>
-> 서버에서 이름, 나이라는 키로 데이터를 템플릿 엔진에 넘겨주고, 템플릿 엔진은 이를 받아 HTML 값에 적용
- 대표적으로 JSP, 타임리프, 프리마커 등이 있다.
- 스프링은 타임리프를 권장한다.
타임리프 표현식과 문법
타임리프 표현식
표현식 | 설명 |
${...} | 변수의 값 표현식 |
#{...} | 속성 파일 값 표현식 |
@{...} | URL 표현식 |
*{...} | 선택한 변수의 표현식. th:object에서 선택한 객체에 접근 |
타임리프 문법
표현식 | 설명 | 예제 |
th:text | 텍스트를 표현할 때 사용 | th:text = ${person.name} |
th:each | 컬렉션을 반복할 때 사용 | th:each = "person: ${persons}" |
th:if | 조건이 true인 때만 표시 | th:if= "${person.age} >= 20" |
타임리프 사용을 위한 의존성 추가
- build.gradle 파일에 implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' 추가
타임리프 문법 익히기용 컨트롤러 작성
ExampleController.java
package me.shinsunyoung.springbootdeveloper.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 = new Person();
examplePerson.setId(1L);
examplePerson.setName("홍길동");
examplePerson.setAge(11);
examplePerson.setHobbies(List.of("운동", "독서"));
model.addAttribute("person", examplePerson); //Person 객체 저장
model.addAttribute("today", LocalDate.now());
return "example";
}
@Setter
@Getter
class Person {
private Long id;
private String name;
private int age;
private List<String> hobbies;
}
}
-> Model 객체는 HTML 쪽으로 값을 넘겨주는 객체.
-> addAttribute() 메서드로 모델에 값을 저장
-> "person"이라는 키에 사람 정보, "today"라는 키에 날짜 정보 저장
-> thymeleafExample이 반환하는 값은 "example" -> 이 값은 클래스에 붙은 애너테이션이 @Controller이므로 뷰의 이름을 반환하는 것 -> resource/templates 디렉터리에서 example.html을 찾은 다음 웹 브라우저에 해당 파일을 보여준다.
모델의 역할 살펴보기
- 모델에는 "person", "today" 이렇게 두 키를 가진 데이터가 들어있다.
- 컨트롤러는 이렇게 모델을 통해 데이터를 설정하고, 모델은 뷰로 이 데이터를 전달해 키에 맞는 데이터를 뷰에서 조회할 수 있게 한다.
- 모델은 컨트롤러와 뷰의 중간다이 역할
뷰 작성하기
- src/main/resources/templates 디렉터리에 example.html 파일 생성
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>
7.2. 블로그 글 목록 뷰 구현하기
컨트롤러 메서드 작성하기
- 6장에서는 컨트롤러 메서드가 데이터를 직렬화한 JSON 문자열을 반환
- 뷰 컨트롤러 메서드는 뷰의 이름을 반환하고, 모델 객체에 값을 담는다.
1단계
- 뷰에게 데이터를 전달하기 위한 객체 생성
ArticleListViewResponse.java
package me.shinsunyoung.springbootdeveloper.dto;
import me.shinsunyoung.springbootdeveloper.domain.Article;
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 요청을 처리할 코드 작성 -> 여기서는 블로그 글 전체 리스트를 담은 뷰를 반환
BlogViewController.java
package me.shinsunyoung.springbootdeveloper.controller;
import lombok.RequiredArgsConstructor;
import me.shinsunyoung.springbootdeveloper.dto.ArticleListViewResponse;
import me.shinsunyoung.springbootdeveloper.service.BlogService;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import java.util.List;
@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 키에 글 리스트를 저장
-> 반환값인 articleList는 resource/templates/articleList.html을 찾도록 뷰의 이름을 명시
HTML 뷰 만들고 테스트
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-text" th:text="${item.content}"></p>
<a href="#" class="btn btn-primary">보러 가기</a>
</div>
</div>
<br>
</div>
</div>
</body>
-> th:each로 "articles" 키에 담긴 데이터 개수만큼 반복
-> th:text는 반복 대상 객체의 id, "text"를 출력 -> 화면을 쉽고 간편하게 만들 수 있는 부트스트랩 사용
7.3. 블로그 글 뷰 구현하기
- 블로그 화면 상의 [보러 가기] 버튼을 누르면 블로그 글이 보이도록 블로그 글 뷰를 구현
- 엔티티에 생성 시간, 수정 시간을 추가하고, 컨트롤러 메서드를 만든 다음 HTML 뷰를 만들고 확인하는 과정으로 개발
엔티티에 생성, 수정 시간 추가하기
1단계
- 엔티티에 생성 시간과 수정 시간을 추가해 글이 언제 생성되었는지 뷰에서 확인
Article.java
package me.shinsunyoung.springbootdeveloper.domain;
import jakarta.persistence.*;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import java.time.LocalDateTime;
@EntityListeners(AuditingEntityListener.class)
@Entity //엔티티로 지정
@Getter //getId(), getTitle()같은 게터 메스드들을 대치
@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 컬럼에 저장
-> @EntityListeners(AuditingEntityListener.class): 엔티티의 생성 및 수정 시간을 자동으로 감시하고 기록
2단계
- 엔티티를 생성하면 생성 시간과 수정 시간이 자동으로 저장되지만, 스프링 부트를 실행할 대마다 SQL문으로 데이터를 넣는 import.sql파일은 created_at과 update_at을 바꾸지 않는다.
- 최초 파일 생서엥도 이 값을 수정하도록 import.sql파일을 수정해 실행할 때마다 create_at, update_at이 바뀌도록 한다.
import.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단계
- SpinrgBootDeveloperApplication.java 파일을 열어 엔티티의 created_at, updated_at을 자동으로 업데이트하기 위한 Annotation 추가
SpringBootDeveloperApplication.java
package me.shinsunyoung.springbootdeveloper;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
@EnableJpaAuditing //creaeted_at, updated_at 자동 업데이트
@SpringBootApplication
public class SpringBootDeveloperApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBootDeveloperApplication.class, args);
}
}
컨트롤러 메서드 작성하기
1단계
- 뷰에서 사용할 DTO를 만들기 위해 dto 디렉터리에 ArticleViewResponse.java 생성한 뒤 클래스 구현
ArticleViewResponse.java
package me.shinsunyoung.springbootdeveloper.dto;
import lombok.Getter;
import lombok.NoArgsConstructor;
import me.shinsunyoung.springbootdeveloper.domain.Article;
import java.time.LocalDateTime;
@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
package me.shinsunyoung.springbootdeveloper.controller;
import lombok.RequiredArgsConstructor;
import me.shinsunyoung.springbootdeveloper.domain.Article;
import me.shinsunyoung.springbootdeveloper.dto.ArticleListViewResponse;
import me.shinsunyoung.springbootdeveloper.service.BlogService;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import java.util.List;
@RequiredArgsConstructor
@Controller
public class BlogViewController {
private final BlogService blogService;
...
@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()메서드로 넘겨 글을 조회하고, 화면에서 사용할 모델에 데이터를 저장한 다음, 보여줄 템플릿 이름을 반환
HTML 뷰 만들기
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, '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>
2단계
articleList.html
...
<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-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>
-> 보러 가기 버튼만 수정
-> @{...}을 사용해 보러 가기를 눌렀을 때, 주소창의 값을 /articles/{item.id}로 변경해 글 상세 화면으로 이동
7.4. 삭제 기능 추가하기
- 글 상세화면에서 [삭제] 버튼을 눌러 글을 삭제
- 삭제 API로 요청을 보낼 코드를 작성
삭제 기능 코드 작성하기
1단계
- src/main/resources/static/js 디렉터리르 생성하고, article.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 요청을 보내는 역할
-> fetch() 메서드에 이어지는 then() 메서드는 fetch()가 잘 완료되면 연이어 실행되는 메서드.
-> alert() 메서드는 then() 메서드가 실행되는 시점에 웹 브라우저 화면으로 삭제가 완료되었음을 알리는 팝업을 띄워주는 메서드
-> location.replace()메서드는 실행 시 사용자의 웹 브라우저 화면을 현재 주소를 기반해 옮겨주는 역할
2단계
- [삭제] 버튼을 눌렀을 때 삭제하도록 button 엘리먼트에 delete-btn이라는 아이디 값 추가하고 앞서 작성한 article.js가 이 화면에서 동작하도록 임포트
article.html
<!DOCTYPE html>
...
<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"
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> <!-- article.js 파일 추가 -->
</body>
7.5. 수정/생성 기능 추가하기
수정/생성 뷰 컨트롤러 작성하기
- 수정: 사용자: /new-article?id=123(id가 123인 글 수정) -> 뷰 컨트롤러: 123 id를 가진 엔티티 조회 후 모델에 추가 -> 뷰
- 생성: 사용자:/new-article(생성) -> 뷰 컨트롤러 -> 뷰
BlogViewController.java
package me.shinsunyoung.springbootdeveloper.controller;
import lombok.RequiredArgsConstructor;
import me.shinsunyoung.springbootdeveloper.domain.Article;
import me.shinsunyoung.springbootdeveloper.dto.ArticleListViewResponse;
import me.shinsunyoung.springbootdeveloper.dto.ArticleViewResponse;
import me.shinsunyoung.springbootdeveloper.service.BlogService;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestParam;
import java.util.List;
@RequiredArgsConstructor
@Controller
public class BlogViewController {
private final BlogService blogService;
...
@GetMapping("/new-article")
//id 키를 가진 쿼리 파라미터의 값을 id 변수에 매핑
public String newArticle(@RequestParam(required = false) Long id, Model model) {
if (id == null) { //id가 없으면 생성
model.addAttribute("article", new ArticleViewResponse());
} else {
Article article = blogService.findById(id);
model.addAttribute("article", new ArticleViewResponse(article));
}
return "newArticle";
}
}
-> 쿼리 파라미터로 넘어온 id 값은 newArticle() 메서드의 Long 타입 id 인자에 매핑한다.
-> id가 있으면 수정, 없으면 생성이므로 id가 없는 경우 기본 생성자를 이용해 빈 ArticleViewResponse 객체를 만들고, id가 있으면 기존 값을 가져오는 findById() 메서드 호출
수정/생성 뷰 만들기
1단계
- 컨트롤러 메서드에서 반환하는 newArticle.html 구현
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>
<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>
2단계
- 실제 수정, 생성 기능을 위한 API 구현
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: 'PUT',
headers: {
"Content-Type": "applications/json",
},
body: JSON.stringify({
title: document.getElementById('title').value,
content: document.getElementById('content').value
})
})
.then(() => {
alert('수정이 완료되었습니다.');
location.replace(`/articles/${id}`);
});
});
}
-> id가 modify-btn인 엘리먼트를 찾고 그 엘리먼트에서 클릭 이벤트가 발생하면 id가 title, content인 엘리먼트의 값을 가져와 fetch() 메서드를 통해 수정 API로 /api/articles/PUT 요청을 보낸다.
-> 요청을 보낼 때는 headersd에 요청 형식을 지정하고, body에 HTML에 입력한 데이터를 JSON 형식으로 바꿔 보낸다.
-> 요청이 완료되면 then() 메서드로 마무리 작업
3단계
- article.html에서 [수정]버튼에 id값과 클릭 이벤트를 추가
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>
...
<section class="mb-5">
<p class="fs-5 mb-4" th:text="${article.content}"></p>
</section>
<!-- 수정 버튼에 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>
</article>
</div>
</div>
</div>
<script src = "/js/article.js"></script>
</body>
7.6. 생성 기능 마무리하기
생성 기능 작성하기
- article.js에서 [등록]버튼을 누르면 입력 칸에 있는 데이터를 가져와 게시글 생성 API에 글 생성 관련 요청을 보내주는 코드를 추가
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");
});
});
}
-> id가 create-btn인 엘리먼트 -> 그 엘리먼트에서 클릭 이벤트가 발생하면 id가 title, content인 엘리먼트의 값을 가져와 fetch() 메서드를 통해 생성 API로 /api/articles/ POST 요청을 보내준다.
2단계
- articleList.html 파일을 수정해 id가 create-btn인 [생성] 버튼 추가
articleList.html
<!DOCTYPE html>
...
<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>
</div>
</body>
참고자료
- 스프링 부트 3 백엔드 개발자 되기, 신선영, 골든래빗, 2023.05.12
'대딩코딩 > 웹개발 스터디' 카테고리의 다른 글
[스프링 부트 3 백엔드 개발자 되기] Chapter 11. AWS에 프로젝트 배포하기 (0) | 2024.06.26 |
---|---|
JWT의 개념 및 활용 (0) | 2024.05.18 |
[스프링 부트 3 백엔드 개발자 되기] Chapter 6. 블로그 기획하고 API 만들기 (0) | 2024.04.07 |
[Express] 19. React Nodejs Start (1) | 2023.12.05 |
[Express] 18. Delete Comment Fetch (1) | 2023.12.04 |