using ExperionCrawler.Core.Application.Interfaces; using ExperionCrawler.Core.Application.Services; using ExperionCrawler.Infrastructure.Certificates; using ExperionCrawler.Infrastructure.Csv; using ExperionCrawler.Infrastructure.Database; using ExperionCrawler.Infrastructure.Kb; using ExperionCrawler.Infrastructure.Mcp; using ExperionCrawler.Infrastructure.OpcUa; using ExperionCrawler.Infrastructure.Trend; using ExperionCrawler.Web; using Microsoft.AspNetCore.Mvc.ApplicationParts; using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.EntityFrameworkCore; var builder = WebApplication.CreateBuilder(args); // ── MVC / Swagger ───────────────────────────────────────────────────────────── var mvcBuilder = builder.Services.AddControllers() .AddJsonOptions(opt => { // JSON 직렬화 시 대소문자 구분 없이 처리하도록 PascalCase 유지 opt.JsonSerializerOptions.PropertyNamingPolicy = null; // Deserialize 시 camelCase 키를 C# PascalCase 속성에 매핑 (프론트엔드 호환) opt.JsonSerializerOptions.PropertyNameCaseInsensitive = true; }); builder.Services.AddMemoryCache(); // ── P&ID 컨트롤러 조건부 활성화 (기본: 비활성화) ───────────────────────────── // PidControllers:Enabled = true 로 설정 시 P&ID 관련 컨트롤러 활성화 bool pidEnabled = builder.Configuration.GetValue("PidControllers:Enabled"); if (!pidEnabled) { var partManager = mvcBuilder.PartManager; var excludedNames = new[] { "PidController", "ExperionPidController", "PidGraphController" }; var existingProvider = partManager.FeatureProviders .OfType() .FirstOrDefault(); if (existingProvider != null) { partManager.FeatureProviders.Remove(existingProvider); partManager.FeatureProviders.Add(new ExcludedControllersFeatureProvider(existingProvider, excludedNames)); } } builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(c => c.SwaggerDoc("v1", new() { Title = "ExperionCrawler API", Version = "v1" })); // ── Infrastructure ──────────────────────────────────────────────────────────── builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); // PostgreSQL – Ubuntu 서버에서 별도 설치 없이 동작 Directory.CreateDirectory("data"); builder.Services.AddDbContext(opt => opt.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection"))); builder.Services.AddScoped(); builder.Services.AddScoped(); // ── Application Services ────────────────────────────────────────────────────── builder.Services.AddScoped(); // ── KST 시간대 관리 서비스 ────────────────────────────────────────────────── builder.Services.AddSingleton(); builder.Services.AddSingleton(); // ── 한글 시간 범위 추출기 ────────────────────────────────────────────────── builder.Services.AddSingleton(); // ── Text-to-SQL Service ────────────────────────────────────────────────────── builder.Services.AddSingleton(_ => new SqlValidatorOptions { RequiredTables = ["history_table"], AllowedTables = ["history_table", "node_map_master", "realtime_table", "tag_metadata", "v_tag_summary"], MaxSubqueryDepth = 4 }); builder.Services.AddSingleton(); builder.Services.AddScoped(); // ── Realtime & History BackgroundServices ───────────────────────────────────── builder.Services.AddSingleton(); builder.Services.AddSingleton( sp => sp.GetRequiredService()); builder.Services.AddHostedService( sp => sp.GetRequiredService()); builder.Services.AddHostedService(); builder.Services.AddHostedService(); // ── MCP Service ─────────────────────────────────────────────────────────────── // Python MCP 서버 (localhost:5001)와 통신 // McpClient: 저수준 HTTP 클라이언트 / McpService: IMcpService 구현 (McpClient 위임) // IHttpClientFactory 패턴: 핸들러 재사용으로 소켓 고갈 방지, 기본 타임아웃 명시 builder.Services.AddHttpClient(McpClient.HttpClientName, c => { c.BaseAddress = new Uri("http://localhost:5001"); // HttpClient.Timeout = 하드 상한. McpClient 내부에서 소프트(120s)→연장(300s) 2단계 적용. // ExtendedTimeoutSeconds(300) + 네트워크/직렬화 여유 60s. c.Timeout = TimeSpan.FromSeconds(McpClient.ExtendedTimeoutSeconds + 60); }).SetHandlerLifetime(TimeSpan.FromMinutes(5)); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddHostedService(); // ── OPC UA Server BackgroundService ────────────────────────────────────────── builder.Services.AddSingleton(); builder.Services.AddSingleton( sp => sp.GetRequiredService()); builder.Services.AddHostedService( sp => sp.GetRequiredService()); // ── FastTable Service ───────────────────────────────────────────────────────── // 중요: Singleton으로 하나만 생성 후 IExperionFastService와 IHostedService 양쪽에 같은 인스턴스 공유 builder.Services.AddSingleton(); builder.Services.AddSingleton(sp => sp.GetRequiredService()); builder.Services.AddHostedService(sp => sp.GetRequiredService()); // ── Metadata Loader Service ─────────────────────────────────────────────────── builder.Services.AddScoped(); // ── Feedforward Advisory Engine ─────────────────────────────────────────────── builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddHostedService(sp => sp.GetRequiredService()); // ── P&ID Services ─────────────────────────────────────────────────────────────── builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddDbContextFactory(); builder.Services.AddScoped(); builder.Services.AddSingleton(); // ── FastTable Cleanup Service ───────────────────────────────────────────────── builder.Services.AddHostedService(); // ── Knowledge Base (RAG) ────────────────────────────────────────────────────── builder.Services.AddHttpClient("KbQdrant", (sp, c) => { var cfg = sp.GetRequiredService(); var baseUrl = cfg["Kb:QdrantUrl"] ?? "http://localhost:6333"; c.BaseAddress = new Uri(baseUrl); c.Timeout = TimeSpan.FromSeconds(30); }); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddScoped(); builder.Services.AddSingleton(); builder.Services.AddHostedService(); builder.Services.AddHostedService(); // ── Ollama HttpClient ───────────────────────────────────────────────────────── builder.Services.AddHttpClient("Ollama", c => { c.BaseAddress = new Uri("http://localhost:11434"); c.Timeout = TimeSpan.FromSeconds(1800); }).SetHandlerLifetime(Timeout.InfiniteTimeSpan); // ── vLLM HttpClient (OpenAI-compatible) ────────────────────────────────────── builder.Services.AddHttpClient("Vllm", c => { c.BaseAddress = new Uri("http://localhost:8000"); c.Timeout = TimeSpan.FromSeconds(1800); }).SetHandlerLifetime(Timeout.InfiniteTimeSpan); // ── CORS ────────────────────────────────────────────────────────────────────── // appsettings.json 의 "Cors:AllowedOrigins" 배열을 화이트리스트로 사용. // 미설정 또는 ["*"] 인 경우에만 AllowAnyOrigin (개발 편의). 운영에서는 명시적 origin 권장. builder.Services.AddCors(opt => opt.AddDefaultPolicy(p => { var origins = builder.Configuration.GetSection("Cors:AllowedOrigins").Get(); if (origins == null || origins.Length == 0 || (origins.Length == 1 && origins[0] == "*")) { // 자격증명 미사용 + 내부망 가정: 와일드카드 허용 (역호환) p.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader(); } else { p.WithOrigins(origins).AllowAnyMethod().AllowAnyHeader(); } })); // ── 포트 설정 (Ubuntu 환경: 기본 5000) ─────────────────────────────────────── builder.WebHost.UseUrls("http://0.0.0.0:5000"); var app = builder.Build(); // ── DB 초기화 ───────────────────────────────────────────────────────────────── try { using (var scope = app.Services.CreateScope()) { var db = scope.ServiceProvider.GetRequiredService(); await db.InitializeAsync(); try { var kbAuth = scope.ServiceProvider.GetRequiredService(); await kbAuth.EnsureCredentialAsync(); } catch (Exception kbEx) { var lg = app.Services.GetRequiredService>(); lg.LogWarning(kbEx, "[Kb] 관리자 비밀번호 초기화 실패"); } } } catch (Exception ex) { // DB 초기화 실패 시 앱 시작 계속 — 기능 사용 시 지연 초기화 var logger = app.Services.GetRequiredService>(); logger.LogWarning(ex, "[DB] 초기화 실패 — DB 관련 기능 비활성화 (앱 시작 계속)"); } // ── Middleware ──────────────────────────────────────────────────────────────── app.UseCors(); if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } app.UseDefaultFiles(); // index.html app.UseStaticFiles(); // wwwroot/ app.MapControllers(); app.MapFallbackToFile("index.html"); app.Run();