JAVA

스프링부트 블로그 만들기 – 7강 게시글

blog app

스프링부트 블로그 만들기 – 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>

 

글삭제하기

자바스크립트 비동기 프로그래밍으로 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);
    
  }

 

게시글 부가기능

blog app
스프링부트 블로그 만들기 - 페이징작은 프로젝트를 하나 만들어 보면서 스프링부트를 다루는 법을 배워요. 단순히 코드를 따라 치는 것이 아닌 프로그램이 돌아가는 원리를 이해합시다....
최신글