[Spring_demo프로젝트] 2. Board(list_검색 조건, 페이징 처리, NEW, HOT) / 로그아웃

jngyoon·2023년 10월 18일
0

혼공일기

목록 보기
20/24

#231004 수업 복습

  index에서 로그인하면 /user/login.html로 post로 전송 
-> UserController.java의 login 메소드 실행 
-> 로그인 성공시 redirect:/board/list로 재요청
-> BoardController 실행

Board

BoardController.java 생성

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>

header.html

<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>

footer.html

<th:block th:fragment="footer">
	<footer>
		<div class="center">
			<div class="copy">
				&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>

src/main/resources/templates > board 폴더 생성 > list.html 생성

list.html

<!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}">&lt;</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}">&gt;</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>

Service

BoardService.java 인터페이스(설계)

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;
	
	
}

BoardServiceImpl.java 클래스(구현)

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);
	}
}

BoardMapper.java 인터페이스

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);
}

BoardMapper.xml (실제 쿼리문)

<!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>

Criteria

페이지를 띄우는 기준을 작성하는 DTO

Criteria.java

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();	//빌더가 가지고 있는 설정대로 문자열 만들기
	}
	
	
}

페이징 처리

pageDTO.java

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;	//다음 버튼
	}
}

BoardController.java 중 list메소드 부분

@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객체를 생성하여 보내줌
	}

BoardMapper.xml 중 getTotal

	<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>-->

list.html 중 페이징 처리 테이블

공통 - 페이지 변경하는 것 class = changePage 
<!-- 페이징 처리하는 테이블 -->
		<table class="pagination">
			<tr align="center" valign="middle">
				<td>
                  	<!-- 이전버튼 -->
					<a class="changePage" th:if="${pageMaker.prev}" th:href="${pageMaker.startPage-1}">&lt;</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}">&gt;</a>
				</td>
			</tr>
		</table>

검색

list.html 중 검색창(searchForm) 부분

<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>

list.html 중 검색, 페이지 javascript 부분

<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>

게시글 HOT, NEW

BoardController.java에서 newly_board, reply_cnt_list, recent_reply addAttribute

@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));
	}

BoardServiceImpl.java에서 기능 구현

	//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;
	}

ReplyMapper.java(인터페이스) 생성

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);
}

ReplyMapper.xml 생성

<!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>

list.html 중 인기순, 최신순 처리

<!-- 게시글 리스트 띄우는 테이블 -->
		<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>

로그아웃

header.html 중 로그아웃 부분

<table class="header_area">
			<tr align="right" valign="middle">
				<td>
					<span>[[${loginUser}]]님 환영합니다</span>	
					<a th:href="@{/user/logout}">로그아웃</a> <!-- 로그아웃 경로 설정 -->
				</td>
			</tr>
		</table>

UserController.java 중 logout 메소드에 의해 첫 화면으로 이동

@GetMapping("logout")
	public String logout(HttpServletRequest req) {
		req.getSession().invalidate();
		return "redirect:/";
	}

0개의 댓글