diff --git a/TaxBaik.Admin/Components/App.razor b/TaxBaik.Admin/Components/App.razor
new file mode 100644
index 0000000..564bc11
--- /dev/null
+++ b/TaxBaik.Admin/Components/App.razor
@@ -0,0 +1,17 @@
+
+
+
+
+
+ 백원숙 세무회계 - 관리자
+
+
+
+
+
+
+
+
+
+
+
diff --git a/TaxBaik.Admin/Components/ConfirmDialog.razor b/TaxBaik.Admin/Components/ConfirmDialog.razor
new file mode 100644
index 0000000..ee3288d
--- /dev/null
+++ b/TaxBaik.Admin/Components/ConfirmDialog.razor
@@ -0,0 +1,18 @@
+@using MudBlazor
+
+
+
+ 정말로 삭제하시겠습니까?
+
+
+ 취소
+ 삭제
+
+
+
+@code {
+ [CascadingParameter] MudDialogInstance MudDialog { get; set; }
+
+ void Cancel() => MudDialog.Cancel();
+ void Confirm() => MudDialog.Close(DialogResult.Ok(true));
+}
diff --git a/TaxBaik.Admin/Components/InquiryTable.razor b/TaxBaik.Admin/Components/InquiryTable.razor
new file mode 100644
index 0000000..324f433
--- /dev/null
+++ b/TaxBaik.Admin/Components/InquiryTable.razor
@@ -0,0 +1,58 @@
+@using TaxBaik.Domain.Interfaces
+@inject IInquiryRepository InquiryRepository
+
+
+
+
+ | 이름 |
+ 전화 |
+ 분야 |
+ 메시지 |
+ 날짜 |
+ |
+
+
+
+ @foreach (var inquiry in filteredInquiries)
+ {
+
+ | @inquiry.Name |
+ @inquiry.Phone |
+ @inquiry.ServiceType |
+ @inquiry.Message.Substring(0, Math.Min(30, inquiry.Message.Length))... |
+ @inquiry.CreatedAt.ToString("yyyy-MM-dd") |
+
+ 보기
+ |
+
+ }
+
+
+
+@code {
+ [Parameter]
+ public string Status { get; set; } = "";
+
+ private List inquiries = [];
+ private List filteredInquiries = [];
+
+ protected override async Task OnInitializedAsync()
+ {
+ var (items, _) = await InquiryRepository.GetPagedAsync(1, 1000);
+ inquiries = items.ToList();
+ FilterInquiries();
+ }
+
+ private void FilterInquiries()
+ {
+ filteredInquiries = string.IsNullOrEmpty(Status)
+ ? inquiries
+ : inquiries.Where(x => x.Status == Status).ToList();
+ }
+
+ protected override async Task OnParametersSetAsync()
+ {
+ FilterInquiries();
+ }
+}
diff --git a/TaxBaik.Admin/Components/Layout/BlankLayout.razor b/TaxBaik.Admin/Components/Layout/BlankLayout.razor
new file mode 100644
index 0000000..08d9c34
--- /dev/null
+++ b/TaxBaik.Admin/Components/Layout/BlankLayout.razor
@@ -0,0 +1,7 @@
+@inherits LayoutComponentBase
+
+
+
+
+
+@Body
diff --git a/TaxBaik.Admin/Components/Layout/MainLayout.razor b/TaxBaik.Admin/Components/Layout/MainLayout.razor
new file mode 100644
index 0000000..b27bd30
--- /dev/null
+++ b/TaxBaik.Admin/Components/Layout/MainLayout.razor
@@ -0,0 +1,41 @@
+@using Microsoft.AspNetCore.Components.Authorization
+@inherits LayoutComponentBase
+
+
+
+
+
+
+
+
+
+ 백원숙 세무회계 관리자
+
+ 공개 사이트
+ 로그아웃
+
+
+
+
+ 📊 대시보드
+ 📝 블로그 관리
+ 💬 문의 관리
+ ⚙️ 설정
+
+
+
+
+
+ @Body
+
+
+
+
+
+ @Body
+
+
+
+@code {
+ private bool drawerOpen = true;
+}
diff --git a/TaxBaik.Admin/Components/Pages/Blog/BlogCreate.razor b/TaxBaik.Admin/Components/Pages/Blog/BlogCreate.razor
new file mode 100644
index 0000000..38b2594
--- /dev/null
+++ b/TaxBaik.Admin/Components/Pages/Blog/BlogCreate.razor
@@ -0,0 +1,96 @@
+@page "/blog/create"
+@using TaxBaik.Application.Services
+@using TaxBaik.Domain.Interfaces
+@attribute [Authorize]
+@inject BlogService BlogService
+@inject ICategoryRepository CategoryRepository
+@inject NavigationManager Navigation
+@inject Snackbar Snackbar
+
+새 포스트 작성
+
+📝 새 포스트
+
+
+
+
+
+
+ @foreach (var category in categories)
+ {
+ @category.Name
+ }
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 저장
+ Navigation.NavigateTo("/taxbaik/admin/blog"))">
+ 취소
+
+
+
+
+
+@code {
+ private MudForm form;
+ private List categories = [];
+ private CreatePostModel model = new();
+
+ protected override async Task OnInitializedAsync()
+ {
+ categories = (await CategoryRepository.GetAllAsync()).ToList();
+ }
+
+ private async Task SavePost()
+ {
+ try
+ {
+ await BlogService.CreateAsync(new TaxBaik.Application.DTOs.CreateBlogPostDto
+ {
+ Title = model.Title,
+ Content = model.Content,
+ CategoryId = model.CategoryId,
+ Tags = model.Tags,
+ SeoTitle = model.SeoTitle,
+ SeoDescription = model.SeoDescription,
+ IsPublished = model.IsPublished,
+ AuthorId = 1 // TODO: From session
+ });
+
+ Snackbar.Add("포스트가 저장되었습니다.", Severity.Success);
+ Navigation.NavigateTo("/taxbaik/admin/blog");
+ }
+ catch (Exception ex)
+ {
+ Snackbar.Add($"오류: {ex.Message}", Severity.Error);
+ }
+ }
+
+ private class CreatePostModel
+ {
+ public string Title { get; set; }
+ public string Content { get; set; }
+ public int CategoryId { get; set; }
+ public string Tags { get; set; }
+ public string SeoTitle { get; set; }
+ public string SeoDescription { get; set; }
+ public bool IsPublished { get; set; }
+ }
+}
diff --git a/TaxBaik.Admin/Components/Pages/Blog/BlogList.razor b/TaxBaik.Admin/Components/Pages/Blog/BlogList.razor
new file mode 100644
index 0000000..b54aa2f
--- /dev/null
+++ b/TaxBaik.Admin/Components/Pages/Blog/BlogList.razor
@@ -0,0 +1,78 @@
+@page "/blog"
+@using TaxBaik.Application.Services
+@using TaxBaik.Domain.Interfaces
+@attribute [Authorize]
+@inject IBlogPostRepository BlogRepository
+@inject DialogService DialogService
+@inject Snackbar Snackbar
+
+블로그 관리
+
+
+ 📝 블로그 관리
+ 새 포스트
+
+
+
+
+
+
+
+ TogglePublish(cell.Item.Id, val))" />
+
+
+
+
+
+
+
+ 수정
+ DeletePost(cell.Item.Id))">삭제
+
+
+
+
+
+
+@code {
+ private List posts = [];
+ private bool isLoading = true;
+
+ protected override async Task OnInitializedAsync()
+ {
+ await LoadPosts();
+ }
+
+ private async Task LoadPosts()
+ {
+ isLoading = true;
+ var (items, total) = await BlogRepository.GetPagedAsync(1, 100);
+ posts = items.ToList();
+ isLoading = false;
+ }
+
+ private async Task TogglePublish(int postId, bool isPublished)
+ {
+ // TODO: Update publish status via service
+ Snackbar.Add("발행 상태가 변경되었습니다.", Severity.Success);
+ }
+
+ private async Task DeletePost(int postId)
+ {
+ var confirmed = await DialogService.ShowAsync(
+ "포스트 삭제", new DialogParameters { },
+ new DialogOptions { MaxWidth = MaxWidth.ExtraSmall });
+
+ var result = await confirmed.Result;
+ if (!result.Canceled)
+ {
+ // TODO: Delete via repository
+ await LoadPosts();
+ Snackbar.Add("포스트가 삭제되었습니다.", Severity.Success);
+ }
+ }
+}
diff --git a/TaxBaik.Admin/Components/Pages/Dashboard.razor b/TaxBaik.Admin/Components/Pages/Dashboard.razor
new file mode 100644
index 0000000..22a7431
--- /dev/null
+++ b/TaxBaik.Admin/Components/Pages/Dashboard.razor
@@ -0,0 +1,92 @@
+@page "/"
+@using TaxBaik.Application.Services
+@using TaxBaik.Domain.Interfaces
+@attribute [Authorize]
+@inject IInquiryRepository InquiryRepository
+@inject BlogService BlogService
+
+대시보드
+
+📊 대시보드
+
+
+
+
+ 이번달 문의
+ @thisMonthInquiries
+
+
+
+
+
+ 신규 문의
+ @newInquiries
+
+
+
+
+
+ 전체 포스트
+ @totalPosts
+
+
+
+
+
+ 발행된 포스트
+ @publishedPosts
+
+
+
+
+
+ 최근 문의
+
+
+
+ | 이름 |
+ 전화 |
+ 분야 |
+ 상태 |
+ 날짜 |
+
+
+
+ @foreach (var inquiry in recentInquiries)
+ {
+
+ | @inquiry.Name |
+ @inquiry.Phone |
+ @inquiry.ServiceType |
+
+
+ @inquiry.Status
+
+ |
+ @inquiry.CreatedAt.ToString("yyyy-MM-dd") |
+
+ }
+
+
+
+
+@code {
+ private int thisMonthInquiries = 0;
+ private int newInquiries = 0;
+ private int totalPosts = 0;
+ private int publishedPosts = 0;
+ private List recentInquiries = [];
+
+ protected override async Task OnInitializedAsync()
+ {
+ var (inquiries, total) = await InquiryRepository.GetPagedAsync(1, 100);
+ recentInquiries = inquiries.OrderByDescending(x => x.CreatedAt).Take(5).ToList();
+
+ var now = DateTime.UtcNow;
+ thisMonthInquiries = inquiries.Count(x => x.CreatedAt.Year == now.Year && x.CreatedAt.Month == now.Month);
+ newInquiries = inquiries.Count(x => x.Status == "new");
+ totalPosts = 0; // TODO: get from blog service
+ publishedPosts = 0; // TODO: get from blog service
+ }
+}
diff --git a/TaxBaik.Admin/Components/Pages/Inquiries/InquiryDetail.razor b/TaxBaik.Admin/Components/Pages/Inquiries/InquiryDetail.razor
new file mode 100644
index 0000000..735d85d
--- /dev/null
+++ b/TaxBaik.Admin/Components/Pages/Inquiries/InquiryDetail.razor
@@ -0,0 +1,64 @@
+@page "/inquiries/{InquiryId:int}"
+@using TaxBaik.Domain.Interfaces
+@attribute [Authorize]
+@inject IInquiryRepository InquiryRepository
+@inject NavigationManager Navigation
+
+문의 상세
+
+@if (inquiry != null)
+{
+ Navigation.NavigateTo("/taxbaik/admin/inquiries"))">
+ ← 돌아가기
+
+
+
+
+
+ 이름
+ @inquiry.Name
+
+
+ 연락처
+ @inquiry.Phone
+
+
+ 이메일
+ @inquiry.Email
+
+
+ 분야
+ @inquiry.ServiceType
+
+
+ 메시지
+ @inquiry.Message
+
+
+ 상태
+
+ 신규
+ 연락함
+ 완료
+
+
+
+
+}
+else
+{
+ 문의를 찾을 수 없습니다.
+}
+
+@code {
+ [Parameter]
+ public int InquiryId { get; set; }
+
+ private Domain.Entities.Inquiry inquiry;
+
+ protected override async Task OnInitializedAsync()
+ {
+ var (inquiries, _) = await InquiryRepository.GetPagedAsync(1, 1000);
+ inquiry = inquiries.FirstOrDefault(x => x.Id == InquiryId);
+ }
+}
diff --git a/TaxBaik.Admin/Components/Pages/Inquiries/InquiryList.razor b/TaxBaik.Admin/Components/Pages/Inquiries/InquiryList.razor
new file mode 100644
index 0000000..ff052e4
--- /dev/null
+++ b/TaxBaik.Admin/Components/Pages/Inquiries/InquiryList.razor
@@ -0,0 +1,23 @@
+@page "/inquiries"
+@using TaxBaik.Domain.Interfaces
+@attribute [Authorize]
+@inject IInquiryRepository InquiryRepository
+
+문의 관리
+
+💬 문의 관리
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/TaxBaik.Admin/Components/Pages/Login.razor b/TaxBaik.Admin/Components/Pages/Login.razor
new file mode 100644
index 0000000..8fdd9d6
--- /dev/null
+++ b/TaxBaik.Admin/Components/Pages/Login.razor
@@ -0,0 +1,49 @@
+@page "/login"
+@using System.ComponentModel.DataAnnotations
+@using Microsoft.AspNetCore.Authentication
+@using Microsoft.AspNetCore.Authentication.Cookies
+@layout BlankLayout
+@attribute [AllowAnonymous]
+
+로그인
+
+
+
+ 관리자 로그인
+
+
+
+
+
+
+ @if (!string.IsNullOrEmpty(errorMessage))
+ {
+ @errorMessage
+ }
+
+ 로그인
+
+
+
+
+@code {
+ private MudForm form;
+ private bool isFormValid = false;
+ private string errorMessage = "";
+
+ private LoginModel model = new();
+
+ private async Task HandleLogin()
+ {
+ errorMessage = "로그인 기능은 준비 중입니다.";
+ }
+
+ private class LoginModel
+ {
+ public string Username { get; set; }
+ public string Password { get; set; }
+ }
+}
diff --git a/TaxBaik.Admin/Components/Pages/Settings/SiteSettings.razor b/TaxBaik.Admin/Components/Pages/Settings/SiteSettings.razor
new file mode 100644
index 0000000..11b8324
--- /dev/null
+++ b/TaxBaik.Admin/Components/Pages/Settings/SiteSettings.razor
@@ -0,0 +1,43 @@
+@page "/settings"
+@using TaxBaik.Domain.Interfaces
+@attribute [Authorize]
+@inject Snackbar Snackbar
+
+설정
+
+⚙️ 사이트 설정
+
+
+
+
+
+
+
+
+
+
+
+ 저장
+
+
+
+@code {
+ private Dictionary settings = new()
+ {
+ { "phone", "010-4122-8268" },
+ { "email", "taxbaik5668@gmail.com" },
+ { "kakao_channel_url", "http://pf.kakao.com/_xoxchTX" },
+ { "instagram_url", "https://www.instagram.com/taxtory5668/" }
+ };
+
+ private async Task SaveSettings()
+ {
+ // TODO: Save to database
+ Snackbar.Add("설정이 저장되었습니다.", Severity.Success);
+ }
+}
diff --git a/TaxBaik.Admin/Components/Routes.razor b/TaxBaik.Admin/Components/Routes.razor
new file mode 100644
index 0000000..4db38c6
--- /dev/null
+++ b/TaxBaik.Admin/Components/Routes.razor
@@ -0,0 +1,14 @@
+@using Microsoft.AspNetCore.Components.Routing
+
+
+
+
+
+
+
+ 찾을 수 없음
+
+ 요청한 페이지를 찾을 수 없습니다.
+
+
+
diff --git a/TaxBaik.Admin/Components/_Imports.razor b/TaxBaik.Admin/Components/_Imports.razor
new file mode 100644
index 0000000..50c33cc
--- /dev/null
+++ b/TaxBaik.Admin/Components/_Imports.razor
@@ -0,0 +1,9 @@
+@using System.Net.Http
+@using System.Net.Http.Json
+@using Microsoft.AspNetCore.Components.Forms
+@using Microsoft.AspNetCore.Components.Routing
+@using Microsoft.AspNetCore.Components.Web
+@using Microsoft.AspNetCore.Components.Web.Virtualization
+@using Microsoft.AspNetCore.Components.Authorization
+@using Microsoft.AspNetCore.Authorization
+@using MudBlazor
diff --git a/TaxBaik.Admin/Program.cs b/TaxBaik.Admin/Program.cs
index b9be318..89d309b 100644
--- a/TaxBaik.Admin/Program.cs
+++ b/TaxBaik.Admin/Program.cs
@@ -9,11 +9,12 @@ builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationSc
.AddCookie(opts => {
opts.LoginPath = "/login";
opts.ExpireTimeSpan = TimeSpan.FromHours(8);
+ opts.Cookie.SameSite = SameSiteMode.Lax;
});
builder.Services.AddAuthorizationCore();
-builder.Services.AddRazorPages();
-builder.Services.AddServerSideBlazor();
+builder.Services.AddRazorComponents()
+ .AddInteractiveServerComponents();
builder.Services.AddMudServices();
builder.Services.AddMemoryCache();
builder.Services.AddInfrastructure();
@@ -23,7 +24,7 @@ var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
- app.UseExceptionHandler("/Error");
+ app.UseExceptionHandler("/Error", createScopeForErrors: true);
app.UseHsts();
}
@@ -34,7 +35,7 @@ app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
-app.MapBlazorHub();
-app.MapFallbackToPage("/_Host");
+app.MapRazorComponents()
+ .AddInteractiveServerRenderMode();
app.Run();
diff --git a/TaxBaik.Admin/Properties/launchSettings.json b/TaxBaik.Admin/Properties/launchSettings.json
index 72ae763..80582b1 100644
--- a/TaxBaik.Admin/Properties/launchSettings.json
+++ b/TaxBaik.Admin/Properties/launchSettings.json
@@ -8,27 +8,11 @@
}
},
"profiles": {
- "http": {
- "commandName": "Project",
- "dotnetRunMessages": true,
- "launchBrowser": true,
- "applicationUrl": "http://localhost:5253",
- "environmentVariables": {
- "ASPNETCORE_ENVIRONMENT": "Development"
- }
- },
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
- "launchBrowser": true,
- "applicationUrl": "https://localhost:7100;http://localhost:5253",
- "environmentVariables": {
- "ASPNETCORE_ENVIRONMENT": "Development"
- }
- },
- "IIS Express": {
- "commandName": "IISExpress",
- "launchBrowser": true,
+ "launchBrowser": false,
+ "applicationUrl": "https://localhost:5002;http://localhost:5003",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}