스프링부트 블로그 만들기 – 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);
}
게시글 부가기능
스프링부트 블로그 만들기 - 페이징작은 프로젝트를 하나 만들어 보면서 스프링부트를 다루는 법을 배워요. 단순히 코드를 따라 치는 것이 아닌 프로그램이 돌아가는 원리를 이해합시다....


