using Application; using Application.Abstractions.Chat; using Application.Abstractions.Crypto; using Infrastructure; using SharedKernel; using System.Reflection; using System.Threading.RateLimiting; using Microsoft.AspNetCore.RateLimiting; using Microsoft.AspNetCore.ResponseCompression; using Microsoft.Extensions.Hosting.WindowsServices; using Serilog; using Web.Api; using Web.Api.Common; using Web.Api.Extensions; using Web.Api.Hubs; using Web.Api.Services; var builder = WebApplication.CreateBuilder(args); var settings = builder.Configuration.Get()!; if (!WindowsServiceHelpers.IsWindowsService()) { Console.Title = settings.App.Name; } Console.WriteLine($"ENV={builder.Environment.EnvironmentName}"); Console.WriteLine($"현재 시간: {DateTime.Now} / {TimeZoneInfo.Local.Id}"); builder.Host.UseWindowsService(); // Serilog — Production에서만 파일 로그 if (builder.Environment.IsProduction()) { Log.Logger = new LoggerConfiguration() .ReadFrom.Configuration(builder.Configuration) .WriteTo.File("logs/webapi-.log", rollingInterval: RollingInterval.Day, retainedFileCountLimit: 30, outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff} [{Level:u3}] {Message:lj}{NewLine}{Exception}") .CreateLogger(); builder.Host.UseSerilog(); } builder.Services.Configure(builder.Configuration); builder.Services.AddApiForwardedHeaders(settings); // Cloudflare → IIS reverse proxy 헤더 신뢰 builder.Services .AddApplication() .AddPresentation() .AddApiInfrastructure(builder.Configuration); // 모든 DateTime 응답에 UTC(Z) 마커 강제 (SQL datetime2 + EF Kind=Unspecified → JS 로컬시간 오해 방지) builder.Services.ConfigureHttpJsonOptions(opts => { opts.SerializerOptions.Converters.Add(new UtcDateTimeConverter()); opts.SerializerOptions.Converters.Add(new UtcNullableDateTimeConverter()); }); // CORS builder.Services.AddCors(options => { options.AddPolicy(settings.CorsPolicy.Name, policy => { policy .WithOrigins(settings.CorsPolicy.AllowedOrigins.ToArray()) .AllowAnyHeader() .AllowAnyMethod() .AllowCredentials() .SetPreflightMaxAge(TimeSpan.FromSeconds(settings.CorsPolicy.PreflightMaxAgeSeconds)); }); }); // SignalR builder.Services.AddSignalR(); builder.Services.AddSingleton(); // Chat builder.Services.AddSingleton(); builder.Services.AddHostedService(); // Response 압축 (Brotli/Gzip) builder.Services.AddResponseCompression(options => { options.EnableForHttps = true; options.Providers.Add(); options.Providers.Add(); }); // Output cache 정책 (endpoint 별 .CacheOutput("PublicShort") opt-in) builder.Services.AddOutputCache(options => { options.AddPolicy("PublicShort", b => b.Cache().Expire(TimeSpan.FromSeconds(30)).SetVaryByQuery("*")); }); // Rate limiter (endpoint 별 .RequireRateLimiting("api") opt-in) builder.Services.AddRateLimiter(options => { options.AddFixedWindowLimiter("api", w => { w.PermitLimit = 100; w.Window = TimeSpan.FromMinutes(1); w.QueueLimit = 0; }); options.RejectionStatusCode = StatusCodes.Status429TooManyRequests; }); // Endpoints builder.Services.AddEndpoints(Assembly.GetExecutingAssembly()); builder.Logging.AddConsole(); /** * ======================================================================================================================================================= */ var app = builder.Build(); /** * ======================================================================================================================================================= */ if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } // 상태 확인\ app.MapHealthChecks("/health"); // ForwardedHeaders 는 다른 미들웨어보다 먼저 — Exception/Cors/Auth 가 client IP 를 알아야 정확. app.UseForwardedHeaders(); app.UseExceptionHandler(); app.UseResponseCompression(); app.UseCors(settings.CorsPolicy.Name); app.UseStaticFiles(); app.UseAuthentication(); app.UseAuthorization(); app.UseOutputCache(); app.UseRateLimiter(); app.MapEndpoints(); app.MapHub("/hubs/crypto"); app.MapHub("/hubs/chat"); // 서버 시작 시 이전 채팅 접속자 정보 초기화 (stale 연결 제거) using (var scope = app.Services.CreateScope()) { await scope.ServiceProvider.GetRequiredService().ClearAllAsync(); } app.Run();