using Bitget_Server.Application.BackgroundServices; using Bitget_Server.Application.Options; using Bitget_Server.Application.Services; using Bitget_Server.Application.State; using Bitget_Server.Infrastructure.Bitget; using Bitget_Server.Infrastructure.Utils; using Microsoft.OpenApi.Models; using Serilog; var builder = WebApplication.CreateBuilder(args); // ========================================================================= // 1. Kestrel: HTTP only / port 5000 // ========================================================================= builder.WebHost.ConfigureKestrel(options => { options.ListenAnyIP(5000); // HTTP 전용, HTTPS 사용하지 않음 }); // ========================================================================= // 2. Options 바인딩 // ========================================================================= builder.Services.Configure(builder.Configuration.GetSection(BitgetOptions.SectionName)); var bitgetOptions = builder.Configuration.GetSection(BitgetOptions.SectionName).Get() ?? new BitgetOptions(); // ========================================================================= // 3. 영속화 경로 설정 (BaseSettings.StorageDirectory) // 컨테이너에서는 /data, 로컬에서는 ./data 로 마운트됨 // ========================================================================= var dataDir = Path.GetFullPath(bitgetOptions.DataDirectory); var logDir = Path.GetFullPath(bitgetOptions.LogDirectory); Directory.CreateDirectory(dataDir); Directory.CreateDirectory(logDir); BaseSettings.StorageDirectory = dataDir; // ========================================================================= // 4. Serilog 구성 (Console + 일자별 File Sink) // ========================================================================= Log.Logger = new LoggerConfiguration() .MinimumLevel.Is(bitgetOptions.LogDebugWrite ? Serilog.Events.LogEventLevel.Debug : Serilog.Events.LogEventLevel.Information) .MinimumLevel.Override("Microsoft.AspNetCore", Serilog.Events.LogEventLevel.Warning) .Enrich.FromLogContext() .WriteTo.Console() .WriteTo.Map( keyPropertyName: "SourceContext", defaultKey: "general", configure: (key, wt) => wt.File( path: Path.Combine(logDir, "bitget-.log"), rollingInterval: RollingInterval.Day, retainedFileCountLimit: 30, outputTemplate: "[{Timestamp:HH:mm:ss.fff} {Level:u3}] {SourceContext} {Message:lj}{NewLine}{Exception}")) .CreateLogger(); builder.Host.UseSerilog(); // ========================================================================= // 5. DI 등록 - 상태 / 설정 싱글톤 // ========================================================================= builder.Services.AddSingleton(); builder.Services.AddSingleton(sp => GlobalOptions.Instance); // BaseSettings 기반 파일 영속화 싱글톤 builder.Services.AddSingleton(); // 기본 API 키 자동 선택 (GlobalOptions.SelectAPI 가 비어 있으면 appsettings.json 의 DefaultApiId 로 초기화) // 반드시 다른 HostedService 보다 먼저 등록되어야 함 (HostedServices 는 등록 순서대로 StartAsync). builder.Services.AddHostedService(); // ========================================================================= // 6. DI 등록 - Bitget Infrastructure // ========================================================================= builder.Services.AddSingleton(sp => new BitgetRateLimiter( bitgetOptions.RateLimitPerSecond, sp.GetRequiredService>())); // Bitget 단일 엔드포인트만 호출하므로 HttpClient 를 Singleton 으로 공유해도 안전. // (BackgroundService 가 Singleton 이므로 BitgetClient 도 Singleton 이어야 캡처 문제가 없음) builder.Services.AddSingleton(sp => { var http = new HttpClient { Timeout = TimeSpan.FromSeconds(30) }; http.DefaultRequestHeaders.Add("User-Agent", "Bitget_Server/1.0"); return http; }); builder.Services.AddSingleton(); builder.Services.AddSingleton(); // ========================================================================= // 7. DI 등록 - Application Services (모두 Singleton: 상태 보관 + BackgroundService 에서 참조) // ========================================================================= builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); // ========================================================================= // 8. DI 등록 - BackgroundServices (HostedServices) // ========================================================================= builder.Services.AddHostedService(); builder.Services.AddHostedService(); builder.Services.AddHostedService(); // ========================================================================= // 9. MVC / Swagger / ProblemDetails // ========================================================================= builder.Services.AddControllers() .AddNewtonsoftJson(opts => { opts.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore; opts.SerializerSettings.NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore; }); builder.Services.AddProblemDetails(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(options => { options.SwaggerDoc("v1", new OpenApiInfo { Title = "Bitget Server", Version = "v1", Description = "기존 Bitget WinForms 애플리케이션을 ASP.NET Core 8 REST API 로 이식한 서버." }); }); // ========================================================================= // 10. 미들웨어 파이프라인 - HTTPS 리디렉션 없음 // ========================================================================= var app = builder.Build(); app.UseExceptionHandler(); app.UseStatusCodePages(); // 정적 파일 (wwwroot/index.html 등). UseStaticFiles 앞에 UseDefaultFiles 필수. app.UseDefaultFiles(); app.UseStaticFiles(); // Swagger 는 환경 불문 항상 활성화 app.UseSwagger(); app.UseSwaggerUI(options => { options.SwaggerEndpoint("/swagger/v1/swagger.json", "Bitget Server v1"); options.RoutePrefix = "swagger"; }); app.UseSerilogRequestLogging(); app.MapControllers(); // 루트에 wwwroot/index.html 이 없을 경우에만 Swagger 로 폴백 리다이렉트 app.MapFallback(() => Results.Redirect("/swagger")); Log.Information("Bitget_Server 시작 — URL http://*:5000, DataDir={DataDir}, LogDir={LogDir}", dataDir, logDir); try { app.Run(); } finally { Log.CloseAndFlush(); } /// /// 애플리케이션 시작 시 appsettings 의 DefaultApiId 로 GlobalOptions.SelectAPI 를 초기화한다. /// internal sealed class ApiKeyBootstrapper : IHostedService { private readonly IServiceProvider _sp; public ApiKeyBootstrapper(IServiceProvider sp) => _sp = sp; public Task StartAsync(CancellationToken cancellationToken) { try { var global = _sp.GetRequiredService(); var opts = _sp.GetRequiredService>().Value; if (global.SelectAPI == null && opts.ApiKeys?.Count > 0) { global.SelectAPI = opts.ApiKeys.FirstOrDefault(k => k.Id == opts.DefaultApiId) ?? opts.ApiKeys.First(); global.Save(); } } catch (Exception ex) { Log.Warning(ex, "ApiKey 부트스트랩 실패 — 이후 /api/settings/api-key 로 수동 설정 필요"); } return Task.CompletedTask; } public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; }