[ASP.NET Core] Shopping mall project - 관리자기능
이전에 만든 project에서는 사용자가 제품을 선택하고 해당 제품을 장바구니에 담은 뒤 자신의 정보를 입력하여 주문을 완료하는 것까지의 기능을 구현했습니다. 그러나 지속적인 shopping mall의 관리를 위해서는 사용자로부터의 주문과 제품을 관리할 수 있는 관리자의 기능도 필요할 것입니다.
관리자의 기능은 Blazor를 사용해 구현할 것입니다. Blazor는 Client-Side인 Javascript Code를 ASP.NET Core에 의해 실행되는 Server-Side Code와 결합하고 persistent HTTP 연결에 의하여 연결됩니다. 참고로 해당 글에서는 Server에서 Code를 실행하는 Blazor Server를 사용할 것입니다. 또 다른 것으로 Blazor WebAssembly가 있는데 이는 WebBrowser에서 실행된다는 차이가 있습니다. Blazor에 관한 자세한 내용은 추후 다른 Posting을 통해 알아볼 것입니다.
1. Blazor Server 시작하기
우선 Project에서 Blazor Server를 사용하기 위해 Program.cs를 수정하여 Blazor를 위한 Middleware와 Service를 추가합니다.
builder.Services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
builder.Services.AddServerSideBlazor();
var app = builder.Build();
app.UseStaticFiles();
app.UseSession();
app.MapControllerRoute("categoryPage", "{category}/Page{currentPage:int}", new { Controller = "Home", action = "Index" });
app.MapControllerRoute("page", "Page{currentPage:int}", new { Controller = "Home", action = "Index", currentPage = 1 });
app.MapControllerRoute("category", "{category}", new { Controller = "Home", action = "Index", currentPage = 1 });
app.MapControllerRoute("default", "Products/Page{currentPage}", new { Controller = "Home", action = "Index", currentPage = 1 });
app.MapDefaultControllerRoute();
app.MapRazorPages();
app.MapBlazorHub();
app.MapFallbackToPage("/admin/{*catchall}", "/Admin/Index");
AddServerSideBlazor()는 말 그대로 Blazor사용을 위한 Service를 생성하며 MapBlazorHub() Method는 Blazor middleware component를 등록합니다. 마지막 MapFallbackToPage() Method는 Blazor가 Application의 관리자 기능을 위한 Routing System을 조정하기 위한 것입니다.
(1) Imports File 생성
Blazor는 사용자고자 하는 namespaces를 특정하기 위해 자체적인 imports file을 사용할 수 있습니다. 이를 위해 Pages folder에서 Admin folder를 생성하고 해당 folder에서 mouse오른쪽 버튼을 눌러 Add -> Razor component... 를 선택하여 아래와 같은 내용으로 _Imports.razor file을 추가합니다. Blazor file은 관례적으로 Pages folder에 위치하는 것이 기본이지만 사실상 Project의 어느 곳이든 존재할 수 있습니다.
@using Microsoft.AspNetCore.Components
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.EntityFrameworkCore
@using MyWebApp.Models
처음 4개의 Namespace는 Blazor사용을 위한 기본적인 Namespace이며 나머지 2개는 Project에서 EntityFrameworkCore와 Project의 Models에 있는 Class들을 편리하게 사용하기 위한 것입니다.
(2) Razor 시작 Page 생성
Blazor는 Razor Page에 의존하여 Server연결을 위한 JavaScript code등의 초기 Content를 제공하고 Blazor HTML content를 Render 합니다. 따라서 아래와 같은 Index.cshtml file을 Pages -> Admin folder에 추가해 줍니다.
@page "/admin"
@{
Layout = null;
}
<!DOCTYPE html>
<html>
<head>
<title>관리자 영역</title>
<base href="/" />
</head>
<body>
<component type="typeof(BlazorRoute)" render-mode="Server" />
<script src="/_framework/blazor.server.js"></script>
</body>
</html>
Razor Component는 Razor Page에 의해 출력되는 Razor Component를 담아내는 영역으로서 예제에서의 Component에는 곧 만들게될 BlazorRoute이름이 지정되어 있습니다. Razor Page는 또한 Blazor Server에서 사용할 Javascript가 지정된 script 요소를 포함하고 있는데 이 file의 요청은 Blazor Server middleware에서 가로채어 처리할 것이므로 해당 script file을 임의로 추가할 필요가 없습니다.
(3) Routing과 Layout Component 생성
위에서 추가된 Razor Component에서는 명명된 Type에 따라 BlazorRoute.razor라는 file이 필요하므로 아래 내용을 가진 BlazorRoute.razor file을 Pages->Admin folder에 추가합니다.
<Router AppAssembly="typeof(Program).Assembly">
<Found>
<RouteView RouteData="@context" DefaultLayout="typeof(MainLayout)" />
</Found>
<NotFound>
<p>Page를 표시할 수 없음</p>
</NotFound>
</Router>
component는 Browser의 현재 URL을 사용해 사용자에게 표시할 Razor Component를 가져오게 되는데 만약 일치하는 Component가 발견되지 않으면 NotFound에서 지정된 Error를 대신 표시하게 됩니다.
또한 예제에서는 DefaultLayout을 통해 자체적인 layout을 지정하고 있으므로 MainLayout.razor이라는 file도 Pages->Admin folder에 추가해 주도록 합니다.
@inherits LayoutComponentBase
<div>
<p>관리자 페이지</p>
</div>
<div>
<div>
<NavLink href="/admin/products" Match="NavLinkMatch.Prefix">
제품관리
</NavLink>
<NavLink href="/admin/orders" Match="NavLinkMatch.Prefix">
주문관리
</NavLink>
</div>
</div>
<div>
@Body
</div>
Blazor는 HTML을 생성하기 위해 Razor구문을 사용하기는 하지만 자체적인 지시자와 기능을 도입하기도 하였습니다. 특히 예제에서는 제품관리와 주문관리라는 Link를 생성하기 위해 NavLink를 사용했는데 이 element는 새로운 HTTP 요청을 유발하지 않고 URL을 변경함으로써 Blazor가 사용자와의 상호작용에서 application state를 잃지 않고 응답할 수 있도록 합니다.
(4) 관리 도구용 Razor Component 생성
이제 위에서 만들어진 NavLink에 따라 제품과 주문관리를 위한 2개의 Razor Component를 생성할 것입니다. 우선 아래와 같이 빈 내용을 포함한 Page를 Products.razor, Orders.razor순으로 Pages->Admin folder에 추가합니다.
@page "/admin/products"
@page "/admin"
<p>제품관리</p>
@page "/admin/orders"
<p>주문관리</p>
예제에서 사용된 @page 지시자는 해당 component가 표시될 URL을 지정합니다. 따라서 Products.razor의 경우 /admin/produts와 /admin이라는 2개의 URL을 통해 해당 component를 표시하게 될 것입니다.
(5) 실행 확인
project를 실행하고 /admin으로 접속을 시도하면 가장 먼저 Pages->Admin folder에 있는 Index.cshtml이라는 razor page의 Rendering을 시도합니다. Index.cshtml안에는 Blazor JavaScript file을 포함하고 있는데 이 Blazor JavaScript file은 ASP.NET Core Server로의 persistent HTTP 연결을 열어 초기 Blazor content를 Render 하게 됩니다.
정리하자면 사용자는 /admin으로 접속을 시도할때 제일 먼저 Products.razor의 @page지시자에 따라 Products.razor의 Rendering 해야 할 것입니다. 다만 Razor Component단독으로는 Webbrowser에 Rendering 될 수 없으므로 Index.cshtml을 사용하게 되고 Index.cshtml은 BlazorRoute라는 Razor Component의 사용을 지정하고 있으므로 Index.cshtml의 component는 곧 BlazorRoute로 대체될 것입니다.
BlazorRoute는 전체적인 Layout으로 MainLayout을 지정하고 있으므로 RouteView는 곧 MainLayout으로 대체되고 MainLayout의 @Body를 통해 최종적으로 Products.razor component가 표시됩니다.
위와같이 Project를 실행하고 '주문관리'를 눌러보게 되면 Orders.razor에 따라 '주문관리'를 화면에 표시합니다. 여기서 주목해야 할 점은 분명 다른 page로의 전환을 시도했지만 그에 맞는 HTTP 요청을 발생시키지 않고 일반 응용 program처럼 그대로 화면 전환이 이루어졌다는 것입니다.
2. 주문관리 처리
현재 project에서는 사용자의 주문을 저장하는 부분까지 완성되었으므로 이제 관리자가 해당 주문을 확인하고 처리할 수 있는 주문관리기능을 만들어볼 것입니다.
(1) Model 변경
우선 Project의 Models folder에서 Order.cs file을 수정해 해당 주문을 관리자가 처리하였는지를 알 수 있는 속성을 추가합니다.
[Required(ErrorMessage = "배송주소가 입력되지 않았습니다.")]
public string? Address { get; set; }
[BindNever]
public bool IsComplete { get; set; }
이제 추가된 IsComplete속성을 database에 반영되어야 하는데 이를 위해 project의 folder에서 아래 명령을 내려 Entity Framework Core migration을 통해 model class를 database로 동기화할 수 있도록 합니다.
dotnet ef migrations add Orders |
그런다음 project를 실행하면 MyData class의 InitData() Method를 호출함으로써 자동으로 migration이 적용될 것입니다.
(2) 주문 표시하기
이제 관리자를 위한 주문상태를 표시하기 위해 OrderList.razor Component를 Pages->Admin folder에 추가합니다. 여기서 주문내역은 상황에 따라 주문 대기 중인 것과 처리된 주문으로 2가지를 선택적으로 표시할 수 있게 하였습니다.
<table>
<thead>
<tr><th colspan="3">@OrderTitle</th></tr>
</thead>
<tbody>
@if (Orders?.Count() > 0) {
@foreach (Order o in Orders) {
<tr>
<td>@o.Name</td>
<td>@o.Address</td>
<th>제품</th>
<th>수량</th>
<td><button @onclick="@(e => OrderSelected.InvokeAsync(o.OrderID))">@ButtonTitle</button></td>
</tr>
@foreach (CartItems line in o.Items)
{
<tr>
<td colspan="2"></td>
<td>@line.Product.Name</td><td>@line.Quantity</td>
<td></td>
</tr>
}
}
}
else
{
<tr><td class="text-center">주문없음</td></tr>
}
</tbody>
</table>
@code {
[Parameter]
public string OrderTitle { get; set; } = "주문내역";
[Parameter]
public string ButtonTitle { get; set; } = "주문처리";
[Parameter]
public IEnumerable<Order> Orders { get; set; } = Enumerable.Empty<Order>();
[Parameter]
public EventCallback<int> OrderSelected { get; set; }
}
예제에서 button element는 onclick을 통해서 사용자가 button을 click하는 경우에 대한 처리를 구현하고 있는데 이 경우에는 OrderSelected의 Invoke Method를 호출하도록 합니다. 이 부분은 다음 과정을 통해 좀 더 자세히 알아볼 것입니다. 또한 예제에서는 Razor component는 HTML element에 Annotated 된 Razor구문 방식을 사용하고 있습니다. Component의 view는 @code부분에서의 구문에 의해 의존하여 처리되는데 예제는 3개의 속성을 정의하고 있고 각 속성은 Parameter attribute를 통해 decorate 되어 있습니다. 이것은 해당 속성의 값이 상위 component에 의해 runtime에서 제공될 수 있다는 것을 의미하므로 그 상위 component인 Orders.razor file을 아래와 같이 변경합니다.
@page "/admin/orders"
@inherits OwningComponentBase<IOrderRepository>
<OrderList OrderTitle="주문내역" Orders="UnCompleteOrders" ButtonTitle="주문처리" OrderSelected="CompleteOrder" />
<OrderList OrderTitle="주문처리내역" Orders="CompletedOrders" ButtonTitle="초기화" OrderSelected="ResetOrder" />
<button class="btn btn-info" @onclick="@(e => UpdateOrderList())">새로고침</button>
@code {
public IOrderRepository Repository => Service;
public IEnumerable<Order> AllOrders { get; set; } = Enumerable.Empty<Order>();
public IEnumerable<Order> UnCompleteOrders { get; set; } = Enumerable.Empty<Order>();
public IEnumerable<Order> CompletedOrders { get; set; } = Enumerable.Empty<Order>();
protected async override Task OnInitializedAsync() {
await UpdateOrderList();
}
public async Task UpdateOrderList()
{
AllOrders = await Repository.Orders.ToListAsync();
UnCompleteOrders = AllOrders.Where(o => !o.IsComplete);
CompletedOrders = AllOrders.Where(o => o.IsComplete);
}
public void CompleteOrder(int id) => UpdateOrder(id, true);
public void ResetOrder(int id) => UpdateOrder(id, false);
private void UpdateOrder(int id, bool shipValue)
{
Order? o = Repository.Orders.FirstOrDefault(o => o.OrderID == id);
if (o != null) {
o.IsComplete = shipValue;
Repository.SaveOrder(o);
}
}
}
예제에서 @inherit는 해당 Component가 자체 Repository객체를 가져올 수 있게 하며 @code안에서 해당 Repository를 이용한 작업을 구현하고 있습니다.
여기서는 사용자에게 주문내역과 처리된주문내역을 표시하기 위해 2개의 OrderLIst Component가 사용되었는데 해당 Component에서 할당된 속성의 값은 OrderList.razor에서 [Parameter]로 정의된 속성으로 전달됩니다.
Orders.razor에서는 처음 시작시 초기화 Method인 OnInitialized()를 호출하게 되는데 이때 AllOrders, UnCompleteOrders, CompleteOrders속성의 값을 초기화합니다. OrderList Component에서는 Orders속성을 통해 Repository의 Data를 제공하고 있으며 각 속성의 값은 초기화 때 처리된 UnCompleteOrders와 CompleteOrders를 할당하고 있습니다. OrderSelected속성에서는 ComleteOrder()와 ResetOrder() Method를 할당하며 OrderList.razor에서 Button을 Click 하게 되면 해당 Method가 호출되는 구조를 갖게 됩니다. 그리고 각 Method는 내부에서 UpdateOrder() Method를 호출하여 해당 id와 일치하는 주문건에 대해 IsComplete속성을 변경함으로써 각 주문의 상태를 Update 합니다.
Project를 실행하여 장바구니(Cart)에 새로운 Item을 저장하고 /admin/orders로 이동하여 '주문처리'와
'초기화'기능이 제대로 작동하는지 확인합니다.
3. 제품 관리 기능
주문관리와 함께 현재 Project에서 필요한 것은 제품을 추가하고 특정 제품의 정보를 변경하는 제품 관리 기능입니다. 소위 제품에 대한 CRUD기능이라고 할 수 있는데 해당 기능도 역시 Blazor를 통해 구현할 것입니다.
(1) Repository 구현
제품에 대한 추가, 삭제, 변경 등을 위해 아래와 같이 IMyDBRepository.cs interface file을 변경하고
namespace MyWebApp.Models
{
public interface IProductRepository
{
IQueryable<Product> Products { get; }
void SaveProduct(Product p);
void CreateProduct(Product p);
void DeleteProduct(Product p);
}
}
구현 Class 역시 아래의 내용으로 수정해 SaveProduct(), CreateProduct(), DeleteProduct() Method를 추가합니다.
namespace MyWebApp.Models
{
public class MyDBRepository : IMyDBRepository
{
private MyDbContext _dbContext;
public MyDBRepository(MyDbContext dbContext)
{
_dbContext = dbContext;
}
public IQueryable<Product> Products => _dbContext.Products;
public void CreateProduct(Product p)
{
_dbContext.Add(p);
_dbContext.SaveChanges();
}
public void DeleteProduct(Product p)
{
_dbContext.Remove(p);
_dbContext.SaveChanges();
}
public void SaveProduct(Product p)
{
_dbContext.SaveChanges();
}
}
}
(2) Data Model의 Validation Attribute구현
Product에 대한 추가나 수정 시에 유효한 값인지의 판단을 위해서 Product.cs를 수정하여 다음과 같이 각 속성에 validation attribute를 적용합니다.
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace MyWebApp.Models
{
public class Product
{
public int Id { get; set; }
[Required(ErrorMessage = "제품 카테고리가 입력되지 않았습니다.")]
public string Category { get; set; } = string.Empty;
[Required(ErrorMessage = "제품명이 입력되지 않았습니다.")]
public string Name { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
[Required]
[Range(0.01, double.MaxValue, ErrorMessage = "가능한 단가 범위를 벗어났습니다.")]
[Column(TypeName = "decimal(8, 2)")]
public decimal Price { get; set; }
}
}
(3) List Component 생성
List Component는 현재 제품의 목록을 표시하고 필요하면 제품의 상세 사항을 확인한 뒤 해당 제품을 편집할 수 있도록 하기 위한 것입니다. 이를 위해 Products.razor file을 아래 내용으로 변경합니다.
@page "/admin/products"
@page "/admin"
@inherits OwningComponentBase<IMyDBRepository>
<table>
<thead>
<tr>
<th>ID</th>
<th>제품명</th>
<th>카테고리</th>
<th>단가</th>
<td />
</tr>
</thead>
<tbody>
@if (ProductData?.Count() > 0)
{
@foreach (Product p in ProductData)
{
<tr>
<td>@p.Id</td>
<td><NavLink href="@GetProductDetail(p.Id)">@p.Name</NavLink></td>
<td>@p.Category</td>
<td>@p.Price.ToString("c")</td>
<td>
<NavLink href="@GetProductEdit(p.Id)">
제품수정
</NavLink>
</td>
</tr>
}
}
else
{
<tr>
<td colspan="5">등록된 제품이 존재하지 않습니다.</td>
</tr>
}
</tbody>
</table>
<NavLink href="/admin/products/create">제품추가</NavLink>
@code {
public IMyDBRepository Repository => Service;
public IEnumerable<Product> ProductData { get; set; } = Enumerable.Empty<Product>();
protected async override Task OnInitializedAsync()
{
await UpdateData();
}
public async Task UpdateData()
{
ProductData = await Repository.Products.ToListAsync();
}
public string GetProductDetail(int id) => $"/admin/products/details/{id}";
public string GetProductEdit(int id) => $"/admin/products/editor/{id}";
}
예제에서 Produts.razor component는 Repository에 있는 각 제품의 객체를 표시할 것이며 NavLink component를 통해서는 제품의 상세와 제품의 정보를 변경할 수 있는 View를 표시할 텐데 마지막의 NavLink는 제품을 추가하기 위한 component입니다.
project를 시작해 /admin/products로 이동하여 정상적으로 화면이 표시되는지를 확인합니다.
물론 아직 완전히 구현된 것이 아니므로 어떤 NavLink도 제대로 작동하지 않을 것입니다.
(4) Detail Component 생성
Detail Component는 특정 제품의 상세정보를 표시하기 위한 Component로서 Details.razor이름으로 Pages->Admin folder에 아래와 같은 내용으로 추가합니다.
@page "/admin/products/details/{id:int}"
<h3>제품상세</h3>
<ul>
<li>ID : @Product?.Id</li>
<li>제품명 : @Product?.Name</li>
<li>제품설명 : @Product?.Description</li>
<li>카테고리 : @Product?.Category</li>
<li>단가 : @Product?.Price.ToString("C")</li>
</ul>
<NavLink href="@EditUrl">수정하기</NavLink>
<NavLink href="/admin/products">뒤로</NavLink>
@code {
[Inject]
public IMyDBRepository? Repository { get; set; }
[Parameter]
public int Id { get; set; }
public Product? Product { get; set; }
protected override void OnParametersSet()
{
Product = Repository?.Products.FirstOrDefault(p => p.Id == Id);
}
public string EditUrl => $"/admin/products/editor/{Product?.Id}";
}
예제에서 사용된 Inject은 IMyDBRepository구현을 위해 정의된 것인데 이러한 방식은 Blazor가 Application의 Service접근을 제공하는 방식 중 하나에 해당합니다. Parameter로 지정된 Id값은 URL에 지정된 값에 의해서 설정되며 Database에서 해당 Product의 객체를 가져오기 위해 사용됩니다.
project를 실행해 Product의 제품명을 click 하여 위에서 추가한 component가 화면에 잘 표시되는지를 확인합니다.
(5) Editor Component
Editor Component는 기존 Product정보의 변경과 더불어 추가하는 기능도 같이 수행할 것입니다. 이를 염두에 두고 Editor.razor라는 file을 Pages->Admin folder에 아래 내용으로 추가합니다.
@page "/admin/products/editor/{id:int}"
@page "/admin/products/create"
@inherits OwningComponentBase<IMyDBRepository>
<h3>Product @Title</h3>
<EditForm Model="Product" OnValidSubmit="SaveProduct">
<DataAnnotationsValidator />
@if(Product.Id != 0) {
<div>ID : <input disabled value="@Product.Id" /></div>
}
<div>Name : <InputText @bind-Value="Product.Name" /> <ValidationMessage For="@(() => Product.Name)" /></div>
<div>Description : <InputText @bind-Value="Product.Description" /> <ValidationMessage For="@(() => Product.Description)" /></div>
<div>Category : <InputText @bind-Value="Product.Category" /> <ValidationMessage For="@(() => Product.Category)" /></div>
<div>Price : <InputNumber @bind-Value="Product.Price" /> <ValidationMessage For="@(() => Product.Price)" /></div>
<div>
<button type="submit">저장</button>
<NavLink href="/admin/products">취소</NavLink>
</div>
</EditForm>
@code {
public IMyDBRepository Repository => Service;
[Inject]
public NavigationManager? NavManager { get; set; }
[Parameter]
public int Id { get; set; } = 0;
public Product Product { get; set; } = new Product();
protected override void OnParametersSet() {
if (Id != 0) {
Product = Repository.Products.FirstOrDefault(p => p.Id == Id) ?? new();
}
}
public void SaveProduct() {
if (Id == 0) {
Repository.CreateProduct(Product);
}
else {
Repository.SaveProduct(Product);
}
NavManager?.NavigateTo("/admin/products");
}
public string Title => Id == 0 ? "추기" : "변경";
}
Blazor는 기본적으로 form을 표시하고 유효성 검증을 수행하는 데 사용되는 일련의 내장된 Razor Component를 제공하고 있습니다. 여기서 중요한 것은 Blazor Component안에서는 web browser가 data를 Post 요청을 통해 submit을 수행할 수 없다는 것입니다. EditForm component는 Blazor친화적인 form을 Render 하며 InputText 와 InputNumber component는 문자열과 숫자값을 수용하는 input element를 Render하며 사용자가 변경을 수행하면 자동적으로 model property를 update 하게 됩니다. Data의 처리가 일반적인 Web의 그것과는 좀 다르다는 것을 인식하는 것이 중요합니다.
Data validation 역시 이들 component안으로 통합되었고 EditForm component에 있는 OnValidSubmit속성은 form안에서 validation attribute에 의해 정의된 규칙을 준수하도록 값이 입력된 경우에만 호출되는 Method를 특정합니다.
Blazor는 또한 NavigationManager라는 class를 제공하는데 이 class는 다수의 component사이를 새로운 HTTP 요청을 생성하지 않으면서 component가 탐색이 가능하도록 지원합니다. 예제에서는 바로 이 NavigationManager를 service를 통해 가져와 SaveProduct() Method를 호출하여 Database를 update한뒤 Products component를 반환하도록 하고 있습니다.
Project를 실행하여 /admin으로 이동한 후 '제품 추가'button을 눌러 Editor화면으로 이동합니다. 그리고 아무것도 입력하지 않은 상태에서 '저장'을 눌러 Validation이 잘 작동하는지를 확인하고
다시 추가하고자 하는 제품 정보를 입력한 뒤 '저장'을 눌러 이번에는 해당 입력값대로 잘 저장되는지를 확인합니다.
그리고 다시 기존에 존재하는 Product 중 하나를 골라 '제품 수정'을 눌러 해당 Product의 값을 변경한 후
해당 값이 제대로 저장되는지의 여부도 같이 확인해 봅니다.
(6) Product 삭제하기
Product의 CRUD과정 중 마지막으로 Product를 삭제하는 것이 있습니다. 이 작업은 아래와 같이 Products.razor file을 수정하여 제품 삭제에 필요한 Method와 Button을 추가하는 것만으로 간단히 구현할 수 있습니다.
@page "/admin/products"
@page "/admin"
@inherits OwningComponentBase<IMyDBRepository>
<table>
<thead>
<tr>
<th>ID</th>
<th>제품명</th>
<th>카테고리</th>
<th>단가</th>
<td />
</tr>
</thead>
<tbody>
@if (ProductData?.Count() > 0)
{
@foreach (Product p in ProductData)
{
<tr>
<td>@p.Id</td>
<td><NavLink href="@GetProductDetail(p.Id)">@p.Name</NavLink></td>
<td>@p.Category</td>
<td>@p.Price.ToString("c")</td>
<td>
<NavLink href="@GetProductEdit(p.Id)">
제품수정
</NavLink>
<button @onclick="@(e => DeleteProduct(p))">
제품삭제
</button>
</td>
</tr>
}
}
else
{
<tr>
<td colspan="5">등록된 제품이 존재하지 않습니다.</td>
</tr>
}
</tbody>
</table>
<NavLink href="/admin/products/create">제품추가</NavLink>
@code {
public IMyDBRepository Repository => Service;
public IEnumerable<Product> ProductData { get; set; } = Enumerable.Empty<Product>();
protected async override Task OnInitializedAsync()
{
await UpdateData();
}
public async Task UpdateData()
{
ProductData = await Repository.Products.ToListAsync();
}
public async Task DeleteProduct(Product p)
{
Repository.DeleteProduct(p);
await UpdateData();
}
public string GetProductDetail(int id) => $"/admin/products/details/{id}";
public string GetProductEdit(int id) => $"/admin/products/editor/{id}";
}
삭제 button에는 @onclick 속성을 사용하여 사용자가 button을 click시 DeleteProduct Method를 호출하도록 되어 있습니다. 선택된 Product는 Database에서 삭제될 것이며 component가 update 됨으로 인해 변경된 Product내역이 다시 표시될 것입니다.
Project를 다시 실행하여 새롭게 추가된 Button을 사용해 제품이 성공적으로 삭제되는지의 여부를 확인합니다.