feat: 시즌별 마케팅 + 공지사항 관리 기능 추가
TaxBaik CI/CD / build-and-deploy (push) Successful in 1m15s
TaxBaik Browser E2E / browser-e2e (push) Successful in 1m31s

- 연간 세무 캘린더(7개 시즌) 기반 자동 Hero 섹션 전환
- 시즌 감지 시 D-Day 카운트다운, 긴박감 배지, 시즌 CTA 표시
- 서비스 카드 순서 시즌 관련 항목 우선 정렬
- 어드민 공지사항 CRUD (등록·수정·삭제, 기간·유형 설정)
- 홈페이지 상단 공지 배너 자동 노출 (일반/배너/긴급)
- CLAUDE.md에 세무 캘린더 및 마케팅 방향 하네스 추가

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-27 22:45:55 +09:00
parent 6af9221fab
commit cc72a67355
27 changed files with 1184 additions and 144 deletions
+5
View File
@@ -9,6 +9,11 @@
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@400;500;700&display=swap" rel="stylesheet" />
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet" />
<link href="_content/MudBlazor/MudBlazor.min.css" rel="stylesheet" />
<script>
document.documentElement.classList.toggle(
'admin-login-route',
window.location.pathname.toLowerCase().endsWith('/admin/login'));
</script>
<link rel="stylesheet" href="/taxbaik/css/admin.css" />
<component type="typeof(HeadOutlet)" render-mode="InteractiveServer" />
</head>
@@ -1,7 +1,7 @@
@using TaxBaik.Application.Services
@inject InquiryService InquiryService
<MudSimpleTable Striped="true" Dense="true" Class="mt-4">
<MudSimpleTable Striped="true" Dense="true" Class="admin-table mt-4">
<thead>
<tr>
<th>이름</th>
@@ -1,24 +1,60 @@
@inherits LayoutComponentBase
<MudLayout>
<MudAppBar Elevation="1">
<MudText Typo="Typo.h6" Class="ml-3">백원숙 세무회계 관리자</MudText>
<MudLayout Class="admin-shell">
<MudAppBar Elevation="0" Class="admin-topbar">
<MudIconButton Icon="@Icons.Material.Filled.Menu"
Color="Color.Inherit"
Edge="Edge.Start"
Class="admin-menu-button"
OnClick="@ToggleDrawer" />
<div class="admin-topbar-title">
<MudText Typo="Typo.caption">TaxBaik Backoffice</MudText>
<MudText Typo="Typo.h6">백원숙 세무회계 관리자</MudText>
</div>
<MudSpacer />
<MudButton Color="Color.Inherit" Href="/taxbaik">공개 사이트</MudButton>
<MudButton Href="/taxbaik/admin/logout">로그아웃</MudButton>
<MudButton Class="admin-topbar-action"
Variant="Variant.Outlined"
Color="Color.Inherit"
StartIcon="@Icons.Material.Filled.OpenInNew"
Href="/taxbaik">
공개 사이트
</MudButton>
<MudButton Class="admin-topbar-action"
Variant="Variant.Filled"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.Logout"
Href="/taxbaik/admin/logout">
로그아웃
</MudButton>
</MudAppBar>
<MudDrawer @bind-open="@drawerOpen" Elevation="1" Variant="DrawerVariant.Responsive" Breakpoint="Breakpoint.Md">
<MudNavMenu>
<MudNavLink Href="/taxbaik/admin/dashboard" Match="NavLinkMatch.All">📊 대시보드</MudNavLink>
<MudNavLink Href="/taxbaik/admin/blog">📝 블로그 관리</MudNavLink>
<MudNavLink Href="/taxbaik/admin/inquiries">💬 문의 관리</MudNavLink>
<MudNavLink Href="/taxbaik/admin/settings">⚙️ 설정</MudNavLink>
<MudDrawer @bind-open="@drawerOpen"
Elevation="0"
Variant="DrawerVariant.Responsive"
Breakpoint="Breakpoint.Md"
Class="admin-drawer">
<div class="admin-drawer-brand">
<div class="admin-brand-mark">T</div>
<div>
<MudText Typo="Typo.subtitle1">TaxBaik</MudText>
<MudText Typo="Typo.caption">세무 운영 콘솔</MudText>
</div>
</div>
<MudNavMenu Class="admin-nav">
<MudNavLink Href="/taxbaik/admin/dashboard" Match="NavLinkMatch.All" Icon="@Icons.Material.Filled.Dashboard">대시보드</MudNavLink>
<MudNavLink Href="/taxbaik/admin/announcements" Icon="@Icons.Material.Filled.Campaign">공지사항</MudNavLink>
<MudNavLink Href="/taxbaik/admin/blog" Icon="@Icons.Material.Filled.Article">블로그 관리</MudNavLink>
<MudNavLink Href="/taxbaik/admin/inquiries" Icon="@Icons.Material.Filled.Forum">문의 관리</MudNavLink>
<MudNavLink Href="/taxbaik/admin/settings" Icon="@Icons.Material.Filled.Tune">설정</MudNavLink>
</MudNavMenu>
<div class="admin-drawer-footer">
<MudText Typo="Typo.caption">운영 기준</MudText>
<MudText Typo="Typo.body2">변경 사항은 배포 후 Playwright로 검증합니다.</MudText>
</div>
</MudDrawer>
<MudMainContent>
<MudContainer MaxWidth="MaxWidth.Large" Class="my-4 px-3 px-md-0">
<MudMainContent Class="admin-main">
<MudContainer MaxWidth="MaxWidth.False" Class="admin-content">
@Body
</MudContainer>
</MudMainContent>
@@ -26,4 +62,9 @@
@code {
private bool drawerOpen = true;
private void ToggleDrawer()
{
drawerOpen = !drawerOpen;
}
}
@@ -0,0 +1,161 @@
@page "/admin/announcements/create"
@page "/admin/announcements/{Id:int}/edit"
@attribute [Authorize]
@using TaxBaik.Application.DTOs
@using TaxBaik.Application.Services
@inject AnnouncementService AnnouncementService
@inject NavigationManager Navigation
@inject ISnackbar Snackbar
<PageTitle>@(Id.HasValue ? "공지 수정" : "공지 등록")</PageTitle>
<section class="admin-page-hero">
<div>
<MudText Typo="Typo.caption" Class="admin-eyebrow">Homepage</MudText>
<MudText Typo="Typo.h4" Class="admin-page-title">@(Id.HasValue ? "공지 수정" : "공지 등록")</MudText>
</div>
</section>
<MudPaper Class="admin-surface" Elevation="0">
<MudForm @ref="form">
<MudGrid>
<MudItem xs="12">
<MudTextField @bind-Value="model.Title"
Label="제목"
Variant="Variant.Outlined"
Required="true"
RequiredError="제목을 입력하세요."
HelperText="홈페이지 상단 공지 바에 표시되는 텍스트입니다." />
</MudItem>
<MudItem xs="12">
<MudTextField @bind-Value="model.Content"
Label="상세 내용 (선택)"
Variant="Variant.Outlined"
Lines="3"
HelperText="부가 설명이 있을 경우 입력합니다. 없으면 제목만 표시됩니다." />
</MudItem>
<MudItem xs="12" sm="6">
<MudSelect @bind-Value="model.DisplayType"
Label="유형"
Variant="Variant.Outlined">
<MudSelectItem Value="@("info")">일반 (파란색)</MudSelectItem>
<MudSelectItem Value="@("banner")">배너 (주황색) — 중요 이벤트</MudSelectItem>
<MudSelectItem Value="@("urgent")">긴급 (빨간색) — 마감 임박</MudSelectItem>
</MudSelect>
</MudItem>
<MudItem xs="12" sm="6">
<MudNumericField @bind-Value="model.SortOrder"
Label="노출 순서"
Variant="Variant.Outlined"
HelperText="숫자가 클수록 먼저 표시됩니다." />
</MudItem>
<MudItem xs="12" sm="6">
<MudDatePicker @bind-Date="startsAtDate"
Label="게시 시작일 (비우면 즉시)"
Variant="Variant.Outlined"
DateFormat="yyyy-MM-dd"
Clearable="true" />
</MudItem>
<MudItem xs="12" sm="6">
<MudDatePicker @bind-Date="endsAtDate"
Label="게시 종료일 (비우면 무기한)"
Variant="Variant.Outlined"
DateFormat="yyyy-MM-dd"
Clearable="true" />
</MudItem>
<MudItem xs="12">
<MudSwitch @bind-Checked="model.IsActive"
Label="@(model.IsActive ? "활성화 (홈페이지에 노출)" : "비활성화 (홈페이지 미노출)")"
Color="Color.Primary" />
</MudItem>
</MudGrid>
<div class="d-flex gap-2 mt-4">
<MudButton Variant="Variant.Filled" Color="Color.Primary"
StartIcon="@Icons.Material.Filled.Save"
Disabled="isSaving"
@onclick="SaveAsync">
@(isSaving ? "저장 중..." : "저장")
</MudButton>
<MudButton Variant="Variant.Outlined"
@onclick="@(() => Navigation.NavigateTo("/taxbaik/admin/announcements"))">
취소
</MudButton>
</div>
</MudForm>
</MudPaper>
@code {
[Parameter] public int? Id { get; set; }
private MudForm? form;
private bool isSaving;
private DateTime? startsAtDate;
private DateTime? endsAtDate;
private AnnouncementDto model = new();
protected override async Task OnInitializedAsync()
{
if (Id.HasValue)
{
var entity = await AnnouncementService.GetByIdAsync(Id.Value);
if (entity is null)
{
Navigation.NavigateTo("/taxbaik/admin/announcements");
return;
}
model = new AnnouncementDto
{
Id = entity.Id,
Title = entity.Title,
Content = entity.Content,
DisplayType = entity.DisplayType,
IsActive = entity.IsActive,
SortOrder = entity.SortOrder
};
startsAtDate = entity.StartsAt?.ToLocalTime();
endsAtDate = entity.EndsAt?.ToLocalTime();
}
}
private async Task SaveAsync()
{
if (form is null) return;
await form.Validate();
if (!form.IsValid) return;
isSaving = true;
try
{
model.StartsAt = startsAtDate.HasValue
? DateTime.SpecifyKind(startsAtDate.Value.Date, DateTimeKind.Local).ToUniversalTime()
: null;
model.EndsAt = endsAtDate.HasValue
? DateTime.SpecifyKind(endsAtDate.Value.Date.AddDays(1).AddSeconds(-1), DateTimeKind.Local).ToUniversalTime()
: null;
if (Id.HasValue)
await AnnouncementService.UpdateAsync(model);
else
await AnnouncementService.CreateAsync(model);
Snackbar.Add("공지사항이 저장되었습니다.", Severity.Success);
Navigation.NavigateTo("/taxbaik/admin/announcements");
}
catch (Exception ex)
{
Snackbar.Add($"저장 실패: {ex.Message}", Severity.Error);
}
finally
{
isSaving = false;
}
}
}
@@ -0,0 +1,148 @@
@page "/admin/announcements"
@attribute [Authorize]
@using TaxBaik.Application.Services
@using TaxBaik.Domain.Entities
@inject AnnouncementService AnnouncementService
@inject NavigationManager Navigation
@inject IDialogService DialogService
@inject ISnackbar Snackbar
<PageTitle>공지사항 관리</PageTitle>
<section class="admin-page-hero">
<div>
<MudText Typo="Typo.caption" Class="admin-eyebrow">Homepage</MudText>
<MudText Typo="Typo.h4" Class="admin-page-title">공지사항 관리</MudText>
<MudText Typo="Typo.body2" Class="admin-page-subtitle">홈페이지 상단에 노출되는 공지사항을 등록하고 관리합니다.</MudText>
</div>
<MudButton Variant="Variant.Filled" Color="Color.Primary"
StartIcon="@Icons.Material.Filled.Add"
Href="/taxbaik/admin/announcements/create">
공지 등록
</MudButton>
</section>
<MudPaper Class="admin-surface" Elevation="0">
@if (announcements is null)
{
<MudProgressLinear Indeterminate="true" />
}
else if (!announcements.Any())
{
<MudText Class="pa-4 text-muted">등록된 공지사항이 없습니다.</MudText>
}
else
{
<MudSimpleTable Striped="true" Dense="true" Class="admin-table">
<thead>
<tr>
<th>제목</th>
<th>유형</th>
<th>상태</th>
<th>게시 기간</th>
<th>순서</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in announcements)
{
<tr>
<td>@item.Title</td>
<td>
<MudChip Size="Size.Small" Color="@GetTypeColor(item.DisplayType)">
@GetTypeLabel(item.DisplayType)
</MudChip>
</td>
<td>
@if (IsCurrentlyActive(item))
{
<MudChip Size="Size.Small" Color="Color.Success">노출 중</MudChip>
}
else if (!item.IsActive)
{
<MudChip Size="Size.Small" Color="Color.Default">비활성</MudChip>
}
else
{
<MudChip Size="Size.Small" Color="Color.Warning">기간 외</MudChip>
}
</td>
<td class="small">
@FormatPeriod(item)
</td>
<td>@item.SortOrder</td>
<td>
<MudButtonGroup Size="Size.Small" Variant="Variant.Outlined">
<MudButton @onclick="@(() => Navigation.NavigateTo($"/taxbaik/admin/announcements/{item.Id}/edit"))">
수정
</MudButton>
<MudButton Color="Color.Error" @onclick="@(() => DeleteAsync(item))">
삭제
</MudButton>
</MudButtonGroup>
</td>
</tr>
}
</tbody>
</MudSimpleTable>
}
</MudPaper>
@code {
private List<Announcement>? announcements;
protected override async Task OnInitializedAsync()
{
await LoadAsync();
}
private async Task LoadAsync()
{
announcements = (await AnnouncementService.GetAllAsync()).ToList();
}
private async Task DeleteAsync(Announcement item)
{
var confirmed = await DialogService.ShowMessageBox(
"공지 삭제",
$"'{item.Title}' 공지를 삭제하시겠습니까?",
yesText: "삭제", cancelText: "취소");
if (confirmed != true) return;
await AnnouncementService.DeleteAsync(item.Id);
Snackbar.Add("공지사항이 삭제되었습니다.", Severity.Success);
await LoadAsync();
}
private static bool IsCurrentlyActive(Announcement a)
{
if (!a.IsActive) return false;
var now = DateTime.UtcNow;
if (a.StartsAt.HasValue && a.StartsAt > now) return false;
if (a.EndsAt.HasValue && a.EndsAt < now) return false;
return true;
}
private static string FormatPeriod(Announcement a)
{
var start = a.StartsAt?.ToLocalTime().ToString("MM/dd") ?? "즉시";
var end = a.EndsAt?.ToLocalTime().ToString("MM/dd") ?? "무기한";
return $"{start} ~ {end}";
}
private static Color GetTypeColor(string type) => type switch
{
"urgent" => Color.Error,
"banner" => Color.Warning,
_ => Color.Info
};
private static string GetTypeLabel(string type) => type switch
{
"urgent" => "긴급",
"banner" => "배너",
_ => "일반"
};
}
@@ -5,20 +5,24 @@
<PageTitle>블로그 관리</PageTitle>
<div class="mb-4 d-flex justify-content-between align-items-center">
<MudText Typo="Typo.h5">📝 블로그 관리</MudText>
<MudButton Variant="Variant.Filled" Color="Color.Primary"
Href="/taxbaik/admin/blog/create">새 포스트</MudButton>
</div>
<section class="admin-page-hero">
<div>
<MudText Typo="Typo.caption" Class="admin-eyebrow">Content</MudText>
<MudText Typo="Typo.h4" Class="admin-page-title">블로그 관리</MudText>
<MudText Typo="Typo.body2" Class="admin-page-subtitle">검색 유입 콘텐츠의 발행 상태와 성과를 관리합니다.</MudText>
</div>
<MudButton Variant="Variant.Filled" Color="Color.Primary" StartIcon="@Icons.Material.Filled.EditNote"
Href="/taxbaik/admin/blog/create">새 포스트 작성</MudButton>
</section>
<MudPaper Class="pa-4 mb-4" Elevation="1">
<MudPaper Class="admin-surface mb-4" Elevation="0">
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.subtitle1">@($"전체 포스트 {totalPosts}개")</MudText>
<MudText Typo="Typo.body2">페이지 @currentPage / @totalPages</MudText>
</MudStack>
</MudPaper>
<MudDataGrid Items="@posts" Striped="true" Hoverable="true" Loading="@isLoading">
<MudDataGrid Items="@posts" Striped="true" Hoverable="true" Loading="@isLoading" Class="admin-grid">
<Columns>
<PropertyColumn Property="x => x.Title" Title="제목" />
<PropertyColumn Property="x => x.IsPublished" Title="발행">
@@ -31,9 +35,9 @@
<PropertyColumn Property="x => x.CreatedAt" Title="작성일" Format="yyyy-MM-dd" />
<TemplateColumn>
<CellTemplate Context="cell">
<MudButton Variant="Variant.Text" Color="Color.Primary"
Href="@($"/taxbaik/admin/blog/{cell.Item.Id}/edit")">수정</MudButton>
<MudButton Variant="Variant.Text" Color="Color.Error"
<MudButton Variant="Variant.Outlined" Size="Size.Small" Color="Color.Primary"
Href="@($"/taxbaik/admin/blog/{cell.Item.Id}/edit")">수정하기</MudButton>
<MudButton Variant="Variant.Text" Size="Size.Small" Color="Color.Error"
@onclick="@(async () => await DeletePost(cell.Item.Id))">삭제</MudButton>
</CellTemplate>
</TemplateColumn>
@@ -5,41 +5,60 @@
<PageTitle>대시보드</PageTitle>
<h1 class="mb-4">대시보드</h1>
<section class="admin-page-hero">
<div>
<MudText Typo="Typo.caption" Class="admin-eyebrow">Overview</MudText>
<MudText Typo="Typo.h4" Class="admin-page-title">대시보드</MudText>
<MudText Typo="Typo.body2" Class="admin-page-subtitle">문의 흐름과 콘텐츠 상태를 한 화면에서 확인합니다.</MudText>
</div>
<MudButton Variant="Variant.Filled" Color="Color.Primary" StartIcon="@Icons.Material.Filled.Add" Href="/taxbaik/admin/blog/create">
새 포스트 작성
</MudButton>
</section>
<MudGrid>
<MudGrid Class="admin-metric-grid">
<MudItem xs="12" sm="6" md="3">
<MudPaper Class="pa-4" Elevation="1">
<MudText Typo="Typo.subtitle2">이번달 문의</MudText>
<MudText Typo="Typo.h4">@summary.ThisMonthInquiries</MudText>
<MudPaper Class="admin-metric-card accent-blue" Elevation="0">
<MudText Typo="Typo.caption">이번달 문의</MudText>
<MudText Typo="Typo.h3">@summary.ThisMonthInquiries</MudText>
<MudText Typo="Typo.body2">월간 상담 유입</MudText>
</MudPaper>
</MudItem>
<MudItem xs="12" sm="6" md="3">
<MudPaper Class="pa-4" Elevation="1">
<MudText Typo="Typo.subtitle2">신규 문의</MudText>
<MudText Typo="Typo.h4">@summary.NewInquiries</MudText>
<MudPaper Class="admin-metric-card accent-amber" Elevation="0">
<MudText Typo="Typo.caption">신규 문의</MudText>
<MudText Typo="Typo.h3">@summary.NewInquiries</MudText>
<MudText Typo="Typo.body2">처리 대기</MudText>
</MudPaper>
</MudItem>
<MudItem xs="12" sm="6" md="3">
<MudPaper Class="pa-4" Elevation="1">
<MudText Typo="Typo.subtitle2">전체 포스트</MudText>
<MudText Typo="Typo.h4">@summary.TotalPosts</MudText>
<MudPaper Class="admin-metric-card accent-slate" Elevation="0">
<MudText Typo="Typo.caption">전체 포스트</MudText>
<MudText Typo="Typo.h3">@summary.TotalPosts</MudText>
<MudText Typo="Typo.body2">콘텐츠 자산</MudText>
</MudPaper>
</MudItem>
<MudItem xs="12" sm="6" md="3">
<MudPaper Class="pa-4" Elevation="1">
<MudText Typo="Typo.subtitle2">발행된 포스트</MudText>
<MudText Typo="Typo.h4">@summary.PublishedPosts</MudText>
<MudPaper Class="admin-metric-card accent-green" Elevation="0">
<MudText Typo="Typo.caption">발행된 포스트</MudText>
<MudText Typo="Typo.h3">@summary.PublishedPosts</MudText>
<MudText Typo="Typo.body2">검색 노출 대상</MudText>
</MudPaper>
</MudItem>
</MudGrid>
<MudPaper Class="pa-4 mt-4" Elevation="1">
<MudText Typo="Typo.h6" Class="mb-3">최근 문의</MudText>
<MudSimpleTable Striped="true" Dense="true">
<MudPaper Class="admin-surface mt-4" Elevation="0">
<div class="admin-section-header">
<div>
<MudText Typo="Typo.h6">최근 문의</MudText>
<MudText Typo="Typo.body2">최근 유입된 상담 요청을 빠르게 확인합니다.</MudText>
</div>
<MudButton Variant="Variant.Outlined" Color="Color.Primary" Href="/taxbaik/admin/inquiries">문의 전체 보기</MudButton>
</div>
<MudSimpleTable Striped="true" Dense="true" Class="admin-table">
<thead>
<tr>
<th>이름</th>
@@ -59,7 +78,7 @@
<td>
<MudChip Size="Size.Small"
Color="@(inquiry.Status == "new" ? Color.Warning : inquiry.Status == "contacted" ? Color.Info : Color.Success)">
@inquiry.Status
@GetStatusLabel(inquiry.Status)
</MudChip>
</td>
<td>@inquiry.CreatedAt.ToString("yyyy-MM-dd")</td>
@@ -75,5 +94,13 @@
protected override async Task OnInitializedAsync()
{
summary = await DashboardService.GetSummaryAsync();
}
}
private static string GetStatusLabel(string status) => status switch
{
"new" => "신규",
"contacted" => "연락함",
"completed" => "완료",
_ => status
};
}
@@ -5,9 +5,16 @@
<PageTitle>문의 관리</PageTitle>
<MudText Typo="Typo.h5" Class="mb-4">💬 문의 관리</MudText>
<section class="admin-page-hero">
<div>
<MudText Typo="Typo.caption" Class="admin-eyebrow">Customer Requests</MudText>
<MudText Typo="Typo.h4" Class="admin-page-title">문의 관리</MudText>
<MudText Typo="Typo.body2" Class="admin-page-subtitle">상담 요청을 상태별로 확인하고 후속 조치를 기록합니다.</MudText>
</div>
</section>
<MudTabs>
<MudPaper Class="admin-surface" Elevation="0">
<MudTabs Rounded="true" Elevation="0" Class="admin-tabs">
<MudTabPanel Text="전체">
<InquiryTable Status="" />
</MudTabPanel>
@@ -21,3 +28,4 @@
<InquiryTable Status="completed" />
</MudTabPanel>
</MudTabs>
</MudPaper>
@@ -5,6 +5,7 @@
@inject IApiClient ApiClient
@inject NavigationManager NavigationManager
@inject CustomAuthenticationStateProvider AuthStateProvider
@inject IJSRuntime Js
<PageTitle>로그인</PageTitle>
@@ -58,6 +59,12 @@
private LoginModel model = new();
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
await Js.InvokeVoidAsync("taxbaikAdminSession.syncRouteClass");
}
private async Task HandleLogin()
{
if (isLoading)
@@ -9,9 +9,23 @@
<PageTitle>설정</PageTitle>
<MudText Typo="Typo.h5" Class="mb-4">⚙️ 사이트 설정</MudText>
<section class="admin-page-hero">
<div>
<MudText Typo="Typo.caption" Class="admin-eyebrow">System</MudText>
<MudText Typo="Typo.h4" Class="admin-page-title">설정</MudText>
<MudText Typo="Typo.body2" Class="admin-page-subtitle">공개 사이트 연락처와 관리자 계정 보안을 관리합니다.</MudText>
</div>
</section>
<MudPaper Class="pa-4" Elevation="1">
<MudGrid>
<MudItem xs="12" md="7">
<MudPaper Class="admin-surface" Elevation="0">
<div class="admin-section-header compact">
<div>
<MudText Typo="Typo.h6">사이트 정보</MudText>
<MudText Typo="Typo.body2">홈페이지와 문의 알림에 노출되는 기본 정보입니다.</MudText>
</div>
</div>
<MudForm>
<MudTextField @bind-Value="phone" Label="전화번호"
Variant="Variant.Outlined" Class="mb-4" />
@@ -26,12 +40,20 @@
Variant="Variant.Outlined" Class="mb-4" />
<MudButton Variant="Variant.Filled" Color="Color.Primary"
@onclick="SaveSettings">저장</MudButton>
StartIcon="@Icons.Material.Filled.Save"
@onclick="SaveSettings">사이트 정보 저장</MudButton>
</MudForm>
</MudPaper>
</MudItem>
<MudPaper Class="pa-4 mt-4" Elevation="1">
<MudText Typo="Typo.h6" Class="mb-4">계정 관리</MudText>
<MudItem xs="12" md="5">
<MudPaper Class="admin-surface admin-account-card" Elevation="0">
<div class="admin-section-header compact">
<div>
<MudText Typo="Typo.h6">계정 관리</MudText>
<MudText Typo="Typo.body2">비밀번호는 12자 이상으로 관리합니다.</MudText>
</div>
</div>
<MudForm>
<MudTextField @bind-Value="currentPassword" Label="현재 비밀번호" InputType="InputType.Password"
@@ -45,11 +67,14 @@
<MudButton Variant="Variant.Filled" Color="Color.Primary"
Disabled="@isChangingPassword"
StartIcon="@Icons.Material.Filled.LockReset"
@onclick="ChangePassword">
@(isChangingPassword ? "변경 중..." : "비밀번호 변경")
</MudButton>
</MudForm>
</MudPaper>
</MudItem>
</MudGrid>
@code {
private string phone = "010-4122-8268";
+200 -93
View File
@@ -1,37 +1,107 @@
@page
@model TaxBaik.Web.Pages.IndexModel
@{
ViewData["Title"] = "백원숙 세무회계 | 사업자·부동산·증여 세무 상담";
var season = Model.CurrentSeason;
ViewData["Title"] = season != null
? $"백원숙 세무회계 | {season.Name} — 지금 상담하세요"
: "백원숙 세무회계 | 사업자·부동산·증여 세무 상담";
ViewData["Description"] = "사업자 기장, 부동산 양도세·증여세, 종합소득세 전문 상담. 온라인 맞춤 상담 제공.";
}
<!-- Hero Section — 강임팩트 -->
<section class="hero-section text-white pt-5 pb-4">
<div class="container">
<div class="row align-items-center py-4">
<div class="col-lg-7">
<span class="badge bg-primary-badge mb-3">경험 있는 세무사의 맞춤 전략</span>
<h1 class="mb-3">
세금과 자산<br/>
<span style="color: #E8E4D8;">한 번에 해결하는</span>
</h1>
<p class="fs-5 mb-4" style="line-height: 1.8; opacity: 0.95;">
사업자 세무, 부동산 거래, 가족자산 관리를 위한<br/>
통합 솔루션을 제공합니다.
</p>
<div class="d-flex gap-3 flex-wrap">
<a href="/taxbaik/contact" class="btn btn-primary btn-lg">무료 상담 신청</a>
<a href="javascript:void(0);" class="btn btn-outline-primary btn-lg" onclick="openKakao()" style="border-color: white; color: white;">
💬 카카오 채널 문의
</a>
</div>
</div>
<div class="col-lg-5 d-none d-lg-block text-center">
<div style="font-size: 120px; opacity: 0.15;">📋</div>
@* ─── 공지사항 배너 (관리자 등록 공지) ─── *@
@if (Model.ActiveAnnouncements.Count > 0)
{
foreach (var notice in Model.ActiveAnnouncements)
{
<div class="announcement-bar announcement-bar--@notice.DisplayType">
<div class="container d-flex align-items-center gap-2 py-2">
<span class="announcement-icon">
@if (notice.DisplayType == "urgent") { <text>🚨</text> }
else if (notice.DisplayType == "banner") { <text>📢</text> }
else { <text>️</text> }
</span>
<span class="announcement-text fw-semibold">@notice.Title</span>
@if (!string.IsNullOrEmpty(notice.Content))
{
<span class="d-none d-md-inline text-muted small ms-2">— @notice.Content</span>
}
</div>
</div>
</div>
</section>
}
}
@* ─── Hero Section ─── *@
@if (season != null)
{
<section class="hero-section hero-section--seasonal text-white pt-5 pb-4">
<div class="container">
<div class="row align-items-center py-4">
<div class="col-lg-7">
<span class="badge bg-danger-badge mb-3 fs-6 px-3 py-2">
@season.UrgencyBadge
</span>
<h1 class="mb-3" style="white-space: pre-line;">@season.HeroHeadline</h1>
<p class="fs-5 mb-4" style="line-height: 1.8; opacity: 0.95;">
@season.HeroSubtext
</p>
<div class="d-flex gap-3 flex-wrap">
<a href="/taxbaik/contact" class="btn btn-warning btn-lg fw-bold">
⏰ @season.CtaText
</a>
<a href="javascript:void(0);" class="btn btn-outline-primary btn-lg"
onclick="openKakao()" style="border-color: white; color: white;">
💬 카카오 채널 문의
</a>
</div>
@if (season.DaysUntilDeadline <= 7)
{
<p class="mt-3 small" style="opacity: 0.8;">
마감까지 <strong>@(season.DaysUntilDeadline)일</strong> 남았습니다.
지금 바로 상담 신청하세요.
</p>
}
</div>
<div class="col-lg-5 d-none d-lg-block text-center">
<div class="seasonal-deadline-badge">
<div class="deadline-label">마감</div>
<div class="deadline-date">@season.Deadline.ToString("M월 d일")</div>
<div class="deadline-days">D-@season.DaysUntilDeadline</div>
</div>
</div>
</div>
</div>
</section>
}
else
{
<section class="hero-section text-white pt-5 pb-4">
<div class="container">
<div class="row align-items-center py-4">
<div class="col-lg-7">
<span class="badge bg-primary-badge mb-3">경험 있는 세무사의 맞춤 전략</span>
<h1 class="mb-3">
세금과 자산<br/>
<span style="color: #E8E4D8;">한 번에 해결하는</span>
</h1>
<p class="fs-5 mb-4" style="line-height: 1.8; opacity: 0.95;">
사업자 세무, 부동산 거래, 가족자산 관리를 위한<br/>
통합 솔루션을 제공합니다.
</p>
<div class="d-flex gap-3 flex-wrap">
<a href="/taxbaik/contact" class="btn btn-primary btn-lg">무료 상담 신청</a>
<a href="javascript:void(0);" class="btn btn-outline-primary btn-lg"
onclick="openKakao()" style="border-color: white; color: white;">
💬 카카오 채널 문의
</a>
</div>
</div>
<div class="col-lg-5 d-none d-lg-block text-center">
<div style="font-size: 120px; opacity: 0.15;">📋</div>
</div>
</div>
</div>
</section>
}
<!-- 신뢰도 스트립 — 자격과 경험 -->
<section class="trust-strip">
@@ -62,7 +132,7 @@
</div>
</section>
<!-- 서비스 영역 — 전문성 강조 -->
<!-- 서비스 영역 -->
<section class="py-5">
<div class="container">
<div class="text-center mb-5">
@@ -72,66 +142,88 @@
</p>
</div>
@{
var focusService = season?.FocusService ?? "";
// 시즌에 따라 서비스 카드 순서 결정: 시즌 관련 카드가 맨 앞
var cardOrder = focusService switch
{
"real-estate-tax" => new[] { "real-estate-tax", "business-tax", "family-asset" },
"family-asset" => new[] { "family-asset", "business-tax", "real-estate-tax" },
_ => new[] { "business-tax", "real-estate-tax", "family-asset" }
};
}
<div class="row g-4">
<!-- 사업자 세무 -->
<div class="col-lg-4 col-md-6">
<div class="card service-card h-100">
<div class="service-icon">🏪</div>
<div class="card-body pt-0">
<h3 class="card-title">사업자 세무</h3>
<ul class="list-unstyled small mb-3">
<li class="mb-2">✓ 정확한 기장 및 결산</li>
<li class="mb-2">✓ 세금계산서 관리</li>
<li class="mb-2">✓ 경비처리 최적화</li>
<li class="mb-2">✓ 절세 전략 수립</li>
</ul>
<p class="text-muted small">
초기부터 세무 전략을 수립하면 연간 최대 수백만 원의 절세가 가능합니다.
</p>
<a href="/taxbaik/services#business-tax" class="btn btn-sm btn-outline-primary mt-3">자세히 보기</a>
@foreach (var cardKey in cardOrder)
{
var isFeatured = cardKey == focusService;
if (cardKey == "business-tax")
{
<div class="col-lg-4 col-md-6">
<div class="card service-card h-100 @(isFeatured ? "service-card--featured" : "")">
@if (isFeatured) { <div class="service-card-badge">현재 시즌</div> }
<div class="service-icon">🏪</div>
<div class="card-body pt-0">
<h3 class="card-title">사업자 세무</h3>
<ul class="list-unstyled small mb-3">
<li class="mb-2">✓ 정확한 기장 및 결산</li>
<li class="mb-2">✓ 세금계산서 관리</li>
<li class="mb-2">✓ 경비처리 최적화</li>
<li class="mb-2">✓ 절세 전략 수립</li>
</ul>
<p class="text-muted small">
초기부터 세무 전략을 수립하면 연간 최대 수백만 원의 절세가 가능합니다.
</p>
<a href="/taxbaik/services#business-tax" class="btn btn-sm btn-outline-primary mt-3">자세히 보기</a>
</div>
</div>
</div>
</div>
</div>
<!-- 부동산 세금 -->
<div class="col-lg-4 col-md-6">
<div class="card service-card h-100">
<div class="service-icon">🏠</div>
<div class="card-body pt-0">
<h3 class="card-title">부동산 세금</h3>
<ul class="list-unstyled small mb-3">
<li class="mb-2">✓ 양도세 최소화</li>
<li class="mb-2">✓ 취득세 절감</li>
<li class="mb-2">✓ 임대소득 관리</li>
<li class="mb-2">✓ 다주택자 세무</li>
</ul>
<p class="text-muted small">
부동산 거래 시 미리 상담하면 세금 부담을 크게 줄일 수 있습니다.
</p>
<a href="/taxbaik/services#real-estate-tax" class="btn btn-sm btn-outline-primary mt-3">자세히 보기</a>
}
else if (cardKey == "real-estate-tax")
{
<div class="col-lg-4 col-md-6">
<div class="card service-card h-100 @(isFeatured ? "service-card--featured" : "")">
@if (isFeatured) { <div class="service-card-badge">현재 시즌</div> }
<div class="service-icon">🏠</div>
<div class="card-body pt-0">
<h3 class="card-title">부동산 세금</h3>
<ul class="list-unstyled small mb-3">
<li class="mb-2">✓ 양도세 최소화</li>
<li class="mb-2">✓ 취득세 절감</li>
<li class="mb-2">✓ 임대소득 관리</li>
<li class="mb-2">✓ 다주택자 세무</li>
</ul>
<p class="text-muted small">
부동산 거래 시 미리 상담하면 세금 부담을 크게 줄일 수 있습니다.
</p>
<a href="/taxbaik/services#real-estate-tax" class="btn btn-sm btn-outline-primary mt-3">자세히 보기</a>
</div>
</div>
</div>
</div>
</div>
<!-- 가족자산 & 증여 -->
<div class="col-lg-4 col-md-6">
<div class="card service-card h-100">
<div class="service-icon">👨‍👩‍👧‍👦</div>
<div class="card-body pt-0">
<h3 class="card-title">가족자산 관리</h3>
<ul class="list-unstyled small mb-3">
<li class="mb-2">✓ 증여세 전략</li>
<li class="mb-2">✓ 상속세 대비</li>
<li class="mb-2">✓ 자산 이전 계획</li>
<li class="mb-2">✓ 가족법인 설립</li>
</ul>
<p class="text-muted small">
세대 이전 전에 사전 계획하면 세금 부담을 현저히 줄일 수 있습니다.
</p>
<a href="/taxbaik/services#family-asset" class="btn btn-sm btn-outline-primary mt-3">자세히 보기</a>
}
else
{
<div class="col-lg-4 col-md-6">
<div class="card service-card h-100 @(isFeatured ? "service-card--featured" : "")">
@if (isFeatured) { <div class="service-card-badge">현재 시즌</div> }
<div class="service-icon">👨‍👩‍👧‍👦</div>
<div class="card-body pt-0">
<h3 class="card-title">가족자산 관리</h3>
<ul class="list-unstyled small mb-3">
<li class="mb-2">✓ 증여세 전략</li>
<li class="mb-2">✓ 상속세 대비</li>
<li class="mb-2">✓ 자산 이전 계획</li>
<li class="mb-2">✓ 가족법인 설립</li>
</ul>
<p class="text-muted small">
세대 이전 전에 사전 계획하면 세금 부담을 현저히 줄일 수 있습니다.
</p>
<a href="/taxbaik/services#family-asset" class="btn btn-sm btn-outline-primary mt-3">자세히 보기</a>
</div>
</div>
</div>
</div>
</div>
}
}
</div>
</div>
</section>
@@ -214,17 +306,32 @@
</div>
</section>
<!-- 최종 CTA — 강렬한 다크 배경 -->
<!-- 최종 CTA -->
<section class="py-5" style="background: linear-gradient(135deg, #2E5C4E 0%, #1F3A30 100%); color: white;">
<div class="container text-center">
<h2 class="mb-3 fw-bold" style="font-size: 2.5rem;">세금 고민은 이제 끝!</h2>
<p class="fs-5 mb-5" style="opacity: 0.95; max-width: 500px; margin-left: auto; margin-right: auto;">
무료 상담으로 현재 상황을 진단하고<br/>
맞춤형 절세 전략을 받아보세요.
</p>
<div class="d-flex gap-3 justify-content-center flex-wrap">
<a href="/taxbaik/contact" class="btn btn-warning btn-lg">상담 신청하기</a>
<a href="javascript:void(0);" onclick="openKakao()" class="btn btn-light btn-lg">카카오로 문의</a>
</div>
@if (season != null)
{
<h2 class="mb-3 fw-bold" style="font-size: 2.5rem;">@season.Name 마감이 다가옵니다!</h2>
<p class="fs-5 mb-5" style="opacity: 0.95; max-width: 500px; margin-left: auto; margin-right: auto;">
마감 <strong>D-@(season.DaysUntilDeadline)일</strong> — 지금 바로 상담을 신청하세요.<br/>
빠른 검토로 불이익 없이 신고를 완료합니다.
</p>
<div class="d-flex gap-3 justify-content-center flex-wrap">
<a href="/taxbaik/contact" class="btn btn-warning btn-lg">⏰ @season.CtaText</a>
<a href="javascript:void(0);" onclick="openKakao()" class="btn btn-light btn-lg">카카오로 문의</a>
</div>
}
else
{
<h2 class="mb-3 fw-bold" style="font-size: 2.5rem;">세금 고민은 이제 끝!</h2>
<p class="fs-5 mb-5" style="opacity: 0.95; max-width: 500px; margin-left: auto; margin-right: auto;">
무료 상담으로 현재 상황을 진단하고<br/>
맞춤형 절세 전략을 받아보세요.
</p>
<div class="d-flex gap-3 justify-content-center flex-wrap">
<a href="/taxbaik/contact" class="btn btn-warning btn-lg">상담 신청하기</a>
<a href="javascript:void(0);" onclick="openKakao()" class="btn btn-light btn-lg">카카오로 문의</a>
</div>
}
</div>
</section>
+22 -1
View File
@@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Mvc.RazorPages;
using TaxBaik.Application.Seasonal;
using TaxBaik.Application.Services;
using TaxBaik.Domain.Entities;
@@ -7,16 +8,36 @@ namespace TaxBaik.Web.Pages;
public class IndexModel : PageModel
{
private readonly BlogService _blogService;
private readonly SeasonalMarketingService _seasonalMarketingService;
private readonly AnnouncementService _announcementService;
public List<BlogPost> RecentPosts { get; set; } = [];
public CurrentSeasonDto? CurrentSeason { get; set; }
public List<Announcement> ActiveAnnouncements { get; set; } = [];
public IndexModel(BlogService blogService)
public IndexModel(
BlogService blogService,
SeasonalMarketingService seasonalMarketingService,
AnnouncementService announcementService)
{
_blogService = blogService;
_seasonalMarketingService = seasonalMarketingService;
_announcementService = announcementService;
}
public async Task OnGetAsync()
{
CurrentSeason = _seasonalMarketingService.GetCurrentSeason();
try
{
ActiveAnnouncements = (await _announcementService.GetActiveAsync()).ToList();
}
catch
{
ActiveAnnouncements = [];
}
try
{
var (posts, _) = await _blogService.GetPublishedPagedAsync(1, 3);
+87
View File
@@ -554,3 +554,90 @@ img {
background-color: rgba(200, 157, 110, 0.15) !important;
color: var(--color-primary) !important;
}
/* ===== 공지사항 배너 ===== */
.announcement-bar {
border-bottom: 1px solid rgba(0,0,0,0.08);
font-size: 0.9rem;
}
.announcement-bar--info {
background: #E8F4FD;
color: #1565C0;
}
.announcement-bar--banner {
background: #FFF8E1;
color: #E65100;
}
.announcement-bar--urgent {
background: #FFEBEE;
color: #C62828;
}
.announcement-icon {
flex-shrink: 0;
}
.announcement-text {
flex: 1;
}
/* ===== 시즌 Hero ===== */
.hero-section--seasonal {
background: linear-gradient(135deg, #1F3A30 0%, #2E5C4E 60%, #3D7A68 100%);
}
.bg-danger-badge {
background-color: rgba(198, 40, 40, 0.85) !important;
color: #fff !important;
letter-spacing: 0.5px;
}
/* D-Day 카운트다운 위젯 */
.seasonal-deadline-badge {
display: inline-flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 220px;
height: 220px;
border-radius: 50%;
border: 4px solid rgba(255,255,255,0.25);
background: rgba(255,255,255,0.08);
color: white;
backdrop-filter: blur(4px);
}
.deadline-label {
font-size: 0.85rem;
opacity: 0.75;
letter-spacing: 2px;
text-transform: uppercase;
}
.deadline-date {
font-size: 1.6rem;
font-weight: 800;
line-height: 1.2;
margin: 0.25rem 0;
}
.deadline-days {
font-size: 2.8rem;
font-weight: 900;
color: #FFD54F;
line-height: 1;
}
/* ===== 서비스 카드 시즌 강조 ===== */
.service-card--featured {
border: 2px solid var(--color-primary) !important;
position: relative;
}
.service-card-badge {
position: absolute;
top: -12px;
left: 50%;
transform: translateX(-50%);
background: var(--color-primary);
color: white;
font-size: 0.75rem;
font-weight: 700;
padding: 3px 14px;
border-radius: 20px;
white-space: nowrap;
letter-spacing: 0.5px;
}
+8
View File
@@ -1,4 +1,9 @@
window.taxbaikAdminSession = {
syncRouteClass: function () {
document.documentElement.classList.toggle(
'admin-login-route',
window.location.pathname.toLowerCase().endsWith('/admin/login'));
},
clearAuthToken: function () {
try {
localStorage.removeItem('auth_token');
@@ -7,6 +12,9 @@ window.taxbaikAdminSession = {
}
},
watchReconnect: function () {
window.taxbaikAdminSession.syncRouteClass();
window.addEventListener('popstate', window.taxbaikAdminSession.syncRouteClass);
const modal = document.getElementById('components-reconnect-modal');
if (!modal) {
return;