본문 바로가기

국비교육/국비교육

day44 - 0927

게시판 첨부파일 업로드 및 다운로드

 

테이블 생성

게시판 첨부파일(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