Phase II:
- FfOperatorAction entity + ff_operator_action DDL/DbSet
- IFeedforwardWriteGuard + FeedforwardWriteGuard (SP bounds, grade C, transient, NaN)
- IFeedforwardAuditService + FeedforwardAuditService (raw ADO insert/query)
- FeedforwardSupervisor.AutoWriteAsync (per-stream OPC UA after Tick, rate-limited)
- FeedforwardConfigStore: advisory_only now read/writes DB, sp_node_id column
- FeedforwardController: auth (X-Kb-Token) on config/delete/write/audit;
POST write/{id}/{key} manual SP write; GET audit; write results in MapColumn
- ff.js: token header, auto-write badge, per-stream write result, spNodeId, advisoryOnly
- ff.css: .ff-write-badge, .ff-write, .ff-write-err, .ff-wg-blocked
- Program.cs: register audit (Scoped) + write guard (Singleton)
WO-2~7 (build 0W/0E, test 22/22):
- PCT monitor, θ auto-tune, slow bias, front position indicator,
total reflux recovery, config form expansion
239 lines
14 KiB
C#
239 lines
14 KiB
C#
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<bool>("PidControllers:Enabled");
|
||
if (!pidEnabled)
|
||
{
|
||
var partManager = mvcBuilder.PartManager;
|
||
var excludedNames = new[] { "PidController", "ExperionPidController", "PidGraphController" };
|
||
var existingProvider = partManager.FeatureProviders
|
||
.OfType<Microsoft.AspNetCore.Mvc.Controllers.ControllerFeatureProvider>()
|
||
.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<IExperionCertificateService, ExperionCertificateService>();
|
||
builder.Services.AddSingleton<IExperionStatusCodeService, ExperionStatusCodeService>();
|
||
builder.Services.AddSingleton<IOpcUaConfigProvider, OpcUaConfigProvider>();
|
||
builder.Services.AddScoped<IExperionOpcClient, ExperionOpcClient>();
|
||
builder.Services.AddScoped<IExperionOpcWriteClient, ExperionOpcWriteClient>();
|
||
builder.Services.AddScoped<IExperionCsvService, ExperionCsvService>();
|
||
builder.Services.AddScoped<AssetLoader>();
|
||
|
||
// PostgreSQL – Ubuntu 서버에서 별도 설치 없이 동작
|
||
Directory.CreateDirectory("data");
|
||
builder.Services.AddDbContext<ExperionDbContext>(opt =>
|
||
opt.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection")));
|
||
builder.Services.AddScoped<IExperionDbService, ExperionDbService>();
|
||
builder.Services.AddScoped<ITrendService, TrendService>();
|
||
|
||
// ── Application Services ──────────────────────────────────────────────────────
|
||
builder.Services.AddScoped<ExperionCrawlService>();
|
||
|
||
// ── KST 시간대 관리 서비스 ──────────────────────────────────────────────────
|
||
builder.Services.AddSingleton<IClock, SystemClock>();
|
||
builder.Services.AddSingleton<KstClock>();
|
||
|
||
// ── 한글 시간 범위 추출기 ──────────────────────────────────────────────────
|
||
builder.Services.AddSingleton<KoreanTimeRangeExtractor>();
|
||
|
||
// ── Text-to-SQL Service ──────────────────────────────────────────────────────
|
||
builder.Services.AddSingleton<SqlValidatorOptions>(_ => new SqlValidatorOptions
|
||
{
|
||
RequiredTables = ["history_table"],
|
||
AllowedTables = ["history_table", "node_map_master", "realtime_table", "tag_metadata", "v_tag_summary"],
|
||
MaxSubqueryDepth = 4
|
||
});
|
||
builder.Services.AddSingleton<SqlValidator>();
|
||
builder.Services.AddScoped<ITextToSqlService, TextToSqlService>();
|
||
|
||
// ── Realtime & History BackgroundServices ─────────────────────────────────────
|
||
builder.Services.AddSingleton<ExperionRealtimeService>();
|
||
builder.Services.AddSingleton<IExperionRealtimeService>(
|
||
sp => sp.GetRequiredService<ExperionRealtimeService>());
|
||
builder.Services.AddHostedService(
|
||
sp => sp.GetRequiredService<ExperionRealtimeService>());
|
||
builder.Services.AddHostedService<ExperionHistoryService>();
|
||
builder.Services.AddHostedService<DigitalEventDetectorService>();
|
||
|
||
// ── 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<McpClient>();
|
||
builder.Services.AddSingleton<IMcpService, McpService>();
|
||
builder.Services.AddHostedService<McpServerHostedService>();
|
||
|
||
// ── OPC UA Server BackgroundService ──────────────────────────────────────────
|
||
builder.Services.AddSingleton<ExperionOpcServerService>();
|
||
builder.Services.AddSingleton<IExperionOpcServerService>(
|
||
sp => sp.GetRequiredService<ExperionOpcServerService>());
|
||
builder.Services.AddHostedService(
|
||
sp => sp.GetRequiredService<ExperionOpcServerService>());
|
||
|
||
// ── FastTable Service ─────────────────────────────────────────────────────────
|
||
// 중요: Singleton으로 하나만 생성 후 IExperionFastService와 IHostedService 양쪽에 같은 인스턴스 공유
|
||
builder.Services.AddSingleton<ExperionFastService>();
|
||
builder.Services.AddSingleton<IExperionFastService>(sp => sp.GetRequiredService<ExperionFastService>());
|
||
builder.Services.AddHostedService(sp => sp.GetRequiredService<ExperionFastService>());
|
||
|
||
// ── Metadata Loader Service ───────────────────────────────────────────────────
|
||
builder.Services.AddScoped<IMetadataLoaderService, MetadataLoaderService>();
|
||
|
||
// ── Feedforward Advisory Engine ───────────────────────────────────────────────
|
||
builder.Services.AddSingleton<ExperionCrawler.Infrastructure.Control.FeedforwardEngine>();
|
||
builder.Services.AddSingleton<ExperionCrawler.Core.Application.Feedforward.IFeedforwardAdvisoryStore, ExperionCrawler.Infrastructure.Control.FeedforwardAdvisoryStore>();
|
||
builder.Services.AddScoped<ExperionCrawler.Core.Application.Feedforward.IFeedforwardConfigStore, ExperionCrawler.Infrastructure.Control.FeedforwardConfigStore>();
|
||
builder.Services.AddScoped<ExperionCrawler.Core.Application.Feedforward.IFeedforwardAuditService, ExperionCrawler.Infrastructure.Control.FeedforwardAuditService>();
|
||
builder.Services.AddSingleton<ExperionCrawler.Core.Application.Feedforward.IFeedforwardWriteGuard, ExperionCrawler.Infrastructure.Control.FeedforwardWriteGuard>();
|
||
builder.Services.AddSingleton<ExperionCrawler.Infrastructure.Control.FeedforwardSupervisor>();
|
||
builder.Services.AddHostedService(sp => sp.GetRequiredService<ExperionCrawler.Infrastructure.Control.FeedforwardSupervisor>());
|
||
|
||
// ── P&ID Services ───────────────────────────────────────────────────────────────
|
||
builder.Services.AddScoped<IPidExtractorService, PidExtractorService>();
|
||
builder.Services.AddScoped<ITagMappingService, TagMappingService>();
|
||
builder.Services.AddScoped<IPidGraphService, PidGraphService>();
|
||
builder.Services.AddDbContextFactory<ExperionDbContext>();
|
||
builder.Services.AddScoped<IStatusStore, DbStatusStore>();
|
||
builder.Services.AddSingleton<IPidGraphEventBroadcaster, PidGraphEventBroadcaster>();
|
||
|
||
// ── FastTable Cleanup Service ─────────────────────────────────────────────────
|
||
builder.Services.AddHostedService<ExperionFastCleanupService>();
|
||
|
||
// ── Knowledge Base (RAG) ──────────────────────────────────────────────────────
|
||
builder.Services.AddHttpClient("KbQdrant", (sp, c) =>
|
||
{
|
||
var cfg = sp.GetRequiredService<IConfiguration>();
|
||
var baseUrl = cfg["Kb:QdrantUrl"] ?? "http://localhost:6333";
|
||
c.BaseAddress = new Uri(baseUrl);
|
||
c.Timeout = TimeSpan.FromSeconds(30);
|
||
});
|
||
builder.Services.AddSingleton<KbQdrantClient>();
|
||
builder.Services.AddSingleton<KbStorageService>();
|
||
builder.Services.AddSingleton<KbEmbeddingClient>();
|
||
builder.Services.AddScoped<IKbAuthService, KbAuthService>();
|
||
builder.Services.AddSingleton<ExperionCrawler.Infrastructure.Docs.DocBrowserService>();
|
||
builder.Services.AddHostedService<KbStartupService>();
|
||
builder.Services.AddHostedService<KbIngestWorker>();
|
||
|
||
// ── 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<string[]>();
|
||
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<IExperionDbService>();
|
||
await db.InitializeAsync();
|
||
|
||
try
|
||
{
|
||
var kbAuth = scope.ServiceProvider.GetRequiredService<IKbAuthService>();
|
||
await kbAuth.EnsureCredentialAsync();
|
||
}
|
||
catch (Exception kbEx)
|
||
{
|
||
var lg = app.Services.GetRequiredService<ILogger<Program>>();
|
||
lg.LogWarning(kbEx, "[Kb] 관리자 비밀번호 초기화 실패");
|
||
}
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
// DB 초기화 실패 시 앱 시작 계속 — 기능 사용 시 지연 초기화
|
||
var logger = app.Services.GetRequiredService<ILogger<Program>>();
|
||
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();
|