Thymeleaf

배세훈·2022년 7월 31일
0

thymeleaf

목록 보기
1/1

Thymeleaf란?

ThymeleafView Template Engine으로 JSP, Freemarked와 같이 서버에서 클라이언트에게 응답할 브라우저 화면을 만들어주는 역할을 한다.
Thymeleaf는 웹뿐만 아니라 다른 환경을 위한 최신의 서버-사이드 자바 Template Engine이며, HTML, CSS, XM, JS 및 Text까지 수용한다.
Thymeleaf의 주 목표는 유지관리가 쉬운 템플릿 생성 방법을 제공하는 것이며, 실제로 템플릿에 영향을 주지 않는 (HTML의 구조를 깨지 않는, 기존 HTML 코드를 변경하지 않고 덧붙이는 코드)방식을 사용한다. 즉, Natural Templates 개념을 기반으로 한다. 이를 통해 디자인 팀과 개발 팀간 갈등, 격차 해소가 기대된다.)

Thymeleaf의 주요 목표

타임리프의 주 목표는 템플릿을 만들 때 유지관리가 쉽도록 하는 것이다. 이를 위해 디자인 프로토타입으로 사용되는 템플릿에 영향을 미치지 않는 방식인 Natural Templates을 기반으로 한다. Natural Templates은 기존 HTML 코드와 구조를 변경하지 않고 덧붙이는 방식이다.

Thymeleaf의 장점

  • 코드를 변경하지 않기 때문에 디자인 팀과 개발 팀간의 협업이 편해진다.
  • JSP와 달리 Servlet Code로 변환되지 않기 때문에 비즈니스 로직과 분리되어 오로지 View에 집중할 수 있다.
  • 서버상에서 동작하지 않아도 되기 때문에 서버 동작 없이 화면을 확인할 수 있다. 때문에 더미 데이터를 넣고 화면 디자인 및 테스트에 용이하다.

Thymeleaf와 Spring Boot

위와같은 Thymeleaf 장점 때문에 Spring에서도 Spring Boot와 Thymeleaf를 함께 사용하는 것을 권장하고 있다. Spring Boot에서는 JSP 사용 시 호환 및 환경설정에 어려움이 많기 때문이다.
반대로 Thymeleaf는 간편하게 Dependency 추가 작업으로 사용할 수 있다.

Thymeleaf 설정

  • build.gradle
<dependency>
	<groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

Dialect - Standard Dialect

  • 기본적으로 Thymeleaf의 핵심 라이브러리는 Standard Dialect라는 dialect를 제공하며 대부분 사용자에게 만족된다.

  • 공식 Thymeleaf-spring 3 및 Thymeleaf-spring 4 통합 패키지는 모두 "SpringStandard Dialect"라는 dialect를 정의한다. 이 dialect는 Standard Dialect와 거의 동일하나, Spring Framework의 일부 기능을 더 잘 사용하기 위해 약간의 조정이 있다. 예를 들어 OGNL 대신 Spring Expression Language 또는 Spring EL을 사용한다.

  • Standard Dialect의 대부분 프로세서는 'Attribute Processor'이다. 이를 통해 브라우저는 단순히 추가 속성을 무시하기 때문에 처리되기 전에도 HTML 템플릿 파일을 올바르게 표시할 수 있다.

  • 예를 들어, Tag Library를 사용하는 JSP에는 다음과 같이 브라우저에서 직접 표시할 수 없는 코드 조각이 포함될 수 있다.

# JSP
<input type="text" name="userName" value="${user.name}">
# Thymeleaf Standard Dialect
<input type="text" name="userName" value="Gorany" th:value="${user.name}">

이렇게 하면 디자이너와 개발자가 동일한 템플릿 파일에서 작업하고 정적인 HTML을 동적인 HTML로 변환하는데 필요한 노력을 줄일 수 있다. 이를 수행하는 기능이 "Natural Templating"이다.

Tmeplate Engine의 구성

  • Thymeleaf는 스프링 부트에서 ViewResolver를 자동 생성해주고 있다.
    Template Engine을 멤버로 갖는 어플리케이션이 초기화(생성)될 때, ServletContextTemplateResolver 객체가 생성되며, 이 templateResolver에 prefix(접두사)와 suffix(접미사)를 설정하고, Template Engine 객체를 생성해 엔진에 TemplateResolver를 넣는 식이다.
public class GTVGApplication {
  
    
    ...
    private final TemplateEngine templateEngine;
    ...
    
    
    public GTVGApplication(final ServletContext servletContext) {

        super();

        ServletContextTemplateResolver templateResolver = 
                new ServletContextTemplateResolver(servletContext);
        
        // HTML is the default mode, but we set it anyway for better understanding of code
        templateResolver.setTemplateMode(TemplateMode.HTML);
        // This will convert "home" to "/WEB-INF/templates/home.html"
        templateResolver.setPrefix("/WEB-INF/templates/");
        templateResolver.setSuffix(".html");
        // Template cache TTL=1h. If not set, entries would be cached until expelled
        templateResolver.setCacheTTLMs(Long.valueOf(3600000L));
        
        // Cache is set to true by default. Set to false if you want templates to
        // be automatically updated when modified.
        templateResolver.setCacheable(true);
        
        this.templateEngine = new TemplateEngine();
        this.templateEngine.setTemplateResolver(templateResolver);
        
        ...

    }

}

Using Text

  • Thymeleaf에서 Text를 사용하는 방법은 조금 특이하다. Standard Dialect는 속성에 주입하는 식으로 동작하기 때문이다. 이것의 장점은 .html파일이 HTML이 아닌 태그를 포함하지 않기 때문에 모든 브라우저에서 올바르게 표시 될 수 있다.(브라우저는 이해하지 못하는 모든 속성을 무시한다. 예를 들어 th:text)
  • th:text는 HTML 속성이 아니다. 해당 속성을 사용하기 위해서는 아래 코드를 작성해야 한다.

<html xmlns:th="http://www.thymeleaf.org">

표준 표현식 구문

간단한 표현

  • 변수(Variable)표현식: ${...}
  • 선택 변수(Selection Variable) 표현식: *{...}
  • 메세지(Message)표현: #{...}
  • 링크 URL(Link URL) 표현식: @{...}
  • 단편(Fragment) 표현식: ~{...} (~{} 생략 가능)

리터럴

  • 텍스트 리터럴: 'one text', 'two text', ...
  • 숫자 리터럴: 0, 34, 3.0, 1.1, ...
  • boolean 리터럴: true, false
  • null 리터럴: null
  • 리터럴 토큰: one, sometext, main, ...

텍스트 작업

  • 문자열 연결: +
  • 리터럴 대체: |The name is ${name}| ('|'을 사용 (shift + )

산술 연산

  • 이진 연산자: +, -, *, /, %
  • 빼기 기호(단항연산자): -

Bool 연산

  • 이항 연산자: and, or
  • Bool 부정(단항 연산자): !, not

비교 및 평등

  • 비교기: >,<,>=,<=(gt, lt, ge, le)
  • 같음 연산자: ==, !=(eq, ne)

조건부 연산자

  • if-then: (if) ? (then)
  • if-then-else: (if) ? (then) : (else)
  • Default: (value) ?: (defaultvalue)

Message Expression - #{...}

message.properties 외부파일을 이용하여 텍스트를 나타낸다. Map 형태로 Key, Value 형태로 구성된다.(application.yml에서 해당 message.properties 설정)

ex) src/main/resources/messages/message.properties가 있다고 가정.

  • application.yml
#message.properties setting
spring:
	messages:
    	basename: messages/message
    encoding=UTF-8
  • message.properties
#message.properties
title=안녕하세요
greeting=감사합니다.
<p th:utext="#{greeting}">Welcome to our grocery store! </p>
=> 감사합니다 text로 변환

변수

  • ${...} 표현식은 컨텍스트에 포함된 변수 맵에서 실행되는 OGNL(Object-Graph Navigation Language) 표현식이다.
// OGNL
ctx.getVariable("today"); => <span th:text="${greeting}">

((User) ctx.getVariable("session").get("user")).getName();
=>
<p th:utext="#{home.welcome(${session.user.name})}">
  • 컨텍스트에 set으로 변수를 담아, get으로 꺼내서 사용한다. 자바에서 exam.getName()으로 썼다면 JSP의 EL에서 ${exam.name}처럼 꺼내쓰던 것을 생각하면 된다.

식 기본 개체

  • Context 변수에서 OGNL 표현식을 평가할 때 일부 객체는 더 높은 유연성/정확성을 위해 표현식에 사용할 수 있다.

  • #ctx: 컨텍스트 객체

  • #vars: 컨텍스트 변수

  • #locale: 컨텍스트 로케일

  • #request: (웹 컨텍스트에서만) HttpServletRequest 객체

  • #response: (웹 컨텍스트에서만) HttpServletResponse 객체

  • #session: (웹 컨텍스트에서만) HttpSession 객체

  • #servletContext: (웹 컨텍스트에서만) ServletContext 객체

식 유틸리티 개체

  • #execInfo: 처리중인 템플릿에 대한 정보입니다.
  • #message: #{...} 구문을 사용하여 얻을 수 있는 것과 동일한 방식으로 변수 표현식 내에서 외부화 된 메시지를 얻는 방법
  • #uris: URL / URI의 일부를 이스케이프하는 방법
  • #conversions: 구성된 변환 서비스를 실행하는 방법(있는 경우)
  • #dates: java.util.Date개체에 대한 방법: 서식 지정, 구성 요소 추출 등
  • #calendars: #dates와 유사하지만 java.util.Calendar 객체에 사용
  • #numbers: 숫자 개체 서식 지정 방법
  • #strings: String객체에 대한 메소드: contains, startsWith, prepending / appending 등
  • #objects: 일반적으로 개체에 대한 메서드입니다.
  • #bools: 부울 평가 방법
  • #arrays: 배열 방법
  • #lists: 목록 방법
  • #sets: 세트에 대한 방법
  • #maps: 지도 방법
  • #aggregates: 배열 또는 컬렉션에 집계를 만드는 방법
  • #ids: 반복 될 수 있는 id 속성을 처리하기 위한 메소드(예: 반복의 결과)

날짜 바꾸기 예시

  • Date: #calendars 사용
<td th:text="*{#calendars.foramt(regDate, 'yyyy-MM-dd')}"> 2019-08-18</td>
<td th:text="${#calendars.format(notice.regDate, 'yyyy년MM월dd일')}">
<td th:text="${#calendars.format(today, 'yyyy/MM/dd')}"> 2019-08-18</td>
  • LocalDateTime / LocalDate: #temporals 사용
    <span th:text="*{#temporals.format(updatedAt, 'yyyy/MM/dd')}"></span>

선택 변수 표현식 - *{...}

  • ${...} 뿐 아니라 {...} 도 변수 표현식으로 쓸 수 있다. 하지만 {...}는 중요한 차이점이 존재한다. 전체 Context가 아닌 선택한 개체에 대한 표현식을 평가한다. 즉, 선택된 개체가 없는 한 $ 및 * 구문은 동일하다.

선택한 개체란 th:object 속성을 사용하는 표현식의 결과이다.

<form th:object="${member}" method="post" action="/signup">
	<label>Nickname</label>
    <input type="text" th:value="*{nickname}" name="nickname">
    
    <label>Password</label>
    <input type="text" th:value="*{password} name="password">
</form>

이는 다음과 같다.
==>

<form method="post" action="/signup">
	<label>Nickname</label>
    <input type="text" th:value="${member.nickname}" name="nickname">
    
    <label>Password</label>
    <input type="text" th:value="${member.password} name="password">
</form>

혼용도 가능하다.
==>

<form th:object="${member}" method="post" action="/signup">
	<label>Nickname</label>
    <input type="text" th:value="*{nickname}" name="nickname">
    
    <label>Password</label>
    <input type="text" th:value="${member.password} name="password">
</form>
  • Thymeleaf의 URL 표현식은 '@{...}'처럼 사용한다.
<a href="www.naver.com" th:href="@{www.naver.com">Naver</a>
<!-- www.naver.com -->

<a th:href="/board/list(field=${field}, query=${query})}">Search</a>
<!-- /board/list?field=title&query=abc -->

<a href="details.html" th:href="@{/order/{orderId}/details(orderId=${o.id})}">view</a>
<!-- /order/3/details?orderId=3 -->

리터럴

<, >, == 등

  • , <, >=, <=, ==, !=

<div th:if="${prodStat.count} &gt; 1">
<span th:text="'Execution mode is ' + ( (${execMode} == 'dev')? 'Development' : 'Production')">
  • XML은 < 및 >를 속성 값에 사용할 수 없으므로 < , > 로 대체한다.

조건식

  • th:if를 사용하거나 삼항연산자를 사용할 수 있다.
  • 삼항연산자에서는 else 부분을 생략할 수 있다.
<tr th:class="${row.even}? 'even' : 'odd'">
...
</tr>

<tr th:class="${row.even}? (${row.first}? 'first' : 'even') : 'odd'">
...
</tr>

<tr th:class="${row.even}? 'alt'">
...
</tr>

기본 표현식 - Elvis Operator

  • Groovy 같은 일부 언어에 존재하는 Elvis 연산자와 동일하며 두 개의 표현식을 지정할 수 있다. 첫번째 표현식이 null이 아니라면 첫번째 표현식을 사용하고, 반대로 null이라면 두번째 표현식을 사용한다. 마치 간소화된 삼항 연산자와 유사하다.
<div th:object="${session.user}">
	<p>Age: <span th:text="*{age}?: '(no age specified)'">27</span></p>
</div>

<!-- 위의 표현식과 아래 표현식과 동일 -->
<p>Age: <span th:text="*{age != null}? *{age} : '(no age specified)'">27</span></p>


<p>Name: <span th:text="*{firstName}?: (*{admin}? 'Admin' : #{default.username})">Sebastian</span></p>

Local Variables (지역변수)

JSTL의 <c:set /> 같이 변수를 만들어 사용하고 싶을 땐 다음과 같이 사용한다. 단, 해당 태그가 끝나기 전 범위 내에서만 사용이 가능하다.

<th:block th:with="temp = ${data}, example = ${foo}">
	<span th:text="${temp}">text</span>
</th:block>
<div th:text="${example}"></div> (사용 불가)

th:text / th:utext

  • th:text -> Model에 HTML 태그가 담겨있어도 '문자열'로써 출력
  • th:utext -> Model에 HTML 태그가 담겨있으면 HTML의 태그로써 출력(u: unescaped)
<!-- model.addAttribute("data", "Hello World <b>How are you?</b>"); -->

<div th:text=${data}></div>
<!-- Hello World~ <b>How are you?</b> -->

<div th:utext="${data}"></div>
<!-- Hello World~ How are you? (How are you 부분 bold 처리) --> 

반복 - each

<!-- model.addAttribute("noticeList", list); -->

<tr th:each="notice : ${noticeList}" th:object="${notice}">
	<td th:text="*{id}">B</td>
    <td class="title indent text-align-left"><a th:text="*{title}" th:href="@{detail(id=*{id}))}}" href="detail.html>예제 코드</a></td>
    <td th:text="*{writerId}">newlec</td>
    <td th:text="*{regDate}">2022-08-06</td>
    <td><input type="checkbox" th:name="open"></td>
    <td><input type="checkbox" th:name="del"></td>
</tr>
  • th:each="[변수명]: ${모델 키}"로 받아서 ${[변수명].title} 이런식으로 사용한다.
    위의 예제에서는 th:object로 한번 더 선언해서 [변수명]. 과정을 생략했다

Template Layout

  • th:insert: 파일(태그)의 내용이 삽입
  • th:replace: 파일(태그)의 내용으로 대체
  • th:include: 파일(태그)의 내용을 포함
<body>
	<div th:insert="footer :: copy"></div>
    <div th:replace="footer :: copy"></div>
    <div th:include="footer :: copy"></div>
</body>

<!-- ---------------------------------------------- -->
<body>
	// insert
    <div>
    	<footer>
        	&copy; 2022
        </footer>
    </div>
    
    // replace
    <footer>
    	&copy; 2022
    </footer>
    
    // include
    <div>
    	&copy; 2022
    </div>
    
</body>

ex)

<!-- header.html -->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<header id="header" th:fragment="hd">

    <div class="content-container">
        <!-- ---------------------------<header>--------------------------------------- -->

        <h1 id="logo">
            <a href="/index.html">
                <img src="/images/logo.png" alt="OOO 온라인" />

            </a>
        </h1>

        <section>
            <h1 class="hidden">헤더</h1>

            <nav id="main-menu">
                <h1>메인메뉴</h1>
                <ul>
                    <li><a href="/guide">학습가이드</a></li>

                    <li><a href="/course">강좌선택</a></li>
                    <li><a href="/answeris/index">AnswerIs</a></li>
                </ul>
            </nav>

            <div class="sub-menu">

                <section id="search-form">
                    <h1>강좌검색 폼</h1>
                    <form action="/course">
                        <fieldset>
                            <legend>과정검색필드</legend>
                            <label>과정검색</label>
                            <input type="text" name="q" value="" />
                            <input type="submit" value="검색" />
                        </fieldset>
                    </form>
                </section>

                <nav id="acount-menu">
                    <h1 class="hidden">회원메뉴</h1>
                    <ul>
                        <li><a href="/index.html">HOME</a></li>
                        <li><a href="/member/login.html">로그인</a></li>
                        <li><a href="/member/agree.html">회원가입</a></li>
                    </ul>
                </nav>

                <nav id="member-menu" class="linear-layout">
                    <h1 class="hidden">고객메뉴</h1>
                    <ul class="linear-layout">
                        <li><a href="/member/home"><img src="/images/txt-mypage.png" alt="마이페이지" /></a></li>
                        <li><a href="/notice/list.html"><img src="/images/txt-customer.png" alt="고객센터" /></a></li>
                    </ul>
                </nav>

            </div>
        </section>

    </div>

</header>
</html>

~{...}

  • 단편 조각의 경로(Path)를 의미한다. Fragment Expression으로 파일의 경로를 의미하고자 할 때 사용한다.
  • ~{}표시는 생략 가능하다.
  • @{...}랑은 조금 다르다. @{...}은 URL을 의미한다
<!-- list.html -->
<body>
	<th:block th:replace="include/header"></th:block> <!-- 루트를 의미할 때 '/'를 사용하지 않아도 된다. -->
</body>

위의 예시처럼 파일 자체(header.html)를 끼워넣는 것은 HTML 구조가 깨지기도 한다.
이를 극복하기위해 Fragment를 이용한다.

위의 내용을 토대로 하면, 다음과 같이 수정할 수 있다.

<!-- list.html -->

<body>
	<th:block th:replace="include/header :: #header}"></th:block> <!-- CSS 선택자 사용 -->
</body>

Template Layout Pro.ver

  • 모든 파일에서 th:replace="include/header :: header", th:replace="include/footer :: footer" 같은 작업을 해주어야 한다.
    만약 이를 하나의 파일(layout)이라는 곳에 정의해두고, main 부분만 작성해서 layout에 꽂아넣고 싶다면 어떻게 해야할까?

먼저 방법은 인자(parameter)를 이용하는 것이다.

<!-- layout.html -->

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org>

<body th:fragment="body(main)">
<!-- header -->
<th:block th:replace="include/header::header"></th:block>
<!-- visual -->
<div id="visual">
	<div class="content-container"></div>
</div>
<!-- body -->
<div id="body">
    <div class="content-container clearfix">

        <!--aside-->
        <aside class="aside">
            <h1>ADMIN PAGE</h1>

            <nav class="menu text-menu first margin-top">
                <h1>마이페이지</h1>
                <ul>
                    <li><a href="/admin/index.html">관리자홈</a></li>
                    <li><a href="/teacher/index.html">선생님페이지</a></li>
                    <li><a href="/student/index.html">수강생페이지</a></li>
                </ul>
            </nav>

            <nav class="menu text-menu">
                <h1>알림관리</h1>
                <ul>
                    <li><a href="/admin/board/notice/list.html">공지사항</a></li>
                </ul>
            </nav>

        </aside>
        
        <!--main-->
        <th:block th:replace="${main}"></th:block>
        
    </div>
</div>

<!--footer-->
<th:block th:replace="~{include/footer :: footer}"></th:block>

</body>
</html>
태그를 보면 th:fragment="body(main)"과 같은 형태를 띄고, 밑에 main 주석 부분에 `th:block th:replace="${main}">` 과 같이 "변수"를 받아 대체하는 형태를 띈다. 이를 사용하려면 main을 작성한 곳에서는 아래와 같이 코드를 작성한다.
<!-- main이 있는 파일 -->

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">

<head>
...
</head>

<!-- admin/include/layout 파일의 th:fragment="body"로 대체를 하라 -->
<!-- 그런데 대체 할 때 인자를 넘겨야 한다. 이 파일의 th:fragment="main"을 넘긴다 -->
<!-- layout 파일에서 th:fragment="body(var)로 선언하고, 하부에서 th:replace="${var}"를 쓰면 현재 파일의 main이 끼워지게 된다. -->

<th:block th:replace="admin/include/layout :: body(~{this::main})}">
	<main class="main" th:fragment="main">

라이브러리 사용해서 레이아웃 설정

먼저 thymeleaf 레이아웃 라이브러리를 추가한다.

<dependency>
	<groupId>nz.net.ultraq.thymeleaf</groupId>
    <artifactId>thymeleaf-layout-dialect</artifactId>
</dependency>

그 다음 레이아웃으로 쓸 페이지(파일)에 xml namespace를 추가한다.

<!-- layout.html -->

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
	xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
  • layout 파일에서 main에 해당하는 조각(fragment)가 필요한 곳에 다음과 같이 선언한다.
<!-- main -->
<th:block layout:fragment="main"></th:block>

이후 main을 갖고있는 파일에서 다음과 같이 설정한다.

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
	xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
    layout:decorator="~{/admin/include/layout}">
<!-- layout:decorator는 현재  파일에 레이아웃에 끼워넣을 것이 있으니  대상을 지정한다.

<main class="main" layout:fragment="main">
...
</main>

#

  • layout에서 "main"이라는 이름으로 쓰일 조각이라고 선언

#

  • 끼워넣을 layout 파일이 해당 경로에 있다고 알려줌

    각 페이지마다 독립적으로 필요한 CSS, JS, Title등은 레이아웃에 어떻게 추가하나요?

  • 끼워넣으려는 파일에 기입해두면 저절로 포함된다.

thymeleaf decoupled

설정

  // ThymeleafConfig.java -- application.yml에 설정값을 세팅할 수 있도록 + decoupled 설정

import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.ConstructorBinding;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.thymeleaf.spring5.templateresolver.SpringResourceTemplateResolver;

@Configuration
public class ThymeleafConfig{
	@Bean
  	public SpringResourceTemplateResolver thymeleafTemplateResolver(
  		SpringResourceTemplateResolver defaultTemplateResolver,
  		Thymeleaf3Properties thymeleaf3Properties){
  		defaultTemplateResolver.setUseDecoupledLogic(thymeleaf3Properties.isDecoupledLogic());
  
  		return defaultTemplateResolver;
  	}
}

@RequiredArgsConstructor
@Getter
@ConstructorBinding // application.yml에서 자동완성으로 사용할 수 있도록
@ConfigurationProperties("spring.thymeleaf3") // application.yml에서 spring: thymeleaf3 : 에 정의된 값을 가져옴
public static class Thymeleaf3Properties{
	/**
  		Use Thymeleaf 3 Decoupled Logic
  	**/
  	private final boolean decoupledLogic;
}
// build.gradle
  
<dependency>
	implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
	implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5'
	 annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
</dependency>
// application.yml
spring:
  thymeleaf3:
  	decoupled-logic: true
  • 파일 경로
// index.html
<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="description" content="">
    <meta name="author" content="shbae">
    <title>게시판 페이지</title>

    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0-beta1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-0evHe/X+R7YkIZDRvuzKMRqM+OrBnVFBL6DOitfPri4tjfHxaWutUpFmBp4vmVor" crossorigin="anonymous">
    <link href="/css/search-bar.css" rel="stylesheet">
    <link href="/css/articles/table-header.css" rel="stylesheet">
</head>

<body>
<header id="header">
    헤더 삽입부
    <hr>
</header>

<main class="container">

    <div class="row">
        <div class="card card-margin search-form">
            <div class="card-body p-0">
                <form id="search-form">
                    <div class="row">
                        <div class="col-12">
                            <div class="row no-gutters">
                                <div class="col-lg-3 col-md-3 col-sm-12 p-0">
                                    <label for="search-type" hidden>검색 유형</label>
                                    <select class="form-control" id="search-type" name="searchType">
                                        <option>제목</option>
                                        <option>본문</option>
                                        <option>id</option>
                                        <option>닉네임</option>
                                        <option>해시태그</option>
                                    </select>
                                </div>
                                <div class="col-lg-8 col-md-6 col-sm-12 p-0">
                                    <label for="search-value" hidden>검색어</label>
                                    <input type="text" placeholder="검색어..." class="form-control" id="search-value" name="searchValue">
                                </div>
                                <div class="col-lg-1 col-md-3 col-sm-12 p-0">
                                    <button type="submit" class="btn btn-base">
                                        <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-search">
                                            <circle cx="11" cy="11" r="8"></circle>
                                            <line x1="21" y1="21" x2="16.65" y2="16.65"></line>
                                        </svg>
                                    </button>
                                </div>
                            </div>
                        </div>
                    </div>
                </form>
            </div>
        </div>
    </div>

    <div class="row">
        <table class="table" id="article-table">
            <thead>
            <tr>
                <th class="title col-6"><a>제목</a></th>
                <th class="hashtag col-2"><a>해시태그</a></th>
                <th class="user-id"><a>작성자</a></th>
                <th class="created-at"><a>작성일</a></th>
            </tr>
            </thead>
            <tbody>
            <tr>
                <td class="title"><a>첫글</a></td>
                <td class="hashtag">#java</td>
                <td class="user-id">Uno</td>
                <td class="created-at"><time>2022-01-01</time></td>
            </tr>
            <tr>
                <td>두번째글</td>
                <td>#spring</td>
                <td>Uno</td>
                <td><time>2022-01-02</time></td>
            </tr>
            <tr>
                <td>세번째글</td>
                <td>#java</td>
                <td>Uno</td>
                <td><time>2022-01-03</time></td>
            </tr>
            </tbody>
        </table>
    </div>

    <div class="row">
        <div class="d-grid gap-2 d-md-flex justify-content-md-end">
            <a class="btn btn-primary me-md-2" role="button" id="write-article">글쓰기</a>
        </div>
    </div>

    <div class="row">
        <nav id="pagination" aria-label="Page navigation">
            <ul class="pagination justify-content-center">
                <li class="page-item"><a class="page-link" href="#">Previous</a></li>
                <li class="page-item"><a class="page-link" href="#">1</a></li>
                <li class="page-item"><a class="page-link" href="#">Next</a></li>
            </ul>
        </nav>
    </div>
</main>

<footer id="footer">
    <hr>
    푸터 삽입부
</footer>

<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0-beta1/dist/js/bootstrap.bundle.min.js" integrity="sha384-pprn3073KE6tl6bjs2QrFaJGz5/SUsLqktiwsUTF55Jfv3qYSDhgCecCxMW52nD2" crossorigin="anonymous"></script>
</body>
</html>
// index.th.xml
<?xml version="1.0"?>
<thlogic>
    <attr sel="#header" th:replace="header :: header" />
    <attr sel="#footer" th:replace="footer :: footer" />

    <attr sel="main" th:object="${articles}">
        <attr sel="#search-form" th:action="@{/articles}" th:method="get" />
        <attr sel="#search-type" th:remove="all-but-first">
            <attr sel="option[0]"
                  th:each="searchType : ${searchTypes}"
                  th:value="${searchType.name}"
                  th:text="${searchType.description}"
                  th:selected="${param.searchType != null && (param.searchType.toString == searchType.name)}"
            />
        </attr>
        <attr sel="#search-value" th:value="${param.searchValue}" />

        <attr sel="#article-table">
            <attr sel="thead/tr">
                <attr sel="th.title/a" th:text="'제목'" th:href="@{/articles(
            page=${articles.number},
            sort='title' + (*{sort.getOrderFor('title')} != null ? (*{sort.getOrderFor('title').direction.name} != 'DESC' ? ',desc' : '') : ''),
            searchType=${param.searchType},
            searchValue=${param.searchValue}
        )}"/>
                <attr sel="th.hashtag/a" th:text="'해시태그'" th:href="@{/articles(
            page=${articles.number},
            sort='hashtag' + (*{sort.getOrderFor('hashtag')} != null ? (*{sort.getOrderFor('hashtag').direction.name} != 'DESC' ? ',desc' : '') : ''),
            searchType=${param.searchType},
            searchValue=${param.searchValue}
        )}"/>
                <attr sel="th.user-id/a" th:text="'작성자'" th:href="@{/articles(
            page=${articles.number},
            sort='userAccount.userId' + (*{sort.getOrderFor('userAccount.userId')} != null ? (*{sort.getOrderFor('userAccount.userId').direction.name} != 'DESC' ? ',desc' : '') : ''),
            searchType=${param.searchType},
            searchValue=${param.searchValue}
        )}"/>
                <attr sel="th.created-at/a" th:text="'작성일'" th:href="@{/articles(
            page=${articles.number},
            sort='createdAt' + (*{sort.getOrderFor('createdAt')} != null ? (*{sort.getOrderFor('createdAt').direction.name} != 'DESC' ? ',desc' : '') : ''),
            searchType=${param.searchType},
            searchValue=${param.searchValue}
        )}"/>
            </attr>

            <attr sel="tbody" th:remove="all-but-first">
                <attr sel="tr[0]" th:each="article : ${articles}">
                    <attr sel="td.title/a" th:text="${article.title}" th:href="@{'/articles/' + ${article.id}}" />
                    <attr sel="td.hashtag" th:text="${article.hashtag}" />
                    <attr sel="td.user-id" th:text="${article.nickname}" />
                    <attr sel="td.created-at/time" th:datetime="${article.createdAt}" th:text="${#temporals.format(article.createdAt, 'yyyy-MM-dd')}" />
                </attr>
            </attr>
        </attr>

        <attr sel="#write-article" sec:authorize="isAuthenticated()" th:href="@{/articles/form}" />

        <attr sel="#pagination">
            <attr sel="li[0]/a"
                  th:text="'previous'"
                  th:href="@{/articles(page=${articles.number - 1}, searchType=${param.searchType}, searchValue=${param.searchValue})}"
                  th:class="'page-link' + (${articles.number} <= 0 ? ' disabled' : '')"
            />
            <attr sel="li[1]" th:class="page-item" th:each="pageNumber : ${paginationBarNumbers}">
                <attr sel="a"
                      th:text="${pageNumber + 1}"
                      th:href="@{/articles(page=${pageNumber}, searchType=${param.searchType}, searchValue=${param.searchValue})}"
                      th:class="'page-link' + (${pageNumber} == ${articles.number} ? ' disabled' : '')"
                />
            </attr>
            <attr sel="li[2]/a"
                  th:text="'next'"
                  th:href="@{/articles(page=${articles.number + 1}, searchType=${param.searchType}, searchValue=${param.searchValue})}"
                  th:class="'page-link' + (${articles.number} >= ${articles.totalPages - 1} ? ' disabled' : '')"
            />
        </attr>
    </attr>
</thlogic>
profile
성장형 인간

0개의 댓글