Bootstrap : https://getbootstrap.kr
Thymeleaf : https://www.thymeleaf.org
Bootstrap 을 이용하기 위한 html 템플릿 소스 : Bootstrap 을 이용한 html 페이지 구성
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<!-- 모바일에서의 적절한 반응형 동작을 위해 -->
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- -->
<title>Title</title>
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"
rel="stylesheet"
integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH"
crossorigin="anonymous"
>
<!-- -->
</head>
<body>
<!-- Popper -->
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.8/dist/umd/popper.min.js"
integrity="sha384-I7E8VVD/ismYTF4hNIPjVp/Zjvgyol6VFvRkX/vR+Vc4jQkC+hVqc2pM8ODewa9r"
crossorigin="anonymous"></script>
<!-- Bootstrap JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.min.js"
integrity="sha384-0pUGZvbkm6XF6gxjEnlmuGrJXVbNuzT9qBBavbLwCsOGabYfZo0T0to5eqruptLy"
crossorigin="anonymous"></script>
</body>
</html>
Home
[공통] 네비게이션바 : https://getbootstrap.kr/docs/5.3/components/navbar/
위 경로에서 적절한 예시 코드를 복사해서 <body> 태그 안에 붙여넣기 한다.
[공통] 푸터 : https://getbootstrap.kr/docs/5.3/examples/footers/
위 경로의 웹페이지에서 F12 를 눌러서 개발자도구를 나타나게 한다.
엘리먼트 선택기를 클릭하여 마음에 드는 영역을 선택한다.
개발자도구의 Elements 목록에서 선택된 항목을 Copy 한다.
일기내용 리스트 : https://getbootstrap.kr/docs/5.3/examples/sidebars/
위 경로의 웹 페이지에서 F12 를 눌러서 개발자도구를 나타나게 한 상태에서 마음에 드는 영역을 선택하고, 엘리먼트 소스를 복사한다.
리스트 항목은 프로그래밍으로 표시되게 할 것이므로 너무 길게 표시되지 않게 중복적인 코드를 몇 개 삭제한다.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<!-- 모바일에서의 적절한 반응형 동작을 위해 -->
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- -->
<title>Title</title>
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"
rel="stylesheet"
integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH"
crossorigin="anonymous"
>
<!-- -->
</head>
<body>
<nav class="navbar navbar-expand-lg bg-body-tertiary">
<div class="container-fluid">
<a class="navbar-brand" href="#">Navbar</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<a class="nav-link active" aria-current="page" href="#">Home</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">Link</a>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
Dropdown
</a>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="#">Action</a></li>
<li><a class="dropdown-item" href="#">Another action</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="#">Something else here</a></li>
</ul>
</li>
<li class="nav-item">
<a class="nav-link disabled" aria-disabled="true">Disabled</a>
</li>
</ul>
<form class="d-flex" role="search">
<input class="form-control me-2" type="search" placeholder="Search" aria-label="Search">
<button class="btn btn-outline-success" type="submit">Search</button>
</form>
</div>
</div>
</nav>
<div class="container">
<div class="d-flex flex-column align-items-stretch flex-shrink-0 bg-body-tertiary" style="width: 380px;">
<a href="/" class="d-flex align-items-center flex-shrink-0 p-3 link-body-emphasis text-decoration-none border-bottom">
<svg class="bi pe-none me-2" width="30" height="24"><use xlink:href="#bootstrap"></use></svg>
<span class="fs-5 fw-semibold">List group</span>
</a>
<div class="list-group list-group-flush border-bottom scrollarea">
<a href="#" class="list-group-item list-group-item-action active py-3 lh-sm" aria-current="true">
<div class="d-flex w-100 align-items-center justify-content-between">
<strong class="mb-1">List group item heading</strong>
<small>Wed</small>
</div>
<div class="col-10 mb-1 small">Some placeholder content in a paragraph below the heading and date.</div>
</a>
<a href="#" class="list-group-item list-group-item-action py-3 lh-sm">
<div class="d-flex w-100 align-items-center justify-content-between">
<strong class="mb-1">List group item heading</strong>
<small class="text-body-secondary">Tues</small>
</div>
<div class="col-10 mb-1 small">Some placeholder content in a paragraph below the heading and date.</div>
</a>
<a href="#" class="list-group-item list-group-item-action py-3 lh-sm">
<div class="d-flex w-100 align-items-center justify-content-between">
<strong class="mb-1">List group item heading</strong>
<small class="text-body-secondary">Mon</small>
</div>
<div class="col-10 mb-1 small">Some placeholder content in a paragraph below the heading and date.</div>
</a>
<a href="#" class="list-group-item list-group-item-action py-3 lh-sm" aria-current="true">
<div class="d-flex w-100 align-items-center justify-content-between">
<strong class="mb-1">List group item heading</strong>
<small class="text-body-secondary">Wed</small>
</div>
<div class="col-10 mb-1 small">Some placeholder content in a paragraph below the heading and date.</div>
</a>
<a href="#" class="list-group-item list-group-item-action py-3 lh-sm">
<div class="d-flex w-100 align-items-center justify-content-between">
<strong class="mb-1">List group item heading</strong>
<small class="text-body-secondary">Tues</small>
</div>
<div class="col-10 mb-1 small">Some placeholder content in a paragraph below the heading and date.</div>
</a>
<a href="#" class="list-group-item list-group-item-action py-3 lh-sm">
<div class="d-flex w-100 align-items-center justify-content-between">
<strong class="mb-1">List group item heading</strong>
<small class="text-body-secondary">Mon</small>
</div>
<div class="col-10 mb-1 small">Some placeholder content in a paragraph below the heading and date.</div>
</a>
<a href="#" class="list-group-item list-group-item-action py-3 lh-sm" aria-current="true">
<div class="d-flex w-100 align-items-center justify-content-between">
<strong class="mb-1">List group item heading</strong>
<small class="text-body-secondary">Wed</small>
</div>
<div class="col-10 mb-1 small">Some placeholder content in a paragraph below the heading and date.</div>
</a>
<a href="#" class="list-group-item list-group-item-action py-3 lh-sm">
<div class="d-flex w-100 align-items-center justify-content-between">
<strong class="mb-1">List group item heading</strong>
<small class="text-body-secondary">Tues</small>
</div>
<div class="col-10 mb-1 small">Some placeholder content in a paragraph below the heading and date.</div>
</a>
</div>
</div>
</div>
<div class="container">
<footer class="d-flex flex-wrap justify-content-between align-items-center py-3 my-4 border-top">
<p class="col-md-4 mb-0 text-body-secondary">© 2024 Company, Inc</p>
<a href="/" class="col-md-4 d-flex align-items-center justify-content-center mb-3 mb-md-0 me-md-auto link-body-emphasis text-decoration-none">
<svg class="bi me-2" width="40" height="32"><use xlink:href="#bootstrap"></use></svg>
</a>
<ul class="nav col-md-4 justify-content-end">
<li class="nav-item"><a href="#" class="nav-link px-2 text-body-secondary">Home</a></li>
<li class="nav-item"><a href="#" class="nav-link px-2 text-body-secondary">Features</a></li>
<li class="nav-item"><a href="#" class="nav-link px-2 text-body-secondary">Pricing</a></li>
<li class="nav-item"><a href="#" class="nav-link px-2 text-body-secondary">FAQs</a></li>
<li class="nav-item"><a href="#" class="nav-link px-2 text-body-secondary">About</a></li>
</ul>
</footer>
</div>
<!-- Popper -->
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.8/dist/umd/popper.min.js"
integrity="sha384-I7E8VVD/ismYTF4hNIPjVp/Zjvgyol6VFvRkX/vR+Vc4jQkC+hVqc2pM8ODewa9r"
crossorigin="anonymous"></script>
<!-- Bootstrap JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.min.js"
integrity="sha384-0pUGZvbkm6XF6gxjEnlmuGrJXVbNuzT9qBBavbLwCsOGabYfZo0T0to5eqruptLy"
crossorigin="anonymous"></script>
</body>
</html>
Write
폼 컨트롤 : https://getbootstrap.kr/docs/5.3/forms/form-control/
위 웹페이지에서 폼 컨트롤에 정의할 클래스 정보를 확인하여 기존 폼 소스에 적용한다.
<h1>Write</h1>
<form action="/diary" method="POST">
<div class="mb-3">
<label for="date" class="form-label">날짜:</label>
<input type="text" class="form-control" id="date" name="date" />
</div>
<div class="mb-3">
<label for="content" class="form-label">내용:</label>
<textarea class="form-control" rows="10" id="content" name="content"></textarea>
</div>
<input type="submit" class="btn btn-primary" value="저장" />
</form>
Bootstrap 사용을 위한 코드와 네비게이션바와 푸터 영역의 코드를 삽입한다.
Edit
수정 화면 양식은 Write 화면 양식과 크게 다르지 않다. 폼 컨트롤에 대한 클래스명을 지정하는 것과 공통요소인 네비게이션바와 푸터 영역 코드의 삽입이 필요하다.
Thymeleaf 적용
<html> 태그의 속성으로 xmlns:th=”http://www.thymeleaf.org” 를 추가한다.
<html lang="en" xmlns:th="http://www.thymeleaf.org">
Thymeleaf – navigation bar
Home, Write 메뉴항목과 검색 컨트롤만 남기고 정리한다.
Home 과 Write 메뉴항목에 대한 링크는 각각 / 와 /write 이다.
<!-- Navigation bar -->
<nav class="navbar navbar-expand-lg bg-body-tertiary">
<div class="container-fluid">
<a class="navbar-brand" href="#">Diary</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<a class="nav-link" th:href="@{/}">Home</a>
</li>
<li class="nav-item">
<a class="nav-link" th:href="@{/write}">Write</a>
</li>
</ul>
<form class="d-flex" role="search">
<input class="form-control me-2" type="search" placeholder="Search" aria-label="Search">
<button class="btn btn-outline-success" type="submit">Search</button>
</form>
</div>
</div>
</nav>
타임리프에서 링크를 표현하는 기본 방식은 th:href=”@{}” 이다. {} 사이에 필요한 유형의 경로를 입력해주는 방식이다.
참고: https://www.thymeleaf.org/doc/articles/standardurlsyntax.html
Thymeleaf – Home – 일기 목록
일기 날짜와 요일, 일기 내용 을 표시하는 태그가 반복되는 형식이다.
반복되는 항목에 thymeleaf 태그를 적용하고, 나머지는 삭제한다.
diary/home.html
...
<div class="d-flex flex-column align-items-stretch flex-shrink-0 bg-body-tertiary" style="width: 100%;">
<div class="list-group list-group-flush border-bottom scrollarea">
<a th:each="diary : ${diaries}" th:href="@{/diary/edit/{id}(id=${diary['id']})}" class="list-group-item list-group-item-action py-3 lh-sm">
<div class="d-flex w-100 align-items-center justify-content-between">
<strong class="mb-1" th:text="${diary['diary_date']}"></strong>
<small class="text-body-secondary" th:text="${#dates.dayOfWeekName(diary['diary_date'])}"></small>
</div>
<div class="col-10 mb-1 small" style="white-space:pre;" th:text="${diary['diary_content']}"></div>
</a>
</div>
</div>
...
${diaries} : 컨트롤러에서 diaries 라는 이름으로 데이터를 전달하도록 설정하였다.
th:each=”diary : ${diaries}” : ${diaries} 는 여러개의 일기 항목이므로 each 를 이용하여 루프처리하고 있다.
th:href=”@{/diary/edit/{id}(id=${diary[‘id’])}” : 일기 항목을 클릭했을 때 수정하는 화면으로 이동하게 하기 위한 URL 을 설정한다. /diary/edit/{id} 의 형식이며, {id} 자리에 일기의 고유번호값이 대입되도록 설정하였다.
th:text=”${#dates.dayOfWeekName(diary[‘diary_date’])}” : # 을 이용하여 타임리프의 함수를 사용하고 있다. dates.dayOfWeekName 은 date 형 값으로부터 요일값을 구하는 기능의 함수이다.
style=”white-space:pre;” : textarea 영역에서는 엔터 및 스페이스가 입력가능하다. 이 데이터가 입력한 그대로 표시되게 하기 위한 스타일 지정이다.
데이터베이스에 저장되어 있는 모든 일기 데이터를 가져오기 위한 기능을 추가한다.
DiaryUIController.java
@Controller
public class DiaryUIController {
...
// Home
@GetMapping("/")
public String Home(Model model) {
// 가장 최신 날짜의 일기를 위쪽으로 나열되게
List<Map<String, Object>> listDiaries = diaryService.GetAllDiaries();
model.addAttribute("diaries", listDiaries);
return "diary/home";
}
...
}
DiaryService.java
@Service
public class DiaryService {
...
public List<Map<String, Object>> GetAllDiaries() {
return diaryMapper.GetAllDiaries();
}
}
DiaryMapper.java
@Mapper
public interface DiaryMapper {
...
List<Map<String, Object>> GetAllDiaries();
}
DiaryMapper.xml
<mapper namespace="com.woohahaapps.study.diary.mapper.DiaryMapper">
...
<select id="GetAllDiaries" resultType="hashmap">
select
*
from diary
order by
diary_date desc
, id desc
</select>
</mapper>
Thymeleaf – Edit
diary/editdiary.html
...
<h1>Edit</h1>
<form th:action="@{/diary/{id}(id=${diary.id})}" th:method="POST">
<input type="hidden" name="_method" value="PUT" />
<!--<input type="hidden" id="id" name="id" th:value="${diary.id}" />-->
<div class="mb-3">
<label for="diary_date" class="form-label">날짜:</label>
<input type="text" class="form-control" id="diary_date" name="diary_date" th:value="${#temporals.format(diary.diary_date, 'yyyy-MM-dd')}" />
</div>
<div class="mb-3">
<label for="diary_content" class="form-label">내용:</label>
<textarea class="form-control" rows="10" id="diary_content" name="diary_content" th:value="${diary.diary_content}" th:text="${diary.diary_content}"></textarea>
</div>
<input type="submit" class="btn btn-primary" value="저장" />
</form>
...
기존 데이터를 수정할 때 URL 에 기존 데이터의 고유번호가 전달되도록 수정하고, 폼 필드로 전달되는 고유번호는 삭제하였다. 고유번호 1의 데이터를 수정하면서 /diary/3 을 호출하면 고유번호 3의 데이터가 수정되는 일이 발생할 가능성은 있다.
Home 화면의 일기 내용을 클릭했을 때 이동하는 URL 이 /diary/edit/{id} 이므로 이 URL 에 대한 핸들러를 다음과 같이 작성한다.
DiaryUIController.java
@Controller
public class DiaryUIController {
...
// Edit
@GetMapping("/diary/edit/{id}")
public String EditDiary(@PathVariable("id") Integer id, Model model) {
Diary diary = diaryService.GetDiary(id);
model.addAttribute("diary", diary);
return "diary/editdiary"; // template/diary/editdiary.html 에 연결됨
}
...
일기내용을 수정 저장하는 /diary/{id} PUT Method 핸들러에서는 / url 로 redirect 해주어야 home 화면에 새로 수정된 내용의 일기가 표시된다.
DiaryController.java
@RestController
public class DiaryController {
...
// Update
@PutMapping("/diary/{id}")
public RedirectView UpdateDiary(@PathVariable("id") Integer id, String diary_date, String diary_content) {
System.out.println("id=" + id + ", date=" + diary_date + ", content=" + diary_content);
diaryService.UpdateDiary(id, diary_date, diary_content);
return new RedirectView("/");
}
...
Thymeleaf – Write
diary/write.html
...
<h1>Write</h1>
<form action="/diary" method="POST">
<div class="mb-3">
<label for="date" class="form-label">날짜:</label>
<input type="text" class="form-control" id="date" name="date" />
</div>
<div class="mb-3">
<label for="content" class="form-label">내용:</label>
<textarea class="form-control" rows="10" id="content" name="content"></textarea>
</div>
<input type="submit" class="btn btn-primary" value="저장" />
</form>
...
새 일기를 저장하는 POST method 의 핸들러는 일기 데이터 저장이 완료되면 Home 화면으로 redirect 해서 새로 작성한 일기 데이터가 보이게 해야 한다.
DiaryController.java
@RestController
public class DiaryController {
...
// Create
@PostMapping("/diary")
public RedirectView CreateDiary(String date, String content) {
System.out.println("date=" + date + ",content=" + content);
diaryService.CreateDiary(date, content);
return new RedirectView("/");
}
...