스프링부트 블로그 만들기 – 7강 게시글
글쓰기
1.야모리 파일(.yml)에서 naming 옵션을 추가한다.
server: port: 8000 spring: mvc: view: prefix: /WEB-INF/views/ suffix: .jsp datasource: driver-class-name: org.mariadb.jdbc.Driver username: cos password: cos1234 url: jdbc:mariadb://localhost:3306/cosdb?serverTimezone=Asia/Seoul jpa: hibernate: ddl-auto: none naming: physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl show-sql: true
naming 옵션은 컴파일시 변수명을 언더스코어 방식(user_id)으로 만들지 못하도록 만들어주는 옵션입니다. 단, 이 옵션을 사용하면 테이블명도 클래스명과 동일하게 만들어지기 때문에 오류가 날 수 있습니다. @Table(name = “테이블명”) 어노테이션을 사용하여 테이블명을 따로 지정할 수도 있으니 원하는 방식으로 코드를 짜면 되요.
2.Board.java 파일에서 UserId를 Foreign Key(외래키)로 만들어준다.
package com.cos.blogapp.domain.board; import javax.persistence.Entity; import javax.persistence.FetchType; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.JoinColumn; import javax.persistence.Lob; import javax.persistence.ManyToOne; import javax.persistence.Table; import com.cos.blogapp.domain.user.User; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @Table(name = "board") @AllArgsConstructor @NoArgsConstructor @Data @Entity public class Board { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private int id; //PK (자동증가 번호) private String title; // 아이디 @Lob private String content; @JoinColumn(name = "userId") // 외래키(포린키)의 이름을 바꿀 수 있다. @ManyToOne(fetch = FetchType.EAGER) private User user; // user_id 만들어준다. 포린키 만들어준다. }
FetchType
LAZY : 지연 로딩. 쿼리를 한번 실행한다. 경우에 따라 데이터를 선택할 수 있다.
EAGER : 즉시로딩. 디폴트 값으로 지정되어있다.foreign key 조건 : 원자성을 해치지 말아야한다.
원자성 : 데이터가 1개만 있는 것foreign key 공식 : N대 1의 관계에서 N에 foreign key를 넣어준다.
사용자와 게시글 : 1대 N의 관계
사용자와 영화 : N대 N의 관계
선수와 팀 : N대 1의 관계N이 드라이빙 테이블이 되어야 한다.
private User user를 사용하는 이유
사용자에 관한 정보를 외래키로 담고 싶은데 이를 하나의 데이터로 표현할 수 없다. 자바는 객체 안에 객체를 넣을 수 있지만 데이터베이스는 테이블 안에 테이블을 넣을 수 없다. 그러므로 자바와 데이터 베이스 세상에 차이가 생겨 모델링시 문제가 된다. 이전에는 두 번 접근하는 방식으로 이 문제를 해결했는데 스프링에서는 이 방법을 사용하여 이 문제를 해결한다.
join 메서드를 실행할 때 호출할 변수를 아래에서 만들 DTO 파일의 toEntity라는 함수로 만들어 사용하면 호출시 코드가 깔끔해지고 재사용에 용이해집니다.
의존성 주입을 하여 재사용할 수 있게 만들어주세요.
3.글쓰기 페이지에서 데이터를 넘겨줄 수 있게 만들어준다.
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <%@ include file="../layout/header.jsp"%> <div class="container p-4 w-70 rounded shadow"> <h5 style="font-family: 'IBM Plex Sans KR', sans-serif; margin-bottom: 30px;">글쓰기</h5> <form action="/board" method="post"> <div class="form-group"> <input type="text" name="title" class="form-control" placeholder="Enter title"> </div> <div class="form-group"> <textarea id="summernote" class="form-control" name="content"></textarea> </div> <button type="submit" class="btn btn-primary col-md-4" style="margin-top: 30px;">글쓰기</button> </form> </div> <script> $('#summernote').summernote({ height:350 }); </script> <%@ include file="../layout/footer.jsp"%>
4.글쓰기 DTO(Data Transfer Oject) 를 만들어준다.
DTO(Data Transfer Object)란 통신을 위한 오브젝트로 변수를 적는 대신 함수로 만들어 재사용할 수 있게 만든다. jsp 파일의 form 태그에서 name으로 데이터를 받아올 때 일반 변수로 받으면 MINE Type으로 데이터를 받아오는데 이렇게 만들면 validation 타입으로 받을 수 없어지기 때문에 Dto를 사용하여 타입에 상관없이 받을 수 있게 만드는 것이다.
방법1. 개인 프로젝트
package com.cos.blogapp.web.dto; import javax.validation.constraints.NotBlank; import javax.validation.constraints.Size; import com.cos.blogapp.domain.board.Board; import com.cos.blogapp.domain.user.User; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @Data @NoArgsConstructor @AllArgsConstructor public class BoardSaveReqDto { private String title; private String content; public Board toEntity(User principal) { Board board = new Board(); board.setTitle(title); board.setContent(content); board.setUser(principal); return board; } }
join 메서드를 실행할 때 DTO에서 toEntity라는 함수로 만들어 사용하면 호출시 코드가 깔끔해지고 재사용에 용이해집니다.
방법2. 회사 실무
package com.cos.blogapp2.web.dto; import com.cos.blogapp2.domain.board.Board; import com.cos.blogapp2.domain.user.User; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; @AllArgsConstructor @Setter @Getter public class BoardSaveReqDto { private String title; private String content; public Board toEntity(User principal) { Board board = Board.builder(). title(title) .content(content) .user(principal) .build(); return board; } }
Builder를 사용하면 toEntity를 만들 때 순서안지켜도 되고 넣고 싶은 것만 넣을 수 있다는 장점이 있어요.
5.BoardController.java에서 글쓰기 메서드를 만들어준다.
//DI private final BoardRepository boardRepository; private final HttpSession session; @PostMapping("/board") public String save(BoardSaveReqDto dto) { Board board = dto.toEntity(); User principal = (User)session.getAttribute("principal"); board.setUser(principal); boardRepository.save(board); return "redirect:/"; }
데이터를 뿌려주기 위해서 필요한 @setter 어노테이션을 Board.java에서 을 추가할 필요가 있어요.
의존성 주입을 위한 어노테이션 @RequiredArgsConstructor도 추가해 주세요.
글목록보기
1.글목록보기 메서드에서 데이터를 가져오는 코드를 추가한다.
@GetMapping("/board") public String list(Model model) { List<Board> boardsEntity = boardRepository.findAll(); model.addAttribute("boardsEntity", boardsEntity); return "board/list"; }
2.넘겨받은 글목록 데이터를 반복문을 사용해서 화면에 뿌려준다.
<div class="container"> <c:forEach var="board" items="${boardsEntity}"> <!-- 카드 글 시작 --> <div class="card"> <div class="card-body"> <!-- el표현식은 변수명을 적으면 자동으로 get함수를 호출해준다 --> <h4 class="card-title">${board.title}</h4> <a href="/board/${board.id}" class="btn btn-primary">상세보기</a> </div> </div> <br /> <!-- 카드 글 끝 --> </c:forEach> </div>
데이터를 뿌려주기 위해서 필요한 @NoArgsConstructor 어노테이션을 Board.java에서 을 추가할 필요가 있어요.
jstl 라이브러리를 사용해서 html에서 반복문을 사용하기 위해서는 header.jsp에서 taglib를 걸어줘야해요.
글상세보기
1.상세보기 메서드에서 데이터 하나를 셀렉트하는 코드를 추가한다.
@GetMapping("/board/{id}") public String detail(@PathVariable int id, Model model) { Board boardEntity = boardRepository.findById(id).get(); model.addAttribute("boardEntity", boardEntity); return "board/detail"; }
2.넘겨받은 글목록 한 건 데이터를 상세보기 화면에 뿌려준다.
<div> 글 번호 : ${boardEntity.id}</span> 작성자 : <span><i>${boardEntity.user.username} </i></span> </div> <br /> <div> <h3>${boardEntity.title}</h3> </div> <hr /> <div> <div>${boardEntity.content}</div> </div>
글삭제하기
글삭제하기를 배우기 전에 배워야 할 것
[Javascript] fetch() 비동기 요청하기fetch() 함수는 HTTP를 요청할 때 키값을 사용해서 다양한 데이터를 함께 가지고 갈 수 있습니다. 키값의 종류는 MDN Web Docs 홈페이지에서 확인할 수 있으면 다음과 같이 사용합니다....
스프링부트 블로그 만들기 - 예외처리프로그램을 만들 때 핵심기능을 만들기 전후로 부가기능을 추가해줘야 한다. 핵심기능을 부가기능과 분리시켜 함수로 만들어 재사용하면 아주 편하게 코딩을 할 수 있다. 하지만 함수로 만들기 위한 공통 로직을 찾는 것은 결코 쉬운 일이 아니다....
자바스크립트 비동기 프로그래밍으로 Delete 메서드 요청하기
1.게시글 삭제 오류에 관한 커스텀 익셉션을 만든다.
package com.cos.blogapp.handler.ex; /** * * @author dahyechoi 2021.09.16 * 1.id를 못찾았을 때 사용 * * */ public class MyAsyncNotFountException extends RuntimeException{ public MyAsyncNotFountException(String msg) { super(msg); } }
2.GlobalExceptionHandler에 메서드를 추가한다.
@ExceptionHandler(value = MyAsyncNotFountException.class) public @ResponseBody String error2(MyAsyncNotFountException e) { System.out.println("Error:"+e.getMessage()); return "fail"; }
3.BoardController.java에서 삭제하기 메서드를 추가한다.(유효성 체크)
@DeleteMapping("/board/{id}") public @ResponseBody String deleteById(@PathVariable int id) { try { boardRepository.deleteById(id); // 게시글 id가 없으면 오류 발생 (Empty result data Exception) -> try ~ catch 처리 } catch (Exception e) { throw new MyAsyncNotFountException(id+"를 찾을 수 없어서 삭제할 수 없습니다."); } return "ok"; }
4.상세보기 페이지에서 코드를 수정한다.
<button id="deleteBtn" class="btn btn-danger" onclick="deleteById(${boardEntity.id})">삭제</button> <script> async function deleteById(id){ let response = await fetch("http://localhost:8000/board/"+id, { method : "DELETE" }); let parseResponse = await response.text(); console.log(parseResponse); if(parseResponse == "ok"){ alert("삭제 성공"); location.href="/"; }else{ alert("삭제 실패"); location.href="/"; } }
들어오는 데이터 타입 포용성을 위한 코드 추가
데이터가 어떤 타입으로 들어올지 결정할 수 없으므로 제네릭 타입으로 변경한다.
package com.cos.blogapp.web.dto; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @Data @NoArgsConstructor @AllArgsConstructor public class CMRespDto<T> { // private String data; //fail or ok -> 다른 데이터 들어올 때 안 좋음 private int code; //제네릭으로 동적으로 만든다. private T body; }
게시글 삭제 인증/권한 체크
해당 글에 인증과 권한을 가진 사용자에게 게시글 삭제를 할 수 있게 만든다.
@DeleteMapping("/board/{id}") public @ResponseBody CMRespDto<String> deleteById(@PathVariable int id) { // 인증이 된 사람만 접근 가능!!(로그인 된 사람) User principal = (User) session.getAttribute("principal"); if (principal == null) { throw new MyAsyncNotFoundException("인증이 되지 않았습니다."); } // 권한이 있는 사람 함수 접근 가능(principal.id == {id}) Board boardEntity = boardRepository.findById(id) .orElseThrow(() -> new MyAsyncNotFoundException("해당글을 찾을 수 없습니다.")); if (principal.getId() != boardEntity.getUser().getId()) { throw new MyAsyncNotFoundException("해당글을 삭제할 권한이 없습니다."); } try { boardRepository.deleteById(id); } catch (Exception e) { throw new MyAsyncNotFoundException(id + "를 찾을 수 없어서 삭제할 수 없습니다."); } return new CMRespDto<String>(1, null); }
예외처리 재사용하기 위한 코드 변경
메시지를 추가해서 다양한 예외처리에 대응할 수 있게 코드를 짠다.
package com.cos.blogapp.web.dto; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @Data @NoArgsConstructor @AllArgsConstructor public class CMRespDto<T> { private int code; private String msg; private T body; }
@ExceptionHandler(value = MyAsyncNotFoundException.class) public @ResponseBody CMRespDto<String> error2(MyAsyncNotFoundException e) { System.out.println("Error:"+e.getMessage()); return new CMRespDto<String>(-1, e.getMessage(), null); }
@DeleteMapping("/board/{id}") public @ResponseBody CMRespDto<String> deleteById(@PathVariable int id) { // 인증이 된 사람만 접근 가능!!(로그인 된 사람) User principal = (User) session.getAttribute("principal"); if (principal == null) { throw new MyAsyncNotFoundException("인증이 되지 않았습니다."); } // 권한이 있는 사람 함수 접근 가능(principal.id == {id}) Board boardEntity = boardRepository.findById(id) .orElseThrow(() -> new MyAsyncNotFoundException("해당글을 찾을 수 없습니다.")); if (principal.getId() != boardEntity.getUser().getId()) { throw new MyAsyncNotFoundException("해당글을 삭제할 권한이 없습니다."); } try { boardRepository.deleteById(id); // 게시글 id가 없으면 오류 발생 (Empty result data Exception) -> try ~ catch 처리 } catch (Exception e) { throw new MyAsyncNotFoundException(id + "를 찾을 수 없어서 삭제할 수 없습니다."); } return new CMRespDto<String>(1, "성공",null); }
<c:if test="${sessionScope.principal.id == boardEntity.user.id}"> <a href="#" class="btn btn-warning">수정</a> <button class="btn btn-danger" onclick="deleteById(${boardEntity.id})">삭제</button> </c:if> <script> async function deleteById(id){ let response = await fetch("http://localhost:8000/board/"+id, { method : "DELETE" }); //json() 함수는 json처럼 생긴 문자열을 자바스크립트 오브젝트로 변환해준다. let parseResponse = await response.json(); console.log(parseResponse); if(parseResponse.code == 1){ alert("삭제 성공"); location.href="/"; }else{ alert("삭제 실패"); location.href="/"; } } </script>
글수정하기
GET 요청으로 수정하기 페이지 불러오기
1.BoardController.java에서 수정하기 페이지로 이동할 메서드를 추가한다.(유효성 체크)
@GetMapping("/board/{id}/updateForm") public String boardUpdateForm(@PathVariable int id, Model model) { // 게시글 정보 Board boardEntity = boardRepository.findById(id) .orElseThrow(() -> new MyNotFoundException(id+"번의 게시글을 찾을 수 없습니다.")); model.addAttribute("boardEntity",boardEntity); return "board/updateForm"; }
2.detail.jsp 파일에서 수정하기 버튼 클릭시 수정하기 페이지로 이동할 수 있게 경로를 수정한다.
<c:if test="${sessionScope.principal.id == boardEntity.user.id}"> <a href="/board/${boardEntity.id}/updateForm" class="btn btn-warning">수정</a> <button class="btn btn-danger" onclick="deleteById(${boardEntity.id})">삭제</button> </c:if>
3.saveForm.jsp를 복사해서 일부 수정하여 updateForm.jsp 파일을 만들고 게시글 데이터를 바인딩한다.
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <%@ include file="../layout/header.jsp" %> <div class="container"> <form onsubmit="update(event, ${boardEntity.id})" > <div class="form-group"> <input id="title" type="text" value="${boardEntity.title}" class="form-control" placeholder="Enter title" > </div> <div class="form-group"> <textarea id="content" class="form-control" rows="5" > ${boardEntity.content} </textarea> </div> <button type="submit" class="btn btn-primary">수정하기</button> </form> </div> <script> async function update(event, id){ event.preventDefault(); let boardUpdateDto = { title: document.querySelector("#title").value, content: document.querySelector("#content").value, }; let response = await fetch("http://localhost:8000/board/"+id, { method: "put", body: JSON.stringify(boardUpdateDto), headers: { "Content-Type": "application/json; charset=utf-8" } }); let parseResponse = await response.json(); console.log(parseResponse); if(parseResponse.code == 1){ alert("업데이트 성공"); location.href="/board/"+id }else{ alert("업데이트 실패"); } } $('#content').summernote({ height: 350 }); </script> <%@ include file="../layout/footer.jsp" %>
PUT 요청으로 수정하기
1.BoardController.java에서 수정하기 메서드를 추가한다.
@PutMapping("/board/{id}") public @ResponseBody CMRespDto<String> update(@PathVariable int id, @RequestBody @Valid BoardSaveReqDto dto, BindingResult bindingResult){ User principal = (User) session.getAttribute("principal"); Board board = dto.toEntity(principal); board.setId(id); // update의 핵심 boardRepository.save(board); return new CMRespDto<String>(1, "업데이트 성공",null); }
2.updateForm.jsp에서 게시글 삭제 후에 메인 페이지로 이동하게 만들어준다.
if(parseResponse.code == 1){ alert("삭제 성공"); location.href="/"; }else{ alert("삭제 실패"); location.href="/"; }
게시글 수정 인증/권한 체크
@PutMapping("/board/{id}") public @ResponseBody CMRespDto<String> update(@PathVariable int id, @RequestBody @Valid BoardSaveReqDto dto, BindingResult bindingResult){ //******bindingResult는 dto 다음에 와야한다. //@RequestBody -> json 데이터를 javascript 오브젝으로 변경해준다. //~공통로직 처리~ : aop 관점지향프로그램 //유효성검사 if (bindingResult.hasErrors()) { Map<String, String> errorMap = new HashMap<>(); for (FieldError error : bindingResult.getFieldErrors()) { errorMap.put(error.getField(), error.getDefaultMessage()); } throw new MyAsyncNotFoundException(errorMap.toString()); } //인증 체크 User principal = (User) session.getAttribute("principal"); if (principal == null) { throw new MyAsyncNotFoundException("인증이 되지 않았습니다."); } //권한 체크 Board boardEntity = boardRepository.findById(id) .orElseThrow(() -> new MyAsyncNotFoundException("해당 게시글을 찾을 수 없습니다.")); if (principal.getId() != boardEntity.getUser().getId()) { throw new MyAsyncNotFoundException("해당 게시글을 수정할 권한이 없습니다."); } //~핵심기능~ Board board = dto.toEntity(principal); board.setId(id); // update의 핵심 boardRepository.save(board); return new CMRespDto<String>(1, "업데이트 성공",null); }
게시글 부가기능
스프링부트 블로그 만들기 - 페이징작은 프로젝트를 하나 만들어 보면서 스프링부트를 다루는 법을 배워요. 단순히 코드를 따라 치는 것이 아닌 프로그램이 돌아가는 원리를 이해합시다....