09009

[Spring Boot] 게시판 연습 (7) - 파일 업로드 본문

Back-End/Spring
[Spring Boot] 게시판 연습 (7) - 파일 업로드
09009

save.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>save</title>
</head>
<body>
    <!-- action속성: 목적지(서버주소), method속성: http request method(get, post) -->
    <form action="/board/save" method="post" enctype="multipart/form-data">
        writer: <input type="text" name="boardWriter"> <br>
        pass: <input type="text" name="boardPass"> <br>
        title: <input type="text" name="boardTitle"> <br>
        contents: <textarea name="boardContents" cols="30" rows="10"></textarea> <br>
        file : <input type="file" name="boardFile"> <br>
        <input type="submit" value="글작성">
    </form>
</body>
</html>

 

 

BoardDTO

코드 추가

    private MultipartFile boardFile; // save.html -> controller에 파일을 담는 용도
    private String originalFileName; // 원본 파일 이름
    private String storedFileName; // 서버 저장용 파일 이름
    private int fileAttached; // 파일 첨부 여부 (첨부 1, 미첨부 0)

 

 

BoardController

코드 확인 (추가한 코드 없음)

    @PostMapping("/save")
    public String save(@ModelAttribute BoardDTO boardDTO) {
        System.out.println("boardDTO = " + boardDTO);
        boardService.save(boardDTO);
        return "index";
    }

 

BoardService

파일 업로드를 고려하지 않을 경우 코드

 

파일 업로드를 고려하여 코드 수정

 

수정해야할 부분 : toSaveEntity() - ctrl + b 클릭

BoardEntity

toSaveEntity() : 파일이 없는 경우 호출하는 메서드 - fileAttached 값에 0 부여

코드 추가

    @Column
    private int fileAttached; // 파일 있으면 1, 파일 없으면 0

    // dto에 담긴 값들을 entity 객체에 옮겨담는 작업
    public static BoardEntity toSaveEntity(BoardDTO boardDTO) {
        BoardEntity boardEntity = new BoardEntity();
        boardEntity.setBoardWriter(boardDTO.getBoardWriter());
        boardEntity.setBoardPass(boardDTO.getBoardPass());
        boardEntity.setBoardTitle(boardDTO.getBoardTitle());
        boardEntity.setBoardContents(boardDTO.getBoardContents());
        boardEntity.setBoardHits(0); // 조회수는 기본으로 0이니까
        boardEntity.setFileAttached(0); // 파일 없음.
        return boardEntity;
    }

 

BoardService

파일이 첨부될 경우 로직 구성 (else문)

* 게시글 정보는 board_table에 저장, 해당 파일 정보는 board_file_table에 저장한다.

 

일단 여기까지 작성

    public void save(BoardDTO boardDTO) throws IOException {
        // 파일 첨부 여부에 따라 로직 분리, MultipartFile 인터페이스에서 isEmpty() 메서드 제공
        if (boardDTO.getBoardFile().isEmpty()) {
            // 첨부 파일 없음.
            BoardEntity boardEntity = BoardEntity.toSaveEntity(boardDTO);
            boardRepository.save(boardEntity);
        } else {
            // 첨부 파일 있음.

            // 1. DTO에 담긴 파일을 꺼냄
            MultipartFile boardFile = boardDTO.getBoardFile();

            // 2. 파일의 이름 가져옴 (실제 사용자가 올린 파일 이름)
            String originalFilename = boardFile.getOriginalFilename();

            // 3. 서버 저장용 이름으로 수정 : 내 사진.jpg --> 8423424252525_내사진.jpg (currentTimeMills() -> 이거 대신 UUID도 사용가능)
            String storedFileName = System.currentTimeMillis() + "_" + originalFilename;

            // 4. 저장 경로 설정 (해당 폴더는 미리 만들어진 상태여야 한다.)
            String savePath = "C:/springboot_img/" + storedFileName;

            // 5. 해당 경로에 파일 저장 
            boardFile.transferTo(new File(savePath));

            // 6. board_table에 해당 데이터 save 처리

            // 7. board_file_table에 해당 데이터 save 처리

        }

    }

 

 

BoardController

    @PostMapping("/save")
    public String save(@ModelAttribute BoardDTO boardDTO) throws IOException {
        System.out.println("boardDTO = " + boardDTO);
        boardService.save(boardDTO);
        return "index";
    }

 

테이블 참고

create table board_table
(
    id             bigint auto_increment primary key,
    created_time   datetime     null,
    updated_time   datetime     null,
    board_contents varchar(500) null,
    board_hits     int          null,
    board_pass     varchar(255) null,
    board_title    varchar(255) null,
    board_writer   varchar(20)  not null,
    file_attached  int          null
);

create table board_file_table
(
    id                 bigint auto_increment primary key,
    created_time       datetime     null,
    updated_time       datetime     null,
    original_file_name varchar(255) null,
    stored_file_name   varchar(255) null,
    board_id           bigint       null,
    constraint FKcfxqly70ddd02xbou0jxgh4o3
    foreign key (board_id) references board_table (id) on delete cascade
);

 

BoardFileEntity 생성

- 게시글 하나에 파일이 여러 개 올 수 있다. 게시글과 파일의 관계는 1:N, 파일의 기준에서는 게시글과 N:1 관계가 된다.

FetchType.LAZY - fetchtype은 주로 이걸 사용. 

package com.yyi.board.entity;

import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;

@Entity
@Getter
@Setter
@SequenceGenerator(
        name = "board_file_seq_generator"
        , sequenceName = "board_file_seq"
        , initialValue = 1
        , allocationSize = 1
)
@Table(name = "board_file_table")
public class BoardFileEntity extends BaseEntity {
    @Id
    @GeneratedValue(
            strategy = GenerationType.SEQUENCE
            , generator = "board_file_seq_generator"
    )
    private Long id;

    @Column
    private String originalFileName;

    @Column
    private String storedFileName;

    // 파일의 기준에서는 게시글과 N:1 관계가 된다.
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "board_id")
    // 반드시 부모 엔터티 타입으로 작성해야 한다. 실제 DB에 들어갈 때는 id값만 들어감
    private BoardEntity boardEntity;


}

 

BoardEntity

코드 추가

 @OneToMany(mappedBy = "boardEntity", cascade = CascadeType.REMOVE, orphanRemoval = true, fetch =  FetchType.LAZY)
 private List<BoardFileEntity> boardFileEntityList = new ArrayList<>();

 

application.yml

ddl-auto: update → create으로 일단 변경 (테이블이 다시 만들어지면서 보이도록 하기 위함 ?)

 

 

DB 저장 처리

BoardService

     // 6. board_table에 해당 데이터 save 처리
    BoardEntity boardEntity = BoardEntity.toSaveFileEntity(boardDTO);

 

 

BoardEntity

코드 추가

    public static BoardEntity toSaveFileEntity(BoardDTO boardDTO) {
        BoardEntity boardEntity = new BoardEntity();
        boardEntity.setBoardWriter(boardDTO.getBoardWriter());
        boardEntity.setBoardPass(boardDTO.getBoardPass());
        boardEntity.setBoardTitle(boardDTO.getBoardTitle());
        boardEntity.setBoardContents(boardDTO.getBoardContents());
        boardEntity.setBoardHits(0); // 조회수는 기본으로 0이니까
        boardEntity.setFileAttached(1); // 파일 있음.
        return boardEntity;
    }

 

BoardService

           // 6. board_table에 해당 데이터 save 처리
            BoardEntity boardEntity = BoardEntity.toSaveFileEntity(boardDTO); // entity 형태로 변환
            // id 값을 얻어오는 이유: 자식 테이블 입장에서 부모가 어떤 id(pk)인지 필요해서
            Long savedId = boardRepository.save(boardEntity).getId(); // 변환 후 저장 --> id값을 얻어온다.
            BoardEntity board = boardRepository.findById(savedId).get(); // 부모 엔터티를 db로부터 가져옴.

 

이제, BoardFileEntity 객체로 변환해주는 작업이 필요하다.

BoardFileEntity

코드 추가

    public static BoardFileEntity toBoardFileEntity(BoardEntity boardEntity, String originalFileName, String storedFileName) {
        BoardFileEntity boardFileEntity = new BoardFileEntity();
        boardFileEntity.setOriginalFileName(originalFileName);
        boardFileEntity.setStoredFileName(storedFileName);
        boardFileEntity.setBoardEntity(boardEntity);
        return boardFileEntity;
    }

 

!! 주의 : pk값이 아닌 부모 엔터티 객체를 넘겨줘야 한다. 

(boardFileEntity.setBoardEntity(boardEntity);)

 

BoardService

코드 추가

BoardFileEntity boardFileEntity = BoardFileEntity.toBoardFileEntity(board, originalFilename, storedFileName);

 

그 후, 저장 작업을 해야 한다. --> BoardFileRepository 생성

BoardFileRepository

package com.yyi.board.repository;

import com.yyi.board.entity.BoardFileEntity;
import org.springframework.data.jpa.repository.JpaRepository;

public interface BoardFileRepository extends JpaRepository<BoardFileEntity, Long> {
    
}

 

주입 작업

BoardService

코드 추가

private final BoardFileRepository boardFileRepository;
// 6. board_table에 해당 데이터 save 처리
BoardEntity boardEntity = BoardEntity.toSaveFileEntity(boardDTO); // entity 형태로 변환
// id 값을 얻어오는 이유: 자식 테이블 입장에서 부모가 어떤 id(pk)인지 필요해서
Long savedId = boardRepository.save(boardEntity).getId(); // 변환 후 저장 --> id값을 얻어온다.
BoardEntity board = boardRepository.findById(savedId).get(); // 부모 엔터티를 db로부터 가져옴.

BoardFileEntity boardFileEntity = BoardFileEntity.toBoardFileEntity(board, originalFilename, storedFileName);
boardFileRepository.save(boardFileEntity);

 

이제, db에 저장까지 한 것이다.

 

 

이제 저장된 파일을 화면에 이미지를 띄워주는 작업을 해야한다.

BoardDTO

코드 추가

 

detail.html

코드 추가

    <tr>
        <th>image</th>
        <td><img th:src="@{|/upload/${board.storedFileName}|}" alt=""></td>
    </tr>

 

BoardDTO

코드 수정

    public static BoardDTO toBoardDTO(BoardEntity boardEntity) {
        BoardDTO boardDTO = new BoardDTO();
        boardDTO.setId(boardEntity.getId());
        boardDTO.setBoardWriter(boardEntity.getBoardWriter());
        boardDTO.setBoardPass(boardEntity.getBoardPass());
        boardDTO.setBoardTitle(boardEntity.getBoardTitle());
        boardDTO.setBoardContents(boardEntity.getBoardContents());
        boardDTO.setBoardHits(boardEntity.getBoardHits());
        boardDTO.setBoardCreatedTime(boardEntity.getCreatedTime());
        boardDTO.setBoardUpdatedTime(boardEntity.getUpdatedTime());
        if (boardEntity.getFileAttached() == 0) {
            boardDTO.setFileAttached(boardEntity.getFileAttached()); // 0
        } else {
            boardDTO.setFileAttached(boardEntity.getFileAttached()); // 1
            boardDTO.setOriginalFileName(boardEntity.getBoardFileEntityList().get(0).getOriginalFileName());
            boardDTO.setStoredFileName(boardEntity.getBoardFileEntityList().get(0).getStoredFileName());
        }
        return boardDTO;
    }

 

!! 부모 엔터티에서 자식 엔터티로 접근 할 때는 내용을 호출하는 메서드에서 반드시 @Transactional을 꼭 붙여줘야 한다.

BoardService

findById()에서 toBoardDTO()를 호출하고 있다. 그런데, toBoardDTO() 안에서 boardEntity(부모)가 boardFileEntity(자식)에 접근하고 있기 때문에 @Transactional 붙여줘야 한다.

    @Transactional
    public BoardDTO findById(Long id) {
        Optional<BoardEntity> optionalBoardEntity = boardRepository.findById(id);
        if (optionalBoardEntity.isPresent()) {
            BoardEntity boardEntity = optionalBoardEntity.get();
            BoardDTO boardDTO = BoardDTO.toBoardDTO(boardEntity);
            return boardDTO;
        } else {
            return null;
        }
    }
    @Transactional
    public List<BoardDTO> findAll() {
        // findAll -> repository로 무언가를 가져올 때 대부분 entity 형태로 가져온다.
        List<BoardEntity> boardEntityList = boardRepository.findAll();
        // entity로 넘어온 객체를 dto 객체로 옮겨담아서 컨트롤러로 return 해주어야 한다.

        // 1. return할 객체 먼저 선언하기
        List<BoardDTO> boardDTOList = new ArrayList<>();

        // 2. boardEntityList에 담긴 데이터를 boardDTOList에 옮긴다 -> entity를 dto로 변환하는 메서드 생성해야함
        for (BoardEntity boardEntity : boardEntityList) {
            boardDTOList.add(BoardDTO.toBoardDTO(boardEntity));
        }

        return boardDTOList;
    }

 

detail.html

경로 접근 폴더 설정 - upload 

 

config 패키지 추가

 

WebConfig

package com.yyi.board.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 WebConfig implements WebMvcConfigurer {
    // 의미 : savePath에 있는 경로를 resourcePath에 적힌 이름으로 접근하겠다는 설정
    private String resourcePath = "/upload/**"; // view에서 접근할 경로
    private String savePath = "file:///C:/springboot_img/"; // 실제 파일 저장 경로

    // view에서 upload 경로로 접근하면 spring이 springboot_img 경로에서 해당 파일을 찾아주도록 설정
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler(resourcePath)
                .addResourceLocations(savePath);
    }


}

 

detail.html

코드 수정

    <tr th:if="${board.fileAttached == 1}">
        <th>image</th>
        <td><img th:src="@{|/upload/${board.storedFileName}|}" alt=""></td>
    </tr>

다중파일 첨부

 

save.html

multiple 속성 추가

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>save</title>
</head>
<body>
    <!-- action속성: 목적지(서버주소), method속성: http request method(get, post) -->
    <form action="/board/save" method="post" enctype="multipart/form-data">
        writer: <input type="text" name="boardWriter"> <br>
        pass: <input type="text" name="boardPass"> <br>
        title: <input type="text" name="boardTitle"> <br>
        contents: <textarea name="boardContents" cols="30" rows="10"></textarea> <br>
        file : <input type="file" name="boardFile" multiple> <br>
        <input type="submit" value="글작성">
    </form>
</body>
</html>

 

BoardDTO

파일이 여러 개일 경우 코드 수정 --> List 적용

private List<MultipartFile> boardFile;

 

BoardController 코드 확인

수정할 필요 x

    @PostMapping("/save")
    public String save(@ModelAttribute BoardDTO boardDTO) throws IOException {
        System.out.println("boardDTO = " + boardDTO);
        boardService.save(boardDTO);
        return "index";
    }

 

BoardService

코드 수정

같은 부모를 가진 자식이 여러 개 있을 수 있음.

--> 부모 데이터가 먼저 저장이 되어야한다.

그래서, 아래 스크린샷에 빨간색으로 표시한 내용이 먼저 작성되어야 한다.

    public void save(BoardDTO boardDTO) throws IOException {
        // 파일 첨부 여부에 따라 로직 분리, MultipartFile 인터페이스에서 isEmpty() 메서드 제공
        if (boardDTO.getBoardFile().isEmpty()) {
            // 첨부 파일 없음.
            BoardEntity boardEntity = BoardEntity.toSaveEntity(boardDTO);
            boardRepository.save(boardEntity);
        } else {
            // 첨부 파일 있음.
            // 6. board_table에 해당 데이터 save 처리
            BoardEntity boardEntity = BoardEntity.toSaveFileEntity(boardDTO); // entity 형태로 변환
            // id 값을 얻어오는 이유: 자식 테이블 입장에서 부모가 어떤 id(pk)인지 필요해서
            Long savedId = boardRepository.save(boardEntity).getId(); // 변환 후 저장 --> id값을 얻어온다.
            // 부모 객체 가져오기
            BoardEntity board = boardRepository.findById(savedId).get(); // 부모 엔터티를 db로부터 가져옴.
            
            // 파일이 여러 개인 상황이므로 for문 작성
            for (MultipartFile boardFile : boardDTO.getBoardFile()) {
                // 1. DTO에 담긴 파일을 꺼냄
//                MultipartFile boardFile = boardDTO.getBoardFile(); 이 문장은 for 반복문에서 실행되므로 삭제

                // 2. 파일의 이름 가져옴 (실제 사용자가 올린 파일 이름)
                String originalFilename = boardFile.getOriginalFilename();

                // 3. 서버 저장용 이름으로 수정 : 내 사진.jpg --> 8423424252525_내사진.jpg (currentTimeMills() -> 이거 대신 UUID도 사용가능)
                String storedFileName = System.currentTimeMillis() + "_" + originalFilename;

                // 4. 저장 경로 설정 (해당 폴더는 미리 만들어진 상태여야 한다.)
                String savePath = "C:/springboot_img/" + storedFileName;

                // 5. 해당 경로에 파일 저장
                boardFile.transferTo(new File(savePath));

                // 7. board_file_table에 해당 데이터 save 처리
                BoardFileEntity boardFileEntity = BoardFileEntity.toBoardFileEntity(board, originalFilename, storedFileName);
                boardFileRepository.save(boardFileEntity);
            }
        }
    }

 

파일을  가져올 때 코드 수정

BoardDTO

    private List<String> originalFileName; // 원본 파일 이름
    private List<String> storedFileName; // 서버 저장용 파일 이름
    public static BoardDTO toBoardDTO(BoardEntity boardEntity) {
        BoardDTO boardDTO = new BoardDTO();
        boardDTO.setId(boardEntity.getId());
        boardDTO.setBoardWriter(boardEntity.getBoardWriter());
        boardDTO.setBoardPass(boardEntity.getBoardPass());
        boardDTO.setBoardTitle(boardEntity.getBoardTitle());
        boardDTO.setBoardContents(boardEntity.getBoardContents());
        boardDTO.setBoardHits(boardEntity.getBoardHits());
        boardDTO.setBoardCreatedTime(boardEntity.getCreatedTime());
        boardDTO.setBoardUpdatedTime(boardEntity.getUpdatedTime());
        if (boardEntity.getFileAttached() == 0) {
            boardDTO.setFileAttached(boardEntity.getFileAttached()); // 0
        } else {
            List<String> originalFileNameList = new ArrayList<>();
            List<String> storedFileNameList = new ArrayList<>();
            
            boardDTO.setFileAttached(boardEntity.getFileAttached()); // 1
            
            for (BoardFileEntity boardFileEntity : boardEntity.getBoardFileEntityList()) {
               originalFileNameList.add(boardFileEntity.getOriginalFileName());
               storedFileNameList.add(boardFileEntity.getStoredFileName());
            }
            boardDTO.setOriginalFileName(originalFileNameList);
            boardDTO.setStoredFileName(storedFileNameList);
        }
        return boardDTO;
    }

 

 

detail.html

파일이 여러 개일 때 반복문으로 수정

    <tr th:if="${board.fileAttached == 1}">
        <th>image</th>
        <td th:each="fileName : ${board.storedFileName}">
            <img th:src="@{|/upload/${fileName}|}" alt="">
        </td>
    </tr>

 

 

ddl-auto 다시 update로 변경하기 !!