Compare commits

...

3 Commits

Author SHA1 Message Date
kjh2064 f4e063280e Standardize admin shared components
TaxBaik CI/CD / build-and-deploy (push) Successful in 56s
2026-07-09 13:41:13 +09:00
kjh2064 03e822f8d5 Persist auth keys across deployments 2026-07-09 13:40:18 +09:00
kjh2064 57ba5fd108 Polish admin dashboard for Tabler demo style 2026-07-09 13:38:38 +09:00
10 changed files with 147 additions and 43 deletions
@@ -9,7 +9,7 @@
<form method="post" class="card shadow-sm border-0">
<div class="card-body">
<div asp-validation-summary="All" class="alert alert-danger"></div>
<partial name="_ValidationSummary" />
<div class="row g-3">
<div class="col-md-6">
<label asp-for="Input.Name" class="form-label"></label>
@@ -9,7 +9,7 @@
<form method="post" class="card shadow-sm border-0">
<div class="card-body">
<div asp-validation-summary="All" class="alert alert-danger"></div>
<partial name="_ValidationSummary" />
<div class="row g-3">
<div class="col-md-6">
<label asp-for="Input.Name" class="form-label"></label>
+94 -21
View File
@@ -4,44 +4,117 @@
ViewData["Title"] = "대시보드";
}
<section class="container py-5">
<div class="d-flex flex-wrap justify-content-between align-items-end gap-3 mb-4">
<div>
<p class="text-uppercase text-muted small mb-2">TaxBaik Admin</p>
<h1 class="h2 fw-bold mb-1">대시보드</h1>
<p class="text-muted mb-0">관리자 업무 흐름의 시작점입니다.</p>
<section class="admin-page-shell">
<div class="page-header d-print-none mb-4">
<div class="row align-items-center">
<div class="col">
<div class="page-pretitle">TaxBaik Admin</div>
<h2 class="page-title">대시보드</h2>
<p class="text-muted mb-0">관리자 업무 흐름의 시작점입니다.</p>
</div>
<div class="col-auto ms-auto">
<div class="text-end small text-muted">
<div>로그인 사용자</div>
<div class="fw-semibold text-body">@User.Identity?.Name</div>
</div>
</div>
</div>
<div class="text-muted small">로그인 사용자: @User.Identity?.Name</div>
</div>
<div class="row g-3">
<div class="col-md-4">
<div class="card shadow-sm border-0 h-100">
<div class="row row-deck row-cards g-3 mb-4">
<div class="col-md-6 col-xl-3">
<div class="card card-sm">
<div class="card-body">
<div class="text-muted small text-uppercase mb-1">고객 관리</div>
<h2 class="h5 mb-2">고객 목록</h2>
<p class="text-muted mb-0">검색, 상세, 등록, 수정, 삭제 흐름을 이곳에서 정리합니다.</p>
<div class="d-flex align-items-center justify-content-between gap-3">
<div>
<h2 class="h4 mb-1">고객 목록</h2>
<p class="text-muted mb-0">검색/상세/수정/삭제</p>
</div>
<span class="avatar avatar-rounded bg-primary-lt text-primary">01</span>
</div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card shadow-sm border-0 h-100">
<div class="col-md-6 col-xl-3">
<div class="card card-sm">
<div class="card-body">
<div class="text-muted small text-uppercase mb-1">DB 관리</div>
<h2 class="h5 mb-2">Flyway migration</h2>
<p class="text-muted mb-0">모든 스키마 변경은 SQL migration으로 추적합니다.</p>
<div class="d-flex align-items-center justify-content-between gap-3">
<div>
<h2 class="h4 mb-1">Flyway</h2>
<p class="text-muted mb-0">SQL migration 추적</p>
</div>
<span class="avatar avatar-rounded bg-indigo-lt text-indigo">02</span>
</div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card shadow-sm border-0 h-100">
<div class="col-md-6 col-xl-3">
<div class="card card-sm">
<div class="card-body">
<div class="text-muted small text-uppercase mb-1">인증</div>
<h2 class="h5 mb-2">Cookie Authentication</h2>
<p class="text-muted mb-0">JWT 대신 서버 쿠키로 관리자 세션을 유지합니다.</p>
<div class="d-flex align-items-center justify-content-between gap-3">
<div>
<h2 class="h4 mb-1">Cookie Auth</h2>
<p class="text-muted mb-0">서버 세션 유지</p>
</div>
<span class="avatar avatar-rounded bg-green-lt text-green">03</span>
</div>
</div>
</div>
</div>
<div class="col-md-6 col-xl-3">
<div class="card card-sm">
<div class="card-body">
<div class="text-muted small text-uppercase mb-1">콘텐츠</div>
<div class="d-flex align-items-center justify-content-between gap-3">
<div>
<h2 class="h4 mb-1">블로그 편집</h2>
<p class="text-muted mb-0">에디터 + 미리보기</p>
</div>
<span class="avatar avatar-rounded bg-yellow-lt text-yellow">04</span>
</div>
</div>
</div>
</div>
</div>
<div class="row row-deck row-cards g-3">
<div class="col-12 col-xl-8">
<div class="card">
<div class="card-header">
<h3 class="card-title">빠른 작업</h3>
</div>
<div class="card-body">
<div class="list-group list-group-flush">
<a class="list-group-item list-group-item-action d-flex justify-content-between align-items-center" href="/admin/customers">
<span>고객 목록으로 이동</span>
<span class="text-muted">관리</span>
</a>
<a class="list-group-item list-group-item-action d-flex justify-content-between align-items-center" href="/admin/blog">
<span>블로그 글 작성</span>
<span class="text-muted">에디터</span>
</a>
<a class="list-group-item list-group-item-action d-flex justify-content-between align-items-center" href="/admin/inquiries">
<span>문의 처리</span>
<span class="text-muted">업무</span>
</a>
</div>
</div>
</div>
</div>
<div class="col-12 col-xl-4">
<div class="card">
<div class="card-header">
<h3 class="card-title">운영 노트</h3>
</div>
<div class="card-body">
<div class="alert alert-info mb-0">
Razor Pages, Dapper, Flyway, Cookie Auth, Tabler 기준으로 서버 렌더링 중심의 어드민을 유지합니다.
</div>
</div>
</div>
</div>
</div>
</section>
+2 -1
View File
@@ -95,7 +95,8 @@ public class LoginModel(AuthService authService) : PageModel
var properties = new AuthenticationProperties
{
IsPersistent = false,
IsPersistent = true,
ExpiresUtc = DateTimeOffset.UtcNow.AddHours(12),
RedirectUri = RedirectUrl
};
@@ -1,20 +1,19 @@
@model (string Eyebrow, string Title, string Subtitle, string? ActionHref, string? ActionText)
<div class="page-header d-print-none mb-4">
<div class="row align-items-center">
<div class="row align-items-center g-2">
<div class="col">
<div class="page-pretitle">@Model.Eyebrow</div>
<h2 class="page-title">@Model.Title</h2>
<div class="page-pretitle text-uppercase">@Model.Eyebrow</div>
<h2 class="page-title mb-1">@Model.Title</h2>
@if (!string.IsNullOrWhiteSpace(Model.Subtitle))
{
<div class="text-muted">@Model.Subtitle</div>
<div class="text-muted small">@Model.Subtitle</div>
}
</div>
@if (!string.IsNullOrWhiteSpace(Model.ActionHref) && !string.IsNullOrWhiteSpace(Model.ActionText))
{
<div class="col-auto ms-auto">
<a class="btn btn-primary" href="@Model.ActionHref">@Model.ActionText</a>
<a class="btn btn-primary btn-pill" href="@Model.ActionHref">@Model.ActionText</a>
</div>
}
</div>
</div>
@@ -1,5 +1,12 @@
@model string
<div class="empty">
<div class="empty-img"><svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-database-off" width="48" height="48" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path d="M4 6c0 1.657 4.03 3 9 3s9-1.343 9-3-4.03-3-9-3-9 1.343-9 3" /><path d="M4 6v6c0 1.657 4.03 3 9 3s9-1.343 9-3V6" /><path d="M4 12v6c0 1.657 4.03 3 9 3s9-1.343 9-3v-6" /><path d="M3 3l18 18" /></svg></div>
<p class="empty-title">@(string.IsNullOrWhiteSpace(Model) ? "데이터가 없습니다." : Model)</p>
<div class="empty py-5">
<div class="empty-img">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-database-off" width="48" height="48" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path d="M4 6c0 1.657 4.03 3 9 3s9-1.343 9-3-4.03-3-9-3-9 1.343-9 3" />
<path d="M4 6v6c0 1.657 4.03 3 9 3s9-1.343 9-3V6" />
<path d="M4 12v6c0 1.657 4.03 3 9 3s9-1.343 9-3v-6" />
<path d="M3 3l18 18" />
</svg>
</div>
<p class="empty-title">@string.IsNullOrWhiteSpace(Model) ? "데이터가 없습니다." : Model</p>
</div>
@@ -1,7 +1,7 @@
@model TaxBaik.Web.Pages.Shared.PaginationModel
@if (Model.TotalPages > 1)
{
<nav class="pagination-wrapper mt-4">
<nav class="mt-4" aria-label="페이지네이션">
<ul class="pagination">
@for (var i = 1; i <= Model.TotalPages; i++)
{
@@ -2,14 +2,14 @@
@{
var badgeClass = Model?.ToLowerInvariant() switch
{
"active" => "bg-green text-green-fg",
"inactive" => "bg-gray text-gray-fg",
"new" => "bg-blue text-blue-fg",
"consulting" => "bg-yellow text-yellow-fg",
"contracted" => "bg-green text-green-fg",
"rejected" => "bg-red text-red-fg",
"closed" => "bg-secondary text-secondary-fg",
_ => "bg-secondary text-secondary-fg"
"active" => "badge bg-green-lt text-green",
"inactive" => "badge bg-gray-lt text-muted",
"new" => "badge bg-blue-lt text-blue",
"consulting" => "badge bg-yellow-lt text-yellow",
"contracted" => "badge bg-green-lt text-green",
"rejected" => "badge bg-red-lt text-red",
"closed" => "badge bg-secondary-lt text-secondary",
_ => "badge bg-secondary-lt text-secondary"
};
}
<span class="badge @badgeClass">@Model</span>
<span class="@badgeClass">@Model</span>
@@ -0,0 +1,15 @@
@{
var hasErrors = ViewData.ModelState.ErrorCount > 0;
}
@if (hasErrors)
{
<div class="alert alert-danger alert-important" role="alert">
<div class="d-flex align-items-start gap-2">
<span class="avatar avatar-xs bg-red-lt text-red">!</span>
<div class="flex-fill">
<div class="fw-semibold mb-1">입력값을 확인하세요.</div>
<div asp-validation-summary="All" class="validation-summary text-sm"></div>
</div>
</div>
</div>
}
+9
View File
@@ -3,6 +3,7 @@ using System.Text;
using System.Text.Encodings.Web;
using System.Text.Unicode;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Authentication.OAuth;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.AspNetCore.RateLimiting;
@@ -54,6 +55,14 @@ builder.Host.UseSerilog((context, config) =>
builder.Services.AddAuthorization();
builder.Services.AddProblemDetails();
builder.Services.AddHealthChecks();
var dataProtectionKeyPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
".taxbaik",
"data-protection-keys");
Directory.CreateDirectory(dataProtectionKeyPath);
builder.Services.AddDataProtection()
.SetApplicationName("TaxBaik.Web")
.PersistKeysToFileSystem(new DirectoryInfo(dataProtectionKeyPath));
builder.Services.AddRateLimiter(options =>
{
options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;