Java/JPA

JPA 기초(file) - AWS 풀스택 과정 84일차

awspspgh 2024. 11. 25. 10:10
목차
1. file

 

1. file

 build.gradle (boot_JPA)

plugins {
	id 'java'
	id 'war'
	id 'org.springframework.boot' version '3.2.11'
	id 'io.spring.dependency-management' version '1.1.6'
}

group = 'com.ezen'
version = '0.0.1-SNAPSHOT'

java {
	toolchain {
		languageVersion = JavaLanguageVersion.of(17)
	}
}

configurations {
	compileOnly {
		extendsFrom annotationProcessor
	}
}

repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
	implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
	// https://mvnrepository.com/artifact/nz.net.ultraq.thymeleaf/thymeleaf-layout-dialect
	implementation 'nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	// https://mvnrepository.com/artifact/org.bgee.log4jdbc-log4j2/log4jdbc-log4j2-jdbc4.1
	implementation 'org.bgee.log4jdbc-log4j2:log4jdbc-log4j2-jdbc4.1:1.16'
	// https://mvnrepository.com/artifact/org.apache.tika/tika-core
	implementation 'org.apache.tika:tika-core:2.4.1'
	// https://mvnrepository.com/artifact/org.apache.tika/tika-parsers
	implementation 'org.apache.tika:tika-parsers:2.4.1'
	// https://mvnrepository.com/artifact/net.coobird/thumbnailator
	implementation 'net.coobird:thumbnailator:0.4.17'
	compileOnly 'org.projectlombok:lombok'
	developmentOnly 'org.springframework.boot:spring-boot-devtools'
	runtimeOnly 'com.mysql:mysql-connector-j'
	annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
	annotationProcessor 'org.projectlombok:lombok'
	/*providedRuntime 'org.springframework.boot:spring-boot-starter-tomcat'*/
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

tasks.named('test') {
	useJUnitPlatform()
}

 

 application.properties

spring.application.name=boot_JPA
server.port=8089

spring.datasource.driver-class-name=net.sf.log4jdbc.sql.jdbcapi.DriverSpy
spring.datasource.url=jdbc:log4jdbc:mysql://localhost:3306/bootdb2
spring.datasource.username=springUser
spring.datasource.password=mysql

spring.jpa.show-sql=true
spring.jpa.database-platform=org.hibernate.dialect.MySQLDialect
spring.jpa.database=mysql
spring.jpa.hibernate.ddl-auto=update
spring.jpa.properties.hibernate.format_sql=true

spring.thymeleaf.cache=false

spring.servlet.multipart.enabled=true
spring.servlet.multipart.location=D:/_myProject/_java/_fileUpload/
spring.servlet.multipart.max-file-size=20MB
spring.servlet.multipart.max-request-size=40MB
uploadPath=file:///D:/_myProject/_java/_fileUpload/

 

 WebMvcConfig.java

package com.ezen.boot_JPA.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
    // upload 경로 매핑

    String uploadPath = "file:///D:\\_myProject\\_java\\_fileUpload\\";

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/upload/**")
                .addResourceLocations(uploadPath);
    }
}

 

 File.java

package com.ezen.boot_JPA.entity;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import lombok.*;

import java.time.LocalDateTime;

@Entity
@Getter
@Setter
@ToString
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class File extends TimeBase{

    @Id
    private String uuid;

    @Column(name = "save_dir", nullable = false)
    private String saveDir;

    @Column(name = "file_name", nullable = false)
    private String fileName;

    @Column(name = "file_type", nullable = false, columnDefinition = "integer default 0")
    private int fileType;

    private long bno;

    @Column(name = "file_size")
    private long fileSize;
}

 

 FileDTO.java

package com.ezen.boot_JPA.dto;

import lombok.*;

import java.time.LocalDateTime;

@Getter
@Setter
@ToString
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class FileDTO {
    private String uuid;
    private String saveDir;
    private String fileName;
    private int fileType;
    private long bno;
    private long fileSize;
    private LocalDateTime regAt;
    private LocalDateTime modAt;
}

 

 BoardFileDTO.java

package com.ezen.boot_JPA.dto;

import lombok.*;

import java.util.List;

@Getter
@Setter
@ToString
@AllArgsConstructor
@NoArgsConstructor
public class BoardFileDTO {
    private BoardDTO boardDTO;
    private List<FileDTO> fileDTOList;
}

 

 BoardController.java

package com.ezen.boot_JPA.controller;

import com.ezen.boot_JPA.dto.BoardDTO;
import com.ezen.boot_JPA.dto.BoardFileDTO;
import com.ezen.boot_JPA.dto.FileDTO;
import com.ezen.boot_JPA.dto.PagingVO;
import com.ezen.boot_JPA.handler.FileHandler;
import com.ezen.boot_JPA.handler.FileRemoveHandler;
import com.ezen.boot_JPA.service.BoardService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
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;

@Slf4j
@RequestMapping("/board/*")
@RequiredArgsConstructor
@Controller
public class BoardController {
    private final BoardService boardService;
    private final FileHandler fileHandler;

    @GetMapping("/register")
    public void register(){}

/*    @PostMapping("/register")
    public String register(BoardDTO boardDTO){
        log.info(">>> boardDTO >>> {}", boardDTO);
        // insert, update, delete => return 1 row
        // jpa insert, update, delete => return id
        Long bno = boardService.insert(boardDTO);
        log.info(">>> insert >>> {}", bno > 0 ? "OK" : "FAIL");
        return "/index";
    }*/

    @PostMapping("/register")
    public String Register(BoardDTO boardDTO, @RequestParam(name = "files", required = false)
                           MultipartFile[] files){
        List<FileDTO> flist = null;
        if(files != null && files[0].getSize() > 0){
            // 파일 핸들러 작업
            flist = fileHandler.uploadFiles(files);
        }
        long bno = boardService.insert(new BoardFileDTO(boardDTO, flist));
        return "/index";
    }

    /*@GetMapping("/list")
    public void list(Model model){
        // paging이 없는 케이스
        List<BoardDTO> list = boardService.getList();
        model.addAttribute("list", list);
    }*/

    @GetMapping("/list")
    public void list(Model model, @RequestParam(value = "pageNo", defaultValue = "0", required = false) int pageNo){
        // 화면에서 들어오는 pageNo = 1 / 0으로 처리가 되어야 함
        // 화면에서 들어오는 pageNo = 2 / 1로 처리가 되어야 함
        log.info(">>> pageNo >>> {}", pageNo);
        pageNo = (pageNo == 0 ? 0 : pageNo - 1);
        log.info(">>> pageNo >>> {}", pageNo);
        Page<BoardDTO> list = boardService.getList(pageNo);

        log.info(">>> list >>> {}", list.toString());
        log.info(">>> totalCount >>> {}", list.getTotalElements()); // 전체 글 수
        log.info(">>> totalPage >>> {}", list.getTotalPages()); // 전체 페이지 수 => realEndPage
        log.info(">>> pageNumber >>> {}", list.getNumber()); // 전체 페이지 번호 => pageNo
        log.info(">>> pageSize >>> {}", list.getSize()); // 한 페이지에 표시되는 길이 => qty
        log.info(">>> next >>> {}", list.hasNext()); // next 여부
        log.info(">>> prev >>> {}", list.hasPrevious()); // prev 여부

        PagingVO pgvo = new PagingVO(list, pageNo);
        log.info(">>> pgvo >>> {}", pgvo.toString());
        model.addAttribute("list", list);
        model.addAttribute("pgvo", pgvo);
    }

    @GetMapping("/detail")
    public void modify(Model model, @RequestParam("bno") Long bno){
//        BoardDTO boardDTO = boardService.getDetail(bno);
        BoardFileDTO boardFileDTO = boardService.getDetail(bno);
        model.addAttribute("boardFileDTO", boardFileDTO);
    }

/*    @PostMapping("/modify")
    public String modify(BoardDTO boardDTO, RedirectAttributes redirectAttributes){
        Long bno = boardService.modify(boardDTO);
        redirectAttributes.addAttribute("bno", boardDTO.getBno());
        return "redirect:/board/detail";
    }   */

    @PostMapping("/modify")
    public String modify(BoardDTO boardDTO, @RequestParam(name = "files", required = false) MultipartFile[] files, RedirectAttributes redirectAttributes){
        List<FileDTO> flist = null;
        if(files != null && files[0].getSize() > 0){
            flist = fileHandler.uploadFiles(files);
        }
        Long bno = boardService.modify(new BoardFileDTO(boardDTO, flist));
        redirectAttributes.addAttribute("bno", boardDTO.getBno());
        return "redirect:/board/detail";
    }

    @GetMapping("/delete")
    public String delete(@RequestParam("bno") Long bno){
        boardService.delete(bno);
        return "redirect:/board/list";
    }

    @ResponseBody
    @DeleteMapping("/file/{uuid}")
    public String fileRemove(@PathVariable("uuid") String uuid){
        FileDTO fvo = boardService.getFile(uuid);
        long bno = boardService.fileRemove(uuid);
        FileRemoveHandler fr = new FileRemoveHandler();
        boolean isDel = fr.deleteFile(fvo);
        return (bno > 0 && isDel) ? "1" : "0";
    }
}

 

 BoardService.java

package com.ezen.boot_JPA.service;

import com.ezen.boot_JPA.dto.BoardDTO;
import com.ezen.boot_JPA.dto.BoardFileDTO;
import com.ezen.boot_JPA.dto.FileDTO;
import com.ezen.boot_JPA.entity.Board;
import com.ezen.boot_JPA.entity.File;
import org.springframework.data.domain.Page;

import java.util.List;

public interface BoardService {
    // 추상 메서드만 가능한 인터페이스
    // 메서드가 default(접근제한자) 구현 가능
    Long insert(BoardDTO boardDTO);
    long insert(BoardFileDTO boardFileDTO);

    // BoardDTO(class): bno title writer content regAt modAt
    // Board(table) : bno title writer content
    // BoardDTO => board 변환
    // 화면에서 가져온 BoardDTO 객체를 저장을 위한 Board 객체로 변환
    default Board convertDtoToEntity(BoardDTO boardDTO){
        return Board.builder()
                .bno(boardDTO.getBno())
                .title(boardDTO.getTitle())
                .writer(boardDTO.getWriter())
                .content(boardDTO.getContent())
                .build();
    }

    // board => BoardDTO 변환
    // DB에서 가져온 Board 객체를 화면에 뿌리기 위한 BoardDTO 객체로 변환
    default BoardDTO convertEntityToDto(Board board){
        return BoardDTO.builder()
                .bno(board.getBno())
                .title(board.getTitle())
                .writer(board.getWriter())
                .content(board.getContent())
                .regAt(board.getRegAt())
                .modAt(board.getModAt())
                .build();
    }

    // File 객체 convert
    // FileDTO => File Entity
    default File convertDtoToEntity(FileDTO fileDTO){
        return File.builder()
                .uuid(fileDTO.getUuid())
                .saveDir(fileDTO.getSaveDir())
                .fileName(fileDTO.getFileName())
                .fileType(fileDTO.getFileType())
                .bno(fileDTO.getBno())
                .fileSize(fileDTO.getFileSize())
                .build();
    }

    // File Entity => FileDTO
    default FileDTO convertEntityToDto(File file){
        return FileDTO.builder()
                .uuid(file.getUuid())
                .saveDir(file.getSaveDir())
                .fileName(file.getFileName())
                .fileType(file.getFileType())
                .bno(file.getBno())
                .fileSize(file.getFileSize())
                .regAt(file.getRegAt())
                .modAt(file.getModAt())
                .build();
    }

//  List<BoardDTO> getList();

    Page<BoardDTO> getList(int pageNo);

//    BoardDTO getDetail(Long bno);

    BoardFileDTO getDetail(Long bno);

//    Long modify(BoardDTO boardDTO);
    Long modify(BoardFileDTO boardFileDTO);

    void delete(Long bno);

    long fileRemove(String uuid);

    FileDTO getFile(String uuid);
}

 

 BoardServiceImpl.java

package com.ezen.boot_JPA.service;

import com.ezen.boot_JPA.dto.BoardDTO;
import com.ezen.boot_JPA.dto.BoardFileDTO;
import com.ezen.boot_JPA.dto.FileDTO;
import com.ezen.boot_JPA.entity.Board;
import com.ezen.boot_JPA.entity.File;
import com.ezen.boot_JPA.repository.BoardRepository;
import com.ezen.boot_JPA.repository.FileRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.Optional;

@Slf4j
@RequiredArgsConstructor
@Service
public class BoardServiceImpl implements BoardService{
    private final BoardRepository boardRepository;
    private final FileRepository fileRepository;

    @Override
    public Long insert(BoardDTO boardDTO) {
        // 저장 객체는 Board
        // save() : insert 후 저장 객체의 id를 리턴
        // save() Entity 객체를 파라미터로 전송
        return boardRepository.save(convertDtoToEntity(boardDTO)).getBno();
    }

    @Transactional
    @Override
    public long insert(BoardFileDTO boardFileDTO) {
//        long bno = boardRepository.save(convertDtoToEntity(boardFileDTO.boardDTO())).getBno();
        long bno = insert(boardFileDTO.getBoardDTO());
        if (bno > 0 && boardFileDTO.getFileDTOList() != null){
            for(FileDTO fileDTO : boardFileDTO.getFileDTOList()){
                fileDTO.setBno(bno);
                bno = fileRepository.save(convertDtoToEntity(fileDTO)).getBno();
            }
        }
        return bno;
    }

//    @Override
//    public List<BoardDTO> getList() {
//        // 컨트롤러로 보내야 하는 리턴은 List<BoardDTO>
//        // DB에서 가져오는 리턴은 List<Board> > BoardDTO 객체로 변환
//        // findAll()
//        // 정렬 : Sort.by(Sort.Direction.DESC, "정렬기준 칼럼명")
//        List<Board> boardList = boardRepository.findAll(Sort.by(Sort.Direction.DESC, "bno"));
//        /* List<BoardDTO> boardDTOList = new ArrayList<>();
//        for(Board board : boardList){
//            boardDTOList.add(convertEntityToDto(board));
//        }*/
//        List<BoardDTO> boardDTOList = boardList.stream()
//                .map(b -> convertEntityToDto(b)).toList();
//        return boardDTOList;
//    }


    @Override
    public Page<BoardDTO> getList(int pageNo) {
        // pageNo = 0부터 시작
        // 0 => limit 0, 10 / 1 => limit 10, 10
        Pageable pageable = PageRequest.of(pageNo, 10,
                Sort.by("bno").descending());
        Page<Board> list = boardRepository.findAll(pageable);
        Page<BoardDTO> boardDTOList = list.map(b -> convertEntityToDto(b));
        return boardDTOList;
    }

    @Override
    public BoardFileDTO getDetail(Long bno) {
        Optional<Board> optional = boardRepository.findById(bno);
        if(optional.isPresent()){
            BoardDTO boardDTO = convertEntityToDto(optional.get());

        // file bno에 일치하는 모든 파일 리스트를 가져오기
        // select * from file where bno = #{bno}
            List<File> flist = fileRepository.findByBno(bno);
            List<FileDTO> fileDTOList = flist.stream()
                    .map(f -> convertEntityToDto(f)).toList();
            BoardFileDTO boardFileDTO = new BoardFileDTO(boardDTO, fileDTOList);
            log.info(">>> boardFileDTO >>> {}", boardFileDTO);
            return boardFileDTO;
        }
        return null;
    }

    @Override
    public Long modify(BoardFileDTO boardFileDTO) {
            long bno = insert(boardFileDTO);
            return bno;
        /*Optional<Board> optional = boardRepository.findById(boardFileDTO.getBoardDTO().getBno());
        if(optional.isPresent()){
            Board board = optional.get();
            board.setTitle(boardFileDTO.getBoardDTO().getTitle());
            board.setContent(boardFileDTO.getBoardDTO().getContent());
            long bno = boardRepository.save(board).getBno();
            if (bno > 0 && boardFileDTO.getFileDTOList() != null) {
                for (FileDTO fileDTO : boardFileDTO.getFileDTOList()) {
                    fileDTO.setBno(bno);
                    bno = fileRepository.save(convertDtoToEntity(fileDTO)).getBno();
                }
                return bno;
            }
        }
        return 0L;*/
    }

   /* @Override
    public BoardDTO getDetail(Long bno) {
        *//* findById : 아이디(PK)를 주고 해당 객체를 리턴
            findById의 리턴타입 Optional<Board> 타입으로 리턴
            Optional<T> : nullPointException이 발생하지 않도록 도와줌.
            Optional.isEmpty() : null일 경우 확인 가능 true / false
            Optional.isPresent() : 값이 있는지 확인 true / false
            Optional.get() : 객체 가져오기
        * *//*
        Optional<Board> optional = boardRepository.findById(bno);
        if(optional.isPresent()){
            BoardDTO boardDTO = convertEntityToDto(optional.get());
            return boardDTO;
        }
        return null;
    }*/

//    @Override
//    public Long modify(BoardDTO boardDTO) {
//        // update : jpa는 업데이트가 없음.
//        // 기존 객체를 가져와서 set 수정 후 다시 저장
//        Optional<Board> optional = boardRepository.findById(boardDTO.getBno());
//        if(optional.isPresent()){
//            Board entity = optional.get();
//            entity.setTitle(boardDTO.getTitle());
//            entity.setContent(boardDTO.getContent());
//            // 다시 저장 (기존 객체에 덮어쓰기)
//            return boardRepository.save(entity).getBno();
//        }
//        return null;
//    }

    // 삭제 : deleteById(id)

    @Override
    public void delete(Long bno) {
        boardRepository.deleteById(bno);
    }

    @Override
    public long fileRemove(String uuid) {
        Optional<File> optional = fileRepository.findById(uuid);
        if(optional.isPresent()){
            fileRepository.deleteById(uuid);
            return optional.get().getBno();
        }
        return 0;
    }

    @Override
    public FileDTO getFile(String uuid) {
        Optional<File> optional = fileRepository.findById(uuid);
        log.info(">>> op >>> {}", optional);
        if(optional.isPresent()){
            FileDTO fileDTO = convertEntityToDto(optional.get());
            return fileDTO;
        }

        return null;
    }

}

 

 FileRepository.java

package com.ezen.boot_JPA.repository;

import com.ezen.boot_JPA.entity.File;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;

public interface FileRepository extends JpaRepository<File, String> {
    List<File> findByBno(long bno);
}

 

 FileHandler.java

package com.ezen.boot_JPA.handler;

import com.ezen.boot_JPA.dto.FileDTO;
import lombok.extern.slf4j.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 final String UP_DIR = "D:\\_myProject\\_java\\_fileUpload\\";

    public List<FileDTO> uploadFiles(MultipartFile[] files){
        List<FileDTO> 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(); // 여러 개 생성
        }

        /* 폴더 생성 완료 */
        for(MultipartFile file : files){
            FileDTO fileDTO = new FileDTO();
            fileDTO.setSaveDir(today);
            fileDTO.setFileSize(file.getSize());

            String originalFileName = file.getOriginalFilename();
            String onlyFileName = originalFileName.substring(originalFileName.lastIndexOf(File.separator)+1);
            fileDTO.setFileName(onlyFileName);

            UUID uuid = UUID.randomUUID();
            fileDTO.setUuid(uuid.toString());

            // ------------ fvo 설정 마무리

            // 디스크에 저장할 파일 설정
            String fullFileName = uuid.toString()+"_"+onlyFileName;
            String thumbFileName = uuid.toString()+"_th_"+onlyFileName;

            File storeFile = new File(folders, fullFileName); // 실제 저장 객체

            // 저장
            try{
                file.transferTo(storeFile); // 실제 파일의 값을 저장 File 객체에 기록
                if(isImageFile(storeFile)){
                    fileDTO.setFileType(1);
                    File thumbnail = new File(folders, thumbFileName);
                    // 썸네일 작업
                    Thumbnails.of(storeFile).size(100, 100).toFile(thumbnail);
                }
            } catch (Exception e){
                e.printStackTrace();
            }
            // for문 안
            flist.add(fileDTO);
        }
        return flist;
    }
    private boolean isImageFile(File file) throws IOException {
        String mimeType = new Tika().detect(file);
        return mimeType.startsWith("image");
    }
}

 

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

 

 boardDetail.js

console.log("board detail.js in!!");

document.getElementById('listBtn').addEventListener('click', ()=>{
    location.href = "/board/list";
});

document.getElementById('modBtn').addEventListener('click',()=>{
    document.getElementById('title').readOnly=false;
    document.getElementById('content').readOnly=false;

    // 버튼 생성
    let modBtn = document.createElement("button");
    modBtn.setAttribute("type","submit");
    modBtn.setAttribute("id","regBtn");
    modBtn.classList.add("btn","btn-outline-warning");
    modBtn.innerText="Submit";

    // 추가
    document.getElementById("modForm").appendChild(modBtn);
    document.getElementById("modBtn").remove();
    document.getElementById("delBtn").remove();

    // file -> fileUpload 버튼 disabled = false
    document.getElementById('trigger').disabled = false;

    let fileDelBtn = document.querySelectorAll(".file-x");
    console.log(fileDelBtn);
    for(let delBtn of fileDelBtn){
        delBtn.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);
    }
}

 

 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" cols="10" rows="3">content...</textarea>
      </div>
      <!-- 파일 등록 라인 -->
      <div class="mb-3">
        <label for="file">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>
  </div>
  <script th:src="@{/js/boardRegister.js}"></script>
</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">
    <div class="container-md">
        <h1>Board Detail Page [[${boardFileDTO.boardDTO.bno}]]</h1>

        <form action="/board/modify" method="post" enctype="multipart/form-data" id="modForm" th:with="boardDTO=${boardFileDTO.boardDTO}">
            <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="${boardDTO.regAt}" readonly>
            </div>
            <div class="mb-3">
                <input type="hidden" class="form-control" name="bno" id="n" th:value="${boardDTO.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="${boardDTO.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="${boardDTO.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>[[${boardFileDTO.boardDTO.content}]]</textarea>
            </div>
            <!-- 파일 출력라인 -->
            <div class="mb-3">
                <ul class="list-group">
                    <li th:each="fvo:${boardFileDTO.fileDTOList}" 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">
                            <!-- 일반 파일은 다운로드 가능 -->
                            <a th:href="@{|/upload/${fvo.saveDir}/${fvo.uuid}_${fvo.fileName}|}" th:download="${fvo.fileName}">
                                <!-- 파일 모양 아이콘 -->
                                <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>
                            </a>
                        </div>
                        <div class="ms-2 me-auto">
                            <div class="fw-bold">[[${fvo.fileName}]]</div>
                            [[${fvo.regAt}]]
                        </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>

            <!-- 파일 수정 등록 라인 -->
            <div class="input-group mb-3" id="fileZone"></div>

            <button type="button" id="listBtn" class="btn btn-primary">List</button>
            <button type="button" id="modBtn" class="btn btn-warning">Modify</button>
            <a th:href="@{/board/delete(bno=${boardDTO.bno})}">
                <button type="button" id="delBtn" class="btn btn-danger">Delete</button>
            </a>
        </form>

        <!-- comment line -->
        <!-- post -->
        <div class="input-group mb-3" >
            <span class="input-group-text" id="cmtWriter"></span>
            <input type="text" id="cmtText" class="form-control" placeholder="Add Comment..." aria-label="Username" aria-describedby="basic-addon1">
            <button type="button" id="cmtAddBtn" class="btn btn-secondary">post</button>
        </div>
        <!-- spread -->
        <ul class="list-group list-group-flush" id="cmtListArea">
            <li class="list-group-item">
                <div class="ms-2 me-auto">
                    <div class="fw-bold">writer</div>
                    Content
                </div>
                <span class="badge text-bg-primary rounded-pill">regDate</span>
            </li>
        </ul>
        <!-- moreBtn-->
        <div>
            <button type="button" id="moreBtn" data-page="1" class="btn btn-dark" style="visibility:hidden">MORE + </button>
        </div>
        <!-- modal -->
        <div class="modal fade" id="myModal" tabindex="-1" aria-labelledby="exampleModalLabel" aria-hidden="true">
            <div class="modal-dialog">
                <div class="modal-content">
                    <div class="modal-header">
                        <h1 class="modal-title fs-5" id="cmtWriterMod">${authNick }</h1>
                        <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
                    </div>
                    <div class="modal-body">
                        <input type="text" class="form-control" id="cmtTextMod">
                    </div>
                    <div class="modal-footer">
                        <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
                        <button type="button" id="cmtModBtn" class="btn btn-primary">changes</button>
                    </div>
                </div>
            </div>
        </div>

        <script th:inline="javascript">
            let bnoVal = [[${boardFileDTO.boardDTO.bno}]]
            console.log(bnoVal);
        </script>

    </div>
    <script th:src="@{/js/boardDetail.js}"></script>
    <script th:src="@{/js/boardComment.js}"></script>
    <script th:src="@{/js/boardRegister.js}"></script>

    <script>
        spreadCommentList(bnoVal);
    </script>
</div>

 

출석