using System.IO.Compression; using System.Text; using System.Text.Encodings.Web; using System.Text.Unicode; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Components.Authorization; using Microsoft.AspNetCore.HttpOverrides; using Microsoft.AspNetCore.ResponseCompression; using Microsoft.IdentityModel.Tokens; using MudBlazor.Services; using Serilog; using TaxBaik.Application; using TaxBaik.Application.Services; using TaxBaik.Infrastructure; using TaxBaik.Web.Services; 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); }); // Controllers (API) builder.Services.AddControllers(); builder.Services.AddProblemDetails(); builder.Services.AddHealthChecks(); // SignalR (Notifications only, no state management) builder.Services.AddSignalR(); // Razor Pages + Blazor Server 통합 builder.Services.AddRazorPages(); builder.Services.AddRazorComponents().AddInteractiveServerComponents(); builder.Services.Configure(options => { options.DetailedErrors = true; }); // 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); 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) }; }); // Blazor 인증 builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(sp => sp.GetRequiredService()); builder.Services.AddScoped(); builder.Services.AddCascadingAuthenticationState(); builder.Services.AddAuthorization(); builder.Services.AddAuthorizationCore(); // Notifications (SignalR) builder.Services.AddScoped(); // Telegram Notification builder.Services.AddHttpClient(); // HTTP Client for API (with automatic token refresh) builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddHttpClient(); var apiBaseUrl = builder.Configuration["ApiClient:BaseUrl"] ?? throw new InvalidOperationException("Missing configuration: ApiClient:BaseUrl"); builder.Services.AddHttpClient(client => { client.BaseAddress = new Uri(apiBaseUrl); }) .AddHttpMessageHandler(); builder.Services.AddHttpClient(client => { client.BaseAddress = new Uri(apiBaseUrl); }) .AddHttpMessageHandler(); builder.Services.AddHttpClient(client => { client.BaseAddress = new Uri(apiBaseUrl); }) .AddHttpMessageHandler(); builder.Services.AddHttpClient(client => { client.BaseAddress = new Uri(apiBaseUrl); }) .AddHttpMessageHandler(); builder.Services.AddHttpClient(client => { client.BaseAddress = new Uri(apiBaseUrl); }) .AddHttpMessageHandler(); builder.Services.AddHttpClient(client => { client.BaseAddress = new Uri(apiBaseUrl); }) .AddHttpMessageHandler(); // UI & 캐시 (MudBlazor Theme Customization) builder.Services.AddMudServices(config => { config.SnackbarConfiguration.HideTransitionDuration = 400; config.SnackbarConfiguration.ShowTransitionDuration = 300; }); builder.Services.AddMemoryCache(); builder.Services.AddResponseCompression(opts => { opts.Providers.Add(); }); builder.Services.AddScoped(); // 한글 포함 다국어 문자를 유니코드 엔티티로 변환하지 않도록 설정 builder.Services.AddSingleton(HtmlEncoder.Create(UnicodeRanges.All)); builder.Services.AddInfrastructure(); builder.Services.AddApplication(); // 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 }); // Run migrations on startup (non-blocking for development) try { using (var scope = app.Services.CreateScope()) { var connectionFactory = scope.ServiceProvider.GetRequiredService(); 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.UseRouting(); app.UseAuthentication(); app.UseAuthorization(); app.UseAntiforgery(); if (!app.Environment.IsDevelopment()) { app.UseExceptionHandler("/Error"); app.UseHsts(); } // API + Razor Pages + Blazor 매핑 app.MapControllers(); app.MapHealthChecks("/healthz"); app.MapRazorPages(); // SignalR Hub app.MapHub("/taxbaik/notifications"); // AllowAnonymous: JWT 미들웨어가 Blazor 셸 요청을 401로 차단하지 않도록 한다. // 인증은 Blazor AuthorizeRouteView → RedirectToLogin 에서 처리한다. app.MapRazorComponents() .AddInteractiveServerRenderMode() .AllowAnonymous(); // 애플리케이션 시작/종료 로깅 try { Log.Information("애플리케이션 시작: {Environment}", app.Environment.EnvironmentName); if (!app.Environment.IsDevelopment()) { // 배포 완료 알림을 백그라운드에서 비동기 전송 (앱 시작 블록 방지) _ = Task.Run(async () => { try { using (var scope = app.Services.CreateScope()) { var telegramService = scope.ServiceProvider.GetRequiredService(); await telegramService.SendInfoAsync( "✅ 배포 완료", $"환경: {app.Environment.EnvironmentName}\n상태: 정상 운영 중"); } } catch (Exception ex) { Log.Error(ex, "배포 완료 알림 전송 실패"); } }); } app.Run(); } catch (Exception ex) { Log.Fatal(ex, "애플리케이션 강종"); if (!app.Environment.IsDevelopment()) { try { using (var scope = app.Services.CreateScope()) { var telegramService = scope.ServiceProvider.GetRequiredService(); await telegramService.SendErrorAsync( "❌ 서버 오류", $"환경: {app.Environment.EnvironmentName}\n오류: {ex.Message}"); } } catch (Exception telegramEx) { Log.Error(telegramEx, "오류 알림 전송 실패"); } } throw; } finally { Log.Information("애플리케이션 종료"); Log.CloseAndFlush(); }