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
@@ -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; }
}
+18
View File
@@ -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;
}