feat: WBS-UX-03 FAQ 관리 기능 구현 — 어드민 CRUD + 홈페이지 DB 연동

DB:
- V007__CreateFaqs.sql: faqs 테이블 (question, answer, category,
  sort_order, is_active) + 기본 FAQ 4개 시드

Domain:
- Faq 엔티티
- IFaqRepository (GetActiveAsync, GetAllAsync, CRUD)

Infrastructure:
- FaqRepository: sort_order 정렬, CRUD

Application:
- FaqService: Categories 상수, Validate (질문·답변 필수)

Admin UI (Blazor):
- FaqList.razor: 전체 목록, 활성/비활성 상태 칩, 삭제 확인
- FaqEdit.razor: 질문/답변/카테고리/순서/활성 토글 폼
- MainLayout: 홈페이지 그룹 하위에 FAQ 관리 메뉴 추가

홈페이지:
- Index.cshtml 하드코딩 FAQ → ActiveFaqs DB 루프로 교체
- FAQ 없으면 섹션 전체 숨김 (빈 DB에 안전)
- IndexModel: FaqService 주입, Task.WhenAll 병렬 로드

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-27 23:39:59 +09:00
parent b67002dcf5
commit ccba017e3e
12 changed files with 462 additions and 72 deletions
@@ -16,6 +16,7 @@ public static class DependencyInjection
services.AddScoped<AnnouncementService>(); services.AddScoped<AnnouncementService>();
services.AddSingleton<SeasonalMarketingService>(); services.AddSingleton<SeasonalMarketingService>();
services.AddScoped<ClientService>(); services.AddScoped<ClientService>();
services.AddScoped<FaqService>();
return services; return services;
} }
} }
@@ -0,0 +1,42 @@
namespace TaxBaik.Application.Services;
using TaxBaik.Domain.Entities;
using TaxBaik.Domain.Interfaces;
public class FaqService(IFaqRepository repository)
{
public static readonly string[] Categories =
["기장·세금신고", "부동산", "증여·상속", "기타"];
public async Task<IEnumerable<Faq>> GetActiveAsync(CancellationToken ct = default) =>
await repository.GetActiveAsync(ct);
public async Task<IEnumerable<Faq>> GetAllAsync(CancellationToken ct = default) =>
await repository.GetAllAsync(ct);
public async Task<Faq?> GetByIdAsync(int id, CancellationToken ct = default) =>
await repository.GetByIdAsync(id, ct);
public async Task<int> CreateAsync(Faq faq, CancellationToken ct = default)
{
Validate(faq);
return await repository.CreateAsync(faq, ct);
}
public async Task UpdateAsync(Faq faq, CancellationToken ct = default)
{
Validate(faq);
await repository.UpdateAsync(faq, ct);
}
public async Task DeleteAsync(int id, CancellationToken ct = default) =>
await repository.DeleteAsync(id, ct);
private static void Validate(Faq faq)
{
if (string.IsNullOrWhiteSpace(faq.Question))
throw new ValidationException("질문을 입력하세요.");
if (string.IsNullOrWhiteSpace(faq.Answer))
throw new ValidationException("답변을 입력하세요.");
}
}
+13
View File
@@ -0,0 +1,13 @@
namespace TaxBaik.Domain.Entities;
public class Faq
{
public int Id { get; set; }
public string Question { get; set; } = null!;
public string Answer { get; set; } = null!;
public string? Category { get; set; }
public int SortOrder { get; set; }
public bool IsActive { get; set; } = true;
public DateTime CreatedAt { get; set; }
public DateTime UpdatedAt { get; set; }
}
@@ -0,0 +1,13 @@
namespace TaxBaik.Domain.Interfaces;
using TaxBaik.Domain.Entities;
public interface IFaqRepository
{
Task<IEnumerable<Faq>> GetActiveAsync(CancellationToken ct = default);
Task<IEnumerable<Faq>> GetAllAsync(CancellationToken ct = default);
Task<Faq?> GetByIdAsync(int id, CancellationToken ct = default);
Task<int> CreateAsync(Faq faq, CancellationToken ct = default);
Task UpdateAsync(Faq faq, CancellationToken ct = default);
Task DeleteAsync(int id, CancellationToken ct = default);
}
@@ -17,6 +17,7 @@ public static class DependencyInjection
services.AddScoped<ISiteSettingRepository, SiteSettingRepository>(); services.AddScoped<ISiteSettingRepository, SiteSettingRepository>();
services.AddScoped<IAnnouncementRepository, AnnouncementRepository>(); services.AddScoped<IAnnouncementRepository, AnnouncementRepository>();
services.AddScoped<IClientRepository, ClientRepository>(); services.AddScoped<IClientRepository, ClientRepository>();
services.AddScoped<IFaqRepository, FaqRepository>();
return services; return services;
} }
@@ -0,0 +1,60 @@
namespace TaxBaik.Infrastructure.Repositories;
using Dapper;
using TaxBaik.Domain.Entities;
using TaxBaik.Domain.Interfaces;
public class FaqRepository(IDbConnectionFactory connectionFactory) : BaseRepository(connectionFactory), IFaqRepository
{
private const string SelectColumns =
"id, question, answer, category, sort_order, is_active, created_at, updated_at";
public async Task<IEnumerable<Faq>> GetActiveAsync(CancellationToken ct = default)
{
using var conn = Conn();
return await conn.QueryAsync<Faq>(
$"SELECT {SelectColumns} FROM faqs WHERE is_active = TRUE ORDER BY sort_order, id");
}
public async Task<IEnumerable<Faq>> GetAllAsync(CancellationToken ct = default)
{
using var conn = Conn();
return await conn.QueryAsync<Faq>(
$"SELECT {SelectColumns} FROM faqs ORDER BY sort_order, id");
}
public async Task<Faq?> GetByIdAsync(int id, CancellationToken ct = default)
{
using var conn = Conn();
return await conn.QueryFirstOrDefaultAsync<Faq>(
$"SELECT {SelectColumns} FROM faqs WHERE id = @Id",
new { Id = id });
}
public async Task<int> CreateAsync(Faq faq, CancellationToken ct = default)
{
using var conn = Conn();
return await conn.QueryFirstAsync<int>(
@"INSERT INTO faqs (question, answer, category, sort_order, is_active, created_at, updated_at)
VALUES (@Question, @Answer, @Category, @SortOrder, @IsActive, NOW(), NOW())
RETURNING id",
faq);
}
public async Task UpdateAsync(Faq faq, CancellationToken ct = default)
{
using var conn = Conn();
await conn.ExecuteAsync(
@"UPDATE faqs
SET question = @Question, answer = @Answer, category = @Category,
sort_order = @SortOrder, is_active = @IsActive, updated_at = NOW()
WHERE id = @Id",
faq);
}
public async Task DeleteAsync(int id, CancellationToken ct = default)
{
using var conn = Conn();
await conn.ExecuteAsync("DELETE FROM faqs WHERE id = @Id", new { Id = id });
}
}
@@ -47,6 +47,7 @@
</MudNavGroup> </MudNavGroup>
<MudNavGroup Title="홈페이지" Icon="@Icons.Material.Filled.Home" Expanded="false"> <MudNavGroup Title="홈페이지" Icon="@Icons.Material.Filled.Home" Expanded="false">
<MudNavLink Href="/taxbaik/admin/announcements" Icon="@Icons.Material.Filled.Campaign">공지사항</MudNavLink> <MudNavLink Href="/taxbaik/admin/announcements" Icon="@Icons.Material.Filled.Campaign">공지사항</MudNavLink>
<MudNavLink Href="/taxbaik/admin/faqs" Icon="@Icons.Material.Filled.QuestionAnswer">FAQ 관리</MudNavLink>
<MudNavLink Href="/taxbaik/admin/blog" Icon="@Icons.Material.Filled.Article">블로그 관리</MudNavLink> <MudNavLink Href="/taxbaik/admin/blog" Icon="@Icons.Material.Filled.Article">블로그 관리</MudNavLink>
</MudNavGroup> </MudNavGroup>
<MudNavLink Href="/taxbaik/admin/inquiries" Icon="@Icons.Material.Filled.Forum">문의 관리</MudNavLink> <MudNavLink Href="/taxbaik/admin/inquiries" Icon="@Icons.Material.Filled.Forum">문의 관리</MudNavLink>
@@ -0,0 +1,133 @@
@page "/admin/faqs/create"
@page "/admin/faqs/{Id:int}/edit"
@attribute [Authorize]
@using TaxBaik.Application.Services
@using TaxBaik.Domain.Entities
@inject FaqService FaqService
@inject NavigationManager Navigation
@inject ISnackbar Snackbar
<PageTitle>@(Id.HasValue ? "FAQ 수정" : "FAQ 등록")</PageTitle>
<section class="admin-page-hero">
<div>
<MudText Typo="Typo.caption" Class="admin-eyebrow">홈페이지</MudText>
<MudText Typo="Typo.h4" Class="admin-page-title">@(Id.HasValue ? "FAQ 수정" : "FAQ 등록")</MudText>
</div>
<MudButton Variant="Variant.Outlined" Href="/taxbaik/admin/faqs"
StartIcon="@Icons.Material.Filled.ArrowBack">목록으로</MudButton>
</section>
<MudPaper Class="admin-surface" Elevation="0" Style="max-width:720px;">
@if (isLoading)
{
<MudProgressLinear Indeterminate="true" />
}
else
{
<MudForm @ref="form" @bind-IsValid="isValid">
<MudGrid Spacing="3">
<MudItem xs="12">
<MudTextField @bind-Value="faq.Question"
Label="질문 *" Required="true"
RequiredError="질문을 입력하세요."
Counter="300" MaxLength="300"
Lines="2" AutoGrow="true"
Placeholder="예: 기장료가 얼마인지 미리 알 수 있나요?" />
</MudItem>
<MudItem xs="12">
<MudTextField @bind-Value="faq.Answer"
Label="답변 *" Required="true"
RequiredError="답변을 입력하세요."
Lines="5" AutoGrow="true"
Placeholder="방문자에게 보여질 답변을 입력하세요." />
</MudItem>
<MudItem xs="12" md="6">
<MudSelect @bind-Value="faq.Category" Label="카테고리" T="string" Clearable="true">
@foreach (var cat in FaqService.Categories)
{
<MudSelectItem Value="@cat">@cat</MudSelectItem>
}
</MudSelect>
</MudItem>
<MudItem xs="12" md="3">
<MudNumericField @bind-Value="faq.SortOrder"
Label="정렬 순서"
HelperText="작을수록 위에 노출"
Min="0" Max="9999" />
</MudItem>
<MudItem xs="12" md="3" Class="d-flex align-center">
<MudSwitch T="bool" @bind-Value="faq.IsActive" Color="Color.Success"
Label="@(faq.IsActive ? "노출 중" : "비활성")" />
</MudItem>
<MudItem xs="12" Class="d-flex gap-2 mt-2">
<MudButton Variant="Variant.Filled" Color="Color.Primary"
StartIcon="@Icons.Material.Filled.Save"
OnClick="@SaveAsync" Disabled="@isSaving">
@(isSaving ? "저장 중..." : "저장")
</MudButton>
<MudButton Variant="Variant.Outlined" Href="/taxbaik/admin/faqs">
취소
</MudButton>
</MudItem>
</MudGrid>
</MudForm>
}
</MudPaper>
@code {
[Parameter] public int? Id { get; set; }
private MudForm form = null!;
private Faq faq = new() { SortOrder = 10, IsActive = true };
private bool isValid;
private bool isLoading = true;
private bool isSaving;
protected override async Task OnInitializedAsync()
{
if (Id.HasValue)
{
var existing = await FaqService.GetByIdAsync(Id.Value);
if (existing is null)
{
Snackbar.Add("FAQ를 찾을 수 없습니다.", Severity.Error);
Navigation.NavigateTo("/taxbaik/admin/faqs");
return;
}
faq = existing;
}
isLoading = false;
}
private async Task SaveAsync()
{
await form.Validate();
if (!isValid) return;
isSaving = true;
try
{
if (Id.HasValue)
{
await FaqService.UpdateAsync(faq);
Snackbar.Add("FAQ가 수정되었습니다.", Severity.Success);
}
else
{
await FaqService.CreateAsync(faq);
Snackbar.Add("FAQ가 등록되었습니다.", Severity.Success);
}
Navigation.NavigateTo("/taxbaik/admin/faqs");
}
catch (Exception ex)
{
Snackbar.Add($"저장 실패: {ex.Message}", Severity.Error);
}
finally
{
isSaving = false;
}
}
}
@@ -0,0 +1,120 @@
@page "/admin/faqs"
@attribute [Authorize]
@using TaxBaik.Application.Services
@using TaxBaik.Domain.Entities
@inject FaqService FaqService
@inject NavigationManager Navigation
@inject IDialogService DialogService
@inject ISnackbar Snackbar
<PageTitle>FAQ 관리</PageTitle>
<section class="admin-page-hero">
<div>
<MudText Typo="Typo.caption" Class="admin-eyebrow">홈페이지</MudText>
<MudText Typo="Typo.h4" Class="admin-page-title">FAQ 관리</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/faqs/create">
FAQ 등록
</MudButton>
</section>
<MudPaper Class="admin-surface" Elevation="0">
@if (faqs is null)
{
<MudProgressLinear Indeterminate="true" />
}
else if (!faqs.Any())
{
<div class="pa-6 text-center">
<MudIcon Icon="@Icons.Material.Filled.QuestionAnswer" Style="font-size:3rem; opacity:.3;" />
<MudText Class="mt-2 text-muted">등록된 FAQ가 없습니다.</MudText>
</div>
}
else
{
<MudSimpleTable Striped="true" Dense="true" Class="admin-table">
<thead>
<tr>
<th style="width:60px;">순서</th>
<th>질문</th>
<th style="width:130px;">카테고리</th>
<th style="width:90px;">상태</th>
<th style="width:160px;"></th>
</tr>
</thead>
<tbody>
@foreach (var item in faqs)
{
<tr>
<td class="text-center">
<MudText Typo="Typo.body2">@item.SortOrder</MudText>
</td>
<td>
<MudText Typo="Typo.body2" Style="max-width:480px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;">
@item.Question
</MudText>
</td>
<td>
@if (!string.IsNullOrEmpty(item.Category))
{
<MudChip Size="Size.Small" Color="Color.Default">@item.Category</MudChip>
}
</td>
<td>
@if (item.IsActive)
{
<MudChip Size="Size.Small" Color="Color.Success">노출 중</MudChip>
}
else
{
<MudChip Size="Size.Small" Color="Color.Default">비활성</MudChip>
}
</td>
<td>
<MudButtonGroup Size="Size.Small" Variant="Variant.Outlined">
<MudButton @onclick="@(() => Navigation.NavigateTo($"/taxbaik/admin/faqs/{item.Id}/edit"))">
수정
</MudButton>
<MudButton Color="Color.Error" @onclick="@(() => DeleteAsync(item))">
삭제
</MudButton>
</MudButtonGroup>
</td>
</tr>
}
</tbody>
</MudSimpleTable>
<MudText Typo="Typo.caption" Class="pa-2 text-muted">
총 @(faqs.Count)개 · 노출 중 @(faqs.Count(f => f.IsActive))개
</MudText>
}
</MudPaper>
@code {
private List<Faq>? faqs;
protected override async Task OnInitializedAsync() => await LoadAsync();
private async Task LoadAsync()
{
faqs = (await FaqService.GetAllAsync()).ToList();
}
private async Task DeleteAsync(Faq item)
{
var confirmed = await DialogService.ShowMessageBox(
"FAQ 삭제",
$"'{item.Question}' 항목을 삭제하시겠습니까?",
yesText: "삭제", cancelText: "취소");
if (confirmed != true) return;
await FaqService.DeleteAsync(item.Id);
Snackbar.Add("FAQ가 삭제되었습니다.", Severity.Success);
await LoadAsync();
}
}
+20 -63
View File
@@ -352,7 +352,9 @@ else
</div> </div>
</section> </section>
<!-- 자주 묻는 질문 --> <!-- 자주 묻는 질문 (DB 연동) -->
@if (Model.ActiveFaqs.Count > 0)
{
<section class="py-5" style="background: #F9F7F3;"> <section class="py-5" style="background: #F9F7F3;">
<div class="container"> <div class="container">
<div class="text-center mb-5"> <div class="text-center mb-5">
@@ -361,70 +363,24 @@ else
</div> </div>
<div class="accordion faq-accordion" id="faqAccordion"> <div class="accordion faq-accordion" id="faqAccordion">
<div class="accordion-item faq-item"> @for (int i = 0; i < Model.ActiveFaqs.Count; i++)
<h3 class="accordion-header"> {
<button class="accordion-button collapsed faq-question" type="button" var faqItem = Model.ActiveFaqs[i];
data-bs-toggle="collapse" data-bs-target="#faq1"> var collapseId = $"faq-{faqItem.Id}";
기장료가 얼마인지 미리 알 수 있나요? <div class="accordion-item faq-item">
</button> <h3 class="accordion-header">
</h3> <button class="accordion-button collapsed faq-question" type="button"
<div id="faq1" class="accordion-collapse collapse" data-bs-parent="#faqAccordion"> data-bs-toggle="collapse" data-bs-target="#@collapseId">
<div class="accordion-body faq-answer"> @faqItem.Question
업종과 매출 규모에 따라 다르지만, 무료 상담 후 정확한 견적을 안내드립니다. </button>
일반적으로 소규모 사업자는 월 10만 원대부터 시작하며, 부가가치세·소득세 신고 시기에는 별도 수수료 없이 포함 처리합니다. </h3>
<strong>먼저 상담해 보시면 구체적인 금액을 바로 말씀드릴 수 있습니다.</strong> <div id="@collapseId" class="accordion-collapse collapse" data-bs-parent="#faqAccordion">
<div class="accordion-body faq-answer">
@faqItem.Answer
</div>
</div> </div>
</div> </div>
</div> }
<div class="accordion-item faq-item">
<h3 class="accordion-header">
<button class="accordion-button collapsed faq-question" type="button"
data-bs-toggle="collapse" data-bs-target="#faq2">
양도세 상담은 어떻게 진행되나요?
</button>
</h3>
<div id="faq2" class="accordion-collapse collapse" data-bs-parent="#faqAccordion">
<div class="accordion-body faq-answer">
등기부등본, 취득·양도 계약서, 보유 기간 확인 서류 등을 카카오 채널 또는 문의폼으로 전달해 주시면
예상 세액과 절세 방법을 검토해 드립니다.
<strong>매도 전에 상담하시면 취득세·비과세 요건 등을 사전에 확인할 수 있어 훨씬 유리합니다.</strong>
</div>
</div>
</div>
<div class="accordion-item faq-item">
<h3 class="accordion-header">
<button class="accordion-button collapsed faq-question" type="button"
data-bs-toggle="collapse" data-bs-target="#faq3">
무료 상담도 가능한가요?
</button>
</h3>
<div id="faq3" class="accordion-collapse collapse" data-bs-parent="#faqAccordion">
<div class="accordion-body faq-answer">
네, 초기 현황 파악과 방향성 검토까지는 무료로 진행합니다.
카카오 채널 또는 문의폼으로 연락 주시면 빠르게 확인해 드립니다.
<strong>실질적인 세무 처리·신고 대행이 시작되는 시점부터 수수료가 발생합니다.</strong>
</div>
</div>
</div>
<div class="accordion-item faq-item">
<h3 class="accordion-header">
<button class="accordion-button collapsed faq-question" type="button"
data-bs-toggle="collapse" data-bs-target="#faq4">
처음 상담 시 어떤 자료를 준비해야 하나요?
</button>
</h3>
<div id="faq4" class="accordion-collapse collapse" data-bs-parent="#faqAccordion">
<div class="accordion-body faq-answer">
상담 목적에 따라 다르지만 일반적으로 아래 자료가 있으면 더 정확한 안내가 가능합니다:
<ul class="mt-2 mb-0">
<li><strong>사업자 세무:</strong> 사업자등록증, 최근 3개월 매출·매입 자료</li>
<li><strong>부동산:</strong> 등기부등본, 취득·매도 계약서, 보유 기간 확인 자료</li>
<li><strong>증여·상속:</strong> 재산 목록, 증여 예정 자산 내역</li>
</ul>
<span class="d-block mt-2"><strong>자료가 없어도 상담은 가능합니다. 먼저 연락 주세요.</strong></span>
</div>
</div>
</div>
</div> </div>
<div class="text-center mt-5"> <div class="text-center mt-5">
@@ -433,6 +389,7 @@ else
</div> </div>
</div> </div>
</section> </section>
}
<!-- 최종 CTA --> <!-- 최종 CTA -->
<section class="py-5" style="background: linear-gradient(135deg, #2E5C4E 0%, #1F3A30 100%); color: white;"> <section class="py-5" style="background: linear-gradient(135deg, #2E5C4E 0%, #1F3A30 100%); color: white;">
+22 -9
View File
@@ -10,35 +10,42 @@ public class IndexModel : PageModel
private readonly BlogService _blogService; private readonly BlogService _blogService;
private readonly SeasonalMarketingService _seasonalMarketingService; private readonly SeasonalMarketingService _seasonalMarketingService;
private readonly AnnouncementService _announcementService; private readonly AnnouncementService _announcementService;
private readonly FaqService _faqService;
public List<BlogPost> RecentPosts { get; set; } = []; public List<BlogPost> RecentPosts { get; set; } = [];
public List<BlogPost> SeasonalPosts { get; set; } = []; public List<BlogPost> SeasonalPosts { get; set; } = [];
public CurrentSeasonDto? CurrentSeason { get; set; } public CurrentSeasonDto? CurrentSeason { get; set; }
public List<Announcement> ActiveAnnouncements { get; set; } = []; public List<Announcement> ActiveAnnouncements { get; set; } = [];
public List<Faq> ActiveFaqs { get; set; } = [];
public IndexModel( public IndexModel(
BlogService blogService, BlogService blogService,
SeasonalMarketingService seasonalMarketingService, SeasonalMarketingService seasonalMarketingService,
AnnouncementService announcementService) AnnouncementService announcementService,
FaqService faqService)
{ {
_blogService = blogService; _blogService = blogService;
_seasonalMarketingService = seasonalMarketingService; _seasonalMarketingService = seasonalMarketingService;
_announcementService = announcementService; _announcementService = announcementService;
_faqService = faqService;
} }
public async Task OnGetAsync() public async Task OnGetAsync()
{ {
CurrentSeason = _seasonalMarketingService.GetCurrentSeason(); CurrentSeason = _seasonalMarketingService.GetCurrentSeason();
try var announcementsTask = LoadSafeAsync(() => _announcementService.GetActiveAsync());
{ var faqsTask = LoadSafeAsync(() => _faqService.GetActiveAsync());
ActiveAnnouncements = (await _announcementService.GetActiveAsync()).ToList(); var blogTask = LoadBlogAsync();
}
catch
{
ActiveAnnouncements = [];
}
await Task.WhenAll(announcementsTask, faqsTask, blogTask);
ActiveAnnouncements = (await announcementsTask)?.ToList() ?? [];
ActiveFaqs = (await faqsTask)?.ToList() ?? [];
}
private async Task LoadBlogAsync()
{
try try
{ {
if (CurrentSeason is not null && !string.IsNullOrEmpty(CurrentSeason.RelatedCategorySlug)) if (CurrentSeason is not null && !string.IsNullOrEmpty(CurrentSeason.RelatedCategorySlug))
@@ -60,4 +67,10 @@ public class IndexModel : PageModel
SeasonalPosts = []; SeasonalPosts = [];
} }
} }
private static async Task<IEnumerable<T>?> LoadSafeAsync<T>(Func<Task<IEnumerable<T>>> loader)
{
try { return await loader(); }
catch { return null; }
}
} }
+36
View File
@@ -0,0 +1,36 @@
-- FAQ 관리 테이블
CREATE TABLE IF NOT EXISTS faqs (
id SERIAL PRIMARY KEY,
question VARCHAR(300) NOT NULL,
answer TEXT NOT NULL,
category VARCHAR(50), -- 기장·세금신고, 부동산, 증여·상속, 기타
sort_order INT NOT NULL DEFAULT 0,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_faqs_active_order ON faqs (is_active, sort_order);
-- 기본 FAQ 시드 데이터 (하드코딩 대체)
INSERT INTO faqs (question, answer, category, sort_order, is_active) VALUES
(
'기장료가 얼마인지 미리 알 수 있나요?',
'업종과 매출 규모에 따라 다르지만, 무료 상담 후 정확한 견적을 안내드립니다. 일반적으로 소규모 사업자는 월 10만 원대부터 시작하며, 부가가치세·소득세 신고 시기에는 별도 수수료 없이 포함 처리합니다. 먼저 상담해 보시면 구체적인 금액을 바로 말씀드릴 수 있습니다.',
'기장·세금신고', 10, TRUE
),
(
'양도세 상담은 어떻게 진행되나요?',
'등기부등본, 취득·양도 계약서, 보유 기간 확인 서류 등을 카카오 채널 또는 문의폼으로 전달해 주시면 예상 세액과 절세 방법을 검토해 드립니다. 매도 전에 상담하시면 취득세·비과세 요건 등을 사전에 확인할 수 있어 훨씬 유리합니다.',
'부동산', 20, TRUE
),
(
'무료 상담도 가능한가요?',
'네, 초기 현황 파악과 방향성 검토까지는 무료로 진행합니다. 카카오 채널 또는 문의폼으로 연락 주시면 빠르게 확인해 드립니다. 실질적인 세무 처리·신고 대행이 시작되는 시점부터 수수료가 발생합니다.',
'기타', 30, TRUE
),
(
'처음 상담 시 어떤 자료를 준비해야 하나요?',
'상담 목적에 따라 다르지만 아래 자료가 있으면 더 정확한 안내가 가능합니다. 사업자 세무: 사업자등록증, 최근 3개월 매출·매입 자료 / 부동산: 등기부등본, 취득·매도 계약서, 보유 기간 확인 자료 / 증여·상속: 재산 목록, 증여 예정 자산 내역. 자료가 없어도 상담은 가능합니다. 먼저 연락 주세요.',
'기타', 40, TRUE
);