| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150 |
- 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<AppSettings>()!;
- 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<AppSettings>(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<ICryptoHubService, CryptoHubService>();
- // Chat
- builder.Services.AddSingleton<IChatHubService, ChatHubService>();
- builder.Services.AddHostedService<KickSubscriberService>();
- // Response 압축 (Brotli/Gzip)
- builder.Services.AddResponseCompression(options =>
- {
- options.EnableForHttps = true;
- options.Providers.Add<BrotliCompressionProvider>();
- options.Providers.Add<GzipCompressionProvider>();
- });
- // 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<CryptoHub>("/hubs/crypto");
- app.MapHub<ChatHub>("/hubs/chat");
- // 서버 시작 시 이전 채팅 접속자 정보 초기화 (stale 연결 제거)
- using (var scope = app.Services.CreateScope())
- {
- await scope.ServiceProvider.GetRequiredService<IChatConnectionTracker>().ClearAllAsync();
- }
- app.Run();
|