본문 바로가기

반응형
검색 > 페이징 > Ajax > 파일 업/다운로드

 

1. 파일 업로드를 위해 DB 설계 (이미지 파일)

create table board_img(
    file_seq number not null primary key,	-- PK명
    real_name varchar(500) not null,		-- 원래 파일명
    save_name varchar(500) not null,		-- 서버에 저장될 파일명
    reg_date date not null,			-- 업로드 날짜
    save_path varchar(500) not null,		-- 서버에 저장될 경로
    
    board_seq number not null,			-- FK명
    constraint board_file foreign key(board_seq) references board (board_seq)
);

 

DB 구성

 

2. 사전 설정

<!-- pom.xml -->
    <!-- 서블릿 3.0이상 사용 가능한 파일 업로드 api-->
    <dependency>
        <groupId>commons-fileupload</groupId>
        <artifactId>commons-fileupload</artifactId>
        <version>1.3.1</version>
    </dependency>

    <!-- commons-io -->
    <dependency>
        <groupId>commons-io</groupId>
        <artifactId>commons-io</artifactId>
        <version>2.4</version>
    </dependency>

<!-- Servlet-context.xml -->
    <!-- 파일 업로드 -->
    <beans:bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
        <beans:property name="maxUploadSize" value="10485760" />
        <beans:property name="defaultEncoding" value="utf-8" />
    </beans:bean>

 

3. ImgMapper 생성 및 작성

<?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="ImgMapper">
	
	<resultMap id='imgMap' type='java.util.HashMap'>
		<result column='FILE_SEQ' property='fileSeq' />
		<result column='REAL_NAME' property='realName' />
		<result column='SAVE_NAME' property='saveName' />
		<result column='REG_DATE' property='regDate' />
		<result column='SAVE_PATH' property='savePath' />
		
		<result column='BOARD_SEQ' property='boardSeq' />
	</resultMap>
	
	<insert id='uploadImg'>
		INSERT INTO BOARD_IMG(
			FILE_SEQ,
			REAL_NAME,
			SAVE_NAME,
			REG_DATE,
			SAVE_PATH,
			BOARD_SEQ
		) VALUES (
			(SELECT NVL(MAX(FILE_SEQ),0)+1 FROM BOARD_IMG),
			#{realName},
			#{saveName},
			sysdate,
			#{savePath},
			#{boardSeq}
		)
	</insert>
	
</mapper>

 

3. BoardMapper 작성

    <select id='getMaxSeq' resultType='int'>
    	select
    	 nvl(max(board_seq), 0) + 1
		from board
    </select>

이미지 정보를 넣어줄 때 게시글 번호를 같이 업로드를 해줘야 하니 게시글 번호를 반환해주는 쿼리를 작성해 줍니다.

4. Img Dao, DaoImpl 작성

// Img Dao
	void uploadImg(List<Map<String, Object>> imgList);
    
// Img DaoImpl
	@Autowired
	private SqlSessionTemplate sqlSession;
	
	@Override
	public void uploadImg(List<Map<String, Object>> imgList) {
		for(Map<String, Object> imgMap : imgList) {
			sqlSession.insert("ImgMapper.uploadImg", imgMap);
		}
	}

 

5. Img Service, ServiceImpl 작성

// Img Service
	void uploadImg(List<MultipartFile> imgs, int boardSeq);

// Img ServiceImpl
@Service
public class ImgServiceImpl implements ImgService{
	
	@Autowired
	private ImgDao imgDao;
	
	@Override
	public void uploadImg(List<MultipartFile> imgs, int boardSeq) {
		List<Map<String, Object>> imgList = new ArrayList<Map<String,Object>>();
		
		for(MultipartFile file : imgs) {
			String ariginalFileName = file.getOriginalFilename();
			String extension = ariginalFileName.substring(ariginalFileName.lastIndexOf("."));
			String savedFileName = UUID.randomUUID().toString()+extension;
			
			Map<String, Object> imgMap = new HashMap<String, Object>();
			imgMap.put("saveName", savedFileName);
			imgMap.put("realName", ariginalFileName);
			imgMap.put("boardSeq", boardSeq);
			
			FileUpload fileUpload = new FileUpload();
			fileUpload.fileUpload(file, savedFileName);
			
			imgMap.put("savePath", fileUpload.getUploadPath());
			
			imgList.add(imgMap);
		}
		
		imgDao.uploadImg(imgList);
	}
}

이미지 정보들을 서버에 저장할 이름으로 가공해서 DB에 insert 해줍니다. 

6. Board Dao, DaoImpl 작성

// Board Dao
	// 이전 코드
	int getMaxSeq();

// Board DaoImpl
	// 이전 코드
	@Override
	public int getMaxSeq() {
		return sqlsession.selectOne("BoardMapper.getMaxSeq");
	}

 

7. Board ServiceImpl 수정

/*
	이전 코드
*/

	@Autowired
	private ImgService imgSer;

/*
	중간 코드
*/

	@Override
	public void createBoard(Map<String, Object> param) {
		Map<String, Object> boardMap = new HashMap<String, Object>();
		
		boardMap.put("writer", param.get("writer"));
		boardMap.put("title", param.get("title"));
		boardMap.put("content", param.get("content"));
		
		int boardSeq = boardDao.getMaxSeq();
		
		boardDao.createBoard(param);
		
		@SuppressWarnings("unchecked")
		List<MultipartFile> imgFiles = (List<MultipartFile>) param.get("imgs");
		imgSer.uploadImg(imgFiles, boardSeq);
	}

 게시글을 업로드 하기 전에 올라갈 게시글 번호를 저장한 후에 업로드를 해줍니다. 그러고나서 param에 담긴 이미지를 가공 후 업로드 할 수 있게 해줍니다.

8. Controller 수정

	@RequestMapping(value="/create", method = RequestMethod.POST)
	public String createPost(@RequestParam Map<String, Object> param, @RequestPart("imgs") List<MultipartFile> imgs) {
		param.put("imgs", imgs);
		boardSer.createBoard(param);
		
		return "redirect:/list";
	}

View로부터 전달받은 이미지 목록들(imgs)을 같이 받은 게시글 정보들(param)에 담아서 7번의 과정을 수행할 수 있도록 해줍니다.

9. create.jsp 수정

<form enctype="multipart/form-data" id='createForm'>
    <table>
        <tr>
            <th>작성자</th>
            <td><input type='text' id='writer' name='writer'></td>
        </tr>
        <tr>
            <th>글 제목</th>
            <td><input type='text' id='title' name='title'></td>
        </tr>
        <tr>
            <th rowspan="2">글 내용</th>
            <td rowspan="2"><textarea id='content' name='content'></textarea></td>
        </tr>
    </table>
    <br>
    <button id='plsImg'>이미지 등록</button>
    <div id='imgPlace'></div>
    <br>
    <button id='createBtn'>글쓰기</button> 
    <button onclick='goToPreviousPage(event)'>이전</button>
</form>

<form> 태그 안에 이미지를 업로드 할 수 있도록 enctype="multipart/form-data"를 추가해주도록 합니다. 그리고 이미지를 한 개 씩 여러 개 올릴 수 있도록 버튼과 영역을 생성해 줍니다.

이해를 돕기 위한 사진

10. JQeury 작성

$(function(){
    $('#createBtn').on('click',function(event){
        event.preventDefault();

        var chkWriter = $('#writer').val();
        var chkTitle = $('#title').val();

        if(chkTitle == '' || chkWriter == ''){
            alert('작성하지 않은 내용이 있습니다.');
        } else {
            var files = $("[name=imgs]")[0].files;
            var checks = [];

            for (var i = 0; i < files.length ; i++){
                checks.push(checkImg(files[i]));
            }

            Promise.all(checks).then(function(results) {
                var exChk = results.reduce((sum, curr) => sum + curr, 0);

                if (exChk === 0) {

                    $('#createForm').attr({
                            'action' : '/create',
                            'method' : 'post'
                        }).submit();
                } else {
                    event.preventDefault();
                    alert("이미지의 크기가 너무 큽니다. 가로세로 500px을 넘기지 마세요.");
                }
            });
        }
    });

    $("#plsImg").on("click",function(event){
        event.preventDefault();
        var putImg = '<input type="file" id="imgBtn" name="imgs" accept="image/*" value="파일 찾기"> <button id="delImgPut">-</button><br>';

        $("#imgPlace").append(putImg);
    });

    $("#imgPlace").on("click", "#delImgPut", function(event){
        event.preventDefault();
        $(this).prev().remove(); // remove the input[type=file]
        $(this).next().remove(); // remove the <br>
        $(this).remove(); // remove the input[type=button]
    });
})

function goToPreviousPage(event) {
    event.preventDefault();

    window.history.back();
}

function checkImg(file) {
    return new Promise(function(resolve, reject) {
        var _URL = window.URL || window.webkitURL;
        var img = new Image();

        img.onload = function() {
            if (this.width > 500 || this.height > 500) {
                resolve(1);
            } else {
                resolve(0);
            }
        };

        img.onerror = function() {
            reject(new Error("Image Load Error"));
        };

        img.src = _URL.createObjectURL(file);
    });
}

일단 이미지 등록 버튼을 눌렀을 때, 이미지 업로드 구역 + 영역 지우기 버튼이 생기도록 해줍니다. ($("#plsImg").on("click",function(event){ ... });)
영역 지우기 버튼을 누르면 그 앞의 태그, 본인, 다음 태그까지 지우도록 해줍니다. ($("#imgPlace").on("click", "#delImgPut", function(event){ ... });)
업로드를 할 때, 이미지의 크기를 체크하는 부분을 추가해주도록 합니다. (성공 시 업로드, 실패 시 alert) 이 부분이 이번 프로젝트에서 제일 어려웠습니다..

11. FileUpload 작성

package com.JoAri.CRUD.img;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;

import org.springframework.web.multipart.MultipartFile;

public class FileUpload {

    private static final String UPLOAD_PATH = "C:\\Work\\temporary\\File_Practice\\"; // 업로드할 파일 경로

    public void fileUpload(MultipartFile file, String savedFileName) {
        if (!file.isEmpty()) {
            try {
                Path uploadPath = new File(UPLOAD_PATH + savedFileName).toPath();
                Files.copy(file.getInputStream(), uploadPath, StandardCopyOption.REPLACE_EXISTING);
            } catch (IOException e) {
                // 예외 처리
            }
        }
    }

    public String getUploadPath() {
        return UPLOAD_PATH;
    }
}

실질적으로 이미지가 저장될 공간과 어떤 형태(위에선 경로+저장될 파일명)로 저장될 지 예외 시엔 어떻게 할 지 지정해 주는 클래스를 만들어 줍니다. (Img Service 에서 사용)

12. 결과

업로드
저장된 형태

후기


휴...  이 번엔 글에서 보기와는 다르게 여기저기 왔다갔다 하며 코딩하고 고민도 하느라 시간이 조금 오래 걸렸습니다.

이번엔 개인적으로 의문점이 생기는 시간이었습니다. 게시글을 올리고 이미지를 올렸는데 이미지 업로드가 실패하면? 게시글을 보는 사람은 이미지를 제대로 볼 수가 없지 않나? 싶었습니다. 그러면 이미지부터 올리면 되는데 그러면 DataBase에서의 FK 때문에 위배되기 때문에 그 또한 안 되지 않나 싶었습니다.
보통은 어떻게 해결하나요??

다음 시간에 뵙겠습니다. 빠잇~!
반응형

'Spring > CRUD Project' 카테고리의 다른 글

[CRUD] 파일 다운로드  (1) 2023.12.06
[CRUD] 파일 업로드 및 조회 2 : 수정 및 출력  (1) 2023.12.05
[CRUD] Ajax : 비동기 통신  (1) 2023.11.30
[CRUD] 페이징  (0) 2023.11.29
[CRUD] 검색  (1) 2023.11.28
댓글