feat(ui): Blazor WebAssembly 마이그레이션 및 API-First 로그인 구현

This commit is contained in:
2026-07-01 11:22:09 +09:00
parent bdb9262f4e
commit 4de9339163
21 changed files with 246 additions and 80 deletions
@@ -1,16 +1,16 @@
using System.Security.Claims; using System.Security.Claims;
using Microsoft.AspNetCore.Components.Authorization; using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage; using QuantEngine.Web.Client.Services;
namespace QuantEngine.Web.Infrastructure namespace QuantEngine.Web.Client.Infrastructure
{ {
public class CustomAuthenticationStateProvider : AuthenticationStateProvider public class CustomAuthenticationStateProvider : AuthenticationStateProvider
{ {
private readonly ProtectedLocalStorage _localStorage; private readonly LocalStorageService _localStorage;
private readonly ClaimsPrincipal _anonymous = new ClaimsPrincipal(new ClaimsIdentity()); private readonly ClaimsPrincipal _anonymous = new ClaimsPrincipal(new ClaimsIdentity());
private const string StorageKey = "quant_admin_session"; private const string StorageKey = "quant_admin_session";
public CustomAuthenticationStateProvider(ProtectedLocalStorage localStorage) public CustomAuthenticationStateProvider(LocalStorageService localStorage)
{ {
_localStorage = localStorage; _localStorage = localStorage;
} }
@@ -19,11 +19,9 @@ namespace QuantEngine.Web.Infrastructure
{ {
try try
{ {
// ProtectedLocalStorage call will throw an exception during pre-rendering var username = await _localStorage.GetAsync<string>(StorageKey);
var result = await _localStorage.GetAsync<string>(StorageKey); if (!string.IsNullOrEmpty(username))
if (result.Success && !string.IsNullOrEmpty(result.Value))
{ {
var username = result.Value;
var identity = new ClaimsIdentity(new[] var identity = new ClaimsIdentity(new[]
{ {
new Claim(ClaimTypes.Name, username), new Claim(ClaimTypes.Name, username),
@@ -36,7 +34,7 @@ namespace QuantEngine.Web.Infrastructure
} }
catch catch
{ {
// Return anonymous state during pre-rendering or if storage reading fails // Return anonymous if localStorage isn't ready
} }
return new AuthenticationState(_anonymous); return new AuthenticationState(_anonymous);
@@ -1,11 +1,10 @@
@inherits LayoutComponentBase @inherits LayoutComponentBase
@inject Microsoft.AspNetCore.Hosting.IWebHostEnvironment WebHostEnvironment @inject HttpClient Http
@inject AuthenticationStateProvider AuthStateProvider @inject AuthenticationStateProvider AuthStateProvider
@inject NavigationManager NavigationManager @inject NavigationManager NavigationManager
@using System.IO @using System.Net.Http.Json
@using System.Text.Json
@using Microsoft.FluentUI.AspNetCore.Components @using Microsoft.FluentUI.AspNetCore.Components
@using QuantEngine.Web.Infrastructure @using QuantEngine.Web.Client.Infrastructure
<FluentStack Orientation="Orientation.Vertical" Class="h-100 w-100"> <FluentStack Orientation="Orientation.Vertical" Class="h-100 w-100">
<!-- Header --> <!-- Header -->
@@ -77,25 +76,15 @@
private string appVersion = "Local Debug"; private string appVersion = "Local Debug";
private string buildTime = "N/A"; private string buildTime = "N/A";
protected override void OnInitialized() protected override async Task OnInitializedAsync()
{ {
try try
{ {
var versionFilePath = Path.Combine(WebHostEnvironment.WebRootPath, "version.json"); var versionInfo = await Http.GetFromJsonAsync<VersionInfo>("version.json");
if (File.Exists(versionFilePath)) if (versionInfo != null)
{ {
var jsonContent = File.ReadAllText(versionFilePath); appVersion = versionInfo.Version ?? "Local Debug";
using var doc = System.Text.Json.JsonDocument.Parse(jsonContent); buildTime = versionInfo.Built ?? "N/A";
var root = doc.RootElement;
if (root.TryGetProperty("version", out var versionProp))
{
appVersion = versionProp.GetString() ?? "Local Debug";
}
if (root.TryGetProperty("built", out var builtProp))
{
buildTime = builtProp.GetString() ?? "N/A";
}
} }
} }
catch catch
@@ -110,5 +99,11 @@
await customProvider.MarkUserAsLoggedOutAsync(); await customProvider.MarkUserAsLoggedOutAsync();
NavigationManager.NavigateTo("login"); NavigationManager.NavigateTo("login");
} }
private class VersionInfo
{
public string? Version { get; set; }
public string? Built { get; set; }
}
} }
@@ -1,5 +1,5 @@
@page "/collection" @page "/collection"
@using QuantEngine.Web.Services @using QuantEngine.Web.Client.Services
@inject ApiClient ApiClient @inject ApiClient ApiClient
@inject ILogger<Collection> Logger @inject ILogger<Collection> Logger
@@ -1,6 +1,6 @@
@page "/" @page "/"
@using QuantEngine.Core.Infrastructure @using QuantEngine.Core.Infrastructure
@inject IWebHostEnvironment Environment @inject HttpClient Http
<PageTitle>Quant Engine - Dashboard</PageTitle> <PageTitle>Quant Engine - Dashboard</PageTitle>
@@ -96,15 +96,26 @@
private string RawFeedLabel = "DISCONNECTED"; private string RawFeedLabel = "DISCONNECTED";
private string ReportPath = "n/a"; private string ReportPath = "n/a";
protected override void OnInitialized() protected override async Task OnInitializedAsync()
{ {
ReportPath = Path.GetFullPath(Path.Combine(Environment.ContentRootPath, "..", "..", "..", "Temp", "operational_report.json")); try
var report = OperationalReportLoader.Load(ReportPath); {
Sections.AddRange(report.Sections); var report = await Http.GetFromJsonAsync<OperationalReportData>("api/operational-report");
SectionCountLabel = report.SectionCount.ToString(); if (report != null)
GeneratedAtLabel = report.GeneratedAt; {
SourceLabel = report.SourceJson; Sections.Clear();
ReportStateLabel = Sections.Count > 0 ? "READY" : "DATA_MISSING"; Sections.AddRange(report.Sections);
ReportChipLabel = Sections.Count > 0 ? "READY" : "DATA_MISSING"; SectionCountLabel = report.SectionCount.ToString();
GeneratedAtLabel = report.GeneratedAt;
SourceLabel = report.SourceJson;
ReportStateLabel = Sections.Count > 0 ? "READY" : "DATA_MISSING";
ReportChipLabel = Sections.Count > 0 ? "READY" : "DATA_MISSING";
}
}
catch
{
ReportStateLabel = "DATA_MISSING";
ReportChipLabel = "DATA_MISSING";
}
} }
} }
@@ -2,7 +2,7 @@
@attribute [AllowAnonymous] @attribute [AllowAnonymous]
@inject AuthenticationStateProvider AuthStateProvider @inject AuthenticationStateProvider AuthStateProvider
@inject NavigationManager NavigationManager @inject NavigationManager NavigationManager
@inject IConfiguration Configuration @inject HttpClient Http
<PageTitle>로그인 - QuantEngine</PageTitle> <PageTitle>로그인 - QuantEngine</PageTitle>
@@ -281,11 +281,9 @@
try try
{ {
// Verify against configurations in appsettings.json var response = await Http.PostAsJsonAsync("api/auth/login", new { Username, Password });
var expectedUser = Configuration["AdminSettings:Username"] ?? "admin";
var expectedPass = Configuration["AdminSettings:Password"] ?? "quant123!";
if (Username == expectedUser && Password == expectedPass) if (response.IsSuccessStatusCode)
{ {
var customProvider = (CustomAuthenticationStateProvider)AuthStateProvider; var customProvider = (CustomAuthenticationStateProvider)AuthStateProvider;
await customProvider.MarkUserAsAuthenticatedAsync(Username); await customProvider.MarkUserAsAuthenticatedAsync(Username);
@@ -1,6 +1,6 @@
@page "/operations" @page "/operations"
@using QuantEngine.Core.Infrastructure @using QuantEngine.Core.Infrastructure
@inject IWebHostEnvironment Environment @inject HttpClient Http
<PageTitle>Quant Engine - Operations</PageTitle> <PageTitle>Quant Engine - Operations</PageTitle>
@@ -97,19 +97,29 @@
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
ReportPath = Path.GetFullPath(Path.Combine(Environment.ContentRootPath, "..", "..", "..", "Temp", "operational_report.json")); try
{
var report = await Http.GetFromJsonAsync<OperationalReportData>("api/operational-report");
if (report != null)
{
SchemaVersion = report.SchemaVersion;
SourceJson = report.SourceJson;
GeneratedAt = report.GeneratedAt;
Sections.Clear();
Sections.AddRange(report.Sections);
var report = OperationalReportLoader.Load(ReportPath); HighlightSections.Clear();
SchemaVersion = report.SchemaVersion; HighlightSections.AddRange(Sections.Take(4));
SourceJson = report.SourceJson;
GeneratedAt = report.GeneratedAt;
Sections.AddRange(report.Sections);
HighlightSections.Clear(); SectionCountLabel = report.SectionCount.ToString();
HighlightSections.AddRange(Sections.Take(4)); RenderedSectionCountLabel = Sections.Count.ToString();
HealthLabel = Sections.Count > 0 ? "PASS" : "DATA_MISSING";
SectionCountLabel = report.SectionCount.ToString(); }
RenderedSectionCountLabel = Sections.Count.ToString(); }
HealthLabel = Sections.Count > 0 ? "PASS" : "DATA_MISSING"; catch
{
HealthLabel = "DATA_MISSING";
}
} }
} }
@@ -0,0 +1,23 @@
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Microsoft.FluentUI.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Authorization;
using QuantEngine.Web.Client.Services;
using QuantEngine.Web.Client.Infrastructure;
var builder = WebAssemblyHostBuilder.CreateDefault(args);
// Register Fluent UI
builder.Services.AddFluentUIComponents();
// Register LocalStorage for cross-platform session persistence
builder.Services.AddScoped<LocalStorageService>();
// Authentication setup in WebAssembly client
builder.Services.AddAuthorizationCore();
builder.Services.AddCascadingAuthenticationState();
builder.Services.AddScoped<AuthenticationStateProvider, CustomAuthenticationStateProvider>();
// HttpClient register (API-First standard)
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
await builder.Build().RunAsync();
@@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<NoDefaultLaunchSettingsFile>true</NoDefaultLaunchSettingsFile>
<StaticWebAssetProjectMode>Default</StaticWebAssetProjectMode>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\QuantEngine.Core\QuantEngine.Core.csproj" />
<ProjectReference Include="..\..\QuantEngine.Application\QuantEngine.Application.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.0-preview.2.25120.18" />
<PackageReference Include="Microsoft.AspNetCore.Components.Authorization" Version="10.0.0-preview.2.25120.18" />
<PackageReference Include="Microsoft.FluentUI.AspNetCore.Components" Version="5.0.0-rc.4-26177.1" />
<PackageReference Include="Microsoft.FluentUI.AspNetCore.Components.Icons" Version="5.0.0-rc.4-26177.1" />
</ItemGroup>
</Project>
@@ -2,19 +2,16 @@ using System.Net.Http.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using QuantEngine.Core.Interfaces; using QuantEngine.Core.Interfaces;
namespace QuantEngine.Web.Services; namespace QuantEngine.Web.Client.Services;
public class ApiClient public class ApiClient
{ {
private readonly HttpClient _http; private readonly HttpClient _http;
private readonly ILogger<ApiClient> _logger; private readonly ILogger<ApiClient> _logger;
private string BaseUrl { get; set; }
public ApiClient(HttpClient http, ILogger<ApiClient> logger) public ApiClient(HttpClient http, ILogger<ApiClient> logger)
{ {
_http = http; _http = http;
_logger = logger; _logger = logger;
BaseUrl = "http://localhost:5001"; // Default for Blazor Server
} }
// Collection API Methods // Collection API Methods
@@ -23,7 +20,7 @@ public class ApiClient
{ {
try try
{ {
return await _http.GetFromJsonAsync<CollectionDashboardStateDto>($"{BaseUrl}/api/collection/state"); return await _http.GetFromJsonAsync<CollectionDashboardStateDto>("api/collection/state");
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -36,7 +33,7 @@ public class ApiClient
{ {
try try
{ {
return await _http.GetFromJsonAsync<CollectionRunsResponse>($"{BaseUrl}/api/collection/runs?limit={limit}"); return await _http.GetFromJsonAsync<CollectionRunsResponse>($"api/collection/runs?limit={limit}");
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -49,7 +46,7 @@ public class ApiClient
{ {
try try
{ {
return await _http.GetFromJsonAsync<CollectionRunSnapshotsResponse>($"{BaseUrl}/api/collection/runs/{runId}/snapshots"); return await _http.GetFromJsonAsync<CollectionRunSnapshotsResponse>($"api/collection/runs/{runId}/snapshots");
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -62,7 +59,7 @@ public class ApiClient
{ {
try try
{ {
return await _http.GetFromJsonAsync<CollectionRunErrorsResponse>($"{BaseUrl}/api/collection/runs/{runId}/errors?limit={limit}"); return await _http.GetFromJsonAsync<CollectionRunErrorsResponse>($"api/collection/runs/{runId}/errors?limit={limit}");
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -75,7 +72,7 @@ public class ApiClient
{ {
try try
{ {
var response = await _http.PostAsJsonAsync($"{BaseUrl}/api/collection/run", new { }); var response = await _http.PostAsJsonAsync("api/collection/run", new { });
if (response.IsSuccessStatusCode) if (response.IsSuccessStatusCode)
{ {
return await response.Content.ReadFromJsonAsync<CollectionRunStartResponse>(); return await response.Content.ReadFromJsonAsync<CollectionRunStartResponse>();
@@ -0,0 +1,43 @@
using Microsoft.JSInterop;
using System.Text.Json;
namespace QuantEngine.Web.Client.Services
{
public class LocalStorageService
{
private readonly IJSRuntime _js;
public LocalStorageService(IJSRuntime js)
{
_js = js;
}
public async Task SetAsync<T>(string key, T value)
{
var json = JsonSerializer.Serialize(value);
await _js.InvokeVoidAsync("localStorage.setItem", key, json);
}
public async Task<T?> GetAsync<T>(string key)
{
try
{
var json = await _js.InvokeAsync<string?>("localStorage.getItem", key);
if (string.IsNullOrEmpty(json))
{
return default;
}
return JsonSerializer.Deserialize<T>(json);
}
catch
{
return default;
}
}
public async Task DeleteAsync(string key)
{
await _js.InvokeVoidAsync("localStorage.removeItem", key);
}
}
}
@@ -8,7 +8,10 @@
@using Microsoft.JSInterop @using Microsoft.JSInterop
@using Microsoft.FluentUI.AspNetCore.Components @using Microsoft.FluentUI.AspNetCore.Components
@using Microsoft.FluentUI.AspNetCore.Components.Icons @using Microsoft.FluentUI.AspNetCore.Components.Icons
@using QuantEngine.Web @using QuantEngine.Web.Client
@using QuantEngine.Web.Components @using QuantEngine.Web.Client.Pages
@using QuantEngine.Web.Components.Layout @using QuantEngine.Web.Client.Layout
@using QuantEngine.Web.Services @using QuantEngine.Web.Client.Infrastructure
@using QuantEngine.Web.Client.Services
@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.Authorization
@@ -13,12 +13,12 @@
<link rel="stylesheet" href="@Assets["QuantEngine.Web.styles.css"]" /> <link rel="stylesheet" href="@Assets["QuantEngine.Web.styles.css"]" />
<ImportMap /> <ImportMap />
<link rel="icon" type="image/png" href="favicon.png" /> <link rel="icon" type="image/png" href="favicon.png" />
<HeadOutlet /> <HeadOutlet @rendermode="InteractiveWebAssembly" />
</head> </head>
<body> <body>
<FluentDesignSystemProvider> <FluentDesignSystemProvider>
<Routes /> <Routes @rendermode="InteractiveWebAssembly" />
<ReconnectModal /> <ReconnectModal />
</FluentDesignSystemProvider> </FluentDesignSystemProvider>
@@ -1,7 +1,11 @@
@using QuantEngine.Web.Client
@using QuantEngine.Web.Client.Pages
@using QuantEngine.Web.Client.Layout
<CascadingAuthenticationState> <CascadingAuthenticationState>
<Router AppAssembly="typeof(Program).Assembly" NotFoundPage="typeof(Pages.NotFound)"> <Router AppAssembly="typeof(Dashboard).Assembly" NotFoundPage="typeof(NotFound)">
<Found Context="routeData"> <Found Context="routeData">
<AuthorizeRouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)"> <AuthorizeRouteView RouteData="routeData" DefaultLayout="typeof(MainLayout)">
<NotAuthorized> <NotAuthorized>
<RedirectToLogin /> <RedirectToLogin />
</NotAuthorized> </NotAuthorized>
+40 -5
View File
@@ -1,7 +1,7 @@
using QuantEngine.Web.Components; using QuantEngine.Web.Components;
using QuantEngine.Web.Services;
using QuantEngine.Infrastructure.Data; using QuantEngine.Infrastructure.Data;
using Microsoft.AspNetCore.Components.Authorization; using Microsoft.AspNetCore.Components.Authorization;
using QuantEngine.Web.Infrastructure;
using QuantEngine.Infrastructure.Repositories; using QuantEngine.Infrastructure.Repositories;
using QuantEngine.Infrastructure.Services; using QuantEngine.Infrastructure.Services;
using QuantEngine.Core.Interfaces; using QuantEngine.Core.Interfaces;
@@ -10,7 +10,8 @@ using System.Text.Json;
using static QuantEngine.Application.Services.DataCollectionService; using static QuantEngine.Application.Services.DataCollectionService;
using Microsoft.FluentUI.AspNetCore.Components; using Microsoft.FluentUI.AspNetCore.Components;
using Serilog; using Serilog;
using QuantEngine.Web.Infrastructure; using QuantEngine.Web.Client.Infrastructure;
using QuantEngine.Web.Client.Services;
using QuantEngine.Web.Endpoints; using QuantEngine.Web.Endpoints;
// Serilog Configuration with Telegram Sink // Serilog Configuration with Telegram Sink
@@ -25,10 +26,11 @@ builder.Host.UseSerilog();
// Add services to the container. // Add services to the container.
builder.Services.AddRazorComponents() builder.Services.AddRazorComponents()
.AddInteractiveServerComponents(); .AddInteractiveWebAssemblyComponents();
// Authentication and Custom State Provider // Authentication and Custom State Provider (Shared client components)
builder.Services.AddCascadingAuthenticationState(); builder.Services.AddCascadingAuthenticationState();
builder.Services.AddScoped<LocalStorageService>();
builder.Services.AddScoped<AuthenticationStateProvider, CustomAuthenticationStateProvider>(); builder.Services.AddScoped<AuthenticationStateProvider, CustomAuthenticationStateProvider>();
builder.Services.AddAuthorizationCore(); builder.Services.AddAuthorizationCore();
@@ -100,6 +102,32 @@ app.MapStaticAssets();
// Collection API Endpoints (must be before MapRazorComponents) // Collection API Endpoints (must be before MapRazorComponents)
app.MapCollectionEndpoints(); app.MapCollectionEndpoints();
// Login API (API-First for Blazor WASM client authentication)
app.MapPost("/api/auth/login", (LoginRequest request, IConfiguration config) =>
{
var expectedUser = config["AdminSettings:Username"] ?? "admin";
var expectedPass = config["AdminSettings:Password"] ?? "quant123!";
if (request.Username == expectedUser && request.Password == expectedPass)
{
return Results.Ok(new { success = true, username = request.Username });
}
return Results.Json(new { success = false, error = "invalid_credentials" }, statusCode: 401);
});
// Operational Report serving API (WASM safe file loading substitute)
app.MapGet("/api/operational-report", async (IWebHostEnvironment env) =>
{
var path = Path.GetFullPath(Path.Combine(env.ContentRootPath, "..", "..", "..", "Temp", "operational_report.json"));
if (!File.Exists(path))
{
return Results.NotFound(new { gate = "FAIL", error = "operational_report_missing" });
}
var json = await File.ReadAllTextAsync(path);
using var doc = JsonDocument.Parse(json);
return Results.Ok(doc.RootElement);
});
app.MapGet("/api/history/{domain}", async (string domain, int? limit, IPostgresqlHistorySnapshotReader reader) => app.MapGet("/api/history/{domain}", async (string domain, int? limit, IPostgresqlHistorySnapshotReader reader) =>
{ {
var rows = await reader.ReadAsync(domain, limit ?? 500); var rows = await reader.ReadAsync(domain, limit ?? 500);
@@ -144,7 +172,14 @@ app.MapPost("/api/history/{domain}", async (string domain, JsonElement payload,
}); });
app.MapRazorComponents<App>() app.MapRazorComponents<App>()
.AddInteractiveServerRenderMode(); .AddInteractiveWebAssemblyRenderMode()
.AddAdditionalAssemblies(typeof(QuantEngine.Web.Client._Imports).Assembly);
app.Run(); app.Run();
public class LoginRequest
{
public string Username { get; set; } = "";
public string Password { get; set; } = "";
}
@@ -4,12 +4,22 @@
<ProjectReference Include="..\QuantEngine.Infrastructure\QuantEngine.Infrastructure.csproj" /> <ProjectReference Include="..\QuantEngine.Infrastructure\QuantEngine.Infrastructure.csproj" />
<ProjectReference Include="..\QuantEngine.Application\QuantEngine.Application.csproj" /> <ProjectReference Include="..\QuantEngine.Application\QuantEngine.Application.csproj" />
<ProjectReference Include="..\QuantEngine.Core\QuantEngine.Core.csproj" /> <ProjectReference Include="..\QuantEngine.Core\QuantEngine.Core.csproj" />
<ProjectReference Include="Client\QuantEngine.Web.Client.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.FluentUI.AspNetCore.Components" Version="5.0.0-rc.4-26177.1" /> <PackageReference Include="Microsoft.FluentUI.AspNetCore.Components" Version="5.0.0-rc.4-26177.1" />
<PackageReference Include="Microsoft.FluentUI.AspNetCore.Components.Icons" Version="5.0.0-rc.4-26177.1" /> <PackageReference Include="Microsoft.FluentUI.AspNetCore.Components.Icons" Version="5.0.0-rc.4-26177.1" />
<PackageReference Include="Serilog.AspNetCore" Version="10.0.0" /> <PackageReference Include="Serilog.AspNetCore" Version="10.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="10.0.0-preview.2.25120.18" />
</ItemGroup>
<ItemGroup>
<!-- Exclude client project files from server build to avoid duplicate compilations -->
<Compile Remove="Client\**" />
<Content Remove="Client\**" />
<EmbeddedResource Remove="Client\**" />
<None Remove="Client\**" />
</ItemGroup> </ItemGroup>
<PropertyGroup> <PropertyGroup>
+17 -1
View File
@@ -1,4 +1,4 @@
Microsoft Visual Studio Solution File, Format Version 12.00 Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17 # Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59 VisualStudioVersion = 17.0.31903.59
@@ -15,6 +15,9 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QuantEngine.Core.Tests", "Q
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QuantEngine.Tools", "QuantEngine.Tools\QuantEngine.Tools.csproj", "{E2B1A0A1-7B13-4A4D-9B31-9C7F4F27A6A1}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QuantEngine.Tools", "QuantEngine.Tools\QuantEngine.Tools.csproj", "{E2B1A0A1-7B13-4A4D-9B31-9C7F4F27A6A1}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QuantEngine.Web.Client", "QuantEngine.Web\Client\QuantEngine.Web.Client.csproj", "{C5F2F3BD-1258-40FC-803A-EE7EEC928107}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@@ -97,8 +100,21 @@ Global
{E2B1A0A1-7B13-4A4D-9B31-9C7F4F27A6A1}.Release|x64.Build.0 = Release|Any CPU {E2B1A0A1-7B13-4A4D-9B31-9C7F4F27A6A1}.Release|x64.Build.0 = Release|Any CPU
{E2B1A0A1-7B13-4A4D-9B31-9C7F4F27A6A1}.Release|x86.ActiveCfg = Release|Any CPU {E2B1A0A1-7B13-4A4D-9B31-9C7F4F27A6A1}.Release|x86.ActiveCfg = Release|Any CPU
{E2B1A0A1-7B13-4A4D-9B31-9C7F4F27A6A1}.Release|x86.Build.0 = Release|Any CPU {E2B1A0A1-7B13-4A4D-9B31-9C7F4F27A6A1}.Release|x86.Build.0 = Release|Any CPU
{C5F2F3BD-1258-40FC-803A-EE7EEC928107}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C5F2F3BD-1258-40FC-803A-EE7EEC928107}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C5F2F3BD-1258-40FC-803A-EE7EEC928107}.Debug|x64.ActiveCfg = Debug|Any CPU
{C5F2F3BD-1258-40FC-803A-EE7EEC928107}.Debug|x64.Build.0 = Debug|Any CPU
{C5F2F3BD-1258-40FC-803A-EE7EEC928107}.Debug|x86.ActiveCfg = Debug|Any CPU
{C5F2F3BD-1258-40FC-803A-EE7EEC928107}.Debug|x86.Build.0 = Debug|Any CPU
{C5F2F3BD-1258-40FC-803A-EE7EEC928107}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C5F2F3BD-1258-40FC-803A-EE7EEC928107}.Release|Any CPU.Build.0 = Release|Any CPU
{C5F2F3BD-1258-40FC-803A-EE7EEC928107}.Release|x64.ActiveCfg = Release|Any CPU
{C5F2F3BD-1258-40FC-803A-EE7EEC928107}.Release|x64.Build.0 = Release|Any CPU
{C5F2F3BD-1258-40FC-803A-EE7EEC928107}.Release|x86.ActiveCfg = Release|Any CPU
{C5F2F3BD-1258-40FC-803A-EE7EEC928107}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
EndGlobalSection EndGlobalSection
EndGlobal EndGlobal