refactor: move buildable .NET source into src/, update CI/doc paths
TaxBaik CI/CD / build-and-deploy (push) Successful in 2m7s

Groups the repo root into src (buildable source), docs (already existed),
and everything else (db/, scripts/, tests/, deploy/ - deployment/ops/test
assets that aren't compiled, already organized as their own folders). CI
now only needs src/ to build: dotnet restore/build/test/publish all point
at src/TaxBaik.sln, src/TaxBaik.Web/, src/TaxBaik.Proxy/.

- git mv every project (Domain, Infrastructure, Application,
  Application.Tests, Web, Web.Client, Proxy) and TaxBaik.sln into src/ as a
  unit, so relative ProjectReference/.sln paths stay valid unchanged.
- .gitea/workflows/deploy.yml: 6 dotnet restore/clean/build/test/publish
  invocations now point at src/. db/migrations and scripts/ stay at root
  (deploy_gb.sh and browser-e2e.yml only touch published output and the
  deployed URL, not source paths - verified, no changes needed there).
- scripts/validate_admin_render.sh: admin render-mode file paths now
  src/TaxBaik.Web.Client/...
- scripts/validate_kst_timestamps.sh: dropped deploy.sh from its target
  list - that script was removed in the prior cleanup commit (dead, no
  CI workflow referenced it) but this validator still expected it to exist.
- CLAUDE.md, docs/ENGINEERING_HARNESS.md, docs/ADMIN_PATTERN_CRITIQUE_WBS.md:
  updated project-structure diagram, dotnet run/build commands, and grep
  targets to the new src/ paths (also fixed a pre-existing stale path in
  ADMIN_PATTERN_CRITIQUE_WBS.md that still said TaxBaik.Web/Components/Admin
  from before that ever moved to TaxBaik.Web.Client).
- Added a Repo Root harness rule + Architecture Guardrail entries: new files
  belong under src/docs/tests/scripts/db/deploy, not loose at root; temp
  work stays outside the repo (or under a gitignored .scratch/) and is
  never committed.

Verified locally: dotnet build/test src/TaxBaik.sln (26/26 tests), and all
three scripts/validate_*.sh pass against the new layout.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-03 10:37:37 +09:00
parent c00d002972
commit ea447495d3
277 changed files with 36 additions and 29 deletions
@@ -0,0 +1,219 @@
namespace TaxBaik.Web.Services;
using System.Net.Http;
using System.Net.Http.Json;
using TaxBaik.Application.DTOs;
using TaxBaik.Domain.Entities;
/// <summary>
/// Inquiry API Client for Admin Blazor
/// SOLID: Single Responsibility - Inquiry API calls only
/// Dependency Inversion - abstraction via interface
/// </summary>
public interface IInquiryBrowserClient
{
Task<(IEnumerable<Inquiry> Items, int Total)> GetPagedAsync(int page = 1, int pageSize = 20, CancellationToken ct = default);
Task<Inquiry?> GetByIdAsync(int id, CancellationToken ct = default);
Task<bool> UpdateStatusAsync(int id, string status, CancellationToken ct = default);
Task<bool> UpdateAdminMemoAsync(int id, string adminMemo, CancellationToken ct = default);
Task<Inquiry?> UpdateAsync(int id, UpdateInquiryDto dto, CancellationToken ct = default);
Task<int> ConvertToClientAsync(int id, string name, string phone, string serviceType, CancellationToken ct = default);
Task<Inquiry?> CreateAsync(SubmitInquiryDto dto, CancellationToken ct = default);
Task<bool> DeleteAsync(int id, CancellationToken ct = default);
}
public class InquiryBrowserClient : IInquiryBrowserClient
{
private readonly HttpClient _http;
private readonly ILogger<InquiryBrowserClient> _logger;
private readonly ITokenStore _tokenStore;
public InquiryBrowserClient(HttpClient http, ILogger<InquiryBrowserClient> logger, ITokenStore tokenStore)
{
_http = http;
_logger = logger;
_tokenStore = tokenStore;
}
private void EnsureAuthHeader()
{
if (!string.IsNullOrEmpty(_tokenStore.AccessToken))
_http.DefaultRequestHeaders.Authorization = new("Bearer", _tokenStore.AccessToken);
else
_http.DefaultRequestHeaders.Authorization = null;
}
public async Task<(IEnumerable<Inquiry> Items, int Total)> GetPagedAsync(
int page = 1, int pageSize = 20, CancellationToken ct = default)
{
try
{
EnsureAuthHeader();
var result = await _http.GetFromJsonAsync<InquiryPagedResponse>(
$"inquiry?page={page}&pageSize={pageSize}",
cancellationToken: ct);
return result != null
? (result.Data, result.Total)
: ([], 0);
}
catch (HttpRequestException ex)
{
_logger.LogError(ex, "Failed to fetch inquiries");
throw;
}
}
public async Task<Inquiry?> GetByIdAsync(int id, CancellationToken ct = default)
{
try
{
EnsureAuthHeader();
return await _http.GetFromJsonAsync<Inquiry>(
$"inquiry/{id}",
cancellationToken: ct);
}
catch (HttpRequestException ex)
{
_logger.LogError(ex, "Failed to fetch inquiry {InquiryId}", id);
throw;
}
}
public async Task<bool> UpdateStatusAsync(int id, string status, CancellationToken ct = default)
{
try
{
EnsureAuthHeader();
var request = new { status };
var response = await _http.PutAsJsonAsync(
$"inquiry/{id}/status",
request,
cancellationToken: ct);
return response.IsSuccessStatusCode;
}
catch (HttpRequestException ex)
{
_logger.LogError(ex, "Failed to update inquiry {InquiryId} status", id);
throw;
}
}
public async Task<bool> UpdateAdminMemoAsync(int id, string adminMemo, CancellationToken ct = default)
{
try
{
EnsureAuthHeader();
var request = new { adminMemo };
var response = await _http.PutAsJsonAsync(
$"inquiry/{id}/memo",
request,
cancellationToken: ct);
return response.IsSuccessStatusCode;
}
catch (HttpRequestException ex)
{
_logger.LogError(ex, "Failed to update inquiry {InquiryId} memo", id);
throw;
}
}
public async Task<Inquiry?> UpdateAsync(int id, UpdateInquiryDto dto, CancellationToken ct = default)
{
try
{
EnsureAuthHeader();
var response = await _http.PutAsJsonAsync($"inquiry/{id}", dto, cancellationToken: ct);
if (!response.IsSuccessStatusCode)
return null;
var content = await response.Content.ReadAsStringAsync(ct);
return System.Text.Json.JsonSerializer.Deserialize<Inquiry>(
content,
new System.Text.Json.JsonSerializerOptions { PropertyNameCaseInsensitive = true });
}
catch (HttpRequestException ex)
{
_logger.LogError(ex, "Failed to update inquiry {InquiryId}", id);
throw;
}
}
public async Task<int> ConvertToClientAsync(int id, string name, string phone, string serviceType, CancellationToken ct = default)
{
try
{
EnsureAuthHeader();
var response = await _http.PostAsJsonAsync(
$"inquiry/{id}/convert-to-client",
new { name, phone, serviceType },
cancellationToken: ct);
if (!response.IsSuccessStatusCode)
return 0;
var content = await response.Content.ReadAsStringAsync(ct);
var result = System.Text.Json.JsonSerializer.Deserialize<ConvertToClientResponse>(
content,
new System.Text.Json.JsonSerializerOptions { PropertyNameCaseInsensitive = true });
return result?.ClientId ?? 0;
}
catch (HttpRequestException ex)
{
_logger.LogError(ex, "Failed to convert inquiry {InquiryId} to client", id);
throw;
}
}
public async Task<Inquiry?> CreateAsync(SubmitInquiryDto dto, CancellationToken ct = default)
{
try
{
EnsureAuthHeader();
var response = await _http.PostAsJsonAsync("inquiry", dto, cancellationToken: ct);
if (!response.IsSuccessStatusCode)
return null;
var content = await response.Content.ReadAsStringAsync(ct);
return System.Text.Json.JsonSerializer.Deserialize<Inquiry>(
content,
new System.Text.Json.JsonSerializerOptions { PropertyNameCaseInsensitive = true });
}
catch (HttpRequestException ex)
{
_logger.LogError(ex, "Failed to create inquiry");
throw;
}
}
public async Task<bool> DeleteAsync(int id, CancellationToken ct = default)
{
try
{
EnsureAuthHeader();
var response = await _http.DeleteAsync($"inquiry/{id}", ct);
return response.IsSuccessStatusCode;
}
catch (HttpRequestException ex)
{
_logger.LogError(ex, "Failed to delete inquiry {InquiryId}", id);
throw;
}
}
private class InquiryPagedResponse
{
public List<Inquiry> Data { get; set; } = [];
public int Total { get; set; }
public int Page { get; set; }
public int PageSize { get; set; }
}
private class ConvertToClientResponse
{
public int ClientId { get; set; }
}
}