본문 바로가기

SpringBoot

[연재] SpringBoot diary - UI 개선 front-end (bootstrap + thymeleaf)

728x90
반응형

원문은 study.diary : UI 개선 front-end (bootstrap + thymeleaf) 에서 확인할 수 있습니다.

 

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

태그의 속성으로 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("/");
    }
...
반응형