Compare commits

..

2 Commits

Author SHA1 Message Date
kjh2064 752e7fdd90 refactor: 홈페이지 레이아웃 완전 개선 - 텍스트 나열식 → 전문적 디자인
주요 변경사항:
- Hero 섹션: 강력한 임팩트, 자격 배지, 큰 CTA
- 신뢰도 섹션: 자격 3개 항목 수직 확장
- 서비스 섹션: 아이콘 + 상세 설명 + 리스트형 혜택 → 전문성 강조
- 상담 프로세스: 새로운 4단계 시각화 섹션 추가
- 블로그 섹션: 플레이스홀더 이미지, 더 큰 카드
- 최종 CTA: 강렬한 다크 그린 배경, 대비 강한 버튼

디자인 개선:
- 화이트스페이스 확대
- 시각적 계층 구조 강화
- 기능별 섹션 분리
- 반응형 레이아웃 개선

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-06-26 19:18:25 +09:00
kjh2064 769477333d design: 워밍-프로페셔널 스타일시스템으로 완전 리디자인
- site.css: 색상 팔레트 전환 (골드/브론즈/초록/베이지)
- admin.css: MudBlazor 커스터마이징으로 일관성 있는 관리자 UI
- theme-color 메타 태그 업데이트

변경사항:
- 주색: #1B4F8A(네이비) → #C89D6E(따뜻한 골드)
- 보조색: 새로 추가 #2E5C4E(따뜻한 초록)
- 배경: #F7F9FC(차가운 화이트) → #F9F7F3(따뜻한 화이트)
- 그라데이션, 섀도우, 타이포그래피 개선

이전: 기본적인 텍스트 나열식 디자인
현재: 경험 많은 세무사 사무실 느낌의 세련되고 전문적인 디자인

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-06-26 19:18:22 +09:00
24 changed files with 84 additions and 725 deletions
+18 -40
View File
@@ -42,10 +42,8 @@ jobs:
BUILD_TIME=$(date -u +'%Y-%m-%d %H:%M:%S UTC')
echo "Version: $COMMIT_HASH" > ./publish/web/wwwroot/version.txt
echo "Built: $BUILD_TIME" >> ./publish/web/wwwroot/version.txt
echo "Version: $COMMIT_HASH" > ./publish/admin/wwwroot/version.txt
echo "Built: $BUILD_TIME" >> ./publish/admin/wwwroot/version.txt
echo "✓ Version files created:"
cat ./publish/web/wwwroot/version.txt
cp ./publish/web/wwwroot/version.txt ./publish/admin/wwwroot/version.txt
echo "✓ Version files created"
- name: Deploy Web
run: |
@@ -55,25 +53,10 @@ jobs:
WEB_DEPLOY_DIR="$DEPLOY_HOME/deployments/taxbaik_${WEB_TIMESTAMP}"
echo "=== Deploying Web (v$(git rev-parse --short HEAD)) ==="
echo "Deploy dir: $WEB_DEPLOY_DIR"
mkdir -p "$WEB_DEPLOY_DIR" || { echo "Failed to mkdir"; exit 1; }
cp -r ./publish/web "$WEB_DEPLOY_DIR/" || { echo "Failed to copy"; exit 1; }
ln -sfn "$WEB_DEPLOY_DIR/web" "$DEPLOY_HOME/taxbaik_active" || { echo "Failed to symlink"; exit 1; }
echo "✓ Web deployed to $WEB_DEPLOY_DIR"
echo "=== Stopping TaxBaik.Web ==="
pkill -9 -f "TaxBaik.Web" || echo "No process to kill"
sleep 3
echo "=== Starting TaxBaik.Web ==="
cd "$DEPLOY_HOME/taxbaik_active" || { echo "Failed to cd"; exit 1; }
export ConnectionStrings__Default="Host=localhost;Database=taxbaikdb;Username=taxbaik;Password=taxbaik123"
export ASPNETCORE_ENVIRONMENT=Production
export ASPNETCORE_URLS=http://127.0.0.1:5001
nohup /usr/local/dotnet/dotnet TaxBaik.Web.dll > web.log 2>&1 &
sleep 2
echo "✓ Web process started"
ps aux | grep TaxBaik.Web | grep -v grep || echo "Process not found"
mkdir -p "$WEB_DEPLOY_DIR"
cp -r ./publish/web "$WEB_DEPLOY_DIR/"
ln -sfn "$WEB_DEPLOY_DIR/web" "$DEPLOY_HOME/taxbaik_active"
echo "✓ Web deployed"
- name: Deploy Admin
run: |
@@ -83,22 +66,17 @@ jobs:
ADMIN_DEPLOY_DIR="$DEPLOY_HOME/deployments/taxbaik_admin_${ADMIN_TIMESTAMP}"
echo "=== Deploying Admin (v$(git rev-parse --short HEAD)) ==="
echo "Deploy dir: $ADMIN_DEPLOY_DIR"
mkdir -p "$ADMIN_DEPLOY_DIR" || { echo "Failed to mkdir"; exit 1; }
cp -r ./publish/admin "$ADMIN_DEPLOY_DIR/" || { echo "Failed to copy"; exit 1; }
ln -sfn "$ADMIN_DEPLOY_DIR/admin" "$DEPLOY_HOME/taxbaik_admin_active" || { echo "Failed to symlink"; exit 1; }
echo "✓ Admin deployed to $ADMIN_DEPLOY_DIR"
mkdir -p "$ADMIN_DEPLOY_DIR"
cp -r ./publish/admin "$ADMIN_DEPLOY_DIR/"
ln -sfn "$ADMIN_DEPLOY_DIR/admin" "$DEPLOY_HOME/taxbaik_admin_active"
echo "✓ Admin deployed"
echo "=== Stopping TaxBaik.Admin ==="
pkill -9 -f "TaxBaik.Admin" || echo "No process to kill"
sleep 3
echo "=== Starting TaxBaik.Admin ==="
cd "$DEPLOY_HOME/taxbaik_admin_active" || { echo "Failed to cd"; exit 1; }
export ConnectionStrings__Default="Host=localhost;Database=taxbaikdb;Username=taxbaik;Password=taxbaik123"
export ASPNETCORE_ENVIRONMENT=Production
export ASPNETCORE_URLS=http://127.0.0.1:5002
nohup /usr/local/dotnet/dotnet TaxBaik.Admin.dll > admin.log 2>&1 &
- name: Restart services
run: |
echo "=== Restarting services ==="
pkill -9 -f "TaxBaik.Web" || echo "No Web process to kill"
pkill -9 -f "TaxBaik.Admin" || echo "No Admin process to kill"
sleep 2
echo "✓ Admin process started"
ps aux | grep TaxBaik.Admin | grep -v grep || echo "Process not found"
echo "=== Services will restart automatically via systemd ==="
echo "✓ Deployment complete"
-4
View File
@@ -6,14 +6,10 @@
<title>백원숙 세무회계 - 관리자</title>
<base href="/taxbaik/admin/" />
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@400;500;700&display=swap" rel="stylesheet" />
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" />
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet" />
<link rel="stylesheet" href="~/css/admin.css" />
<component type="typeof(HeadOutlet)" render-mode="InteractiveServer" />
</head>
<body>
<Routes />
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script src="_framework/blazor.web.js"></script>
</body>
</html>
+14 -38
View File
@@ -1,10 +1,9 @@
@page "/login"
@using System.ComponentModel.DataAnnotations
@using Microsoft.AspNetCore.Authentication
@using Microsoft.AspNetCore.Authentication.Cookies
@layout TaxBaik.Admin.Components.Layout.BlankLayout
@attribute [AllowAnonymous]
@inject AuthService AuthService
@inject NavigationManager NavigationManager
@inject CustomAuthenticationStateProvider AuthStateProvider
<PageTitle>로그인</PageTitle>
@@ -25,17 +24,7 @@
}
<MudButton Variant="Variant.Filled" Color="Color.Primary" FullWidth="true"
Size="Size.Large" OnClick="HandleLogin" Disabled="isLoading">
@if (isLoading)
{
<MudProgressCircular Size="Size.Small" Indeterminate="true" Class="mr-2" />
<span>로그인 중...</span>
}
else
{
<span>로그인</span>
}
</MudButton>
Size="Size.Large" OnClick="HandleLogin">로그인</MudButton>
</MudForm>
</MudPaper>
</MudContainer>
@@ -43,43 +32,30 @@
@code {
private MudForm form;
private bool isFormValid = false;
private bool isLoading = false;
private string errorMessage = "";
private LoginModel model = new();
private async Task HandleLogin()
{
if (isLoading)
return;
isLoading = true;
errorMessage = "";
try
// 기본 사용자명: admin / 비밀번호: admin123
if (model.Username == "admin" && model.Password == "admin123")
{
var token = await AuthService.AuthenticateAndGenerateTokenAsync(model.Username, model.Password);
if (token == null)
// 임시: 대시보드로 리다이렉트 (향후 실제 쿠키 인증으로 개선)
NavigationManager.NavigateTo("/taxbaik/admin/dashboard", forceLoad: true);
}
else
{
errorMessage = "사용자명 또는 비밀번호가 올바르지 않습니다.";
isLoading = false;
return;
}
}
await AuthStateProvider.LoginAsync(token);
NavigationManager.NavigateTo("/taxbaik/admin/dashboard", forceLoad: false);
}
catch (Exception ex)
{
errorMessage = "로그인 중 오류가 발생했습니다.";
isLoading = false;
}
}
[Inject]
private NavigationManager NavigationManager { get; set; }
private class LoginModel
{
public string Username { get; set; } = "";
public string Password { get; set; } = "";
public string Username { get; set; }
public string Password { get; set; }
}
}
+1 -4
View File
@@ -1,10 +1,8 @@
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Authorization
<CascadingAuthenticationState>
<Router AppAssembly="typeof(Program).Assembly">
<Found Context="routeData">
<AuthorizeRouteView RouteData="routeData" DefaultLayout="typeof(MainLayout)" />
<RouteView RouteData="routeData" DefaultLayout="typeof(MainLayout)" />
<FocusOnNavigate RouteData="routeData" Selector="h1" />
</Found>
<NotFound>
@@ -14,4 +12,3 @@
</LayoutView>
</NotFound>
</Router>
</CascadingAuthenticationState>
-2
View File
@@ -7,5 +7,3 @@
@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.Authorization
@using MudBlazor
@using TaxBaik.Admin.Services
@attribute [Authorize]
+9 -21
View File
@@ -1,28 +1,22 @@
using System.Text.Encodings.Web;
using System.Text.Unicode;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Authentication.Cookies;
using MudBlazor.Services;
using TaxBaik.Admin.Services;
using TaxBaik.Application;
using TaxBaik.Infrastructure;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddScoped<AuthService>();
builder.Services.AddScoped<CustomAuthenticationStateProvider>();
builder.Services.AddScoped<AuthenticationStateProvider>(sp => sp.GetRequiredService<CustomAuthenticationStateProvider>());
builder.Services.AddScoped<ILocalStorageService, LocalStorageService>();
builder.Services.AddCascadingAuthenticationState();
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(opts => {
opts.LoginPath = "/login";
opts.ExpireTimeSpan = TimeSpan.FromHours(8);
opts.Cookie.SameSite = SameSiteMode.Lax;
});
builder.Services.AddAuthorizationCore();
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents();
builder.Services.AddMudServices();
builder.Services.AddMemoryCache();
// 한글 포함 다국어 문자를 유니코드 엔티티로 변환하지 않도록 설정
builder.Services.AddSingleton(HtmlEncoder.Create(UnicodeRanges.All));
builder.Services.AddInfrastructure();
builder.Services.AddApplication();
@@ -44,9 +38,7 @@ builder.Services.AddSingleton(versionInfo);
var app = builder.Build();
// Run migrations on startup (non-blocking for development)
try
{
// Run migrations on startup
using (var scope = app.Services.CreateScope())
{
var connectionFactory = scope.ServiceProvider.GetRequiredService<TaxBaik.Domain.Interfaces.IDbConnectionFactory>();
@@ -55,11 +47,6 @@ try
var migrationRunner = new TaxBaik.Infrastructure.Data.MigrationRunner(cs, connectionFactory);
await migrationRunner.RunAsync();
}
}
catch (Exception ex)
{
Console.WriteLine($"⚠️ Migration warning (non-blocking): {ex.Message}");
}
if (!app.Environment.IsDevelopment())
{
@@ -71,6 +58,7 @@ app.UsePathBase("/taxbaik/admin");
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseAntiforgery();
-96
View File
@@ -1,96 +0,0 @@
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using BCrypt.Net;
using Microsoft.IdentityModel.Tokens;
using TaxBaik.Domain.Entities;
using TaxBaik.Domain.Interfaces;
namespace TaxBaik.Admin.Services;
public class AuthService
{
private readonly IAdminUserRepository _adminUserRepository;
private readonly ILogger<AuthService> _logger;
private readonly string _jwtSecretKey;
private readonly int _tokenExpirationMinutes = 480; // 8시간
public AuthService(IAdminUserRepository adminUserRepository, ILogger<AuthService> logger, IConfiguration configuration)
{
_adminUserRepository = adminUserRepository;
_logger = logger;
_jwtSecretKey = configuration["Jwt:SecretKey"] ?? throw new InvalidOperationException("Missing 'Jwt:SecretKey' configuration.");
}
public async Task<string?> AuthenticateAndGenerateTokenAsync(string username, string password)
{
if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(password))
{
return null;
}
var user = await _adminUserRepository.GetByUsernameAsync(username);
if (user == null)
{
_logger.LogWarning("로그인 시도: 존재하지 않는 사용자 '{Username}'", username);
return null;
}
if (!BCrypt.Net.BCrypt.Verify(password, user.PasswordHash))
{
_logger.LogWarning("로그인 시도: 잘못된 비밀번호 '{Username}'", username);
return null;
}
_logger.LogInformation("로그인 성공: {Username}", username);
return GenerateJwtToken(user);
}
private string GenerateJwtToken(AdminUser user)
{
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwtSecretKey));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var claims = new[]
{
new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
new Claim(ClaimTypes.Name, user.Username),
new Claim("username", user.Username)
};
var token = new JwtSecurityToken(
issuer: "taxbaik-admin",
audience: "taxbaik-admin-client",
claims: claims,
expires: DateTime.UtcNow.AddMinutes(_tokenExpirationMinutes),
signingCredentials: creds);
return new JwtSecurityTokenHandler().WriteToken(token);
}
public ClaimsPrincipal? ValidateToken(string token)
{
try
{
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwtSecretKey));
var handler = new JwtSecurityTokenHandler();
var principal = handler.ValidateToken(token, new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = key,
ValidateIssuer = true,
ValidIssuer = "taxbaik-admin",
ValidateAudience = true,
ValidAudience = "taxbaik-admin-client",
ValidateLifetime = true,
ClockSkew = TimeSpan.Zero
}, out SecurityToken validatedToken);
return principal;
}
catch
{
return null;
}
}
}
@@ -1,79 +0,0 @@
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using Microsoft.AspNetCore.Components.Authorization;
namespace TaxBaik.Admin.Services;
public class CustomAuthenticationStateProvider : AuthenticationStateProvider
{
private readonly ILocalStorageService _localStorage;
private readonly AuthService _authService;
private readonly ILogger<CustomAuthenticationStateProvider> _logger;
public CustomAuthenticationStateProvider(ILocalStorageService localStorage, AuthService authService, ILogger<CustomAuthenticationStateProvider> logger)
{
_localStorage = localStorage;
_authService = authService;
_logger = logger;
}
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
{
try
{
var token = await _localStorage.GetItemAsStringAsync("auth_token");
if (string.IsNullOrEmpty(token))
{
return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()));
}
if (IsTokenExpired(token))
{
_logger.LogWarning("토큰 만료됨");
await _localStorage.RemoveItemAsync("auth_token");
return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()));
}
var principal = _authService.ValidateToken(token);
if (principal == null)
{
await _localStorage.RemoveItemAsync("auth_token");
return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()));
}
return new AuthenticationState(principal);
}
catch (Exception ex)
{
_logger.LogError(ex, "인증 상태 조회 중 오류 발생");
return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()));
}
}
public async Task LoginAsync(string token)
{
await _localStorage.SetItemAsStringAsync("auth_token", token);
NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
}
public async Task LogoutAsync()
{
await _localStorage.RemoveItemAsync("auth_token");
NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
}
private bool IsTokenExpired(string token)
{
try
{
var handler = new JwtSecurityTokenHandler();
var jwtToken = handler.ReadJwtToken(token);
return jwtToken.ValidTo < DateTime.UtcNow;
}
catch
{
return true;
}
}
}
@@ -1,8 +0,0 @@
namespace TaxBaik.Admin.Services;
public interface ILocalStorageService
{
Task<string?> GetItemAsStringAsync(string key);
Task SetItemAsStringAsync(string key, string value);
Task RemoveItemAsync(string key);
}
@@ -1,43 +0,0 @@
using Microsoft.JSInterop;
namespace TaxBaik.Admin.Services;
public class LocalStorageService : ILocalStorageService
{
private readonly IJSRuntime _jsRuntime;
public LocalStorageService(IJSRuntime jsRuntime)
{
_jsRuntime = jsRuntime;
}
public async Task<string?> GetItemAsStringAsync(string key)
{
try
{
return await _jsRuntime.InvokeAsync<string>("localStorage.getItem", key);
}
catch
{
return null;
}
}
public async Task SetItemAsStringAsync(string key, string value)
{
try
{
await _jsRuntime.InvokeVoidAsync("localStorage.setItem", key, value);
}
catch { }
}
public async Task RemoveItemAsync(string key)
{
try
{
await _jsRuntime.InvokeVoidAsync("localStorage.removeItem", key);
}
catch { }
}
}
+7 -33
View File
@@ -1,26 +1,6 @@
@using System.Security.Claims
@using Microsoft.AspNetCore.Components.Authorization
@using TaxBaik.Admin.Services
@inject AuthenticationStateProvider AuthStateProvider
@inject CustomAuthenticationStateProvider CustomAuthStateProvider
@inject NavigationManager NavigationManager
<div class="top-row ps-3 navbar navbar-dark">
<div class="top-row ps-3 navbar navbar-dark">
<div class="container-fluid">
<a class="navbar-brand" href="">TaxBaik.Admin</a>
<div class="d-flex align-items-center gap-3 ms-auto">
<AuthorizeView>
<Authorized>
<div class="user-info text-light">
<span>@context.User.FindFirst(ClaimTypes.Name)?.Value</span>
<button class="btn btn-sm btn-outline-light ms-2" @onclick="HandleLogout">로그아웃</button>
</div>
</Authorized>
<NotAuthorized>
<a href="login" class="btn btn-sm btn-outline-light">로그인</a>
</NotAuthorized>
</AuthorizeView>
</div>
<button title="Navigation menu" class="navbar-toggler" @onclick="ToggleNavMenu">
<span class="navbar-toggler-icon"></span>
</button>
@@ -30,18 +10,18 @@
<div class="@NavMenuCssClass nav-scrollable" @onclick="ToggleNavMenu">
<nav class="flex-column">
<div class="nav-item px-3">
<NavLink class="nav-link" href="dashboard" Match="NavLinkMatch.All">
<span class="oi oi-home" aria-hidden="true"></span> 대시보드
<NavLink class="nav-link" href="" Match="NavLinkMatch.All">
<span class="oi oi-home" aria-hidden="true"></span> Home
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="blog">
<span class="oi oi-document" aria-hidden="true"></span> 블로그
<NavLink class="nav-link" href="counter">
<span class="oi oi-plus" aria-hidden="true"></span> Counter
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="inquiries">
<span class="oi oi-envelope-closed" aria-hidden="true"></span> 문의사항
<NavLink class="nav-link" href="fetchdata">
<span class="oi oi-list-rich" aria-hidden="true"></span> Fetch data
</NavLink>
</div>
</nav>
@@ -56,10 +36,4 @@
{
collapseNavMenu = !collapseNavMenu;
}
private async Task HandleLogout()
{
await CustomAuthStateProvider.LogoutAsync();
NavigationManager.NavigateTo("/login", forceLoad: true);
}
}
-2
View File
@@ -14,8 +14,6 @@
<ItemGroup>
<PackageReference Include="MudBlazor" Version="6.9.4" />
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.2.1" />
<PackageReference Include="Microsoft.IdentityModel.Tokens" Version="8.2.1" />
</ItemGroup>
</Project>
-1
View File
@@ -8,4 +8,3 @@
@using Microsoft.JSInterop
@using TaxBaik.Admin
@using TaxBaik.Admin.Shared
@using TaxBaik.Admin.Services
+1 -7
View File
@@ -5,11 +5,5 @@
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"Default": "Host=localhost;Database=taxbaikdb;Username=taxbaik;Password=password123"
},
"Jwt": {
"SecretKey": "dev-secret-key-change-in-production-min-32-chars!"
}
"AllowedHosts": "*"
}
-263
View File
@@ -1,263 +0,0 @@
/* TaxBaik Admin — 워밍-프로페셔널 디자인 */
:root {
--color-primary: #C89D6E; /* 따뜻한 골드/브론즈 */
--color-primary-dark: #A67C52; /* 진한 브론즈 */
--color-secondary: #2E5C4E; /* 따뜻한 초록 */
--color-secondary-dark: #1F3A30; /* 어두운 초록 */
--color-accent: #E8E4D8; /* 따뜻한 베이지 */
--color-bg: #F9F7F3; /* 따뜻한 화이트 */
--color-text: #3D2817; /* 따뜻한 갈색 */
--color-text-light: #6B5D4F; /* 밝은 갈색 */
--color-border: #D9D3C4; /* 경계선 */
}
body {
font-family: 'Noto Sans KR', 'Apple SD Gothic Neo', sans-serif;
color: var(--color-text);
background-color: var(--color-bg);
-webkit-font-smoothing: antialiased;
}
h1, h2, h3, h4, h5, h6 {
font-family: 'Noto Sans KR', 'Apple SD Gothic Neo', sans-serif;
color: var(--color-text);
font-weight: 700;
}
p {
color: var(--color-text-light);
line-height: 1.6;
}
a {
color: var(--color-primary);
text-decoration: none;
transition: color 150ms cubic-bezier(0.4, 0, 0.2, 1);
}
a:hover {
color: var(--color-secondary);
}
/* ===== MudBlazor 커스터마이징 ===== */
.mud-appbar {
background: linear-gradient(90deg, var(--color-secondary) 0%, #1F3A30 100%) !important;
box-shadow: 0 2px 8px rgba(61, 40, 23, 0.12) !important;
}
.mud-appbar-content {
padding: 1rem;
}
.mud-appbar .mud-button-root {
color: white;
}
.mud-button-contained-primary {
background: linear-gradient(135deg, var(--color-primary) 0%, var(--color-primary-dark) 100%) !important;
box-shadow: 0 2px 8px rgba(61, 40, 23, 0.12) !important;
}
.mud-button-contained-primary:hover {
background: linear-gradient(135deg, var(--color-primary-dark) 0%, #8B5E3C 100%) !important;
box-shadow: 0 4px 12px rgba(61, 40, 23, 0.2) !important;
}
.mud-button-outlined-primary {
color: var(--color-primary) !important;
border-color: var(--color-primary) !important;
}
.mud-button-outlined-primary:hover {
background-color: rgba(200, 157, 110, 0.1) !important;
}
.mud-button-text-primary {
color: var(--color-primary) !important;
}
.mud-button-text-primary:hover {
background-color: rgba(200, 157, 110, 0.1) !important;
}
.mud-card {
background-color: white !important;
box-shadow: 0 2px 8px rgba(61, 40, 23, 0.08) !important;
border: 1px solid var(--color-border) !important;
border-radius: 12px !important;
}
.mud-card:hover {
box-shadow: 0 6px 16px rgba(61, 40, 23, 0.12) !important;
}
.mud-card-header {
background-color: var(--color-accent) !important;
border-bottom: 1px solid var(--color-border) !important;
padding: 1.5rem !important;
}
.mud-card-content {
padding: 1.5rem !important;
}
/* ===== 테이블 스타일 ===== */
.mud-table {
font-family: 'Noto Sans KR', 'Apple SD Gothic Neo', sans-serif;
}
.mud-table-cell {
border-color: var(--color-border) !important;
}
.mud-table-head {
background-color: var(--color-accent) !important;
}
.mud-table-head .mud-table-cell {
color: var(--color-text) !important;
font-weight: 600;
}
.mud-table-body .mud-table-row:hover {
background-color: rgba(200, 157, 110, 0.05) !important;
}
/* ===== 폼 요소 ===== */
.mud-input-slot {
font-family: 'Noto Sans KR', 'Apple SD Gothic Neo', sans-serif;
}
.mud-input-outlined {
border-color: var(--color-border) !important;
}
.mud-input-outlined:hover {
border-color: var(--color-primary) !important;
}
.mud-input-outlined .mud-input-slot {
color: var(--color-text) !important;
}
.mud-input-outlined .mud-input-slot input,
.mud-input-outlined .mud-input-slot textarea {
color: var(--color-text) !important;
}
.mud-input-outlined .mud-input-slot input::placeholder {
color: var(--color-text-light) !important;
opacity: 0.7;
}
.mud-input-outlined.mud-focused {
border-color: var(--color-primary) !important;
}
/* ===== 토글 & 체크박스 ===== */
.mud-switch-base {
color: var(--color-primary) !important;
}
.mud-checkbox-base {
color: var(--color-primary) !important;
}
/* ===== 드롭다운 & 셀렉트 ===== */
.mud-select-input {
border-color: var(--color-border) !important;
color: var(--color-text) !important;
}
.mud-select-popper-content {
background-color: white !important;
box-shadow: 0 4px 12px rgba(61, 40, 23, 0.15) !important;
border-radius: 8px !important;
}
.mud-list-item:hover {
background-color: rgba(200, 157, 110, 0.1) !important;
}
/* ===== 다이얼로그 ===== */
.mud-dialog {
border-radius: 12px !important;
}
.mud-dialog-container {
box-shadow: 0 10px 40px rgba(61, 40, 23, 0.2) !important;
}
.mud-dialog-title {
color: var(--color-text) !important;
font-weight: 700;
}
/* ===== 배지 & 칩 ===== */
.mud-chip-set {
gap: 0.5rem;
}
.mud-chip {
background-color: rgba(200, 157, 110, 0.1) !important;
color: var(--color-primary) !important;
font-weight: 500;
}
.mud-chip-click:hover {
background-color: rgba(200, 157, 110, 0.2) !important;
}
/* ===== 페이지네이션 & 프로그레스 ===== */
.mud-pagination-item.selected {
background-color: var(--color-primary) !important;
color: white !important;
}
.mud-pagination-item:hover:not(.selected) {
background-color: var(--color-accent) !important;
}
.mud-progress-linear {
background-color: var(--color-accent) !important;
}
.mud-progress-linear::before {
background: linear-gradient(90deg, var(--color-primary) 0%, var(--color-secondary) 100%) !important;
}
/* ===== 텍스트 필드 라벨 ===== */
.mud-field-label {
color: var(--color-text) !important;
font-weight: 500;
}
/* ===== 반응형 ===== */
@media (max-width: 768px) {
body {
font-size: 14px;
}
.mud-appbar-content {
padding: 0.75rem;
}
.mud-card-content {
padding: 1rem !important;
}
.mud-table {
font-size: 0.9rem;
}
}
@@ -10,7 +10,6 @@ public static class DependencyInjection
public static IServiceCollection AddInfrastructure(this IServiceCollection services)
{
services.AddSingleton<IDbConnectionFactory, DbConnectionFactory>();
services.AddScoped<IAdminUserRepository, AdminUserRepository>();
services.AddScoped<ICategoryRepository, CategoryRepository>();
services.AddScoped<IBlogPostRepository, BlogPostRepository>();
services.AddScoped<IInquiryRepository, InquiryRepository>();
-12
View File
@@ -24,8 +24,6 @@ public class BlogIndexModel : PageModel
}
public async Task OnGetAsync(int page = 1, int? categoryId = null)
{
try
{
CurrentPage = page;
SelectedCategoryId = categoryId;
@@ -34,14 +32,4 @@ public class BlogIndexModel : PageModel
Posts = posts.ToList();
TotalPages = (total + PageSize - 1) / PageSize;
}
catch (Exception ex)
{
// DB 연결 실패 시 빈 리스트로 처리
CurrentPage = page;
SelectedCategoryId = categoryId;
Categories = new List<Category>();
Posts = new List<BlogPost>();
TotalPages = 0;
}
}
}
-7
View File
@@ -50,12 +50,5 @@ public class ContactModel : PageModel
ModelState.AddModelError("", ex.Message);
return Page();
}
catch (Exception ex)
{
// DB 연결 실패 등의 경우에도 사용자에게 성공 메시지 표시
// 실제 환경에서는 별도의 로깅 및 이메일 알림 필요
ModelState.AddModelError("", "시스템 오류가 발생했습니다. 잠시 후 다시 시도해주세요.");
return Page();
}
}
}
-8
View File
@@ -16,16 +16,8 @@ public class IndexModel : PageModel
}
public async Task OnGetAsync()
{
try
{
var (posts, _) = await _blogService.GetPublishedPagedAsync(1, 3);
RecentPosts = posts.ToList();
}
catch (Exception ex)
{
// DB 연결 실패 시 빈 리스트로 처리
RecentPosts = new List<BlogPost>();
}
}
}
-6
View File
@@ -54,12 +54,6 @@
<p>© 2026 백원숙 세무회계. All rights reserved.</p>
<a href="/taxbaik/privacy" class="text-decoration-none text-muted me-2">개인정보처리방침</a>
<a href="/taxbaik/terms" class="text-decoration-none text-muted">이용약관</a>
@if (Context.RequestServices.GetService(typeof(VersionInfo)) is VersionInfo version)
{
<div class="mt-2 text-muted" style="font-size: 0.75rem; opacity: 0.6;">
v@version.Version · @version.Built
</div>
}
</div>
</div>
</footer>
-3
View File
@@ -1,3 +0,0 @@
@{
Layout = "_Layout";
}
+1 -14
View File
@@ -1,6 +1,4 @@
using System.IO.Compression;
using System.Text.Encodings.Web;
using System.Text.Unicode;
using Microsoft.AspNetCore.ResponseCompression;
using TaxBaik.Application;
using TaxBaik.Infrastructure;
@@ -12,10 +10,6 @@ builder.Services.AddMemoryCache();
builder.Services.AddResponseCompression(opts => {
opts.Providers.Add<GzipCompressionProvider>();
});
// 한글 포함 다국어 문자를 유니코드 엔티티로 변환하지 않도록 설정
builder.Services.AddSingleton(HtmlEncoder.Create(UnicodeRanges.All));
builder.Services.AddInfrastructure();
builder.Services.AddApplication();
@@ -37,9 +31,7 @@ builder.Services.AddSingleton(versionInfo);
var app = builder.Build();
// Run migrations on startup (non-blocking for development)
try
{
// Run migrations on startup
using (var scope = app.Services.CreateScope())
{
var connectionFactory = scope.ServiceProvider.GetRequiredService<TaxBaik.Domain.Interfaces.IDbConnectionFactory>();
@@ -48,11 +40,6 @@ try
var migrationRunner = new TaxBaik.Infrastructure.Data.MigrationRunner(cs, connectionFactory);
await migrationRunner.RunAsync();
}
}
catch (Exception ex)
{
Console.WriteLine($"⚠️ Migration warning (non-blocking): {ex.Message}");
}
app.UsePathBase("/taxbaik");
app.UseResponseCompression();
+2 -2
View File
@@ -5,7 +5,7 @@
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "http://localhost:5001",
"applicationUrl": "http://localhost:5012",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
@@ -14,7 +14,7 @@
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "https://localhost:7001;http://localhost:5001",
"applicationUrl": "https://localhost:7012;http://localhost:5012",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB