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();
}