feat: 시즌별 마케팅 + 공지사항 관리 기능 추가
- 연간 세무 캘린더(7개 시즌) 기반 자동 Hero 섹션 전환 - 시즌 감지 시 D-Day 카운트다운, 긴박감 배지, 시즌 CTA 표시 - 서비스 카드 순서 시즌 관련 항목 우선 정렬 - 어드민 공지사항 CRUD (등록·수정·삭제, 기간·유형 설정) - 홈페이지 상단 공지 배너 자동 노출 (일반/배너/긴급) - CLAUDE.md에 세무 캘린더 및 마케팅 방향 하네스 추가 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -744,6 +744,55 @@ SELECT * FROM inquiries ORDER BY created_at DESC LIMIT 1;
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
## 13. 시즌별 마케팅 (Seasonal Marketing)
|
||||
|
||||
### 13.1 핵심 방향
|
||||
|
||||
세무사 사무실은 **1년 중 특정 시기에 특정 고객이 집중**된다. 홈페이지는 이 시기마다 자동으로 전환되어야 한다.
|
||||
|
||||
**목표**: 방문자가 접속한 날짜에 맞는 세무 이벤트를 즉시 인지하고 상담 신청으로 전환
|
||||
|
||||
**전환 방식**:
|
||||
- Hero 섹션 헤드라인과 CTA가 시즌에 맞게 변경됨
|
||||
- 마감 D-7일 이내에는 긴박감 메시지 추가 표시
|
||||
- 시즌 관련 서비스 카드가 맨 앞으로 이동
|
||||
- 최종 CTA도 시즌 문구로 전환
|
||||
- 관리자가 별도 공지사항을 등록하면 모든 페이지 최상단에 배너로 노출
|
||||
|
||||
### 13.2 연간 세무 캘린더
|
||||
|
||||
| 기간 | 이벤트 | Key | 타깃 서비스 |
|
||||
|------|--------|-----|-------------|
|
||||
| 1/1 ~ 1/25 | 부가가치세 2기 확정신고 | `vat-2nd` | business-tax |
|
||||
| 1/15 ~ 2/28 | 연말정산 | `year-end-settlement` | business-tax |
|
||||
| 3/1 ~ 3/31 | 법인세 신고 | `corporate-tax` | business-tax |
|
||||
| 5/1 ~ 5/31 | **종합소득세 신고** (연중 최대 피크) | `income-tax` | business-tax |
|
||||
| 7/1 ~ 7/25 | 부가가치세 1기 확정신고 | `vat-1st` | business-tax |
|
||||
| 11/15 ~ 11/30 | 종합부동산세 납부 | `comprehensive-real-estate-tax` | real-estate-tax |
|
||||
| 12/1 ~ 12/31 | 연말 증여·절세 플래닝 | `year-end-gift` | family-asset |
|
||||
|
||||
캘린더 정의 위치: `TaxBaik.Application/Seasonal/TaxSeasonCalendar.cs`
|
||||
|
||||
시즌 추가/수정은 이 파일만 변경하면 된다. DB·마이그레이션 변경 없음.
|
||||
|
||||
### 13.3 공지사항 (Announcement)
|
||||
|
||||
어드민 `/taxbaik/admin/announcements`에서 관리.
|
||||
|
||||
- **유형**: 일반(info, 파란색) / 배너(banner, 주황색) / 긴급(urgent, 빨간색)
|
||||
- **게시 기간**: 시작일~종료일 설정 가능. 비우면 즉시~무기한
|
||||
- **노출 위치**: 홈페이지 최상단 (공지 배너 스트립)
|
||||
- **우선순위**: sort_order 내림차순
|
||||
|
||||
공지사항은 시즌 Hero와 독립적으로 동작한다. 동시 표시 가능.
|
||||
|
||||
### 13.4 시즌 우선순위 / 광고 규칙 준수
|
||||
|
||||
- 허용: "지금 신고 준비하세요", "마감 전 사전 검토", "D-N일 남았습니다"
|
||||
- 금지: "100% 절세 보장", "최저가 신고", "무료"
|
||||
|
||||
**마지막 체크리스트:**
|
||||
- [ ] 솔루션 빌드 성공 (`dotnet build`)
|
||||
- [ ] 모든 프로젝트 참조 정확
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
namespace TaxBaik.Application.DTOs;
|
||||
|
||||
public class AnnouncementDto
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Title { get; set; } = "";
|
||||
public string? Content { get; set; }
|
||||
public string DisplayType { get; set; } = "info";
|
||||
public bool IsActive { get; set; } = true;
|
||||
public DateTime? StartsAt { get; set; }
|
||||
public DateTime? EndsAt { get; set; }
|
||||
public int SortOrder { get; set; }
|
||||
}
|
||||
@@ -13,6 +13,8 @@ public static class DependencyInjection
|
||||
services.AddScoped<IInquiryNotificationService, NoopInquiryNotificationService>();
|
||||
services.AddScoped<SiteSettingService>();
|
||||
services.AddScoped<CategoryService>();
|
||||
services.AddScoped<AnnouncementService>();
|
||||
services.AddSingleton<SeasonalMarketingService>();
|
||||
return services;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace TaxBaik.Application.Seasonal;
|
||||
|
||||
public record CurrentSeasonDto
|
||||
{
|
||||
public string Key { get; init; } = "";
|
||||
public string Name { get; init; } = "";
|
||||
public string HeroHeadline { get; init; } = "";
|
||||
public string HeroSubtext { get; init; } = "";
|
||||
public string UrgencyBadge { get; init; } = "";
|
||||
public string FocusService { get; init; } = "";
|
||||
public string CtaText { get; init; } = "상담 신청하기";
|
||||
public int DaysUntilDeadline { get; init; }
|
||||
public DateTime Deadline { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
namespace TaxBaik.Application.Seasonal;
|
||||
|
||||
public record TaxSeason
|
||||
{
|
||||
public string Key { get; init; } = "";
|
||||
public string Name { get; init; } = "";
|
||||
|
||||
public int StartMonth { get; init; }
|
||||
public int StartDay { get; init; }
|
||||
public int EndMonth { get; init; }
|
||||
public int EndDay { get; init; }
|
||||
|
||||
public string HeroHeadline { get; init; } = "";
|
||||
public string HeroSubtext { get; init; } = "";
|
||||
public string UrgencyBadge { get; init; } = "";
|
||||
public string FocusService { get; init; } = "";
|
||||
public string CtaText { get; init; } = "상담 신청하기";
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
namespace TaxBaik.Application.Seasonal;
|
||||
|
||||
/// <summary>
|
||||
/// 한국 세무사 사무실 연간 시즌 캘린더.
|
||||
/// 각 시즌이 활성화되면 홈페이지 Hero가 해당 세무 이벤트에 맞게 전환된다.
|
||||
/// </summary>
|
||||
public static class TaxSeasonCalendar
|
||||
{
|
||||
public static readonly IReadOnlyList<TaxSeason> Seasons =
|
||||
[
|
||||
new TaxSeason
|
||||
{
|
||||
Key = "vat-2nd",
|
||||
Name = "부가가치세 2기 확정신고",
|
||||
StartMonth = 1, StartDay = 1,
|
||||
EndMonth = 1, EndDay = 25,
|
||||
HeroHeadline = "부가가치세 2기\n1월 25일 마감",
|
||||
HeroSubtext = "일반과세 사업자 확정신고 · 기한 내 신고로 가산세 방지",
|
||||
UrgencyBadge = "D-{n}일 | 부가세 마감",
|
||||
FocusService = "business-tax",
|
||||
CtaText = "부가세 신고 상담"
|
||||
},
|
||||
new TaxSeason
|
||||
{
|
||||
Key = "year-end-settlement",
|
||||
Name = "연말정산",
|
||||
StartMonth = 1, StartDay = 15,
|
||||
EndMonth = 2, EndDay = 28,
|
||||
HeroHeadline = "연말정산\n지금 준비하세요",
|
||||
HeroSubtext = "직원이 있는 사업자 원천징수 신고 · 환급 최대화",
|
||||
UrgencyBadge = "연말정산 진행 중",
|
||||
FocusService = "business-tax",
|
||||
CtaText = "연말정산 상담"
|
||||
},
|
||||
new TaxSeason
|
||||
{
|
||||
Key = "corporate-tax",
|
||||
Name = "법인세 신고",
|
||||
StartMonth = 3, StartDay = 1,
|
||||
EndMonth = 3, EndDay = 31,
|
||||
HeroHeadline = "법인세\n3월 31일 마감",
|
||||
HeroSubtext = "법인사업자 결산 · 세무조정 · 절세 전략 수립",
|
||||
UrgencyBadge = "D-{n}일 | 법인세 마감",
|
||||
FocusService = "business-tax",
|
||||
CtaText = "법인세 신고 상담"
|
||||
},
|
||||
new TaxSeason
|
||||
{
|
||||
Key = "income-tax",
|
||||
Name = "종합소득세 신고",
|
||||
StartMonth = 5, StartDay = 1,
|
||||
EndMonth = 5, EndDay = 31,
|
||||
HeroHeadline = "종합소득세\n5월 31일 마감",
|
||||
HeroSubtext = "개인사업자 · 임대소득 · 프리랜서 · 기타소득 모두 해당",
|
||||
UrgencyBadge = "D-{n}일 | 종합소득세 마감",
|
||||
FocusService = "business-tax",
|
||||
CtaText = "종합소득세 상담"
|
||||
},
|
||||
new TaxSeason
|
||||
{
|
||||
Key = "vat-1st",
|
||||
Name = "부가가치세 1기 확정신고",
|
||||
StartMonth = 7, StartDay = 1,
|
||||
EndMonth = 7, EndDay = 25,
|
||||
HeroHeadline = "부가가치세 1기\n7월 25일 마감",
|
||||
HeroSubtext = "일반과세 사업자 1기 확정신고 · 매입세액 공제 점검",
|
||||
UrgencyBadge = "D-{n}일 | 부가세 마감",
|
||||
FocusService = "business-tax",
|
||||
CtaText = "부가세 신고 상담"
|
||||
},
|
||||
new TaxSeason
|
||||
{
|
||||
Key = "comprehensive-real-estate-tax",
|
||||
Name = "종합부동산세",
|
||||
StartMonth = 11, StartDay = 15,
|
||||
EndMonth = 11, EndDay = 30,
|
||||
HeroHeadline = "종합부동산세\n납부 시즌",
|
||||
HeroSubtext = "다주택자 · 임대사업자 세부담 분석 · 분납·합산배제 검토",
|
||||
UrgencyBadge = "D-{n}일 | 종부세 납부",
|
||||
FocusService = "real-estate-tax",
|
||||
CtaText = "종부세 절세 상담"
|
||||
},
|
||||
new TaxSeason
|
||||
{
|
||||
Key = "year-end-gift",
|
||||
Name = "연말 증여·절세 플래닝",
|
||||
StartMonth = 12, StartDay = 1,
|
||||
EndMonth = 12, EndDay = 31,
|
||||
HeroHeadline = "연말 절세 플래닝\n마지막 기회",
|
||||
HeroSubtext = "증여 공제 한도 · 자산 이전 · 법인전환 연간 마감",
|
||||
UrgencyBadge = "D-{n}일 | 연간 증여 한도 마감",
|
||||
FocusService = "family-asset",
|
||||
CtaText = "연말 절세 상담"
|
||||
}
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
namespace TaxBaik.Application.Services;
|
||||
|
||||
using TaxBaik.Application.DTOs;
|
||||
using TaxBaik.Domain.Entities;
|
||||
using TaxBaik.Domain.Interfaces;
|
||||
|
||||
public class AnnouncementService(IAnnouncementRepository repository)
|
||||
{
|
||||
public Task<IEnumerable<Announcement>> GetActiveAsync(CancellationToken ct = default)
|
||||
=> repository.GetActiveAsync(ct);
|
||||
|
||||
public Task<IEnumerable<Announcement>> GetAllAsync(CancellationToken ct = default)
|
||||
=> repository.GetAllAsync(ct);
|
||||
|
||||
public Task<Announcement?> GetByIdAsync(int id, CancellationToken ct = default)
|
||||
=> repository.GetByIdAsync(id, ct);
|
||||
|
||||
public Task<int> CreateAsync(AnnouncementDto dto, CancellationToken ct = default)
|
||||
{
|
||||
var entity = MapToEntity(dto);
|
||||
return repository.CreateAsync(entity, ct);
|
||||
}
|
||||
|
||||
public Task UpdateAsync(AnnouncementDto dto, CancellationToken ct = default)
|
||||
{
|
||||
var entity = MapToEntity(dto);
|
||||
return repository.UpdateAsync(entity, ct);
|
||||
}
|
||||
|
||||
public Task DeleteAsync(int id, CancellationToken ct = default)
|
||||
=> repository.DeleteAsync(id, ct);
|
||||
|
||||
private static Announcement MapToEntity(AnnouncementDto dto) => new()
|
||||
{
|
||||
Id = dto.Id,
|
||||
Title = dto.Title.Trim(),
|
||||
Content = string.IsNullOrWhiteSpace(dto.Content) ? null : dto.Content.Trim(),
|
||||
DisplayType = dto.DisplayType,
|
||||
IsActive = dto.IsActive,
|
||||
StartsAt = dto.StartsAt,
|
||||
EndsAt = dto.EndsAt,
|
||||
SortOrder = dto.SortOrder
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
namespace TaxBaik.Application.Services;
|
||||
|
||||
using TaxBaik.Application.Seasonal;
|
||||
|
||||
public class SeasonalMarketingService
|
||||
{
|
||||
public CurrentSeasonDto? GetCurrentSeason()
|
||||
{
|
||||
var today = DateTime.Today;
|
||||
|
||||
foreach (var season in TaxSeasonCalendar.Seasons)
|
||||
{
|
||||
var start = new DateTime(today.Year, season.StartMonth, season.StartDay);
|
||||
var end = new DateTime(today.Year, season.EndMonth, season.EndDay);
|
||||
|
||||
if (today >= start && today <= end)
|
||||
{
|
||||
var days = (end - today).Days;
|
||||
return new CurrentSeasonDto
|
||||
{
|
||||
Key = season.Key,
|
||||
Name = season.Name,
|
||||
HeroHeadline = season.HeroHeadline,
|
||||
HeroSubtext = season.HeroSubtext,
|
||||
UrgencyBadge = season.UrgencyBadge.Replace("{n}", days.ToString()),
|
||||
FocusService = season.FocusService,
|
||||
CtaText = season.CtaText,
|
||||
DaysUntilDeadline = days,
|
||||
Deadline = end
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public IReadOnlyList<TaxSeason> GetFullCalendar() => TaxSeasonCalendar.Seasons;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
namespace TaxBaik.Domain.Entities;
|
||||
|
||||
public class Announcement
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Title { get; set; } = null!;
|
||||
public string? Content { get; set; }
|
||||
public string DisplayType { get; set; } = "info";
|
||||
public bool IsActive { get; set; }
|
||||
public DateTime? StartsAt { get; set; }
|
||||
public DateTime? EndsAt { get; set; }
|
||||
public int SortOrder { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public DateTime UpdatedAt { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
namespace TaxBaik.Domain.Interfaces;
|
||||
|
||||
using TaxBaik.Domain.Entities;
|
||||
|
||||
public interface IAnnouncementRepository
|
||||
{
|
||||
Task<IEnumerable<Announcement>> GetActiveAsync(CancellationToken cancellationToken = default);
|
||||
Task<IEnumerable<Announcement>> GetAllAsync(CancellationToken cancellationToken = default);
|
||||
Task<Announcement?> GetByIdAsync(int id, CancellationToken cancellationToken = default);
|
||||
Task<int> CreateAsync(Announcement announcement, CancellationToken cancellationToken = default);
|
||||
Task UpdateAsync(Announcement announcement, CancellationToken cancellationToken = default);
|
||||
Task DeleteAsync(int id, CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -15,6 +15,7 @@ public static class DependencyInjection
|
||||
services.AddScoped<IBlogPostRepository, BlogPostRepository>();
|
||||
services.AddScoped<IInquiryRepository, InquiryRepository>();
|
||||
services.AddScoped<ISiteSettingRepository, SiteSettingRepository>();
|
||||
services.AddScoped<IAnnouncementRepository, AnnouncementRepository>();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
namespace TaxBaik.Infrastructure.Repositories;
|
||||
|
||||
using Dapper;
|
||||
using TaxBaik.Domain.Entities;
|
||||
using TaxBaik.Domain.Interfaces;
|
||||
|
||||
public class AnnouncementRepository(IDbConnectionFactory connectionFactory)
|
||||
: BaseRepository(connectionFactory), IAnnouncementRepository
|
||||
{
|
||||
private const string SelectColumns =
|
||||
"id, title, content, display_type, is_active, starts_at, ends_at, sort_order, created_at, updated_at";
|
||||
|
||||
public async Task<IEnumerable<Announcement>> GetActiveAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var conn = Conn();
|
||||
return await conn.QueryAsync<Announcement>(
|
||||
$@"SELECT {SelectColumns}
|
||||
FROM announcements
|
||||
WHERE is_active = TRUE
|
||||
AND (starts_at IS NULL OR starts_at <= NOW())
|
||||
AND (ends_at IS NULL OR ends_at >= NOW())
|
||||
ORDER BY sort_order DESC, created_at DESC");
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<Announcement>> GetAllAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var conn = Conn();
|
||||
return await conn.QueryAsync<Announcement>(
|
||||
$"SELECT {SelectColumns} FROM announcements ORDER BY sort_order DESC, created_at DESC");
|
||||
}
|
||||
|
||||
public async Task<Announcement?> GetByIdAsync(int id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var conn = Conn();
|
||||
return await conn.QueryFirstOrDefaultAsync<Announcement>(
|
||||
$"SELECT {SelectColumns} FROM announcements WHERE id = @Id",
|
||||
new { Id = id });
|
||||
}
|
||||
|
||||
public async Task<int> CreateAsync(Announcement announcement, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var conn = Conn();
|
||||
return await conn.QueryFirstAsync<int>(
|
||||
@"INSERT INTO announcements
|
||||
(title, content, display_type, is_active, starts_at, ends_at, sort_order, created_at, updated_at)
|
||||
VALUES
|
||||
(@Title, @Content, @DisplayType, @IsActive, @StartsAt, @EndsAt, @SortOrder, NOW(), NOW())
|
||||
RETURNING id",
|
||||
announcement);
|
||||
}
|
||||
|
||||
public async Task UpdateAsync(Announcement announcement, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var conn = Conn();
|
||||
await conn.ExecuteAsync(
|
||||
@"UPDATE announcements
|
||||
SET title = @Title,
|
||||
content = @Content,
|
||||
display_type = @DisplayType,
|
||||
is_active = @IsActive,
|
||||
starts_at = @StartsAt,
|
||||
ends_at = @EndsAt,
|
||||
sort_order = @SortOrder,
|
||||
updated_at = NOW()
|
||||
WHERE id = @Id",
|
||||
announcement);
|
||||
}
|
||||
|
||||
public async Task DeleteAsync(int id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var conn = Conn();
|
||||
await conn.ExecuteAsync("DELETE FROM announcements WHERE id = @Id", new { Id = id });
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
CREATE TABLE IF NOT EXISTS announcements (
|
||||
id SERIAL PRIMARY KEY,
|
||||
title VARCHAR(200) NOT NULL,
|
||||
content TEXT,
|
||||
display_type VARCHAR(20) NOT NULL DEFAULT 'info',
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
starts_at TIMESTAMPTZ,
|
||||
ends_at TIMESTAMPTZ,
|
||||
sort_order INT NOT NULL DEFAULT 0,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
COMMENT ON COLUMN announcements.display_type IS 'banner | info | urgent';
|
||||
Reference in New Issue
Block a user