login register Sysop! about ME  
qrcode
    최초 작성일 :    2012년 08월 17일
  최종 수정일 :    2012년 08월 17일
  작성자 :    songgun
  편집자 :    songgun (송 원석)
  읽음수 :    28,480

강좌 목록으로 돌아가기

필자의 잡담~

본문은 ASP.net에서 제공되는 자습서 시리즈 중, 두 번째로 MVC Music Store 관련 자습서들을 번역하여 제공해드리는 것입니다. 원문은 총 10편으로 구성되어 있으며, 각각의 분량을 고려하여 한 편씩 제공되거나 두 편이 함께 제공됩니다.

먼저 편역이 마무리 된 ASP.NET MVC 4 자습서 - 만들면서 배우기 시리즈를 읽어보지 않으신 분들은, 우선 해당 자습서 시리즈부터 읽어보시는 것을 권해드립니다.
본문은 ASP.net의 공식 MVC 관련 자습서인 Part 7: Membership and AuthorizationPart 8: Shopping Cart with Ajax Updates를 편역한 글입니다. 마이크로소프트의 공식 번역 문서가 아니며 태오 사이트 MS 컬럼 번역팀에서 번역한 내용입니다. 그렇기에, 일부 오역이나 오타가 존재할 수 있는 점 미리 양해를 드립니다. 원문에 대한 모든 저작권은 마이크로소프트에 있으며, 컬럼 내용과 관련한 질의 문답 역시 원문 사이트에 문의하시는 것을 추천드립니다.

파트 7: 멤버십과 권한

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

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

지금 이 상태에서는 MVC 뮤직 스토어 사이트를 방문한 모든 사용자들이 StoreManager 컨트롤러에 접근할 수 있습니다. 이번 파트에서는 이 문제점을 보완해서 사이트 관리자만 StoreManager 컨트롤러에 접근할 수 있도록 권한을 제한해보도록 하겠습니다.

Account 컨트롤러 및 뷰 추가하기

완전한 ASP.NET MVC 3 Web Application 템플릿과, 본 자습서에서 MVC 뮤직 스토어 프로젝트를 생성할 때 선택했던 ASP.NET MVC 3 Empty Web Application 템플릿 간의 몇 가지 중요한 차이점들 중 하나는, 빈(Empty) 템플릿에는 Account 컨트롤러(즉, AccountController 클래스)가 포함되어 있지 않다는 것입니다. 본문에서는 작업 상의 편의를 위해, 완전한 ASP.NET MVC 3 Web Application 템플릿을 이용해서 새롭게 만든 ASP.NET MVC 응용 프로그램에서 몇 개의 파일을 복사해서 Account 컨트롤러를 추가해보도록 하겠습니다.

먼저, ASP.NET MVC 3 Web Application 템플릿을 사용해서 새로운 ASP.NET MVC 응용 프로그램을 생성한 다음, 아래의 파일들을 MVC 뮤직 스토어 프로젝트의 동일 디렉터리로 복사합니다:

  1. Controllers 디렉터리의 AccountController.cs 파일을 복사합니다.
  2. Models 디렉터리의 AccountModels.cs 파일을 복사합니다.
  3. Views 디렉터리 하위에 Account라는 이름의 디렉터리를 만들고, 네 개의 뷰 파일들을 모두 복사합니다.

그런 다음, 이 컨트롤러 클래스와 모델 클래스들의 네임스페이스를 MvcMusicStore로 시작하도록 변경해야 합니다. 즉, AccountController 클래스는 MvcMusicStore.Controllers 네임스페이스를, 그리고 AccountModels 클래스는 MvcMusicStore.Models 네임스페이스를 사용하도록 변경해야 합니다.

노트: 이 파일들은 본 자습서의 파트 3에서 사이트의 디자인과 관련된 파일들을 복사하기 위해서 다운로드 받았던 MvcMusicStore-Assets.zip 파일에도 포함되어 있습니다. 다만, 이 파일들은 방금 설명한 위치가 아닌 MvcMusicStore-Assets.zip 파일의 Code 디렉터리 하위에 위치해있습니다.

파일 복사 작업을 모두 마치고 나면, 솔루션 탐색기의 모습은 다음과 같을 것입니다:


출처 : http://i1.asp.net/asp.net/images/mvc/musicstore/image096.png

ASP.NET 웹 사이트 관리 도구를 이용한 관리자 추가하기

당연한 얘기겠지만, 웹사이트에 권한을 적용하려면, 먼저 적당한 권한을 갖고 있는 사용자부터 만들어야 합니다. 사용자를 생성할 수 있는 가장 간단한 방법은 내장되어 있는 ASP.NET 구성 웹사이트를 이용하는 것입니다.

이 ASP.NET 웹 사이트 관리 도구를 실행하려면, 솔루션 탐색기에서 다음과 같은 모양의 아이콘을 마우스로 클릭합니다.


출처 : http://i1.asp.net/asp.net/images/mvc/musicstore/image097.png

그러면, ASP.NET 구성 웹사이트가 실행되는데, 먼저 홈(Home) 화면에서 보안(Security) 탭을 클릭한 다음, 화면 중앙에 위치한 "역할 사용(Enable roles)" 링크를 클릭합니다.


출처 : http://i1.asp.net/asp.net/images/mvc/musicstore/image098.png

그리고, "역할 만들기 또는 관리(Create or Manage roles)" 링크를 클릭합니다.


출처 : http://i1.asp.net/asp.net/images/mvc/musicstore/image099.png

새 역할 이름에 "Administrator"라고 입력한 다음, "역할 추가(Add Role)" 버튼을 누릅니다.


출처 : http://i1.asp.net/asp.net/images/mvc/musicstore/image100.png

다시 "뒤로(Back)" 버튼을 클릭해서 이전 화면으로 돌아간 다음, 이번에는 좌측의 "사용자 만들기(Create user)" 링크를 클릭합니다.


출처 : http://i1.asp.net/asp.net/images/mvc/musicstore/image101.png

그리고, 화면 좌측의 사용자 정보 필드에 다음의 정보들을 입력합니다:

필드
사용자 이름(User Name) Administrator
암호(Password) password123!
암호 확인(Confirm Password) password123!
전자 메일(E-mail) (실제로 존재하는 임의의 전자우편 주소)
보안 질문(Security Question) (여러분이 원하는 임의의 질문)
보안 대답(Security Answer) (여러분이 원하는 임의의 답변)
노트: 물론, 원한다면 본문에 제시된 비밀번호 대신, 여러분이 선호하는 비밀번호를 지정할 수도 있습니다. 본문의 비밀번호는 단지 하나의 사례일 뿐이며, CodePlex 포럼에서 사용되는 구조를 갖고 있습니다. 기본 비밀번호 보안 설정에 따라 비밀번호는 7 자리 이상의 문자로 이루어져 있어야만 하고, 비-알파벳 문자가 포함되어 있어야만 합니다.

마지막으로, 이 사용자에게 Administrator 역할을 부여한 다음, "사용자 만들기(Create User)" 버튼을 클릭합니다.


출처 : http://i1.asp.net/asp.net/images/mvc/musicstore/image102.png

그러면, 사용자가 정상적으로 생성되었다는 내용의 메시지가 나타날 것입니다.


출처 : http://i1.asp.net/asp.net/images/mvc/musicstore/image103.png

모든 작업을 마쳤으면 브라우저 창을 닫습니다.

역할 기반 인증

이제 [Authorize] 어트리뷰트를 적용해서 StoreManagerController 클래스에 대한 사용자의 접근을 제한할 수 있습니다. 다음과 같이 [Authorize] 어트리뷰트를 적용하면, 오직 Administrator 역할에 포함된 사용자만 이 클래스의 액션에 접근이 가능하게 됩니다.

[Authorize(Roles = "Administrator")] 
public class StoreManagerController : Controller 
{ 
    // 컨트롤러 코드 
}
노트: [Authorize] 어트리뷰트는 Controller 클래스 수준뿐만 아니라, 액션 메서드 각각에 대해서 개별적으로 지정할 수도 있습니다.

이제부터는 /StoreManager로 이동하면, 다음과 같은 Log On 대화 상자가 나타나게 됩니다:


출처 : http://i1.asp.net/asp.net/images/mvc/musicstore/image104.png

본문에서 새로 만든 관리자 계정으로 로그온하면, 예전처럼 음반 정보 편집 화면으로 이동할 수 있습니다.

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

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를 선택합니다.


출처 : http://i1.asp.net/asp.net/images/mvc/msv30/image087.jpg

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


출처 : http://i1.asp.net/asp.net/images/mvc/msv30/image088.png

그런 다음, 이 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 컨트롤러를 추가합니다.


출처 : http://i1.asp.net/asp.net/images/mvc/msv30/image089.png

다음은 완전한 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 뷰 페이지를 생성해보겠습니다.


출처 : http://i1.asp.net/asp.net/images/mvc/msv30/image090.png

그러나, 이번에는 장바구니에서 음반을 제거하기 위해 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 페이지로 이동합니다.


출처 : http://i1.asp.net/asp.net/images/mvc/msv30/image091.png

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


출처 : http://i1.asp.net/asp.net/images/mvc/msv30/image092.png

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


출처 : http://i1.asp.net/asp.net/images/mvc/msv30/image093.png

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


출처 : http://i1.asp.net/asp.net/images/mvc/msv30/image094.png

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


출처 : http://i1.asp.net/asp.net/images/mvc/msv30/image095.png

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

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

다음 강좌에서 계속 이어집니다...


authored by

  TikTak
  2012-08-21(15:50)
캐릭 이미지
감사합니다.~
  gnb300
  2015-10-15(10:57)
캐릭 이미지
VS2015 mvc5 사용하는데 웹사이트관리도구가 아무리 찾아봐도 없네요..
그래서 관리자생성을 어떻게해야할지 모르겠고...storemanager 컨트롤러에 authorize
걸어서 접속하면 http not found 만 뜨지 로그온 뷰가 호출이안되네요 ;;
어떻게 해결해야할까요


 
 
.NET과 Java 동영상 기반의 교육사이트

로딩 중입니다...

서버 프레임워크 지원 : NeoDEEX
based on ASP.NET 3.5
Creative Commons License
{5}
{2} 읽음   :{3} ({4})