파트 8: 장바구니와 Ajax 업데이트

등록일시: 2012-08-17 13:34,  수정일시: 2013-09-12 23:11
조회수: 6,673
이 문서는 ASP.NET MVC 기술을 널리 알리고자 하는 개인적인 취지로 제공되는 번역문서입니다. 이 문서에 대한 모든 저작권은 마이크로소프트에 있으며 요청이 있을 경우 언제라도 게시가 중단될 수 있습니다. 번역 내용에 오역이 존재할 수 있고 주석은 번역자 개인의 의견일 뿐이며 마이크로소프트는 이에 관한 어떠한 보장도 하지 않습니다. 번역이 완료된 이후에도 대상 제품 및 기술이 개선되거나 변경됨에 따라 원문의 내용도 변경되거나 보완되었을 수 있으므로 주의하시기 바랍니다.

MVC 뮤직 스토어 응용 프로그램은 ASP.NET MVC와 Visual Studio for Web Development를 소개하고, 그 사용법을 단계별로 살펴보기 위한 자습용 응용 프로그램으로, 온라인 음반 판매 기능을 비롯하여, 기초적인 사이트 관리, 사용자 로그인, 장바구니 기능 등이 구현된 간단한 전자상거래 사이트 예제입니다.

본 자습서 시리즈에서는 ASP.NET MVC 뮤직 스토어 응용 프로그램을 구축하기 위해서 필요한 모든 단계들을 자세하게 살펴볼 것입니다. 이번 파트 8에서는 장바구니와 Ajax 업데이트에 관해서 살펴봅니다.

이번 파트에서는 장바구니를 만들어보려고 합니다. 사용자 등록 없이도 장바구니에 음반을 담을 수는 있지만 주문을 마치려면 반드시 등록이 필요하도록 구현해볼 계획입니다. 그리고, 전체적인 쇼핑과 결제 과정은 누구나 장바구니에 음반을 담을 수 있도록 만들어주는 ShoppingCart 컨트롤러와 최종 결제 과정을 처리를 수행해주는 Checkout 컨트롤러, 이렇게 두 개의 컨트롤러로 분리해서 구현해볼 것입니다. 먼저, 이번 파트에서는 장바구니까지만 구현해보고, 다음 파트에서 결제 과정을 이어서 구현해보도록 하겠습니다.

Cart, Order, OrderDetail 모델 클래스 추가하기

장바구니와 결제 처리에서는 몇 가지 새로운 클래스들이 사용됩니다. 먼저, Models 폴더를 마우스 오른쪽 버튼으로 클릭한 다음, Cart 클래스(Cart.cs)를 추가하고 다음의 코드를 작성합니다.

using System.ComponentModel.DataAnnotations; 

namespace MvcMusicStore.Models 
{ 
    public class Cart 
    { 
        [Key] 
        public int     RecordId    { get; set; } 
        public string  CartId      { get; set; } 
        public int     AlbumId     { get; set; } 
        public int     Count       { get; set; } 
        public System.DateTime DateCreated { get; set; } 
        public virtual Album Album { get; set; } 
    } 
}

이 클래스는 지금까지 살펴봤던 다른 모델 클래스들과 매우 비슷합니다. 유일한 차이점은 RecordId 속성에 적용되어 있는 [Key] 어트리뷰트 뿐입니다. 각각의 장바구니 항목들은 CartId라는 이름의 문자열 식별자를 갖고 있지만, 이 속성은 단지 익명 쇼핑을 가능하게 만들어주는 용도로 사용될 뿐, 실제로 테이블에서 기본 키로 사용되는 속성은 [Key] 어트리뷰트가 적용되어 있는 RecordId라는 이름의 정수형 속성입니다. Entity Framework Code First는 기본 규약에 따라, 테이블 이름이 Cart인 경우, 그 기본 키로 CartId나 ID라는 속성이 사용될 것이라고 기대하지만, 필요한 경우 이 예제 코드처럼 주석이나 코드를 사용해서 간단히 재지정할 수 있습니다. 이는 Entity Framework Code First의 간결한 규약을 적절히 이용할 수 있는 방법과, 불필요한 경우 해당 규약에 구애받지 않을 수 있는 방법을 보여주는 좋은 예라고 말할 수 있습니다.

계속해서 Order 클래스(Order.cs)를 추가하고 다음의 코드를 작성합니다.

using System.Collections.Generic; 

namespace MvcMusicStore.Models 
{ 
    public partial class Order 
    { 
        public int    OrderId    { get; set; } 
        public string Username   { get; set; } 
        public string FirstName  { get; set; } 
        public string LastName   { get; set; } 
        public string Address    { get; set; } 
        public string City       { get; set; } 
        public string State      { get; set; } 
        public string PostalCode { get; set; } 
        public string Country    { get; set; } 
        public string Phone      { get; set; } 
        public string Email      { get; set; } 
        public decimal Total     { get; set; } 
        public System.DateTime OrderDate      { get; set; } 
        public List<OrderDetail> OrderDetails { get; set; } 
    } 
}

이 클래스에는 주문 요약 정보와 배송 정보가 담겨지게 되겠지만, 아직 컴파일 되지는 않을 것입니다. 작성되지 않은 클래스를 참조하는 OrderDetails 탐색 속성이 존재하기 때문입니다. 그러므로 이번에는 OrderDetail.cs 클래스를 추가한 다음, 다음의 코드를 작성하여 이 문제점을 해결해보겠습니다.

namespace MvcMusicStore.Models 
{ 
    public class OrderDetail 
    { 
        public int OrderDetailId { get; set; } 
        public int OrderId { get; set; } 
        public int AlbumId { get; set; } 
        public int Quantity { get; set; } 
        public decimal UnitPrice { get; set; } 
        public virtual Album Album { get; set; } 
        public virtual Order Order { get; set; } 
    } 
}

그리고 마지막으로, DbSet<Artist>를 비롯해서, 이번 파트에서 새로 작성한 모델 클래스들을 노출하는 DbSets 속성들을 포함하도록 MusicStoreEntities 클래스를 수정합니다. 모든 작업이 완료된 MusicStoreEntities 클래스의 모습은 다음과 같습니다.

역주: 본 자습서를 지금까지 충실하게 따라왔다면, 이미 파트 5에서 수행했던 일련을 작업들을 통해서 DbSet<Artist>에 대한 속성이 존재하고 있을 것입니다. 심지어 버그를 수정하기 위해 이 속성의 이름을 변경해보기까지 했었습니다. 참고하시기 바랍니다.
using System.Data.Entity; 

namespace MvcMusicStore.Models 
{ 
    public class MusicStoreEntities : DbContext 
    {
        public DbSet<Album>       Albums  { get; set; } 
        public DbSet<Genre>       Genres  { get; set; } 
        public DbSet<Artist>      Artists { get; set; } 
        public DbSet<Cart>        Carts   { get; set; } 
        public DbSet<Order>       Orders  { get; set; } 
        public DbSet<OrderDetail> OrderDetails { get; set; } 
    } 
}

장바구니 업무 로직 관리하기

이번에는 Models 폴더에 ShoppingCart라는 모델 클래스를 생성해보겠습니다. 이 모델 클래스는 Cart 테이블에 대한 데이터 접근을 비롯해서, 장바구니에 음반을 추가하거나 제거하는 등의 업무 로직을 처리하게 될 것입니다.

MVC 뮤직 스토어에서는 사용자가 단지 자신의 장바구니에 음반을 추가하기 위해서 로그인해야 할 필요는 없도록 구현하려고 하고 있기 때문에, 사용자들이 최초에 장바구니에 접근하는 시점에, 각각의 사용자들에게 임시 고유 식별자를 (GUID, Globally Unique IDentifier를 이용해서) 부여할 것입니다. 그리고, ASP.NET의 세션 클래스를 사용해서 이 ID를 저장할 것입니다.

노트: ASP.NET의 세션은 각 사용자별 정보를 저장하기 위한 편리한 장소로, 해당 정보는 사용자들이 사이트를 떠난 이후에 만료됩니다. 대형 사이트의 경우, 세션을 오용하면 성능에 악영향을 줄 수 있지만, 지금과 같은 데모 용도로 사용하는 경우에는 별다른 무리가 없습니다.

이 ShoppingCart 모델 클래스는 다음과 같은 메서드들을 노출합니다:

  • AddToCart 메서드는 매개변수로 Album 클래스의 인스턴스를 받아서, 이 개체를 사용자의 장바구니에 추가합니다. 장바구니(Cart) 테이블은 각 음반별 수량을 명시적으로 관리하고 있기 때문에, 처음 장바구니에 담겨지는 음반인 경우 새로운 로우를 생성하거나, 이미 사용자가 해당 음반을 한 번 이상 장바구니에 담은 경우 수량만 증가시키는 등의 업무 로직이 메서드에 포함되어 있습니다.
  • RemoveFromCart 메서드는 특정 음반의 ID를 매개변수로 받아서, 해당 음반의 수량을 감소시키거나 장바구니에서 제거합니다. 만약, 사용자의 장바구니에 담겨 있는 해당 음반의 수량이 하나 뿐이라면 해당 로우가 제거됩니다.
  • EmptyCart 메서드는 사용자의 장바구니에 존재하는 모든 음반들을 제거합니다.
  • GetCartItems 메서드는 출력 및 각종 처리에 사용하기 위한 장바구니 항목(Cart 클래스)들의 목록을 반환해줍니다.
  • GetCount 메서드는 사용자의 장바구니에 들어있는 음반들의 전체 갯수를 반환해줍니다.
  • GetTotal 메서드는 장바구니에 담겨 있는 모든 음반들의 전체 합계 금액을 반환해줍니다.
  • CreateOrder 메서드는 결제 과정 중, 장바구니의 내용들을 주문으로 변환해줍니다.
  • GetCart 메서드는 컨트롤러에서 장바구니(ShoppingCart 클래스) 개체를 가져오기 위해 사용되는 정적 메서드입니다. 이 메서드는 내부적으로 GetCartId 메서드를 이용해서 사용자의 세션으로부터 CartId를 읽어오며, 그러기 위해서 HttpContextBase를 필요로 합니다.

이 ShoppingCart 클래스의 완전한 코드는 다음과 같습니다:

using System; 
using System.Collections.Generic; 
using System.Linq; 
using System.Web; 
using System.Web.Mvc; 

namespace MvcMusicStore.Models 
{ 
    public partial class ShoppingCart 
    { 
        MusicStoreEntities storeDB = new MusicStoreEntities(); 
        string ShoppingCartId { get; set; } 
        public const string CartSessionKey = "CartId"; 

        public static ShoppingCart GetCart(HttpContextBase context) 
        { 
            var cart = new ShoppingCart(); 
            cart.ShoppingCartId = cart.GetCartId(context); 
            return cart; 
        } 

        // 장바구니 호출을 간단하게 만들어주는 도우미 메서드입니다.  
        public static ShoppingCart GetCart(Controller controller)  
        {  
            return GetCart(controller.HttpContext);  
        }  

        public void AddToCart(Album album)  
        {  
            // 전달된 Album 클래스의 인스턴스와 일치하는 장바구니 항목을 가져옵니다.  
            var cartItem = storeDB.Carts.SingleOrDefault(  
                c => c.CartId == ShoppingCartId && c.AlbumId == album.AlbumId);  

            if (cartItem == null)  
            {   
                // 기존의 장바구니 항목이 존재하지 않으면,   
                // 새로운 장바구니 항목을 생성합니다.   
                cartItem = new Cart   
                {  
                    AlbumId = album.AlbumId,  
                    CartId = ShoppingCartId,  
                    Count = 1,  
                    DateCreated = DateTime.Now  
                };  
                storeDB.Carts.Add(cartItem);  
            }  
            else  
            {  
                // 장바구니에 해당 음반이 이미 존재하면,  
                // 수량을 하나 증가시킵니다.   
                cartItem.Count++;  
            }  

            // 변경사항을 저장합니다.  
            storeDB.SaveChanges();  
        }  

        public int RemoveFromCart(int id)  
        {  
            // 장바구니 항목을 가져옵니다.  
            var cartItem = storeDB.Carts.Single(   
                cart => cart.CartId == ShoppingCartId && cart.RecordId == id);   

            int itemCount = 0;  

            if (cartItem != null)  
            {  
                if (cartItem.Count > 1)  
                {  
                    cartItem.Count--;  
                    itemCount = cartItem.Count;  
                }  
                else   
                {  
                    storeDB.Carts.Remove(cartItem);  
                }  

                // 변경사항을 저장합니다.   
                storeDB.SaveChanges();   
            }  

            return itemCount;  
        }  

        public void EmptyCart()  
        {  
            var cartItems = storeDB.Carts.Where(cart => cart.CartId == ShoppingCartId);   
    
            foreach (var cartItem in cartItems)  
            {  
                storeDB.Carts.Remove(cartItem);   
            }   

            // 변경사항을 저장합니다.  
            storeDB.SaveChanges();  
        }  

        public List<Cart> GetCartItems()  
        {  
            return storeDB.Carts.Where(cart => cart.CartId == ShoppingCartId).ToList();  
        }  

        public int GetCount()  
        {  
            // 장바구니에 담긴 각 음반들의 갯수를 가져온 다음, 모두 합합니다.  
            int? count = (from cartItems in storeDB.Carts  
                          where cartItems.CartId == ShoppingCartId  
                          select (int?)cartItems.Count).Sum();  
            // 모든 항목들이 null인 경우, 0을 반환합니다.  
            return count ?? 0;  
        }  

        public decimal GetTotal()  
        {  
            // 장바구니에 담겨 있는 각각의 음반들에 대해서  
            // 음반의 단가와 갯수를 곱해서 현재 가격을 구하고,  
            // 모든 음반의 가격들을 더해서 장바구니 전체의 음반 금액을 구합니다.  
            decimal? total = (from cartItems in storeDB.Carts  
                              where cartItems.CartId == ShoppingCartId  
                              select (int?)cartItems.Count * cartItems.Album.Price).Sum();  

            return total ?? decimal.Zero;  
        }  

        public int CreateOrder(Order order)  
        {  
            decimal orderTotal = 0;  

            var cartItems = GetCartItems();  

            // 장바구니에 담겨 있는 모든 음반들을 대상으로 루프문을  
            // 수행하여, 각 음반에 대한 상세 주문을 추가합니다.  
            foreach (var item in cartItems)  
            {  
                var orderDetail = new OrderDetail  
                {  
                    AlbumId = item.AlbumId,  
                    OrderId = order.OrderId,  
                    UnitPrice = item.Album.Price,  
                    Quantity = item.Count  
                };  

                // 장바구니의 전체 합계 금액을 설정합니다.   
                orderTotal += (item.Count * item.Album.Price);   

                storeDB.OrderDetails.Add(orderDetail);  

            }   

            // order.Total 속성값을 orderTotal 변수값으로 설정합니다.   
            order.Total = orderTotal;   

            // 주문을 저장합니다.   
            storeDB.SaveChanges();   
            // 장바구니를 비웁니다.  
            EmptyCart();  
            // 주문 확인 번호로 사용하기 위해 OrderId를 반환합니다.  
            return order.OrderId;  
        }   

        // 세션에 접근하기 위해서 HttpContextBase를 사용합니다.  
        public string GetCartId(HttpContextBase context)  
        {  
            if (context.Session[CartSessionKey] == null)  
            {  
                if (!string.IsNullOrWhiteSpace(context.User.Identity.Name))  
                {  
                    context.Session[CartSessionKey] = context.User.Identity.Name;   
                }   
                else  
                {  
                    // System.Guid 클래스를 이용해서 새로운 임의의 GUID를 생성합니다.  
                    Guid tempCartId = Guid.NewGuid();  
                    // 임시 장바구니 ID(tempCartId)를 세션에 설정합니다.  
                    context.Session[CartSessionKey] = tempCartId.ToString();  
                }  
            }  
            return context.Session[CartSessionKey].ToString();  
        }  

        // 사용자가 로그인을 하면, 사용자 이름과 연결되도록  
        // 해당 사용자의 장바구니를 마이그레이션합니다.  
        public void MigrateCart(string userName)  
        {  
            var shoppingCart = storeDB.Carts.Where(c => c.CartId == ShoppingCartId);  
            foreach (Cart item in shoppingCart)  
            {  
                item.CartId = userName;  
            }  
            storeDB.SaveChanges();  
        }  
    }  
}

뷰 모델 (ViewModels)

잠시 뒤에 작성해보게 될 ShoppingCart 컨트롤러에서는 모델 개체에 깔끔하게 맵핑하기 어려운 복잡한 정보들을 뷰와 주고 받아야만 합니다. 그러나, 뷰를 기준으로 모델 클래스들을 변경할 수는 없습니다. 모델 클래스는 도메인을 반영하기 위한 것이지, 사용자 인터페이스를 반영하기 위한 것이 아니기 때문입니다. 이 문제점을 해결할 수 있는 한 가지 방법은 StoreManager 컨트롤러에서 드롭다운 정보를 전달할 때 사용했던 뷰백(ViewBag) 클래스를 이용해서 뷰로 정보를 전달하는 것입니다. 그러나, 뷰백에 너무 많은 정보를 설정하면 관리가 어려워집니다.

그 대신 권장되는 접근 방식은 뷰 모델(ViewModel) 패턴을 이용하는 것입니다. 이 패턴을 이용하면 특정 뷰 시나리오에 적합한 강력한 형식의 클래스들을 생성할 수 있으며, 뷰 템플릿에 필요한 다양한 값들과 콘텐트 속성들을 노출할 수 있습니다. 그리고, 컨트롤러 클래스에서는 뷰에 최적화된 이 클래스들에 값을 채운 다음, 이를 뷰 템플릿에 전달하여 사용할 수 있습니다. 게다가, 이 방식은 형-안정성(Type-Safety)을 보장해주고, 컴파일 시점 검사 및 뷰 템플릿에 대한 편집기에서의 인텔리센스도 지원해줍니다.

따라서 이번에는 ShoppingCart 컨트롤러에서 사용할 두 가지 뷰 모델부터 작성해보도록 하겠습니다. 먼저, ShoppingCartViewModel은 사용자의 장바구니에 담겨 있는 내용들을 담아서 뷰에 전달하는데 사용될 것입니다. 그리고, ShoppingCartRemoveViewModel은 사용자가 장바구니에서 음반을 제거할 때, 확인 정보를 출력하기 위해서 사용될 것입니다.

뷰 모델을 조직적으로 관리하기 위한 새로운 ViewModels 폴더부터 프로젝트 루트에 추가합니다. 프로젝트를 마우스 오른쪽 버튼으로 클릭한 다음, Add > New Folder를 선택합니다.

그리고, 폴더의 이름을 ViewModels로 지정합니다.

그런 다음, 이 ViewModels 폴더에 ShoppingCartViewModel 클래스를 추가합니다. 이 클래스에는 두 가지 속성이 존재하는데, 장바구니 항목(Cart 개체)들의 목록과 장바구니 내의 모든 음반들의 전체 합계 금액이 바로 그것입니다.

using System.Collections.Generic;  
using MvcMusicStore.Models;  

namespace MvcMusicStore.ViewModels  
{  
    public class ShoppingCartViewModel  
    {  
        public List<Cart> CartItems { get; set; }  
        public decimal CartTotal { get; set; }  
    }  
}

이번에는 ViewModels 폴더에 ShoppingCartRemoveViewModel 클래스를 추가하고, 다음과 같은 네 가지 속성들을 작성합니다.

namespace MvcMusicStore.ViewModels  
{  
    public class ShoppingCartRemoveViewModel  
    {  
        public string Message { get; set; }  
        public decimal CartTotal { get; set; }  
        public int CartCount { get; set; }  
        public int ItemCount { get; set; }  
        public int DeleteId { get; set; }  
    }  
}

ShoppingCart 컨트롤러

지금부터 작성하게 될 ShoppingCart 컨트롤러는 장바구니에 음반을 추가하고, 장바구니에서 음반을 제거하고, 장바구니 내부의 음반들을 보여주는 세 가지 중요한 용도를 갖고 있습니다. 그리고, 그 과정에서 이번 파트에서 작성한 세 가지 클래스들, 즉 ShoppingCartViewModel 클래스와 ShoppingCartRemoveViewModel 클래스, 그리고 ShoppingCart 클래스를 사용하게 됩니다. 또한, Store 컨트롤러나 StoreManager 컨트롤러와 마찮가지로, MusicStoreEntities의 인스턴스를 담고 있는 필드가 추가됩니다.

다음과 같이 빈 컨트롤러(Empty controller) 템플릿을 이용해서 프로젝트에 새로운 ShoppingCart 컨트롤러를 추가합니다.

다음은 완전한 ShoppingCart 컨트롤러의 코드입니다. Index 컨트롤러 액션과 AddToCart 컨트롤러 액션은 매우 익숙한 모습임을 확인할 수 있습니다. 그리고, RemoveFromCart 컨트롤러 액션과 CartSummary 컨트롤러 액션은 다음 절에서 살펴보게 될 두 가지 특별한 처리를 수행하게 됩니다.

using System.Linq;  
using System.Web.Mvc;  
using MvcMusicStore.Models;  
using MvcMusicStore.ViewModels;  

namespace MvcMusicStore.Controllers  
{  
    public class ShoppingCartController : Controller  
    {  
        MusicStoreEntities storeDB = new MusicStoreEntities();  

        //  
        // GET: /ShoppingCart/  
        public ActionResult Index()  
        {  
            var cart = ShoppingCart.GetCart(this.HttpContext);  

            // 뷰 모델을 설정합니다.  
            var viewModel = new ShoppingCartViewModel  
            {  
                CartItems = cart.GetCartItems(),  
                CartTotal = cart.GetTotal()  
            };  
            // 뷰를 반환합니다.  
            return View(viewModel);  
        }  

        //  
        // GET: /Store/AddToCart/5  
        public ActionResult AddToCart(int id)  
        {  
            // 데이터베이스에서 지정한 음반 정보(Album 개체)를 가져옵니다.  
            var addedAlbum = storeDB.Albums.Single(album => album.AlbumId == id);  
    
            // 가져온 음반 정보를 장바구니에 추가합니다.  
            var cart = ShoppingCart.GetCart(this.HttpContext);  
            cart.AddToCart(addedAlbum);  

            // 계속 쇼핑을 할 수 있도록 메인 페이지로 돌아갑니다.  
            // 역주: 이 주석은 잘못되었습니다. 코드대로라면 현재 컨트롤러의
            // Index 컨트롤러 액션, 즉 장바구니 목록 페이지로 이동하게 됩니다.
            return RedirectToAction("Index");  
        }  

        //  
        // AJAX: /ShoppingCart/RemoveFromCart/5  
        [HttpPost]  
        public ActionResult RemoveFromCart(int id)  
        {  
            // 장바구니에서 지정한 음반 정보를 제거합니다.  
            var cart = ShoppingCart.GetCart(this.HttpContext);  
    
            // 확인 메시지에 출력할 음반의 이름을 가져옵니다.  
            string albumName = storeDB.Carts.Single(item => item.RecordId == id).Album.Title;  
    
            // 장바구니에서 음반 정보를 제거합니다.  
            int itemCount = cart.RemoveFromCart(id);  
    
            // 뷰 모델에 확인 메시지를 설정합니다.  
            var results = new ShoppingCartRemoveViewModel  
            {  
                Message = Server.HtmlEncode(albumName) +  
                    " has been removed from your shopping cart.",  
                CartTotal = cart.GetTotal(),  
                CartCount = cart.GetCount(),  
                ItemCount = itemCount,  
                DeleteId = id   
            };  
            return Json(results);  
        } 

        //  
        // GET: /ShoppingCart/CartSummary  
        [ChildActionOnly]  
        public ActionResult CartSummary()  
        {  
            var cart = ShoppingCart.GetCart(this.HttpContext);  

            ViewData["CartCount"] = cart.GetCount();  
            return PartialView("CartSummary");  
        }  
    }  
}

jQuery를 이용한 Ajax 업데이트

계속해서 언제나처럼 List 뷰 템플릿을 이용해서 ShoppingCartViewModel에 대한 강력한 형식인 ShoppingCart Index 뷰 페이지를 생성해보겠습니다.

그러나, 이번에는 장바구니에서 음반을 제거하기 위해 Html.ActionLink를 사용하는 대신, jQuery를 이용해서 뷰에 존재하는 RemoveLink라는 HTML 클래스가 지정된 모든 링크들의 클릭 이벤트를 컨트롤러 액션 메서드에 연결할 것입니다. 이 클릭 이벤트는 폼을 전송하는 것이 아니라, RemoveFromCart 컨트롤러 액션에 대한 AJAX 콜백을 만들어내게 됩니다. 그 결과로 RemoveFromCart 컨트롤러 액션은 JSON으로 직렬화된 결과를 반환하고, jQuery 콜백은 이를 파싱한 다음 jQuery를 이용해서 다음과 같은 네 가지 간단한 페이지에 대한 갱신을 수행하게 됩니다.

  1. 장바구니에서 제거된 음반을 목록에서 제거합니다.
  2. 헤더의 장바구니 음반 갯수를 갱신합니다. (역주: 장바구니에 담긴 음반의 갯수를 헤더에 출력하는 작업은 파트 10에서 구현해보게 될 것입니다.)
  3. 갱신 메시지를 사용자에게 출력합니다.
  4. 장바구니의 전체 합계 금액을 갱신합니다.

장바구니에서 음반을 제거하는 작업이 Ajax 콜백으로 처리되기 때문에, RemoveFromCart 액션을 뷰를 별도로 만들 필요가 없습니다. 다음은 /ShoppingCart/Index 뷰 템플릿의 완전한 코드입니다:

역주: 이 뷰 템플릿에서도 프로젝트의 Scripts 폴더에 실제로 존재하는 jQuery 자바스크립트 라이브러리의 버전과 뷰 템플릿에 참조된 버전(jquery-1.4.4.min.js)이 일치하는지 확인해보시기 바랍니다.
@model MvcMusicStore.ViewModels.ShoppingCartViewModel   
@{  
    ViewBag.Title = "Shopping Cart";   
}  

<script src="/Scripts/jquery-1.4.4.min.js" type="text/javascript"></script>  
<script type="text/javascript">  
    $(function () {  
        // Document.ready -> "Remove from cart" 링크의 이벤트 헨들러를 설정합니다.  
        $(".RemoveLink").click(function () {  
            // 해당 링크의 id를 가져옵니다.  
            var recordToDelete = $(this).attr("data-id");  
            if (recordToDelete != '') {  
                // Ajax post를 수행합니다.  
                $.post("/ShoppingCart/RemoveFromCart", {"id": recordToDelete },  
                    function (data) {  
                        // 정상적으로 작업이 수행되고 나면,  
                        // 페이지의 요소들을 갱신합니다.  
                        if (data.ItemCount == 0) {   
                            $('#row-' + data.DeleteId).fadeOut('slow');  
                        } else {  
                            $('#item-count-' + data.DeleteId).text(data.ItemCount);  
                        }  
                        $('#cart-total').text(data.CartTotal);  
                        $('#update-message').text(data.Message);  
                        $('#cart-status').text('Cart (' + data.CartCount + ')');  
                    });  
            }  
        });  
    });  
</script>  
<h3>  
    <em>Review</em> your cart:   
</h3>  
<p class="button">   
    @Html.ActionLink("Checkout >>", "AddressAndPayment", "Checkout")
</p>  
<div id="update-message">  
</div>  
<table>  
    <tr>  
        <th>   
            Album Name   
        </th>  
        <th>   
            Price (each)   
        </th>  
        <th>   
            Quantity   
        </th>  
        <th></th>  
    </tr>   
    @foreach (var item in Model.CartItems)   
    {   
        <tr id="row-@item.RecordId">  
            <td>   
                @Html.ActionLink(item.Album.Title, "Details", "Store", new { id = item.AlbumId }, null)   
            </td>  
            <td>   
                @item.Album.Price   
            </td>  
            <td id="item-count-@item.RecordId">   
                @item.Count   
            </td>  
            <td>  
                <a href="#" class="RemoveLink" data-id="@item.RecordId">Remove from cart</a>  
            </td>  
        </tr>   
    }   
    <tr>  
        <td>   
            Total   
        </td>  
        <td>  
        </td>  
        <td>  
        </td>  
        <td id="cart-total">   
            @Model.CartTotal   
        </td>  
    </tr>  
</table>

당연한 얘기겠지만, 이 작업 결과를 테스트해보려면 장바구니에 음반을 담을 수 있어야만 합니다. 따라서, Store 컨트롤러의 Details 뷰를 수정해서 "Add to cart" 버튼을 구현해보도록 하겠습니다. 그리고, 그 과정 중에 이 뷰를 마지막으로 수정한 뒤로 추가된 몇 가지 다른 음반 정보들, 즉 장르, 아티스트, 가격, 그리고 앨범 아트 등을 출력하도록 만들어 보겠습니다. 변경된 Store 컨트롤러의 Details 뷰 템플릿의 코드는 다음과 같습니다.

@model MvcMusicStore.Models.Album   
@{   
    ViewBag.Title = "Album - " + Model.Title;   
}  

<h2>@Model.Title</h2>  
<p>  
    <img alt="@Model.Title" src="@Model.AlbumArtUrl" />  
</p>  
<div id="album-details">  
    <p>  
        <em>Genre:</em>   
        @Model.Genre.Name   
    </p>  
    <p>  
        <em>Artist:</em>   
        @Model.Artist.Name   
    </p>  
    <p>  
        <em>Price:</em>   
        @String.Format("{0:F}", Model.Price)   
    </p>  
    <p class="button">   
        @Html.ActionLink("Add to cart", "AddToCart",  "ShoppingCart", new { id = Model.AlbumId }, "")   
    </p>  
</div>

이제 장바구니에 음반을 추가하거 삭제하는 등의 테스트를 수행해 볼 수 있습니다. MVC 뮤직 스토어 응용 프로그램을 시작한 다음, Store 영역의 Index 페이지로 이동합니다.

그런 다음, 음반들의 목록을 보고자 하는 장르를 클릭합니다.

그리고 음반의 타이틀을 클릭하면, "Add to cart" 버튼이 포함되어 있는, 변경된 음반의 상세 정보 뷰가 나타나게 됩니다.

이 화면에서 "Add to cart" 버튼을 클릭하면, 장바구니 요약 목록이 제공되는 ShoppingCart 컨트롤러의 Index 뷰가 나타나게 됩니다.

장바구니 목록에서 "Remove from cart" 링크를 클릭해보면, Ajax를 통해서 장바구니가 갱신되는 것을 확인할 수 있습니다.

이번 파트에서는 등록되지 않은 사용자들도 음반을 담거나 제거할 수 있는 실제로 동작하는 장바구니를 구현해봤습니다. 계속해서 다음 파트에서는 사용자들이 등록 및 결제 과정을 완료할 수 있도록 구현해보겠습니다.

질문이나 의견이 있으시면 http://mvcmusicstore.codeplex.com/을 방문해주시기 바랍니다.