Files
taxbaik/TaxBaik.Web/Components/Admin/Pages/Login.razor
T
kjh2064 cc72a67355
TaxBaik CI/CD / build-and-deploy (push) Successful in 1m15s
TaxBaik Browser E2E / browser-e2e (push) Successful in 1m31s
feat: 시즌별 마케팅 + 공지사항 관리 기능 추가
- 연간 세무 캘린더(7개 시즌) 기반 자동 Hero 섹션 전환
- 시즌 감지 시 D-Day 카운트다운, 긴박감 배지, 시즌 CTA 표시
- 서비스 카드 순서 시즌 관련 항목 우선 정렬
- 어드민 공지사항 CRUD (등록·수정·삭제, 기간·유형 설정)
- 홈페이지 상단 공지 배너 자동 노출 (일반/배너/긴급)
- CLAUDE.md에 세무 캘린더 및 마케팅 방향 하네스 추가

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-27 22:45:55 +09:00

129 lines
4.3 KiB
Plaintext

@page "/admin/login"
@using System.ComponentModel.DataAnnotations
@layout TaxBaik.Web.Components.Admin.Layout.BlankLayout
@attribute [AllowAnonymous]
@inject IApiClient ApiClient
@inject NavigationManager NavigationManager
@inject CustomAuthenticationStateProvider AuthStateProvider
@inject IJSRuntime Js
<PageTitle>로그인</PageTitle>
<MudThemeProvider />
<MudContainer MaxWidth="MaxWidth.Small" Class="admin-login-page d-flex align-center justify-center" Style="min-height: 100vh;">
<MudPaper Class="pa-8" Elevation="3" Style="width: 100%; max-width: 400px;">
<MudText Typo="Typo.h4" Class="mb-6 text-center">관리자 로그인</MudText>
<div>
<InputText class="mud-input mud-input-outlined mud-input-root mud-input-root-adorned-start mb-4"
style="width: 100%; min-height: 56px; padding: 16px 14px;"
placeholder="사용자명"
autocomplete="username"
@bind-Value="model.Username" />
<InputText type="password"
class="mud-input mud-input-outlined mud-input-root mud-input-root-adorned-start mb-4"
style="width: 100%; min-height: 56px; padding: 16px 14px;"
placeholder="비밀번호"
autocomplete="current-password"
@bind-Value="model.Password" />
@if (!string.IsNullOrEmpty(errorMessage))
{
<MudAlert Severity="Severity.Error" Class="mb-4">@errorMessage</MudAlert>
}
<button type="button"
class="mud-button-root mud-button mud-button-filled mud-button-filled-primary mud-elevation-0"
style="width: 100%; min-height: 52px; border: 0; border-radius: 4px; color: white;"
@onclick="HandleLogin"
disabled="@isLoading">
@if (isLoading)
{
<MudProgressCircular Size="Size.Small" Indeterminate="true" Class="mr-2" />
<span>로그인 중...</span>
}
else
{
<span>로그인</span>
}
</button>
</div>
</MudPaper>
</MudContainer>
@code {
private bool isLoading = false;
private string errorMessage = "";
private LoginModel model = new();
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
await Js.InvokeVoidAsync("taxbaikAdminSession.syncRouteClass");
}
private async Task HandleLogin()
{
if (isLoading)
return;
isLoading = true;
errorMessage = "";
try
{
var request = new { model.Username, model.Password };
var response = await ApiClient.PostAsync<LoginResponse>("auth/login", request);
if (response?.Token == null)
{
errorMessage = "사용자명 또는 비밀번호가 올바르지 않습니다.";
isLoading = false;
return;
}
await ApiClient.SetAuthToken(response.Token);
await AuthStateProvider.LoginAsync(response.Token);
NavigationManager.NavigateTo(GetReturnUrl(), forceLoad: false);
}
catch
{
errorMessage = "로그인 중 오류가 발생했습니다.";
isLoading = false;
}
}
private class LoginResponse
{
public string Token { get; set; } = "";
public int ExpiresIn { get; set; }
}
private class LoginModel
{
public string Username { get; set; } = "";
public string Password { get; set; } = "";
}
private string GetReturnUrl()
{
var uri = NavigationManager.ToAbsoluteUri(NavigationManager.Uri);
if (!Microsoft.AspNetCore.WebUtilities.QueryHelpers.ParseQuery(uri.Query).TryGetValue("returnUrl", out var returnUrl)
|| string.IsNullOrWhiteSpace(returnUrl))
{
return "/taxbaik/admin/dashboard";
}
var value = returnUrl.ToString();
if (!value.StartsWith("admin", StringComparison.OrdinalIgnoreCase))
{
return "/taxbaik/admin/dashboard";
}
return $"/taxbaik/{value.TrimStart('/')}";
}
}