Spring boot 게시판 만들기(게시판 CRUD)
by 볼빵빵오춘기게시판을 만드는 이유
자바를 배우고 나서 다음 단계를 넘어가려 할 때, 책이나 인터넷에서 자료를 찾아보면 게시판 만들기 예제가 많이 등장하는 것을 알 수 있다.
국비 교육 과정에서도 최종 프로젝트로 게시판을 만들어 포트폴리오로 활용하는 경우가 흔하다.
처음에는 아무 생각 없이 그냥 공부하다 보면 자연스럽게 알게 되겠지 하고 지나쳤던 것 같다.
하지만 다시 생각해보니, 게시판 만들기가 자주 등장하는 데는 이유가 있지 않을까 싶다.
이제 그 이유와 더불어, CRUD의 중요성, 그리고 CRUD를 이용해 게시판을 만드는 이유와 게시판 구현은 어떻게 했는지 정리해본다.
CRUD(Create, Read, Update, Delete)가 중요한 이유
더보기
- 데이터 관리의 기본 기능 제공
대부분의 애플리케이션은 데이터를 다룬다.
예를 들어, 사용자 정보, 상품 목록, 게시물, 댓글 등 다양한 데이터를 다루는데, 이 데이터를 관리하기 위해서는 생성, 읽기, 수정, 삭제가 필요하다.
CRUD는 이러한 데이터 관리의 기본적인 요구사항을 충족시킨다. - 유연한 데이터 조작 가능
CRUD 기능을 통해 사용자는 데이터를 쉽게 추가, 조회, 수정, 삭제할 수 있다.
이는 데이터의 유연한 관리를 가능하게 하며, 사용자가 요구하는 다양한 데이터 조작 작업을 지원한다. - 애플리케이션의 기능 확장성
CRUD는 데이터베이스와의 상호작용을 위한 기본적인 인터페이스를 제공한다.
이를 기반으로 추가적인 기능을 구현하거나 시스템을 확장할 수 있다.
예를 들어, 필터링, 정렬, 검색 등의 기능도 CRUD 작업을 기반으로 쉽게 구현할 수 있다. - 표준화된 인터페이스 제공
대부분의 개발자들이 CRUD 개념에 익숙하기 때문에, 이를 사용하면 코드의 가독성과 유지보수성이 향상된다.
이는 개발팀 간의 협업을 원활하게 하고, 새로운 팀원이 프로젝트에 쉽게 적응할 수 있도록 도와준다. - 데이터 무결성 보장
CRUD를 통해 데이터의 생성부터 삭제까지 일관된 작업을 수행할 수 있다.
이를 통해 데이터의 무결성을 유지하고, 시스템의 신뢰성을 높일 수 있다.
CRUD를 이용해 게시판을 만드는 이유
더보기
- 실전 같은 학습 경험
- 기본적인 웹 애플리케이션 구조 이해
게시판은 대부분의 웹 애플리케이션에서 필요한 기능들을 작은 범위에서 경험할 수 있는 좋은 예시이다.
사용자는 게시판을 통해 데이터베이스와의 상호작용, 사용자 입력 처리, 화면 렌더링 등 다양한 기술을 종합적으로 학습할 수 있다. - 웹 개발 전반에 대한 이해: 게시판 프로젝트는 프론트엔드(HTML, CSS, JavaScript)와 백엔드(서버, 데이터베이스)의 연동을 포함하여, 전반적인 웹 개발의 흐름을 이해할 수 있게 도와준다.
- 기본적인 웹 애플리케이션 구조 이해
- CRUD 기능의 실습과 이해
- Create, Read, Update, Delete
게시판은 자연스럽게 CRUD 기능을 구현할 수 있는 주제이다.
사용자는 게시판에서 글을 작성(Create), 조회(Read), 수정(Update), 삭제(Delete)하며, 각 기능이 어떻게 작동하는지 직접 경험하게 된다.
이 과정에서 데이터베이스와의 상호작용, 입력 검증, 트랜잭션 관리 등을 실습할 수 있다.
- Create, Read, Update, Delete
- 프로젝트 완성도 높이기
- 포트폴리오에 적합
게시판은 작은 규모의 프로젝트이지만, 여러 가지 중요한 기능을 포함하고 있어 포트폴리오로 적합하다.
특히, 이를 통해 얻은 결과물은 취업 준비나 면접에서 유용하게 사용될 수 있다. - 확장 가능성
게시판은 기본적인 CRUD 기능에서 시작하여, 이후에 페이징, 검색, 권한 관리, 댓글 기능 등 다양한 기능을 추가하면서 확장할 수 있다.
이를 통해 프로젝트를 점진적으로 개선해 나갈 수 있는 경험을 얻게 된다.
- 포트폴리오에 적합
- 현실적인 문제 해결 경험
- 실제 웹 애플리케이션에서 발생할 수 있는 문제를 해결
게시판 프로젝트를 진행하면서 다양한 문제에 부딪히게 된다.
예를 들어, 입력 검증, 데이터 무결성 유지, 동시성 이슈 등의 문제를 해결하면서 현실적인 프로그래밍 문제를 경험하게 된다. - 보안과 성능 고려
게시판을 만들 때는 사용자 인증, 권한 관리, 데이터 보호 등 보안 문제도 고려해야 한다.
이를 통해 보안에 대한 기본적인 이해를 높일 수 있다.
- 실제 웹 애플리케이션에서 발생할 수 있는 문제를 해결
- 커뮤니티와 협업 경험
- 다른 개발자들과 코드 리뷰 및 협업: 게시판 프로젝트는 다양한 개발자들과 공유하기 쉽고, 코드 리뷰를 통해 피드백을 받을 수 있는 좋은 주제이다.
이를 통해 협업 능력도 키울 수 있다.
- 다른 개발자들과 코드 리뷰 및 협업: 게시판 프로젝트는 다양한 개발자들과 공유하기 쉽고, 코드 리뷰를 통해 피드백을 받을 수 있는 좋은 주제이다.
글쓰기(Create)
controller
// 글쓰기 Form @GetMapping("/communityW") public String writeForm(){ return "community/write"; }
write.html
- 기본 글쓰기 부분이라 제목과 내용정도만 입력하도록 하였고 제목과 내용은 required을 하여 필수 입력사항으로 했다.
- 파일은 파일추가 버튼과 파일삭제버튼을 눌러 갯수를 유동적으로 넣을 수 있도록 했다.(bbsAddFile.js 연결한 이유)
<div class="col-12 mb-15"> <form method="post" action="/communityW" enctype="multipart/form-data"> <input type="hidden" name="username" Id="username" placeholder="username" th:value="${#authentication.principal.username}" > <div class="form-group"> <label for="title">제목</label><br> <input type="text" name="title" Id="title" placeholder="제목을 입력해주세요." required class="width100p"> </div> <div class="form-group"> <label for="content">내용</label> <div> <textarea name="content" id="content" style="width:100%;" rows="3" placeholder="내용을 입력해주세요." required></textarea> </div> </div> <div class="form-group"> <label>파일 - 이미지 첨부(png, jpeg, jpg만 가능합니다.)</label> <span id="btn-addFile">+</span> </div> <div id="inputFile"> </div> <button class="btn bg_03A3F1 color-fff" type="submit">글쓰기 완료</button> </form> </div>
bbsAddFile.js
html에서 + 버튼을 누르면 파일첨부할 수 있는 input type=’file’를 추가해주도록 만들었고, 추가된 + 버튼 옆에 x 버튼을 만들어 파일첨부를 안할시 삭제하도록 처리하도록 하였다.
let index = { init: function() { $("#btn-addFile").on("click", () => { this.addFile(); }); // 이벤트 위임 적용 $("#inputFile").on("click", ".btn-removeFile", function() { index.removeParentFormGroup($(this)); }); $("#inputFile").on("click", ".btn-hiddenFile", function() { index.hiddenParentFormGroup($(this)); }); }, addFile: function() { var addContent = ""; addContent += '<div class="form-group">' + '<input name="files" type="file" accept="image/png, image/jpeg, image/jpg" required>' + '<span class="btn-removeFile">x</span>' + '</div>'; $("#inputFile").append(addContent); }, removeParentFormGroup: function(button) { button.closest("div.form-group").remove(); }, hiddenParentFormGroup: function(button) { button.closest("div.form-group").addClass("d-none"); } } index.init();
Controller
받아온 file들과 communityDto를 service에 save() 파라미터로 넘겨준다.
// 글 작성 @PostMapping("/communityW") public String write(CommunityDto communityDto, List<MultipartFile> files,Model model) throws Exception{ commnunityService.save(communityDto,files); model.addAttribute("message", "글 작성이 완료되었습니다."); model.addAttribute("searchUrl", "/communityL"); return "message/message"; }
Service
파일을 유동적으로 첨부할 수 있도록 해놓았기 때문에 파일이 있는 경우와 없는 경우를 나누었다.
@Autowired private CommunityReplyRepository communityReplyRepository; public void save(CommunityDto communityDto, List<MultipartFile> files) throws Exception{ CommunityEntity communityEntity = CommunityEntity.toSaveCommnunityEntity(communityDto); // 여러 개의 파일을 가져올 경우 if(files!=null){ // 파일이 있을 경우 communityEntity.setFileAttached(1); CommunityEntity saveEntitiy = commnunityRepository.save(communityEntity); // 파일 이름가져오기 for(MultipartFile file : files){ // 원본 이름 String originalFileName = file.getOriginalFilename(); // 확장자 가져오기 String[] extension = originalFileName.split("[.]"); int lastIdx = extension.length-1; // 저장용 파일 이름 만들기 UUID uuid = UUID.randomUUID(); String storedFileName = System.currentTimeMillis()+"-"+uuid+"."+extension[lastIdx]; // boardFile DB에 넣을 BoardFileEntity 세팅 BoardFileEntity boardFileEntity = new BoardFileEntity(); boardFileEntity.setBoardId(saveEntitiy.getId()); boardFileEntity.setOriginalFileName(originalFileName); boardFileEntity.setStoredFileName(storedFileName); boardFileEntity.setBoard("community"); // 파일 저장용 폴더 String savePath = System.getProperty("user.dir")+"/src/main/resources/static/img/community"; // 파일 저장 File saveFile = new File(savePath,storedFileName); file.transferTo(saveFile); // boardFile DB에 넣음 boardFileRepository.save(boardFileEntity); } }else{ // 파일이 없을 경우 communityEntity.setFileAttached(0); commnunityRepository.save(communityEntity); } }
글 상세페이지(Read)
Controller
글 상세페이지는 게시글과 그 게시물과 연관되어있는 댓글들을 보여준다.
(※ 댓글은 댓글 crud 참고)
// 글 상세 @GetMapping("/communityV/{id}") public String view(Model model,@PathVariable int id){ // community에서 글 정보 가져오기 CommunityEntity communityEntity = commnunityService.view(id); model.addAttribute("board", communityEntity); // 저장 파일 가져오기 List<BoardFileEntity> boardFiles = commnunityService.getFileList(id); // Entity로 변경하기 model.addAttribute("boardFileList",boardFiles); // 댓글 가져오기 // 댓글은 가져올 필요x // communityEntity를 보면 // private List<CommunityReplyEntity> reply; // 이 부분을 통해 reply는 board 검색 시 그냥 가지고 오게 된다. // 그렇기 때문에 따로 가져오는 로직을 만들 필요가 없다. // 여기서 내가 말한 로직이란 service -> repository로 가는 로직을 말한다. List<CommunityReplyEntity> replies = communityEntity.getReply(); // 각 댓글 내용 출력 model에 담기 model.addAttribute("replies",replies); return "community/view"; }
Service
게시글의 내용과 제목, 파일 등 정보를 가져오면서 해당 글을 작성한 이가 아니면 조회수를 올라가도록 처리했다.
@Transactional public CommunityEntity view(int id){ CommunityEntity communityEntity = commnunityRepository.findById(id).get(); // 조회수 올리기 Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if (authentication != null && authentication.isAuthenticated() && authentication.getPrincipal() instanceof UserDetails) { UserDetails userDetails = (UserDetails) authentication.getPrincipal(); String username = userDetails.getUsername(); // 로그인한 사용자의 정보를 이용하여 처리 if(username!=communityEntity.getUsername()){ commnunityRepository.updateConunt(id); // 위의 코드에 @transation을 이용하여 바로 DB는 변경이 되나 view페이지에는 +1 된 숫자로 세팅이 되지않으므로 위에 가져온 communityEntity의 count를 +1해서 다시 세팅해준다. // 사용자가 view페이지에 들어오는 순간 작성자가 아니라면 바로 +1해서 보여준다. communityEntity.setCount(communityEntity.getCount()+1); } }else { // 기획안에 첨은에는 로그인한 회원만을 하였기 떄문에 비로그인사용자는 따로 추가 처리하지는 않았다. commnunityRepository.updateConunt(id); communityEntity.setCount(communityEntity.getCount()+1); } return commnunityRepository.findById(id).get(); }
Repository
// 조회수 올리기 @Modifying @Query(value=" update CommunityEntity c set c.count = c.count + 1 where c.id = :id ") void updateConunt(@Param("id") int id);
글 수정(Update)
controller
// 글 수정 Form @GetMapping("/communityU/{id}") public String updateForm(@PathVariable int id,Model model){ CommunityEntity communityEntity = commnunityService.updateForm(id); // 등록되어있던 글 정보 가져오기 model.addAttribute("board",communityEntity); // 등록되어있던 글에 파일 정보 가져오기 if(communityEntity.getFileAttached()==1){ List<BoardFileEntity> boardFiles = commnunityService.getFileList(id); // Entity로 변경해서 받아오기 model.addAttribute("boardFileList",boardFiles); } return "community/update"; }
update.html
- write.html 과 동일하나 input value에 값을 가져오고 파일에도 첨부된 파일은 보여주도록 처리했다.
- 수정 시에도 파일은 유동적이로 첨부할 수 있기에 bbsAddFile.js 연결했다.
<form method="post" action="/communityU" enctype="multipart/form-data"> <input type="hidden" name="id" Id="id" placeholder="id" th:value="${board.id}" > <input type="hidden" name="count" Id="count" placeholder="count" th:value="${board.count}" > <input type="hidden" name="username" Id="username" placeholder="username" th:value="${#authentication.principal.username}" > <div class="form-group"> <label for="title">제목</label><br> <input type="text" name="title" Id="title" th:value="${board.title}" placeholder="제목을 입력해주세요." class="width100p" required> </div> <div class="form-group"> <label for="content">내용</label> <div> <textarea name="content" id="content" style="width:100%;" th:utext="${board.content}" rows="3" placeholder="내용을 입력해주세요." required></textarea> </div> </div> <div class="form-group"> <label>파일 - 이미지 첨부(png, jpeg, jpg만 가능합니다.)</label> <span id="btn-addFile">+</span> </div> <div id="inputFile"> <div class="form-group" th:if="${board.fileAttached}==1" th:each="boardFile : ${boardFileList}"> <input name="files" type="file" accept="image/png, image/jpeg, image/jpg" ><br> 첨부 되어있는 파일 : <span th:text="${boardFile.originalFileName}"></span><br> <input name="del" type="checkbox" th:value="${boardFile.id}">첨부 파일 삭제 </div> </div> <button class="btn bg_03A3F1 color-fff" type="submit">수정 완료</button> </form>
controller
// 글 수정 @PostMapping("/communityU") public String update(CommunityDto communityDto, List<MultipartFile> files, @RequestParam (required = false) List<Integer> del,Model model,@AuthenticationPrincipal PrincipalDetails principalDetails) throws Exception{ commnunityService.update(communityDto,files,del); return "redirect:/communityL"; }
Service
- 파일 첨부와 첨부된 파일삭제조건에 따라 실행되도록 했다.
- 조건은 다음과 같다.
- 새로 들어온 파일이 없고 삭제도 없을 때 => 파일 변경 없음.
- 새로 들어온 파일이 없고 삭제만 있을 때
- 새로 들어온 파일만 있고 삭제는 없을 때
- 새로 들어온 파일이 있고 삭제도 있을 때
@Transactional public void update(CommunityDto communityDto,List<MultipartFile> files,List<Integer> del)throws Exception{ CommunityEntity communityEntity = CommunityEntity.toUpdateCommnunityEntity(communityDto); // 조건 // 새로 들어온 파일이 없고 삭제도 없을 때 => 파일 변경 없음. // 새로 들어온 파일이 없고 삭제만 있을 때 // 새로 들어온 파일만 있고 삭제는 없을 때 // 새로 들어온 파일이 있고 삭제도 있을 때 // 조건을 정리하자면 // 1. del 삭제해야할 파일이 있는가? => 있다면 orginalName, storedName 을 공백으로 변경 if(del !=null){ for(Integer id : del){ boardFileRepository.gapUpdate(id); } } // 2. 새로 들어온 파일이 있는가? // 기존 파일 List List<BoardFileEntity> boardFileEntities = boardFileRepository.findByBoardIdAndBoard(communityDto.getId(), "community"); // 2-1. 조건1. 기존파일 리스트 길이보다 작거나 같은경우 새로운 파일로 update if(files != null){ // 새 파일이 있을 때 if(files.size()<=boardFileEntities.size()){ for(int i=0;i<boardFileEntities.size();i++){ // 기존 해당 인덱스 부분에 새로운 파일을 set해주기 MultipartFile file = files.get(i); if (!file.getOriginalFilename().isEmpty()) { // 원본 이름 String originalFileName = file.getOriginalFilename(); // 확장자 가져오기 String[] extension = originalFileName.split("[.]"); int lastIdx = extension.length-1; // 저장용 파일 이름 만들기 UUID uuid = UUID.randomUUID(); String storedFileName = System.currentTimeMillis()+"-"+uuid+"."+extension[lastIdx]; BoardFileEntity newFile = new BoardFileEntity(); newFile.setId(boardFileEntities.get(i).getId()); newFile.setBoard(boardFileEntities.get(i).getBoard()); newFile.setBoardId(boardFileEntities.get(i).getBoardId()); newFile.setOriginalFileName(originalFileName); newFile.setStoredFileName(storedFileName); boardFileEntities.set(i,newFile); // 파일 저장용 폴더 String savePath = System.getProperty("user.dir")+"/src/main/resources/static/img/community"; // 파일 저장 File saveFile = new File(savePath,storedFileName); file.transferTo(saveFile); } } for(BoardFileEntity file : boardFileEntities){ boardFileRepository.update(file.getId(),file.getOriginalFileName(),file.getStoredFileName()); } }else{ // 2-2. 조건2. 기존파일 리스트 길이보다 크다. 큰 부분만 insert for(int i=0;i<boardFileEntities.size();i++){ // 기존 해당 인덱스 부분에 새로운 파일을 set해주기 MultipartFile file = files.get(i); if (!file.getOriginalFilename().isEmpty()) { // 원본 이름 String originalFileName = file.getOriginalFilename(); // 확장자 가져오기 String[] extension = originalFileName.split("[.]"); int lastIdx = extension.length-1; // 저장용 파일 이름 만들기 UUID uuid = UUID.randomUUID(); String storedFileName = System.currentTimeMillis()+"-"+uuid+"."+extension[lastIdx]; BoardFileEntity newFile = new BoardFileEntity(); newFile.setId(boardFileEntities.get(i).getId()); newFile.setBoard(boardFileEntities.get(i).getBoard()); newFile.setBoardId(boardFileEntities.get(i).getBoardId()); newFile.setOriginalFileName(originalFileName); newFile.setStoredFileName(storedFileName); boardFileEntities.set(i,newFile); // 파일 저장용 폴더 String savePath = System.getProperty("user.dir")+"/src/main/resources/static/img/community"; // 파일 저장 File saveFile = new File(savePath,storedFileName); file.transferTo(saveFile); } } for(BoardFileEntity file : boardFileEntities){ boardFileRepository.update(file.getId(),file.getOriginalFileName(),file.getStoredFileName()); } for(int i = boardFileEntities.size();i <files.size();i++){ MultipartFile file = files.get(i); // 원본 이름 String originalFileName = file.getOriginalFilename(); // 확장자 가져오기 String[] extension = originalFileName.split("[.]"); int lastIdx = extension.length-1; // 저장용 파일 이름 만들기 UUID uuid = UUID.randomUUID(); String storedFileName = System.currentTimeMillis()+"-"+uuid+"."+extension[lastIdx]; // boardFile DB에 넣을 BoardFileEntity 세팅 BoardFileEntity boardFileEntity = new BoardFileEntity(); boardFileEntity.setBoardId(communityDto.getId()); boardFileEntity.setOriginalFileName(originalFileName); boardFileEntity.setStoredFileName(storedFileName); boardFileEntity.setBoard("community"); // 파일 저장용 폴더 String savePath = System.getProperty("user.dir")+"/src/main/resources/static/img/community"; // 파일 저장 File saveFile = new File(savePath,storedFileName); file.transferTo(saveFile); // boardFile DB에 넣음 boardFileRepository.save(boardFileEntity); } } } // 3. DB에서 orginalName, storedName 을 공백인 컬럼은 삭제한다. boardFileRepository.deleteFile(communityDto.getId(),"community"); // 4. 해당 게시물에 첨부파일이 있다면 community BBS table에 fileattached 를 1로 바꿔준다. boardFileEntities = boardFileRepository.findByBoardIdAndBoard(communityDto.getId(), "community"); if(boardFileEntities == null || boardFileEntities.isEmpty()){ communityEntity.setFileAttached(0); }else{ communityEntity.setFileAttached(1); } commnunityRepository.save(communityEntity); }
Repository
// 파일 변경 // 첨부되어있는 파일 삭제에 체크박스에 체크 했을 경우 @Modifying @Query(value=" update BoardFileEntity c set c.originalFileName = '', c.storedFileName = '' where c.id = :id ") void gapUpdate(@Param("id") int id); // 파일 변경 // 첨부되어있는 파일 삭제에 체크 했고 새 파일을 넣을 경우 or 해당 새 파일 넣는 경우 => 새 파일을 넣는 경우 @Modifying @Query(value=" update BoardFileEntity c set c.originalFileName = :originalFileName, c.storedFileName = :storedFileName where c.id = :id ") void update(@Param("id") int id,@Param("originalFileName") String originalFileName,@Param("storedFileName") String storedFileName); // 첨부되어있는 파일 삭제에 체크박스에 체크 했고 새 파일이 들어오지않은경우 - c.originalFileName = '' and c.storedFileName = '' 빈칸으로 변경했고 불필요한 데이터이므로 삭제 // 두 번 나눠서 삭제이유는 기존 파일은 냅둬야하기때문에 구분하고자 '' 으로 변경 후 삭제함. @Modifying @Query(value=" delete BoardFileEntity c where c.boardId = :boardId and c.originalFileName = '' and c.storedFileName = '' and c.board = :board ") void deleteFile(@Param("boardId") int boardId,@Param("board") String board); // boardId 와 board를 통해 파일 정보 삭제 // boardId만 있을 경우 다른 테이블에 pk인 boardId도 삭제 될 수 있기 때문에 board(protect,shelter,community)인지 확인 @Modifying @Query(value=" delete BoardFileEntity c where c.boardId = :boardId and c.board = :board") void deleteBbsFile(@Param("boardId") int boardId,@Param("board") String board);
글 삭제(Delete)
Controller
//글 삭제 @GetMapping("/communityD/{id}") public String delete(@PathVariable int id,Model model){ // community table에서 해당 글 삭제하기 // reply tabled에서 해당 글과 관련된 댓글 삭제하기 // entity만들 때 연관관계에 의해 reply table에서 댓글은 같이 삭제된다. // file table에서 해당 게시글에 연관된 파일 삭제하기 file table에서 boardID 가 매개변수로 받은 id와 같으면 삭제하기 commnunityService.delete(id); model.addAttribute("message", "글이 삭제가 되었습니다."); model.addAttribute("searchUrl", "/communityL"); return "message/message"; }
Service
// 글 삭제 @Transactional public void delete(int id){ commnunityRepository.deleteById(id); boardFileRepository.deleteBbsFile(id,"community"); // 실질적 저장 파일 삭제하기 List<BoardFileEntity> boardFiles = getFileList(id); if(boardFiles!=null){ for (BoardFileEntity s : boardFiles) { String srcFileName = null; String fileName = s.getStoredFileName(); String uploadPath = System.getProperty("user.dir")+"/src/main/resources/static/img/community"; try { srcFileName = URLDecoder.decode(fileName,"UTF-8"); File file = new File(uploadPath + File.separator + srcFileName); // 매개변수 => 파일경로 이다. boolean result = file.delete(); // true이면 지우기 성공, false면 지우기 실패 }catch (UnsupportedEncodingException e){ e.printStackTrace(); } } } }
Repository
// boardId 와 board를 통해 파일 정보 삭제 // boardId만 있을 경우 다른 테이블에 pk인 boardId도 삭제 될 수 있기 때문에 board(protect,shelter,community)인지 확인 @Modifying @Query(value=" delete BoardFileEntity c where c.boardId = :boardId and c.board = :board") void deleteBbsFile(@Param("boardId") int boardId,@Param("board") String board);

블로그의 정보
Hello 춘기's world
볼빵빵오춘기