AngularJS 를 활용하여 CartView 생성하기.
Cart메뉴 생성
사용자가 로그인하면 Cart메뉴가 보이도록 Cart메뉴를 생성한다.
<c:if test="${pageContext.request.userPrincipal.name != 'admin'}">
<li class="nav-item"><a class="nav-link" href="<c:url value="/cart"/> ">Cart</a></li>
</c:if>
위의 코드를 menu.jsp애 추가
-menu.jsp
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<header>
<nav class="navbar navbar-expand-md navbar-dark fixed-top bg-dark">
<a class="navbar-brand" href="#">Carousel</a>
<button class="navbar-toggler" type="button" data-toggle="collapse"
data-target="#navbarCollapse" aria-controls="navbarCollapse"
aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarCollapse">
<ul class="navbar-nav mr-auto">
<li class="nav-item active"><a class="nav-link"
href="<c:url value="/"/>">Home <span class="sr-only">(current)</span></a>
</li>
<li class="nav-item"><a class="nav-link"
href="<c:url value="/products"/>">Products</a></li>
<%-- <li class="nav-item"><a class="nav-link"
href="<c:url value="/admin"/>">Admin</a>
</li> --%>
<c:if test="${pageContext.request.userPrincipal.name != null }">
<c:if test="${pageContext.request.userPrincipal.name == 'admin'}">
<li class="nav-item"><a class="nav-link"
href="<c:url value="/admin"/> ">AdminPage</a></li>
</c:if>
<c:if test="${pageContext.request.userPrincipal.name != 'admin'}">
<li class="nav-item"><a class="nav-link"
href="<c:url value="/cart"/> ">Cart</a></li>
</c:if>
<li class="nav-item"><a class="nav-link"
href="javascript:document.getElementById('logout').submit()">Logout</a>
</li>
<form id="logout" action="<c:url value="/logout"/>" method="post">
<input type="hidden" name="${_csrf.parameterName }"
value="${_csrf.token }" />
</form>
</c:if>
<c:if test="${pageContext.request.userPrincipal.name == null }">
<li class="nav-item"><a class="nav-link"
href="<c:url value="/login"/> ">Login</a></li>
</c:if>
<c:if test="${pageContext.request.userPrincipal.name == null }">
<li class="nav-item"><a class="nav-link"
href="<c:url value="/register"/> ">Register</a></li>
</c:if>
</ul>
<form class="form-inline mt-2 mt-md-0">
<input class="form-control mr-sm-2" type="text" placeholder="Search"
aria-label="Search">
<button class="btn btn-outline-success my-2 my-sm-0" type="submit">Search</button>
</form>
</div>
</nav>
</header>
if문을 활용하여 로그인했을 경우 ( ${pageContext.request.userPrincipal.name != null } )
그중 admin이 아닐 경우 ( ${pageContext.request.userPrincipal.name != 'admin'} ) 에만 Cart메뉴가 보이도록 설정.
- Cart메뉴가 생성된 모습.
CartController.java 생성
package kr.ac.hansung.cse.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import kr.ac.hansung.cse.model.User;
import kr.ac.hansung.cse.service.UserService;
@Controller
@RequestMapping("/cart")
public class CartController {
@Autowired
private UserService userService;
@RequestMapping
public String getCart(Model model) {
//현재 로그인한 User의 CartId를 가져온다.
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String username = authentication.getName();
User user = userService.getUserByUsername(username);
int cartId = user.getCart().getId();
model.addAttribute("cartId", cartId);
return "cart";
}
}
tiles.xml에 추가
<definition name="cart" extends="base">
<put-attribute name="title" value="Cart information page" />
<put-attribute name="body" value="/WEB-INF/views/cart.jsp" />
</definition>
Cart.jsp 생성
<%@ taglib uri="http://www.springframework.org/tags" prefix="spring"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<script src="<c:url value="/resources/js/controller.js"/>"></script>
<div class="container-wrapper">
<div class="container">
<div class="jumbotron">
<div class="container">
<h2>Cart</h2>
<p>All the selected products in YOUR shopping cart</p>
</div>
</div>
<section class="container" ng-app="cartApp">
<div ng-controller="cartCtrl" ng-init="initCartId('${cartId}')">
<a class="btn btn-warning pull-left" ng-click="clearCart()">
<i class="fa fa-trash"></i> Clear Cart
</a>
<br/>
<table class="table table-hover">
<tr>
<th>Product</th>
<th>Unit Price</th>
<th>Quantity</th>
<th>Total Price</th>
<th>Action</th>
</tr>
<tr ng-repeat="item in cart.cartItems">
<td>{{item.product.name}}</td>
<td>{{item.product.price}}</td>
<td>{{item.quantity}}</td>
<td>{{item.totalPrice}}</td>
<td><a class="btn btn-danger" ng-click="removeFromCart(item.product.id)">
<i class="fa fa-minus"></i>remove</a></td>
</tr>
<tr>
<td></td>
<td></td>
<td>Grand Total</td>
<td>{{calGrandTotal()}}</td>
<td></td>
</tr>
</table>
<a class="btn btn-info" href="<c:url value="/products" />" class="btn btn-default">Continue Shopping</a>
</div>
</section>
</div>
</div>
<script src="<c:url value="/resources/js/controller.js"/>"></script>
앵귤러로 구현한 contoller.js를 사용하기 위한 구문.
- controller.js 와 비교하며 기능 확인하기
var cartApp = angular.module('cartApp', []);
cartApp.controller("cartCtrl", function($scope, $http) {
$scope.initCartId = function(cartId){
$scope.cartId = cartId;
$scope.refreshCart();
};
$scope.refreshCart = function(){
/*get메서드를 통해 rest server 로부터 cart 정보를 가져와 저장함.*/
$http.get('/eStore/api/cart/' + $scope.cartId).then(
function successCallback(response) {
$scope.cart = response.data;
});
};
$scope.clearCart = function(){
$http({
method : 'DELETE',
url : '/eStore/api/cart/' + $scope.cartId
}). then(function successCallback(){
$scope.refreshCart();
}, function errorCallback(response){
console.log(response.data);
});
};
$scope.addToCart = function(productId){
$http.put('/eStoer/api/cart/add/' + productId). then(
function successCallback(){
alert("Product successfully added to the cart!");
}, function errorCallback(){
alert("Adding to the cart failed!");
});
};
$scope.removeFromCart = function(productId){
$http({
method : 'DELETE',
url : '/eStore/api/cart/cartitem/' + productId
}).then(function successCallback(){
$scope.refreshCart();
}, function errorCallback(response){
console.log(response.data);
});
};
$scope.calGrandTotal = function(){
var grandTotal = 0;
for(var i = 0 ; i < $scope.cart.cartItems.length; i++){
grandTotal += $scope.cart.cartItems[i].totalPrice;
}
return grandTotal;
};
});
- CartController.java
@RequestMapping
public String getCart(Model model) {
//현재 로그인한 User의 CartId를 가져온다.
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String username = authentication.getName();
User user = userService.getUserByUsername(username);
System.out.println("user" + user);
int cartId = user.getCart().getId();
model.addAttribute("cartId", cartId);
return "cart";
}
제품을 장바구니에 담는 버튼 구현
angulerJs 사용하기 위해
- <script src="<c:url value="/resources/js/controller.js"/>"></script>
- <div class="container" ng-app="cartApp"> -- ng-app="cartApp"추가
- <div class="row" ng-controller="cartCtrl"> --ng-controller="cartCtrl" 추가.
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<script src="<c:url value="/resources/js/controller.js"/>"></script>
<div class="container-wrapper">
<div class="container" ng-app="cartApp">
<h2>All Detail</h2>
<p class = "lead"> Here is the detail information of the product</p>
<div class="row" ng-controller="cartCtrl">
<div class="col-md-6">
<img src="<c:url value="/resources/images/${product.imageFilename}"/>" alt ="image" style="width:80%"/>
</div>
<div class="col-md-6">
<h3>${product.name}</h3>
<p><strong> Description : </strong> ${product.description}</p>
<p><strong> Manufacturer : </strong> ${product.manufacturer}</p>
<p><strong> Category : </strong> ${product.category}</p>
<p><strong> Price : </strong> ${product.price} 원</p>
<br>
<!-- 로그인을 했는지 확인 (null이 아니면 로그인) -->
<c:if test="${pageContext.request.userPrincipal.name != null}">
<p>
<a href = "<c:url value="/products"/>" class="btn btn-danger">Back</a>
<button class = "btn btn-warning btn large" ng-click="addToCart('${product.id}')">
<i class = "fa fa-shopping-cart"></i>Order now
</button>
<a href = "<c:url value="/cart"/>" class="btn btn-info">
<i class = "fa fa-eye"></i>View Cart
</a>
</p>
</c:if>
</div>
</div>
</div>
</div>
viewProduct.java
@RequestMapping("/viewProduct/{productId}")
public String viewProduct(@PathVariable int productId, Model model) {
Product product = productService.getProductById(productId);
model.addAttribute("product", product);
return "viewProduct";
}
에서 모델에 넣어준 product를 활용해 viewProduct 페이지에서 product에 대한 접근이 가능해진다.
Adding to the cart failed! 카트에 데이터를 넣는 것에 실패했다.
CSRF(Cross-Site Request Forgery)
- 사이트 간 요청 위조
- 일반 사용자가 악의적인 공격자에 의해 '등록, 수정, 삭제'등의 행위를 특정 웹사이트에 요청하도록 만드는 공격이다.
Spring Security는 CSRF 공격 방어가 활성화되어있어,
Could not verify the provided CSRF token because your session was not found.
등의 400번대 오류가 발생한다.
이를 방지하기 위해 csrf token을 넣어준다.
( CSRF 토큰 필터는 GET방식의 데이터 전송에는 관여하지 않는다. )
spring form (sf) 은 자동으로 csrf token을 보내주지만 html form은 token을 직접 넣어주어야 한다.
form이 없는 경우는 밑의 코드처럼 meta tag를 활용한다.
<html>
<head>
<meta name="_csrf" content="${_csrf.token}"/>
<!-- default header name is X-CSRF-TOKEN -->
<meta name="_csrf_header" content="${_csrf.headerName}"/>
<!-- ... -->
</head>
<!-- ... -->
https://docs.spring.io/spring-security/site/docs/5.0.x/reference/html/csrf.html
layout.jsp 밑의 태그를 추가한다.
<meta name="_csrf" content="${_csrf.token}"/>
<meta name="_csrf_header" content="${_csrf.headerName}"/>
- layout.jsp
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib uri="http://tiles.apache.org/tags-tiles" prefix="tiles"%>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport"
content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="">
<meta name="author" content="">
<meta name="_csrf" content="${_csrf.token}"/>
<meta name="_csrf_header" content="${_csrf.headerName}"/>
...
controller.js에
$scope.setCsrfToken = function() {
var csrfToken = $("meta[name='_csrf']").attr("content");
var csrfHeader = $("meta[name = '_csrf_header']").attr("content");
/*http의 header 정보에 csrftoken 값이 담겨지게된다.*/
$http.defaults.headers.common[csrfHeader] = csrfToken;
}
를 추가하고 clearCart, addToCart, removeCart에서 호출해준다.
- controller.js
var cartApp = angular.module('cartApp', []);
cartApp.controller("cartCtrl", function($scope, $http) {
$scope.initCartId = function(cartId){
$scope.cartId = cartId;
$scope.refreshCart();
};
$scope.refreshCart = function(){
/*get메서드를 통해 rest server 로부터 cart 정보를 가져와 저장함.*/
$http.get('/eStore/api/cart/' + $scope.cartId).then(
function successCallback(response) {
$scope.cart = response.data;
});
};
$scope.clearCart = function(){
$scope.setCsrfToken();
$http({
method : 'DELETE',
url : '/eStore/api/cart/' + $scope.cartId
}). then(function successCallback(){
$scope.refreshCart();
}, function errorCallback(response){
console.log(response.data);
});
};
$scope.addToCart = function(productId){
$scope.setCsrfToken();
$http.put('/eStoer/api/cart/add/' + productId). then(
function successCallback(){
alert("Product successfully added to the cart!");
}, function errorCallback(){
alert("Adding to the cart failed!");
});
};
$scope.removeFromCart = function(productId){
$scope.setCsrfToken();
$http({
method : 'DELETE',
url : '/eStore/api/cart/cartitem/' + productId
}).then(function successCallback(){
$scope.refreshCart();
}, function errorCallback(response){
console.log(response.data);
});
};
$scope.calGrandTotal = function(){
var grandTotal = 0;
for(var i = 0 ; i < $scope.cart.cartItems.length; i++){
grandTotal += $scope.cart.cartItems[i].totalPrice;
}
return grandTotal;
};
$scope.setCsrfToken = function() {
var csrfToken = $("meta[name='_csrf']").attr("content");
var csrfHeader = $("meta[name = '_csrf_header']").attr("content");
/*http의 header 정보에 csrftoken 값이 담겨지게된다.*/
$http.defaults.headers.common[csrfHeader] = csrfToken;
}
});
request header 에 X-CSRF_TOKEN 라는 헤더 이름으로 csrf token이 들어가 있는 것을 확인할 수 있다.
에러 1 : 사이클 생성 에러
위와 같이 toString 에서 오버플로우 에러가 발생했다.
원인은 Cart와 CartItem사이에서 서로를 호출하며 사이클이 생겨서였는데, 이를 방지하기 위해 @JsonIgnore 를 사용했음에도 이런 오류가 발생하였다.
그러나 구글링을 통해 문제를 해결할 수 있었다.
@ToString(exclude = "cart")
- ToString의 exclude 속성을 사용하면, 특정 필드를 toString() 결과에서 제외할 수 있다.
- CartItem.java
package kr.ac.hansung.cse.model;
import java.io.Serializable;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@Getter
@Setter
@ToString(exclude = "cart")
@Entity
public class CartItem implements Serializable {
// version id
private static final long serialVersionUID = -7296960050350583877L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
@ManyToOne
@JoinColumn(name = "cartId")
@JsonIgnore
private Cart cart;
@ManyToOne
@JoinColumn(name = "productId")
private Product product;
private int quantity;
private double totalPrice;
}
Cart가 CartItem을 가져오는 데에 사이클이 형성되지 않고 정상적으로 작동할 수 있었다.
에러 2 : product update 하면 파일 못 찾는 오류
해당 이미지처럼, Product를 수정하고 submit을 누를 경우 이미지 파일이 없어지는 오류를 발견하게 되었다.
로그를 확인한 결과 imageFilename에 null 값이 들어가기 때문이었다.
hidden 태그를 이용하여 updateProduct.jsp에서 파일 네임을 보낼 수 있도록 하였다.
<input type=“hidden”>
사용자에게는 보이지 않는 숨겨진 입력 필드를 정의
- 숨겨진 입력 필드는 렌더링이 끝난 웹 페이지에서는 전혀 보이지 않으며, 페이지 콘텐츠 내에서 그것을 볼 수 있게 만드는 방법도 없다.
- 따라서 숨겨진 입력 필드는 폼 제출 시 사용자가 변경해서는 안 되는 데이터를 함께 보낼 때 유용하게 사용된다.
- 이러한 입력 필드는 업데이트되어야 하는 데이터베이스의 레코드를 저장하거나, 고유한 보안 토큰 등을 서버로 보낼 때 주로 사용된다.
- updateProduct.jsp
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<%@ taglib uri="http://www.springframework.org/tags/form" prefix="sf"%>
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<div class="container-wrapper">
<div class="container">
<h1>Update Product</h1>
<p class="lead">Fill the below information to add a product: </p>
<sf:form action="${pageContext.request.contextPath}/admin/productInventory/updateProduct?${_csrf.parameterName}=${_csrf.token}"
method="post" modelAttribute="product" enctype="multipart/form-data">
...
<sf:hidden path="imageFilename"/>
...
<input type="submit" value="submit" class="btn btn-default">
<a href="<c:url value="/admin/productInventory" />" class="btn btn-default">Cancel</a>
</sf:form>
<br/>
</div>
</div>
페이지 테스트
1. remove 버튼
- DELETE 요청 메서드가 보내진 것을 알 수 있다.
- 204로 response가 넘어온다.
CartRestController.java 에서
@RequestMapping(value="/cartitem/{productId}", method = RequestMethod.DELETE)
public ResponseEntity<Void> removeItem(@PathVariable(value="productId") int productId){
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String username = authentication.getName();
User user = userService.getUserByUsername(username);
Cart cart = user.getCart();
CartItem cartItem = cartItemService.getCartItemByProductId(cart.getId(),productId);
cartItemService.removeCartItem(cartItem);
return new ResponseEntity<Void>(HttpStatus.NO_CONTENT);
}
return new ResponseEntity<Void>(HttpStatus.NO_CONTENT);
NO_CONTENT = 204로 응답을 했기 때문이다.
- NO_CONTENT는 서버가 요청을 성공적으로 처리했지만 제공할 컨텐츠는 없음을 의미한다.
- controller.js
$scope.removeFromCart = function(productId){
$scope.setCsrfToken();
$http({
method : 'DELETE',
url : '/eStore/api/cart/cartitem/' + productId
}).then(function successCallback(){
$scope.refreshCart();
}, function errorCallback(response){
console.log(response.data);
});
};
...
$scope.refreshCart = function(){
/*get메서드를 통해 rest server 로부터 cart 정보를 가져와 저장함.*/
$http.get('/eStore/api/cart/' + $scope.cartId).then(
function successCallback(response) {
$scope.cart = response.data;
});
};
위의 코드에서 DELETE후엔 refresh를 해주어 REST SERVER로부터 CART 의 정보를 가져와 다시 저장하게 되는데,
따라서 응답(Response)을 보면 cart 객체를 시리얼라이제이션 해서 나온 json 포맷을 볼 수 있다.
{
"id":3,
"cartItems":[
{
"id":4,
"product":{
"id":14,
"name":"TV",
"category":"가전",
"price":8000000,
"manufacturer":"큰 화면",
"unitInStock":10,
"description":"엘지전자",
"productImage":null,
"imageFilename":"b63c2d5c-304d-47c6-b293-7df155bae343_TV.jpg"
},
"quantity":2,
"totalPrice":1.6E7
},
{
"id":6,
"product":{
"id":16,
"name":"운동화",
"category":"잡화",
"price":27000,
"manufacturer":"경량운동화",
"unitInStock":5,
"description":"아디다스",
"productImage":null,
"imageFilename":"329eefaf-4aaa-41fd-b36a-c5ad045fcf80_운동화.jpg"
},
"quantity":3,
"totalPrice":81000.0
}
],
"grandTotal":0.0
}
...
여기까지 Spring MVC + AngularJS 를 활용한 Store.제작 끝!
이전에 못 올린 내용들은 깃에 조금씩 올렸었는데, 다시 보기 좋게 블로그에 글을 써놔야겠다!
https://www.daleseo.com/lombok-popular-annotations/
http://www.tcpschool.com/html-input-types/hidden
[보안/인증] CSRF 로 인해서, 403에러가 발생했을 때 (tistory.com)
HTTP Status code / HTTP 상태 코드 정리 :: Velog (tistory.com)
'Study > Spring' 카테고리의 다른 글
[Spring] Store - Cart View (장바구니) / AngularJS - RestAPI (0) | 2022.08.25 |
---|---|
[Spring] Store - CartModel / REST API (0) | 2022.08.23 |
[Spring] Store - Product Detail (0) | 2022.08.20 |
[Spring] Store - Register User / Spring security (0) | 2022.08.18 |
[Spring] 파일 업로드 중복 제거 - UUID (0) | 2022.08.13 |