게시판 첨부파일 업로드 및 다운로드
테이블 생성
게시판 첨부파일(board_attachment) - 게시글 번호(board_no) : 외래키, 게시판 테이블(board)의 게시글 번호(board_no) 참조, 필수 입력 - 첨부파일 번호(attachment_no) : 외래키, 첨부파일 테이블(attachment)의 첨부파일 번호(attachment_no) 참조, 필수 입력 ** 하나의 게시글에 하나의 파일만 첨부할 수 있도록 설정하기 위해 게시글 번호(board_no)와 첨부파일 번호(attachment_no)를 복합키로 설정 |
-- 테이블 생성
create table board_attachment(
board_no references board(board_no) on delete cascade not null,
attachment_no references attachment(attachment_no) on delete cascade not null,
primary key(board_no, attachment_no)
);
게시글의 첨부파일 업로드
AttachmentDao
public interface AttachmentDao {
// 추상 메소드 - 첨부파일 번호를 위한 다음 시퀀스 번호 반환
int sequence();
// 추상 메소드 - 첨부파일 테이블(attachment) 등록
void insert(AttachmentDto attachmentDto);
}
AttachmentDaoImpl
@Repository
public class AttachmentDaoImpl implements AttachmentDao {
// 의존성 주입
@Autowired
private JdbcTemplate jdbcTemplate;
// 추상 메소드 오버라이딩 - 첨부파일 번호를 위한 다음 시퀀스 번호 반환
@Override
public int sequence() {
String sql = "select attachment_seq.nextval from dual";
return jdbcTemplate.queryForObject(sql, int.class);
}
// 추상 메소드 오버라이딩 - 첨부파일 테이블(attachment) 등록
@Override
public void insert(AttachmentDto attachmentDto) {
String sql = "insert into attachment("
+ "attachment_no, "
+ "attachment_name, "
+ "attachment_type, "
+ "attachment_size) "
+ "values(?, ?, ?, ?)";
Object[] param = new Object[] {attachmentDto.getAttachmentNo(),
attachmentDto.getAttachmentName(),
attachmentDto.getAttachmentType(),
attachmentDto.getAttachmentSize()};
jdbcTemplate.update(sql, param);
}
}
BoardDao
public interface BoardDao {
// 추상 메소드 - 게시글 첨부파일 테이블(board_attachment) 등록
void connectAttachment(int boardNo, int attachmentNo);
}
BoardDaoImpl
@Repository
public class BoardDaoImpl implements BoardDao {
// 의존성 주입
@Autowired
JdbcTemplate jdbcTemplate;
// 추상 메소드 오버라이딩 - 게시글 첨부파일 테이블(board_attachment) 등록
@Override
public void connectAttachment(int boardNo, int attachmentNo) {
String sql = "insert into board_attachment(board_no, attachment_no) values(?, ?)";
Object[] param = new Object[] {boardNo, attachmentNo};
jdbcTemplate.update(sql, param);
}
}
BoardController
- 첨부 파일의 상위 경로의 문자열을 추상 경로로 변환하여 File 클래스의 인스턴스 생성(의존성 주입)
- @PostConstruct를 통해 의존성 주입 후 실행되어야 하는 메소드를 정의할 수 있다
여기서는 첨부파일의 상위 경로 디렉토리 생성 (폴더가 없으면 자동으로 생성)
- 게시글 작성시 DB에 총 3번의 등록(INSERT)을 실행한다
1) 게시판 테이블(board)에 게시글 정보 등록
2) 첨부파일 테이블(attachment)에 해당 게시글의 첨부파일 정보 등록
3) 게시글 첨부파일 테이블(board_attachment)에 해당 게시글 번호와 해당 첨부파일 번호 등록
@Controller
@RequestMapping("/board")
public class BoardController {
// 의존성 주입
@Autowired
BoardDao boardDao;
// 의존성 주입
@Autowired
private AttachmentDao attachmentDao;
// 첨부 파일의 상위 경로의 문자열을 추상 경로로 변환하여 File 클래스의 인스턴스 생성(의존성 주입)
private final File directory = new File("C:\\Users\\hyeul\\upload");
// 의존성 주입 후 실행되어야 하는 메소드
@PostConstruct
public void prepare() {
directory.mkdirs(); // 해당 추상 경로의 디렉토리 생성 (폴더가 없으면 자동으로 생성)
}
// 1. 게시글 작성
// 게시글 작성 Mapping
@GetMapping("/write")
public String write() {
return "board/write";
}
// 게시글 작성 Mapping에 DTO 전달 및 DB 처리
@PostMapping("/write")
public String write(HttpSession session,
@ModelAttribute BoardDto boardDto,
@RequestParam List<MultipartFile> attachment,
RedirectAttributes attr) throws IllegalStateException, IOException {
// 게시글 작성자(boardWriter)가 로그인 중인 아이디가 되도록 session에서 회원 아이디를 반환
String boardWriter = (String) session.getAttribute("loginId");
// setter 메소드로 게시글 작성자(boardWriter)를 session에서 반환한 로그인 아이디로 설정
boardDto.setBoardWriter(boardWriter);
// 게시글 번호(boardNo)를 위한 다음 시퀀스 번호 반환
int boardNo = boardDao.sequence();
// 등록 전 '게시글'인지 '답글'인지 판정
if(boardDto.getBoardParent() == 0) { // 게시글이라면
// 그룹(boardGroup)은 게시글 번호(= 반환한 다음 시퀀스 번호)
boardDto.setBoardGroup(boardNo);
// 상위글(boardParent)은 0 (없다)
boardDto.setBoardParent(0);
// 차수(boardDepth)는 0
boardDto.setBoardDepth(0);
}
else { // 답글이라면
// View에서 하이퍼링크를 통해 전달받은 boardParent(key)의 값인 boardNo(value)로 단일 조회 실행
BoardDto parentDto = boardDao.selectOne(boardDto.getBoardParent());
// 그룹(boardGroup)은 단일 조회의 결과로 얻은 parentDto의 그룹과 같다
boardDto.setBoardGroup(parentDto.getBoardGroup());
// 차수(boardDepth)는 단일 죄호의 결과로 얻은 parentDto의 차수에 1을 더한 값
boardDto.setBoardDepth(parentDto.getBoardDepth() + 1);
// 상위글(boardParent)은 하이퍼링크를 통해 전달받으며 그 값(value)는 boardNo가 된다
}
// setter 메소드로 게시글 번호(boardNo)가 다음 시퀀스 번호가 되도록 설정
boardDto.setBoardNo(boardNo);
// 설정이 끝난 boardDto로 게시판 테이블(board)에 등록(INSERT) 실행
boardDao.write(boardDto);
// 게시판 테이블(board)에 등록(INSERT) 후 첨부 파일 등록 및 저장
for(MultipartFile file : attachment) {
// 첨부 파일이 있다면 (file의 isEmpty()의 결과가 false라면)
if(!file.isEmpty()) {
// 첨부 파일 번호(attachmentNo)를 위한 다음 시퀀스 번호 반환
int attachmentNo = attachmentDao.sequence();
// AttachmentDto의 인스턴스를 생성하여 첨부 파일 테이블(attachment)에 등록(INSERT) 실행
attachmentDao.insert(AttachmentDto.builder()
.attachmentNo(attachmentNo)
.attachmentName(file.getOriginalFilename())
.attachmentType(file.getContentType())
.attachmentSize(file.getSize())
.build());
// directory의 추상 경로를 상위 경로, 해당 파일의 시퀀스 번호를 하위 경로로 하는 File의 인스턴스 생성
File target = new File(directory, String.valueOf(attachmentNo));
// 첨부 파일을 해당 디렉토리에 저장
file.transferTo(target);
// 게시판 첨부파일 테이블(board_attachment)에도 등록(INSERT) 실행
boardDao.connectAttachment(boardNo, attachmentNo);
}
}
// 게시글 작성 후 해당 게시글의 상세 Mapping으로 강제 이동(redirect)
attr.addAttribute("boardNo", boardNo);
return "redirect:detail";
}
}
board의 write.jsp
- form의 인코딩 방식 추가
- 첨부파일 입력 추가
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
<jsp:include page="/WEB-INF/views/template/header.jsp">
<jsp:param value="자유 게시판" name="title"/>
</jsp:include>
<%-- 게시글인지 답글인지에 따라 제목이 달라지도록 설정 --%>
<c:set var = "isReply" value = "${param.boardParent != null}"></c:set>
<c:choose>
<c:when test = "${isReply}">
<h1>답글 작성</h1>
</c:when>
<c:otherwise>
<h1>게시글 작성</h1>
</c:otherwise>
</c:choose>
<div align = "center">
<%-- form의 인코딩 방식 추가 --%>
<form action = "write" method = "post" enctype = "multipart/form-data">
<table border="1" width="500">
<tbody>
<tr>
<td>
<%-- 답글이라면 상위글 번호를 추가로 전송하도록 설정 --%>
<c:if test="${isReply}">
<input type = "hidden" name = "boardParent" value = "${param.boardParent}">
</c:if>
</td>
</tr>
<tr>
<th>말머리</th>
<td>
<select name="boardHead">
<option value="">선택</option>
<option>정보</option>
<option>유머</option>
<%-- 관리자만 공지글을 작성할 수 있도록 설정 --%>
<c:if test="${mg == '관리자'}">
<option>공지</option>
</c:if>
</select>
</td>
</tr>
<tr>
<th>제목</th>
<td>
<input type="text" name="boardTitle" required>
</td>
</tr>
<tr>
<th>내용</th>
<td>
<textarea name="boardContent" rows="10" cols="50" required></textarea>
</td>
</tr>
<%-- 첨부파일 입력 추가 --%>
<tr>
<th>첨부파일</th>
<td>
<input type = "file" name = "attachment" multiple>
</td>
</tr>
</tbody>
<tfoot>
<tr>
<td align = "right" colspan = "2">
<a href = "list">목록으로</a>
<button type = "submit">작성하기</button>
</td>
</tr>
</tfoot>
</table>
</form>
</div>
<jsp:include page = "/WEB-INF/views/template/footer.jsp"></jsp:include>
해당 게시글의 첨부파일 조회
- 게시글 첨부파일 테이블(board_attachment) 조회는 이미 RowMapper가 있는 AttachmentDaoImpl에 만든다
AttachmentDao
public interface AttachmentDao {
// 추상 메소드 - 게시글 첨부파일 테이블(board_attachment) 조회
List<AttachmentDto> selectBoardAttachmentList(int boardNo);
}
AttachmentDaoImpl
@Repository
public class AttachmentDaoImpl implements AttachmentDao {
// 의존성 주입
@Autowired
private JdbcTemplate jdbcTemplate;
// AttachmentDto에 대한 RowMapper
private RowMapper<AttachmentDto> mapper = new RowMapper<>() {
@Override
public AttachmentDto mapRow(ResultSet rs, int rowNum) throws SQLException {
return AttachmentDto.builder()
.attachmentNo(rs.getInt("attachment_no"))
.attachmentName(rs.getString("attachment_name"))
.attachmentType(rs.getString("attachment_type"))
.attachmentSize(rs.getLong("attachment_size"))
.attachmentTime(rs.getDate("attachment_time"))
.build();
}
};
// 추상 메소드 오버라이딩 - 게시글 첨부파일 테이블(board_attachment) 조회
@Override
public List<AttachmentDto> selectBoardAttachmentList(int boardNo) {
String sql = "select * from board_attachment_view where board_no = ?";
Object[] param = new Object[] {boardNo};
return jdbcTemplate.query(sql, mapper, param);
}
}
BoardController
- model에 게시글 첨부파일 테이블(board_attachment) 조회 결과를 첨부
@Controller
@RequestMapping("/board")
public class BoardController {
// 의존성 주입
@Autowired
BoardDao boardDao;
// 의존성 주입
@Autowired
ReplyDao replyDao;
// 의존성 주입
@Autowired
private MemberBoardLikeDao memberBoardLikeDao;
// 의존성 주입
@Autowired
private AttachmentDao attachmentDao;
// 3. 게시글 상세
// 게시글 상세 Mapping
@GetMapping("/detail")
public String detail(Model model, @RequestParam int boardNo, HttpSession session) {
// HttpSession에 읽은 게시글 번호를 저장할 수 있는 Set 생성
Set<Integer> history = (Set<Integer>) session.getAttribute("history");
// 만약 history가 없다면 새로 생성
if(history == null) {
history = new HashSet<>();
}
// 현재 게시글 번호를 저장한 적이 있는지 판정
// - HashSet의 .add(E e) 명령은 저장할 수 있는지(boolean)을 반환
if(history.add(boardNo)) { // 저장할 수 있다면 (처음 들어온 게시글)
// 조회수 증가 + 게시글 상세의 결과를 model에 첨부
BoardDto boardDto = boardDao.read(boardNo);
model.addAttribute("boardDto", boardDto);
}
else { // 저장할 수 없다면 (이미 들어온 적이 있는 게시물)
// 게시글 상세의 결과를 model에 첨부
BoardDto boardDto = boardDao.selectOne(boardNo);
model.addAttribute("boardDto", boardDto);
}
// 갱신된 history를 다시 HttpSession에 저장 (해당 게시글을 다시 볼 때 조회수 증가 방지)
session.setAttribute("history", history);
// 2. 댓글 목록
// - 댓글 목록은 해당 게시글의 상세 페이지에 표시된다
// - 댓글 전체 조회의 결과를 model에 첨부
model.addAttribute("replyList", replyDao.replyList(boardNo));
// 2. 게시글 좋아요 여부
// - 로그인 상태이면 해당 게시글 좋아요 여부를 조회하여 model에 첨부
// HttpSession에서 로그인 중인 회원 아이디를 반환
String memberId = (String) session.getAttribute("loginId");
if(memberId != null) { // 로그인 중이라면(memberId의 값이 null이 아니라면)
// DTO의 인스턴스 생성
MemberBoardLikeDto memberBoardLikeDto = new MemberBoardLikeDto();
// DTO의 회원 아이디(memberId)를 HttpSession에서 반환한 회원 아이디(memberId)로 설정
memberBoardLikeDto.setMemberId(memberId);
// DTO의 게시글 번호(boardNo)를 RequestParam에서 입력받은 게시글 번호(boardNo)로 설정
memberBoardLikeDto.setBoardNo(boardNo);
// 설정이 끝난 memberBoardLikeDto로 게시글 좋아요 여부를 조회한 결과를 반환
model.addAttribute("isLike", memberBoardLikeDao.check(memberBoardLikeDto));
}
// 3. 게시글 총 좋아요 갯수 첨부
model.addAttribute("likeCount", memberBoardLikeDao.count(boardNo));
// 해당 게시글의 첨부파일 목록 조회 결과 첨부
model.addAttribute("attachmentList", attachmentDao.selectBoardAttachmentList(boardNo));
// 게시글 상세 페이지(detail.jsp)로 연결
return "board/detail";
}
}
board 폴더의 detail.jsp
- 해당 게시글에 포함된 첨부파일 목록 표시
<h1>게시글 상세</h1>
<table border = "1" width = "900">
<tbody>
<tr>
<th width = "25%">번호</th>
<td>${boardDto.boardNo}</td>
</tr>
<tr>
<th>말머리</th>
<td>${boardDto.boardHead}</td>
</tr>
<tr>
<th>제목</th>
<td>
${boardDto.boardTitle}
<%-- 게시글 좋아요 여부에 따라 표시되는 하트 모양 --%>
<c:choose>
<c:when test = "${isLike}">
<a href = "like?boardNo=${boardDto.boardNo}">♥</a>
</c:when>
<c:otherwise>
<a href = "like?boardNo=${boardDto.boardNo}">♡</a>
</c:otherwise>
</c:choose>
</td>
</tr>
<tr>
<th>작성자</th>
<td>${boardDto.boardWriter}</td>
</tr>
<tr height = "200" valign = "top">
<th>내용</th>
<td>
<%-- pre 태그는 엔터, 띄어쓰기, 탭키 등을 있는 그대로 표시하는 영역 --%>
<pre>${boardDto.boardContent}</pre>
</td>
</tr>
<tr>
<th>조회수</th>
<td>${boardDto.boardRead}</td>
</tr>
<tr>
<th>작성일</th>
<td>
<fmt:formatDate value = "${boardDto.boardWritetime}" pattern = "y년 M월 d일 E요일 a h시 m분 s초"/>
</td>
</tr>
<%-- 한 번이라도 수정한 적이 있다면(수정 시간이 null이 아니라면) 수정 시간을 표시 --%>
<c:if test = "${boardDto.boardUpdatetime != null}">
<tr>
<th>수정일</th>
<td>
<fmt:formatDate value = "${boardDto.boardUpdatetime}" pattern = "y년 M월 d일 E요일 a h시 m분 s초"/>
</td>
</tr>
</c:if>
<%-- 첨부파일이 있다면 첨부파일 목록이 표시되도록 --%>
<c:if test = "${attachmentList != null}">
<tr>
<th>첨부파일</th>
<td>
<ul>
<c:forEach var = "attachmentList" items = "${attachmentList}">
<li>
${attachmentList.attachmentName}
(${attachmentList.attachmentSize} bytes)
-
[${attachmentList.attachmentType}]
</li>
</c:forEach>
</ul>
</td>
</tr>
</c:if>
</tbody>
<tfoot>
<tr>
<td colspan = "2" align = "right">
<%-- 로그인 상태에서만 게시글 및 답글 작성이 가능하도록 설정 --%>
<c:if test = "${loginId != null}">
<a href = "write">글쓰기</a>
<%-- 답글 작성시 하이퍼링크를 통해 boardParent(key)와 boardNo(value)를 전달 --%>
<a href = "write?boardParent=${boardDto.boardNo}">답글쓰기</a>
</c:if>
<%-- 로그인한 회원이 게시글 작성자인지에 대한 변수(boolean) 생성 --%>
<c:set var = "owner" value = "${loginId == boardDto.boardWriter}"></c:set>
<%-- 로그인한 회원의 등급이 관리자인지에 대한 변수(boolean) 생성 --%>
<c:set var = "admin" value = "${mg == '관리자'}"></c:set>
<%-- 만약 로그인한 회원이 게시글 작성자라면 게시글을 수정할 수 있도록 설정--%>
<c:if test = "${owner}">
<a href = "edit?boardNo=${boardDto.boardNo}">수정</a>
</c:if>
<%-- 만약 로그인한 회원이 게시글 작성자이거나 등급이 관리자라면 게시글을 삭제할 수 있도록 설정--%>
<c:if test = "${owner || admin}">
<a href = "delete?boardNo=${boardDto.boardNo}">삭제</a>
</c:if>
<a href = "list">목록으로</a>
</td>
</tr>
</tfoot>
</table>
다음과 같이 첨부파일 목록이 표시된다
해당 게시글의 첨부파일 다운로드
@RestController
- @Controller + @ResponseBody의 역할
- @Controller는 View의 이름 반환
- @RestController는 JSON 형태의 데이터를 반환
AttachmentRestController
- 첨부파일 번호(attachmentNo)를 입력받아 해당 첨부파일을 다운로드하는 RestController
@RestController
@RequestMapping("/attachment")
public class AttachmentController {
// 의존성 주입
@Autowired
private AttachmentDao attachmentDao;
// 첨부파일 다운로드 (경로 변수를 이용하는 방법)
@GetMapping("/download/{attachmentNo}")
public ResponseEntity<ByteArrayResource> download(@PathVariable int attachmentNo) throws IOException{
// 입력받은 첨부파일 번호로 단일조회 실행 후 해당 첨부파일의 DTO 반환
AttachmentDto attachmentDto = attachmentDao.selectOne(attachmentNo);
// 만약 해당 첨부파일 번호에 대한 DTO가 없을 경우 404 Not Found 에러를 반환
if(attachmentDto == null) {
return ResponseEntity.notFound().build();
}
// 해당 첨부파일 번호에 대한 DTO가 존재할 경우
// 다운로드 상위 경로 지정
File dir = new File("C:\\\\Users\\\\hyeul\\\\upload");
// 다운로드 하위 경로 지정 (저장할 때 파일명을 숫자로 저장했으므로 경로로 쓸 때는 숫자를 문자열로 변환해야 한다)
File target = new File(dir, String.valueOf(attachmentNo));
// 다운로드 경로를 Byte 배열로 변환
byte[] data = FileUtils.readFileToByteArray(target);
// Byte 배열로 ByteArrayResource의 인스턴스 생성
ByteArrayResource resource = new ByteArrayResource(data);
// HTTP Response Header에 내용의 인코딩 방식, 길이, 배치 방식, resource의 형식 정보를 반환
// HTTP Response Body에 ByteArrayResource를 포함하는 ResponseEntity 반환
return ResponseEntity.ok()
.header("Content-Encoding", "UTF-8")
.header("Content-Length", String.valueOf(attachmentDto.getAttachmentSize()))
.header("Content-Disposition", "attachment; filename=" + attachmentDto.getAttachmentName())
.header("Content-Type", attachmentDto.getAttachmentType())
.body(resource);
}
}
detail.jsp
- 첨부파일 정보 칸에 첨부파일을 다운로드할 수 있도록 링크 추가
<h1>게시글 상세</h1>
<table border = "1" width = "900">
<tbody>
<tr>
<th width = "25%">번호</th>
<td>${boardDto.boardNo}</td>
</tr>
<tr>
<th>말머리</th>
<td>${boardDto.boardHead}</td>
</tr>
<tr>
<th>제목</th>
<td>
${boardDto.boardTitle}
<%-- 게시글 좋아요 여부에 따라 표시되는 하트 모양 --%>
<c:choose>
<c:when test = "${isLike}">
<a href = "like?boardNo=${boardDto.boardNo}">♥</a>
</c:when>
<c:otherwise>
<a href = "like?boardNo=${boardDto.boardNo}">♡</a>
</c:otherwise>
</c:choose>
</td>
</tr>
<tr>
<th>작성자</th>
<td>${boardDto.boardWriter}</td>
</tr>
<tr height = "200" valign = "top">
<th>내용</th>
<td>
<%-- pre 태그는 엔터, 띄어쓰기, 탭키 등을 있는 그대로 표시하는 영역 --%>
<pre>${boardDto.boardContent}</pre>
</td>
</tr>
<tr>
<th>조회수</th>
<td>${boardDto.boardRead}</td>
</tr>
<tr>
<th>작성일</th>
<td>
<fmt:formatDate value = "${boardDto.boardWritetime}" pattern = "y년 M월 d일 E요일 a h시 m분 s초"/>
</td>
</tr>
<%-- 한 번이라도 수정한 적이 있다면(수정 시간이 null이 아니라면) 수정 시간을 표시 --%>
<c:if test = "${boardDto.boardUpdatetime != null}">
<tr>
<th>수정일</th>
<td>
<fmt:formatDate value = "${boardDto.boardUpdatetime}" pattern = "y년 M월 d일 E요일 a h시 m분 s초"/>
</td>
</tr>
</c:if>
<%-- 첨부파일이 있다면 첨부파일 목록이 표시되도록 --%>
<c:if test = "${attachmentList != null}">
<tr>
<th>첨부파일</th>
<td>
<ul>
<c:forEach var = "attachmentList" items = "${attachmentList}">
<li>
${attachmentList.attachmentName}
(${attachmentList.attachmentSize} bytes)
-
[${attachmentList.attachmentType}]
<%-- 첨부파일 다운로드 링크 추가 --%>
<a href = "/attachment/download/${attachmentList.attachmentNo}">↓</a>
</li>
</c:forEach>
</ul>
</td>
</tr>
</c:if>
</tbody>
<tfoot>
<tr>
<td colspan = "2" align = "right">
<%-- 로그인 상태에서만 게시글 및 답글 작성이 가능하도록 설정 --%>
<c:if test = "${loginId != null}">
<a href = "write">글쓰기</a>
<%-- 답글 작성시 하이퍼링크를 통해 boardParent(key)와 boardNo(value)를 전달 --%>
<a href = "write?boardParent=${boardDto.boardNo}">답글쓰기</a>
</c:if>
<%-- 로그인한 회원이 게시글 작성자인지에 대한 변수(boolean) 생성 --%>
<c:set var = "owner" value = "${loginId == boardDto.boardWriter}"></c:set>
<%-- 로그인한 회원의 등급이 관리자인지에 대한 변수(boolean) 생성 --%>
<c:set var = "admin" value = "${mg == '관리자'}"></c:set>
<%-- 만약 로그인한 회원이 게시글 작성자라면 게시글을 수정할 수 있도록 설정--%>
<c:if test = "${owner}">
<a href = "edit?boardNo=${boardDto.boardNo}">수정</a>
</c:if>
<%-- 만약 로그인한 회원이 게시글 작성자이거나 등급이 관리자라면 게시글을 삭제할 수 있도록 설정--%>
<c:if test = "${owner || admin}">
<a href = "delete?boardNo=${boardDto.boardNo}">삭제</a>
</c:if>
<a href = "list">목록으로</a>
</td>
</tr>
</tfoot>
</table>
서비스 계층
- @Service
- 여러 도메인의 비즈니스 로직을 의미있는 수준으로 묶어서 처리하는 계층
ex) 게시글 작성
- 게시글 작성시 게시판 테이블과 첨부파일 테이블에 INSERT 후 해당 게시글 번호를 반환
BoardService
public interface BoardService {
// 추상 메소드 - 게시글 작성 후 해당 게시글의 번호를 반환하도록
int write(BoardDto boardDto, List<MultipartFile> attachment) throws IllegalStateException, IOException;
}
BoardServiceImpl
@Service
public class BoardServiceImpl implements BoardService{
// 의존성 주입
@Autowired
private BoardDao boardDao;
// 의존성 주입
@Autowired
private AttachmentDao attachmentDao;
// 파일 업로드를 위한 상위 경로 설정
private final File directory = new File("C:\\Users\\hyeul\\upload");
@Override
public int write(BoardDto boardDto, List<MultipartFile> attachment) throws IllegalStateException, IOException {
// 게시글 번호(boardNo)를 위한 다음 시퀀스 번호 반환
int boardNo = boardDao.sequence();
// 등록 전 '게시글'인지 '답글'인지 판정
if(boardDto.getBoardParent() == 0) { // 게시글이라면
// 그룹(boardGroup)은 게시글 번호(= 반환한 다음 시퀀스 번호)
boardDto.setBoardGroup(boardNo);
// 상위글(boardParent)은 0 (없다)
boardDto.setBoardParent(0);
// 차수(boardDepth)는 0
boardDto.setBoardDepth(0);
}
else { // 답글이라면
// View에서 하이퍼링크를 통해 전달받은 boardParent(key)의 값인 boardNo(value)로 단일 조회 실행
BoardDto parentDto = boardDao.selectOne(boardDto.getBoardParent());
// 그룹(boardGroup)은 단일 조회의 결과로 얻은 parentDto의 그룹과 같다
boardDto.setBoardGroup(parentDto.getBoardGroup());
// 차수(boardDepth)는 단일 죄호의 결과로 얻은 parentDto의 차수에 1을 더한 값
boardDto.setBoardDepth(parentDto.getBoardDepth() + 1);
// 상위글(boardParent)은 하이퍼링크를 통해 전달받으며 그 값(value)는 boardNo가 된다
}
// setter 메소드로 게시글 번호(boardNo)가 다음 시퀀스 번호가 되도록 설정
boardDto.setBoardNo(boardNo);
// 설정이 끝난 boardDto로 게시판 테이블(board)에 등록(INSERT) 실행
boardDao.write(boardDto);
// 게시판 테이블(board)에 등록(INSERT) 후 첨부 파일 등록 및 저장
for(MultipartFile file : attachment) {
// 첨부 파일이 있다면 (file의 isEmpty()의 결과가 false라면)
if(!file.isEmpty()) {
// 첨부 파일 번호(attachmentNo)를 위한 다음 시퀀스 번호 반환
int attachmentNo = attachmentDao.sequence();
// AttachmentDto의 인스턴스를 생성하여 첨부파일 테이블(attachment)에 등록(INSERT) 실행
attachmentDao.insert(AttachmentDto.builder()
.attachmentNo(attachmentNo)
.attachmentName(file.getOriginalFilename())
.attachmentType(file.getContentType())
.attachmentSize(file.getSize())
.build());
// directory의 추상 경로를 상위 경로, 해당 파일의 시퀀스 번호를 하위 경로로 하는 File의 인스턴스 생성
File target = new File(directory, String.valueOf(attachmentNo));
// 첨부파일을 해당 디렉토리에 저장
file.transferTo(target);
// 게시판 첨부파일 테이블(board_attachment)에도 등록(INSERT) 실행
boardDao.connectAttachment(boardNo, attachmentNo);
}
}
// 게시글 번호 반환
return boardNo;
}
}
BoardController
- Service에서 게시글 등록시 필요한 과정을 한번에 처리하도록
@Controller
@RequestMapping("/board")
public class BoardController {
// 의존성 주입
@Autowired
private BoardService boardService;
// 의존성 주입 후 실행되어야 하는 메소드
@PostConstruct
public void prepare() {
directory.mkdirs(); // 해당 추상 경로의 디렉토리 생성 (폴더가 없으면 자동으로 생성)
}
// 게시글 작성 Mapping
@GetMapping("/write")
public String write() {
return "board/write";
}
// 게시글 작성 Mapping에 DTO 전달 및 DB 처리
@PostMapping("/write")
public String write(HttpSession session,
@ModelAttribute BoardDto boardDto,
@RequestParam List<MultipartFile> attachment,
RedirectAttributes attr) throws IllegalStateException, IOException {
// 게시글 작성자(boardWriter)가 로그인 중인 아이디가 되도록 session에서 회원 아이디를 반환
String boardWriter = (String) session.getAttribute("loginId");
// setter 메소드로 게시글 작성자(boardWriter)를 session에서 반환한 로그인 아이디로 설정
boardDto.setBoardWriter(boardWriter);
// 게시글 작성시 게시판과 첨부파일 테이블에 INSERT 후 해당 게시글의 번호를 반환 (@Service 적용)
int boardNo = boardService.write(boardDto, attachment);
// 게시글 작성 후 해당 게시글의 상세 Mapping으로 강제 이동(redirect)
attr.addAttribute("boardNo", boardNo);
return "redirect:detail";
}
}
예외 처리
@ControllerAdvice
- @Controller나 @RestController에서 발생한 예외를 한 곳에서 관리하고 처리
- 예외 처리 대상을 특정하는 방법
1) 어노테이션으로 특정
@ControllerAdvice(annotations = {Controller.class}) | Controller라는 어노테이션이 붙은 모든 클래스 |
2) 패키지명으로 특정
@ControllerAdvice(basePackages = {"com.kh.springhome.controller"}) | springhome 패키지에 있는 모든 컨트롤러 |
@ExceptionHandler
- @Controller 내에 Method 범위로 예외 처리
@ExceptionHandler(예외 종류) |
ExceptionProcessor 클래스 생성
- Controller에서 예외 발생시
@ControllerAdvice(basePackages = {"com.kh.springhome.controller"}) // springhome 패키지에 있는 모든 컨트롤러
public class ExceptionProcessor {
// 전반적인 예외에 대한 예외 처리
@ExceptionHandler(Exception.class)
public String exception(Exception e) {
return "error/exception";
}
// TargetNotFoundException에 대한 예외처리
@ExceptionHandler(TargetNotFoundException.class)
public String notFound(Exception e) {
return "error/notFound";
}
}
src/main/resources - public - error 폴더 생성 후 exception.jsp와 notFound.jsp 생성
exception.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<jsp:include page = "/WEB-INF/views/template/header.jsp">
<jsp:param name = "title" value = "게시글 목록"/>
</jsp:include>
<h1>일시적인 오류가 발생했습니다</h1>
<img src = "https://placeimg.com/700/400/tech">
<jsp:include page = "/WEB-INF/views/template/footer.jsp"></jsp:include>
notFound.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<jsp:include page = "/WEB-INF/views/template/header.jsp">
<jsp:param name = "title" value = "게시글 목록"/>
</jsp:include>
<h1>대상을 찾을 수 없습니다</h1>
<img src = "https://placeimg.com/700/400/tech">
<jsp:include page = "/WEB-INF/views/template/footer.jsp"></jsp:include>
첨부파일 삭제
게시글 첨부파일 삭제
- 게시글 삭제시 해당 게시글에 엮여있는 첨부파일도 모두 삭제되어야 한다
- 게시판 첨부파일 테이블(boardAttachment)은 on delete cascade 조건에 의해 글 삭제시 자동으로 레코드가 삭제된다
- 첨부파일 테이블(attachment)의 레코드 삭제와 실제 첨부파일 삭제 처리가 필요하다
BoardController
1. 게시글 삭제 전 삭제할 게시글의 첨부파일 정보 조회 (하나의 게시글에 여러 파일을 첨부할 수 있으므로 List 형태)
2. 반복문을 이용하여 실제 첨부파일 삭제 : 첨부파일 테이블(attachment)의 레코드 삭제
- 게시판 첨부파일 테이블(boardAttachment)의 레코드는 자동 삭제(on delete cascade)
@Controller
@RequestMapping("/board")
public class BoardController {
// 의존성 주입
@Autowired
private AttachmentDao attachmentDao;
// 게시글 삭제
// - 게시글 삭제 Mapping
@GetMapping("/delete")
public String delete(@RequestParam int boardNo) {
// 1. 게시글 삭제 전 삭제할 게시글의 게시글 번호로 해당 게시글의 첨부파일 정보 조회
List<AttachmentDto> attachmentList = attachmentDao.selectBoardAttachmentList(boardNo);
// 게시글 삭제 성공 여부
// - 게시글이 삭제되면 자동으로 게시글 첨부파일 테이블(board_attachment)의 데이터가 삭제됨 (on delete cascade)
boolean result = boardDao.delete(boardNo);
// 게시글 삭제에 성공했다면 해당 게시글의 상세 Mapping으로 강제 이동(redirect)
if(result) {
// 2. 첨부파일 데이터 삭제
for(AttachmentDto attachmentDto : attachmentList) {
// 첨부파일 테이블(attachment)의 데이터 삭제
attachmentDao.delete(attachmentDto.getAttachmentNo());
// 실제 첨부파일 삭제
// 1) 첨부파일명 반환
String filename = String.valueOf(attachmentDto.getAttachmentName());
// 2) 하위 경로 설정
File target = new File(directory, filename);
// 3) 첨부파일 삭제
target.delete();
}
return "redirect:list";
}
// 게시글 수정에 실패했다면 설정한 예외 발생
else {
throw new TargetNotFoundException();
}
}
}
게시글 삭제시 첨부파일 삭제를 Service화
BoardService
public interface BoardService {
// 추상 메소드 - 게시글 삭제시 첨부파일 삭제
boolean delete(int boardNo);
}
BoardServiceImpl
@Service
public class BoardServiceImpl implements BoardService{
// 의존성 주입
@Autowired
private BoardDao boardDao;
// 의존성 주입
@Autowired
private AttachmentDao attachmentDao;
// 파일 업로드를 위한 상위 경로 설정
private final File directory = new File("C:\\Users\\hyeul\\upload");
// 추상 메소드 오버라이딩 - 게시글 삭제시 첨부파일 삭제
@Override
public boolean delete(int boardNo) {
// 1. 게시글 삭제 전 삭제할 게시글의 게시글 번호로 해당 게시글의 첨부파일 정보 조회
List<AttachmentDto> attachmentList = attachmentDao.selectBoardAttachmentList(boardNo);
// 게시글 삭제 성공 여부
// - 게시글이 삭제되면 자동으로 게시글 첨부파일 테이블(board_attachment)의 데이터가 삭제됨 (on delete cascade)
boolean result = boardDao.delete(boardNo);
// 게시글 삭제 여부에 따른 처리
if(result) {
// 2. 첨부파일 데이터 삭제
for(AttachmentDto attachmentDto : attachmentList) {
// 첨부파일 테이블(attachment)의 데이터 삭제
attachmentDao.delete(attachmentDto.getAttachmentNo());
// 실제 첨부파일 삭제
// 1) 첨부파일명 반환
String filename = String.valueOf(attachmentDto.getAttachmentName());
// 2) 하위 경로 설정
File target = new File(directory, filename);
// 3) 첨부파일 삭제
target.delete();
}
}
return result;
}
}
BoardController
@Controller
@RequestMapping("/board")
public class BoardController {
// 의존성 주입
@Autowired
private BoardService boardService;
// 첨부 파일의 상위 경로의 문자열을 추상 경로로 변환하여 File 클래스의 인스턴스 생성(의존성 주입)
private final File directory = new File("C:\\Users\\hyeul\\upload");
// 의존성 주입 후 실행되어야 하는 메소드
@PostConstruct
public void prepare() {
directory.mkdirs(); // 해당 추상 경로의 디렉토리 생성 (폴더가 없으면 자동으로 생성)
}
// 게시글 삭제
// - 게시글 삭제 Mapping
@GetMapping("/delete")
public String delete(@RequestParam int boardNo) {
// 게시글 삭제 (@Service 적용)
boolean result = boardService.remove(boardNo);
// 게시글 삭제 성공 여부
if(result) {
// 게시글 삭제에 성공했다면 게시글 목록 Mapping으로 강제 이동(redirect)
return "redirect:list";
}
// 게시글 삭제에 실패했다면 설정한 예외 발생
else {
throw new TargetNotFoundException();
}
}
}
'국비교육 > 국비교육' 카테고리의 다른 글
day70 - 1107 (0) | 2022.11.07 |
---|---|
day45 - 0928 (0) | 2022.09.28 |
day43 - 0926 (0) | 2022.09.26 |
day42 - 0923 (0) | 2022.09.24 |
day41 - 0922 (0) | 2022.09.22 |