Files
taxbaik/TaxBaik.Web/Program.cs
T
kjh2064 160afb7c7e
TaxBaik CI/CD / build-and-deploy (push) Successful in 49s
refactor: Phase 7-2 Complete - Full Inquiry page API-First migration
**Blockers Fixed:**

1. InquiryBrowserClient URL hardcoding
   - Removed: \"http://localhost:5001\" hardcoded in each method
   - Added: Configured BaseAddress in Program.cs
   - Now uses: Relative paths (\"inquiry\", \"inquiry/{id}\", etc)
   - HttpClientFactory pipeline includes TokenRefreshHandler

2. Missing API endpoints in InquiryController
   - Added: PUT /api/inquiry/{id}/memo
   - Added: POST /api/inquiry/{id}/convert-to-client
   - Request DTOs: UpdateAdminMemoRequest, ConvertToClientRequest
   - ClientService injected (for client creation)

**Implementation:**

- InquiryBrowserClient: Extended interface
   * UpdateAdminMemoAsync(id, memo)
   * ConvertToClientAsync(id, name, phone, serviceType)
   * All methods use relative paths

- InquiryBrowserClient.ConvertToClientResponse
   * Deserialize API response to extract clientId

- InquiryDetail.razor: Full refactor
   * Before: @inject InquiryService, ClientService (direct service calls)
   * After: @inject IInquiryBrowserClient (API-only)
   * OnInitializedAsync: InquiryClient.GetByIdAsync
   * OnStatusChanged: InquiryClient.UpdateStatusAsync
   * SaveMemo: InquiryClient.UpdateAdminMemoAsync
   * ConvertToClient: InquiryClient.ConvertToClientAsync

**InquiryList.razor status:**
   * Also still injects IInquiryRepository (line 4)
   * Consider refactoring to use IInquiryBrowserClient for consistency

**Phase 7 Status:**
-  Blog page: Already API-First (ApiClient)
-  Inquiry page: Fully API-First (IInquiryBrowserClient)
  * InquiryTable:  Migrated
  * InquiryDetail:  Migrated
  * InquiryList:  Still uses IInquiryRepository (minor - reads only)

**SOLID Applied:**
✓ S: InquiryBrowserClient single responsibility
✓ D: Blazor → IInquiryBrowserClient (not ServiceLayer)
✓ O: Client can change without Blazor impact

Next: Check FAQ, Client, TaxFiling pages for same pattern.
If all still injecting services directly, migrate sequentially.
Then: Phase 6 (SignalR) will have all pages ready.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-28 10:59:02 +09:00

169 lines
5.9 KiB
C#

using System.IO.Compression;
using System.Text;
using System.Text.Encodings.Web;
using System.Text.Unicode;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.AspNetCore.ResponseCompression;
using Microsoft.IdentityModel.Tokens;
using MudBlazor.Services;
using TaxBaik.Application;
using TaxBaik.Application.Services;
using TaxBaik.Infrastructure;
using TaxBaik.Web.Services;
var builder = WebApplication.CreateBuilder(args);
var isProduction = builder.Environment.IsProduction();
// Controllers (API)
builder.Services.AddControllers();
builder.Services.AddProblemDetails();
builder.Services.AddHealthChecks();
// Razor Pages + Blazor Server 통합
builder.Services.AddRazorPages();
builder.Services.AddRazorComponents().AddInteractiveServerComponents();
builder.Services.Configure<Microsoft.AspNetCore.Components.Server.CircuitOptions>(options =>
{
options.DetailedErrors = true;
});
// JWT 인증
var connectionString = builder.Configuration.GetConnectionString("Default")
?? throw new InvalidOperationException("Missing connection string");
var jwtKey = builder.Configuration["Jwt:SecretKey"] ?? throw new InvalidOperationException("Missing JWT SecretKey");
if (isProduction && jwtKey.Contains("dev-secret", StringComparison.OrdinalIgnoreCase))
throw new InvalidOperationException("Production JWT SecretKey must not use the development default.");
var key = Encoding.ASCII.GetBytes(jwtKey);
builder.Services.AddAuthentication(opts =>
{
opts.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
opts.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(opts =>
{
opts.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuer = true,
ValidIssuer = "taxbaik-admin",
ValidateAudience = true,
ValidAudience = "taxbaik-admin-client",
ValidateLifetime = true,
ClockSkew = TimeSpan.FromMinutes(1)
};
});
// Blazor 인증
builder.Services.AddScoped<AuthService>();
builder.Services.AddScoped<CustomAuthenticationStateProvider>();
builder.Services.AddScoped<AuthenticationStateProvider>(sp => sp.GetRequiredService<CustomAuthenticationStateProvider>());
builder.Services.AddScoped<ILocalStorageService, LocalStorageService>();
builder.Services.AddCascadingAuthenticationState();
builder.Services.AddAuthorization();
builder.Services.AddAuthorizationCore();
// HTTP Client for API (with automatic token refresh)
builder.Services.AddScoped<ITokenStore, TokenStore>();
builder.Services.AddScoped<TokenRefreshHandler>();
builder.Services.AddHttpClient<IApiClient, ApiClient>();
builder.Services.AddHttpClient<IAdminDashboardClient, AdminDashboardClient>(client =>
{
client.BaseAddress = new Uri("http://localhost:5001/taxbaik/api/");
})
.AddHttpMessageHandler<TokenRefreshHandler>();
builder.Services.AddHttpClient<IInquiryBrowserClient, InquiryBrowserClient>(client =>
{
client.BaseAddress = new Uri("http://localhost:5001/taxbaik/api/");
})
.AddHttpMessageHandler<TokenRefreshHandler>();
// UI & 캐시
builder.Services.AddMudServices();
builder.Services.AddMemoryCache();
builder.Services.AddResponseCompression(opts => {
opts.Providers.Add<GzipCompressionProvider>();
});
builder.Services.AddScoped<IInquiryNotificationService, TelegramInquiryNotificationService>();
// 한글 포함 다국어 문자를 유니코드 엔티티로 변환하지 않도록 설정
builder.Services.AddSingleton(HtmlEncoder.Create(UnicodeRanges.All));
builder.Services.AddInfrastructure();
builder.Services.AddApplication();
// Register version info
var versionInfo = new VersionInfo();
var versionJsonPath = Path.Combine(AppContext.BaseDirectory, "wwwroot", "version.json");
if (File.Exists(versionJsonPath))
{
try
{
var json = System.Text.Json.JsonDocument.Parse(File.ReadAllText(versionJsonPath));
var root = json.RootElement;
if (root.TryGetProperty("version", out var versionProp))
versionInfo.Version = versionProp.GetString() ?? "unknown";
if (root.TryGetProperty("built", out var builtProp))
versionInfo.Built = builtProp.GetString() ?? "unknown";
}
catch (Exception ex)
{
Console.WriteLine($"Warning: Failed to parse version.json: {ex.Message}");
}
}
builder.Services.AddSingleton(versionInfo);
var app = builder.Build();
app.UseForwardedHeaders(new ForwardedHeadersOptions
{
ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto
});
// Run migrations on startup (non-blocking for development)
try
{
using (var scope = app.Services.CreateScope())
{
var connectionFactory = scope.ServiceProvider.GetRequiredService<TaxBaik.Domain.Interfaces.IDbConnectionFactory>();
var migrationRunner = new TaxBaik.Infrastructure.Data.MigrationRunner(connectionString, connectionFactory);
await migrationRunner.RunAsync();
}
}
catch (Exception ex)
{
if (!app.Environment.IsDevelopment())
throw;
Console.WriteLine($"Migration warning (development only): {ex.Message}");
}
app.UsePathBase("/taxbaik");
app.UseResponseCompression();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseAntiforgery();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
// API + Razor Pages + Blazor 매핑
app.MapControllers();
app.MapHealthChecks("/healthz");
app.MapRazorPages();
// AllowAnonymous: JWT 미들웨어가 Blazor 셸 요청을 401로 차단하지 않도록 한다.
// 인증은 Blazor AuthorizeRouteView → RedirectToLogin 에서 처리한다.
app.MapRazorComponents<TaxBaik.Web.Components.Admin.App>()
.AddInteractiveServerRenderMode()
.AllowAnonymous();
app.Run();