목차 | |
1. | file |
1. file
◈ application.properties
spring.application.name=spring
server.port=8089
# 타임리프 캐싱 끄기. 새로고침 반영 설정
spring.thymeleaf.cache=false
spring.thymeleaf.enabled=true
spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.suffix=.html
# DB 설정
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.jdbc-url=jdbc:mysql://localhost:3306/bootdb
spring.datasource.username=springUser
spring.datasource.password=mysql
# mybatis
mybatis.mapper-locations=classpath:/mappers/*.xml
mybatis.configuration.map-underscore-to-camel-case=true
#fileUpload 경로
spring.servlet.multipart.enabled=true
spring.servlet.multipart.max-file-size=20MB
spring.servlet.multipart.max-request-size=40MB
spring.servlet.multipart.location=D:/_myProject/_java/_fileUpload
◈ BoardDTO.java
package com.ezen.spring.domain;
import lombok.*;
import java.util.List;
@Getter
@Setter
@ToString
@AllArgsConstructor
@NoArgsConstructor
public class BoardDTO {
private BoardVO bvo;
private List<FileVO> flist;
}
◈ FileVO.java
package com.ezen.spring.domain;
import lombok.*;
@Getter
@Setter
@ToString
@AllArgsConstructor
@NoArgsConstructor
public class FileVO {
private String uuid;
private String saveDir;
private String fileName;
private int fileType;
private long bno;
private long fileSize;
private String regDate;
}
◈ FileHandler.java
package com.ezen.spring.handler;
import com.ezen.spring.domain.FileVO;
import groovy.util.logging.Slf4j;
import net.coobird.thumbnailator.Thumbnails;
import org.apache.tika.Tika;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
@Slf4j
@Component
public class FileHandler {
private String UP_DIR = "D:\\_myProject\\_java\\_fileUpload\\";
public List<FileVO> uploadFiles(MultipartFile[] files) {
List<FileVO> flist = new ArrayList<>();
LocalDate date = LocalDate.now();
// 2024-11-15 =? 2024\\11\\15
String today = date.toString().replace("-", File.separator);
// D:\_myProject\_java\_fileUpload\2024\11\15
File folders = new File(UP_DIR, today);
// mkdir = 1개의 폴더만 // mkdirs = 여러 개
if(!folders.exists()){
folders.mkdirs(); // 여러 개 생성
}
// FileVO 생성
for(MultipartFile file : files){
// file => name / size
FileVO fvo = new FileVO();
fvo.setSaveDir(today);
fvo.setFileSize(file.getSize());
// file.name => 경로를 포함하는 경우도 있음. /test/test.txt
String originalFileName = file.getOriginalFilename();
String onlyFileName = originalFileName.substring(originalFileName.lastIndexOf(File.separator)+1);
fvo.setFileName(onlyFileName);
UUID uuid = UUID.randomUUID();
String uuidStr = uuid.toString();
fvo.setUuid(uuidStr);
// ----- fvo 설정 마무리
// 디스크 저장
String fileName = uuidStr + "_" + onlyFileName;
File storeFile = new File(folders, fileName);
// 저장
try {
file.transferTo(storeFile);
// 파일 타입 : 그림파일만 썸네일 생성
if(isImageFile(storeFile)){
fvo.setFileType(1);
File thumbnail = new File(folders, uuidStr+"_th_"+fileName);
Thumbnails.of(storeFile).size(100, 100).toFile(thumbnail);
}
} catch (Exception e) {
e.printStackTrace();
}
// for문 안
flist.add(fvo);
}
return flist;
}
private boolean isImageFile(File file) throws IOException {
String mimeType = new Tika().detect(file);
return mimeType.startsWith("image");
}
}
◈ FileRemoveHandler.java
package com.ezen.spring.handler;
import com.ezen.spring.domain.FileVO;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.io.File;
@Slf4j
@Component
public class FileRemoveHandler {
private final String BASE_PATH = "D:\\_myProject\\_java\\_fileUpload\\";
public boolean deleteFile(FileVO fvo){
File delFile = new File(BASE_PATH, fvo.getSaveDir());
boolean isDel = false;
String delete = fvo.getUuid()+"_"+fvo.getFileName();
try{
File deleteFile = new File(delFile, delete);
log.info("deleteFile {}", deleteFile);
isDel = deleteFile.delete();
if(fvo.getFileType()>0){
String deleteThumb = fvo.getUuid()+"_th_"+fvo.getFileName();
File deleteThumbFile = new File(delFile, deleteThumb);
log.info("deleteThumb {}", deleteThumbFile);
if(deleteThumbFile.exists()){
deleteThumbFile.delete();
}
}
} catch (Exception e) {
e.printStackTrace();
}
return isDel;
}
}
◈ register.html
<!DOCTYPE html>
<html lang="en"
xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layout/layout}">
<div layout:fragment="content" class="container-md">
<h1>Boot Register page</h1>
<hr>
<div>
<form action="/board/register" method="post" enctype="multipart/form-data">
<div class="mb-3">
<label for="t" class="form-label">Title</label>
<input type="text" class="form-control" name="title" id="t" placeholder="title...">
</div>
<div class="mb-3">
<label for="w" class="form-label">Writer</label>
<input type="text" class="form-control" name="writer" id="w" placeholder="writer...">
</div>
<div class="mb-3">
<label for="c" class="form-label">Content</label>
<textarea class="form-control" name="content" id="c" rows="3"></textarea>
</div>
<!-- file 추가 라인 -->
<div class="mb-3">
<label for="file" class="form-label">File</label>
<input type="file" class="form-control" name="files" id="file" multiple style="display:none;">
</div>
<button type="button" id="trigger" class="btn btn-primary">File Upload</button>
<!-- file 출력 라인 -->
<div class="input-group mb-3" id="fileZone"></div>
<button type="submit" class="btn btn-primary" id="regBtn">register</button>
</form>
<script th:src="@{/js/boardRegister.js}"></script>
</div>
</div>
◈ detail.html
<!DOCTYPE html>
<html lang="en"
xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layout/layout}">
<div layout:fragment="content" class="container-md">
<h1>Boot Detail page</h1>
<hr>
<th:block th:with="bvo=${bdto.bvo}">
<form action="/board/modify" method="post" id="modForm" enctype="multipart/form-data">
<div class="mb-3">
<label for="r" class="form-label">Created At</label>
<input type="text" class="form-control" name="regDate" id="r" th:value="${bvo.regDate}" readonly>
</div><div class="mb-3">
<input type="hidden" class="form-control" name="bno" id="n" th:value="${bvo.bno}">
</div>
<div class="mb-3">
<label for="title" class="form-label">Title</label>
<input type="text" class="form-control" name="title" id="title" th:value="${bvo.title}" readonly>
</div>
<div class="mb-3">
<label for="writer" class="form-label">Writer</label>
<input type="text" class="form-control" name="writer" id="writer" th:value="${bvo.writer}" readonly>
</div>
<div class="mb-3">
<label for="content" class="form-label">Content</label>
<textarea class="form-control" name="content" id="content" rows="3" readonly>[[${bvo.content}]]</textarea>
</div>
<!-- file print line -->
<div class="mb-3">
<ul class="list-group">
<li th:each="fvo:${bdto.flist}" class="list-group-item">
<div th:if="${fvo.fileType > 0}" class="ms-2 me-auto">
<img th:src="@{|/upload/${fvo.saveDir}/${fvo.uuid}_${fvo.fileName}|}" alt="img" />
</div>
<div th:unless="${fvo.fileType > 0}" class="ms-2 me-auto">
<!-- icon -->
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-paperclip" viewBox="0 0 16 16">
<path d="M4.5 3a2.5 2.5 0 0 1 5 0v9a1.5 1.5 0 0 1-3 0V5a.5.5 0 0 1 1 0v7a.5.5 0 0 0 1 0V3a1.5 1.5 0 1 0-3 0v9a2.5 2.5 0 0 0 5 0V5a.5.5 0 0 1 1 0v7a3.5 3.5 0 1 1-7 0z"/>
</svg>
</div>
<div class="ms-2 me-auto">
<div class="fw-bold">[[${fvo.fileName}]]</div>
[[${fvo.regDate}]]
</div>
<span class="badge text-bg-success rounded-pill">[[${fvo.fileSize}]]Bytes</span>
<button type="button" th:data-uuid="${fvo.uuid}" class="btn btn-outline-danger bnt-nm file-x" disabled>x</button>
</li>
</ul>
</div>
<!-- file 추가 라인 -->
<div class="mb-3">
<input type="file" class="form-control" name="files" id="file" multiple style="display:none;">
</div>
<button type="button" id="trigger" class="btn btn-primary" disabled>File Upload</button> <br>
<button type="button" id="listBtn" class="btn btn-primary">List</button>
<!-- de-tail page에서 modify 상태로 변경하기 위한 버튼 -->
<button type="button" id="modBtn" class="btn btn-warning">Modify</button>
<a th:href="@{/board/delete(bno=${bvo.bno})}">
<button type="button" id="delBtn" class="btn btn-danger">Delete</button>
</a>
</form>
</th:block>
<script th:src="@{/js/boardDetail.js}"></script>
<script th:src="@{/js/boardRegister.js}"></script>
</div>
◈ BoardController.java
package com.ezen.spring.controller;
import com.ezen.spring.domain.BoardDTO;
import com.ezen.spring.domain.BoardVO;
import com.ezen.spring.domain.FileVO;
import com.ezen.spring.domain.PagingVO;
import com.ezen.spring.handler.FileHandler;
import com.ezen.spring.handler.FileRemoveHandler;
import com.ezen.spring.handler.PagingHandler;
import com.ezen.spring.service.BoardService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import java.util.List;
@RequestMapping("/board/*")
@RequiredArgsConstructor
@Slf4j
@Controller
public class BoardController {
private final BoardService bsv;
private final FileHandler fh;
@GetMapping("/register")
public String register(){
return"/board/register";
}
@PostMapping("/register")
public String register(BoardVO boardVO, @RequestParam(name = "files", required = false)MultipartFile[] files){
log.info(">>>> boardVO >>>> {}", boardVO);
List<FileVO> flist = null;
if(files[0].getSize() > 0 && files != null){
flist = fh.uploadFiles(files);
log.info(">>>> flist >>>> {}", flist);
}
int isOk = bsv.register(new BoardDTO(boardVO, flist));
return "index";
}
@GetMapping("/list")
public String list(Model m, PagingVO pgvo){
// 전체 게시글 수 가져오기
int totalCount = bsv.getTotalCount(pgvo);
PagingHandler ph = new PagingHandler(pgvo, totalCount);
m.addAttribute("list", bsv.getList(pgvo));
m.addAttribute("ph", ph);
return"/board/list";
}
@GetMapping("/detail")
public String detail(@RequestParam("bno") long bno, Model m){
m.addAttribute("bdto", bsv.getDetail(bno));
return "/board/detail";
}
@PostMapping("/modify")
public String modify(BoardVO bvo, RedirectAttributes redirectAttributes, @RequestParam(value = "files", required = false)MultipartFile[] files){
List<FileVO> flist = null;
if(files[0].getSize()>0){
flist = fh.uploadFiles(files);
}
int isOk = bsv.update(new BoardDTO(bvo, flist));
redirectAttributes.addAttribute("bno", bvo.getBno());
return "redirect:/board/detail";
}
@GetMapping("/delete")
public String delete(@RequestParam("bno") long bno){
int isOk = bsv.delete(bno);
return "redirect:/board/list";
}
@ResponseBody
@DeleteMapping("/file/{uuid}")
public String removeFile(@PathVariable("uuid") String uuid){
FileVO fvo = bsv.getFile(uuid);
int isOk = bsv.removeFile(uuid);
// 파일 삭제
FileRemoveHandler fr = new FileRemoveHandler();
boolean isDel = fr.deleteFile(fvo);
return (isOk > 0 && isDel) ? "1" : "0";
}
}
◈ BoardService.java
package com.ezen.spring.service;
import com.ezen.spring.domain.BoardDTO;
import com.ezen.spring.domain.BoardVO;
import com.ezen.spring.domain.FileVO;
import com.ezen.spring.domain.PagingVO;
import java.util.List;
public interface BoardService {
// int register(BoardDTO boardVO);
int register(BoardDTO boardDTO);
List<BoardVO> getList(PagingVO pgvo);
BoardDTO getDetail(long bno);
int update(BoardDTO boardDTO);
int delete(long bno);
int getTotalCount(PagingVO pgvo);
int removeFile(String uuid);
FileVO getFile(String uuid);
}
◈ BoardServiceImpl.java
package com.ezen.spring.service;
import com.ezen.spring.domain.BoardDTO;
import com.ezen.spring.domain.BoardVO;
import com.ezen.spring.domain.FileVO;
import com.ezen.spring.domain.PagingVO;
import com.ezen.spring.repository.BoardMapper;
import com.ezen.spring.repository.FileMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.List;
@Slf4j
@RequiredArgsConstructor
@Service
public class BoardServiceImpl implements BoardService{
private final BoardMapper boardMapper;
private final FileMapper fileMapper;
@Override
public int register(BoardDTO boardDTO) {
int isOk = boardMapper.register(boardDTO.getBvo());
if(boardDTO.getFlist()==null){
return isOk;
}
if(isOk > 0 && boardDTO.getFlist().size() > 0){
// 파일저장
// board의 bno 가져오기 => 가장 큰 bno
long bno = boardMapper.getBno();
for(FileVO fvo : boardDTO.getFlist()){
fvo.setBno(bno);
isOk *= fileMapper.insertFile(fvo);
}
}
return isOk;
}
@Override
public List<BoardVO> getList(PagingVO pgvo) {
return boardMapper.getList(pgvo);
}
@Override
public BoardDTO getDetail(long bno) {
// fileList 가져와서 DTO 생성
BoardDTO bdto = new BoardDTO(boardMapper.getDetail(bno), fileMapper.getFileList(bno));
return bdto;
}
@Override
public int update(BoardDTO boardDTO) {
int isOk = boardMapper.update(boardDTO.getBvo());
if(boardDTO.getFlist()==null){
return isOk;
}
if(isOk > 0 && !boardDTO.getFlist().isEmpty()){
for(FileVO fvo : boardDTO.getFlist()){
fvo.setBno(boardDTO.getBvo().getBno());
isOk *= fileMapper.insertFile(fvo);
}
}
return isOk;
}
@Override
public int delete(long bno)
{
return boardMapper.delete(bno);
}
@Override
public int getTotalCount(PagingVO pgvo) {
return boardMapper.getTotalCount(pgvo);
}
@Override
public int removeFile(String uuid) {
return fileMapper.removeFile(uuid);
}
@Override
public FileVO getFile(String uuid) {
return fileMapper.getFile(uuid);
}
}
◈ BoardMapper.java
package com.ezen.spring.repository;
import com.ezen.spring.domain.BoardDTO;
import com.ezen.spring.domain.BoardVO;
import com.ezen.spring.domain.PagingVO;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper
public interface BoardMapper {
int register(BoardDTO boardDTO);
int register(BoardVO boardVO);
List<BoardVO> getList(PagingVO pgvo);
BoardVO getDetail(long bno);
int update(BoardVO bvo);
int delete(long bno);
int getTotalCount(PagingVO pgvo);
long getBno();
}
◈ boardMapper.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.ezen.spring.repository.BoardMapper">
<insert id="register">
insert into board(title, writer, content)
values(#{title}, #{writer}, #{content})
</insert>
<select id="getBno" resultType="long">
select max(bno) from board;
</select>
<select id="getList" resultType="com.ezen.spring.domain.BoardVO">
select * from board
<include refid="search"></include>
order by bno desc
limit #{startIndex}, #{qty}
</select>
<select id="getDetail" resultType="com.ezen.spring.domain.BoardVO">
select * from board where bno = #{bno}
</select>
<update id="update">
update board set title = #{title}, content = #{content}, reg_date = now()
where bno = #{bno}
</update>
<delete id="delete">
delete from board where bno = #{bno}
</delete>
<select id="getTotalCount" resultType="int">
select count(bno) from board
<include refid="search"></include>
</select>
<sql id="search">
<if test="type != null">
<trim prefix="where (" suffix=")" suffixOverrides="or">
<foreach collection="typeToArray" item="type">
<trim suffix="or">
<choose>
<when test="type=='t'.toString()">
title like concat('%', #{keyword}, '%')
</when>
<when test="type=='w'.toString()">
title like concat('%', #{keyword}, '%')
</when>
<when test="type=='c'.toString()">
title like concat('%', #{keyword}, '%')
</when>
</choose>
</trim>
</foreach>
</trim>
</if>
</sql>
</mapper>
◈ FileMapper.java
package com.ezen.spring.repository;
import com.ezen.spring.domain.FileVO;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper
public interface FileMapper {
int insertFile(FileVO fvo);
List<FileVO> getFileList(long bno);
int removeFile(String uuid);
FileVO getFile(String uuid);
}
◈ fileMapper.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.ezen.spring.repository.FileMapper">
<insert id="insertFile">
insert into file(uuid, save_dir, file_name, file_type, bno, file_size)
values(#{uuid}, #{saveDir}, #{fileName}, #{fileType}, #{bno}, #{fileSize})
</insert>
<select id="getFileList" resultType="com.ezen.spring.domain.FileVO">
select * from file where bno = #{bno}
</select>
<delete id="removeFile">
delete from file where uuid = #{uuid}
</delete>
<select id="getFile">
select * from file where uuid = #{uuid}
</select>
</mapper>
◈ WebMvcConfig.java
package com.ezen.spring.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceChainRegistration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
String uploadPath = "file:///D:\\_myProject\\_java\\_fileUpload\\";
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/upload/**")
.addResourceLocations(uploadPath);
}
}
◈ boardDetail.js
console.log("boardDetail.js in!!");
document.getElementById('listBtn').addEventListener('click', () => {
// 리스트로 이동
location.href="/board/list";
});
document.getElementById('modBtn').addEventListener('click', ()=>{
// title, content의 readonly를 해지 readOnly = true / false
document.getElementById('title').readOnly = false;
document.getElementById('content').readOnly = false;
// modBtn delBtn 삭제
document.getElementById('modBtn').remove();
document.getElementById('delBtn').remove();
// modBtn => submit
// <button></button>
let modBtn = document.createElement('button');
// <button type="submit"></button>
modBtn.setAttribute('type','submit');
modBtn.setAttribute('id','regBtn');
// class="btn btn-warning"
modBtn.classList.add('btn','btn-outline-warning');
// <button type="submit" class="btn btn-outline-warning">submit</button>
modBtn.innerText="submit";
// form 태그의 자식 요소로 추가 - form에 가장 마지막에 추가
document.getElementById('modForm').appendChild(modBtn);
// file-x 버튼 disabled 해지
let fileDelBtn = document.querySelectorAll(".file-x");
console.log(fileDelBtn);
for(let delBtn of fileDelBtn){
delBtn.disabled = false;
}
// 파일 업로드 버튼 disabled 해지
document.getElementById('trigger').disabled = false;
});
document.addEventListener('click',(e)=>{
if(e.target.classList.contains('file-x')){
console.log(e.target);
let uuid = e.target.dataset.uuid;
fileRemoveToServer(uuid).then(result => {
if(result > 0){
alert("파일 삭제 성공");
e.target.closest('li').remove();
}
})
}
});
// 비동기 데이터 보내기
async function fileRemoveToServer(uuid) {
try{
const url = '/board/file/'+uuid;
const config = {
method:"delete"
}
const resp = await fetch(url, config);
const result = await resp.text();
return result;
}catch(error){
console.log(error);
}
}
◈ boardRegister.js
console.log("boardRegister.js in!!");
document.getElementById('trigger').addEventListener('click',()=>{
document.getElementById('file').click();
});
// 실행파일 막기 / 20MB 이상
const regExp = new RegExp("\.(exe|sh|bat|jar|dll|msi)$");
const maxSize = 1024*1024*20;
function fileValidation(fileName, fileSize){
if(regExp.test(fileName)){
return 0;
}else if(fileSize > maxSize){
return 0;
}else{
return 1;
}
}
document.addEventListener('change',(e)=>{
if(e.target.id == 'file'){
const fileObject = document.getElementById('file').files;
console.log(fileObject);
document.getElementById('regBtn').disabled = false;
const fileZone = document.getElementById('fileZone');
// 이전에 추가한 파일 삭제
fileZone.innerHTML = "";
let ul = `<ul class="list-group list-group-flush">`;
let isOk = 1; // 여러 파일에 대한 값을 확인하기 위해 1 *
for(let file of fileObject){
let vaild = fileValidation(file.name, file.size);
isOk *= vaild;
ul += `<li class="list-group-item">`;
ul += `<div class="ms-2 me-auto">`;
ul += `${vaild ? '<div class="fw-bold">업로드 가능</div>' : '<div class="fw-bold text-danger">업로드 불가능</div>'}`;
ul += `${file.name}</div>`;
ul += `<span class="badge text-bg-${vaild ? 'success' : 'danger'} rounded-pill">${file.size}Bytes</span></li>`;
}
ul += `</ul>`;
fileZone.innerHTML = ul;
if(isOk == 0){
document.getElementById('regBtn').disabled = true;
}
}
});
/* <ul class="list-group list-group-flush">
<li class="list-group-item">An item</li>
</ul> */
◈ Application.java
package com.ezen.spring;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
@EnableScheduling
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
▷ 출력
'Java > Spring Boot' 카테고리의 다른 글
Spring Boot 기초(security) - AWS 풀스택 과정 81일차 (0) | 2024.11.20 |
---|---|
Spring Boot 기초(comment) - AWS 풀스택 과정 80일차 (0) | 2024.11.19 |
Spring Boot 기초(paging) - AWS 풀스택 과정 78일차 (0) | 2024.11.15 |
Spring Boot 기초(DB) - AWS 풀스택 과정 77일차 (0) | 2024.11.14 |
Spring Boot 설정 및 기초 - AWS 풀스택 과정 76일차 (0) | 2024.11.13 |