GDJ 24/06/05 (Spring MVC, File Upload)

kimuki·2024년 6월 5일

Spring MVC + File Upload

BoardController.java (Controller)

package com.gd.article.controller;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
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.RequestParam;

import com.gd.article.dto.BoardArticle;
import com.gd.article.dto.BoardRequest;
import com.gd.article.service.BoardService;
import com.gd.article.util.TeamColor;

import lombok.extern.slf4j.Slf4j;

@Slf4j
@Controller
public class BoardController {
	
	@Autowired
	BoardService boardService;

	@GetMapping("/addBoard")
	public String addBoard() {
		return "addBoard";
	}
	
	@PostMapping("/addBoard")
	/*
	-- 커맨드 객체를 사용하지 않고 코드를 풀어 쓴다면 --
	@PostMapping("/addBoard")
	public String addBoard(
		@RequestParam(name="boardTitle") String boardTitle,
		@RequestParam(name="boardContent") String boardContent,
		MultipartFile boardFile
	)	
		>> File 업로드 변수인 MultipartFile의 경우
		@RequestParam(name="")을 사용하지 않는 것을 확인할 수 있음
		그럼 MultipartFile 타입의 경우 @RequestParam을 사용할 수 없는 것인가?
		하면 그것도 아닌거 같음
	 */
	public String addBoard(BoardRequest br) {
		
		log.debug(TeamColor.YELLOW + "boardTitle : " + br.getBoardTitle());
		log.debug(TeamColor.YELLOW + "boardContent : " + br.getBoardContent());
		log.debug(TeamColor.YELLOW + "boardFile.originalName : " + br.getBoardFile().getOriginalFilename());
		
		boardService.addBoard(br);
		
		return "redirect:/boardList";
	}
	
	@GetMapping("/boardList")
	public String boardList(Model model,
							@RequestParam(name="currentPage", defaultValue = "1") int currentPage,
							@RequestParam(name="rowPerPage", defaultValue = "5") int rowPerPage) {
		
		List<String> list = boardService.selectBoardList(currentPage, rowPerPage);
		
		int lastPage = boardService.getBoardCount() / rowPerPage;
		if(lastPage % rowPerPage != 0) {
			lastPage++;
		}
		
		model.addAttribute("list", list);
		model.addAttribute("currentPage", currentPage);
		model.addAttribute("rowPerPage", rowPerPage);
		model.addAttribute("lastPage", lastPage);
		
		return "boardList";
	}
	
	@GetMapping("/deleteBoard")
	public String deleteBoard(int articleNo) {
		
		boardService.deleteBoard(articleNo);
		
		return "redirect:/boardList";
	}
}

BoardService.java (Service)

package com.gd.article.service;

import java.io.File;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;

import com.gd.article.dto.BoardArticle;
import com.gd.article.dto.BoardFile;
import com.gd.article.dto.BoardRequest;
import com.gd.article.mapper.BoardArticleMapper;
import com.gd.article.mapper.BoardFileMapper;
import com.gd.article.util.TeamColor;

import lombok.extern.slf4j.Slf4j;

@Service
@Slf4j
@Transactional
public class BoardService {

	@Autowired
	BoardArticleMapper boardArticleMapper;
	
	@Autowired
	BoardFileMapper boardFileMapper;
	
	public void addBoard(BoardRequest br) {
		
		// boardArticle 등록
		BoardArticle ba = new BoardArticle();
		ba.setArticleTitle(br.getBoardTitle());
		ba.setArticleContent(br.getBoardContent());
		
		int addArticleRow = boardArticleMapper.insertBoardArticle(ba);
		log.debug(TeamColor.YELLOW + "after insert articleNo : " + ba.getArticleNo());
		
		// article 등록에 실패 했을경우 트랜잭션
		if(addArticleRow != 1) {
			throw new RuntimeException();
		}
		
		// boardFile 등록
		MultipartFile mf = br.getBoardFile();
		BoardFile bf = new BoardFile();
		bf.setArticleNo(ba.getArticleNo());
		bf.setOriginalName(br.getBoardFile().getOriginalFilename());
		bf.setFileType(mf.getContentType());
		bf.setFileSize(mf.getSize());
		
		// 저장될 파일 이름은 UUID 사용
		String prefix = UUID.randomUUID().toString().replace("-", "");
		
		int p = mf.getOriginalFilename().lastIndexOf(".");
		String suffix = mf.getOriginalFilename().substring(p);
		
		bf.setFileName(prefix + suffix);
		
		// Mapper 호출
		int addFileRow = boardFileMapper.insertBoardFile(bf);
		
		// file 등록에 실패했을 경우 트랜잭션
		if(addFileRow != 1) {
			throw new RuntimeException();
		}
		
		// 파일 저장
		// MultipartFile mf 의 파일(스트림)을 비어있는 emptyFile로 복사 
		File emptyFile = new File("C:\\upload\\" + prefix + suffix);
		try {
			mf.transferTo(emptyFile);
		} catch (Exception e) {
			e.printStackTrace();
			// 위 코드에서 예외가 발생할 경우 프로그램이 죽어버림
			// try catch 를 사용하지 않지만 에러(예외)가 발생한 경우 transactional 을 발동 시키기 위해 예외 발생시키기
			throw new RuntimeException();	
		}
	}
	
	public List<String> selectBoardList(int currentPage, int rowPerPage) {
		
		int startRow = (currentPage - 1) * rowPerPage;
		
		Map<String, Integer> map = new HashMap<>();
		map.put("startRow", startRow);
		map.put("rowPerPage", rowPerPage);
		
		List<String> list = boardArticleMapper.selectBoardList(map);
		log.debug("list : " + list.toString());
		
		return list;
	}
	
	public int getBoardCount() {
		
		int boardCount = boardArticleMapper.getBoardCount();
		
		return boardCount;
	}
	
	public void deleteBoard(int articleNo) {
		
		int deleteArticleRow = boardArticleMapper.deleteBoardArticle(articleNo);
		log.debug("deleteArticleRow : " + deleteArticleRow);
		if(deleteArticleRow != 1) {
			throw new RuntimeException();
		}
		
		int deleteFileRow = boardFileMapper.deleteBoardFile(articleNo);
		log.debug("deleteFileRow : " + deleteFileRow);
		if(deleteFileRow != 1) {
			throw new RuntimeException();
		}
	}
}

BoardArticle.java (DTO)

package com.gd.article.dto;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

// 커맨드 객체 생성
@Data
@AllArgsConstructor
@NoArgsConstructor
public class BoardArticle {

	private int articleNo;
	private String articleTitle;
	private String articleContent;
	private String updateDate;
	private String createDate;
	
	// BoardFile 의 이미지를 가져오기 위해 추가
	private String fileName;
}

BoardFile.java (DTO)

package com.gd.article.dto;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class BoardFile {
	
	private int fileNo;
	private int articleNo;
	private String fileName;
	private String originalName;
	private String fileType;
	private long fileSize;
	private String updateDate;
	private String createDate;
}

BoardRequest.java (DTO - Insert Transaction)

package com.gd.article.dto;

import org.springframework.web.multipart.MultipartFile;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class BoardRequest {

	private String boardTitle;
	private String boardContent;
	private MultipartFile boardFile;
}

BoardArticleMapper.java (Mapper)

package com.gd.article.mapper;

import java.util.List;
import java.util.Map;

import org.apache.ibatis.annotations.Mapper;

import com.gd.article.dto.BoardArticle;

@Mapper
public interface BoardArticleMapper {
	
	int insertBoardArticle(BoardArticle ba);
	
	List<String> selectBoardList(Map<String, Integer> map);
	
	int getBoardCount();
	
	int deleteBoardArticle(int articleNo);
}

BoardArticleMapper.xml (Mapper.xml)

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.gd.article.mapper.BoardArticleMapper">
	<insert id="insertBoardArticle" parameterType="com.gd.article.dto.BoardArticle">
		<selectKey resultType="int" keyProperty="articleNo" order="AFTER" >
			SELECT LAST_INSERT_ID()
		</selectKey>
	
		INSERT INTO board_article (
			article_title,
			article_content,
			update_date,
			create_date
		) VALUES (
			#{articleTitle},
			#{articleContent},
			NOW(),
			NOW()
		)
	</insert>
	
	<select id="selectBoardList" resultType="com.gd.article.dto.BoardArticle" parameterType="Map">
		SELECT
			a.article_no articleNo,
			a.article_title articleTitle,
			a.article_content articleContent,
			b.file_name fileName,
			a.update_date updateDate,
			a.create_date createDate
		FROM
			board_article a
		INNER JOIN
			board_file b
		ON
			a.article_no = b.article_no
		LIMIT
			#{startRow}, #{rowPerPage}
	</select>
	
	<delete id="deleteBoardArticle" parameterType="int">
		DELETE FROM
			board_article
		WHERE
			article_no = #{articleNo}
	</delete>
	
	<select id="getBoardCount" resultType="int">
		SELECT
			COUNT(*)
		FROM
			board_article
	</select>
</mapper>

BoardFileMapper.java (Mapper)

package com.gd.article.mapper;

import org.apache.ibatis.annotations.Mapper;

import com.gd.article.dto.BoardFile;

@Mapper
public interface BoardFileMapper {
	
	int insertBoardFile(BoardFile bf);
	
	int deleteBoardFile(int articleNo);
}

BoardFileMapper.xml (Mapper.xml)

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.gd.article.mapper.BoardFileMapper">
	<insert id="insertBoardFile" parameterType="com.gd.article.dto.BoardFile">
		INSERT INTO board_file (
			article_no,
			file_name,
			original_name,
			file_type,
			file_size,
			update_date,
			create_date
		) VALUES (
			#{articleNo},
			#{fileName},
			#{originalName},
			#{fileType},
			#{fileSize},
			NOW(),
			NOW()
		)
	</insert>
	
	<delete id="deleteBoardFile" parameterType="int">
		DELETE FROM
			board_file
		WHERE
			article_no = #{articleNo}
	</delete>
</mapper>

boardList.jsp (View)

<%@ 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"%>
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions"%>
<!DOCTYPE html>
<html>
<head>
	<meta charset="UTF-8">
	<title>Board List</title>
	<style>
		td{
			text-align: center;
			vertical-align: middle;
		}
		
		h1{
			text-align: center;
			padding-top: 30px;	
		}
		
		.btn{text-align: center;}
	</style>
	<!-- Latest compiled and minified CSS -->
	<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
	
	<!-- Latest compiled JavaScript -->
	<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
</head>
<body>
	<div class="container">
		<h1>Board List</h1>
		<br>
		<table class="table table-bordered table-striped">
			<tr>
				<td>Article No</td>
				<td>Article Title</td>
				<td>Article Content</td>
				<td>Article Image</td>
				<td>Update Date</td>
				<td>Create Date</td>
				<td>&nbsp;</td>
			</tr>
			<c:forEach var="a" items="${list}">
				<tr>
					<td>${a.articleNo}</td>
					<td>${a.articleTitle}</td>
					<td>${a.articleContent}</td>
					<td><img src="/article/img/${a.fileName}" width="100px" height="100px"></td>
					<td>${a.updateDate}</td>
					<td>${a.createDate}</td>
					<td><a class="btn btn-danger"href="${pageContext.request.contextPath}/deleteBoard?articleNo=${a.articleNo}">삭제하기</a></td>
				</tr>
			</c:forEach>
		</table>
		<br>
		<a class="btn btn-primary" href="${pageContext.request.contextPath}/addBoard">등록하기</a>
		<c:if test="${currentPage > 1}">
			<a class="btn btn-dark" href="${pageContext.request.contextPath}/boardList?currentPage=${currentPage-1}&rowPerPage=${rowPerPage}">이전</a>
		</c:if>
		
		<c:if test="${currentPage < lastPage}">
			<a class="btn btn-dark" href="${pageContext.request.contextPath}/boardList?currentPage=${currentPage+1}&rowPerPage=${rowPerPage}">다음</a>
		</c:if>
	</div>
</body>
</html>

boardList.jsp 출력결과

profile
Road OF Developer

0개의 댓글