c71d858cd2
TaxBaik CI/CD / build-and-deploy (push) Failing after 3m38s
CRITICAL FIX: MapRazorComponents was missing AddAdditionalAssemblies call, which is required for Blazor WebAssembly to discover and render all admin components (Routes, Pages, Shared, Layout). Without this, Root component (App.razor) alone cannot resolve child components, causing ComponentDisposedException and page initialization failure. As documented in CLAUDE.md Phase 8: '⚠️ 중요: AddAdditionalAssemblies 필수 이유: - Root 컴포넌트(App.razor)만으로는 모든 WASM 컴포넌트를 탐색할 수 없음 - Routes.razor, 모든 Page 컴포넌트, Shared 컴포넌트는 명시적 등록 필수 - 제거하면 컴포넌트 탐색 실패 → ObjectDisposedException → 초기화 실패 - 절대 제거하지 말 것' CHANGE: app.MapRazorComponents<TaxBaik.Web.Components.Admin.App>() .AddInteractiveWebAssemblyRenderMode() + .AddAdditionalAssemblies(typeof(TaxBaik.Web.Components.Admin._Imports).Assembly) .AllowAnonymous(); Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
458 lines
19 KiB
C#
458 lines
19 KiB
C#
using System.IO.Compression;
|
|
using System.Text;
|
|
using System.Text.Encodings.Web;
|
|
using System.Text.Unicode;
|
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
|
using Microsoft.AspNetCore.Authentication.Cookies;
|
|
using Microsoft.AspNetCore.Authentication.OAuth;
|
|
using Microsoft.AspNetCore.Components.Authorization;
|
|
using Microsoft.AspNetCore.HttpOverrides;
|
|
using Microsoft.AspNetCore.RateLimiting;
|
|
using Microsoft.AspNetCore.ResponseCompression;
|
|
using Microsoft.IdentityModel.Tokens;
|
|
using MudBlazor.Services;
|
|
using Serilog;
|
|
using FastEndpoints;
|
|
using System.Threading.RateLimiting;
|
|
using TaxBaik.Application;
|
|
using TaxBaik.Application.Services;
|
|
using TaxBaik.Application.Seasonal;
|
|
using TaxBaik.Application.Utils;
|
|
using TaxBaik.Infrastructure;
|
|
using TaxBaik.Web.Services;
|
|
using TaxBaik.Web.Components.Admin.Services;
|
|
using TaxBaik.Web.Components.Admin.Services.AdminClients;
|
|
using TaxBaik.Web.Components.Admin.Shared;
|
|
|
|
var builder = WebApplication.CreateBuilder(args);
|
|
var isProduction = builder.Environment.IsProduction();
|
|
|
|
// HTTP 요청 헤더/쿠키 크기 제한 증가 (400 Bad Request 해결)
|
|
builder.WebHost.ConfigureKestrel(options =>
|
|
{
|
|
options.Limits.MaxRequestBodySize = 100 * 1024 * 1024; // 100MB
|
|
});
|
|
|
|
// Serilog 설정
|
|
builder.Host.UseSerilog((context, config) =>
|
|
{
|
|
config
|
|
.MinimumLevel.Information()
|
|
.WriteTo.Console()
|
|
.WriteTo.File(
|
|
path: "logs/taxbaik-.log",
|
|
rollingInterval: RollingInterval.Day,
|
|
outputTemplate: "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz}] [{Level:u3}] {Message:lj}{NewLine}{Exception}")
|
|
.Enrich.FromLogContext()
|
|
.Enrich.WithProperty("Environment", context.HostingEnvironment.EnvironmentName);
|
|
|
|
var botToken = context.Configuration["Telegram:BotToken"];
|
|
var systemChatId = context.Configuration["Telegram:SystemChatId"] ?? context.Configuration["Telegram:ChatId"];
|
|
if (!string.IsNullOrEmpty(botToken) && !string.IsNullOrEmpty(systemChatId))
|
|
{
|
|
config.WriteTo.Sink(new TaxBaik.Web.Logging.TelegramSink(botToken, systemChatId), Serilog.Events.LogEventLevel.Error);
|
|
}
|
|
});
|
|
|
|
// Controllers + FastEndpoints (API-First)
|
|
builder.Services.AddControllers();
|
|
builder.Services.AddFastEndpoints(config =>
|
|
{
|
|
config.Assemblies = new[] { typeof(Program).Assembly };
|
|
});
|
|
builder.Services.AddProblemDetails();
|
|
builder.Services.AddHealthChecks();
|
|
builder.Services.AddRateLimiter(options =>
|
|
{
|
|
options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
|
|
options.AddPolicy("client-logs", httpContext =>
|
|
{
|
|
var ip = httpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown";
|
|
return RateLimitPartition.GetFixedWindowLimiter(
|
|
partitionKey: $"client-logs:{ip}",
|
|
factory: _ => new FixedWindowRateLimiterOptions
|
|
{
|
|
PermitLimit = 10,
|
|
Window = TimeSpan.FromMinutes(1),
|
|
QueueLimit = 0,
|
|
AutoReplenishment = true
|
|
});
|
|
});
|
|
});
|
|
|
|
// Razor Pages + Blazor WebAssembly 통합
|
|
builder.Services.AddRazorPages();
|
|
builder.Services.AddRazorComponents()
|
|
.AddInteractiveWebAssemblyComponents();
|
|
builder.Services.Configure<Microsoft.AspNetCore.Components.Server.CircuitOptions>(options =>
|
|
{
|
|
options.DetailedErrors = true;
|
|
});
|
|
|
|
// Session & TempData (쿠키 저장소)
|
|
builder.Services.AddSession(options =>
|
|
{
|
|
options.IdleTimeout = TimeSpan.FromMinutes(20);
|
|
options.Cookie.HttpOnly = true;
|
|
options.Cookie.IsEssential = true;
|
|
options.Cookie.Name = "TaxBaik.SessionId";
|
|
});
|
|
builder.Services.AddDistributedMemoryCache();
|
|
// TempData는 기본적으로 쿠키 저장소 사용 (여기서 명시적 설정)
|
|
|
|
// JWT 인증
|
|
var connectionString = builder.Configuration.GetConnectionString("Default")
|
|
?? throw new InvalidOperationException("Missing connection string");
|
|
var jwtKey = builder.Configuration["Jwt:SecretKey"] ?? throw new InvalidOperationException("Missing JWT SecretKey");
|
|
if (isProduction && jwtKey.Contains("dev-secret", StringComparison.OrdinalIgnoreCase))
|
|
throw new InvalidOperationException("Production JWT SecretKey must not use the development default.");
|
|
var key = Encoding.ASCII.GetBytes(jwtKey);
|
|
|
|
var authenticationBuilder = builder.Services.AddAuthentication(opts =>
|
|
{
|
|
opts.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
|
|
opts.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
|
|
})
|
|
.AddJwtBearer(opts =>
|
|
{
|
|
opts.TokenValidationParameters = new TokenValidationParameters
|
|
{
|
|
ValidateIssuerSigningKey = true,
|
|
IssuerSigningKey = new SymmetricSecurityKey(key),
|
|
ValidateIssuer = true,
|
|
ValidIssuer = "taxbaik-admin",
|
|
ValidateAudience = true,
|
|
ValidAudience = "taxbaik-admin-client",
|
|
ValidateLifetime = true,
|
|
ClockSkew = TimeSpan.FromMinutes(1)
|
|
};
|
|
})
|
|
.AddCookie(PortalAuthDefaults.Scheme, opts =>
|
|
{
|
|
opts.Cookie.Name = PortalAuthDefaults.CookieName;
|
|
opts.Cookie.HttpOnly = true;
|
|
opts.Cookie.SameSite = SameSiteMode.Lax;
|
|
opts.Cookie.SecurePolicy = isProduction ? CookieSecurePolicy.Always : CookieSecurePolicy.SameAsRequest;
|
|
opts.LoginPath = "/taxbaik/portal/login";
|
|
opts.AccessDeniedPath = "/taxbaik/portal/login";
|
|
opts.SlidingExpiration = true;
|
|
opts.ExpireTimeSpan = TimeSpan.FromDays(7);
|
|
})
|
|
.AddCookie(PortalOAuthDefaults.ExternalScheme, opts =>
|
|
{
|
|
opts.Cookie.Name = "TaxBaik.Portal.External";
|
|
opts.Cookie.HttpOnly = true;
|
|
opts.Cookie.SameSite = SameSiteMode.Lax;
|
|
opts.Cookie.SecurePolicy = isProduction ? CookieSecurePolicy.Always : CookieSecurePolicy.SameAsRequest;
|
|
});
|
|
|
|
var googleClientId = builder.Configuration["Authentication:Google:ClientId"];
|
|
var googleClientSecret = builder.Configuration["Authentication:Google:ClientSecret"];
|
|
if (!string.IsNullOrWhiteSpace(googleClientId) && !string.IsNullOrWhiteSpace(googleClientSecret))
|
|
{
|
|
authenticationBuilder.AddGoogle(PortalOAuthDefaults.GoogleScheme, opts =>
|
|
{
|
|
opts.SignInScheme = PortalOAuthDefaults.ExternalScheme;
|
|
opts.ClientId = googleClientId;
|
|
opts.ClientSecret = googleClientSecret;
|
|
opts.CallbackPath = "/taxbaik/portal/signin-google";
|
|
});
|
|
}
|
|
|
|
var naverClientId = builder.Configuration["Authentication:Naver:ClientId"];
|
|
var naverClientSecret = builder.Configuration["Authentication:Naver:ClientSecret"];
|
|
if (!string.IsNullOrWhiteSpace(naverClientId) && !string.IsNullOrWhiteSpace(naverClientSecret))
|
|
{
|
|
authenticationBuilder.AddOAuth(PortalOAuthDefaults.NaverScheme, opts =>
|
|
{
|
|
opts.SignInScheme = PortalOAuthDefaults.ExternalScheme;
|
|
opts.ClientId = naverClientId;
|
|
opts.ClientSecret = naverClientSecret;
|
|
opts.CallbackPath = "/taxbaik/portal/signin-naver";
|
|
opts.AuthorizationEndpoint = "https://nid.naver.com/oauth2.0/authorize";
|
|
opts.TokenEndpoint = "https://nid.naver.com/oauth2.0/token";
|
|
opts.UserInformationEndpoint = "https://openapi.naver.com/v1/nid/me";
|
|
opts.SaveTokens = true;
|
|
opts.Events = new OAuthEvents
|
|
{
|
|
OnCreatingTicket = async context =>
|
|
{
|
|
var request = new HttpRequestMessage(HttpMethod.Get, opts.UserInformationEndpoint);
|
|
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", context.AccessToken);
|
|
var response = await context.Backchannel.SendAsync(request, context.HttpContext.RequestAborted);
|
|
response.EnsureSuccessStatusCode();
|
|
using var payload = System.Text.Json.JsonDocument.Parse(await response.Content.ReadAsStringAsync(context.HttpContext.RequestAborted));
|
|
var responseRoot = payload.RootElement.GetProperty("response");
|
|
context.Identity?.AddClaim(new System.Security.Claims.Claim(System.Security.Claims.ClaimTypes.NameIdentifier, responseRoot.GetProperty("id").GetString() ?? ""));
|
|
context.Identity?.AddClaim(new System.Security.Claims.Claim(System.Security.Claims.ClaimTypes.Name, responseRoot.GetProperty("name").GetString() ?? ""));
|
|
context.Identity?.AddClaim(new System.Security.Claims.Claim(System.Security.Claims.ClaimTypes.Email, responseRoot.GetProperty("email").GetString() ?? ""));
|
|
}
|
|
};
|
|
});
|
|
}
|
|
|
|
var kakaoClientId = builder.Configuration["Authentication:Kakao:ClientId"];
|
|
var kakaoClientSecret = builder.Configuration["Authentication:Kakao:ClientSecret"];
|
|
if (!string.IsNullOrWhiteSpace(kakaoClientId) && !string.IsNullOrWhiteSpace(kakaoClientSecret))
|
|
{
|
|
authenticationBuilder.AddOAuth(PortalOAuthDefaults.KakaoScheme, opts =>
|
|
{
|
|
opts.SignInScheme = PortalOAuthDefaults.ExternalScheme;
|
|
opts.ClientId = kakaoClientId;
|
|
opts.ClientSecret = kakaoClientSecret;
|
|
opts.CallbackPath = "/taxbaik/portal/signin-kakao";
|
|
opts.AuthorizationEndpoint = "https://kauth.kakao.com/oauth/authorize";
|
|
opts.TokenEndpoint = "https://kauth.kakao.com/oauth/token";
|
|
opts.UserInformationEndpoint = "https://kapi.kakao.com/v2/user/me";
|
|
opts.SaveTokens = true;
|
|
opts.Events = new OAuthEvents
|
|
{
|
|
OnCreatingTicket = async context =>
|
|
{
|
|
var request = new HttpRequestMessage(HttpMethod.Get, opts.UserInformationEndpoint);
|
|
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", context.AccessToken);
|
|
var response = await context.Backchannel.SendAsync(request, context.HttpContext.RequestAborted);
|
|
response.EnsureSuccessStatusCode();
|
|
using var payload = System.Text.Json.JsonDocument.Parse(await response.Content.ReadAsStringAsync(context.HttpContext.RequestAborted));
|
|
var kakaoAccount = payload.RootElement.GetProperty("kakao_account");
|
|
var profile = kakaoAccount.GetProperty("profile");
|
|
context.Identity?.AddClaim(new System.Security.Claims.Claim(System.Security.Claims.ClaimTypes.NameIdentifier, payload.RootElement.GetProperty("id").GetInt64().ToString()));
|
|
context.Identity?.AddClaim(new System.Security.Claims.Claim(System.Security.Claims.ClaimTypes.Name, profile.GetProperty("nickname").GetString() ?? ""));
|
|
if (kakaoAccount.TryGetProperty("email", out var emailProp))
|
|
context.Identity?.AddClaim(new System.Security.Claims.Claim(System.Security.Claims.ClaimTypes.Email, emailProp.GetString() ?? ""));
|
|
}
|
|
};
|
|
});
|
|
}
|
|
|
|
// Blazor 인증
|
|
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.AddAuthorization();
|
|
builder.Services.AddAuthorizationCore();
|
|
|
|
// Telegram Notification
|
|
builder.Services.AddHttpClient<ITelegramNotificationService, TelegramNotificationService>();
|
|
|
|
// HTTP Client for API (with automatic token refresh)
|
|
builder.Services.AddScoped<ITokenStore, TokenStore>();
|
|
builder.Services.AddScoped<TokenRefreshHandler>();
|
|
builder.Services.AddHttpClient<IApiClient, ApiClient>();
|
|
|
|
var apiBaseUrl = builder.Configuration["ApiClient:BaseUrl"]
|
|
?? throw new InvalidOperationException("Missing configuration: ApiClient:BaseUrl");
|
|
|
|
builder.Services.AddHttpClient<IAdminDashboardClient, AdminDashboardClient>(client =>
|
|
{
|
|
client.BaseAddress = new Uri(apiBaseUrl);
|
|
}).AddHttpMessageHandler<TokenRefreshHandler>();
|
|
builder.Services.AddHttpClient<IInquiryBrowserClient, InquiryBrowserClient>(client =>
|
|
{
|
|
client.BaseAddress = new Uri(apiBaseUrl);
|
|
}).AddHttpMessageHandler<TokenRefreshHandler>();
|
|
builder.Services.AddHttpClient<IClientBrowserClient, ClientBrowserClient>(client =>
|
|
{
|
|
client.BaseAddress = new Uri(apiBaseUrl);
|
|
}).AddHttpMessageHandler<TokenRefreshHandler>();
|
|
builder.Services.AddHttpClient<ITaxFilingBrowserClient, TaxFilingBrowserClient>(client =>
|
|
{
|
|
client.BaseAddress = new Uri(apiBaseUrl);
|
|
}).AddHttpMessageHandler<TokenRefreshHandler>();
|
|
builder.Services.AddHttpClient<IFaqBrowserClient, FaqBrowserClient>(client =>
|
|
{
|
|
client.BaseAddress = new Uri(apiBaseUrl);
|
|
}).AddHttpMessageHandler<TokenRefreshHandler>();
|
|
builder.Services.AddHttpClient<IAnnouncementBrowserClient, AnnouncementBrowserClient>(client =>
|
|
{
|
|
client.BaseAddress = new Uri(apiBaseUrl);
|
|
}).AddHttpMessageHandler<TokenRefreshHandler>();
|
|
|
|
// Phase 5: Tax Accounting & CRM Browser Clients
|
|
builder.Services.AddHttpClient<ITaxProfileBrowserClient, TaxProfileBrowserClient>(client =>
|
|
{
|
|
client.BaseAddress = new Uri(apiBaseUrl);
|
|
}).AddHttpMessageHandler<TokenRefreshHandler>();
|
|
|
|
builder.Services.AddHttpClient<ITaxFilingScheduleBrowserClient, TaxFilingScheduleBrowserClient>(client =>
|
|
{
|
|
client.BaseAddress = new Uri(apiBaseUrl);
|
|
}).AddHttpMessageHandler<TokenRefreshHandler>();
|
|
|
|
builder.Services.AddHttpClient<IConsultingActivityBrowserClient, ConsultingActivityBrowserClient>(client =>
|
|
{
|
|
client.BaseAddress = new Uri(apiBaseUrl);
|
|
}).AddHttpMessageHandler<TokenRefreshHandler>();
|
|
|
|
builder.Services.AddHttpClient<IContractBrowserClient, ContractBrowserClient>(client =>
|
|
{
|
|
client.BaseAddress = new Uri(apiBaseUrl);
|
|
}).AddHttpMessageHandler<TokenRefreshHandler>();
|
|
|
|
builder.Services.AddHttpClient<IRevenueTrackingBrowserClient, RevenueTrackingBrowserClient>(client =>
|
|
{
|
|
client.BaseAddress = new Uri(apiBaseUrl);
|
|
}).AddHttpMessageHandler<TokenRefreshHandler>();
|
|
|
|
builder.Services.AddHttpClient<ICommonCodeBrowserClient, CommonCodeBrowserClient>(client =>
|
|
{
|
|
client.BaseAddress = new Uri(apiBaseUrl);
|
|
}).AddHttpMessageHandler<TokenRefreshHandler>();
|
|
|
|
// UI & 캐시 (MudBlazor Theme Customization)
|
|
builder.Services.AddMudServices(config =>
|
|
{
|
|
config.SnackbarConfiguration.HideTransitionDuration = 400;
|
|
config.SnackbarConfiguration.ShowTransitionDuration = 300;
|
|
config.PopoverOptions.ThrowOnDuplicateProvider = false;
|
|
});
|
|
builder.Services.AddMemoryCache();
|
|
builder.Services.AddResponseCompression(opts => {
|
|
opts.Providers.Add<GzipCompressionProvider>();
|
|
});
|
|
builder.Services.AddHostedService<TelegramReportBackgroundService>();
|
|
builder.Services.AddHttpContextAccessor();
|
|
builder.Services.AddScoped<PortalAuthService>();
|
|
|
|
builder.Services.Configure<PortalAuthOptions>(builder.Configuration.GetSection("Authentication"));
|
|
|
|
// 한글 포함 다국어 문자를 유니코드 엔티티로 변환하지 않도록 설정
|
|
builder.Services.AddSingleton(HtmlEncoder.Create(UnicodeRanges.All));
|
|
|
|
builder.Services.AddInfrastructure();
|
|
builder.Services.AddApplication();
|
|
builder.Services.AddScoped<IInquiryNotificationService, TelegramInquiryNotificationService>();
|
|
|
|
// Register version info
|
|
var versionInfo = new VersionInfo();
|
|
var versionJsonPath = Path.Combine(AppContext.BaseDirectory, "wwwroot", "version.json");
|
|
if (File.Exists(versionJsonPath))
|
|
{
|
|
try
|
|
{
|
|
var json = System.Text.Json.JsonDocument.Parse(File.ReadAllText(versionJsonPath));
|
|
var root = json.RootElement;
|
|
if (root.TryGetProperty("version", out var versionProp))
|
|
versionInfo.Version = versionProp.GetString() ?? "unknown";
|
|
if (root.TryGetProperty("built", out var builtProp))
|
|
versionInfo.Built = builtProp.GetString() ?? "unknown";
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine($"Warning: Failed to parse version.json: {ex.Message}");
|
|
}
|
|
}
|
|
builder.Services.AddSingleton(versionInfo);
|
|
|
|
var app = builder.Build();
|
|
|
|
app.UseForwardedHeaders(new ForwardedHeadersOptions
|
|
{
|
|
ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto
|
|
});
|
|
|
|
app.Use(async (context, next) =>
|
|
{
|
|
var path = context.Request.Path.Value ?? string.Empty;
|
|
if (path.Equals("/favicon.ico", StringComparison.OrdinalIgnoreCase) ||
|
|
path.Equals("/taxbaik/favicon.ico", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
context.Response.ContentType = "image/svg+xml";
|
|
await context.Response.SendFileAsync(Path.Combine(app.Environment.WebRootPath ?? "wwwroot", "favicon.svg"));
|
|
return;
|
|
}
|
|
|
|
await next();
|
|
});
|
|
|
|
// Run migrations on startup (non-blocking for development)
|
|
try
|
|
{
|
|
using (var scope = app.Services.CreateScope())
|
|
{
|
|
var connectionFactory = scope.ServiceProvider.GetRequiredService<TaxBaik.Domain.Interfaces.IDbConnectionFactory>();
|
|
var migrationRunner = new TaxBaik.Infrastructure.Data.MigrationRunner(connectionString, connectionFactory);
|
|
await migrationRunner.RunAsync();
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
if (!app.Environment.IsDevelopment())
|
|
throw;
|
|
|
|
Console.WriteLine($"Migration warning (development only): {ex.Message}");
|
|
}
|
|
|
|
app.UsePathBase("/taxbaik");
|
|
app.UseResponseCompression();
|
|
app.UseStaticFiles();
|
|
app.UseSession(); // TempData 쿠키 저장소
|
|
app.UseRouting();
|
|
app.UseRateLimiter();
|
|
app.UseAuthentication();
|
|
app.UseAuthorization();
|
|
app.UseAntiforgery();
|
|
|
|
if (!app.Environment.IsDevelopment())
|
|
{
|
|
app.UseExceptionHandler("/Error");
|
|
app.UseHsts();
|
|
}
|
|
|
|
// API + Razor Pages + Blazor 매핑
|
|
app.MapControllers();
|
|
app.MapFastEndpoints();
|
|
app.MapHealthChecks("/healthz");
|
|
app.MapRazorPages();
|
|
app.MapStaticAssets();
|
|
|
|
// AllowAnonymous: JWT 미들웨어가 Blazor 셸 요청을 401로 차단하지 않도록 한다.
|
|
// 인증은 Blazor AuthorizeRouteView → RedirectToLogin 에서 처리한다.
|
|
// Phase 8: WebAssembly 렌더 모드 완전 마이그레이션
|
|
// - App.razor: TaxBaik.Web (메인 웹 서버)
|
|
// - Routes + Pages + Shared + Layout + Forms: TaxBaik.Web (메인 웹 서버)
|
|
// 모든 Blazor 컴포넌트가 웹 서버에서 통합 서비스됨
|
|
// API는 웹 서버에서만 제공 (클라이언트 프로젝트 분리 불필요)
|
|
app.MapRazorComponents<TaxBaik.Web.Components.Admin.App>()
|
|
.AddInteractiveWebAssemblyRenderMode()
|
|
.AddAdditionalAssemblies(typeof(TaxBaik.Web.Components.Admin._Imports).Assembly)
|
|
.AllowAnonymous();
|
|
|
|
// 애플리케이션 시작/종료 로깅
|
|
try
|
|
{
|
|
Log.Information("애플리케이션 시작: {Environment}", app.Environment.EnvironmentName);
|
|
app.Run();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Log.Fatal(ex, "애플리케이션 강종");
|
|
if (!app.Environment.IsDevelopment())
|
|
{
|
|
try
|
|
{
|
|
var fatalMessage = $"환경: {app.Environment.EnvironmentName}\n오류: {ex.Message}";
|
|
if (TaxBaik.Web.Services.TelegramAlertGate.ShouldSend("telegram:fatal", fatalMessage, TimeSpan.FromMinutes(30)))
|
|
{
|
|
using var scope = app.Services.CreateScope();
|
|
var telegramService = scope.ServiceProvider.GetRequiredService<ITelegramNotificationService>();
|
|
await telegramService.SendErrorAsync(
|
|
"❌ 서버 오류",
|
|
fatalMessage);
|
|
}
|
|
}
|
|
catch (Exception telegramEx)
|
|
{
|
|
Log.Error(telegramEx, "오류 알림 전송 실패");
|
|
}
|
|
}
|
|
throw;
|
|
}
|
|
finally
|
|
{
|
|
Log.Information("애플리케이션 종료");
|
|
Log.CloseAndFlush();
|
|
}
|