스프링 MVC

준비중..

스프링 MVC

스프링을 사용한 웹서비스 만들기

14 도서 리뷰 등록하기

# 도서 리뷰 등록하기 각 도서에 리뷰를 등록하는 기능을 구현한다. 이를 위해 일대다 관계에 대한 학습이 필요하다. ## 목차 1. 일대다 관계란? 2. 실습하기: 도서별 리뷰작성 - View: 폼 만들기 - Controller: 폼 데이터 받기 -> 맵퍼 호출 - Model: VO 생성 -> Mapper 등록 - DB: 테이블 생성 3. 연습문제: 음식별 재료등록 ## One-to-Many 관계란? One-to-Many 관계란 데이터베이스에서 사용하는 개념으로, 하나의 row가 다른 테이블의 여러가지 row들과 연결성을 갖는 경우를 뜻한다. ![Imgur](https://i.imgur.com/ulQ1O7p.png) 온라인 음식 주문 사이트를 예로 들어보자. 하나의 음식(`foods`)은 여러가지 재료(`ingredients`)들로 구성될 수 있다. 이 경우 음식-재료의 관계는 `one-to-many` 관계가 된다. ## 실습하기: 도서별 리뷰 생성 일대다 관계를 적용하여 각 도서별로 리뷰를 작성해보자. ### 기존 도서 상세 페이지 ![Imgur](http://i.imgur.com/4BZvKgF.png) **views/books/show.jsp** ```jsp <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%> <%@ taglib uri="http://www.springframework.org/tags/form" prefix="f" %> <%@ page pageEncoding="utf-8"%> <div class="jumbotron"> <h1>${ book.title }</h1> <p>${ book.author } 저</p> </div> <div class="thumbnail"> <img src="${ book.image }"> </div> ``` ### 폼 생성 리뷰를 등록 할 수 있도록 아래와 같은 폼을 만들자. ![Imgur](http://i.imgur.com/Jn3dfVJ.png) ```jsp <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%> <%@ taglib uri="http://www.springframework.org/tags/form" prefix="f" %> <%@ page pageEncoding="utf-8"%> <div class="jumbotron"> <h1>${ book.title }</h1> <p>${ book.author } 저</p> </div> <div class="thumbnail"> <img src="${ book.image }"> </div> <div class="page-header"> <h2>리뷰</h2> </div> <c:url var="reviewsPath" value="/reviews" /> <f:form modelAttribute="review" action="${ reviewsPath }" method="post"> <c:forEach var="error" items="${ fieldErrors }"> <div class="alert alert-warning"> <strong>${ error.getField() }</strong>: ${ error.getDefaultMessage() } </div> </c:forEach> <f:textarea path="text" cssClass="form-control" rows="5" /> <f:hidden path="bookId" /> <f:hidden path="userId" /> <button class="btn btn-block btn-primary" type="submit">리뷰 등록</button> </f:form> ``` 하지만 이게 왠걸.. 분명 에러가 날 것이다. 아래처럼.. ![Imgur](http://i.imgur.com/EK0rDgc.png) 해당 에러는 새롭게 추가한 스프링 폼태그 라이브러리에서 리뷰객체를 찾을 수 없기때문에 생기는 문제이다. 따라서 해당 컨트롤러의 메소드에 리뷰객체를 생성하고 모델에 등록을 해주어야 한다. **BooksController.java** ```java ... @Autowired private UserMapper userMapper; ... @RequestMapping(value = "/books/{id}", method = RequestMethod.GET) public String show(@PathVariable int id, Model model, Principal principal) { Book book = bookMapper.getBook(id); model.addAttribute("book", book); // 폼 태그에서 modelAttribute="review" 속성을 읽어올 수 있어야함. Review review = new Review(); review.setBookId(id); String email = principal.getName(); int userId = userMapper.getUserIdByEmail(email); review.setUserId(userId); model.addAttribute("review", review); return "books/show"; } ``` 추가로 현재 유저의 id 값을 가져올 수 있어야 한다. UserMapper.java ``` @Select("select id from users where email = #{email}") public int getUserIdByEmail(String email); ``` 이제 VO 객체인 Review 클래스를 만들어줘야 겠다. **Review.java** ```java public class Review { Integer id; String text; Integer bookId; Integer userId; public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getText() { return text; } public void setText(String text) { this.text = text; } public Integer getBookId() { return bookId; } public void setBookId(Integer bookId) { this.bookId = bookId; } public Integer getUserId() { return userId; } public void setUserId(Integer userId) { this.userId = userId; } @Override public String toString() { return "Review [id=" + id + ", text=" + text + ", bookId=" + bookId + ", userId=" + userId + "]"; } } ``` ### 폼 데이터 전송 리뷰 등록 폼에서 리뷰를 던져보도록 하자. ![Imgur](http://i.imgur.com/E7YKFx0.png) 전송될 위치는 `action` 속성에 적혀있는 url이다. **views/books/show.jsp** ```jsp ... <c:url var="reviewsPath" value="/reviews" /> <f:form modelAttribute="review" action="${ reviewsPath }" method="post"> ... ``` 역시나 에러가 난다. ![Imgur](http://i.imgur.com/cjhckvu.png) ### 컨트롤러 생성 위에서 생긴 에러는 해당 url 요청을 처리해줄 컨트롤러가 존재하지 않았기 때문에 생기는 것이다. 따라서 ReviewsController를 만들어 주어 이를 해결하자. **ReviewsController.java** ```java @Controller public class ReviewsController { @ResponseBody @RequestMapping(value = "/reviews", method = RequestMethod.POST) public String create(@ModelAttribute Review review) { return review.toString(); } } ``` 이제 다시 폼 데이터를 던져볼까?? ![Imgur](http://i.imgur.com/E7YKFx0.png) 음.. 한글이 깨지긴 했으나.. 잘 던져지는 듯 하다.. ![Imgur](http://i.imgur.com/qvwE96K.png) 혹시모르니 콘솔창에서도 찍어보았다. **ReviewsController.java** ```java @Controller public class ReviewsController { @ResponseBody @RequestMapping(value = "/reviews", method = RequestMethod.POST) public String create(@ModelAttribute Review review) { System.out.println(review); return review.toString(); } } ``` 데이터가 잘 넘어왔음을 확인했다. ![Imgur](http://i.imgur.com/OzN846Z.png) ### 맵퍼 등록 받아온 데이터를 DB로 저장해야 한다. 어떻게 해야할까? 맵퍼를 만들어 처리하자. **ReviewsController.java** ```java @Controller public class ReviewsController { @Autowired private ReviewMapper reviewMapper; @RequestMapping(value = "/reviews", method = RequestMethod.POST) public String create(@ModelAttribute Review review) { reviewMapper.create(review); return "redirect:/books/" + review.getBookId(); } } ``` 위 코드가 컴파일 될 수 있도록 먼저 ReviewMapper 인터페이스를 만들어주자. **ReviewMapper.java** ```java public interface ReviewMapper { @Insert("INSERT INTO reviews (text, book_id, user_id) VALUES (#{text}, #{bookId}, #{userId})") void create(Review review); } ``` 맵퍼를 만들었다면 이제 root-context.xml에 등록해야한다. root-context.xml ```xml ... <bean id="reviewMapper" class="org.mybatis.spring.mapper.MapperFactoryBean"> <property name="mapperInterface" value="com.mycompany.mapper.ReviewMapper" /> <property name="sqlSessionFactory" ref="sqlSessionFactory" /> </bean> ... ``` xml을 바꾸었다면, 서버 재시작은 바로바로 해주자. ### 리뷰 등록 DB 확인 리뷰 테이블을 만들어 준다. ``` create table reviews ( id serial primary key, text text, book_id integer, ); ``` 현재 DB에는 book_id 값이 2인 리뷰는 하나도 없다. ![Imgur](http://i.imgur.com/GM58sbx.png) 최종적으로 DB에 데이터가 들어가지는지 확인 해보자. ![](http://i.imgur.com/E7YKFx0.png) 리뷰 폼을 전송하니 페이지가 리다이렉팅 되었다. ![Imgur](http://i.imgur.com/4BZvKgF.png) DB를 확인해보면 데이터가 잘 들어왔음을 알 수 있다!! ![Imgur](http://i.imgur.com/kpVP7tQ.png) ### 리뷰 데이터 가져오기 이제 리뷰등록기능이 구현 되었으니, 등록된 리뷰를 볼 수 있어야 겠다. 리뷰 맵퍼를 등록하고 이를 통해 해당 리뷰들을 가져올 수 있도록 한다. **BooksController.java** ```java @Controller public class BooksController { ... @Autowired private ReviewMapper reviewMapper; ... @RequestMapping(value = "/books/{id}", method = RequestMethod.GET) public String show(@PathVariable int id, Model model, Principal principal) { Book book = bookMapper.getBook(id); model.addAttribute("book", book); // 기존 리뷰들 List<Review> reviews = reviewMapper.getReviews(id); model.addAttribute("reviews", reviews); // 폼 태그에서 modelAttribute="review" 속성을 읽어올 수 있어야함. Review review = new Review(); review.setBookId(id); String email = principal.getName(); int userId = userMapper.getUserIdByEmail(email); review.setUserId(userId); model.addAttribute("review", review); return "books/show"; } ... } ``` 위 코드가 정상 동작하기위해 ReviewMapper에 getReviews() 메소드를 추가해준다. **ReviewMapper.java** ```java public interface ReviewMapper { @Insert("INSERT INTO reviews (text, book_id, user_id) VALUES (#{text}, #{bookId}, #{userId})") void create(Review review); @Select("SELECT * FROM reviews WHERE book_id = #{bookId} ORDER BY id DESC") @Results(value = { @Result(property = "id", column = "id"), @Result(property = "text", column = "text"), @Result(property = "bookId", column = "book_id"), @Result(property = "userId", column = "user_id"), @Result(property = "user", column = "id", javaType = User.class, one = @One(select = "getUserById")) }) List<Review> getReviews(int bookId); @Select("select * from users where id = #{userId}") public User getUserById(int userId); } ``` 최종적으로 뷰페이지 통해 해당 리뷰들을 보여준다. **views/books/show.jsp** ```jsp <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%> <%@ taglib uri="http://www.springframework.org/tags/form" prefix="f"%> <%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn"%> <%@ page pageEncoding="utf-8"%> <div class="jumbotron"> <h1>${ book.title }</h1> <p>${ book.author }저</p> </div> <div class="container"> <div class="row"> <div class="col-6"> <img src="${ book.image }"> </div> <div class="col-6"> <h3>${ book.title }</h3> <p>저자: ${ book.author }</p> </div> </div> <div class="my-5"> <h3>리뷰</h3> </div> <c:if test="${ fn:length(reviews) gt 0 }"> <table class="table table-stripped"> <thead> <tr> <th>User</th> <th>Text</th> </tr> </thead> <tbody> <c:forEach var="review" items="${ reviews }"> <tr> <td>${ review.user.email }</td> <td>${ review.text }</td> </tr> </c:forEach> </tbody> </table> </c:if> <c:url var="reviewsPath" value="/reviews" /> <f:form modelAttribute="review" action="${ reviewsPath }" method="post"> <c:forEach var="error" items="${ fieldErrors }"> <div class="alert alert-warning"> <strong>${ error.getField() }</strong>: ${ error.getDefaultMessage() } </div> </c:forEach> <f:textarea path="text" cssClass="form-control" rows="5" /> <f:hidden path="bookId" /> <f:hidden path="userId" /> <button class="btn btn-block btn-primary" type="submit">리뷰 등록</button> </f:form> </div> ``` ### 최종 결과!! ![Imgur](http://i.imgur.com/hLLcSoY.png) ## 연습하기: 음식별 재료 등록 아래의 다이어그램을 참고하여 foods의 CRUD를 구현하고, 각 food 별 ingredients를 CRUD 하게 하시오. ![Imgur](https://i.imgur.com/MwJO9R2.png) ### 음식-재료 추가 설명 예를들어 에그마요 샌드위치를 클릭한 경우, 아래와 같은 정보가 표시되어야 한다. + 에그마요 샌드위치(기본가: 1000원) - 식빵 (500원) ✅ - 계란 (500원) ✅ - 마요네즈 (500원) ✅ - 샐러드: 감자,당근,오이 (500원) ✅ - 오이 피클 (500원) + 최종 가격: 3000원