#231005 수업 내용 복습
<!-- 글쓰기 버튼 배치하는 테이블 -->
<table>
<tr align="right" valign="middle">
<td>
<!-- 글쓰기 버튼을 누르면 글쓰기 페이지(write.html)로 이동 -->
<a class="write" th:href="${'/board/write'+pageMaker.cri.listLink}">글쓰기</a>
</td>
</tr>
</table>
getListLink() : 페이지 이동시 필요한 pagenum, amount, keyword, type 파라미터를
쿼리스트림으로 돌려주는 메소드
public String getListLink() {
// /board/write?userid=apple 라는 uri를 만들고 싶다면
// fromPath("/board/write").queryParam("userid","apple")
//fromPath : ? 앞에 붙는 uri 문자열
UriComponentsBuilder builder = UriComponentsBuilder.fromPath("") //uri 생성
.queryParam("pagenum", pagenum) //queryParam : 파라미터 추가
.queryParam("amount", amount)
.queryParam("keyword",keyword)
.queryParam("type", type);
return builder.toUriString(); //toUriString : 빌더가 가지고 있는 설정대로 문자열 만들기
}

@GetMapping("write") //a태그는 get 방식
public void write(@ModelAttribute("cri") Criteria cri,Model model) {
System.out.println(cri);
}
//void로 선언했기 때문에 '/board/wirte.html'을 찾음
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Write</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 id="wrap">
<form id="boardForm" method="post" name="boardForm" th:action="@{/board/write}" enctype="multipart/form-data">
<!-- 날아온 네개의 파라미터를 받아 놓음 -->
<input type="hidden" th:value="${cri.pagenum}" name="pagenum">
<input type="hidden" th:value="${cri.amount}" name="amount">
<input type="hidden" th:value="${cri.type}" name="type">
<input type="hidden" th:value="${cri.keyword}" name="keyword">
<!-- 글 작성 테이블 -->
<table style="border-collapse: collapse;" border="1">
<tr style="height:30px;">
<th style="text-align:center; width:150px;">제목</th>
<td>
<input type="text" name="boardtitle" size="50" maxlength="50" placeholder="제목을 입력하세요">
</td>
</tr>
<tr style="height:30px;">
<th style="text-align:center; width:150px;">작성자</th>
<td>
<!-- 로그인유저 작성자에 박아 놓기 -->
<input type="text" name="userid" size="50" maxlength="50" th:value="${session.loginUser}" readonly>
</td>
</tr>
<tr style="height:300px;">
<th style="text-align:center; width:150px;">내 용</th>
<td>
<textarea name="boardcontents" style="width:700px;height:290px;resize:none;"></textarea>
</td>
</tr>
<tr class="r0 at">
<th>파일 첨부1</th>
<td class="file0_cont">
<div style="float:left;">
<input type="file" name="files" id="file0" style="display:none">
<span id="file0name">선택된 파일 없음</span>
</div>
<div style="float:right; margin-right: 100px;">
<a href="javascript:upload('file0')">파일 선택</a>
<a href="javascript:cancelFile('file0')">첨부 삭제</a>
</div>
</td>
</tr>
</table>
</form>
<!-- 등록, 목록 버튼 테이블 -->
<table class="btn_area">
<tr align="right" valign="middle">
<td>
<a href="javascript:sendit()">등록</a> <!-- javascript의 sendit 함수 호출 -->
<a th:href="${'/board/list'+cri.listLink}">목록</a> <!-- cri.listLink를 붙여 목록으로 돌아가기 -->
</td>
</tr>
</table>
</div>
<th:block th:replace="~{layout/footer::footer}"></th:block>
</body>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script>
let i = 0;
function upload(name){
$("#"+name).click();
}
//$(선택자).change(함수) : 해당 선택자의 요소에 변화가 일어난다면 넘겨주는 함수 호출
$("[type='file']").change(function(e){
//e : 파일이 업로드된 상황 자체를 담고있는 객체
//e.target : 파일이 업로드가 된 input[type=file] 객체(태그객체)
//e.target.files : 파일태그에 업로드를 한 파일 객체들의 배열
const file = e.target.files[0];
const fileTag = e.target;
if(file == undefined){
//파일이 업로드 되었다가 없어진 경우
cancelFile(fileTag.id);
}
else{
//파일이 없었다가 업로드 한 경우
//#file0name
$("#"+fileTag.id+"name").text(file.name);
//업로드 된 파일의 확장자명
let ext = file.name.split(".").pop();
if(ext == 'jpeg' || ext == 'jpg' || ext == 'png' || ext == 'gif' || ext == 'webp'){
$("."+fileTag.id+"_cont .thumbnail").remove();
const reader = new FileReader();
reader.onload = function(ie){
const img = document.createElement("img");
img.setAttribute("src",ie.target.result)
img.setAttribute("class","thumbnail");
document.querySelector("."+fileTag.id+"_cont").appendChild(img);
}
reader.readAsDataURL(file);
}
else{
const temp = $("."+fileTag.id+"_cont .thumbnail");
if(temp != null){
temp.remove();
}
}
//가장 마지막 파일 선택 버튼을 눌렀을 때
if(fileTag.id.split("e")[1] == i){
const cloneElement = $(".r"+i).clone(true);
i++;
cloneElement.appendTo("#boardForm tbody")
const lastElement = $("#boardForm tbody").children().last();
lastElement.attr("class","r"+i+" at");
lastElement.children("th").text("파일 첨부"+(i+1));
lastElement.children("td").attr("class","file"+i+"_cont");
lastElement.find("input[type='file']").attr("name","files");
lastElement.find("input[type='file']").attr("id","file"+i);
lastElement.find("input[type='file']").val("");
lastElement.find("span").attr("id","file"+i+"name");
lastElement.find("span").text("선택된 파일 없음");
lastElement.find("a")[0].href = "javascript:upload('file"+i+"')";
lastElement.find("a")[1].href = "javascript:cancelFile('file"+i+"')"
}
}
})
function cancelFile(name){
//가장 마지막 첨부 삭제 버튼을 누른 경우
if(name.split("e")[1] == i){ return; }
//현재 업로드된 파일이 여러개일 때
if(i != 0){
//tr지우기
let temp = Number(name.split("e")[1]);
//해당 행 지우기
$(".r"+temp).remove();
//지워진 다음 행 부터 숫자 바꿔주기
for(let j=temp+1;j<=i;j++){
const el = $("#boardForm tbody").find(".r"+j);
el.attr("class","r"+(j-1)+" at");
el.children('th').text("파일 첨부"+j);
el.children('td').attr("class","file"+(j-1)+"_cont");
const fileTag = el.find("input[type='file']");
fileTag.attr("name","file"+(j-1));
fileTag.attr("id","file"+(j-1));
el.find("span").attr("id","file"+(j-1)+"name");
el.find("a")[0].href = "javascript:upload('file"+(j-1)+"')"
el.find("a")[1].href = "javascript:cancelFile('file"+(j-1)+"')"
}
i--;
}
}
function sendit(){
const boardForm = document.boardForm;
const boardtitle = boardForm.boardtitle;
if(boardtitle.value == ""){
alert("제목을 입력하세요!");
boardtitle.focus();
return false;
}
const boardcontents = boardForm.boardcontents;
if(boardcontents.value == ""){
alert("내용을 입력하세요!");
boardcontents.focus();
return false;
}
boardForm.submit();
}
</script>
</html>
@PostMapping("write")
public String write(BoardDTO board, MultipartFile[] files, Criteria cri) throws Exception{
Long boardnum = 0l; //long 타입의 0(0+l)
if(service.regist(board, files)) {
boardnum = service.getLastNum(board.getUserid());
return "redirect:/board/get"+cri.getListLink()+"&boardnum="+boardnum;
} //성공시 게시글 보는 페이지로 이동(get.html)
else {
return "redirect:/board/list"+cri.getListLink();
} //실패시 게시글 목록 페이지로 이동
}
package com.kh.demo.service;
//import 생략
@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));
//transferTo : 지정한 패스대로 파일을 실제 경로에 써주는 메소드
//DB에 저장
flag = fmapper.insertFile(fdto) == 1;
if(!flag) {
//업로드 했던 파일 삭제, 게시글 데이터 삭제(생략)
return flag;
}
}
}
return true;
}
//게시글 등록 이외 생략
}
package com.kh.demo.mapper;
import java.util.List;
import org.apache.ibatis.annotations.Mapper;
import com.kh.demo.domain.dto.FileDTO;
@Mapper
public interface FileMapper {
int insertFile(FileDTO file);
List<FileDTO> getFiles(Long boardnum); //파일 목록보여주는거
int deleteBySystemname(String systemname);
int deleteByBoardnum(Long boardnum);
}
<insert id="insertBoard">
insert into t_board (boardtitle,boardcontents,userid)
values(#{boardtitle},#{boardcontents},#{userid})
</insert>
<select id="getLastNum">
select max(boardnum) from t_board where userid=#{userid}
</select>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.kh.demo.mapper.FileMapper">
<insert id="insertFile">
insert into t_file values(#{systemname},#{orgname},#{boardnum})
</insert>
</mapper>
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Get</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 id="wrap">
<form>
<!-- 게시글 테이블 -->
<table style="border-collapse: collapse;" border="1" class="board_area">
<tr style="height:30px;">
<th style="text-align:center; width:150px;">제목</th>
<td>
<input type="text" name="boardtitle" size="50" maxlength="50" th:value="${board.boardtitle}" readonly>
</td>
</tr>
<tr style="height:30px;">
<th style="text-align:center; width:150px;">작성자</th>
<td>
<input type="text" name="userid" size="50" maxlength="50" th:value="${board.userid}" readonly>
</td>
</tr>
<tr>
<th>조회수</th>
<td>[[${board.readcount}]]</td>
</tr>
<tr style="height:300px;">
<th style="text-align:center; width:150px;">내 용</th>
<td>
<textarea name="boardcontents" style="width:700px;height:290px;resize:none;" readonly>[[${board.boardcontents}]]</textarea>
</td>
</tr>
<!-- 파일 갯수에 따라 반복문 -->
<th:block th:if="${files != null and files.size() > 0}" th:each="file : ${files}">
<!-- th:with = 타임리프 내에서 사용가능한 어떤 변수가 설정된 구간-->
<th:block th:with="sar=${file.orgname.split('[.]')}">
<!-- .은 정규식이어서 대괄호 []로 감싸야 문자 . 으로 사용 가능 -->
<tr>
<th>첨부파일[[${fileStat.index+1}]]</th>
<td>
<a th:href="@{/board/file (systemname=${file.systemname},orgname=${file.orgname})}">[[${file.orgname}]]</a>
</td>
</tr>
<!-- ext : 확장자 변수 -->
<th:block th:with="ext=${sar[sar.length-1]}">
<tr th:if="${ext == 'jpg' or ext == 'jpeg' or ext == 'png' or ext == 'gif' or ext == 'webp'}">
<td></td>
<td>
<!-- 썸네일 -->
<img style="width:100%;" th:src="@{/board/thumbnail (systemname=${file.systemname})}">
</td>
</tr>
</th:block>
</th:block>
</th:block>
<th:block th:unless="${files != null and files.size() > 0}">
<tr>
<td colspan="2" style="text-align: center; font-size: 20px;">첨부 파일이 없습니다.</td>
</tr>
</th:block>
</table>
</form>
<!-- 수정, 삭제 버튼 테이블 -->
<table class="btn_area">
<tr align="right" valign="middle">
<td>
<th:block th:if="${board.userid == session.loginUser}">
<form name="boardForm" method="post" th:action="@{/board/remove}">
<input name="boardnum" th:value="${board.boardnum}" type="hidden">
<input name="pagenum" th:value="${cri.pagenum}" type="hidden">
<input name="amount" th:value="${cri.amount}" type="hidden">
<input name="type" th:value="${cri.type}" type="hidden">
<input name="keyword" th:value="${cri.keyword}" type="hidden">
<a href="javascript:modify()">수정</a>
<a href="javascript:document.boardForm.submit()">삭제</a>
</form>
</th:block>
<a th:href="${'/board/list'+cri.listLink}">목록</a>
</td>
</tr>
</table>
<!-- 댓글 div -->
<div class="reply_line">
<a href="#" class="regist">댓글 등록</a>
<div class="replyForm row">
<div style="width:20%">
<h4>작성자</h4>
<input type="text" name="userid" th:value="${session.loginUser}" readonly style="text-align: center;">
</div>
<div style="width:65%">
<h4>내 용</h4>
<textarea name="replycontents" placeholder="Contents" style="resize:none;"></textarea>
</div>
<div style="width:15%">
<a href="#" class="button finish" style="margin-bottom:1rem;">등록</a>
<a href="#" class="button cancel">취소</a>
</div>
</div>
<ul class="replies"></ul>
<div class="page"></div>
</div>
</div>
<th:block th:replace="~{layout/footer::footer}"></th:block>
</body>
<script th:src="@{/board/js/reply.js}"></script>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script th:inline="javascript">
const loginUser = /*[[${session.loginUser}]]*/'';
const boardnum = /*[[${board.boardnum}]]*/'';
const replies = $(".replies")
const page = $(".page")
let pagenum = 0;
$(document).ready(function(){
$(".replyForm").hide();
pagenum = 1;
})
$(".regist").on("click",function(e){
e.preventDefault();
$(".replyForm").show();
$(this).hide();
})
$(".finish").on("click",function(e){
e.preventDefault();
let replycontents = $("[name='replycontents']").val();
replyService.add(
{"boardnum":boardnum,"userid":loginUser, "replycontents":replycontents},
function(result){
alert("등록!");
}
)
})
function showList(pagenum){
replyService.getList(
{boardnum:boardnum, pagenum:pagenum||1},
function(replyCnt, list){
let str = "";
if(list == null || list.length == 0){
str+= '<li class="noreply" style="clear:both;">등록된 댓글이 없습니다.</li>';
replies.html(str);
return;
}
for(let i=0;i<list.length;i++){
//<li style="clear:both;" class="li3">
str += '<li style="clear:both;" class="li'+list[i].replynum+'">';
str += '<div style="display:inline; float:left; width:80%;">';
//<strong class="userid3">apple</strong>
str += '<strong class="userid'+list[i].replynum+'">'+list[i].userid+'</strong>';
//<p class="reply3">댓글내용</p>
str += '<p class="reply'+list[i].replynum+'">'+list[i].replycontents+'</p>';
str += '</div><div style="text-align:right;">';
str += '<strong>'+replyService.displayTime(list[i])+'</strong>'
if(list[i].userid){
}
}
}
)
}
function showReplyPage(replyCnt, pagenum){
let endPage = Math.ceil(pagenum/5)*5;
let startPage = endPage - 4;
let prev = startPage != 1;
endPage = (endPage-1)*5 >= replyCnt ? Math.ceil(replyCnt/5) : endPage;
let next = endPage*5 < replyCnt ? true : false;
let str = "";
if(prev){
//<a class="changePage" href="5"><code><</code></a>
str += '<a class="changePage" href="'+(startPage-1)+'"><code><</code></a>';
}
for(let i=startPage;i<endPage;i++){
if(i == pagenum){
//<code class="nowPage">7</code>
str += '<code class="nowPage">'+i+'</code>';
}
else{
//<a class="changePage" href="9"><code>9</code></a>
str += '<a class="changePage" href="'+i+'"><code>'+i+'</code></a>';
}
}
if(next){
str += '<a class="changePage" href="'+(endPage-1)+'"><code>></code></a>';
}
page.html(str);
$(".changePage").on("click")
}
function modify(){
const boardForm = document.boardForm;
boardForm.setAttribute("action",/*[[@{/board/modify}]]*/'');
boardForm.setAttribute("method","get");
boardForm.submit();
}
</script>
</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>
<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태그 부분을 클릭하면 보여짐 -->
<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>
<!-- 이하 생략 -->
<!-- get부분 javascript -->
<script th:inline="javascript">
$(".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();
})
</script>
@GetMapping(value = {"get","modify"})
public String get(Criteria cri, Long boardnum, HttpServletRequest req, HttpServletResponse resp, Model model) {
model.addAttribute("cri",cri); //Criteria 추가
HttpSession session = req.getSession(); //session 받아오기
BoardDTO board = service.getDetail(boardnum); //클릭한 boardnum에 해당하는 상세정보 board 객체에 담기
model.addAttribute("board",board); //board에 board 객체 추가
model.addAttribute("files",service.getFileList(boardnum)); //files 추가
String loginUser = (String)session.getAttribute("loginUser"); //현재 로그인유저 담기
String requestURI = req.getRequestURI();
if(requestURI.contains("/get")) {
//게시글의 작성자가 로그인된 유저가 아닐 때 => 게시글 조회수 1 증가(cookie가 없을때만)
if(!board.getUserid().equals(loginUser)) {
//쿠키 검사
Cookie[] cookies = req.getCookies();
Cookie read_board = null;
if(cookies != null) {
for(Cookie cookie : cookies) {
//ex) 1번 게시글을 조회하고자 클릭했을 때에는 "read_board1" 쿠키를 찾음
//cookie의 이름이 read_board+boardnum이라면
if(cookie.getName().equals("read_board"+boardnum)) {
read_board = cookie;
break; //cookie가 있으면 조회수 안올릴거니까 break
}
}
}
//read_board가 null이라는 뜻은 위에서 쿠키를 찾았을 때 존재하지 않았다는 뜻
//첫 조회거나 조회한지 1시간이 지난 후(cookie가 없는 경우)
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;
}
@GetMapping("thumbnail")
public ResponseEntity<Resource> thumbnail(String systemname) throws Exception{
return service.getThumbnailResource(systemname);
}
//ResponseEntity<Resource> : 응답으로 특정한 데이터를 보낼건데 그 데이터는Resource 타입이고, Resource라는 타입은 어떤 파일을 자원화시킨 타입 => 특정 파일을 자원화시켜 바디에 담아서 응답으로 보내는 메소드
@Override
public void updateReadCount(Long boardnum) {
bmapper.updateReadCount(boardnum);
}
@Override
public BoardDTO getDetail(Long boardnum) {
return bmapper.findByNum(boardnum);
}
@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);
}
<update id="updateReadCount">
update t_board set readcount = readcount+1 where boardnum = #{boardnum}
</update>
<select id="findByNum">
select * from t_board where boardnum=#{boardnum}
</select>
<select id="getFiles">
select * from t_file where boardnum=#{boardnum}
</select>