
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);
}
}
}
board์ id๊ฐ์ ๋ฐ์ ํ์ผ์ ์ ์ฅํฉ๋๋ค. mapper.insert(board) : Board ๊ฐ์ฒด์ ID ํ๋๊ฐ DB์์ ์๋ ์์ฑ๋ ํค ๊ฐ์ผ๋ก ์ค์ ๋์ด board.getID() ๊ฐ์ ๋ฐ์์ฌ ์ ์์ต๋๋ค.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 ํ๋์ ์ค์ ํฉ๋๋ค.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);
GROUP BY๋ฅผ ์ฌ์ฉํฉ๋๋ค. <Td>
{board.title}
{board.numberOfImages > 0 && (
<Badge>
<FontAwesomeIcon icon={faImages} />
{board.numberOfImages}
</Badge>
)}
</Td>
<Td>{board.writer}</Td>

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

ํ์ผ์ด ์๋ ๊ฒ์๋ฌผ ์ญ์ ํ๊ธฐ ์ํด์๋ ๋จผ์ ์ปดํจํฐ์ ์๋ ํ์ผ์ ์ญ์ ํ๊ณ ์ญ์ ํ๋ ค๋ 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);
}
selectFileNameByBoardId(id)๋ฅผ ํธ์ถํ์ฌ ํด๋น ๊ฒ์๋ฌผ์ ์ฒจ๋ถ๋ ํ์ผ๋ช
๋ชฉ๋ก์ ๊ฐ์ ธ์ค๊ณ ๊ฐ์ ธ์จ ํ์ผ ๋ชฉ๋ก์ ๋์คํธ์์ ์ญ์ ํฉ๋๋ค. ๋ชจ๋ ํ์ผ์ด ์ญ์ ๋๋ฉด ํด๋น ๋๋ ํ ๋ฆฌ๋ ์ญ์ ํฉ๋๋ค.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);
}
BoardMapper.java
@Select("""
SELECT id FROM board WHERE member_id=#{memberId}
""")
List<Board> selectByMemberId(Integer memberId);
member_id์ ํด๋นํ๋ board์ id๋ฅผ ์กฐํํฉ๋๋ค.๐ฆ ์์ ํ์ด์ง์์ ํ์ผ ์ญ์ ํ๊ธฐ
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);
}
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)
}
/>
removeFileList์ ๋ฃ๊ณ ๊ทธ๋ ์ง ์์ผ๋ฉด removeFileList ๋ฐฐ์ด์ ์ํํ๋ฉฐ, item์ด name๊ณผ ์ผ์นํ์ง ์๋ ์์๋ง ๋จ๊ฒจ setRemoveFileList๋ก ์ํ๋ฅผ ์
๋ฐ์ดํธํฉ๋๋ค.axios.putForm("/api/board/edit", {
id: board.id,
title: board.title,
content: board.content,
removeFileList,
})
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);
}
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>
);
}
