Back-End/Spring

[Spring] 예제 2 - 파일 업로드

09009 2023. 5. 23. 15:48

DB에서 데이터는 날짜, 숫자, 문자만 저장할 수 있다. 이미지 자체는 DB에 저장할 수 없다.

 

이미지는 WAS에 연결된 HDD에 저장한다.

파일 자체는 WAS 하드디스크에 저장, 파일 연결고리는 DB에 넣는다

 

글쓰기에서 파일 업로드 구현

writeContentPage.jsp

 

만약 파일을 업로드 해야하는 경우 form 태그에 enctype="multipart/form-data"을 추가해주어야 한다.

 

 

클라이언트 처리

writeContentPage.jsp

multiple="multiple" : 여러 개를 선택할 수 있다.

accept="image/*" : default를 이미지 파일로 설정함.

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<h1>게시글 쓰기</h1>

<!-- 만약 파일을 보내야되는 경우 - enctype="multipart/form-data" 로 꼭 변경해야 한다. 무조건 post 방식-->

<form action="./writeContentProcess" method="post" enctype="multipart/form-data">
	작성자 : ${sessionUser.nickname} <br>
	제목 : <input type="text" name="title"><br>
	내용 : <br>
	<textarea rows="10" cols="60" name="content"></textarea>
	<br>
	<input name="boardFiles" type="file" multiple="multiple" accept="image/*">
	<br>

	<button>등록</button>
</form>

</body>
</html>

파일 업로드 작업을 수행하기 위해서는 해당 외부 라이브러리를 pom.xml에 추가하여야 한다.

 

- 외부 라이브러리 pom.xml에 추가

<!-- 업로드용 파일 파서... -->
		<dependency>
			<groupId>commons-fileupload</groupId>
			<artifactId>commons-fileupload</artifactId>
			<version>1.3.3</version>
		</dependency>

여러 라이브러리 추가

	<!-- Mail 관련 라이브러리 -->
		<dependency>
		    <groupId>org.springframework</groupId>
		    <artifactId>spring-context-support</artifactId>
		    <version>5.0.7.RELEASE</version>
		</dependency>
		
		<dependency>
			<groupId>javax.mail</groupId>
			<artifactId>mail</artifactId>
			<version>1.4.7</version>
		</dependency>
		
		<!-- 업로드용 파일 파서... -->
		<dependency>
			<groupId>commons-fileupload</groupId>
			<artifactId>commons-fileupload</artifactId>
			<version>1.3.3</version>
		</dependency>		

		<!-- json 변환기 -->
		<dependency>
			<groupId>com.fasterxml.jackson.core</groupId>
			<artifactId>jackson-databind</artifactId>
			<version>2.9.5</version>
		</dependency>

파알 업로드를 위한 servlet-context.xml에 bean 등록

<!-- ja : 파일 업로드 관련.. 빈 등록.. -->
	<beans:bean id="multipartResolver" 
	class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
		<beans:property name="maxUploadSize" value="100000000"></beans:property>
	</beans:bean>

 

- 참고로 jsp에서 파일을 한 개도 선택하지 않은 채 submit을 하여도 null값이 날라오지 않는다.

✍ BoardController

테스트 코드

	@RequestMapping("writeContentProcess") // boardFiles : 변수명 jsp에 있는 name과 같아야함
	// 파일이 여러 개 전송될 수 있으므로 배열로 받는다. -> 반복문 적용하여 저장
	public String writeContentProcess(HttpSession session, BoardDto params, MultipartFile [] boardFiles) {
		
		// 파일 저장 로직
		if (boardFiles != null) {
			
			for(MultipartFile file : boardFiles) {
				
				// 이미지가 존재하지 않을 때
				if (file.isEmpty()) {
					continue;
				}
				System.out.println("파일명: " + file.getOriginalFilename());
				
				try {
					// java.io.file 불러오기, 폴더를 포함한 파일명을 넣는다
					file.transferTo(new File("c:\\uploadFiles\\" + file.getOriginalFilename()));
				} catch (Exception e) {
					e.printStackTrace();
				}
				
			}
		}

- 참고: 경로에서 uploadFiles는 위 소스코드 작성 이전에 c드라이브에서 임의로 생성한 폴더이다.

위 소스코드를 확정하고 코드를 실행하면 이미 존재한 파일명과 똑같은 파일명으로 새 파일을 업로드하였을 때

uploadFiles 폴더에는 새 파일이 기존에 동일한 파일명을 가진 파일을 덮어씌우게 된다.

 

그러므로 코드를 아래와 같이 수정해주어야 한다.

	@RequestMapping("writeContentProcess") // boardFiles : 변수명 jsp에 있는 name과 같아야함
	// 파일이 여러 개 전송될 수 있으므로 배열로 받는다. -> 반복문 적용하여 저장
	public String writeContentProcess(HttpSession session, BoardDto params, MultipartFile [] boardFiles) {
		
		// 파일 저장 로직
		if (boardFiles != null) {
			
			for(MultipartFile multipartFile : boardFiles) {
				
				// 이미지가 존재하지 않을 때
				if (multipartFile.isEmpty()) {
					continue;
				}
			
				
				System.out.println("파일명: " + multipartFile.getOriginalFilename());
				
				String rootFolder = "C:/uploadFiles/";
				
				// 날짜별 폴더 생성 로직
				
				SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd"); // 날짜를 문자로 바꿔주는 역할
				String today = sdf.format(new Date()); // new Date(): 오늘 날짜 출력
				
				File targetFolder = new File(rootFolder + today); // C:/uploadFolder/2023/05/23
				
				if(!targetFolder.exists()) {
					 targetFolder.mkdirs(); // 폴더 생성
					
				}
				
				// 저장 파일명 만들기. 핵심은 파일명 충돌 방지 = 랜덤 + 시간
				String fileName = UUID.randomUUID().toString();
				fileName += "_" + System.currentTimeMillis();
				
				// 확장자 추출
				String originalFileName = multipartFile.getOriginalFilename(); // originalFileName : 사용자가 컴퓨터에 올리는 파일명
				String ext =  originalFileName.substring(originalFileName.lastIndexOf(".")); // lastindexof: 뒤에서부터 .의 위치를 찾아서 숫자를 반환
				
				// 슬래시 주의할 것 기억하기
				String saveFileName = rootFolder + today + "/"  + fileName + ext;
				
				
				try {
					// java.io.file 불러오기, 폴더를 포함한 파일명을 넣는다
					// 다른 이미지이지만 파일명이 같은 경우, 충돌을 피하려면
					multipartFile.transferTo(new File(saveFileName));
				} catch (Exception e) {
					e.printStackTrace();
				}
				
			}
		}
		
		

		// 데이터 저장 로직
		 MemberDto sessionUser = (MemberDto) session.getAttribute("sessionUser"); // key로 값을 뽑아온다.
		 
		int memberId = sessionUser.getId(); // 로그인한 회원 아이디 추출
		params.setMember_id(memberId);
		
		boardService.writeContent(params);
		
		return "redirect:./mainPage";
	}

 

업로드 한 이미지를 db와 연결시키는 작업을 해야한다,

 

게시판 이미지 테이블 생성

하나의 게시판에는 여러 개의 이미지를 업로드할 수 있도록 위에서 설계해두었기 때문에

1:N 관계로 설정한다.

파일 업로드 모듈 추가 - Tomcat의 Module창 이동하여 아래와 같이 설정

add external web module 클릭 - 아래 경로로 작성하고 저장

위 작업까지 마치고 아래 링크로 request를 수행한다.

특정 글 번호에 위 링크를 저장해야한다.

2023/05/23/45e7c12e-37aa-4e6b-82b1-8de0f5f44fa3_1684819998645.jpg

위 링크를 복사

 

이미지 테이블 추가

-- 게시판 이미지 T
DROP TABLE fp_board_image;
CREATE TABLE fp_board_image(
  id NUMBER PRIMARY KEY,
  board_id NUMBER,
  original_filename VARCHAR2(400),
  link VARCHAR2(500)
 ); 
 
DROP SEQUENCE fp_board_image_seq;
CREATE SEQUENCE fp_board_image_seq;

DTO 생성

BoardImageDto

package com.ja.finalproject.dto;

public class BoardImageDto {
	
	private int id;
	private int board_id;
	private String original_filename;
	private String link;
	public BoardImageDto() {
		super();
	}
	public BoardImageDto(int id, int board_id, String original_filename, String link) {
		super();
		this.id = id;
		this.board_id = board_id;
		this.original_filename = original_filename;
		this.link = link;
	}
	public int getId() {
		return id;
	}
	public void setId(int id) {
		this.id = id;
	}
	public int getBoard_id() {
		return board_id;
	}
	public void setBoard_id(int board_id) {
		this.board_id = board_id;
	}
	public String getOriginal_filename() {
		return original_filename;
	}
	public void setOriginal_filename(String original_filename) {
		this.original_filename = original_filename;
	}
	public String getLink() {
		return link;
	}
	public void setLink(String link) {
		this.link = link;
	}
	
}

 

이제 글쓰기 버튼을 누르면 두 개의 테이블에 insert 될 것이다.

BoardSqlMapper

코드 추가

public int createPk();

두 테이블에 데이터가 동시에 insert되므로 primary key를 따로 생성하는 로직을 구현한다. (createPk)

BoardSqlMapper.xml

	<select id="createPk" resultType="int">
		SELECT fp_board_seq.nextval FROM dual
	</select>
    
        <insert id="insert">
            INSERT INTO fp_board VALUES(
              #{id},
              #{member_id},
              #{title},
              #{content},
              0, 
              SYSDATE
            )	
        </insert>

 BoardSqlMapper

 

	// 이미지 등록
	public void insertBoardImage(BoardImageDto boardImageDto);

✍ BoardSqlMapper.xml

 

	<insert id="insertBoardImage">
		INSERT INTO fp_board_image VALUES(
			fp_board_image_seq.nextval,
			#{board_id},
			#{original_filename},
			#{link}
		)
	</insert>

BoardController

파일 저장 로직 하나를 돌때마다 boardimagedto를 만들 것이다.

각 dto를  ArrayList에 담아서 service에 넘기도록 구현할 것이다.

반복문 한 바퀴를 돌때마다 파일을 저장한다.

 

writeContent 메서드는 service에서 수정할 것이다.

	@RequestMapping("writeContentProcess") // boardFiles : 변수명 jsp에 있는 name과 같아야함
	// 파일이 여러 개 전송될 수 있으므로 배열로 받는다. -> 반복문 적용하여 저장
	public String writeContentProcess(HttpSession session, BoardDto params, MultipartFile [] boardFiles) {
		
		List<BoardImageDto> boardImageDtoList = new ArrayList<>();
		
		// 파일 저장 로직
		if (boardFiles != null) {
			
			for(MultipartFile multipartFile : boardFiles) {
				
				// 이미지가 존재하지 않을 때
				if (multipartFile.isEmpty()) {
					continue;
				}
			
				
				System.out.println("파일명: " + multipartFile.getOriginalFilename());
				
				String rootFolder = "C:/uploadFiles/";
				
				// 날짜별 폴더 생성 로직
				
				SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd"); // 날짜를 문자로 바꿔주는 역할
				String today = sdf.format(new Date()); // new Date(): 오늘 날짜 출력
				
				File targetFolder = new File(rootFolder + today); // C:/uploadFolder/2023/05/23
				
				if(!targetFolder.exists()) {
					 targetFolder.mkdirs(); // 폴더 생성
					
				}
				
				// 저장 파일명 만들기. 핵심은 파일명 충돌 방지 = 랜덤 + 시간
				String fileName = UUID.randomUUID().toString();
				fileName += "_" + System.currentTimeMillis();
				
				// 확장자 추출
				String originalFileName = multipartFile.getOriginalFilename(); // originalFileName : 사용자가 컴퓨터에 올리는 파일명
				String ext =  originalFileName.substring(originalFileName.lastIndexOf(".")); // lastindexof: 뒤에서부터 .의 위치를 찾아서 숫자를 반환
				
				// 슬래시 주의할 것 기억하기
				String saveFileName = today + "/"  + fileName + ext;
				
				
				try {
					// java.io.file 불러오기, 폴더를 포함한 파일명을 넣는다
					// 다른 이미지이지만 파일명이 같은 경우, 충돌을 피하려면
					multipartFile.transferTo(new File(rootFolder + saveFileName));
				} catch (Exception e) {
					e.printStackTrace();
				}
				
				BoardImageDto boardImageDto = new BoardImageDto();
				boardImageDto.setOriginal_filename(originalFileName);
				boardImageDto.setLink(saveFileName);
				
				boardImageDtoList.add(boardImageDto);
			}
		}
		
		

		// 데이터 저장 로직
		 MemberDto sessionUser = (MemberDto) session.getAttribute("sessionUser"); // key로 값을 뽑아온다.
		 
		int memberId = sessionUser.getId(); // 로그인한 회원 아이디 추출
		params.setMember_id(memberId);
		
		boardService.writeContent(params, boardImageDtoList);
		
		return "redirect:./mainPage";
	}

boardid는 set을 못한다.

글쓰기 작업을 수행하고 있을 때에는 게시글이 insert되기 전이므로 게시글 번호가 지정되지 않은 상태이기 때문이다.

service에서 boardid를 지정해줄 것이다.

 

 

BoardServiceImpl

	public void writeContent(BoardDto boardDto, List<BoardImageDto> boardImageDtoList) {
			int boardid = boardSqlMapper.createPk();

			boardDto.setId(boardid);
			boardSqlMapper.insert(boardDto);	
			
			for(BoardImageDto boardImageDto : boardImageDtoList) {
				boardImageDto.setBoard_id(boardid);
				boardSqlMapper.insertBoardImage(boardImageDto);
			}
		}

쿼리 테스트

 

게시글 상세보기할때도 이미지를 출력해야한다.

 

쿼리 작성

BoardSqlMapper

외래키로 SELECT → List로 출력

	public List<BoardImageDto> selectBoardImageByBoardId(int board_id);

✍ BoardSqlMapper.xml

 

	<select id="selectBoardImageByBoardId" resultType="com.ja.finalproject.dto.BoardImageDto">
		SELECT * FROM fp_board_image
		WHERE board_id = #{board_id}
	</select>

✍ BoardServiceImpl

 

		// List일 필요없고 조회할 개시글은 한 개이므로 hashmap
		public  Map<String, Object> getBoard(int id) {
			
			Map<String, Object>  map = new HashMap<>();
			
			BoardDto boardDto = boardSqlMapper.selectById(id);
			MemberDto memberDto =  memberSqlMapper.selectById(boardDto.getMember_id());
			
			List<BoardImageDto> boardImageDtoList = boardSqlMapper.selectBoardImageByBoardId(id); // id = boardid
			
			map.put("boardDto", boardDto);
			map.put("memberDto", memberDto);
			map.put("boardImageDtoList", boardImageDtoList);
			
			return map;
		}

readContentPage.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ taglib uri = "http://java.sun.com/jsp/jstl/core" prefix="c" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<h1>게시글 상세 보기</h1>

제목 : ${data.boardDto.title} <br>
작성자 : ${data.memberDto.nickname} <br>
작성일 : ${data.boardDto.reg_date} <br>
조회수 : ${data.boardDto.read_count} <br>


<br>
<c:forEach items="${data.boardImageDtoList}" var="boardImageDto">
<%-- 절대 경로 --%>
	<img src="/uploadFiles/${boardImageDto.link}"> <br>
</c:forEach>


내용 : <br>
${data.boardDto.content} 

<br><br>
<a href="./mainPage">목록으로</a>

<c:if test="${!empty sessionUser && sessionUser.id == data.memberDto.id}">
	<a href="./updatePage?id=${data.boardDto.id}">수정</a>
	<a href="./deleteProcess?id=${data.boardDto.id}">삭제</a>
</c:if>

</body>
</html>

이미지 등록 결과