#231004 수업 복습
index에서 로그인하면 /user/login.html로 post로 전송
-> UserController.java의 login 메소드 실행
-> 로그인 성공시 redirect:/board/list로 재요청
-> BoardController 실행
package com.kh.demo.controller;
import java.util.Iterator;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.core.io.Resource;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.multipart.MultipartFile;
import com.kh.demo.domain.dto.BoardDTO;
import com.kh.demo.domain.dto.Criteria;
import com.kh.demo.domain.dto.PageDTO;
import com.kh.demo.service.BoardService;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
@Controller
@RequestMapping("/board/*")
public class BoardController {
@Autowired @Qualifier("boardServiceImpl")
private BoardService service;
@GetMapping("list") // '/board/list.html'인 경우 => list 메소드 실행
public void list(Criteria cri, Model model) throws Exception { // 여러 매개변수를 선언하는게 아니라 네개가 종합적으로 담겨있는 Criteria 타입으로 한번에 받아줌
System.out.println(cri);
List<BoardDTO> list = service.getBoardList(cri);
// model.addAttribute("name",value) : value 객체를 name 이름으로 추가
// 뷰 코드에서는 name으로 지정한 이름을 통해서 value를 샤용
model.addAttribute("list",list);
model.addAttribute("pageMaker",new PageDTO(service.getTotal(cri), cri));
model.addAttribute("newly_board",service.getNewlyBoardList(list));
model.addAttribute("reply_cnt_list",service.getReplyCntList(list));
model.addAttribute("recent_reply",service.getRecentReplyList(list));
}
@GetMapping("write")
public void write(@ModelAttribute("cri") Criteria cri,Model model) {
System.out.println(cri);
}
@PostMapping("write")
public String write(BoardDTO board, MultipartFile[] files, Criteria cri) throws Exception{
Long boardnum = 0l;
if(service.regist(board, files)) {
boardnum = service.getLastNum(board.getUserid());
return "redirect:/board/get"+cri.getListLink()+"&boardnum="+boardnum;
}
else {
return "redirect:/board/list"+cri.getListLink();
}
}
@GetMapping(value = {"get","modify"})
public String get(Criteria cri, Long boardnum, HttpServletRequest req, HttpServletResponse resp, Model model) {
model.addAttribute("cri",cri);
HttpSession session = req.getSession();
BoardDTO board = service.getDetail(boardnum);
model.addAttribute("board",board);
model.addAttribute("files",service.getFileList(boardnum));
String loginUser = (String)session.getAttribute("loginUser");
String requestURI = req.getRequestURI();
if(requestURI.contains("/get")) {
//게시글의 작성자가 로그인된 유저가 아닐 때
if(!board.getUserid().equals(loginUser)) {
//쿠키 검사
Cookie[] cookies = req.getCookies();
Cookie read_board = null;
if(cookies != null) {
for(Cookie cookie : cookies) {
//ex) 1번 게시글을 조회하고자 클릭했을 때에는 "read_board1" 쿠키를 찾음
if(cookie.getName().equals("read_board"+boardnum)) {
read_board = cookie;
break;
}
}
}
//read_board가 null이라는 뜻은 위에서 쿠키를 찾았을 때 존재하지 않았다는 뜻
//첫 조회거나 조회한지 1시간이 지난 후
if(read_board == null) {
//조회수 증가
service.updateReadCount(boardnum);
//read_board1 이름의 쿠키(유효기간 : 3600초)를 생성해서 클라이언트 컴퓨터에 저장
Cookie cookie = new Cookie("read_board"+boardnum, "r");
cookie.setMaxAge(3600);
resp.addCookie(cookie);
}
}
}
return requestURI;
}
@PostMapping("modify")
public String modify(BoardDTO board, MultipartFile[] files, String updateCnt, Criteria cri, Model model) throws Exception {
if(files != null){
for (int i = 0; i < files.length; i++) {
System.out.println("controller : "+files[i].getOriginalFilename());
}
}
System.out.println("controller : "+updateCnt);
if(service.modify(board, files, updateCnt)) {
return "redirect:/board/get"+cri.getListLink()+"&boardnum="+board.getBoardnum();
}
else {
return "redirect:/board/list"+cri.getListLink();
}
}
@PostMapping("remove")
public String remove(Long boardnum, Criteria cri, HttpServletRequest req) {
HttpSession session = req.getSession();
String loginUser = (String)session.getAttribute("loginUser");
if(service.remove(loginUser, boardnum)) {
return "redirect:/board/list"+cri.getListLink();
}
else {
return "redirect:/board/get"+cri.getListLink()+"&boardnum="+boardnum;
}
}
@GetMapping("thumbnail")
public ResponseEntity<Resource> thumbnail(String systemname) throws Exception{
return service.getThumbnailResource(systemname);
}
@GetMapping("file")
public ResponseEntity<Object> download(String systemname, String orgname) throws Exception{
return service.downloadFile(systemname,orgname);
}
}
header와 footer는 한 홈페이지에서 페이지마다 동일하게 반복되므로
html 파일을 각각 만들어놓고 th:replace="경로"로 연결시켜줌
<th:block th:replace="~{layout/header::header(${session.loginUser})}"></th:block>
<th:block th:replace="~{layout/footer::footer}"></th:block>
<link>
<th:block th:fragment="header(loginUser)"> <!-- th:fragment = "이름" -->
<th:block th:if="${loginUser == null}"> <!-- 로그인 검사 -->
<script>
alert("로그인 후 이용하세요!");
location.replace("/"); <!-- 로그인 안됐을때 처음으로 돌아가기 -->
</script>
</th:block>
<header>
<table class="header_area">
<tr align="right" valign="middle">
<td>
<span>[[${loginUser}]]님 환영합니다</span>
<a th:href="@{/user/logout}">로그아웃</a>
</td>
</tr>
</table>
<table class="title">
<tr align="center" valign="middle">
<td>
<a th:href="@{/board/list}">
<h3>MVC 게시판
<img th:src="@{/board/images/title.png}">
</h3>
</a>
</td>
</tr>
</table>
</header>
</th:block>
<th:block th:fragment="footer">
<footer>
<div class="center">
<div class="copy">
© DS Company
<div class="contact">
<div class="phone">010-XXXX-XXXX</div>
<div class="addr">서울특별시 강남구 테헤란로 71-1<br>굳세어라빌딩 102호</div>
</div>
</div>
<div class="sns_area">
<div class="ad_box">광고</div>
<div class="sns_box">
<a href="#" class="instagram"><span class="text">dot_ssam</span></a>
<a href="#" class="youtube"><span class="text">dot_ssam</span></a>
<a href="#" class="tistory"><span class="text">dot_ssam</span></a>
<a href="#" class="thread"><span class="text">dot_ssam</span></a>
</div>
</div>
</div>
</footer>
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>List</title>
<style>
스타일 생략
</style>
<link rel="stylesheet" th:href="@{/board/css/layout.css}"> <!-- 스타일시트 연결 -->
</head>
<body>
<th:block th:replace="~{layout/header::header(${session.loginUser})}"></th:block>
<div class="wrap">
<!-- 게시글 리스트 띄우는 테이블 -->
<table class="list">
<tr align="right" valign="middle">
<td colspan="6">글 개수 : [[${pageMaker.total}]]</td> <!-- BoardController의 list 메소드의 pageMaker -->
</tr>
<tr align="center" valign="middle">
<th width="8%">번호</th>
<th></th>
<th>제목</th>
<th width="15%">작성자</th>
<th width="17%">날짜</th>
<th width="10%">조회수</th>
</tr>
<!-- BoardController의 list 메소드, list를 board 변수에 담기 -->
<tr th:if="${list != null and list.size()>0}" th:each="board : ${list}">
<td>[[${board.boardnum}]]</td>
<td>
<sup class="hot" th:if="${recent_reply[boardStat.index] == 'O'}">Hot</sup> <!-- 인기순 -->
<sup class="new" th:if="${newly_board[boardStat.index] == 'O'}">New</sup> <!-- 최신순 -->
</td>
<td>
<a class="get" th:href="${board.boardnum}">
[[${board.boardtitle}]]
<span class="reply_cnt" th:text="'['+${reply_cnt_list[boardStat.index]}+']'"></span>
</a>
</td>
<td>[[${board.userid}]]</td>
<td>
[[${board.regdate}]]
<th:block th:if="${board.regdate != board.updatedate}">
(수정됨)
</th:block>
</td>
<td>[[${board.readcount}]]</td>
</tr>
<th:block th:if="${list == null or list.size() == 0}">
<tr>
<td colspan="6" style="text-align: center; font-size: 20px;">등록된
게시글이 없습니다.</td>
</tr>
</th:block>
</table>
<br>
<!-- 페이징 처리하는 테이블 -->
<table class="pagination">
<tr align="center" valign="middle">
<td>
<a class="changePage" th:if="${pageMaker.prev}" th:href="${pageMaker.startPage-1}"><</a>
<th:block th:each="i : ${#numbers.sequence(pageMaker.startPage,pageMaker.endPage)}">
<span class="nowPage" th:text="${i}" th:if="${pageMaker.cri.pagenum == i}"></span>
<a class="changePage" th:href="${i}" th:text="${i}" th:unless="${pageMaker.cri.pagenum == i}"></a>
</th:block>
<a class="changePage" th:if="${pageMaker.next}" th:href="${pageMaker.endPage+1}">></a>
</td>
</tr>
</table>
<!-- 글쓰기 버튼 배치하는 테이블 -->
<table>
<tr align="right" valign="middle">
<td>
<a class="write" th:href="${'/board/write'+pageMaker.cri.listLink}">글쓰기</a>
</td>
</tr>
</table>
<form id="searchForm" th:action="@{/board/list}">
<div class="search_area">
<select name="type">
<option value="" th:selected="${pageMaker.cri.type == null}">검색</option>
<option value="T" th:selected="${pageMaker.cri.type == 'T'}">제목</option>
<option value="C" th:selected="${pageMaker.cri.type == 'C'}">내용</option>
<option value="W" th:selected="${pageMaker.cri.type == 'W'}">작성자</option>
<option value="TC" th:selected="${pageMaker.cri.type == 'TC'}">제목 또는 내용</option>
<option value="TW" th:selected="${pageMaker.cri.type == 'TW'}">제목 또는 작성자</option>
<option value="TCW" th:selected="${pageMaker.cri.type == 'TCW'}">제목 또는 내용 또는 작성자</option>
</select>
<input type="text" name="keyword" id="keyword" th:value="${pageMaker.cri.keyword}">
<a href="#" class="button primary">검색</a>
</div>
<input type="hidden" value="1" name="pagenum">
<input type="hidden" value="10" name="amount">
</form>
</div>
<div id="chat-circle" class="btn btn-raised">
<div id="chat-overlay"></div>
<span class="material-symbols-outlined">speaker_phone</span>
</div>
<div class="chat-box">
<div class="chat-box-header">
사용자 채팅 <span class="chat-box-toggle"><span
class="material-symbols-outlined">close</span></span>
</div>
<div class="chat-box-body">
<div class="chat-box-overlay"></div>
<div class="chat-logs"></div>
<!--chat-log -->
</div>
<div class="chat-input">
<form>
<input type="hidden" id="userid" name="userid" th:value="${session.loginUser}">
<span class="echo-receiver"></span> <input type="text"
id="chat-input" placeholder="Send a message..."
onkeyup="sendEcho();" />
<button type="submit" class="chat-submit" id="chat-submit">
<span class="material-symbols-outlined">send</span>
</button>
</form>
</div>
</div>
<form name="pageForm" id="pageForm" th:action="@{/board/list}">
<input type="hidden" name="pagenum" th:value="${pageMaker.cri.pagenum}">
<input type="hidden" name="amount" th:value="${pageMaker.cri.amount}">
<input type="hidden" name="type" th:value="${pageMaker.cri.type}">
<input type="hidden" name="keyword" th:value="${pageMaker.cri.keyword}">
</form>
<th:block th:replace="~{layout/footer::footer}"></th:block>
</body>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script th:inline="javascript">
const searchForm = $("#searchForm");
const pageForm = $("#pageForm");
$(".changePage").on("click",function(e){
//e(이벤트)의 기본 작동 막기
e.preventDefault();
let pagenum = $(this).attr("href");
pageForm.find("input[name='pagenum']").val(pagenum);
pageForm.submit();
});
$(".get").on("click",function(e){
e.preventDefault();
let boardnum = $(this).attr("href");
let url=/*[[@{/board/get}]]*/'';
pageForm.append("<input type='hidden' name='boardnum' value='"+boardnum+"'>")
pageForm.attr("action",url);
pageForm.attr("method","get");
pageForm.submit();
})
$("#searchForm a").on("click",sendit);
function sendit(){
if(!searchForm.find("option:selected").val()){
alert("검색 기준을 선택하세요!");
return false;
}
if(!$("input[name='keyword']").val()){
alert("키워드를 입력하세요!");
return false;
}
searchForm.submit();
}
</script>
</html>
package com.kh.demo.service;
import java.util.ArrayList;
import java.util.List;
import org.springframework.core.io.Resource;
import org.springframework.http.ResponseEntity;
import org.springframework.web.multipart.MultipartFile;
import com.kh.demo.domain.dto.BoardDTO;
import com.kh.demo.domain.dto.Criteria;
import com.kh.demo.domain.dto.FileDTO;
public interface BoardService {
//insert (등록)
boolean regist(BoardDTO board, MultipartFile[] files) throws Exception;
//update
public boolean modify(BoardDTO board, MultipartFile[] files, String updateCnt) throws Exception;
public void updateReadCount(Long boardnum);
//delete
public boolean remove(String loginUser, Long boardnum);
//select
Long getTotal(Criteria cri);
List<BoardDTO> getBoardList(Criteria cri);
BoardDTO getDetail(Long boardnum);
Long getLastNum(String userid);
ArrayList<String> getNewlyBoardList(List<BoardDTO> list) throws Exception;
ArrayList<Integer> getReplyCntList(List<BoardDTO> list);
ArrayList<String> getRecentReplyList(List<BoardDTO> list);
List<FileDTO> getFileList(Long boardnum);
ResponseEntity<Resource> getThumbnailResource(String systemname) throws Exception;
ResponseEntity<Object> downloadFile(String systemname, String orgname) throws Exception;
}
package com.kh.demo.service;
import java.io.File;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.UUID;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.InputStreamResource;
import org.springframework.core.io.Resource;
import org.springframework.http.ContentDisposition;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import com.kh.demo.domain.dto.BoardDTO;
import com.kh.demo.domain.dto.Criteria;
import com.kh.demo.domain.dto.FileDTO;
import com.kh.demo.mapper.BoardMapper;
import com.kh.demo.mapper.FileMapper;
import com.kh.demo.mapper.ReplyMapper;
@Service
public class BoardServiceImpl implements BoardService{
@Autowired
private BoardMapper bmapper;
@Autowired
private ReplyMapper rmapper;
@Autowired
private FileMapper fmapper;
@Value("${file.dir}")
private String saveFolder;
@Override
public boolean regist(BoardDTO board, MultipartFile[] files) throws Exception {
int row = bmapper.insertBoard(board);
if(row != 1) {
return false;
}
if(files == null || files.length == 0) {
return true;
}
else {
//방금 등록한 게시글 번호
Long boardnum = bmapper.getLastNum(board.getUserid());
boolean flag = false;
for(int i=0;i<files.length-1;i++) {
MultipartFile file = files[i];
//apple.png
String orgname = file.getOriginalFilename();
//5
int lastIdx = orgname.lastIndexOf(".");
//.png
String extension = orgname.substring(lastIdx);
LocalDateTime now = LocalDateTime.now();
String time = now.format(DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS"));
//20231005103911237랜덤문자열.png
String systemname = time+UUID.randomUUID().toString()+extension;
System.out.println(systemname);
//실제 저장될 파일의 경로
String path = saveFolder+systemname;
FileDTO fdto = new FileDTO();
fdto.setBoardnum(boardnum);
fdto.setSystemname(systemname);
fdto.setOrgname(orgname);
//실제 파일 업로드
file.transferTo(new File(path));
flag = fmapper.insertFile(fdto) == 1;
if(!flag) {
//업로드 했던 파일 삭제, 게시글 데이터 삭제
return flag;
}
}
}
return true;
}
@Override
public boolean modify(BoardDTO board, MultipartFile[] files, String updateCnt) throws Exception {
int row = bmapper.updateBoard(board);
if(row != 1) {
return false;
}
List<FileDTO> org_file_list = fmapper.getFiles(board.getBoardnum());
if(org_file_list.size()==0 && (files == null || files.length == 0)) {
return true;
}
else {
if(files != null) {
boolean flag = false;
//후에 비즈니스 로직 실패 시 원래대로 복구하기 위해 업로드 성공했던 파일들도 삭제해주어야 한다.
//업로드 성공한 파일들의 이름을 해당 리스트에 추가하면서 로직을 진행한다.
ArrayList<String> sysnames = new ArrayList<>();
System.out.println("service : "+files.length);
for(int i=0;i<files.length-1;i++) {
MultipartFile file = files[i];
String orgname = file.getOriginalFilename();
//수정의 경우 중간에 있는 파일은 수정이 되지 않은 경우도 있다.
//그런 경우의 file의 orgname은 null 이거나 "" 이다.
//따라서 업로드가 될 필요가 없으므로 continue로 다음 파일로 넘어간다.
if(orgname == null || orgname.equals("")) {
continue;
}
int lastIdx = orgname.lastIndexOf(".");
String extension = orgname.substring(lastIdx);
LocalDateTime now = LocalDateTime.now();
String time = now.format(DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS"));
String systemname = time+UUID.randomUUID().toString()+extension;
sysnames.add(systemname);
String path = saveFolder+systemname;
FileDTO fdto = new FileDTO();
fdto.setBoardnum(board.getBoardnum());
fdto.setOrgname(orgname);
fdto.setSystemname(systemname);
file.transferTo(new File(path));
flag = fmapper.insertFile(fdto) == 1;
if(!flag) {
break;
}
}
//강제탈출(실패)
if(!flag) {
//아까 추가했던 systemname들(업로드 성공한 파일의 systemname)을 꺼내오면서
//실제 파일이 존재한다면 삭제 진행
for(String systemname : sysnames) {
File file = new File(saveFolder,systemname);
if(file.exists()) {
file.delete();
}
fmapper.deleteBySystemname(systemname);
}
}
}
//지워져야 할 파일(기존에 있었던 파일들 중 수정된 파일)들의 이름 추출
String[] deleteNames = updateCnt.split("\\\\");
for(int i=1;i<deleteNames.length;i++) {
File file = new File(saveFolder,deleteNames[i]);
//해당 파일 삭제
if(file.exists()) {
file.delete();
//DB상에서도 삭제
fmapper.deleteBySystemname(deleteNames[i]);
}
}
return true;
}
}
@Override
public void updateReadCount(Long boardnum) {
bmapper.updateReadCount(boardnum);
}
@Override
public boolean remove(String loginUser, Long boardnum) {
BoardDTO board = bmapper.findByNum(boardnum);
if(board.getUserid().equals(loginUser)) {
List<FileDTO> files = fmapper.getFiles(boardnum);
for(FileDTO fdto : files) {
File file = new File(saveFolder,fdto.getSystemname());
if(file.exists()) {
file.delete();
fmapper.deleteBySystemname(fdto.getSystemname());
}
}
return bmapper.deleteBoard(boardnum) == 1;
}
return false;
}
@Override
public Long getTotal(Criteria cri) {
return bmapper.getTotal(cri);
}
@Override
public List<BoardDTO> getBoardList(Criteria cri) {
return bmapper.getList(cri);
}
@Override
public BoardDTO getDetail(Long boardnum) {
return bmapper.findByNum(boardnum);
}
@Override
public Long getLastNum(String userid) {
return bmapper.getLastNum(userid);
}
@Override
public ArrayList<String> getNewlyBoardList(List<BoardDTO> list) throws Exception {
ArrayList<String> newly_board = new ArrayList<>();
DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date now = new Date();
for(BoardDTO board : list) {
Date regdate = df.parse(board.getRegdate());
if(now.getTime() - regdate.getTime() < 1000*60*60*2) {
newly_board.add("O");
}
else {
newly_board.add("X");
}
}
return newly_board;
}
@Override
public ArrayList<Integer> getReplyCntList(List<BoardDTO> list) {
ArrayList<Integer> reply_cnt_list = new ArrayList<>();
for(BoardDTO board : list) {
reply_cnt_list.add(rmapper.getTotal(board.getBoardnum()));
}
return reply_cnt_list;
}
@Override
public ArrayList<String> getRecentReplyList(List<BoardDTO> list) {
ArrayList<String> recent_reply = new ArrayList<>();
for(BoardDTO board : list) {
if(rmapper.getRecentReply(board.getBoardnum()) >= 5) {
recent_reply.add("O");
}
else {
recent_reply.add("X");
}
}
return recent_reply;
}
@Override
public List<FileDTO> getFileList(Long boardnum) {
return fmapper.getFiles(boardnum);
}
@Override
public ResponseEntity<Resource> getThumbnailResource(String systemname) throws Exception{
//경로에 관련된 객체(자원으로 가지고 와야 하는 파일에 대한 경로)
Path path = Paths.get(saveFolder+systemname);
//경로에 있는 파일의 MIME타입을 조사해서 그대로 담기
String contentType = Files.probeContentType(path);
//응답 헤더 생성
HttpHeaders headers = new HttpHeaders();
headers.add(HttpHeaders.CONTENT_TYPE, contentType);
//해당 경로(path)에 있는 파일에서부터 뻗어나오는 InputStream(Files.newInputStream)을 통해 자원화(InputStreamResource)
Resource resource = new InputStreamResource(Files.newInputStream(path));
return new ResponseEntity<>(resource,headers,HttpStatus.OK);
}
@Override
public ResponseEntity<Object> downloadFile(String systemname, String orgname) throws Exception{
//경로에 관련된 객체(자원으로 가지고 와야 하는 파일에 대한 경로)
Path path = Paths.get(saveFolder+systemname);
//해당 경로(path)에 있는 파일에서부터 뻗어나오는 InputStream(Files.newInputStream)을 통해 자원화(InputStreamResource)
Resource resource = new InputStreamResource(Files.newInputStream(path));
File file = new File(saveFolder,systemname);
HttpHeaders headers = new HttpHeaders();
String dwName = "";
try {
dwName = URLEncoder.encode(orgname,"UTF-8").replaceAll("\\+","%20");
} catch (UnsupportedEncodingException e) {
dwName = URLEncoder.encode(file.getName(),"UTF-8").replaceAll("\\+","%20");
}
headers.setContentDisposition(ContentDisposition.builder("attachment").filename(dwName).build());
return new ResponseEntity<Object>(resource,headers,HttpStatus.OK);
}
}
package com.kh.demo.mapper;
import java.util.List;
import org.apache.ibatis.annotations.Mapper;
import com.kh.demo.domain.dto.BoardDTO;
import com.kh.demo.domain.dto.Criteria;
@Mapper
public interface BoardMapper {
//insert
int insertBoard(BoardDTO board);
//update
int updateBoard(BoardDTO board);
int updateReadCount(Long boardnum);
//delete
int deleteBoard(Long boardnum);
//select
List<BoardDTO> getList(Criteria cri);
Long getTotal(Criteria cri);
Long getLastNum(String userid);
BoardDTO findByNum(Long boardnum);
}
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.kh.demo.mapper.BoardMapper">
<sql id="cri"> <!-- sql태그 : 쿼리문을 담는 변수 -->
<if test="keyword != '' and type != ''"> <!-- keyword와 type이 비어져있지 않다면 무언가 검색했다는 뜻 => if문 실행 -->
<trim prefixOverrides="or" prefix="(" suffix=") and">
<!-- trim 안에 foreach문을 담아서 prefixOverrides="or"로 or를 없애줌
대신 prefix로 여는 괄호, suffix로 닫는 괄호 붙여줌 -->
<foreach collection="typeArr" item="type"> <!-- typeArr라는 이름으로 for문 실행 => getter 호출 => 거기서 리턴 받는 배열로 for문 실행 -->
or
<choose>
<when test="type == 'T'.toString()">
(boardtitle like('%${keyword}%'))
</when> <!-- type이 'T'와 같다면 boardtitle like('%${keyword}%') 쿼리문 생성 -->
<when test="type == 'C'.toString()">
(boardcontents like('%${keyword}%'))
</when> <!-- type이 'C'와 같다면 boardcontents like('%${keyword}%') 쿼리문 생성 -->
<when test="type == 'W'.toString()">
(userid like('%${keyword}%'))
</when> <!-- type이 'W'와 같다면 userid like('%${keyword}%') 쿼리문 생성 -->
</choose>
</foreach>
</trim>
</if>
</sql>
<insert id="insertBoard">
insert into t_board (boardtitle,boardcontents,userid)
values(#{boardtitle},#{boardcontents},#{userid})
</insert>
<update id="updateReadCount">
update t_board set readcount = readcount+1 where boardnum = #{boardnum}
</update>
<update id="updateBoard">
update t_board set boardtitle=#{boardtitle}, boardcontents=#{boardcontents},updatedate=now()
where boardnum=#{boardnum}
</update>
<delete id="deleteBoard">
delete from t_board where boardnum=#{boardnum}
</delete>
<select id="getList">
select * from t_board where
<include refid="cri"></include>
<![CDATA[
0 < boardnum order by boardnum desc limit #{startrow},#{amount}
]]>
<!-- boardnum이 0보다 큰 경우 boardnum으로 내림차순 정렬 <= if문이 비어있어도 위의 CDATA 조건이 실행되게끔 작성 -->
</select>
<select id="getTotal">
select count(*) from t_board where
<include refid="cri"></include> boardnum > 0
</select>
<select id="getLastNum">
select max(boardnum) from t_board where userid=#{userid}
</select>
<select id="findByNum">
select * from t_board where boardnum=#{boardnum}
</select>
</mapper>
페이지를 띄우는 기준을 작성하는 DTO
package com.kh.demo.domain.dto;
import org.springframework.web.util.UriComponentsBuilder;
import lombok.Data;
@Data
public class Criteria {
private int pagenum;
private int amount;
private String type;
private String keyword;
private int startrow;
public Criteria() {
this(1,10); // 기본생성자 호출 => 1페이지 10개
}
public Criteria(int pagenum, int amount) {
this.pagenum = pagenum;
this.amount = amount;
this.startrow = (this.pagenum - 1) * this.amount;
}
//pagenum이 바뀔때 startrow도 바뀌는 메소드
public void setPagenum(int pagenum) {
this.pagenum = pagenum;
this.startrow = (this.pagenum - 1) * this.amount;
}
// MyBatis에서 #{typeArr} 로 사용 가능
public String[] getTypeArr() {
//type이 null이라면 return {}
//type이 "TC"라면 return {"T","C"}
return type == null ? new String[] {} : type.split("");
}
public String getListLink() {
// /board/write?userid=apple
// fromPath("/board/write").queryParam("userid","apple")
// //? 앞에 붙는 uri 문자열
UriComponentsBuilder builder = UriComponentsBuilder.fromPath("")
.queryParam("pagenum", pagenum) //파라미터 추가
.queryParam("amount", amount)
.queryParam("keyword",keyword)
.queryParam("type", type);
return builder.toUriString(); //빌더가 가지고 있는 설정대로 문자열 만들기
}
}
package com.kh.demo.domain.dto;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@Getter
@Setter
@ToString
public class PageDTO {
private int startPage;
private int endPage;
private int realEnd;
private Long total;
private boolean prev,next;
private Criteria cri;
public PageDTO(Long total, Criteria cri) {
int pagenum = cri.getPagenum();
this.cri = cri;
this.total = total;
this.endPage = (int)Math.ceil(pagenum/10.0)*10; //ex. 13p를 보고 있다면, 13/10.0=1.3 -> 올림 -> 2 -> *10 = 20 => endPage = 20
this.startPage = this.endPage - 9; //ex. endPage = 20 => startPage = 11 (1~10 / 11~20 / 21~30 …)
this.realEnd = (int)Math.ceil(total*1.0/10); //실제로 게시글이 있는 끝 페이지
this.endPage = endPage > realEnd ? realEnd : endPage;
this.prev = this.startPage > 1; //이전 버튼
this.next = this.endPage < this.realEnd; //다음 버튼
}
}
@GetMapping("list") // '/board/list.html'인 경우 => list 메소드 실행
public void list(Criteria cri, Model model) throws Exception { // 여러 매개변수를 선언하는게 아니라 네개가 종합적으로 담겨있는 Criteria 타입으로 한번에 받아줌
System.out.println(cri);
List<BoardDTO> list = service.getBoardList(cri);
model.addAttribute("list",list);
model.addAttribute("pageMaker",new PageDTO(service.getTotal(cri), cri));
//pageMaker라는 이름으로 PageDTO객체를 생성하여 보내줌
}
<select id="getTotal">
select count(*) from t_board where
<include refid="cri"></include> boardnum > 0
<!-- refid : 레퍼런스id -->
</select>
<!-- sql태그로 담은 cri 쿼리문 include -->
<!--<sql id="cri">
<if test="keyword != '' and type != ''">
<trim prefixOverrides="or" prefix="(" suffix=") and">
<foreach collection="typeArr" item="type">
or
<choose>
<when test="type == 'T'.toString()">
(boardtitle like('%${keyword}%'))
</when>
<when test="type == 'C'.toString()">
(boardcontents like('%${keyword}%'))
</when>
<when test="type == 'W'.toString()">
(userid like('%${keyword}%'))
</when>
</choose>
</foreach>
</trim>
</if>
</sql>-->
공통 - 페이지 변경하는 것 class = changePage
<!-- 페이징 처리하는 테이블 -->
<table class="pagination">
<tr align="center" valign="middle">
<td>
<!-- 이전버튼 -->
<a class="changePage" th:if="${pageMaker.prev}" th:href="${pageMaker.startPage-1}"><</a> <!-- pageMaker의 prev이 true면 이전버튼 생성 / href는 startPage-1 -->
<!-- 페이지 숫자 -->
<th:block th:each="i : ${#numbers.sequence(pageMaker.startPage,pageMaker.endPage)}"> <!-- startPage~endPage까지 배열(sequence)을 만들고 i를 하나씩 꺼내옴 -->
<span class="nowPage" th:text="${i}" th:if="${pageMaker.cri.pagenum == i}"></span> <!-- 현재 페이지가 i 이면 span태그 부분 띄워줌 아니면 밑에 a 태그 부분 -->
<a class="changePage" th:href="${i}" th:text="${i}" th:unless="${pageMaker.cri.pagenum == i}"></a> <!-- i 값을 내부에 내용으로 써줌 -->
</th:block>
<!-- 다음 버튼 -->
<a class="changePage" th:if="${pageMaker.next}" th:href="${pageMaker.endPage+1}">></a>
</td>
</tr>
</table>
<form id="searchForm" th:action="@{/board/list}"> <!-- th:action = 제출시 이동되는 곳 -->
<div class="search_area">
<select name="type">
<option value="" th:selected="${pageMaker.cri.type == null}">검색</option>
<option value="T" th:selected="${pageMaker.cri.type == 'T'}">제목</option>
<option value="C" th:selected="${pageMaker.cri.type == 'C'}">내용</option>
<option value="W" th:selected="${pageMaker.cri.type == 'W'}">작성자</option>
<option value="TC" th:selected="${pageMaker.cri.type == 'TC'}">제목 또는 내용</option>
<option value="TW" th:selected="${pageMaker.cri.type == 'TW'}">제목 또는 작성자</option>
<option value="TCW" th:selected="${pageMaker.cri.type == 'TCW'}">제목 또는 내용 또는 작성자</option>
</select>
<input type="text" name="keyword" id="keyword" th:value="${pageMaker.cri.keyword}">
<a href="#" class="button primary">검색</a>
</div>
<input type="hidden" value="1" name="pagenum">
<input type="hidden" value="10" name="amount">
</form>
<script th:inline="javascript">
const searchForm = $("#searchForm");
const pageForm = $("#pageForm");
$(".changePage").on("click",function(e){
//e(이벤트)의 기본 작동 막기
e.preventDefault();
let pagenum = $(this).attr("href"); //pagenum 변수에 지금 클릭되어 있는 href 페이지 번호 담기
pageForm.find("input[name='pagenum']").val(pagenum); //pageForm의 name이 pagenum인 input 태그를 찾아 그 값을 pagenum에 담긴 값(클릭한 값)으로 변환
pageForm.submit(); //리스트로 제출
});
$(".get").on("click",function(e){
e.preventDefault();
let boardnum = $(this).attr("href");
let url=/*[[@{/board/get}]]*/'';
pageForm.append("<input type='hidden' name='boardnum' value='"+boardnum+"'>")
pageForm.attr("action",url);
pageForm.attr("method","get");
pageForm.submit();
})
$("#searchForm a").on("click",sendit); //#searchForm 안에 있는 a태그를 눌렀을 때 sendit 함수 호출(함수를 따로 만든 이유 : 유효성 검사)
function sendit(){
if(!searchForm.find("option:selected").val()){ //searchForm에서 선택된 옵션의 value가 없다면
alert("검색 기준을 선택하세요!");
return false;
}
if(!$("input[name='keyword']").val()){ //name이 keyword인 input위 value가 없다면
alert("키워드를 입력하세요!");
return false;
}
searchForm.submit(); //위 두가지를 통과하면 submit
}
</script>
@GetMapping("list") // '/board/list.html'인 경우 => list 메소드 실행
public void list(Criteria cri, Model model) throws Exception { // 여러 매개변수를 선언하는게 아니라 네개가 종합적으로 담겨있는 Criteria 타입으로 한번에 받아줌
System.out.println(cri);
List<BoardDTO> list = service.getBoardList(cri);
model.addAttribute("list",list);
model.addAttribute("pageMaker",new PageDTO(service.getTotal(cri), cri));
model.addAttribute("newly_board",service.getNewlyBoardList(list));
model.addAttribute("reply_cnt_list",service.getReplyCntList(list));
model.addAttribute("recent_reply",service.getRecentReplyList(list));
}
//NEW
@Override
public ArrayList<String> getNewlyBoardList(List<BoardDTO> list) throws Exception {
ArrayList<String> newly_board = new ArrayList<>();
DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date now = new Date();
for(BoardDTO board : list) {
Date regdate = df.parse(board.getRegdate());
if(now.getTime() - regdate.getTime() < 1000*60*60*2) {
newly_board.add("O");
}
else {
newly_board.add("X");
}
}
return newly_board;
}
@Override
public ArrayList<Integer> getReplyCntList(List<BoardDTO> list) {
ArrayList<Integer> reply_cnt_list = new ArrayList<>();
for(BoardDTO board : list) {
reply_cnt_list.add(rmapper.getTotal(board.getBoardnum()));
}
return reply_cnt_list;
}
//HOT
@Override
public ArrayList<String> getRecentReplyList(List<BoardDTO> list) {
ArrayList<String> recent_reply = new ArrayList<>();
for(BoardDTO board : list) {
if(rmapper.getRecentReply(board.getBoardnum()) >= 5) {
recent_reply.add("O");
}
else {
recent_reply.add("X");
}
}
return recent_reply;
}
package com.kh.demo.mapper;
import java.util.List;
import org.apache.ibatis.annotations.Mapper;
import com.kh.demo.domain.dto.Criteria;
import com.kh.demo.domain.dto.ReplyDTO;
@Mapper
public interface ReplyMapper {
//insert
int insertReply(ReplyDTO reply);
//update
int updateReply(ReplyDTO reply);
//delete
int deleteReply(Long replynum);
int deleteByBoardnum(Long boardnum);
//select
Long getLastNum(String userid);
int getTotal(Long boardnum);
List<ReplyDTO> getList(Criteria cri, Long boardnum);
int getRecentReply(Long boardnum);
}
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.kh.demo.mapper.ReplyMapper">
<select id="getTotal">
select count(*) from t_reply where boardnum=#{boardnum}
</select>
<select id="getRecentReply">
<![CDATA[
select count(*) from t_reply where boardnum=#{boardnum} and timestampdiff(HOUR,regdate,now())<1
]]>
</select>
</mapper>
<!-- 게시글 리스트 띄우는 테이블 -->
<table class="list">
<tr align="right" valign="middle">
<td colspan="6">글 개수 : [[${pageMaker.total}]]</td>
</tr>
<tr align="center" valign="middle">
<th width="8%">번호</th>
<th></th>
<th>제목</th>
<th width="15%">작성자</th>
<th width="17%">날짜</th>
<th width="10%">조회수</th>
</tr>
<tr th:if="${list != null and list.size()>0}" th:each="board : ${list}">
<td>[[${board.boardnum}]]</td>
<td>
<!-- 변수명+Stat을 붙이면 타임리프에서 제공하는 status 변수 사용해 index 값 추출 가능 -->
<sup class="hot" th:if="${recent_reply[boardStat.index] == 'O'}">Hot</sup> <!-- 인기순 => index가 O이면 Hot 띄워줌 -->
<sup class="new" th:if="${newly_board[boardStat.index] == 'O'}">New</sup> <!-- 최신순 => index가 O이면 New 띄워줌 -->
</td>
<td>
<a class="get" th:href="${board.boardnum}">
[[${board.boardtitle}]]
<!-- 댓글 갯수 -->
<span class="reply_cnt" th:text="'['+${reply_cnt_list[boardStat.index]}+']'"></span>
</a>
</td>
<td>[[${board.userid}]]</td>
<td>
[[${board.regdate}]]
<th:block th:if="${board.regdate != board.updatedate}">
(수정됨)
</th:block>
</td>
<td>[[${board.readcount}]]</td>
</tr>
<th:block th:if="${list == null or list.size() == 0}">
<tr>
<td colspan="6" style="text-align: center; font-size: 20px;">등록된
게시글이 없습니다.</td>
</tr>
</th:block>
</table>
<table class="header_area">
<tr align="right" valign="middle">
<td>
<span>[[${loginUser}]]님 환영합니다</span>
<a th:href="@{/user/logout}">로그아웃</a> <!-- 로그아웃 경로 설정 -->
</td>
</tr>
</table>
@GetMapping("logout")
public String logout(HttpServletRequest req) {
req.getSession().invalidate();
return "redirect:/";
}
