IT/development

[springBoot/thymeleaf] ajax 페이지네이션 sample(feat. study용)

알 수 없는 사용자 2023. 6. 4. 10:27
반응형

목차

    ddl(Oracle)

    /* 게시판 */
    CREATE TABLE "BOARD_INFO"
       ("BOARD_NO" NUMBER NOT NULL ENABLE,
    	"TITLE" VARCHAR2(50) NOT NULL ENABLE, 
    	"CONTENT" VARCHAR2(4000) NOT NULL ENABLE, 
    	"USER_ID" VARCHAR2(20) NOT NULL ENABLE,
    	"USE_YN" CHAR(1) DEFAULT 'Y' NOT NULL ENABLE, 
    	"REG_DATE" DATE DEFAULT SYSDATE NOT NULL ENABLE, 
    	"MOD_DATE" DATE, 
    	 CONSTRAINT "BOARD_INFO_PK" PRIMARY KEY ("BOARD_NO")
    ); 
    COMMENT ON COLUMN BOARD_INFO.BOARD_NO IS '게시글 순번(시퀀스: BOARD_NO_SEQ)';
    COMMENT ON COLUMN BOARD_INFO.TITLE IS '제목';
    COMMENT ON COLUMN BOARD_INFO.CONTENT IS '내용';
    COMMENT ON COLUMN BOARD_INFO.USER_ID IS '작성자 아이디';
    COMMENT ON COLUMN BOARD_INFO.USE_YN IS '사용여부';
    COMMENT ON COLUMN BOARD_INFO.REG_DATE IS '등록일';
    COMMENT ON COLUMN BOARD_INFO.MOD_DATE IS '수정일';
    --시퀀스
    CREATE SEQUENCE BOARD_NO_SEQ INCREMENT BY 1 MINVALUE 1 MAXVALUE 99999999 CYCLE NOCACHE ORDER ;

    Vo

    package study.thboard2.domain.vo;
    
    import lombok.Data;
    
    import javax.validation.constraints.NotBlank;
    
    @Data
    //게시판 관련 vo
    public class BoardVo extends CommonVo{
        
        private Integer no;                             //게시글 rownum
        private Integer boardNo;                       //게시글 순번(시퀀스)
        
        @NotBlank(message = "제목은 꼭 입력해야 해")
        private String title;                       //제목
    
        private String userId;                      //작성자
    
        @NotBlank(message = "내용도 반드시 알아야겠지?")
        private String content;                     //내용
    
        private char useYn;                         //사용여부
    }
    package study.thboard2.domain.vo;
    
    import lombok.Data;
    
    @Data
    //공통 vo
    public class CommonVo {
        /*검색 필드*/
        private String type;                              //검색 타입
        private String keyword;                           //검색 키워드
        
        /* 페이지네이션 필드 */
        private int currentPage = 1;                          //현재 페이지 번호
        private int totalCount;                           //전체 게시글 개수
        private int firstRecordIndex, lastRecordIndex;    // 페이징 SQL에서 사용하는 첫번 째, 마지막 인덱스
        
        /* 날짜 필드 */
        private String regDate;                           //등록일
        private String modDate;                           //수정일
    }

    xml

    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://www.mybatis.org/dtd/mybatis-3-mapper.dtd">
    
    <mapper namespace="study.thboard2.mapper.BoardMapper">
    
    <!-- 검색 parameter값 -->
    <sql id="appendSearch">
        <!-- 제목 -->
        <if test="type == 'T'.toString()">
            AND TITLE LIKE '%' || #{keyword} || '%'
        </if>
        <!-- 내용 -->
        <if test="type == 'C'.toString()">
            AND CONTENT LIKE '%' || #{keyword} || '%'
        </if>
        <!-- 작성자 -->
        <if test="type == 'W'.toString()">
            AND USER_ID LIKE '%' || #{keyword} || '%'
        </if>
    </sql>
    
        <!--게시글 목록 조회-->
        <select id="selectBoardList" parameterType="CommonVo" resultType="BoardVo">
            /* 게시글 목록 조회 */
             SELECT T2.no
                  , T2.BOARD_NO AS boardNo
                  , T2.TITLE AS title
                  , T2.CONTENT AS content
                  , T2.USER_ID AS userId
                  , T2.USE_YN AS userYn
                  , TO_CHAR(T2.REG_DATE, 'YYYY-MM-DD') AS regDate
                  , NVL(TO_CHAR(T2.MOD_DATE, 'YYYY-MM-DD'),'수정사항 없음') AS modDate
               FROM
                   (
                        SELECT ROWNUM AS no
                             , T1.BOARD_NO
                             , T1.TITLE
                             , T1.CONTENT
                             , T1.USER_ID
                             , T1.USE_YN
                             , T1.REG_DATE
                             , T1.MOD_DATE
                          FROM
                                  ( SELECT BOARD_NO
                                         , TITLE
                                         , CONTENT
                                         , USER_ID
                                         , USE_YN
                                         , REG_DATE
                                         , MOD_DATE
                                      FROM BOARD_INFO
                                     WHERE 1=1
                                       AND USE_YN = 'Y'
                                    <include refid="appendSearch"/>
                                     ORDER BY REG_DATE DESC ) T1
               <![CDATA[ WHERE ROWNUM <= #{lastRecordIndex} ]]>) T2
             WHERE T2.no >= #{firstRecordIndex}
        </select>
    
        <!--게시글 전체 카운트-->
        <select id="selectBoardCnt" parameterType="CommonVo" resultType="int">
            SELECT COUNT(*)
              FROM BOARD_INFO
             <where>
             <include refid="appendSearch"/>
             </where>
        </select>
    </mapper>

    mapper

    package study.thboard2.mapper;
    
    import org.apache.ibatis.annotations.Mapper;
    import org.apache.ibatis.annotations.Param;
    import org.springframework.stereotype.Repository;
    import study.thboard2.domain.vo.BoardVo;
    import study.thboard2.domain.vo.CommonVo;
    
    import java.util.List;
    
    @Repository @Mapper
    public interface BoardMapper {
    
        /*게시글 목록 조회*/
        List<BoardVo> selectBoardList(CommonVo commonVo);
    
        /*게시글 전체 카운트*/
        int selectBoardCnt(CommonVo commonVo);
    }

    service

    package study.thboard2.service;
    
    import lombok.RequiredArgsConstructor;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Transactional;
    import org.springframework.validation.BindingResult;
    import study.thboard2.domain.vo.BoardVo;
    import study.thboard2.domain.vo.CommonVo;
    import study.thboard2.domain.vo.PaginationInfo;
    import study.thboard2.mapper.BoardMapper;
    
    import java.util.List;
    
    import static study.thboard2.common.utils.ValidationUtil.invokeErrors;
    
    @Service
    @Slf4j
    @RequiredArgsConstructor
    @Transactional(readOnly = true)
    public class BoardService extends CommonService{
    
        private final BoardMapper boardMapper;
    
        /**
         * 게시글 목록 조회
         * @param commonVo
         * @return
         * @throws Exception
         */
        public List<BoardVo> getBoardList(CommonVo commonVo) throws Exception{
            return boardMapper.selectBoardList(commonVo);
        }
    
        /**
         * 게시글 전체 카운트
         * @param commonVo
         * @return
         */
        public int getBoardCnt(CommonVo commonVo) throws Exception{
            return boardMapper.selectBoardCnt(commonVo);
        }
    }
    package study.thboard2.service;
    
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.stereotype.Service;
    import study.thboard2.domain.vo.CommonVo;
    import study.thboard2.domain.vo.PaginationInfo;
    
    @Service
    @Slf4j
    //공통 서비스
    public class CommonService {
    
        /**
         * 페이지네이션 처리 후 페이지네이션 객체 반환
         * @param commonVo
         * @return
         */
        public PaginationInfo getPaginationInfo(CommonVo commonVo) {
            PaginationInfo paging = new PaginationInfo();
            paging.setTotalCount(commonVo.getTotalCount());
            paging.setCurrentPage(commonVo.getCurrentPage());
            return paging;
        }
        
    }

    pagination

    package study.thboard2.domain.vo;
    
    import lombok.Getter;
    import lombok.Setter;
    import lombok.ToString;
    
    @ToString
    public class PaginationInfo {
    
     // 페이지네이션 처리를 위한 parameter값
        @Getter @Setter
        private int currentPage = 1;            // 현재 페이지 번호 (초기값 : 1)
        @Getter @Setter
        private int totalCount; 		        // 전체 목록 건수 (목록)
        @Getter @Setter
        private int recordCountPerPage = 10;	// 페이지당 목록 건수 (목록)
        @Getter @Setter
        private int pageSize = 10; 		        // 페이징 목록에 게시되는 페이지 건수 (페이징)
    
    // 페이지네이션 계산된 값(외부에서 값이 변경되면 안되기에 @getter, @setter 미설정)    
        private int totalPageCount;		        // 전체 페이지 건수 (페이징)
        private int firstPageNo; 		        // 페이지 목록의 첫 페이지 번호 (페이징)
        private int lastPageNo; 		        // 페이지 목록의 마지막 페이지 번호 (페이징)
        private int firstRecordIndex;	        // 페이징 SQL의 조건절에 사용되는 시작 Index (SQL)
        private int lastRecordIndex;	        // 페이징 SQL의 조건절에 사용되는 마지막 Index (SQL)
    
    
        public int getTotalPageCount() {
            totalPageCount = ((getTotalCount() - 1) / getRecordCountPerPage()) + 1;
            return totalPageCount;
        }
    
        public int getFirstPageNo() {
            firstPageNo = ((getCurrentPage() - 1) / getPageSize()) * getPageSize() + 1;
            return firstPageNo;
        }
    
        public int getLastPageNo() {
            lastPageNo = getFirstPageNo() + getPageSize() - 1;
            if(lastPageNo > getTotalPageCount()) {
                lastPageNo = getTotalPageCount();
            }
            return lastPageNo;
        }
    
        public int getFirstRecordIndex() {
            firstRecordIndex = (getCurrentPage() - 1) * getRecordCountPerPage() + 1;
            return firstRecordIndex;
        }
    
        public int getLastRecordIndex() {
            lastRecordIndex = getCurrentPage() * getRecordCountPerPage();
            return lastRecordIndex;
        }
    
        public int getFirstPage() {
            return 1;
        }
        public int getLastPage() {
            return getTotalPageCount();
        }
    
    }

    controller

    package study.thboard2.controller;
    
    import lombok.RequiredArgsConstructor;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.http.HttpStatus;
    import org.springframework.http.MediaType;
    import org.springframework.http.ResponseEntity;
    import org.springframework.stereotype.Controller;
    import org.springframework.validation.BindingResult;
    import org.springframework.web.bind.annotation.*;
    import org.springframework.web.multipart.MultipartFile;
    import org.springframework.web.servlet.ModelAndView;
    import study.thboard2.domain.vo.*;
    import study.thboard2.service.BoardService;
    import study.thboard2.service.FileService;
    import study.thboard2.service.ReplyService;
    
    import javax.servlet.http.HttpSession;
    import javax.validation.Valid;
    import java.util.HashMap;
    import java.util.List;
    import java.util.Map;
    
    @Controller
    @Slf4j
    @RequiredArgsConstructor
    @RequestMapping("/")
    public class BoardController extends CommonController{
    
        private final BoardService boardService;
        private final ReplyService replyService;
        private final FileService fileService;
    
        
         /**
         * 게시글 목록(ajax)
         * @param commonVo
         * @return
         */
        @PostMapping("listAjax")
        @ResponseBody
        public ResponseEntity<?> listAjax(@ModelAttribute("commonVo") CommonVo commonVo) throws Exception {
            ModelAndView mv = new ModelAndView("pages/main");
    
            Map<String, Object> map = new HashMap<>();
            //전체 게시글 수
            int totalCnt = boardService.getBoardCnt(commonVo);
            commonVo.setTotalCount(totalCnt);
            //페이징 처리 후 반환 객체
            PaginationInfo paging = boardService.getPaginationInfo(commonVo);
            log.info("commonVo = [{}]", commonVo);
            log.info("paging = [{}]", paging);
            commonVo.setFirstRecordIndex(paging.getFirstRecordIndex());
            commonVo.setLastRecordIndex(paging.getLastRecordIndex());
    
            List<BoardVo> boardList = boardService.getBoardList(commonVo);
    
            map.put("list", boardList);
            map.put("paging", paging);
            return new ResponseEntity<>(map, HttpStatus.OK);
        }
    }

    main.html

    <!DOCTYPE html>
    <html xmlns:th="http://www.thymeleaf.org"
          xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
          layout:decorate="~{layouts/default_layout}">
    
    <!-- Content -->
    <div layout:fragment="content">
        <main>
            <div class="container-fluid px-4">
                <h1 class="mt-4">게시글 목록</h1>
                    <form class="d-none d-md-inline-block form-inline ms-auto me-0 me-md-3 my-2 my-md-0" method="get">
                    <div class="input-group">
                        <select name="type" id="type" th:field="${search.type}">
                            <option value="">선택해라</option>
                            <option value="W">작성자</option>
                            <option value="T">제목</option>
                            <option value="C">내용</option>
                        </select>
                        <input class="form-control" th:field="${search.keyword}" type="text" id="keyword" name="keyword" placeholder="검색어를 입력해라." aria-label="Search for..." aria-describedby="btnNavbarSearch" />
                        <button class="btn btn-primary" id="btnSearch" type="button"><i class="fas fa-search"></i></button>
                    </div>
                </form>
                    <div class="card mb-4">
                        <div class="card-header">
                            <i class="fas fa-table me-1"></i>
                            게시글이야.
                        </div>
                        <form id="frm" action="/del" method="post">
                        <div class="card-body">
                            <table id="boardList" class="table table-striped">
                                <thead>
                                    <tr>
                                        <th>순번</th>
                                        <th>제목</th>
                                        <th>내용</th>
                                        <th>작성자</th>
                                        <th>등록일</th>
                                        <th>수정일</th>
                                        <th></th>
                                    </tr>
                                </thead>
                                <!-- 동적 tbody -->
                                <tbody id="boardBody"></tbody>
                            </table>
                        </div>
                        <!-- 동적 페이지네이션 -->
                        <nav id="pagiNav" aria-label="Page navigation"></nav>
                        </form>
                    </div>
                <div class="input-group">
                    <a th:href="@{/reg}" class="btn btn-success">등록</a>
                </div>
            </div>
        </main>
    
        <script layout:fragment="script" th:inline="javascript" type="text/javascript">
    
            const defaultParam = { type: "", keyword: ""};
            let searchParam = defaultParam;
    
            $(document).ready(function () {
    
                $("#btnSearch").on("click", function (e) {
                    e.preventDefault();
                    if ($("#type option:selected").val() === "") {
                            alert("키워드를 선택하라니까?");
                            return;
                        } else {
                            console.log("검색한다?");
                            searchParam = {type: $("#type").val(), keyword: $("#keyword").val() }
                            getList(searchParam, 1);
                    }
                });
                //초기 목록은 1페이지로 고정
                getList(defaultParam, 1);
            });
    
            //페이징
            function movePage(pageNum) {
                getList(searchParam, pageNum);
            }
    
            //게시글 목록
            function getList(params, currentPage) {
                $.ajax({
                    url : '/listAjax?currentPage=' + currentPage,
                    type : 'post',
                    data: params,
                    success: function (result) {
                        let boardHtml = "";
    
                        let list = result.list;
                        let paging = result.paging;
                        //테이블 draw
                        for (let i = 0; i < list.length; i++) {
                                boardHtml += "<tr>";
                                boardHtml += "<td>" + list[i].no;
                                boardHtml += "</td>";
                                boardHtml += "<td>" + list[i].title;
                                boardHtml += "</td>";
                                boardHtml += "<td>" + list[i].content;
                                boardHtml += "</td>";
                                boardHtml += "<td>" + list[i].userId;
                                boardHtml += "</td>";
                                boardHtml += "<td>" + list[i].regDate;
                                boardHtml += "</td>";
                                boardHtml += "<td>" + list[i].modDate;
                                boardHtml += "</td>";
                                boardHtml += "</tr>";
                        }
                        $("#boardBody").html(boardHtml);
    
                        //페이지네이션 draw
                        let pageHtml = "";
    
                        pageHtml += "<ul class='pagination justify-content-center'>";
    
                        if (result.paging.firstPageNo > 1) {
                            const prev = parseInt(result.paging.firstPageNo) - 1;
                            pageHtml += "<li class='page-item'>";
                            pageHtml += "<a href='#' class='page-link' onclick='movePage("+ prev +")' style='cursor: pointer'>Prev</a>";
                            pageHtml += "</li>";
                        }
    
                        for (let i = result.paging.firstPageNo; i <= result.paging.lastPageNo; i++) {
                            pageHtml += "<li class='page-item'>";
    
                            if (result.paging.currentPage === i) {
                                pageHtml += "<a class='page-link active' onclick='movePage(" + i + ")' style='cursor: pointer'>" + i + "</a>";
                                pageHtml += "</li>";
                            } else {
                                pageHtml += "<a class='page-link' onclick='movePage(" + i + ")' style='cursor: pointer'>" + i + "</a>";
                                pageHtml += "</li>";
                            }
                        }
    
                        if (result.paging.lastPageNo < result.paging.totalPageCount) {
                            const next = parseInt(result.paging.lastPageNo) + 1;
                            pageHtml += "<li class='page-item'>";
                            pageHtml += "<a href='#' class='page-link' onclick='movePage("+ next +")' style='cursor: pointer'>Next</a>";
                            pageHtml += "</li>";
                        }
                        pageHtml += "</ul>";    
    
                        $("#pagiNav").html(pageHtml);
    
    
                    },
                    error: function (request, status, error) {
                        console.log(error);
                    }
                });
            }
        </script>
    
    </div>
    </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>
        <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="" />
        <title>thymeleaf board test</title>
    <!--    <link href="https://cdn.jsdelivr.net/npm/simple-datatables@7.1.2/dist/style.min.css" rel="stylesheet" />-->
        <link href="css/styles.css" rel="stylesheet" /> 
        <!--jquery script-->
        <script src="http://code.jquery.com/jquery-latest.min.js"></script>
        <script src="https://use.fontawesome.com/releases/v6.3.0/js/all.js" crossorigin="anonymous"></script>
    </head>
    
    <body class="sb-nav-fixed">
    <!-- header fragment 사용 -->
    <th:block th:replace="fragments/header :: headerFragment"></th:block>
    <div id="layoutSidenav">
        <div id="layoutSidenav_nav">
            <!-- sidebar fragment 사용 -->
            <th:block th:replace="fragments/sidebar :: sidebarFragment"></th:block>
        </div>
        <div id="layoutSidenav_content">
            <main>
                <!-- content fragment 사용 -->
                <th:block layout:fragment="content"></th:block>
                <!-- content script -->
                <th:block layout:fragment="script"></th:block>
            </main>
            <!-- footer fragment 사용 -->
            <th:block th:replace="fragments/footer :: footerFragment"></th:block>
        </div>
    </div>
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js" crossorigin="anonymous"></script>
    <script src="js/scripts.js"></script>
    <!--<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.8.0/Chart.min.js" crossorigin="anonymous"></script>-->
    <!--<script src="assets/demo/chart-area-demo.js"></script>-->
    <!--<script src="assets/demo/chart-bar-demo.js"></script>-->
    <!--<script src="https://cdn.jsdelivr.net/npm/simple-datatables@7.1.2/dist/umd/simple-datatables.min.js" crossorigin="anonymous"></script>-->
    <!--<script src="js/datatables-simple-demo.js"></script>-->
    </body>
    </html>

    header.html

    <html lagn="ko" xmlns:th="http://www.thymeleaf.org">
        <!--headerFragment 선언-->
        <nav th:fragment="headerFragment" class="sb-topnav navbar navbar-expand navbar-dark bg-dark">
            <!-- Navbar Brand-->
                <a class="navbar-brand ps-3" th:href="@{/}">Start Bootstrap</a>
                <!-- Sidebar Toggle-->
                <button class="btn btn-link btn-sm order-1 order-lg-0 me-4 me-lg-0" id="sidebarToggle" href="#!"><i class="fas fa-bars"></i></button>
                <!-- Navbar Search-->
                <form class="d-none d-md-inline-block form-inline ms-auto me-0 me-md-3 my-2 my-md-0" action="/logout" method="post">
    
                <!-- Navbar-->
                <ul class="navbar-nav ms-auto ms-md-0 me-3 me-lg-4">
                    <li class="nav-item dropdown">
                        <th:block th:if="${session.id != null}">
                        <a class="nav-link dropdown-toggle" id="navbarDropdown" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false" th:text="${session.id} + '님 환영합니다.'"><i class="fas fa-user fa-fw"></i></a>
                        <ul class="dropdown-menu dropdown-menu-end" aria-labelledby="navbarDropdown">
                            <li><button type="submit" class="dropdown-item" id="logout">Logout</button></li>
                        </ul>
                        </th:block>
                    </li>
                </ul>
                </form>
        </nav>
    </html>

    sidebar.html

    <html lagn="ko" xmlns:th="http://www.thymeleaf.org">
        <!--sidebarFragment 선언-->
        <nav th:fragment="sidebarFragment" class="sb-sidenav accordion sb-sidenav-dark" id="sidenavAccordion">
        <div class="sb-sidenav-menu">
                <div class="nav">
    <!--                <div class="sb-sidenav-menu-heading">Interface</div>-->
                    <a class="nav-link collapsed" href="#" data-bs-toggle="collapse" data-bs-target="#collapseLayouts" aria-expanded="false" aria-controls="collapseLayouts">
                        <div class="sb-nav-link-icon"><i class="fas fa-columns"></i></div>
                        메뉴
                        <div class="sb-sidenav-collapse-arrow"><i class="fas fa-angle-down"></i></div>
                    </a>
                    <div class="collapse" id="collapseLayouts" aria-labelledby="headingOne" data-bs-parent="#sidenavAccordion">
                        <nav class="sb-sidenav-menu-nested nav">
                            <a class="nav-link" th:href="@{/}">게시판관리</a>
                        </nav>
                    </div>
            </div>
        </nav>
    </html>

    footer.html

    <html lagn="ko" xmlns:th="http://www.thymeleaf.org">
        <!--footerFragment 선언-->
        <footer th:fragment="footerFragment" class="py-4 bg-light mt-auto">
            <div class="container-fluid px-4">
                <div class="d-flex align-items-center justify-content-between small">
                    <div class="text-muted">Copyright &copy; Your Website 2022</div>
                    <div>
                        <a href="#">Privacy Policy</a>
                        &middot;
                        <a href="#">Terms &amp; Conditions</a>
                    </div>
                </div>
            </div>
        </footer>
    </html>

    build.gradle

    plugins {
        id 'java'
        id 'org.springframework.boot' version '2.7.11'
        id 'io.spring.dependency-management' version '1.0.15.RELEASE'
    }
    
    group = 'study'
    version = '0.0.1-SNAPSHOT'
    sourceCompatibility = '11'
    
    configurations {
        compileOnly {
            extendsFrom annotationProcessor
        }
    }
    
    repositories {
        mavenCentral()
    }
    
    dependencies {
        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:2.3.0'
        implementation group: 'org.springframework.boot', name: 'spring-boot-starter-validation'
        implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
        implementation 'org.bgee.log4jdbc-log4j2:log4jdbc-log4j2-jdbc4.1:1.16'
        implementation 'nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect'
        compileOnly 'org.projectlombok:lombok'
        developmentOnly 'org.springframework.boot:spring-boot-devtools'
        runtimeOnly 'com.oracle.database.jdbc:ojdbc8'
        testCompileOnly 'org.projectlombok:lombok'
        annotationProcessor 'org.projectlombok:lombok'
        testImplementation 'org.springframework.boot:spring-boot-starter-test'
    }
    
    tasks.named('test') {
        useJUnitPlatform()
    }

    result

    ※ 디테일 처리는 되지 않은 기초 샘플 소스

    front template은 sb-admin 사용(첨부파일 첨부)

    startbootstrap-sb-admin-gh-pages.zip
    0.06MB

    반응형