diff --git a/backend/backup.sql b/backend/backup.sql index fb3a341c..e382cc35 100644 --- a/backend/backup.sql +++ b/backend/backup.sql @@ -239,7 +239,7 @@ CREATE TABLE IF NOT EXISTS `board` ( `content` text, `view_count` int DEFAULT '0', `writer` varchar(255) DEFAULT NULL, - `file` varchar(255) DEFAULT NULL, + `fileList` TEXT DEFAULT NULL, `create_date` datetime DEFAULT CURRENT_TIMESTAMP, `category` varchar(50) DEFAULT NULL, `department_id` bigint DEFAULT NULL, diff --git a/backend/src/main/java/org/example/backend/board/controller/BoardController.java b/backend/src/main/java/org/example/backend/board/controller/BoardController.java index 483647f5..57e69895 100644 --- a/backend/src/main/java/org/example/backend/board/controller/BoardController.java +++ b/backend/src/main/java/org/example/backend/board/controller/BoardController.java @@ -19,7 +19,9 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestPart; import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; @RestController @RequiredArgsConstructor @@ -29,9 +31,10 @@ public class BoardController { private final BoardService boardService; @Operation(summary = "게시판 생성 API 입니다.", description = "게시판 생성입니다.") - @PostMapping - public ResponseEntity createBoard(@RequestBody BoardReqDto boardReqDto) { - Long boardId = boardService.saveBoard(boardReqDto); + @PostMapping(consumes = "multipart/form-data") + public ResponseEntity createBoard(@RequestPart(value = "boardReqDto") BoardReqDto boardReqDto, + @RequestPart(value = "boardFiles") List multipartFileList) { + Long boardId = boardService.saveBoard(boardReqDto, multipartFileList); return new ResponseEntity<>(boardId, HttpStatus.OK); } @@ -60,8 +63,9 @@ public ResponseEntity getBoard(@PathVariable(name = "boardId") Long @Operation(summary = "게시판 정보 업데이트 API", description = "게시판 정보 업데이트") @PostMapping("/{boardId}") public ResponseEntity updateBoard(@PathVariable(name = "boardId") Long boardId, - @RequestBody BoardReqDto boardReqDto) { - BoardResDto boardResDto = boardService.updateBoard(boardId, boardReqDto); + @RequestPart(value = "boardReqDto") BoardReqDto boardReqDto, + @RequestPart(value = "boardFiles") List multipartFileList) { + BoardResDto boardResDto = boardService.updateBoard(boardId, boardReqDto, multipartFileList); return new ResponseEntity<>(boardResDto, HttpStatus.OK); } diff --git a/backend/src/main/java/org/example/backend/board/domain/dto/BoardReqDto.java b/backend/src/main/java/org/example/backend/board/domain/dto/BoardReqDto.java index 9a5e70ed..c188b1d1 100644 --- a/backend/src/main/java/org/example/backend/board/domain/dto/BoardReqDto.java +++ b/backend/src/main/java/org/example/backend/board/domain/dto/BoardReqDto.java @@ -1,5 +1,6 @@ package org.example.backend.board.domain.dto; +import java.util.List; import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; @@ -11,16 +12,20 @@ public class BoardReqDto { private String title; private String content; private String writer; - private String file; + private List fileList; private String category; @Builder private BoardReqDto(String title, String content, String writer, - String file, String category) { + List fileList, String category) { this.title = title; this.content = content; this.writer = writer; - this.file = file; + this.fileList = fileList; this.category = category; } + + public void setFileList(List fileList) { + this.fileList = fileList; + } } \ No newline at end of file diff --git a/backend/src/main/java/org/example/backend/board/domain/dto/BoardResDto.java b/backend/src/main/java/org/example/backend/board/domain/dto/BoardResDto.java index 0438b7d5..2fb55259 100644 --- a/backend/src/main/java/org/example/backend/board/domain/dto/BoardResDto.java +++ b/backend/src/main/java/org/example/backend/board/domain/dto/BoardResDto.java @@ -1,6 +1,7 @@ package org.example.backend.board.domain.dto; import java.time.LocalDateTime; +import java.util.List; import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; @@ -14,18 +15,18 @@ public class BoardResDto { private String title; private String content; private String writer; - private String file; + private List fileList; private LocalDateTime createDate; private String category; @Builder private BoardResDto(Long id, String title, String content, String writer, - String file, LocalDateTime createDate, String category) { + List fileList, LocalDateTime createDate, String category) { this.id = id; this.title = title; this.content = content; this.writer = writer; - this.file = file; + this.fileList = fileList; this.createDate = createDate; this.category = category; } @@ -36,7 +37,7 @@ public static BoardResDto of(Board board) { .title(board.getTitle()) .content(board.getContent()) .writer(board.getWriter()) - .file(board.getFile()) + .fileList(board.getFileList()) .createDate(board.getCreatedDateTime()) .category(board.getCategory().name()) .build(); diff --git a/backend/src/main/java/org/example/backend/board/domain/entity/Board.java b/backend/src/main/java/org/example/backend/board/domain/entity/Board.java index 418151e5..7d5699eb 100644 --- a/backend/src/main/java/org/example/backend/board/domain/entity/Board.java +++ b/backend/src/main/java/org/example/backend/board/domain/entity/Board.java @@ -1,12 +1,14 @@ package org.example.backend.board.domain.entity; import jakarta.persistence.*; +import java.util.List; import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import org.example.backend.board.domain.dto.BoardReqDto; import org.example.backend.global.config.BaseEntity; +import org.example.backend.global.config.StringListConverter; @Entity @Getter @@ -30,8 +32,9 @@ public class Board extends BaseEntity { @Column(name = "writer") private String writer; - @Column(name = "file") - private String file; + @Convert(converter = StringListConverter.class) + @Column(name = "file_list", length = 1000) + private List fileList; @Enumerated(EnumType.STRING) @Column(name = "category") @@ -39,11 +42,11 @@ public class Board extends BaseEntity { @Builder private Board(String title, String content, String writer, - String file, Category category) { + List fileList, Category category) { this.title = title; this.content = content; this.writer = writer; - this.file = file; + this.fileList = fileList; this.category = category; this.viewCount = 0; } @@ -53,7 +56,7 @@ public static Board of(BoardReqDto dto) { .title(dto.getTitle()) .content(dto.getContent()) .writer(dto.getWriter()) - .file(dto.getFile()) + .fileList(dto.getFileList()) .category(Category.valueOf(dto.getCategory())) .build(); } @@ -62,7 +65,7 @@ public void update(BoardReqDto dto) { this.title = dto.getTitle(); this.content = dto.getContent(); this.writer = dto.getWriter(); - this.file = dto.getFile(); + this.fileList = dto.getFileList(); this.category = Category.valueOf(dto.getCategory()); } } \ No newline at end of file diff --git a/backend/src/main/java/org/example/backend/board/exception/BoardExceptionType.java b/backend/src/main/java/org/example/backend/board/exception/BoardExceptionType.java index cb1c806d..b1a278c0 100644 --- a/backend/src/main/java/org/example/backend/board/exception/BoardExceptionType.java +++ b/backend/src/main/java/org/example/backend/board/exception/BoardExceptionType.java @@ -13,6 +13,7 @@ public enum BoardExceptionType implements BaseExceptionType { REQUIRED_TITLE(BAD_REQUEST, "제목은 필수 입력값입니다."), REQUIRED_CONTENT(BAD_REQUEST, "내용은 필수 입력값입니다."), REQUIRED_DEPARTMENT_ID(BAD_REQUEST, "부서 ID는 필수 입력값입니다."), + REQUIRED_FILE(BAD_REQUEST, "파일이 비어 있습니다.") ; private final HttpStatus httpStatus; diff --git a/backend/src/main/java/org/example/backend/board/service/BoardService.java b/backend/src/main/java/org/example/backend/board/service/BoardService.java index 84b54716..7f00e8b8 100644 --- a/backend/src/main/java/org/example/backend/board/service/BoardService.java +++ b/backend/src/main/java/org/example/backend/board/service/BoardService.java @@ -2,6 +2,8 @@ import static org.example.backend.board.exception.BoardExceptionType.NOT_FOUND_BOARD; +import java.util.ArrayList; +import java.util.List; import lombok.RequiredArgsConstructor; import org.example.backend.board.domain.dto.BoardReqDto; import org.example.backend.board.domain.dto.BoardResDto; @@ -10,20 +12,27 @@ import org.example.backend.board.exception.BoardException; import org.example.backend.board.exception.BoardExceptionType; import org.example.backend.board.repository.BoardRepository; +import org.example.backend.global.config.S3Uploader; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; @Service @RequiredArgsConstructor @Transactional(readOnly = true) public class BoardService { private final BoardRepository boardRepository; + private final S3Uploader s3Uploader; + private static final String dirName = "image"; @Transactional - public Long saveBoard(BoardReqDto boardReqDto) { + public Long saveBoard(BoardReqDto boardReqDto, List multipartFileList) { validateUserRequiredFields(boardReqDto); + + fileUpload(boardReqDto, multipartFileList); + Board board = Board.of(boardReqDto); Board savedBoard = boardRepository.save(board); return savedBoard.getId(); @@ -54,7 +63,8 @@ public Page getBoardsByCategory(Category category, Pageable pageabl } @Transactional - public BoardResDto updateBoard(Long boardId, BoardReqDto boardReqDto) { + public BoardResDto updateBoard(Long boardId, BoardReqDto boardReqDto, List multipartFileList) { + fileUpload(boardReqDto, multipartFileList); Board board = findBoardById(boardId); board.update(boardReqDto); return BoardResDto.of(board); @@ -70,4 +80,18 @@ private Board findBoardById(Long boardId) { return boardRepository.findById(boardId) .orElseThrow(() -> new BoardException(NOT_FOUND_BOARD)); } + + private void fileUpload(BoardReqDto boardReqDto, List multipartFileList) { + List updateImageUrlList = new ArrayList<>(); + if (!multipartFileList.isEmpty()) { + for (MultipartFile multipartFile : multipartFileList) { + if (multipartFile.isEmpty()) { + throw new BoardException(BoardExceptionType.REQUIRED_FILE); + } + String uploadImageUrl = s3Uploader.upload(multipartFile, dirName); + updateImageUrlList.add(uploadImageUrl); + } + boardReqDto.setFileList(updateImageUrlList); + } + } } \ No newline at end of file diff --git a/backend/src/main/java/org/example/backend/global/config/StringListConverter.java b/backend/src/main/java/org/example/backend/global/config/StringListConverter.java new file mode 100644 index 00000000..95a05170 --- /dev/null +++ b/backend/src/main/java/org/example/backend/global/config/StringListConverter.java @@ -0,0 +1,31 @@ +package org.example.backend.global.config; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; +import java.util.List; + +@Converter +public class StringListConverter implements AttributeConverter, String> { + + private final ObjectMapper mapper = new ObjectMapper(); + + @Override + public String convertToDatabaseColumn(List dataList) { + try { + return mapper.writeValueAsString(dataList); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + + @Override + public List convertToEntityAttribute(String data) { + try { + return mapper.readValue(data, List.class); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } +} \ No newline at end of file diff --git a/backend/src/main/resources/data.sql b/backend/src/main/resources/data.sql index ce428e62..81703fa3 100644 --- a/backend/src/main/resources/data.sql +++ b/backend/src/main/resources/data.sql @@ -58,15 +58,15 @@ VALUES -- Board 더미 데이터 (10개) -INSERT INTO board (title, content, view_count, writer, file, category) +INSERT INTO board (title, content, view_count, writer, file_list, category) VALUES - ('첫 번째 게시글', '게시글 내용 1', 10, '작성자1', 'file1.txt', 'undergraduate'), - ('두 번째 게시글', '게시글 내용 2', 20, '작성자2', 'file2.txt', 'graduate'), - ('세 번째 게시글', '게시글 내용 3', 30, '작성자3', 'file3.txt', 'employment'), - ('네 번째 게시글', '게시글 내용 4', 40, '작성자4', 'file4.txt', 'scholarship'), - ('다섯 번째 게시글', '게시글 내용 5', 50, '작성자5', 'file5.txt', 'undergraduate'), - ('여섯 번째 게시글', '게시글 내용 6', 60, '작성자6', 'file6.txt', 'graduate'), - ('일곱 번째 게시글', '게시글 내용 7', 70, '작성자7', 'file7.txt', 'employment'), - ('여덟 번째 게시글', '게시글 내용 8', 80, '작성자8', 'file8.txt', 'scholarship'), - ('아홉 번째 게시글', '게시글 내용 9', 90, '작성자9', 'file9.txt', 'undergraduate'), - ('열 번째 게시글', '게시글 내용 10', 100, '작성자10', 'file10.txt','graduate'); + ('첫 번째 게시글', '게시글 내용 1', 10, '작성자1', '["file1.txt", "file2.txt"]', 'undergraduate'), + ('두 번째 게시글', '게시글 내용 2', 20, '작성자2', '["file3.txt", "file4.txt"]', 'graduate'), + ('세 번째 게시글', '게시글 내용 3', 30, '작성자3', '["file5.txt", "file6.txt"]', 'employment'), + ('네 번째 게시글', '게시글 내용 4', 40, '작성자4', '["file7.txt", "file8.txt"]', 'scholarship'), + ('다섯 번째 게시글', '게시글 내용 5', 50, '작성자5', '["file9.txt", "file10.txt"]', 'undergraduate'), + ('여섯 번째 게시글', '게시글 내용 6', 60, '작성자6', '["file11.txt", "file12.txt"]', 'graduate'), + ('일곱 번째 게시글', '게시글 내용 7', 70, '작성자7', '["file13.txt", "file14.txt"]', 'employment'), + ('여덟 번째 게시글', '게시글 내용 8', 80, '작성자8', '["file15.txt", "file16.txt"]', 'scholarship'), + ('아홉 번째 게시글', '게시글 내용 9', 90, '작성자9', '["file17.txt", "file18.txt"]', 'undergraduate'), + ('열 번째 게시글', '게시글 내용 10', 100, '작성자10', '["file19.txt", "file20.txt"]', 'graduate');