Files
BitGet_API/Bitget_Server/Program.cs

196 lines
7.9 KiB
C#
Raw Normal View History

2026-04-20 20:27:42 +09:00
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;
2024-06-02 19:32:08 +09:00
2026-04-20 20:27:42 +09:00
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<BitgetOptions>(builder.Configuration.GetSection(BitgetOptions.SectionName));
var bitgetOptions = builder.Configuration.GetSection(BitgetOptions.SectionName).Get<BitgetOptions>() ?? new BitgetOptions();
// =========================================================================
// 3. 영속화 경로 설정 (BaseSettings<GlobalOptions>.StorageDirectory)
// 컨테이너에서는 /data, 로컬에서는 ./data 로 마운트됨
// =========================================================================
var dataDir = Path.GetFullPath(bitgetOptions.DataDirectory);
var logDir = Path.GetFullPath(bitgetOptions.LogDirectory);
Directory.CreateDirectory(dataDir);
Directory.CreateDirectory(logDir);
BaseSettings<GlobalOptions>.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<MarketState>();
builder.Services.AddSingleton(sp => GlobalOptions.Instance); // BaseSettings 기반 파일 영속화 싱글톤
builder.Services.AddSingleton<PriceLevelTracker>();
// 기본 API 키 자동 선택 (GlobalOptions.SelectAPI 가 비어 있으면 appsettings.json 의 DefaultApiId 로 초기화)
// 반드시 다른 HostedService 보다 먼저 등록되어야 함 (HostedServices 는 등록 순서대로 StartAsync).
builder.Services.AddHostedService<ApiKeyBootstrapper>();
// =========================================================================
// 6. DI 등록 - Bitget Infrastructure
// =========================================================================
builder.Services.AddSingleton(sp => new BitgetRateLimiter(
bitgetOptions.RateLimitPerSecond,
sp.GetRequiredService<ILogger<BitgetRateLimiter>>()));
// Bitget 단일 엔드포인트만 호출하므로 HttpClient 를 Singleton 으로 공유해도 안전.
// (BackgroundService 가 Singleton 이므로 BitgetClient 도 Singleton 이어야 캡처 문제가 없음)
builder.Services.AddSingleton(sp =>
2024-06-02 19:32:08 +09:00
{
2026-04-20 20:27:42 +09:00
var http = new HttpClient { Timeout = TimeSpan.FromSeconds(30) };
http.DefaultRequestHeaders.Add("User-Agent", "Bitget_Server/1.0");
return http;
});
builder.Services.AddSingleton<BitgetClient>();
builder.Services.AddSingleton<BitgetWebSocketClient>();
// =========================================================================
// 7. DI 등록 - Application Services (모두 Singleton: 상태 보관 + BackgroundService 에서 참조)
// =========================================================================
builder.Services.AddSingleton<SettingsService>();
builder.Services.AddSingleton<BitgetTradingService>();
builder.Services.AddSingleton<InitialCandleLoader>();
builder.Services.AddSingleton<SignalProcessService>();
// =========================================================================
// 8. DI 등록 - BackgroundServices (HostedServices)
// =========================================================================
builder.Services.AddHostedService<MarketInfoBackgroundService>();
builder.Services.AddHostedService<WebSocketCalcBackgroundService>();
builder.Services.AddHostedService<SignalProcessBackgroundService>();
// =========================================================================
// 9. MVC / Swagger / ProblemDetails
// =========================================================================
builder.Services.AddControllers()
.AddNewtonsoftJson(opts =>
2024-06-02 19:32:08 +09:00
{
2026-04-20 20:27:42 +09:00
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 로 이식한 서버."
});
});
2024-06-02 20:48:47 +09:00
2026-04-20 20:27:42 +09:00
// =========================================================================
// 10. 미들웨어 파이프라인 - HTTPS 리디렉션 없음
// =========================================================================
var app = builder.Build();
2024-06-02 19:32:08 +09:00
2026-04-20 20:27:42 +09:00
app.UseExceptionHandler();
app.UseStatusCodePages();
2024-06-02 19:32:08 +09:00
2026-04-21 01:19:08 +09:00
// 정적 파일 (wwwroot/index.html 등). UseStaticFiles 앞에 UseDefaultFiles 필수.
app.UseDefaultFiles();
app.UseStaticFiles();
2026-04-20 20:27:42 +09:00
// Swagger 는 환경 불문 항상 활성화
app.UseSwagger();
app.UseSwaggerUI(options =>
{
options.SwaggerEndpoint("/swagger/v1/swagger.json", "Bitget Server v1");
options.RoutePrefix = "swagger";
});
2024-06-02 19:32:08 +09:00
2026-04-20 20:27:42 +09:00
app.UseSerilogRequestLogging();
2024-06-02 19:32:08 +09:00
2026-04-20 20:27:42 +09:00
app.MapControllers();
2026-04-21 01:19:08 +09:00
// 루트에 wwwroot/index.html 이 없을 경우에만 Swagger 로 폴백 리다이렉트
app.MapFallback(() => Results.Redirect("/swagger"));
2026-04-20 20:27:42 +09:00
Log.Information("Bitget_Server 시작 — URL http://*:5000, DataDir={DataDir}, LogDir={LogDir}", dataDir, logDir);
try
{
app.Run();
}
finally
{
Log.CloseAndFlush();
}
/// <summary>
/// 애플리케이션 시작 시 appsettings 의 DefaultApiId 로 GlobalOptions.SelectAPI 를 초기화한다.
/// </summary>
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<GlobalOptions>();
var opts = _sp.GetRequiredService<Microsoft.Extensions.Options.IOptions<BitgetOptions>>().Value;
if (global.SelectAPI == null && opts.ApiKeys?.Count > 0)
{
global.SelectAPI = opts.ApiKeys.FirstOrDefault(k => k.Id == opts.DefaultApiId)
?? opts.ApiKeys.First();
global.Save();
}
2024-06-02 19:32:08 +09:00
}
2026-04-20 20:27:42 +09:00
catch (Exception ex)
{
Log.Warning(ex, "ApiKey 부트스트랩 실패 — 이후 /api/settings/api-key 로 수동 설정 필요");
}
return Task.CompletedTask;
2024-06-02 19:32:08 +09:00
}
2026-04-20 20:27:42 +09:00
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
2024-06-02 19:32:08 +09:00
}