๐Ÿ’ป ์ฝ”๋”ฉ ์ผ๊ธฐ : [์Šคํ”„๋ง ๊ฒŒ์‹œํŒ with React] '๊ฒŒ์‹œํŒ ํŒŒ์ผ ์ฒจ๋ถ€' ํŽธ

ybkยท2024๋…„ 5์›” 28์ผ

spring

๋ชฉ๋ก ๋ณด๊ธฐ
44/55
post-thumbnail

๐Ÿ”” '๊ฒŒ์‹œํŒ ํŒŒ์ผ ์ฒจ๋ถ€ CRUD'์— ๋Œ€ํ•ด์„œ ์•Œ์•„๋ณด์ž!


๐Ÿ’Ÿ ๊ฒŒ์‹œํŒ ํŒŒ์ผ ์ฒจ๋ถ€ํ•˜๊ธฐ(CREATE)


BoardWriter.java(React)

  const [files, setFiles] = useState([]);

        <Box mt={"30px"}>
          <FormControl>
            <FormLabel>ํŒŒ์ผ</FormLabel>
            <Input
              multiple={true}
              type={"file"}
              accept={"image/*"}
              onChange={(e) => {
                setFiles(e.target.files);
              }}
            />
            <FormHelperText color={"red"}>
              ์ด ์šฉ๋Ÿ‰์€ 10MB ํ•œ ํŒŒ์ผ์€ 1MB๋ฅผ ์ดˆ๊ณผํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.
            </FormHelperText>
          </FormControl>
        </Box>
        <Box>
          <ul>{fileNameList}</ul>
        </Box>
  • multiple : ์—ฌ๋Ÿฌ ํŒŒ์ผ ์„ ํƒ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.
  • accept : ํŒŒ์ผ ์œ ํ˜• ์ง€์ •ํ•ฉ๋‹ˆ๋‹ค.
  • onChange{} : ํŒŒ์ผ์„ ์„ ํƒํ•˜๋ฉด ์„ ํƒ๋œ ํŒŒ์ผ๋“ค์„ files ์ƒํƒœ ๋ณ€์ˆ˜์— ์—…๋ฐ์ดํŠธ ํ•ฉ๋‹ˆ๋‹ค.

// file ๋ชฉ๋ก ์ž‘์„ฑ
const fileNameList = [];
for (let i = 0; i < files.length; i++) {
  fileNameList.push(<li>{files[i].name}</li>);
}
  • <input type="file">์—์„œ ๋ฐ˜ํ™˜๋˜๋Š” FileList ๊ฐ์ฒด์—๋Š” map ํ•จ์ˆ˜๊ฐ€ ์—†์–ด์„œ ํŒŒ์ผ ๋ชฉ๋ก ๋ฐฐ์—ด(fileNameList)์„ ์ƒˆ๋กœ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.
  • ํŒŒ์ผ์˜ ๋ชฉ๋ก์„ ํ™”๋ฉด์— ํ‘œ์‹œํ•˜๊ธฐ ์œ„ํ•ด files์•ˆ์— ์žˆ๋Š” ํŒŒ์ผ๋“ค์„ fileNameList์— ๋„ฃ์–ด์ค๋‹ˆ๋‹ค.

    axios
      .postForm("/api/board/add", {
        title,
        content,
        files,
      })
  • ํŒŒ์ผ์„ ๋ณด๋‚ผ ๋•Œ multipart/form-data๋กœ ์ „์†กํ•ด์•ผ ํ•˜๊ธฐ ๋•Œ๋ฌธ์— axios.postForm()์œผ๋กœ ์„œ๋ฒ„์—๊ฒŒ ์š”์ฒญํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

BoardController.java

@PostMapping("add")
@PreAuthorize("isAuthenticated()")
public ResponseEntity add(Authentication authentication,
                          Board board, @RequestParam(value = "files[]", required = false) MultipartFile[] files) throws IOException {

    if (service.validate(board)) {
        service.add(board, files, authentication);
        return ResponseEntity.ok().build();
    } else {
        return ResponseEntity.badRequest().build();
    }
}
  • Board์—์„œ ์š”์ฒญ ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ๋ฐ”์ธ๋”ฉํ•ด์ฃผ๊ธฐ ๋•Œ๋ฌธ์— ๋”ฐ๋กœ ์–ด๋…ธํ…Œ์ด์…˜์„ ์ •์˜ํ•  ํ•„์š”๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.
  • ํŒŒ์ผ์„ ์„ ํƒํ•˜์ง€ ์•Š์„ ์ˆ˜๋„ ์žˆ์œผ๋‹ˆ reqired = false๋กœ ์ง€์ •ํ•ฉ๋‹ˆ๋‹ค.

BoardService.java

public void add(Board board, MultipartFile[] files, Authentication authentication) throws IOException {
    board.setMemberId(Integer.valueOf(authentication.getName()));
    mapper.insert(board); //๊ฒŒ์‹œ๋ฌผ ์ €์žฅ ๋จผ์ €

    if (files != null) {
        for (MultipartFile file : files) {
            //DB์— ํ•ด๋‹น ๊ฒŒ์‹œ๋ฌผ์˜ ํŒŒ์ผ ๋ชฉ๋ก ์ €์žฅ
            mapper.insertFileName(board.getId(), file.getOriginalFilename());

            // ์‹ค์ œ ํŒŒ์ผ ์ €์žฅ
            // ๋ถ€๋ชจ ๋””๋ ‰ํ† ๋ฆฌ ๋งŒ๋“ค๊ธฐ
            String dir = STR."C:/Temp/prj2/\{board.getId()}";
            File dirFile = new File(dir);
            if (!dirFile.exists()) {
                dirFile.mkdirs();
            }

            //ํŒŒ์ผ ๊ฒฝ๋กœ
            String path = STR."C:/Temp/prj2/\{board.getId()}/\{file.getOriginalFilename()}";
            File destination = new File(path);
            file.transferTo(destination);
        }
    }
}
  • mapper์—์„œ ๋จผ์ € ๊ฒŒ์‹œ๋ฌผ์„ ์ €์žฅํ•œ ํ›„, ์ƒ์„ฑ๋œ board์˜ id๊ฐ’์„ ๋ฐ›์•„ ํŒŒ์ผ์„ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค.
  • mapper.insert(board) : Board ๊ฐ์ฒด์˜ ID ํ•„๋“œ๊ฐ€ DB์—์„œ ์ž๋™ ์ƒ์„ฑ๋œ ํ‚ค ๊ฐ’์œผ๋กœ ์„ค์ •๋˜์–ด board.getID() ๊ฐ’์„ ๋ฐ›์•„์˜ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • DB์— ์ €์žฅ๋œ ํŒŒ์ผ์„ ์‹ค์ œ ์ปดํ“จํ„ฐ(disk)์— ์ €์žฅํ•  ์ˆ˜ ์žˆ๋„๋ก ๊ฒฝ๋กœ๋ฅผ ์„ค์ •ํ•ด์„œ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค.
  • ๋ถ€๋ชจ ๋””๋ ‰ํ† ๋ฆฌ๋ฅผ ์ƒ์„ฑํ•ด์„œ board์˜ id ํด๋”๋ฅผ ์ƒ์„ฑํ•˜๊ณ  ํŒŒ์ผ ๊ฒฝ๋กœ๋ฅผ ์ง€์ •ํ•˜์—ฌ MultipartFile ๊ฐ์ฒด๋ฅผ ํ•ด๋‹น ๊ฒฝ๋กœ์— ์ €์žฅ(transferTo)ํ•ฉ๋‹ˆ๋‹ค.

BoardMapper.java

@Insert("""
        INSERT INTO board (title, content, member_id)
        VALUES (#{title}, #{content}, #{memberId})
        """)
@Options(useGeneratedKeys = true, keyProperty = "id")
int insert(Board board);

    @Insert("""
            INSERT INTO board_file (board_id, name)
            VALUES (#{boardId}, #{name})
            """)
    int insertFileName(Integer boardId, String name);
  • @Options : ์ž๋™ ์ƒ์„ฑ๋œ ํ‚ค(auto-increment)๋ฅผ ์‚ฌ์šฉํ•˜๋„๋ก ์„ค์ •ํ•˜๊ณ  ๊ทธ๊ฒƒ์„ id ํ•„๋“œ์— ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ’Ÿ ๊ฒŒ์‹œ๋ฌผ ์ „์ฒด ์กฐํšŒ ์‹œ ๊ทธ๋ฆผ ํŒŒ์ผ ๊ฐฏ์ˆ˜ ํ‘œ์‹œ(READ)


Board.java

private Integer numberOfImages;
  • ํด๋ผ์ด์–ธํŠธ์™€ ์„œ๋ฒ„๋ž‘ numberOfImages๋ฅผ ๋ฐ”์ธ๋”ฉํ•˜๊ธฐ ์œ„ํ•ด์„œ Board ์ž๋ฐ”๋นˆ์— ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.

@Select("""
        <script>
        SELECT b.id, 
               b.title,
               m.nick_name writer,
               COUNT(f.name) number_of_images
        FROM board b JOIN member m ON b.member_id = m.id 
                     LEFT JOIN board_file f ON b.id = f.board_id
          < ~~~~~ >
        GROUP BY b.id
        ORDER BY b.id DESC
        LIMIT #{offset}, 10
        </script>
        """)
List<Board> selectAllPaging(Integer offset, String searchType, String keyword);
  • ๊ฒŒ์‹œ๊ธ€์„ ์กฐํšŒํ•  ๋•Œ ์‚ฌ์ง„์ด ๋ช‡๊ฐœ ์ €์žฅํ–ˆ๋Š”์ง€ ์•Œ๊ธฐ ์œ„ํ–‡ Count๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ฟผ๋ฆฌ๋ฌธ์„ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค. board์˜ id ๋ณ„๋กœ ์ €์žฅ๋˜์–ด์•ผ ํ•˜๊ธฐ ๋•Œ๋ฌธ์— GROUP BY๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

                  <Td>
                    {board.title}
                    {board.numberOfImages > 0 && (
                      <Badge>
                        <FontAwesomeIcon icon={faImages} />
                        {board.numberOfImages}
                      </Badge>
                    )}
                  </Td>
                  <Td>{board.writer}</Td>
  • ์‚ฌ์ง„์˜ ๊ฐฏ์ˆ˜๊ฐ€ 0๋ณด๋‹ค ํฌ๋‹ค๋ฉด icon๊ณผ ์‚ฌ์ง„์˜ ๊ฐฏ์ˆ˜๋ฅผ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค.


๐Ÿ’Ÿ ๊ฒŒ์‹œ๋ฌผ ์„ ํƒ ์‹œ ๊ทธ๋ฆผ ํŒŒ์ผ ๋ณด์ด๊ฒŒ ํ•˜๊ธฐ(READ)


BoardFile.java

@Data
@AllArgsConstructor
@NoArgsConstructor
public class BoardFile {
    private String name;
    private String src;
}
  • BoardFile ํด๋ž˜์Šค๋ฅผ ๋งŒ๋“ค์–ด ํŒŒ์ผ๋ช…๊ณผ ํŒŒ์ผ ๊ฒฝ๋กœ๋ฅผ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค.

Board.java

private List<BoardFile> fileList;
  • ํด๋ผ์ด์–ธํŠธ์™€ ๋ฐ”์ธ๋”ฉํ•˜๊ธฐ ์œ„ํ•ด Board ์ž๋ฐ”๋นˆ์—์„œ fileList๋ฅผ ์„ ์–ธํ•ด์ค๋‹ˆ๋‹ค.

BoardService.java

// ๊ฒŒ์‹œ๋ฌผ ํ•˜๋‚˜ ์กฐํšŒ
public Board get(Integer id) {
    Board board = mapper.selectById(id);
    List<String> fileNames = mapper.selectFileNameByBoardId(id);
    List<BoardFile> files = fileNames.stream()
            .map(name -> new BoardFile(name, STR."http://~/\{id}/\{name}"))
            .toList();
    board.setFileList(files);

    // http://~/{id}/{name}
    return board;
}
  • ํŒŒ์ผ ์ด๋ฆ„ ๋ชฉ๋ก์„ ๊ฐ€์ ธ์™€ ํŒŒ์ผ ์ด๋ฆ„ ๋ชฉ๋ก์„ ์ด๋ฏธ์ง€ URL ๋ชฉ๋ก์œผ๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. ์ƒ์„ฑ๋œ ์ด๋ฏธ์ง€ URL ๋ชฉ๋ก์„ setFiles๋ฅผ ํ†ตํ•ด Board ๊ฐ์ฒด์— ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.

BoardMapper.java

@Select("""
        SELECT name FROM board_file
        WHERE board_id=#{boardId}
        """)
List<String> selectFileNameByBoardId(Integer boardId);
  • board์˜ id์— ํ•ด๋‹นํ•˜๋Š” ํŒŒ์ผ๋ช…์„ ์กฐํšŒํ•˜์—ฌ ์กฐํšŒ๋œ ํŒŒ์ผ์„ ๋ชจ๋‘ List์— ๋‹ด์Šต๋‹ˆ๋‹ค.

BoardView.jsx(React)

        <Box display={"flex"} flexWrap={"wrap"} mt={"30px"}>
          {board.fileList &&
            board.fileList.map((file) => (
              <Box
                boxSize={"310px"}
                border={"2px solid black"}
                m={3}
                key={file.name}
              >
                <Image boxSize={"300px"} src={file.src} />
              </Box>
            ))}
        </Box>
  • ์ €์žฅ๋œ ์ด๋ฏธ์ง€ ํŒŒ์ผ์— ์žˆ๋Š” ์‚ฌ์ง„์„ ๊บผ๋‚ด ์ถœ๋ ฅํ•ฉ๋‹ˆ๋‹ค.


๐Ÿ’Ÿ ํŒŒ์ผ ์žˆ๋Š” ๊ฒŒ์‹œ๋ฌผ ์‚ญ์ œ(DELETE)


ํŒŒ์ผ์ด ์žˆ๋Š” ๊ฒŒ์‹œ๋ฌผ ์‚ญ์ œํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” ๋จผ์ € ์ปดํ“จํ„ฐ์— ์žˆ๋Š” ํŒŒ์ผ์„ ์‚ญ์ œํ•˜๊ณ  ์‚ญ์ œํ•˜๋ ค๋Š” board_id์— ํ•ด๋‹นํ•˜๋Š” ํŒŒ์ผ ํ…Œ์ด๋ธ”์— ์žˆ๋Š” ํŒŒ์ผ์„ ์‚ญ์ œํ•˜๊ณ  ๊ทธ ๋‹ค์Œ์— ๊ฒŒ์‹œ๋ฌผ์„ ์‚ญ์ œํ•ฉ๋‹ˆ๋‹ค.


BoardService.java

public void remove(Integer id) {

    // file๋ช… ์กฐํšŒ
    List<String> fileNames = mapper.selectFileNameByBoardId(id);

    // disk์— ์žˆ๋Š” file ์‚ญ์ œ
    String dir = STR."C:/Temp/prj2/\{id}/";
    for (String fileName : fileNames) {
        File file = new File(dir + fileName);
        file.delete();
    }
    File dirFile = new File(dir);
    if (dirFile.exists()) {
        dirFile.delete();
    }

    // board_file ์‚ญ์ œ
    mapper.deleteFileByBoardId(id);

    // board
    mapper.deleteById(id);
}
  • mapper์—์„œ selectFileNameByBoardId(id)๋ฅผ ํ˜ธ์ถœํ•˜์—ฌ ํ•ด๋‹น ๊ฒŒ์‹œ๋ฌผ์— ์ฒจ๋ถ€๋œ ํŒŒ์ผ๋ช… ๋ชฉ๋ก์„ ๊ฐ€์ ธ์˜ค๊ณ  ๊ฐ€์ ธ์˜จ ํŒŒ์ผ ๋ชฉ๋ก์„ ๋””์ŠคํŠธ์—์„œ ์‚ญ์ œํ•ฉ๋‹ˆ๋‹ค. ๋ชจ๋“  ํŒŒ์ผ์ด ์‚ญ์ œ๋˜๋ฉด ํ•ด๋‹น ๋””๋ ‰ํ† ๋ฆฌ๋„ ์‚ญ์ œํ•ฉ๋‹ˆ๋‹ค.
  • ํ•ด๋‹น ๊ฒŒ์‹œ๋ฌผ๊ณผ ๊ด€๋ จ๋œ ํŒŒ์ผ ์ •๋ณด(board_file)๋ฅผ DB์— ์‚ญ์ œํ•˜๊ณ  ํ•ด๋‹น ๊ฒŒ์‹œ๋ฌผ(board)์—์„œ DB์— ์‚ญ์ œํ•ฉ๋‹ˆ๋‹ค.

BoardMapper.java

@Delete("""
        DELETE FROM board_file WHERE board_id=#{boardId}
        """)
int deleteFileByBoardId(Integer boardId);
  • ํŒŒ์ผ์„ ์ง€์šฐ๊ธฐ ์œ„ํ•ด board_file ํ…Œ์ด๋ธ”์— ํ•ด๋‹นํ•˜๋Š” board_id๋ฅผ ์‚ญ์ œํ•ฉ๋‹ˆ๋‹ค.

ํšŒ์› ํƒˆํ‡ด ํ•  ๋•Œ๋„ ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ ํŒŒ์ผ์„ ์ง€์šฐ๊ณ  ํƒˆํ‡ด๋˜๊ธฐ ๋•Œ๋ฌธ์— ์ฝ”๋“œ ์ˆ˜์ •ํ•ฉ๋‹ˆ๋‹ค.

public void delete(Integer id) {
    // ํšŒ์›์ด ์“ด ๊ฒŒ์‹œ๋ฌผ ์กฐํšŒ
    List<Board> boardList = boardMapper.selectByMemberId(id);

    // ๊ฐ ๊ฒŒ์‹œ๋ฌผ ์ง€์šฐ๊ธฐ
    boardList.forEach(board -> boardService.remove(board.getId()));

    // board ์ง€์šด ํ›„ member ํ…Œ์ด๋ธ” ์ง€์šฐ๊ธฐ
    mapper.deleteById(id);
}
  • ํŠน์ • ํšŒ์›์ด ์ž‘์„ฑํ•œ ๊ฒŒ์‹œ๋ฌผ์˜ id๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค.
  • ๊ฐ€์ ธ์˜จ ๊ฒŒ์‹œ๋ฌผ์˜ ID ๋ชฉ๋ก์„ ๋ฐ˜๋ณตํ•˜๋ฉด์„œ ๊ฐ ๊ฒŒ์‹œ๋ฌผ์„ ์‚ญ์ œํ•ฉ๋‹ˆ๋‹ค.
    boardService.remove()์—๋Š” ์ด๋ฏธ ํŒŒ์ผ์„ ์ง€์šฐ๋Š” ๊ณผ์ •์ด ํฌํ•จ๋˜์–ด ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ๊ทธ ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

BoardMapper.java

@Select("""
        SELECT id FROM board WHERE member_id=#{memberId}
        """)
List<Board> selectByMemberId(Integer memberId);
  • member_id์— ํ•ด๋‹นํ•˜๋Š” board์˜ id๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ’Ÿ ํŒŒ์ผ ์žˆ๋Š” ๊ฒŒ์‹œ๋ฌผ ์ˆ˜์ •(UPDATE)


๐ŸŸฆ ์ˆ˜์ • ํŽ˜์ด์ง€์—์„œ ํŒŒ์ผ ์‚ญ์ œํ•˜๊ธฐ

BoardController.java

@PutMapping("edit")
@PreAuthorize("isAuthenticated()")
public ResponseEntity edit(Board board,
                           @RequestParam(value = "removeFileList[]", required = false)
                           List<String> removeFileList,
                           Authentication authentication) {
    if (!service.hasAccess(board.getId(), authentication)) {
        return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
    }

    if (service.validate(board)) {
        service.edit(board, removeFileList);
        return ResponseEntity.ok().build();
    } else {
        return ResponseEntity.badRequest().build();
    }
}
  • ํด๋ผ์ด์–ธํŠธ๋กœ๋ถ€ํ„ฐ ์š”์ฒญ์„ ๋ฐ›์„ ๋•Œ ์ž๋ฐ”๋นˆ์˜ Board์™€ removeFileList๋ฅผ ๋ฐ›์•„์•ผ ํ•ฉ๋‹ˆ๋‹ค.

BoardService.java

public void edit(Board board, List<String> removeFileList) {
    if (removeFileList != null && removeFileList.size() > 0) {

        // disk์˜ ํŒŒ์ผ ์‚ญ์ œ
        for (String fileName : removeFileList) {
            String path = STR."C:/Temp/prj2/\{board.getId()}/\{fileName}";
            File file = new File(path);
            file.delete();

            // db records ์‚ญ์ œ
            mapper.deleteFileByBoardIdAndName(board.getId(), fileName);
        }
    }


    mapper.update(board);
}
  • ํŒŒ์ผ์„ ์‚ญ์ œํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” ์ปดํ“จํ„ฐ์™€ DB์— ์ €์žฅ๋˜์–ด ์žˆ๋Š” ํŒŒ์ผ์„ ์‚ญ์ œํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
  • ๋งŒ์•ฝ ์‚ญ์ œํ•  ํŒŒ์ผ์ด ์žˆ๋‹ค๋ฉด disk ๊ฒฝ๋กœ๋ฅผ ์ฐพ์•„ ํŒŒ์ผ์„ ์‚ญ์ œ์‹œํ‚ค๊ณ  DB์—๋„ ์‚ญ์ œ์‹œํ‚ต๋‹ˆ๋‹ค.

BoardMapper.java

@Delete("""
        DELETE FROM board_file WHERE board_id=#{boardId} AND name=#{fileName}
        """)
int deleteFileByBoardIdAndName(Integer boardId, String fileName);
  • board์˜ id์™€ ํŒŒ์ผ๋ช…(name)์„ ์‚ญ์ œํ•˜๋Š” ์ฟผ๋ฆฌ๋ฅผ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.

BoardEdit.jsx(React)

function handleRemoveSwitchChange(name, checked) {
  if (checked) {
    // ์ฒดํฌ ๋˜๋ฉด ํŒŒ์ผ ์‚ญ์ œํ•  ๋ฐฐ์—ด์— ์ถ”๊ฐ€
    setRemoveFileList([...removeFileList, name]);
  } else {
    // ์ง€์šธ ๋ชฉ๋ก์— ์žˆ์—ˆ๋‹ค๋ฉด ํ•ด๋‹น ์ด๋ฆ„ ์ œ์™ธ
    setRemoveFileList(removeFileList.filter((item) => item !== name));
  }
}

<Switch
    onChange={(e) =>
    handleRemoveSwitchChange(file.name, e.target.checked)
  }
/>
  • Switch ๋ฒ„ํŠผ์„ ํ™œ์„ฑํ™” ์‹œํ‚ค๋ฉด ํ•ด๋‹น ํŒŒ์ผ์„ removeFileList์— ๋„ฃ๊ณ  ๊ทธ๋ ‡์ง€ ์•Š์œผ๋ฉด removeFileList ๋ฐฐ์—ด์„ ์ˆœํšŒํ•˜๋ฉฐ, item์ด name๊ณผ ์ผ์น˜ํ•˜์ง€ ์•Š๋Š” ์š”์†Œ๋งŒ ๋‚จ๊ฒจ setRemoveFileList๋กœ ์ƒํƒœ๋ฅผ ์—…๋ฐ์ดํŠธํ•ฉ๋‹ˆ๋‹ค.

axios.putForm("/api/board/edit", {
        id: board.id,
        title: board.title,
        content: board.content,
        removeFileList,
      })
  • ์ €์žฅ ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅด๋ฉด ๊ฒŒ์‹œ๋ฌผ์˜ id, title, content์™€ ํŒŒ์ผ ์‚ญ์ œ ๋ชฉ๋ก์„ ์„œ๋ฒ„์—๊ฒŒ ๋„˜๊ฒจ์ค๋‹ˆ๋‹ค.
  • ๋‚˜์ค‘์— ์ƒˆ ๊ทธ๋ฆผ ์ถ”๊ฐ€์‹œ JSON ํ˜•์‹์œผ๋กœ ๋„˜๊ฒจ์•ผ ํ•˜๊ธฐ ๋•Œ๋ฌธ์— putForm์œผ๋กœ ์š”์ฒญํ•ฉ๋‹ˆ๋‹ค.

๐ŸŸฆ ์ˆ˜์ • ํŽ˜์ด์ง€์—์„œ ์ƒˆ ํŒŒ์ผ ์ถ”๊ฐ€ํ•˜๊ธฐ

BoardController.java

@PutMapping("edit")
@PreAuthorize("isAuthenticated()")
public ResponseEntity edit(Board board,
                           @RequestParam(value = "removeFileList[]", required = false)
                           List<String> removeFileList,
                           @RequestParam(value = "addFileList[]", required = false) MultipartFile[] addFileList,
                           Authentication authentication) throws IOException {
    if (!service.hasAccess(board.getId(), authentication)) {
        return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
    }

    if (service.validate(board)) {
        service.edit(board, removeFileList, addFileList);
        return ResponseEntity.ok().build();
    } else {
        return ResponseEntity.badRequest().build();
    }
}
  • ์ƒˆ๋กœ์šด ํŒŒ์ผ์„ ์ถ”๊ฐ€ ํ•˜๊ธฐ ์œ„ํ•ด์„œ addFileList๋ฅผ ๋ฐ›์•„์„œ ์„œ๋น„์Šค์—์„œ ํŒŒ์ผ์„ ์ถ”๊ฐ€ํ•˜๋„๋ก ํ•ฉ๋‹ˆ๋‹ค.

BoardService.java

public void edit(Board board, List<String> removeFileList, MultipartFile[] addFileList) throws IOException {
    
		~~~
    if (addFileList != null && addFileList.length > 0) {
        List<String> fileNameList = mapper.selectFileNameByBoardId(board.getId());
        for (MultipartFile file : addFileList) {
            String fileName = file.getOriginalFilename();
            if (!fileNameList.contains(fileName)) {
                // ์ƒˆ ํŒŒ์ผ์ด ์—†์„ ๋•Œ๋งŒ ์ƒˆ ํŒŒ์ผ DB์— ์ถ”๊ฐ€
                mapper.insertFileName(board.getId(), fileName);
            }
            // disk์— ์“ฐ๊ธฐ
            String dir = STR."C:/Temp/prj2/\{board.getId()}";
            File dirFile = new File(dir);
            if (!dirFile.exists()) {
                dirFile.mkdirs();
            }

            String path = STR."C:/Temp/prj2/\{board.getId()}/\{fileName}";
            File destination = new File(path);
            file.transferTo(destination);
        }
    }
    mapper.update(board);
}
  • ์ถ”๊ฐ€๋œ ํŒŒ์ผ ๋ชฉ๋ก์ด ์žˆ๋‹ค๋ฉด ๋จผ์ € ๊ทธ ํŒŒ์ผ์ด ์žˆ๋Š”์ง€ ์กฐํšŒํ•˜๊ณ  ํ•ด๋‹น ํŒŒ์ผ์ด ์—†๋‹ค๋ฉด DB์— ์ถ”๊ฐ€ํ•ด์ค๋‹ˆ๋‹ค.
  • ์ปดํ“จํ„ฐ(disk)์— ํŒŒ์ผ์„ ์ถ”๊ฐ€ํ•˜๊ฑฐ๋‚˜ ๋ฎ์–ด์”๋‹ˆ๋‹ค.

BoardEdit.jsx(React)์˜ ์ „์ฒด ์ฝ”๋“œ

export function BoardEdit() {
  const { id } = useParams();
  const [board, setBoard] = useState(null);
  const [removeFileList, setRemoveFileList] = useState([]);
  const [addFileList, setAddFileList] = useState([]);
  const toast = useToast();
  const navigate = useNavigate();
  const { isOpen, onClose, onOpen } = useDisclosure();

  useEffect(() => {
    axios.get(`/api/board/${id}`).then((res) => setBoard(res.data));
  }, []);

  function handleClickSave() {
    axios
      .putForm("/api/board/edit", {
        id: board.id,
        title: board.title,
        content: board.content,
        removeFileList,
        addFileList,
      })
      .then(() => {
        toast({
          status: "success",
          description: `${board.id}๋ฒˆ ๊ฒŒ์‹œ๋ฌผ์ด ์ˆ˜์ •๋˜์—ˆ์Šต๋‹ˆ๋‹ค.`,
          position: "bottom-right",
        });
        navigate(`/board/${board.id}`);
      })
      .catch((err) => {
        if (err.response.status === 400) {
          toast({
            status: "error",
            description: `๊ฒŒ์‹œ๋ฌผ์ด ์ˆ˜์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค. ์ž‘์„ฑํ•œ ๋‚ด์šฉ์„ ํ™•์ธํ•ด์ฃผ์„ธ์š”.`,
            position: "bottom-right",
          });
        }
      })
      .finally(() => {
        onClose();
      });
  }

  if (board === null) {
    return <Spinner />;
  }

  function handleRemoveSwitchChange(name, checked) {
    if (checked) {
      // ์ฒดํฌ ๋˜๋ฉด ํŒŒ์ผ ์‚ญ์ œํ•  ๋ฐฐ์—ด์— ์ถ”๊ฐ€
      setRemoveFileList([...removeFileList, name]);
    } else {
      // ์ง€์šธ ๋ชฉ๋ก์— ์žˆ์—ˆ๋‹ค๋ฉด ํ•ด๋‹น ์ด๋ฆ„ ์ œ์™ธ
      setRemoveFileList(removeFileList.filter((item) => item !== name));
    }
  }

  // file ๋ชฉ๋ก ์ž‘์„ฑ
  const fileNameList = [];
  for (let addFile of addFileList) {
    // ์ด๋ฏธ ์žˆ๋Š” ํŒŒ์ผ๊ณผ ์ค‘๋ณต๋œ ํŒŒ์ผ๋ช…์ธ์ง€ ํ™•์ธ
    let duplicate = false;
    for (let file of board.fileList) {
      if (file.name === addFile.name) {
        duplicate = true;
        break;
      }
    }
    fileNameList.push(
      <li>
        {addFile.name}
        {duplicate && (
          <Badge ml={"10px"} colorScheme={"red"}>
            override
          </Badge>
        )}
      </li>,
    );
  }

  return (
    <Box mt={"30px"}>
      <Box>{board.id}๋ฒˆ ๊ฒŒ์‹œ๋ฌผ ์ˆ˜์ •</Box>
      <Box>
        <Box>
          <FormControl>
            <FormLabel>์ œ๋ชฉ</FormLabel>
            <Input
              defaultValue={board.title}
              onChange={(e) => setBoard({ ...board, title: e.target.value })}
            />
          </FormControl>
        </Box>
        <Box mt={"30px"}>
          <FormControl>
            <FormLabel>๋ณธ๋ฌธ</FormLabel>
            <Textarea
              defaultValue={board.content}
              onChange={(e) => setBoard({ ...board, content: e.target.value })}
            ></Textarea>
          </FormControl>
        </Box>
        <Box display={"flex"} flexWrap={"wrap"} mt={"30px"}>
          {board.fileList &&
            board.fileList.map((file) => (
              <Box
                boxSize={"210px"}
                border={"2px solid black"}
                m={3}
                key={file.name}
              >
                <Center>
                  <Box>
                    <Flex>
                      <FontAwesomeIcon icon={faTrashCan} />
                      <Switch
                        ml="10px"
                        onChange={(e) =>
                          handleRemoveSwitchChange(file.name, e.target.checked)
                        }
                      />
                      <Text ml="10px">{file.name}</Text>
                    </Flex>
                  </Box>
                </Center>
                <Center>
                  <Box>
                    <Image
                      sx={
                        removeFileList.includes(file.name)
                          ? { filter: "blur(5px)" }
                          : {}
                      }
                      borderRadius={"full"}
                      boxSize={"180px"}
                      src={file.src}
                    />
                  </Box>
                </Center>
              </Box>
            ))}
        </Box>

        <Box mt={"30px"}>
          <FormControl>
            <FormLabel>ํŒŒ์ผ</FormLabel>
            <Input
              multiple={true}
              type={"file"}
              accept={"image/*"}
              onChange={(e) => {
                setAddFileList(e.target.files);
              }}
            />
            <FormHelperText color={"red"}>
              ์ด ์šฉ๋Ÿ‰์€ 10MB ํ•œ ํŒŒ์ผ์€ 1MB๋ฅผ ์ดˆ๊ณผํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.
            </FormHelperText>
          </FormControl>
        </Box>
        <Box>
          <ul>{fileNameList}</ul>
        </Box>
        <Box mt={"30px"}>
          <FormControl>
            <FormLabel>์ž‘์„ฑ์ž</FormLabel>
            <Input defaultValue={board.writer} readOnly />
          </FormControl>
        </Box>
        <Box mt={"30px"}>
          <Button w={"100px"} colorScheme={"green"} onClick={onOpen}>
            ์ €์žฅ
          </Button>
        </Box>
      </Box>
      <Modal isOpen={isOpen} onClose={onClose}>
        <ModalOverlay />
        <ModalContent>
          <ModalHeader></ModalHeader>
          <ModalBody>์ €์žฅํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?</ModalBody>
          <ModalFooter>
            <Button onClick={onClose}>์ทจ์†Œ</Button>
            <Button onClick={handleClickSave} colorScheme={"blue"}>
              ํ™•์ธ
            </Button>
          </ModalFooter>
        </ModalContent>
      </Modal>
    </Box>
  );
}

profile
๊ฐœ๋ฐœ์ž ์ค€๋น„์ƒ~

0๊ฐœ์˜ ๋Œ“๊ธ€