본문 바로가기

boot_JPA 수업 정리

Boot_JPA 3일차

파일 기능 구현 ( 다했던 내용이므로 순서와 코드정도만  자세한내용은 처음 파일기능 구현 게시글 참고 JPA하면서 추가된내용만 추가적으로 서술 ) 

 

파일 추가

● register 화면에 file 추가, 출력라인 화면 구성 

        <!--/* file 추가 라인 */-->
        <div class="mb-3">
          <label for="file" class="form-label">File</label>
          <input type="file" class="form-control" id="file" name="files" multiple style="display:none">
        </div>
        <button type="button" id="trigger" class="btn btn-primary">File Upload</button>
        <!--/* file 출력 라인 */-->
        <div class="mb-3" id="fileZone"></div>

 

● 파일 업로드버튼을 작동시켜줄 js 생성 / 구현 

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; // 여러 파일에 대한 값을 확인하기위해 하나라도 검열에 걸리면 0 
        for(let file of fileObject){
            let valid = fileValidation(file.name, file.size)
            isOk *=  valid
            ul += `<li class="list-group-item">`;
            ul += `<div class="ms-2 me-auto">`;
            ul += `${valid ? '<div class="fw-bold text-success-emphasis">업로드 가능</div>' : '<div class="fw-bold text-danger-emphasis">업로드 불가능</div>'}`;
            ul += `${file.name}</div>`;
            ul += `<span class="badge text-bg-${valid ? 'success' : 'danger'} rounded-pill">${file.size}Bytes</span>`
            ul += `</li>`;
        }
        ul += `</li>`;
        fileZone.innerHTML = ul;
        if(isOk == 0){     
            document.getElementById('regBtn').disabled = true;
        }
    }
})

 

  File Entity 랑 FileDTO 생성 / BoardFileDTO 생성

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@ToString
@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;
}
@Entity
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@ToString
@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;

}

 

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

●  파일 핸들러 생성

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


            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);
                } else{
                    fileDTO.setFileType(0);
                }

            } catch (Exception e){
                e.printStackTrace();
            }

            flist.add(fileDTO);
        }

        return flist;
    }
    private boolean isImageFile(File file) throws IOException {
        String mimeType = new Tika().detect(file);
        return mimeType.startsWith("image");
    }
}

 

● 컨트롤러에 파일핸들러를 final로 생성하고 

 private final FileHandler fileHandler;

 

BoardFileDTO에 보드객체와 파일객체의 리스트를 넣어서 생성해서 insert해줌 

    @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";
    }

 

● File Entity와 FileDTO를 서로 변환하는 메서드를 서비스에서 구현

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

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

 

● 파일을 포함하지않고 만들어놨던 insert를 이용해서 bno를 리턴받도록함 같은 insert지만 매개변수 타입이달라서 사용가능 bno가 0보다크고 FileDTO의 리스트가 null아니면 파일이 있는것이니 for 문으로 파일객체를 꺼내서 setBno로 bno를 설정한다음 파일 객체를 엔터티로 변환해서 save로 insert한다음 id를 bno로 리턴

    @Transactional
    @Override
    public long insert(BoardFileDTO boardFileDTO) {
        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;
    }

 

파일 표시

  상세페이지 화면에 이미지가 나오도록 표시라인 구성 

 	<div class="mb-3">
                <ul class="list-group list-group-flush">
                    <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">
                            <!--/* 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 text-success-emphasis">[[${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 btn-sm file-x" disabled>x</button>
                    </li>
                </ul>
            </div>

 

● 컨트롤러에서 보드, 파일 객체를 합친 BoardFileDTO를  bno받아 DB에서 가져와서 화면에 model로 전송

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

 

●  FileRepository에 id가아닌 bno로 값을 가져올 수 있게 추가 

public interface FileRepository extends JpaRepository<File, String> {

    List<File> findByBno(Long bno);
}

 

● BoarderiviceImpl에서 보드 객체를 optional로 가져와서 객체가 있다면 DTO로 변환해서 boardDTO에 저장

Repository에서 만들어둔걸로 파일 엔터티의 리스트를만들어서 bno를 주고 가져와 파일리스트에 저장

파일리스트를 stream.map으로펼쳐서 안에 파일엔터티를 DTO로 변경해서 리스트형식으로 바꿔 DTOList에 저장한 후 리턴

    @Override
    public BoardFileDTO getDetail(Long bno) {
        Optional<Board> optional = boardRepository.findById(bno);
        if(optional.isPresent()){
            BoardDTO boardDTO = convertEntityToDto(optional.get());
            List<File> fileList = fileRepository.findByBno(bno);
            List<FileDTO> fileDTOList = fileList.stream().map(f -> convertEntityToDto(f)).toList();
            BoardFileDTO boardFileDTO = new BoardFileDTO(boardDTO, fileDTOList);
            return boardFileDTO;
        }

        return null;
    }

 

파일 수정 / 삭제

 

● Detail  js파일에 파일 modify 버튼을누르면 삭제버튼의 disabled를 풀 수있게하고 삭제버튼을 눌렀을때 파일이 삭제하도록 구현추가.

    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')){
        let uuid = e.target.dataset.uuid;
        console.log(uuid);
        let li = e.target.closest('li');

        removeFileToServer(uuid).then(result => {
            if(result == "1"){
                li.remove();
                alert("파일삭제 성공!!");
            } else{
                alert("파일삭제 실패!!");
            }
        })
    }
})

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

 

● 삭제는 리턴값이 없으니까 임의로 만들어서 받을예정 컨트롤러에서 uuid를 받아 삭제하게 구현 

    @ResponseBody
    @DeleteMapping("/file/{uuid}")
    public String deleteFile(@PathVariable("uuid") String uuid){

        long bno = boardService.deleteFile(uuid);

        return bno > 0 ? "1" : "0";
    }

 

● 삭제하기전 삭제하는 파일의 bno를 가져오게 처리 삭제가 성공하면 bno를 리턴하고 삭제가 실패하면 0을 리턴하도록 함.

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

 

● 파일추가, 출력라인을 register에서 가져와서 detail에 넣어주고 

	<!--/* file 추가 라인 */-->
            <div class="mb-3">
                <label for="file" class="form-label">File</label>
                <input type="file" class="form-control" id="file" name="files" multiple style="display:none">
            </div>
            <button type="button" id="trigger" class="btn btn-primary" disabled>File Upload</button>

            <!--/* file 출력 라인 */-->
            <div class="mb-3" id="fileZone"></div>

 

스크립트도 추가해줌

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

 

● 수정버튼을 누르면 File Upload버튼이 활성화되도록하고 

  document.getElementById('trigger').disabled = false;

 

  Register에서 했던것과 똑같이 컨트롤러와 ServiceImpl에서 처리해주면됨.

    @PostMapping("/modify")
    public String modify(BoardDTO boardDTO, RedirectAttributes redirectAttributes,
                         @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.modify(new BoardFileDTO(boardDTO,flist));

        redirectAttributes.addAttribute("bno",boardDTO.getBno());
        return "redirect:/board/detail";
        
    }

 

- 보드랑 파일 둘다 업데이트 되어야하므로 optional을 사용해서 board 엔터티가 있다면 받아온 BoardFileDTO에서 

제목과 내용을 가지고와서 set해주고 파일 insert는 register와 똑같이 구현 

    @Transactional
    @Override
    public Long modify(BoardFileDTO boardFileDTO) {
        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;
    }

 

폴더에 있는 실제 파일 삭제 

 

● 파일 삭제 핸들러를 따로 생성해서 구현 

    private final String BASE_PATH = "D:\\_myProject\\_java\\_fileUpload\\";

    public int deleteFile(String saveDir, String uuid, String imageFileName){
        boolean isDel = false;
        File fileUuid = new File(uuid);
        File fileSaveDir = new File(saveDir);
        File removeFile = new File(BASE_PATH+fileSaveDir+File.separator+fileUuid+"_"+imageFileName);
        File removeThFile = new File(BASE_PATH+fileSaveDir+File.separator+fileUuid+"_th_"+imageFileName);

        if(removeFile.exists() || removeThFile.exists()) {
            isDel = removeFile.delete(); // 원래파일 삭제
            log.info(">>> removeFile !! > {} ", isDel);
            if(isDel) {
                isDel = removeThFile.delete();
                log.info(">>> removeThFile !! > {} ", isDel);
            }
        }

        return isDel ? 1 : 0 ;
    }

 

  파일이 삭제될 때 핸들러가 실행되도록 컨트롤러에 추가

    @ResponseBody
    @DeleteMapping("/file/{uuid}")
    public String deleteFile(@PathVariable("uuid") String uuid){

        FileDTO fileDTO = boardService.getFile(uuid);

        FileDeleteHandler fileDeleteHandler = new FileDeleteHandler();

        int delOk = fileDeleteHandler.deleteFile(fileDTO.getSaveDir(),fileDTO.getUuid(),fileDTO.getFileName());

        log.info(" delOk >> {} ", delOk);

        long bno = boardService.deleteFile(uuid);

        return bno > 0 ? "1" : "0";
    }

 

●  삭제할 파일을 가져올 수 있도록 ServiceImpl에서 구현 uuid를 받고 파일이있다면 가져와서 Entity를 DTO로 변환하고 리턴할 수 있도록 함.

    @Override
    public FileDTO getFile(String uuid) {

       Optional<File> optional = fileRepository.findById(uuid);
        if(optional.isPresent()){
            File file = optional.get();
            FileDTO fileDTO = convertEntityToDto(file);
            return fileDTO;
        }
        return null;
    }

'boot_JPA 수업 정리' 카테고리의 다른 글

5일  (0) 2024.11.26
Boot_JPA 4일차  (1) 2024.11.25
Boot_JPA 2일차  (3) 2024.11.21
Boot_JPA 1일  (1) 2024.11.20