IT/development

[thymeleaf] Thymeleaf Layout (feat. sb-admin 2)

어흥꼬비 2024. 12. 22.
반응형

목차

    bootstrap sb-admin 2를 이용한 Thymeleaf Layout 적용

    폴더 구조

    fragments폴더의 sidebar, config, footer 등의 공통으로 사용될 파일조각들을 default_layout.html에서 연결 후 layout을 각 html에 적용해서 사용


    build.gradle

    plugins {
        id 'java'
        id 'org.springframework.boot' version '3.3.7'
        id 'io.spring.dependency-management' version '1.1.7'
    }
    
    group = 'com.duo'
    version = '0.0.1-SNAPSHOT'
    
    java {
        toolchain {
            languageVersion = JavaLanguageVersion.of(21)
        }
    }
    
    configurations {
        compileOnly {
            extendsFrom annotationProcessor
        }
    }
    
    repositories {
        mavenCentral()
    }
    
    dependencies {
        implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    //    implementation 'org.springframework.boot:spring-boot-starter-security'
        implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
        implementation 'org.springframework.boot:spring-boot-starter-web'
        implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.4'
        implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6'
        implementation "commons-codec:commons-codec:1.15"
        implementation 'org.mariadb.jdbc:mariadb-java-client:3.0.7'
        // Logging
        implementation 'org.bgee.log4jdbc-log4j2:log4jdbc-log4j2-jdbc4.1:1.16'
        // thymeleaf-layout
        implementation 'nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect'
        // lombok
        compileOnly 'org.projectlombok:lombok'
        // devtool
        developmentOnly 'org.springframework.boot:spring-boot-devtools'
        runtimeOnly 'org.mariadb.jdbc:mariadb-java-client'
        annotationProcessor 'org.projectlombok:lombok'
        testCompileOnly 'org.projectlombok:lombok'
        // test
        testImplementation 'org.springframework.boot:spring-boot-starter-test'
        testImplementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter-test:3.0.4'
        testImplementation 'org.springframework.security:spring-security-test'
        testImplementation 'org.assertj:assertj-core:3.24.0'
        // Lombok을 테스트에서도 사용할 수 있도록 추가
        testAnnotationProcessor 'org.projectlombok:lombok'
        testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
    }
    
    tasks.named('test') {
        useJUnitPlatform()
    }

    config.html

    <!DOCTYPE html>
    <html lang="ko"
          xmlns:th="http://www.thymeleaf.org">
    
    <th:block th:fragment="configFragment">
        <head>
            <meta charset="utf-8">
            <meta http-equiv="X-UA-Compatible" content="IE=edge">
            <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
            <meta name="description" content="">
            <meta name="author" content="devLsy">
    
            <title>게시판</title>
    
            <!-- Custom fonts for this template-->
            <link th:href="@{vendor/fontawesome-free/css/all.min.css}" rel="stylesheet" type="text/css">
            <link
                    href="https://fonts.googleapis.com/css?family=Nunito:200,200i,300,300i,400,400i,600,600i,700,700i,800,800i,900,900i"
                    rel="stylesheet">
            <!-- Custom styles for this template-->
            <link th:href="@{css/sb-admin-2.min.css}" rel="stylesheet">
        </head>
    </th:block>

    sidebar.html

    <html lagn="ko" xmlns:th="http://www.thymeleaf.org">
    
    <th:block th:fragment="sidebarFragment">
        <!-- Sidebar -->
        <ul class="navbar-nav bg-gradient-primary sidebar sidebar-dark accordion" id="accordionSidebar">
            <!-- Sidebar - Brand -->
            <a class="sidebar-brand d-flex align-items-center justify-content-center" th:href="@{/}">
                <div class="sidebar-brand-icon rotate-n-15">
                    <i class="fas fa-laugh-wink"></i>
                </div>
                <div class="sidebar-brand-text mx-3">게시판</div>
            </a>
    
            <!-- Divider -->
            <hr class="sidebar-divider my-0">
    
            <!-- Nav Item - Pages Collapse Menu -->
            <li class="nav-item">
                <a class="nav-link collapsed" href="#" data-toggle="collapse" data-target="#collapsePages"
                   aria-expanded="true" aria-controls="collapsePages">
                    <i class="fas fa-fw fa-table"></i>
                    <span>리스트</span>
                </a>
                <div id="collapsePages" class="collapse" aria-labelledby="headingPages" data-parent="#accordionSidebar">
                    <div class="bg-white py-2 collapse-inner rounded">
    <!--                    <h6 class="collapse-header">리스트</h6>-->
                        <a class="collapse-item" th:href="@{/dining}">리스트</a>
                    </div>
                </div>
            </li>
        </ul>
        <!-- End of Sidebar -->
    </th:block>
    </html>

    header.html

    <html lagn="ko" xmlns:th="http://www.thymeleaf.org">
    
    <th:block th:fragment="headerFragment">
    
    <nav class="navbar navbar-expand navbar-light bg-white topbar mb-4 static-top shadow">
    
        <!-- Sidebar Toggle (Topbar) -->
        <button id="sidebarToggleTop" class="btn btn-link d-md-none rounded-circle mr-3">
            <i class="fa fa-bars"></i>
        </button>
    
        <!-- Topbar Search -->
        <form
                class="d-none d-sm-inline-block form-inline mr-auto ml-md-3 my-2 my-md-0 mw-100 navbar-search">
            <div class="input-group">
                <input type="text" class="form-control bg-light border-0 small" placeholder="Search for..."
                       aria-label="Search" aria-describedby="basic-addon2">
                <div class="input-group-append">
                    <button class="btn btn-primary" type="button">
                        <i class="fas fa-search fa-sm"></i>
                    </button>
                </div>
            </div>
        </form>
    
        <!-- Topbar Navbar -->
        <ul class="navbar-nav ml-auto">
    
            <!-- Nav Item - Search Dropdown (Visible Only XS) -->
            <li class="nav-item dropdown no-arrow d-sm-none">
                <a class="nav-link dropdown-toggle" href="#" id="searchDropdown" role="button"
                   data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
                    <i class="fas fa-search fa-fw"></i>
                </a>
                <!-- Dropdown - Messages -->
                <div class="dropdown-menu dropdown-menu-right p-3 shadow animated--grow-in"
                     aria-labelledby="searchDropdown">
                    <form class="form-inline mr-auto w-100 navbar-search">
                        <div class="input-group">
                            <input type="text" class="form-control bg-light border-0 small"
                                   placeholder="Search for..." aria-label="Search"
                                   aria-describedby="basic-addon2">
                            <div class="input-group-append">
                                <button class="btn btn-primary" type="button">
                                    <i class="fas fa-search fa-sm"></i>
                                </button>
                            </div>
                        </div>
                    </form>
                </div>
            </li>
    
            <!-- Nav Item - Alerts -->
            <li class="nav-item dropdown no-arrow mx-1">
                <a class="nav-link dropdown-toggle" href="#" id="alertsDropdown" role="button"
                   data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
                    <i class="fas fa-bell fa-fw"></i>
                    <!-- Counter - Alerts -->
                    <span class="badge badge-danger badge-counter">3+</span>
                </a>
                <!-- Dropdown - Alerts -->
                <div class="dropdown-list dropdown-menu dropdown-menu-right shadow animated--grow-in"
                     aria-labelledby="alertsDropdown">
                    <h6 class="dropdown-header">
                        Alerts Center
                    </h6>
                    <a class="dropdown-item d-flex align-items-center" href="#">
                        <div class="mr-3">
                            <div class="icon-circle bg-primary">
                                <i class="fas fa-file-alt text-white"></i>
                            </div>
                        </div>
                        <div>
                            <div class="small text-gray-500">December 12, 2019</div>
                            <span class="font-weight-bold">A new monthly report is ready to download!</span>
                        </div>
                    </a>
                    <a class="dropdown-item d-flex align-items-center" href="#">
                        <div class="mr-3">
                            <div class="icon-circle bg-success">
                                <i class="fas fa-donate text-white"></i>
                            </div>
                        </div>
                        <div>
                            <div class="small text-gray-500">December 7, 2019</div>
                            $290.29 has been deposited into your account!
                        </div>
                    </a>
                    <a class="dropdown-item d-flex align-items-center" href="#">
                        <div class="mr-3">
                            <div class="icon-circle bg-warning">
                                <i class="fas fa-exclamation-triangle text-white"></i>
                            </div>
                        </div>
                        <div>
                            <div class="small text-gray-500">December 2, 2019</div>
                            Spending Alert: We've noticed unusually high spending for your account.
                        </div>
                    </a>
                    <a class="dropdown-item text-center small text-gray-500" href="#">Show All Alerts</a>
                </div>
            </li>
    
            <!-- Nav Item - Messages -->
            <li class="nav-item dropdown no-arrow mx-1">
                <a class="nav-link dropdown-toggle" href="#" id="messagesDropdown" role="button"
                   data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
                    <i class="fas fa-envelope fa-fw"></i>
                    <!-- Counter - Messages -->
                    <span class="badge badge-danger badge-counter">7</span>
                </a>
                <!-- Dropdown - Messages -->
                <div class="dropdown-list dropdown-menu dropdown-menu-right shadow animated--grow-in"
                     aria-labelledby="messagesDropdown">
                    <h6 class="dropdown-header">
                        Message Center
                    </h6>
                    <a class="dropdown-item d-flex align-items-center" href="#">
                        <div class="dropdown-list-image mr-3">
                            <img class="rounded-circle" src="img/undraw_profile_1.svg"
                                 alt="...">
                            <div class="status-indicator bg-success"></div>
                        </div>
                        <div class="font-weight-bold">
                            <div class="text-truncate">Hi there! I am wondering if you can help me with a
                                problem I've been having.</div>
                            <div class="small text-gray-500">Emily Fowler · 58m</div>
                        </div>
                    </a>
                    <a class="dropdown-item d-flex align-items-center" href="#">
                        <div class="dropdown-list-image mr-3">
                            <img class="rounded-circle" src="img/undraw_profile_2.svg"
                                 alt="...">
                            <div class="status-indicator"></div>
                        </div>
                        <div>
                            <div class="text-truncate">I have the photos that you ordered last month, how
                                would you like them sent to you?</div>
                            <div class="small text-gray-500">Jae Chun · 1d</div>
                        </div>
                    </a>
                    <a class="dropdown-item d-flex align-items-center" href="#">
                        <div class="dropdown-list-image mr-3">
                            <img class="rounded-circle" src="img/undraw_profile_3.svg"
                                 alt="...">
                            <div class="status-indicator bg-warning"></div>
                        </div>
                        <div>
                            <div class="text-truncate">Last month's report looks great, I am very happy with
                                the progress so far, keep up the good work!</div>
                            <div class="small text-gray-500">Morgan Alvarez · 2d</div>
                        </div>
                    </a>
                    <a class="dropdown-item d-flex align-items-center" href="#">
                        <div class="dropdown-list-image mr-3">
                            <img class="rounded-circle" src="https://source.unsplash.com/Mv9hjnEUHR4/60x60"
                                 alt="...">
                            <div class="status-indicator bg-success"></div>
                        </div>
                        <div>
                            <div class="text-truncate">Am I a good boy? The reason I ask is because someone
                                told me that people say this to all dogs, even if they aren't good...</div>
                            <div class="small text-gray-500">Chicken the Dog · 2w</div>
                        </div>
                    </a>
                    <a class="dropdown-item text-center small text-gray-500" href="#">Read More Messages</a>
                </div>
            </li>
    
            <div class="topbar-divider d-none d-sm-block"></div>
    
            <!-- Nav Item - User Information -->
            <li class="nav-item dropdown no-arrow">
                <a class="nav-link dropdown-toggle" href="#" id="userDropdown" role="button"
                   data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
                    <span class="mr-2 d-none d-lg-inline text-gray-600 small">Douglas McGee</span>
                    <img class="img-profile rounded-circle"
                         src="img/undraw_profile.svg">
                </a>
                <!-- Dropdown - User Information -->
                <div class="dropdown-menu dropdown-menu-right shadow animated--grow-in"
                     aria-labelledby="userDropdown">
                    <a class="dropdown-item" href="#">
                        <i class="fas fa-user fa-sm fa-fw mr-2 text-gray-400"></i>
                        Profile
                    </a>
                    <a class="dropdown-item" href="#">
                        <i class="fas fa-cogs fa-sm fa-fw mr-2 text-gray-400"></i>
                        Settings
                    </a>
                    <a class="dropdown-item" href="#">
                        <i class="fas fa-list fa-sm fa-fw mr-2 text-gray-400"></i>
                        Activity Log
                    </a>
                    <div class="dropdown-divider"></div>
                    <a class="dropdown-item" href="#" data-toggle="modal" data-target="#logoutModal">
                        <i class="fas fa-sign-out-alt fa-sm fa-fw mr-2 text-gray-400"></i>
                        Logout
                    </a>
                </div>
            </li>
    
        </ul>
    
    </nav>
    <!-- End of Topbar -->
    </th:block>
    </html>

    footer.html

    <html lagn="ko" xmlns:th="http://www.thymeleaf.org">
        <!--footerFragment 선언-->
        <th:block th:fragment="footerFragment">
            <footer class="sticky-footer bg-white">
                <div class="container my-auto">
                    <div class="copyright text-center my-auto">
                        <span>Copyright &copy; devLsy 2024</span>
                    </div>
                </div>
            </footer>
            <!-- End of Footer -->
        </th:block>
    </html>

    default_layout.html

    <!DOCTYPE html>
    <html lang="ko"
          xmlns:th="http://www.thymeleaf.org"
          xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
    <!-- head -->
    <th:block th:replace="~{fragments/config :: configFragment}"></th:block>
    
    <body>
    <!-- Page Wrapper -->
    <div id="wrapper">
            <!-- sidebar fragment 사용 -->
            <th:block th:replace="~{fragments/sidebar :: sidebarFragment}"></th:block>
        <!-- Content Wrapper -->
        <div id="content-wrapper" class="d-flex flex-column">
            <!-- Main Content -->
            <div id="content">
                <!-- topbar -->
                <th:block th:replace="~{fragments/header :: headerFragment}"></th:block>
                <!-- content fragment 사용 -->
                <th:block layout:fragment="content">
                </th:block>
                <!-- content script -->
                <th:block layout:fragment="script"></th:block>
            </div>
        </div>
    </div>
            <!-- footer fragment 사용 -->
            <th:block th:replace="~{fragments/footer :: footerFragment}"></th:block>
    
    <!-- Bootstrap core JavaScript-->
    <script th:src="@{vendor/jquery/jquery.min.js}"></script>
    <script th:src="@{vendor/bootstrap/js/bootstrap.bundle.min.js}"></script>
    
    <!-- Core plugin JavaScript-->
    <script th:src="@{vendor/jquery-easing/jquery.easing.min.js}"></script>
    
    <!-- Custom scripts for all pages-->
    <script th:src="@{js/sb-admin-2.min.js}"></script>
    
    <!-- Page level plugins -->
    <script th:src="@{vendor/chart.js/Chart.min.js}"></script>
    
    <!-- Page level custom scripts -->
    <script th:src="@{js/demo/chart-area-demo.js}"></script>
    <script th:src="@{js/demo/chart-pie-demo.js}"></script>
    </body>
    </html>

    list.html(thymeleaf layout 적용)

    <!DOCTYPE html>
    <html xmlns:th="http://www.thymeleaf.org"
          xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
          layout:decorate="~{layouts/default_layout}">
    
    <div layout:fragment="content">
        <table class="table">
            <thead>
            <tr>
                <th>맛집 이름</th>
                <th>메인 주소</th>
                <th>상세 주소</th>
            </tr>
            </thead>
            <tbody>
            <tr th:each="list : ${list}">
                <td th:text="${list.name}"></td>
                <td th:text="${list.mainAddress}"></td>
                <td th:text="${list.detailAddress}"></td>
            </tr>
            </tbody>
        </table>
    </div>
    
    </html>

    적용 결과

    layout 적용한 html

    만일 layout을 세팅해 두지 않으면 반복적으로 사용되는 config, header, sidebar, footer를 모든 html에 다 적용해줘야 된다.(만일 html파일이 200개라면?? 😨)

    레이아웃을 사용하면 content영역만 작성하면 된다.


    개인 스터디 기록을 메모하는 공간이라 틀린점이 있을 수 있습니다.

    틀린 점 있을 경우 댓글 부탁드립니다.

    startbootstrap-sb-admin-2-gh-pages.zip
    6.32MB

     

    반응형

    댓글

    💲 추천 글