09009
[Spring Boot] 게시판 연습 (7) - 파일 업로드 본문
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로 변경하기 !!
'Back-End > Spring' 카테고리의 다른 글
[Spring Boot] 게시판 연습 (9) - 댓글 DB 저장 (0) | 2024.01.25 |
---|---|
[Spring Boot] 게시판 연습 (8) - 댓글 작성 내용 서버로 요청하기 (0) | 2024.01.25 |
[Spring Boot] 게시판 연습 (6) - 페이징 처리 (0) | 2024.01.23 |
[Spring Boot] 게시판 연습 (5) - 게시글 수정 및 삭제 (0) | 2024.01.23 |
[Spring Boot] 게시판 연습 (4) - 게시글 상세조회 (0) | 2024.01.23 |