ㅁ 기존의 게시글 수정보다 어렵다.
- 한 게시글에 첨부파일이 여러개 있다. 다중파일 관련 수정이라 수정하기 기능의 로직이 전보다 어렵다.
ㅁ modify.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<c:set var="contextPath" value="${pageContext.request.contextPath}" />
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
<style>
.origin_attach_del{
color: gray;
font-size: 0.8em;
cursor: pointer;
}
</style>
</head>
<body>
<div class="container p-3">
<!-- Header, Nav start -->
<jsp:include page="/WEB-INF/views/common/header.jsp"/>
<!-- Header, Nav end -->
<!-- Section start -->
<section class="row m-3" style="min-height: 500px">
<div class="container border p-5 m-4 rounded">
<h2 class="m-4">게시글 수정</h2>
<br>
<form id="modify-form" method="post" action="${contextPath}/board/update.do" enctype="multipart/form-data">
<input type="hidden" name="boardNo" value="${b.boardNo}">
<div class="form-group">
<label for="title">제목 </label>
<input type="text" class="form-control" id="title" name="boardTitle" value="${ b.boardTitle }" required><br>
<label for="writer">작성자 </label>
<input type="text" class="form-control" id="writer" value="${ b.boardWriter }" readonly><br>
<label for="upfile">첨부파일 </label>
<input type="file" class="form-control-file border" id="upfile" name="uploadFiles" multiple>
<c:forEach var="at" items="${ b.attachList }">
<div>
<a href="${contextPath}${at.filePath}/${at.filesystemName}" download="${ at.originalName }">${ at.originalName }</a>
<span class="origin_attach_del" data-fileno="${at.fileNo}">x</span>
</div>
</c:forEach>
<br><br>
<label for="userName">내용 </label>
<textarea class="form-control" required name="boardContent" id="content" rows="10" style="resize:none;">${b.boardContent}</textarea><br>
</div>
<br>
<div align="center">
<button type="submit" class="btn btn-primary">수정하기</button>
<button type="button" class="btn btn-danger" onclick="javascript:history.go(-1);">이전으로</button>
</div>
</form>
<script>
$(document).ready(function(){
$(".origin_attach_del").on("click", function(){
// 삭제할 첨부파일 번호를 submit시 넘기기위한 작업
// 수정하기 submit시 form요소 내에 input type="hidden"으로 첨부파일 번호 숨기기
let hiddenEl = "<input type='hidden' name='delFileNo' value='" + $(this).data("fileno") + "'>";
$("#modify-form").append(hiddenEl);
// 화면으로부터 삭제된거 처럼 보여지게 해당 첨부파일 링크 삭제 처리
$(this).parent().remove();
})
})
</script>
</div>
</section>
<!-- Section end -->
<!-- Footer start -->
<jsp:include page="/WEB-INF/views/common/footer.jsp"/>
<!-- Footer end -->
</div>
</body>
</html>
- body~body 가져와서 복붙.
- 헤더 푸터 작성.
- $("#modify-form").append(hiddenEl);
jQuery를 사용하여 #modify-form이라는 id를 가진 요소에 hiddenEl이라는 요소를 추가하는 코드.
hiddenEl이라는 요소나 HTML 문자열을 #modify-form 요소의 자식 요소로 추가합니다.
- $(this).parent().remove();
jQuery를 사용하여 현재 요소의 부모 요소를 제거하는 코드.
ㅁ BoardController
package com.br.spring.controller;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import javax.servlet.http.HttpSession;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import com.br.spring.dto.AttachDto;
import com.br.spring.dto.BoardDto;
import com.br.spring.dto.MemberDto;
import com.br.spring.dto.PageInfoDto;
import com.br.spring.dto.ReplyDto;
import com.br.spring.service.BoardService;
import com.br.spring.util.FileUtil;
import com.br.spring.util.PagingUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@RequestMapping("/board")
@RequiredArgsConstructor
@Controller
public class BoardController {
private final BoardService boardService;
private final PagingUtil pagingUtil;
private final FileUtil fileUtil;
// 메뉴 바에 있는 메뉴 클릭시 /board/list.do => 1번 페이지 요청
// 페이징 바에 있는 페이지 클릭시 /board/list.do?page=xx
@GetMapping("/list.do")
public void list(@RequestParam(value="page", defaultValue="1") int currentPage, Model model) {
int listCount = boardService.selectBoardListCount();
PageInfoDto pi = pagingUtil.getPageInfoDto(listCount, currentPage, 5, 5);
List<BoardDto> list = boardService.selectBoardList(pi);
model.addAttribute("pi", pi);
model.addAttribute("list", list);
// return "board/list"; 생략해도 됨.
}
@GetMapping("/search.do")
public String search(@RequestParam(value="page") int currentPage
, @RequestParam Map<String, String> search
, Model model) {
// 요청시 전달값을 매개변수를 둬서 받기
// 지금은 무조건 page라는 key값으로 1번이 올거라 defaultValue를 안썼다. 써도 된다.
// 요청하는 페이지 번호는 currentPage에 담긴다.
// 지금 condition과 keyword 값을 받아줄 dto 커맨드 객체가 없다. String 형 변수 2개를 둬서 받아도 된다. 근데 어차피 넘길때는 Map에 담아서 넘기도록 서비스 impl를 설계했었다.
// ajax때 했던 @RequestBody로 Map으로 바로 받을 수 있었다.
// 아니면 @RequestParam Map<String, String> search로 해도 된다. 이렇게 해본다. 알아서 key과 value값이 담긴다.
// Map<String, String> search => {condition=user_id|board_Title|board_content, keyword=란}
int listCount = boardService.selectSearchListCount(search);
PageInfoDto pi = pagingUtil.getPageInfoDto(listCount, currentPage, 5, 5);
List<BoardDto> list = boardService.selectSearchList(search, pi);
model.addAttribute("pi", pi);
model.addAttribute("list", list);
model.addAttribute("search", search); // list.jsp가 로드되는 경우가 2가지 경우다. /search.do라는 요청으로 로드될 때는 search라는 key값으로 Map이 담겨있다.
return "board/list"; // 이미 만든 list.jsp로 포워딩
}
@GetMapping("/regist.do")
public void registPage() {} // 메소드명은 별 의미 없다. 마음대로 작성.
@PostMapping("/insert.do")
public String regist(BoardDto board // 글제목, 글내용은 담겨있다.
, List<MultipartFile> uploadFiles // 첨부파일 개수만큼 MultipartFile 객체가 담긴다.
, HttpSession session // 로그인한 회원정보(회원번호)는 session에서 뽑아야 한다.
, RedirectAttributes rdAttributes) {
// board테이블에 insert할 데이터
board.setBoardWriter( String.valueOf( ((MemberDto)session.getAttribute("loginUser")).getUserNo() ) );
// 첨부파일 업로드 후에 attachment테이블에 insert할 데이터
List<AttachDto> attachList = new ArrayList<>();
for(MultipartFile file : uploadFiles) {
if(file != null && !file.isEmpty()) {
Map<String, String> map = fileUtil.fileupload(file, "board");
attachList.add(AttachDto.builder()
.filePath(map.get("filePath"))
.originalName(map.get("originalName"))
.filesystemName(map.get("filesystemName"))
.refType("B")
.build() );
}
}
board.setAttachList(attachList); // 제목, 내용, 작성자회원번호, 첨부파일들정보
int result = boardService.insertBoard(board);
if( attachList.isEmpty() && result == 1 || !attachList.isEmpty() && result == attachList.size() ) { // && (AND) 연산자가 || (OR) 연산자보다 우선순위가 높습니다.
rdAttributes.addFlashAttribute("alertMsg", "게시글 등록 성공");
}else {
rdAttributes.addFlashAttribute("alertMsg", "게시글 등록 실패");
}
return "redirect:/board/list.do";
}
@GetMapping("/increase.do") // 조회수 증가용 (타인의 글일 경우 호출) => /board/detail.do 재요청
public String increaseCount(int no) {
boardService.updateIncreaseCount(no);
return "redirect:/board/detail.do?no=" + no;
}
@GetMapping("/detail.do")
public void detail(int no, Model model) { // 게시글 상세조회용 (내 글일 경우 이걸로 바로 호출)
// 상세페이지에 필요한 데이터
// 게시글(제목, 작성자, 작성일, 내용) 데이터, 첨부파일 데이터(원본명, 저장경로, 실제파일명)들
// 게시글을 조회하는 쿼리 따로, 첨부파일 조회하는 쿼리 따로 담아서 상세페이지로 이동을 해도 된다.
// 모든 데이터를 하나의 쿼리로 조회해서 하나의 BoardDto 객체에 담아서 상세페이지로 이동할 예정이다.
BoardDto b = boardService.selectBoard(no);
// boardNo, boardTitle, boardContent, boardWriter, registDt, attachList
model.addAttribute("b", b);
log.debug("BoardDto:{}", b);
}
@ResponseBody // 기본적으로 응답 뷰로 인식하기 때문에 붙여야 한다.
@GetMapping(value="/rlist.do", produces="application/json")
public List<ReplyDto> replyList(int no) {
return boardService.selectReplyList(no); // ajax로 응답데이터를 돌려줄 때는 그냥 return만 해주면 된다.
}
@ResponseBody
@PostMapping("/rinsert.do")
public String replyInsert(ReplyDto r, HttpSession session) { // ajax의 data에서 보낸 데이터의 key값과 매개변수 dto의 필드명이 같으면 바로 담긴다.
r.setReplyWriter(String.valueOf( ((MemberDto)session.getAttribute("loginUser")).getUserNo() ));
int result = boardService.insertReply(r);
return result > 0 ? "SUCCESS" : "FAIL";
}
@PostMapping("/delete.do")
public String remove(int no, RedirectAttributes rdAttributes) {
int result = boardService.deleteBoard(no);
if(result > 0) {
rdAttributes.addFlashAttribute("alertMsg", "성공적으로 삭제되었습니다.");
}else {
rdAttributes.addFlashAttribute("alertMsg", "게시글 삭제에 실패하였습니다.");
}
return "redirect:/board/list.do";
}
@PostMapping("/modify.do")
public void modifyPage(int no, Model model) {
model.addAttribute("b", boardService.selectBoard(no));
}
@PostMapping("/update.do")
public String modify(BoardDto board // 번호, 제목, 내용
, String[] delFileNo // 삭제할 첨부파일 번호들 (x누르면 hidden으로 생긴다)
, List<MultipartFile> uploadFiles // 새로 넘어온 첨부파일들
, RedirectAttributes rdAttributes) {
// log.debug("board: {}", board);
// log.debug("delFileNo: {}", Arrays.toString(delFileNo));
// log.debug("uploadFiles: {}", uploadFiles);
// 후에 db에 반영 성공시, 삭제할 파일들의 삭제를 위해 미리 조회
// db에 수정 요청 성공시 new File(~).delete()로 삭제할 파일 정보 미리 조회
List<AttachDto> delAttachList = boardService.selectDelAttach(delFileNo);
List<AttachDto> attachList = new ArrayList<>();
for(MultipartFile file : uploadFiles) {
if(file != null && !file.isEmpty()) {
Map<String, String> map = fileUtil.fileupload(file, "board");
attachList.add(AttachDto.builder()
.filePath(map.get("filePath"))
.originalName(map.get("originalName"))
.filesystemName(map.get("filesystemName"))
.refType("B")
.refNo(board.getBoardNo())
.build() );
}
}
board.setAttachList(attachList); // BoardDto가 첨부파일 리스트를 필드로 가짐
int result = boardService.updateBoard(board, delFileNo);
if(result > 0) { // 성공
rdAttributes.addFlashAttribute("alertMsg", "성공적으로 수정되었습니다.");
for(AttachDto at : delAttachList) {
new File(at.getFilePath() + "/" + at.getFilesystemName()).delete();
}
}else { // 실패
rdAttributes.addFlashAttribute("alertMsg", "게시글 수정에 실패했습니다.");
// db에 기록에 실패했을 때도 새로 넘어온 첨부파일은 저장이 된다. 삭제해줘야 한다. 그건 직접 해보세요.
}
return "redirect:/board/detail.do?no=" + board.getBoardNo();
// board 테이블 update 무조건 진행
// 삭제할 첨부파일이 있었을 경우 => attachment 테이블로부터 delete, 파일 삭제
// 새로 넘어온 첨부파일이 있었을 경우 => 파일업로드, attachment 테이블로부터 insert
}
}
ㅁ BoardService
package com.br.spring.service;
import java.util.List;
import java.util.Map;
import com.br.spring.dto.AttachDto;
import com.br.spring.dto.BoardDto;
import com.br.spring.dto.PageInfoDto;
import com.br.spring.dto.ReplyDto;
public interface BoardService {
// 게시글 목록 조회 (페이징 처리)
int selectBoardListCount();
List<BoardDto> selectBoardList(PageInfoDto pi);
// 게시글 검색 조회 (페이징 처리)
int selectSearchListCount(Map<String, String> search);
List<BoardDto> selectSearchList(Map<String, String> search, PageInfoDto pi);
// 게시글 등록
int insertBoard(BoardDto b); // 첨부파일도 BoardDto 객체에 담아버릴 예정
// 게시글 상세 - 조회수 증가
int updateIncreaseCount(int boardNo);
// 게시글 상세 - 게시글 조회
BoardDto selectBoard(int boardNo); // 첨부파일도 BoardDto 객체에 담아버릴 예정
// 게시글 삭제
int deleteBoard(int boardNo);
// 댓글 목록 조회
List<ReplyDto> selectReplyList(int boardNo);
// 댓글 등록
int insertReply(ReplyDto r);
// 게시글 수정 <- 다중 첨부파일 수정
List<AttachDto> selectDelAttach(String[] delFileNo);
int updateBoard(BoardDto b, String[] delFileNo);
}
ㅁ BoardServiceImpl
package com.br.spring.service;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.springframework.stereotype.Service;
import com.br.spring.dao.BoardDao;
import com.br.spring.dto.AttachDto;
import com.br.spring.dto.BoardDto;
import com.br.spring.dto.PageInfoDto;
import com.br.spring.dto.ReplyDto;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
@Service
public class BoardServiceImpl implements BoardService {
private final BoardDao boardDao;
@Override
public int selectBoardListCount() {
return boardDao.selectBoardListCount();
}
@Override
public List<BoardDto> selectBoardList(PageInfoDto pi) {
return boardDao.selectBoardList(pi);
}
@Override
public int selectSearchListCount(Map<String, String> search) {
return boardDao.selectSearchListCount(search);
}
@Override
public List<BoardDto> selectSearchList(Map<String, String> search, PageInfoDto pi) {
return boardDao.selectSearchList(search, pi);
}
@Override
public int insertBoard(BoardDto b) { // 컨트롤러에서 BoardDto에 작성자, 제목, 내용, 첨부파일 list도 담아서 넘겼다.
int result = boardDao.insertBoard(b);
List<AttachDto> list = b.getAttachList();
if(result > 0 && !list.isEmpty()) {
result = 0;
for(AttachDto attach : list) {
result += boardDao.insertAttach(attach);
}
}
return result;
}
@Override
public int updateIncreaseCount(int boardNo) {
return boardDao.updateIncreaseCount(boardNo);
}
@Override
public BoardDto selectBoard(int boardNo) {
return boardDao.selectBoard(boardNo);
}
@Override
public int deleteBoard(int boardNo) {
return boardDao.deleteBoard(boardNo);
}
@Override
public List<ReplyDto> selectReplyList(int boardNo) {
return boardDao.selectReplyList(boardNo);
}
@Override
public int insertReply(ReplyDto r) {
return boardDao.insertReply(r);
}
@Override
public List<AttachDto> selectDelAttach(String delFileNo[]) {
return delFileNo == null ? new ArrayList<>() : boardDao.selectDelAttach(delFileNo);
}
@Override
public int updateBoard(BoardDto b, String[] delFileNo) {
// 1) board 테이블에 update
int result1 = boardDao.updateBoard(b);
// 2) attachment 테이블에 delete
int result2 = 1; // delFileNo이 null일 때도 ~ 성공으로 해야하니까(이 경우에는)
if(result1 > 0 && delFileNo != null) {
result2 = boardDao.deleteAttach(delFileNo);
}
// 3) attachment 테이블에 insert
List<AttachDto> list = b.getAttachList();
int result3 = 0;
for(AttachDto at : list) {
result3 += boardDao.insertAttach(at);
}
// 성공에 대한 조건- result1이 1이어야 하고, result2가 0보다 커야 하고, result3가 list의 사이즈와 동일해야 한다.
return result1 == 1
&& result2 > 0
&& result3 == list.size()
? 1 : -1;
}
}
ㅁ BoardDao
package com.br.spring.dao;
import java.util.List;
import java.util.Map;
import org.apache.ibatis.session.RowBounds;
import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.stereotype.Repository;
import com.br.spring.dto.AttachDto;
import com.br.spring.dto.BoardDto;
import com.br.spring.dto.PageInfoDto;
import com.br.spring.dto.ReplyDto;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
@Repository
public class BoardDao {
private final SqlSessionTemplate sqlSession;
public int selectBoardListCount() {
return sqlSession.selectOne("boardMapper.selectBoardListCount");
}
public List<BoardDto> selectBoardList(PageInfoDto pi) {
RowBounds rowBounds = new RowBounds((pi.getCurrentPage() - 1) * pi.getBoardLimit(), pi.getBoardLimit());
return sqlSession.selectList("boardMapper.selectBoardList", null, rowBounds);
}
public int selectSearchListCount(Map<String, String> search) {
return sqlSession.selectOne("boardMapper.selectSearchListCount", search);
}
public List<BoardDto> selectSearchList(Map<String, String> search, PageInfoDto pi) {
RowBounds rowBounds = new RowBounds((pi.getCurrentPage() - 1) * pi.getBoardLimit(), pi.getBoardLimit());
return sqlSession.selectList("boardMapper.selectSearchList", search, rowBounds); // 쿼리를 완성시키기 위해 2번째 인자값으로 map객체를 넘김.
}
public int insertBoard(BoardDto b) {
return sqlSession.insert("boardMapper.insertBoard", b);
}
public int insertAttach(AttachDto attach) {
return sqlSession.insert("boardMapper.insertAttach", attach);
}
public BoardDto selectBoard(int boardNo) {
return sqlSession.selectOne("boardMapper.selectBoard", boardNo); // 결국 우리는 하나의 객체로 받는다.
}
public int updateIncreaseCount(int boardNo) {
return sqlSession.update("boardMapper.updateIncreaseCount", boardNo);
}
public List<ReplyDto> selectReplyList(int boardNo) {
return sqlSession.selectList("boardMapper.selectReplyList", boardNo);
}
public int insertReply(ReplyDto r) {
return sqlSession.insert("boardMapper.insertReply", r);
}
public int deleteBoard(int boardNo) {
return sqlSession.update("boardMapper.deleteBoard", boardNo);
}
public List<AttachDto> selectDelAttach(String[] delFileNo) {
return sqlSession.selectList("boardMapper.selectDelAttach", delFileNo);
}
public int updateBoard(BoardDto b) {
return sqlSession.update("boardMapper.updateBoard", b);
}
public int deleteAttach(String[] delFileNo) {
return sqlSession.delete("boardMapper.deleteAttach", delFileNo);
}
}
ㅁ board-mapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<mapper namespace="boardMapper">
<resultMap id="boardResultMap" type="BoardDto">
<result column="board_no" property="boardNo" />
<result column="board_title" property="boardTitle" />
<result column="board_content" property="boardContent" />
<result column="user_id" property="boardWriter" />
<result column="count" property="count" />
<result column="regist_date" property="registDt" />
<result column="attach_count" property="attachCount" />
<!-- has many 관계(한 객체에 여러객체(List)를 가지고 있는)일 경우 collection -->
<!-- case 1) List 내의 객체를 매핑시켜주는 resultMap이 따로 존재하지 않을 경우
<collection ofType="AttachDto" property="attachList">
<result column="file_no" property="fileNo" />
<result column="file_path" property="filePath" />
<result column="filesystem_name" property="filesystemName" />
<result column="original_name" property="originalName" />
</collection>-->
<!-- case 2) List내의 객체를 매핑시켜주는 resultMap이 따로 존재할 경우 -->
<collection resultMap="attachResultMap" property="attachList" />
</resultMap>
<resultMap id="attachResultMap" type="AttachDto">
<result column="file_no" property="fileNo" />
<result column="file_path" property="filePath" />
<result column="filesystem_name" property="filesystemName" />
<result column="original_name" property="originalName" />
</resultMap>
<!-- 혹시라도 has a 관계(1:1)일 경우 => collection 대신에 association으로 사용. 그리고 type이 다름. -->
<resultMap id="replyResultMap" type="ReplyDto">
<result column="reply_no" property="replyNo" />
<result column="user_id" property="replyWriter" />
<result column="reply_content" property="replyContent" />
<result column="regist_date" property="registDt" />
</resultMap>
<select id="selectBoardListCount" resultType="_int">
select
count(*)
from board
where status = 'Y'
</select>
<select id="selectBoardList" resultMap="boardResultMap">
select
b.board_no
, board_title
, user_id
, count
, to_char(regist_date, 'YYYY-MM-DD') as "regist_date"
, (
select count(*)
from attachment
where ref_type = 'B'
and ref_no = b.board_no
) as "attach_count"
from board b
join member on (user_no = board_writer)
where b.status = 'Y'
order by board_no desc
</select>
<select id="selectSearchListCount" resultType="_int">
select
count(*)
from board b
join member on (user_no = board_writer)
where b.status = 'Y'
and ${condition} like '%' || #{keyword} || '%'
</select>
<select id="selectSearchList" resultMap="boardResultMap">
select
b.board_no
, board_title
, user_id
, count
, to_char(regist_date, 'YYYY-MM-DD') as "regist_date"
, (
select count(*)
from attachment
where ref_type = 'B'
and ref_no = b.board_no
) as "attach_count"
from board b
join member on (user_no = board_writer)
where b.status = 'Y'
and ${condition} like '%' || #{keyword} || '%'
order by board_no desc
</select>
<insert id="insertBoard">
insert
into board
(
board_no
, board_title
, board_writer
, board_content
)
values
(
seq_bno.nextval
, #{boardTitle}
, #{boardWriter}
, #{boardContent}
)
</insert>
<insert id="insertAttach">
insert
into attachment
(
file_no
, file_path
, filesystem_name
, original_name
, ref_type
, ref_no
)
values
(
seq_ano.nextval
, #{filePath}
, #{filesystemName}
, #{originalName}
, #{refType}
<choose>
<when test="refNo == 0">
, seq_bno.currval
</when>
<otherwise>
, #{refNo}
</otherwise>
</choose>
)
</insert>
<select id="selectBoard" resultMap="boardResultMap">
select
board_no
, board_title
, board_content
, user_id
, to_char(regist_date, 'YYYY-MM-DD') regist_date
, file_no
, file_path
, filesystem_name
, original_name
from board b
join member on (user_no = board_writer)
left join attachment on (ref_type = 'B' and ref_no = board_no)
where b.status = 'Y'
and board_no = #{boardNo}
</select>
<update id="updateIncreaseCount">
update
board
set count = count + 1
where board_no = #{boardNo}
and status = 'Y'
</update>
<select id="selectReplyList" resultMap="replyResultMap">
select
reply_no
, user_id
, reply_content
, to_char(regist_date, 'YYYY-MM-DD') regist_date
from reply r
join member on (user_no = reply_writer)
where ref_bno = #{boardNo}
and r.status = 'Y'
order by reply_no desc
</select>
<insert id="insertReply">
insert
into reply
(
reply_no
, reply_content
, ref_bno
, reply_writer
)
values
(
seq_rno.nextval
, #{replyContent}
, #{refBoardNo}
, #{replyWriter}
)
</insert>
<update id="deleteBoard">
update
board
set status = 'N'
where board_no = #{boardNo}
</update>
<select id="selectDelAttach" resultMap="attachResultMap">
select
file_path
, filesystem_name
from attachment
<where>
<foreach item="no" collection="array"
open="file_no in (" separator="," close=")">
#{no}
</foreach>
</where>
</select>
<update id="updateBoard">
update
board
set board_title = #{boardTitle}
, board_content = #{boardContent}
where board_no = #{boardNo}
</update>
<delete id="deleteAttach">
delete
from attachment
<where>
<foreach item="no" collection="array"
open="file_no in (" separator="," close=")">
#{no}
</foreach>
</where>
</delete>
</mapper>
- 기존의 #insertAttach 쿼리를 수정했다.
게시글을 작성할 때도 첨부파일이 있다면 #insertAttach 쿼리가 수행되지만,
게시글을 수정할 때도 추가할 첨부파일이 있다면 #insertAttach 쿼리가 수행되게 수정하였다.
(아니면 그냥 쿼리를 하나 더 만들어서 따로 수행되게 해도된다)
- 게시글 수정시 첨부파일 추가할 때는 ref_no 컬럼(참조 게시글 번호)에
기존처럼(게시글 작성) seq_bno.currval가 아니라
현재 게시글의 번호가 넘어가야 한다.
- result3 += boardDao.insertAttach(at);
BoardServiceImpl 클래스의 updateBoard 메소드에서 BoardDao 클래스의 insertAttach 메소드를 호출하면서 매개변수로 at를 넘기고 있다.
- at는 BoardDto에 있는 List<AttachDto> 타입의 attachList 변수에서 반복문으로 하나씩 꺼낸 AttachDto 클래스이다.
AttachDto 클래스에는 아래와 같은 필드들이 있다.
private int fileNo;
private String filePath;
private String filesystemName;
private String originalName;
private Date uploadDt;
private String refType;
private int refNo;
- 이 AttachDto 클래스를 #insertAttach 쿼리로 보내서, AttachDto 클래스의 refNo 컬럼을 마이바티스의 #{ } 구문으로 값을 바인딩 (변수에 값을 삽입) 한다.
- refNo 필드에 기존 게시글 번호가 담겨있다. 그 값이 존재하냐 존재하지 않냐에 따라서 if~else 처리를 한다.
refNo 필드가 값이 담기지 않은 채로 보내지면 0이 된다.
Java의 int 타입은 초기화되지 않은 경우 기본값이 0으로 설정되기 때문이다.
- <choose>의 <when>의 test 속성에는 중괄호 블럭이 필요 없다. 바로 쓰면 된다.
ㅁ 서버 start
- x에 커서 우클릭 검사하면 저렇게 뜬다.
- x를 클릭하면 form 내에 hidden 요소가 추가되는걸 볼 수 있다. 파일번호를 넘긴다.
- x를 누른다고 실제로 파일 삭제 요청이 가는것은 아니지만 화면에서 삭제된 것처럼 해당 첨부파일 링크를 삭제처리 한다.
- 이미 첨부파일이 3개 있는 게시글을 수정한다. 글 제목과 글 내용만 수정한다.
첨부파일은 추가도 삭제도 하지 않는다.
로그를 보면 delFileNo는 null이다.
파일을 선택하지 않아도 List는 항상 생성되고 MultipartFile 객체가 담겨있을 수 있다.
ㅁ
- 제목과 내용만 수정해도 잘 반영되고, 첨부파일 추가해도 잘 보여지고, 첨부파일 일부나 전체를 삭제해도 잘 보여진다.
화면에도 보여지고 db에도 반영되고 C:\upload\board\20241031에도 파일이 생성, 삭제된다.
수정 후에는 상세페이지가 다시 보여진다.