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,209 @@
@page "/admin/settings"
@rendermode @(new InteractiveWebAssemblyRenderMode(prerender: false))
@attribute [Authorize]
@using System.ComponentModel.DataAnnotations
@using System.Collections.Generic
@using TaxBaik.Web.Services
@using TaxBaik.Domain.Interfaces
@inject IApiClient ApiClient
@inject ISnackbar Snackbar
<PageTitle>설정</PageTitle>
<MudContainer MaxWidth="MaxWidth.Large" Class="pa-6">
<section class="admin-page-hero">
<div>
<MudText Typo="Typo.caption" Class="admin-eyebrow">System</MudText>
<MudText Typo="Typo.h4" Class="admin-page-title">설정</MudText>
<MudText Typo="Typo.body2" Class="admin-page-subtitle">공개 사이트 연락처와 관리자 계정 보안을 관리합니다.</MudText>
</div>
</section>
</MudContainer>
<MudGrid>
<MudItem xs="12" md="7">
<MudPaper Class="admin-surface" Elevation="0">
<div class="admin-section-header compact">
<div>
<MudText Typo="Typo.h6">사이트 정보</MudText>
<MudText Typo="Typo.body2">홈페이지와 문의 알림에 노출되는 기본 정보입니다.</MudText>
</div>
</div>
<MudForm>
<MudTextField @bind-Value="phone" Label="전화번호"
Variant="Variant.Outlined" Class="mb-4" />
<MudTextField @bind-Value="email" Label="이메일"
Variant="Variant.Outlined" Class="mb-4" />
<MudTextField @bind-Value="kakaoUrl" Label="카카오채널 URL"
Variant="Variant.Outlined" Class="mb-4" />
<MudTextField @bind-Value="instagramUrl" Label="인스타그램"
Variant="Variant.Outlined" Class="mb-4" />
<MudButton Variant="Variant.Filled" Color="Color.Primary"
StartIcon="@Icons.Material.Filled.Save"
@onclick="SaveSettings">사이트 정보 저장</MudButton>
</MudForm>
</MudPaper>
</MudItem>
<MudItem xs="12" md="5">
<MudPaper Class="admin-surface admin-account-card" Elevation="0">
<div class="admin-section-header compact">
<div>
<MudText Typo="Typo.h6">계정 관리</MudText>
<MudText Typo="Typo.body2">비밀번호는 12자 이상으로 관리합니다.</MudText>
</div>
</div>
<MudForm>
<MudTextField @bind-Value="currentPassword" Label="현재 비밀번호" InputType="InputType.Password"
Variant="Variant.Outlined" Class="mb-4" />
<MudTextField @bind-Value="newPassword" Label="새 비밀번호" InputType="InputType.Password"
Variant="Variant.Outlined" Class="mb-4" />
<MudTextField @bind-Value="confirmNewPassword" Label="새 비밀번호 확인" InputType="InputType.Password"
Variant="Variant.Outlined" Class="mb-4" />
<MudButton Variant="Variant.Filled" Color="Color.Primary"
Disabled="@isChangingPassword"
StartIcon="@Icons.Material.Filled.LockReset"
@onclick="ChangePassword">
@(isChangingPassword ? "변경 중..." : "비밀번호 변경")
</MudButton>
</MudForm>
</MudPaper>
</MudItem>
</MudGrid>
@code {
private string phone = "010-4122-8268";
private string email = "taxbaik5668@gmail.com";
private string kakaoUrl = "http://pf.kakao.com/_xoxchTX";
private string instagramUrl = "https://www.instagram.com/taxtory5668/";
private string currentPassword = "";
private string newPassword = "";
private string confirmNewPassword = "";
private bool isChangingPassword;
private bool isLoadingSettings;
protected override async Task OnInitializedAsync()
{
await LoadSettingsAsync();
}
private async Task LoadSettingsAsync()
{
isLoadingSettings = true;
try
{
var settings = await ApiClient.GetAsync<Dictionary<string, string>>("site-settings");
if (settings is null || settings.Count == 0)
return;
if (settings.TryGetValue("PhoneNumber", out var loadedPhone) && !string.IsNullOrWhiteSpace(loadedPhone))
phone = loadedPhone;
if (settings.TryGetValue("EmailAddress", out var loadedEmail) && !string.IsNullOrWhiteSpace(loadedEmail))
email = loadedEmail;
if (settings.TryGetValue("KakaoChannelUrl", out var loadedKakao) && !string.IsNullOrWhiteSpace(loadedKakao))
kakaoUrl = loadedKakao;
if (settings.TryGetValue("InstagramUrl", out var loadedInstagram) && !string.IsNullOrWhiteSpace(loadedInstagram))
instagramUrl = loadedInstagram;
}
catch
{
Snackbar.Add("사이트 설정을 불러오지 못했습니다.", Severity.Warning);
}
finally
{
isLoadingSettings = false;
}
}
private async Task SaveSettings()
{
if (isLoadingSettings)
return;
var response = await ApiClient.PutAsync<SaveSettingsResponse>("site-settings", new
{
Phone = phone,
Email = email,
KakaoUrl = kakaoUrl,
InstagramUrl = instagramUrl
});
if (response?.Message is null)
{
Snackbar.Add("설정 저장에 실패했습니다.", Severity.Error);
return;
}
Snackbar.Add(response.Message, Severity.Success);
}
private async Task ChangePassword()
{
if (isChangingPassword)
return;
if (string.IsNullOrWhiteSpace(currentPassword) || string.IsNullOrWhiteSpace(newPassword))
{
Snackbar.Add("현재 비밀번호와 새 비밀번호를 입력하세요.", Severity.Warning);
return;
}
if (newPassword != confirmNewPassword)
{
Snackbar.Add("새 비밀번호 확인이 일치하지 않습니다.", Severity.Warning);
return;
}
isChangingPassword = true;
try
{
var response = await ApiClient.PostAsync<ChangePasswordResponse>("auth/change-password", new
{
CurrentPassword = currentPassword,
NewPassword = newPassword
});
if (response?.Message == null)
{
Snackbar.Add("비밀번호 변경에 실패했습니다.", Severity.Error);
return;
}
Snackbar.Add(response.Message, Severity.Success);
currentPassword = "";
newPassword = "";
confirmNewPassword = "";
}
catch
{
Snackbar.Add("비밀번호 변경 중 오류가 발생했습니다.", Severity.Error);
}
finally
{
isChangingPassword = false;
}
}
private class ChangePasswordResponse
{
public string Message { get; set; } = "";
}
private class SaveSettingsResponse
{
public string Message { get; set; } = "";
}
}