Kotlin

첨부파일 처리

purecho 2021. 5. 2. 15:46

첨부파일 등록, 수정, 삭제에 대한 처리

 

 

※ 개발 스펙

  • 개발도구 : intelliJ,
  • 빌드도구 : gradle,
  • 서버언어 : kotlin,
  • 프론트언어 : Thymeleaf,
  • SQL : MySql ( + MyBatis )

 

 

 

 

FileInfoModel

data class FileInfoModel (

    var f_order: String? = null,       // 파일 순번
    var b_order: String? = null,       // 게시판 순번
    var f_name: String? = null,        // 파일 이름
    var f_path: String? = null         // 파일 경로

)

 

 

BoardMapper.xml

    <!-- 첨부파일 목록 -->
    <select id="fileList"   parameterType="String" resultType="kr.or.korea.model.FileInfoModel">
        SELECT f_order,    -- 파일 순번
                b_order,   -- 게시판 순번
                f_name,    -- 파일 이름
                f_path     -- 파일 경로
        FROM file_list
        WHERE b_order = #{b_order}
        <if test="f_order != null and f_order != ''"><!-- 파일 순번 -->
            AND f_order = #{f_order}
        </if>
    </select>



    <!-- 첨부파일 등록 -->
    <insert id="fileInsert"    parameterType="kr.or.korea.model.FileInfoModel">
        INSERT INTO file_list (
            f_order,
            b_order,
            f_name,
            f_path
        ) VALUES (
            (
                SELECT IFNULL(MAX(f_order)+1, 1)
                FROM file_list fl
                WHERE b_order = #{b_order}
            ),
            #{b_order},
            if(instr(#{f_name}, '\\') = '0', #{f_name}, SUBSTRING_INDEX(#{f_name}, '\\', -1)),
            #{f_path}
        )
    </insert>



    <!-- 첨부파일 삭제 -->
    <delete id="fileDelete"   parameterType="String">
        DELETE
        FROM file_list
        WHERE b_order = #{b_order}
        AND f_order = #{f_order}
    </delete>

 

 

 

BoardMapper.kt (인터페이스)

/**
 * 첨부파일 목록
 * */
@Throws(Exception::class)
fun fileList(b_order: String, f_order: String) : List<FileInfoModel>


/**
 * 첨부파일 등록
 * */
@Throws(Exception::class)
fun fileInsert(param: FileInfoModel)


/**
 * 첨부파일 삭제
 * */
@Throws(Exception::class)
fun fileDelete(b_order: String, f_order: String)

 

 

 

BoardService.kt

@Service
class BoardService {


    @Autowired
    private lateinit var mapper : BoardMapper

    @Autowired
    private lateinit var session : HttpSession

    @Autowired
    lateinit var env : Environment




    /**
     * 첨부파일 목록
     * @throws Exception
     */
    @Throws(Exception::class)
    fun fileList(b_order: String, f_order: String) : List<FileInfoModel> {
        return mapper.fileList(b_order, f_order)
    }


    /**
     * 첨부파일 등록
     * @throws Exception
     */
    @Throws(Exception::class)
    @Transactional(rollbackFor = [(Exception::class)])
    fun fileInsert(b_order: String, files: List<MultipartFile>) {
        
        var fileData = FileInfoModel()
        fileData.b_order = b_order

        // 파일 저장 위치
        val getYear = LocalDateTime.now().year
        val fileUploadDir: String = env.getProperty("file.upload.dir") + getYear + File.separator
        if (File(fileUploadDir).exists().not()) {
            File(fileUploadDir).mkdirs()
        }

        // 파일 업로드
        for (item in files) {
            val fileUploadName : String = UUID.randomUUID().toString()
            val fileUploadPath : String = fileUploadDir + fileUploadName
            val originName : String? = item.originalFilename

            fileData.f_name = originName
            fileData.f_path = fileUploadPath
            item.transferTo(File(fileUploadPath))

            mapper.fileInsert(fileData)
        }
    }


    /**
     * 첨부파일 삭제
     * @throws Exception
     */
    @Throws(Exception::class)
    @Transactional(rollbackFor = [(Exception::class)])
    fun fileDelete(b_order: String, f_order: String) {
        val finfo = this.fileList(b_order, f_order)
        if(finfo != null && Files.exists(Paths.get(finfo[0].f_path))){
            // 실제 서버에 저장된 파일 삭제
            Files.delete(Paths.get(finfo[0].f_path))
            // 데이터베이스에서 삭제
            mapper.fileDelete(b_order, f_order)
        }
    }






    /**
     * 게시글 등록
     * @throws Exception
     */
    @Throws(Exception::class)
    @Transactional(rollbackFor = [(Exception::class)])
    fun boardInsert(param: BoardModel, files: List<MultipartFile>?): JSONObject {
        
        // 게시글 등록
        param.id = session.getAttribute("id").toString()
        mapper.boardInsert(param)

        // 첨부파일 등록
        if (files != null) {
            this.fileInsert(param.b_order!!, files)
        }

        return JSONObject()
    }


    /**
     * 게시글 수정
     * @throws Exception
     */
    @Throws(Exception::class)
    @Transactional(rollbackFor = [(Exception::class)])
    fun boardModify(param: BoardModel, files: List<MultipartFile>?, f_order: String): JSONObject {
        
        // 게시글 수정
        mapper.boardModify(param)

        // 첨부파일 삭제
        if (f_order != "") {
            val f_order = f_order.split(",")
            for (item in f_order) {
                this.fileDelete(param.b_order!!, item)
            }
        }

        // 첨부파일 등록
        if (files != null) {
            this.fileInsert(param.b_order!!, files)
        }

        return JSONObject()
    }


}

 

 

 

 

 

BoardController.kt

@Controller
class BoardController {


    @Autowired
    private lateinit var service: BoardService




    /**
     * 첨부파일 다운로드
     * @throws Exception
     */
    @RequestMapping(value = [("/file/download")])
    @ResponseBody
    @Throws(Exception::class)
    fun webFileDownload(
            response: HttpServletResponse,
            @RequestParam(value = "b_order", required = true) b_order: String,
            @RequestParam(value = "f_order", required = true) f_order: String
    ): ByteArray {

        val finfo = service.fileList(b_order, f_order)
        if (finfo != null) {
            if (Files.exists(Paths.get(finfo[0].f_path))) {

                val ins = FileInputStream(File(finfo[0].f_path))
                val temp: ByteArray = IOUtils.toByteArray(ins)
                ins.close()

                response.characterEncoding = "UTF-8"
                response.contentType = "application/octet-stream"
                response.setHeader("Expires", "-1;")
                response.setHeader("Pragma", "no-cache;")
                response.setHeader("Content-Transfer-Encoding", "binary;")
                response.setHeader("Content-Disposition", "Attachment;Filename=" + URLEncoder.encode(finfo[0].f_name, "UTF-8"))

                response.outputStream.write(temp)
                return temp
            }
        }
        return ByteArray(0)
    }



    

   
    /**
     * 게시글 등록 처리
     * @throws Exception
     */
    @RequestMapping(value = [("/board/write/submit")], method = [(RequestMethod.POST)])
    @ResponseBody
    @Throws(Exception::class)
    fun boardInsert(param: BoardModel, @RequestParam("files") files: List<MultipartFile>?) : JSONObject {
        return service.boardInsert(param, files)
    }



    /**
     * 게시글 수정 처리
     * @throws Exception
     */
    @RequestMapping(value = [("/board/modify/submit")], method = [(RequestMethod.POST)])
    @ResponseBody
    @Throws(Exception::class)
    fun boardModify(param: BoardModel, files: List<MultipartFile>?, f_order: String) : JSONObject {
        return service.boardModify(param, files, f_order)
    }

    
}

 

 

 

 

 

 

 

 

글 등록할 땐 첨부파일을 등록한다.

 

글 상세화면에서는 첨부파일을 다운로드 할 수 있다.

 

글 수정할 땐 기존 첨부파일을 목록형태로 보여주고 첨부파일 하나를 삭제하면 UI에서만 삭제되고 

글 수정 버튼을 누를 시에만 서버에 실제 파일이 삭제되도록 구현했다. 

그리고 새로운 첨부파일도 추가할 수 있다. 

그러므로 서버로직에서는 삭제된 파일을 삭제하는 로직 하나, 새로운 첨부파일을 등록하는 로직하나가 수정로직에 들어가 있다. 

 

 

 

 

register.html

<!-- 글 등록 -->
<div class="container">
    <form id="registerForm" name="registerForm" method="post" enctype="multipart/form-data">
        <!--/* 파일첨부 */-->
        <div id="fileArea">
            <div class="filebox">
                <input type="file" id="file1" name="files" onchange="handleFile(this);">
                <input type="button" onclick="cancelFile(this);" value="삭제">
            </div>
            <div class="filebox">
                <input type="file" id="file2" name="files" onchange="handleFile(this);">
                <input type="button" onclick="cancelFile(this);" value="삭제">
            </div>
        </div>
    </form>
</div>






<!-- 글 상세 -->
<div th:if="not#lists.isEmpty(FILE)}">
    <!--/* 첨부파일 목록 */-->
    <div th:each="file : ${FILE}">
        <label th:text="${file['f_name']}"></label>
        <a th:href="@{/file/download(b_order=${INFO['b_order']}, f_order=${file['f_order']})}">
            <span>다운로드</span>
        </a>
    </div>
</div>







<!-- 글 수정 -->
<div class="container">
    <form id="modifyForm" name="modifyForm" method="post" enctype="multipart/form-data">
        <input type="hidden" id="b_order" name="b_order" th:value="${INFO['b_order']}"><!-- 게시글 번호 -->
        <input type="hidden" id="f_order" name="f_order"><!-- 삭제한 첨부파일 번호 -->

        <!--/* 파일첨부 */-->
        <div id="fileArea">
            <!--/* 기존에 첨부된 파일 목록 */-->
            <div th:if="${not#lists.isEmpty(FILE)}" th:each="file : ${FILE}">
                <label th:text="${file['f_name']}"></label>
                <a th:onclick="deleteFile( [[${file['f_order']}]] , this);">
                    <span>삭제</span>
                </a>
            </div>

            <!--/* 파일 추가 */-->
            <div th:if="${#lists.isEmpty(FILE) or #lists.size(FILE) < 5}">
                <input type="file" id="file1" name="files" onchange="handleFile(this);">
                <input type="button" onclick="cancelFile(this);" value="삭제">
            </div>
        </div>
    </form>
</div>

 

 

 

javascript

/* 첨부파일 취소 (input file 에서 첨부한 파일 삭제) */
function cancelFile(obj) {
    $(obj).prevAll().val("");
},



/* 첨부파일 삭제 (서버에서 삭제하기 위해 삭제한 파일번호 저장) */
function deleteFile(order, obj) {
    fileArr.push(order);        // 삭제한 첨부파일 번호 저장
    obj.parentNode.remove();    // 화면에서 삭제
},



/* 파일 검증 */
function handleFile(obj) {
    var fileName = obj.value.substring(obj.value.lastIndexOf('\\')+1, obj.length);
    var fileExt = obj.value.substring(obj.value.lastIndexOf('.')+1);    // 확장자

    // 파일명 길이 체크
    if (fileName.length > 100) {
        obj.value = '';
        alert("[ERR] 파일명이 너무 길어 첨부할 수 없습니다.");
        return;

    // 파일 용량 체크
    } else if (obj.files && obj.files[0].size > (100 * 1024 * 1024)) {
        obj.value = '';
        alert("[ERR] 최대 파일 용량인 100MB를 초과했습니다.");
        return;

    // 확장자 null 체크
    } else if (obj.value.lastIndexOf('.') == -1) {
        obj.value = '';
        alert("[ERR] 확장자가 없는 파일입니다.");
        return;
            
    // 특정 확장자 제외
    } else if (fileExt.toLowerCase() == "exe" || fileExt.toLowerCase() == "com" || fileExt.toLowerCase() == "bat" || fileExt.toLowerCase() == "msi") {
        obj.value = '';
        alert("[ERR] 첨부가 불가능한 파일입니다.");
        return;

    } else {
        // 파일명 변경
        obj.value = fileName;
    }
},



/* 글 수정 폼전송 */
var fileArr = new Array();
function modifySubmit() {

    document.getElementById('f_order').value = fileArr;   // 삭제한 첨부파일 번호 저장

    $('#modifyForm').ajaxForm({
        url: '/board/modify/submit',
        type: 'POST',
        dataType: 'json',
        beforeSubmit: function (data, frm, opt) {
            return true;
        },
        success: function (responseText, statusText) {
            document.location.href = '/board/detail?order=' + INFO['b_order'] + '&page=' + PAGE;
        },
        error: function () {
            alert('에러발생');
            return;
        }
    }).submit();

}