반응형
목차
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 © Your Website 2022</div>
<div>
<a href="#">Privacy Policy</a>
·
<a href="#">Terms & 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 사용(첨부파일 첨부)
반응형
'IT > development' 카테고리의 다른 글
[JavaScript] 현재날짜와 특정날짜 비교 (0) | 2023.06.13 |
---|---|
[springBoot] spring security passwordEncode (0) | 2023.06.04 |
[mybatis] mybatis oracle merge into (0) | 2023.06.03 |
[spring] springBoot ajax json과 file 전송 (0) | 2023.06.03 |
[thymeleaf] thymeleaf javascript로 변수 1개 이상 전달 (0) | 2023.05.30 |