using System.Threading.RateLimiting; using Microsoft.AspNetCore.RateLimiting; using Serilog; using SPMS.API.Extensions; using SPMS.Application; using SPMS.Domain.Common; using SPMS.Infrastructure; var builder = WebApplication.CreateBuilder(new WebApplicationOptions { WebRootPath = Environment.GetEnvironmentVariable("ASPNETCORE_WEBROOT") ?? "wwwroot" }); // ===== 1. Serilog ===== builder.Host.UseSerilog((context, config) => config.ReadFrom.Configuration(context.Configuration)); // ===== 2. Services (DI) ===== builder.Services.AddApplication(); builder.Services.AddInfrastructure(builder.Configuration); // ===== 3. Presentation ===== builder.Services.AddControllers() .ConfigureApiBehaviorOptions(options => { options.InvalidModelStateResponseFactory = context => { var fieldErrors = context.ModelState .Where(e => e.Value?.Errors.Count > 0) .SelectMany(e => e.Value!.Errors.Select(err => new FieldError { Field = e.Key, Message = err.ErrorMessage })) .ToList(); var response = ApiResponseExtensions.ValidationFail( ErrorCodes.BadRequest, "입력값 검증 실패", fieldErrors); return new Microsoft.AspNetCore.Mvc.BadRequestObjectResult(response); }; }); builder.Services.AddSwaggerDocumentation(); builder.Services.AddJwtAuthentication(builder.Configuration); builder.Services.AddAuthorizationPolicies(); // ===== 4. Rate Limiting ===== builder.Services.AddRateLimiter(options => { options.RejectionStatusCode = StatusCodes.Status429TooManyRequests; options.OnRejected = async (context, cancellationToken) => { context.HttpContext.Response.ContentType = "application/json"; var response = ApiResponse.Fail( ErrorCodes.LimitExceeded, "요청 한도를 초과했습니다. 잠시 후 다시 시도해주세요."); await context.HttpContext.Response.WriteAsJsonAsync(response, cancellationToken); }; options.AddFixedWindowLimiter("auth_sensitive", opt => { opt.PermitLimit = 20; opt.Window = TimeSpan.FromMinutes(15); }); options.GlobalLimiter = PartitionedRateLimiter.Create(httpContext => RateLimitPartition.GetFixedWindowLimiter( partitionKey: httpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown", factory: _ => new FixedWindowRateLimiterOptions { PermitLimit = 100, Window = TimeSpan.FromMinutes(1) })); }); var app = builder.Build(); // ===== 5. DB 마이그레이션 자동 적용 ===== using (var scope = app.Services.CreateScope()) { var db = scope.ServiceProvider.GetRequiredService(); await db.Database.MigrateAsync(); } // ===== 6. Middleware Pipeline ===== app.UseMiddlewarePipeline(); app.Run();