Program.cs 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150
  1. using Application;
  2. using Application.Abstractions.Chat;
  3. using Application.Abstractions.Crypto;
  4. using Infrastructure;
  5. using SharedKernel;
  6. using System.Reflection;
  7. using System.Threading.RateLimiting;
  8. using Microsoft.AspNetCore.RateLimiting;
  9. using Microsoft.AspNetCore.ResponseCompression;
  10. using Microsoft.Extensions.Hosting.WindowsServices;
  11. using Serilog;
  12. using Web.Api;
  13. using Web.Api.Common;
  14. using Web.Api.Extensions;
  15. using Web.Api.Hubs;
  16. using Web.Api.Services;
  17. var builder = WebApplication.CreateBuilder(args);
  18. var settings = builder.Configuration.Get<AppSettings>()!;
  19. if (!WindowsServiceHelpers.IsWindowsService())
  20. {
  21. Console.Title = settings.App.Name;
  22. }
  23. Console.WriteLine($"ENV={builder.Environment.EnvironmentName}");
  24. Console.WriteLine($"현재 시간: {DateTime.Now} / {TimeZoneInfo.Local.Id}");
  25. builder.Host.UseWindowsService();
  26. // Serilog — Production에서만 파일 로그
  27. if (builder.Environment.IsProduction())
  28. {
  29. Log.Logger = new LoggerConfiguration()
  30. .ReadFrom.Configuration(builder.Configuration)
  31. .WriteTo.File("logs/webapi-.log",
  32. rollingInterval: RollingInterval.Day,
  33. retainedFileCountLimit: 30,
  34. outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff} [{Level:u3}] {Message:lj}{NewLine}{Exception}")
  35. .CreateLogger();
  36. builder.Host.UseSerilog();
  37. }
  38. builder.Services.Configure<AppSettings>(builder.Configuration);
  39. builder.Services.AddApiForwardedHeaders(settings); // Cloudflare → IIS reverse proxy 헤더 신뢰
  40. builder.Services
  41. .AddApplication()
  42. .AddPresentation()
  43. .AddApiInfrastructure(builder.Configuration);
  44. // 모든 DateTime 응답에 UTC(Z) 마커 강제 (SQL datetime2 + EF Kind=Unspecified → JS 로컬시간 오해 방지)
  45. builder.Services.ConfigureHttpJsonOptions(opts =>
  46. {
  47. opts.SerializerOptions.Converters.Add(new UtcDateTimeConverter());
  48. opts.SerializerOptions.Converters.Add(new UtcNullableDateTimeConverter());
  49. });
  50. // CORS
  51. builder.Services.AddCors(options =>
  52. {
  53. options.AddPolicy(settings.CorsPolicy.Name, policy =>
  54. {
  55. policy
  56. .WithOrigins(settings.CorsPolicy.AllowedOrigins.ToArray())
  57. .AllowAnyHeader()
  58. .AllowAnyMethod()
  59. .AllowCredentials()
  60. .SetPreflightMaxAge(TimeSpan.FromSeconds(settings.CorsPolicy.PreflightMaxAgeSeconds));
  61. });
  62. });
  63. // SignalR
  64. builder.Services.AddSignalR();
  65. builder.Services.AddSingleton<ICryptoHubService, CryptoHubService>();
  66. // Chat
  67. builder.Services.AddSingleton<IChatHubService, ChatHubService>();
  68. builder.Services.AddHostedService<KickSubscriberService>();
  69. // Response 압축 (Brotli/Gzip)
  70. builder.Services.AddResponseCompression(options =>
  71. {
  72. options.EnableForHttps = true;
  73. options.Providers.Add<BrotliCompressionProvider>();
  74. options.Providers.Add<GzipCompressionProvider>();
  75. });
  76. // Output cache 정책 (endpoint 별 .CacheOutput("PublicShort") opt-in)
  77. builder.Services.AddOutputCache(options =>
  78. {
  79. options.AddPolicy("PublicShort", b => b.Cache().Expire(TimeSpan.FromSeconds(30)).SetVaryByQuery("*"));
  80. });
  81. // Rate limiter (endpoint 별 .RequireRateLimiting("api") opt-in)
  82. builder.Services.AddRateLimiter(options =>
  83. {
  84. options.AddFixedWindowLimiter("api", w =>
  85. {
  86. w.PermitLimit = 100;
  87. w.Window = TimeSpan.FromMinutes(1);
  88. w.QueueLimit = 0;
  89. });
  90. options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
  91. });
  92. // Endpoints
  93. builder.Services.AddEndpoints(Assembly.GetExecutingAssembly());
  94. builder.Logging.AddConsole();
  95. /**
  96. * =======================================================================================================================================================
  97. */
  98. var app = builder.Build();
  99. /**
  100. * =======================================================================================================================================================
  101. */
  102. if (app.Environment.IsDevelopment())
  103. {
  104. app.UseSwagger();
  105. app.UseSwaggerUI();
  106. }
  107. // 상태 확인\
  108. app.MapHealthChecks("/health");
  109. // ForwardedHeaders 는 다른 미들웨어보다 먼저 — Exception/Cors/Auth 가 client IP 를 알아야 정확.
  110. app.UseForwardedHeaders();
  111. app.UseExceptionHandler();
  112. app.UseResponseCompression();
  113. app.UseCors(settings.CorsPolicy.Name);
  114. app.UseStaticFiles();
  115. app.UseAuthentication();
  116. app.UseAuthorization();
  117. app.UseOutputCache();
  118. app.UseRateLimiter();
  119. app.MapEndpoints();
  120. app.MapHub<CryptoHub>("/hubs/crypto");
  121. app.MapHub<ChatHub>("/hubs/chat");
  122. // 서버 시작 시 이전 채팅 접속자 정보 초기화 (stale 연결 제거)
  123. using (var scope = app.Services.CreateScope())
  124. {
  125. await scope.ServiceProvider.GetRequiredService<IChatConnectionTracker>().ClearAllAsync();
  126. }
  127. app.Run();