fix: per-page WASM render mode, Contact checkbox binding, Telegram inquiry channel
TaxBaik CI/CD / build-and-deploy (push) Successful in 2m11s
TaxBaik CI/CD / build-and-deploy (push) Successful in 2m11s
- Admin: replace the global @rendermode on <Routes>/<Router> with per-page render mode. Login.razor now prerenders (form visible before WASM loads); every other [Authorize] page stays prerender: false to avoid the AuthorizeRouteView blank-render regression from earlier attempts. Adds a "준비 중" -> "로그인" splash tied to WASM boot completion, and lets the authenticated-shell loading overlay stay up until AdminShell actually renders. - Contact.cshtml: fix the "Agree" checkbox missing value="true" - a checked box sent the browser-default "on", which bool model binding can't parse, so ModelState.IsValid silently went false and OnPostAsync returned a blank form with no visible error on every submission. Validation summary widened from ModelOnly to All so this class of failure isn't silent again. - TelegramInquiryNotificationService: read Telegram:InquiryChatId (falling back to ChatId) instead of only ChatId, matching the channel routing CLAUDE.md documents and deploy.yml already provisions as separate secrets. - Reconcile CLAUDE.md's self-contradicting Phase 8 prerender notes (Phase 9), rewrite validate_admin_render.sh for the per-page design, and add a SmartAdmin 5.5 design reference section to DOUZONE_UX_GUIDE.md for future admin screens (existing screens unchanged, tracked as WBS P4-03). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -134,7 +134,24 @@ export DOTNET_PRINT_TELEMETRY_MESSAGE=false
|
|||||||
|
|
||||||
**완료**: 2026-07-03 / WebAssembly 기반 아키텍처 확정 + 프로덕션 검증
|
**완료**: 2026-07-03 / WebAssembly 기반 아키텍처 확정 + 프로덕션 검증
|
||||||
|
|
||||||
**현재 상태**: **✅ Phase 1-8 COMPLETE & VERIFIED (2026-07-03)**
|
**⚠️ Phase 8 알려진 한계 (Phase 9에서 수정됨)**:
|
||||||
|
Phase 8에서는 `<Routes>`(App.razor)와 `<Router>`(Routes.razor)에 전역 `@rendermode`를 지정해 `prerender: false`로 고정했다. 그 결과 로그인 화면을 포함한 모든 어드민 페이지가 WASM 다운로드 완료 전까지 빈 화면/스피너만 보여주는 문제가 있었다(`scripts/validate_admin_render.sh`에 이 트레이드오프가 "기능 우선, 흰 화면 0.5~2초 감수"로 기록되어 있었음). 이는 `docs/ENGINEERING_HARNESS.md`의 "로그인 화면은 예외적으로 서버 프리렌더 허용" 규칙을 충족하지 못한 상태였다. Phase 9에서 페이지별 개별 렌더모드 지정으로 교체했다.
|
||||||
|
|
||||||
|
#### Phase 9: 어드민 페이지별 렌더모드 정상화 ✅ (2026-07-03)
|
||||||
|
- [x] `App.razor`/`Routes.razor`에서 전역 `@rendermode` 제거 (Router/Routes 자체는 렌더모드를 강제하지 않음)
|
||||||
|
- [x] `Login.razor`만 `@rendermode @(new InteractiveWebAssemblyRenderMode(prerender: true))`로 명시 → 로그인 폼이 최초 HTML 응답에 정적으로 포함되어 WASM 다운로드 중에도 즉시 표시됨
|
||||||
|
- [x] 나머지 `[Authorize]` 어드민 페이지는 `@rendermode @(new InteractiveWebAssemblyRenderMode(prerender: false))`로 명시 유지 → 인증 컨텍스트 없이 prerender될 때 `AuthorizeRouteView`가 빈 화면을 그리는 문제(Phase 8 초기에 겪었던 문제) 재발 방지
|
||||||
|
- [x] WASM 부팅 완료 전 로그인 버튼은 "준비 중" 비활성 상태로 표시, 부팅 완료 시 정상 상태로 전환(업데이트 스플래시)
|
||||||
|
|
||||||
|
**핵심 원칙**: Blazor Web App은 "전역 렌더모드" 또는 "페이지별 렌더모드" 중 하나만 선택할 수 있다. Router/Routes에 렌더모드를 지정하면 그 하위 모든 페이지의 개별 `@rendermode` 지시자는 무시된다. 로그인만 예외적으로 prerender가 필요하므로 전역 방식을 버리고 페이지별 방식으로 전환했다.
|
||||||
|
|
||||||
|
**완료**: 2026-07-03 / 로그인 흰 화면 제거 + 인증 페이지 안정성 유지
|
||||||
|
|
||||||
|
**보류된 결정 (2026-07-03, 향후 별도 Phase)**:
|
||||||
|
- 공개 홈페이지 Razor Pages → MVC(Controller+View) 전면 재작성: 기능적 이득 없이 운영 중인 SEO 트래픽 페이지 전체를 기계적으로 재작성하는 고비용 작업이라 이번엔 보류. 필요 시 Phase 10으로 별도 진행.
|
||||||
|
- 포털(고객용, `Pages/Portal/*`, 현재 Razor Pages + 쿠키/OAuth) → 어드민과 동일한 MudBlazor+WASM 전환: 완전히 새로운 프로젝트 구조가 필요해 이번 범위에서 제외. 필요 시 Phase 11로 별도 진행.
|
||||||
|
|
||||||
|
**현재 상태**: **✅ Phase 1-9 COMPLETE & VERIFIED (2026-07-03)**
|
||||||
- ✅ 모든 API 엔드포인트 구현됨
|
- ✅ 모든 API 엔드포인트 구현됨
|
||||||
- ✅ 모든 Browser Client 구현됨
|
- ✅ 모든 Browser Client 구현됨
|
||||||
- ✅ 16개 Blazor 페이지 API-First 마이그레이션 완료
|
- ✅ 16개 Blazor 페이지 API-First 마이그레이션 완료
|
||||||
@@ -2035,7 +2052,7 @@ else
|
|||||||
|
|
||||||
| 항목 | 이전 | 현재 | 개선 |
|
| 항목 | 이전 | 현재 | 개선 |
|
||||||
|------|------|------|------|
|
|------|------|------|------|
|
||||||
| **Blazor 프리렌더링** | `prerender: false` | `prerender: true` | 흰 화면 제거 |
|
| **Blazor 프리렌더링** | 전역 `prerender: false` (로그인 포함 전체 흰 화면) | 페이지별 지정 (로그인만 `prerender: true`, 나머지 `false`) | 로그인 흰 화면 제거, 인증 페이지는 그대로 안정 |
|
||||||
| **배포 헬스 체크** | 40 × 3초 = 120초 | 20 × 3초 = 60초 | -50% |
|
| **배포 헬스 체크** | 40 × 3초 = 120초 | 20 × 3초 = 60초 | -50% |
|
||||||
| **E2E 배포 대기** | 30 × 5초 = 150초 | 20 × 3초 = 60초 | -60% |
|
| **E2E 배포 대기** | 30 × 5초 = 150초 | 20 × 3초 = 60초 | -60% |
|
||||||
| **Playwright 병렬** | `fullyParallel: false` | CI에서 `true` | 테스트 병렬화 |
|
| **Playwright 병렬** | `fullyParallel: false` | CI에서 `true` | 테스트 병렬화 |
|
||||||
|
|||||||
@@ -35,8 +35,17 @@
|
|||||||
<p>로드 중...</p>
|
<p>로드 중...</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<script>
|
||||||
|
// 로그인 화면은 prerender로 즉시 표시되므로 스피너가 필요 없다.
|
||||||
|
// 그 외 인증 화면은 WASM 부팅이 끝날 때까지(AdminShell.OnAfterRenderAsync에서 hideLoading 호출)
|
||||||
|
// 스피너를 "업데이트 스플래시"로 보여준다.
|
||||||
|
if (!document.documentElement.classList.contains('admin-login-route')) {
|
||||||
|
var loadingOverlay = document.getElementById('blazor-loading');
|
||||||
|
if (loadingOverlay) loadingOverlay.classList.add('show');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
<MudThemeProvider @bind-IsDarkMode="isDarkMode" Theme="mudTheme" />
|
<MudThemeProvider @bind-IsDarkMode="isDarkMode" Theme="mudTheme" />
|
||||||
<Routes @rendermode="new InteractiveWebAssemblyRenderMode(prerender: false)" />
|
<Routes />
|
||||||
<script src="/taxbaik/_content/MudBlazor/MudBlazor.min.js"></script>
|
<script src="/taxbaik/_content/MudBlazor/MudBlazor.min.js"></script>
|
||||||
<script src="/taxbaik/js/admin-session.js"></script>
|
<script src="/taxbaik/js/admin-session.js"></script>
|
||||||
<script src="/taxbaik/_framework/blazor.web.js"></script>
|
<script src="/taxbaik/_framework/blazor.web.js"></script>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
@page "/admin"
|
@page "/admin"
|
||||||
@rendermode InteractiveWebAssembly
|
@rendermode @(new InteractiveWebAssemblyRenderMode(prerender: false))
|
||||||
@attribute [Authorize]
|
@attribute [Authorize]
|
||||||
@inject NavigationManager NavigationManager
|
@inject NavigationManager NavigationManager
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
@page "/admin/announcements/create"
|
@page "/admin/announcements/create"
|
||||||
@page "/admin/announcements/{Id:int}/edit"
|
@page "/admin/announcements/{Id:int}/edit"
|
||||||
@rendermode InteractiveWebAssembly
|
@rendermode @(new InteractiveWebAssemblyRenderMode(prerender: false))
|
||||||
@attribute [Authorize]
|
@attribute [Authorize]
|
||||||
@using TaxBaik.Application.DTOs
|
@using TaxBaik.Application.DTOs
|
||||||
@using TaxBaik.Web.Services
|
@using TaxBaik.Web.Services
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
@page "/admin/announcements"
|
@page "/admin/announcements"
|
||||||
@rendermode InteractiveWebAssembly
|
@rendermode @(new InteractiveWebAssemblyRenderMode(prerender: false))
|
||||||
@attribute [Authorize]
|
@attribute [Authorize]
|
||||||
@using TaxBaik.Web.Services
|
@using TaxBaik.Web.Services
|
||||||
@using TaxBaik.Domain.Entities
|
@using TaxBaik.Domain.Entities
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
@page "/admin/blog/create"
|
@page "/admin/blog/create"
|
||||||
@rendermode InteractiveWebAssembly
|
@rendermode @(new InteractiveWebAssemblyRenderMode(prerender: false))
|
||||||
@attribute [Authorize]
|
@attribute [Authorize]
|
||||||
@using TaxBaik.Application.DTOs
|
@using TaxBaik.Application.DTOs
|
||||||
@using TaxBaik.WasmClient.Components.Admin.Pages.Blog
|
@using TaxBaik.WasmClient.Components.Admin.Pages.Blog
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
@page "/admin/blog/{id:int}/edit"
|
@page "/admin/blog/{id:int}/edit"
|
||||||
@rendermode InteractiveWebAssembly
|
@rendermode @(new InteractiveWebAssemblyRenderMode(prerender: false))
|
||||||
@attribute [Authorize]
|
@attribute [Authorize]
|
||||||
@using TaxBaik.Application.DTOs
|
@using TaxBaik.Application.DTOs
|
||||||
@using TaxBaik.WasmClient.Components.Admin.Pages.Blog
|
@using TaxBaik.WasmClient.Components.Admin.Pages.Blog
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
@page "/admin/blog"
|
@page "/admin/blog"
|
||||||
@rendermode InteractiveWebAssembly
|
@rendermode @(new InteractiveWebAssemblyRenderMode(prerender: false))
|
||||||
@attribute [Authorize]
|
@attribute [Authorize]
|
||||||
@inject IBlogBrowserClient BlogClient
|
@inject IBlogBrowserClient BlogClient
|
||||||
@inject ISnackbar Snackbar
|
@inject ISnackbar Snackbar
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
@page "/admin/clients/{ClientId:int}"
|
@page "/admin/clients/{ClientId:int}"
|
||||||
@rendermode InteractiveWebAssembly
|
@rendermode @(new InteractiveWebAssemblyRenderMode(prerender: false))
|
||||||
@attribute [Authorize]
|
@attribute [Authorize]
|
||||||
@using TaxBaik.Web.Services
|
@using TaxBaik.Web.Services
|
||||||
@using TaxBaik.Web.Services.AdminClients
|
@using TaxBaik.Web.Services.AdminClients
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
@page "/admin/clients/create"
|
@page "/admin/clients/create"
|
||||||
@page "/admin/clients/{Id:int}/edit"
|
@page "/admin/clients/{Id:int}/edit"
|
||||||
@rendermode InteractiveWebAssembly
|
@rendermode @(new InteractiveWebAssemblyRenderMode(prerender: false))
|
||||||
@attribute [Authorize]
|
@attribute [Authorize]
|
||||||
@using TaxBaik.Application.DTOs
|
@using TaxBaik.Application.DTOs
|
||||||
@using TaxBaik.Web.Services
|
@using TaxBaik.Web.Services
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
@page "/admin/clients"
|
@page "/admin/clients"
|
||||||
@rendermode InteractiveWebAssembly
|
@rendermode @(new InteractiveWebAssemblyRenderMode(prerender: false))
|
||||||
@attribute [Authorize]
|
@attribute [Authorize]
|
||||||
@using TaxBaik.Web.Services
|
@using TaxBaik.Web.Services
|
||||||
@using TaxBaik.Domain.Entities
|
@using TaxBaik.Domain.Entities
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
@page "/admin/common-codes"
|
@page "/admin/common-codes"
|
||||||
@rendermode InteractiveWebAssembly
|
@rendermode @(new InteractiveWebAssemblyRenderMode(prerender: false))
|
||||||
@using TaxBaik.Web.Services.AdminClients
|
@using TaxBaik.Web.Services.AdminClients
|
||||||
@using TaxBaik.Domain.Entities
|
@using TaxBaik.Domain.Entities
|
||||||
@attribute [Authorize]
|
@attribute [Authorize]
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
@page "/admin/companies/create"
|
@page "/admin/companies/create"
|
||||||
@rendermode InteractiveWebAssembly
|
@rendermode @(new InteractiveWebAssemblyRenderMode(prerender: false))
|
||||||
@attribute [Authorize]
|
@attribute [Authorize]
|
||||||
@using TaxBaik.WasmClient.Components.Admin.Forms
|
@using TaxBaik.WasmClient.Components.Admin.Forms
|
||||||
@inject IApiClient ApiClient
|
@inject IApiClient ApiClient
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
@page "/admin/companies/{id:int}/edit"
|
@page "/admin/companies/{id:int}/edit"
|
||||||
@rendermode InteractiveWebAssembly
|
@rendermode @(new InteractiveWebAssemblyRenderMode(prerender: false))
|
||||||
@attribute [Authorize]
|
@attribute [Authorize]
|
||||||
@using TaxBaik.WasmClient.Components.Admin.Forms
|
@using TaxBaik.WasmClient.Components.Admin.Forms
|
||||||
@inject IApiClient ApiClient
|
@inject IApiClient ApiClient
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
@page "/admin/companies"
|
@page "/admin/companies"
|
||||||
@rendermode InteractiveWebAssembly
|
@rendermode @(new InteractiveWebAssemblyRenderMode(prerender: false))
|
||||||
@attribute [Authorize]
|
@attribute [Authorize]
|
||||||
@inject IApiClient ApiClient
|
@inject IApiClient ApiClient
|
||||||
@inject ISnackbar Snackbar
|
@inject ISnackbar Snackbar
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
@page "/admin/consulting-activities"
|
@page "/admin/consulting-activities"
|
||||||
@rendermode InteractiveWebAssembly
|
@rendermode @(new InteractiveWebAssemblyRenderMode(prerender: false))
|
||||||
@using TaxBaik.Web.Services.AdminClients
|
@using TaxBaik.Web.Services.AdminClients
|
||||||
@using TaxBaik.WasmClient.Components.Admin.Shared
|
@using TaxBaik.WasmClient.Components.Admin.Shared
|
||||||
@inject IConsultingActivityBrowserClient ActivityClient
|
@inject IConsultingActivityBrowserClient ActivityClient
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
@page "/admin/contracts"
|
@page "/admin/contracts"
|
||||||
@rendermode InteractiveWebAssembly
|
@rendermode @(new InteractiveWebAssemblyRenderMode(prerender: false))
|
||||||
@using TaxBaik.Web.Services.AdminClients
|
@using TaxBaik.Web.Services.AdminClients
|
||||||
@using TaxBaik.WasmClient.Components.Admin.Shared
|
@using TaxBaik.WasmClient.Components.Admin.Shared
|
||||||
@inject IContractBrowserClient ContractClient
|
@inject IContractBrowserClient ContractClient
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
@page "/admin/dashboard"
|
@page "/admin/dashboard"
|
||||||
@rendermode InteractiveWebAssembly
|
@rendermode @(new InteractiveWebAssemblyRenderMode(prerender: false))
|
||||||
@attribute [Authorize]
|
@attribute [Authorize]
|
||||||
@using TaxBaik.Web.Services
|
@using TaxBaik.Web.Services
|
||||||
@using TaxBaik.WasmClient.Components.Admin.Shared
|
@using TaxBaik.WasmClient.Components.Admin.Shared
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
@page "/admin/faqs/create"
|
@page "/admin/faqs/create"
|
||||||
@page "/admin/faqs/{Id:int}/edit"
|
@page "/admin/faqs/{Id:int}/edit"
|
||||||
@rendermode InteractiveWebAssembly
|
@rendermode @(new InteractiveWebAssemblyRenderMode(prerender: false))
|
||||||
@attribute [Authorize]
|
@attribute [Authorize]
|
||||||
@using TaxBaik.Web.Services
|
@using TaxBaik.Web.Services
|
||||||
@using TaxBaik.Domain.Entities
|
@using TaxBaik.Domain.Entities
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
@page "/admin/faqs"
|
@page "/admin/faqs"
|
||||||
@rendermode InteractiveWebAssembly
|
@rendermode @(new InteractiveWebAssemblyRenderMode(prerender: false))
|
||||||
@attribute [Authorize]
|
@attribute [Authorize]
|
||||||
@using TaxBaik.Web.Services
|
@using TaxBaik.Web.Services
|
||||||
@using TaxBaik.Domain.Entities
|
@using TaxBaik.Domain.Entities
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
@page "/admin/inquiries/create"
|
@page "/admin/inquiries/create"
|
||||||
@rendermode InteractiveWebAssembly
|
@rendermode @(new InteractiveWebAssemblyRenderMode(prerender: false))
|
||||||
@attribute [Authorize]
|
@attribute [Authorize]
|
||||||
@using TaxBaik.Application.DTOs
|
@using TaxBaik.Application.DTOs
|
||||||
@using TaxBaik.WasmClient.Components.Admin.Forms
|
@using TaxBaik.WasmClient.Components.Admin.Forms
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
@page "/admin/inquiries/{InquiryId:int}"
|
@page "/admin/inquiries/{InquiryId:int}"
|
||||||
@rendermode InteractiveWebAssembly
|
@rendermode @(new InteractiveWebAssemblyRenderMode(prerender: false))
|
||||||
@attribute [Authorize]
|
@attribute [Authorize]
|
||||||
@using TaxBaik.Web.Services
|
@using TaxBaik.Web.Services
|
||||||
@inject IInquiryBrowserClient InquiryClient
|
@inject IInquiryBrowserClient InquiryClient
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
@page "/admin/inquiries/{id:int}/edit"
|
@page "/admin/inquiries/{id:int}/edit"
|
||||||
@rendermode InteractiveWebAssembly
|
@rendermode @(new InteractiveWebAssemblyRenderMode(prerender: false))
|
||||||
@attribute [Authorize]
|
@attribute [Authorize]
|
||||||
@using TaxBaik.Application.DTOs
|
@using TaxBaik.Application.DTOs
|
||||||
@using TaxBaik.WasmClient.Components.Admin.Forms
|
@using TaxBaik.WasmClient.Components.Admin.Forms
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
@page "/admin/inquiries"
|
@page "/admin/inquiries"
|
||||||
@rendermode InteractiveWebAssembly
|
@rendermode @(new InteractiveWebAssemblyRenderMode(prerender: false))
|
||||||
@attribute [Authorize]
|
@attribute [Authorize]
|
||||||
@using TaxBaik.Web.Services
|
@using TaxBaik.Web.Services
|
||||||
@inject IInquiryBrowserClient InquiryClient
|
@inject IInquiryBrowserClient InquiryClient
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
@page "/admin/login"
|
@page "/admin/login"
|
||||||
@layout TaxBaik.WasmClient.Components.Admin.Layout.BlankLayout
|
@layout TaxBaik.WasmClient.Components.Admin.Layout.BlankLayout
|
||||||
@attribute [AllowAnonymous]
|
@attribute [AllowAnonymous]
|
||||||
@rendermode InteractiveWebAssembly
|
@rendermode @(new InteractiveWebAssemblyRenderMode(prerender: true))
|
||||||
<PageTitle>로그인</PageTitle>
|
<PageTitle>로그인</PageTitle>
|
||||||
<AdminLoginForm />
|
<AdminLoginForm />
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
@page "/admin/logout"
|
@page "/admin/logout"
|
||||||
@rendermode InteractiveWebAssembly
|
@rendermode @(new InteractiveWebAssemblyRenderMode(prerender: false))
|
||||||
@using TaxBaik.Web.Services
|
@using TaxBaik.Web.Services
|
||||||
@inject CustomAuthenticationStateProvider AuthStateProvider
|
@inject CustomAuthenticationStateProvider AuthStateProvider
|
||||||
@inject NavigationManager NavigationManager
|
@inject NavigationManager NavigationManager
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
@page "/admin/revenue-trackings"
|
@page "/admin/revenue-trackings"
|
||||||
@rendermode InteractiveWebAssembly
|
@rendermode @(new InteractiveWebAssemblyRenderMode(prerender: false))
|
||||||
@using TaxBaik.Web.Services.AdminClients
|
@using TaxBaik.Web.Services.AdminClients
|
||||||
@using TaxBaik.WasmClient.Components.Admin.Shared
|
@using TaxBaik.WasmClient.Components.Admin.Shared
|
||||||
@inject IRevenueTrackingBrowserClient RevenueClient
|
@inject IRevenueTrackingBrowserClient RevenueClient
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
@page "/admin/season-simulator"
|
@page "/admin/season-simulator"
|
||||||
@rendermode InteractiveWebAssembly
|
@rendermode @(new InteractiveWebAssemblyRenderMode(prerender: false))
|
||||||
@attribute [Authorize]
|
@attribute [Authorize]
|
||||||
@using TaxBaik.Application.Seasonal
|
@using TaxBaik.Application.Seasonal
|
||||||
@using TaxBaik.Application.Services
|
@using TaxBaik.Application.Services
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
@page "/admin/settings"
|
@page "/admin/settings"
|
||||||
@rendermode InteractiveWebAssembly
|
@rendermode @(new InteractiveWebAssemblyRenderMode(prerender: false))
|
||||||
@attribute [Authorize]
|
@attribute [Authorize]
|
||||||
@using System.ComponentModel.DataAnnotations
|
@using System.ComponentModel.DataAnnotations
|
||||||
@using System.Collections.Generic
|
@using System.Collections.Generic
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
@page "/admin/tax-filing-schedules"
|
@page "/admin/tax-filing-schedules"
|
||||||
@rendermode InteractiveWebAssembly
|
@rendermode @(new InteractiveWebAssemblyRenderMode(prerender: false))
|
||||||
@using TaxBaik.Web.Services.AdminClients
|
@using TaxBaik.Web.Services.AdminClients
|
||||||
@using TaxBaik.Domain.Entities
|
@using TaxBaik.Domain.Entities
|
||||||
@using TaxBaik.WasmClient.Components.Admin.Shared
|
@using TaxBaik.WasmClient.Components.Admin.Shared
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
@page "/admin/tax-filings"
|
@page "/admin/tax-filings"
|
||||||
@rendermode InteractiveWebAssembly
|
@rendermode @(new InteractiveWebAssemblyRenderMode(prerender: false))
|
||||||
@attribute [Authorize]
|
@attribute [Authorize]
|
||||||
@using TaxBaik.Web.Services
|
@using TaxBaik.Web.Services
|
||||||
@using TaxBaik.Domain.Entities
|
@using TaxBaik.Domain.Entities
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
@page "/admin/tax-profiles"
|
@page "/admin/tax-profiles"
|
||||||
@rendermode InteractiveWebAssembly
|
@rendermode @(new InteractiveWebAssemblyRenderMode(prerender: false))
|
||||||
@using TaxBaik.Web.Services.AdminClients
|
@using TaxBaik.Web.Services.AdminClients
|
||||||
@using TaxBaik.WasmClient.Components.Admin.Shared
|
@using TaxBaik.WasmClient.Components.Admin.Shared
|
||||||
@inject ITaxProfileBrowserClient TaxProfileClient
|
@inject ITaxProfileBrowserClient TaxProfileClient
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
@namespace TaxBaik.WasmClient.Components.Admin
|
@namespace TaxBaik.WasmClient.Components.Admin
|
||||||
@using Microsoft.AspNetCore.Components.Routing
|
@using Microsoft.AspNetCore.Components.Routing
|
||||||
|
|
||||||
<Router AppAssembly="@typeof(TaxBaik.WasmClient._Imports).Assembly" @rendermode="new InteractiveWebAssemblyRenderMode(prerender: false)">
|
<Router AppAssembly="@typeof(TaxBaik.WasmClient._Imports).Assembly">
|
||||||
<Found Context="routeData">
|
<Found Context="routeData">
|
||||||
<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(TaxBaik.WasmClient.Components.Admin.Layout.MainLayout)">
|
<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(TaxBaik.WasmClient.Components.Admin.Layout.MainLayout)">
|
||||||
<NotAuthorized>
|
<NotAuthorized>
|
||||||
|
|||||||
@@ -28,9 +28,11 @@
|
|||||||
<div class="mud-alert mud-alert-filled-error mb-4 login-error-message" style="display:none;">로그인 중 오류가 발생했습니다.</div>
|
<div class="mud-alert mud-alert-filled-error mb-4 login-error-message" style="display:none;">로그인 중 오류가 발생했습니다.</div>
|
||||||
|
|
||||||
<button type="submit"
|
<button type="submit"
|
||||||
|
id="admin-login-submit"
|
||||||
|
disabled
|
||||||
class="mud-button-root mud-button mud-button-filled mud-button-filled-primary mud-elevation-0"
|
class="mud-button-root mud-button mud-button-filled mud-button-filled-primary mud-elevation-0"
|
||||||
style="width: 100%; min-height: 52px; border: 0; border-radius: 4px; color: white;">
|
style="width: 100%; min-height: 52px; border: 0; border-radius: 4px; color: white;">
|
||||||
<span>로그인</span>
|
<span>준비 중...</span>
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</MudPaper>
|
</MudPaper>
|
||||||
|
|||||||
@@ -38,7 +38,7 @@
|
|||||||
|
|
||||||
<form method="post" id="contactForm">
|
<form method="post" id="contactForm">
|
||||||
@Html.AntiForgeryToken()
|
@Html.AntiForgeryToken()
|
||||||
<div asp-validation-summary="ModelOnly" class="text-danger mb-3"></div>
|
<div asp-validation-summary="All" class="text-danger mb-3"></div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="name" class="form-label">이름 <span class="text-danger">*</span></label>
|
<label for="name" class="form-label">이름 <span class="text-danger">*</span></label>
|
||||||
@@ -73,7 +73,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3 form-check">
|
<div class="mb-3 form-check">
|
||||||
<input type="checkbox" class="form-check-input" id="agree" name="Agree" required />
|
<input type="checkbox" class="form-check-input" id="agree" name="Agree" value="true" required />
|
||||||
<label class="form-check-label" for="agree">
|
<label class="form-check-label" for="agree">
|
||||||
개인정보 수집·이용에 동의합니다
|
개인정보 수집·이용에 동의합니다
|
||||||
</label>
|
</label>
|
||||||
|
|||||||
@@ -23,10 +23,12 @@ public class TelegramInquiryNotificationService : IInquiryNotificationService
|
|||||||
public async Task NotifyCreatedAsync(int inquiryId, string name, string phone, string serviceType, string message, string? ipAddress, DateTime createdAtUtc, CancellationToken ct = default)
|
public async Task NotifyCreatedAsync(int inquiryId, string name, string phone, string serviceType, string message, string? ipAddress, DateTime createdAtUtc, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
var botToken = _configuration["Telegram:BotToken"];
|
var botToken = _configuration["Telegram:BotToken"];
|
||||||
var chatId = _configuration["Telegram:ChatId"];
|
var chatId = _configuration["Telegram:InquiryChatId"];
|
||||||
|
if (string.IsNullOrWhiteSpace(chatId))
|
||||||
|
chatId = _configuration["Telegram:ChatId"];
|
||||||
if (string.IsNullOrWhiteSpace(botToken) || string.IsNullOrWhiteSpace(chatId))
|
if (string.IsNullOrWhiteSpace(botToken) || string.IsNullOrWhiteSpace(chatId))
|
||||||
{
|
{
|
||||||
_logger.LogWarning("텔레그램 새 문의 알림 설정이 누락되었습니다. Telegram:BotToken 또는 Telegram:ChatId를 확인하세요.");
|
_logger.LogWarning("텔레그램 새 문의 알림 설정이 누락되었습니다. Telegram:BotToken 또는 Telegram:InquiryChatId/ChatId를 확인하세요.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,10 +82,12 @@ public class TelegramInquiryNotificationService : IInquiryNotificationService
|
|||||||
public async Task NotifyStatusChangedAsync(int inquiryId, string name, string phone, string serviceType, string previousStatus, string newStatus, string? changedBy = null, CancellationToken ct = default)
|
public async Task NotifyStatusChangedAsync(int inquiryId, string name, string phone, string serviceType, string previousStatus, string newStatus, string? changedBy = null, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
var botToken = _configuration["Telegram:BotToken"];
|
var botToken = _configuration["Telegram:BotToken"];
|
||||||
var chatId = _configuration["Telegram:ChatId"];
|
var chatId = _configuration["Telegram:InquiryChatId"];
|
||||||
|
if (string.IsNullOrWhiteSpace(chatId))
|
||||||
|
chatId = _configuration["Telegram:ChatId"];
|
||||||
if (string.IsNullOrWhiteSpace(botToken) || string.IsNullOrWhiteSpace(chatId))
|
if (string.IsNullOrWhiteSpace(botToken) || string.IsNullOrWhiteSpace(chatId))
|
||||||
{
|
{
|
||||||
_logger.LogWarning("텔레그램 상태 변경 알림 설정이 누락되었습니다. Telegram:BotToken 또는 Telegram:ChatId를 확인하세요.");
|
_logger.LogWarning("텔레그램 상태 변경 알림 설정이 누락되었습니다. Telegram:BotToken 또는 Telegram:InquiryChatId/ChatId를 확인하세요.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -239,11 +239,12 @@ window.taxbaikAdminSession = {
|
|||||||
window.addEventListener('hashchange', window.taxbaikAdminSession.syncRouteClass);
|
window.addEventListener('hashchange', window.taxbaikAdminSession.syncRouteClass);
|
||||||
|
|
||||||
if (document.documentElement.classList.contains('admin-login-route')) {
|
if (document.documentElement.classList.contains('admin-login-route')) {
|
||||||
|
// Login prerenders immediately; no boot splash needed.
|
||||||
window.taxbaikAdminSession.hideLoading();
|
window.taxbaikAdminSession.hideLoading();
|
||||||
}
|
}
|
||||||
|
// Non-login routes: leave the overlay showing until AdminShell's
|
||||||
// Keep the initial overlay hidden unless explicitly enabled elsewhere.
|
// OnAfterRenderAsync(firstRender) calls hideLoading once WASM has
|
||||||
window.taxbaikAdminSession.hideLoading();
|
// actually rendered the authenticated shell.
|
||||||
|
|
||||||
const modal = document.getElementById('components-reconnect-modal');
|
const modal = document.getElementById('components-reconnect-modal');
|
||||||
if (!modal) return;
|
if (!modal) return;
|
||||||
@@ -266,6 +267,16 @@ window.taxbaikAdminSession = {
|
|||||||
|
|
||||||
form.dataset.bound = '1';
|
form.dataset.bound = '1';
|
||||||
window.taxbaikAdminSession.traceUiState('admin-login', 'bindLoginForm attached');
|
window.taxbaikAdminSession.traceUiState('admin-login', 'bindLoginForm attached');
|
||||||
|
|
||||||
|
// 업데이트 스플래시: 제출 핸들러가 실제로 붙기 전까지는 버튼을 "준비 중" 상태로 두고,
|
||||||
|
// 여기서 활성화해 사용자가 로그인 가능 시점을 알 수 있게 한다.
|
||||||
|
const readyButton = form.querySelector('#admin-login-submit');
|
||||||
|
if (readyButton) {
|
||||||
|
readyButton.disabled = false;
|
||||||
|
const label = readyButton.querySelector('span');
|
||||||
|
if (label) label.textContent = '로그인';
|
||||||
|
}
|
||||||
|
|
||||||
form.addEventListener('submit', async function (event) {
|
form.addEventListener('submit', async function (event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ Razor Page/Form
|
|||||||
| P5-01 | CI gate 명문화 | workflow 체크 목록 | 6개 gate 모두 required |
|
| P5-01 | CI gate 명문화 | workflow 체크 목록 | 6개 gate 모두 required |
|
||||||
| P5-02 | 배포본 API smoke 확장 | workflow curl 추가 | Blog/Inquiry create-read-update test 2xx |
|
| P5-02 | 배포본 API smoke 확장 | workflow curl 추가 | Blog/Inquiry create-read-update test 2xx |
|
||||||
| P5-03 | 운영 회귀 대시보드 | test report/version endpoint | 배포 커밋과 E2E 결과 추적 가능 |
|
| P5-03 | 운영 회귀 대시보드 | test report/version endpoint | 배포 커밋과 E2E 결과 추적 가능 |
|
||||||
|
| P4-03 | 기존 20개+ 어드민 화면을 SmartAdmin 5.5 참조(`legacy/smartadmin/`, `DOUZONE_UX_GUIDE.md`)로 재단장 (2026-07-03 시점 미착수, 향후 별도 진행) | 각 화면의 색상/카드/타이포그래피 갱신 | SmartAdmin 매핑 표 기준 적용 화면 수 / 전체 화면 수 100% |
|
||||||
|
|
||||||
## Immediate Refactor Order
|
## Immediate Refactor Order
|
||||||
|
|
||||||
|
|||||||
@@ -65,6 +65,29 @@ ContentSurface
|
|||||||
- 색상은 의미를 유지한다.
|
- 색상은 의미를 유지한다.
|
||||||
- 동일 상태는 동일 색을 사용한다.
|
- 동일 상태는 동일 색을 사용한다.
|
||||||
|
|
||||||
|
## SmartAdmin 5.5 Design Reference (2026-07-03, 신규 화면부터 적용)
|
||||||
|
|
||||||
|
이 섹션은 어드민의 **시각적 스킨**(색상, 카드 크롬, 로그인 화면 스타일, 셸 레이아웃) 기준이다. 위 UX Principles(고밀도, 표준 동선, 더존 정신)는 그대로 유지하고, SmartAdmin 5.5는 그 위에 입히는 룩앤필만 담당한다.
|
||||||
|
|
||||||
|
- 소스: `legacy/smartadmin/`(로컬에 이미 포함된 v5.5 HTML/CSS 데모 패키지, Bootstrap 5 기반). 정확한 색상/여백 값이 필요하면 이 디렉터리를 직접 참조한다(추측 금지).
|
||||||
|
- 적용 범위: **향후 신규 어드민 화면부터**. 기존 20개+ 화면(Dashboard, Blog, Inquiry, Client 등)은 이번엔 재단장하지 않는다. 기존 화면을 다른 이유로 수정할 때 자연스럽게 이 기준으로 수렴시킨다.
|
||||||
|
|
||||||
|
### 매핑 표
|
||||||
|
|
||||||
|
| SmartAdmin 5.5 참조 | 파일 | TaxBaik MudBlazor 대응 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 상단 `<header>` 툴바 | `dashboard-control-center.html` | `AdminShell`의 `MudAppBar` |
|
||||||
|
| `<aside class="app-sidebar">` (로고 + 필터 입력 + 메뉴) | `dashboard-control-center.html` | `AdminShell`의 `MudDrawer` (검색/필터 입력 포함) |
|
||||||
|
| 로그인 카드 (`rounded-4`, 반투명 다크 글래스, `bg-dark bg-opacity-50`) | `auth-login.html` | `AdminLoginForm.razor`의 `MudPaper` 카드 — 반투명/블러 배경 톤 참고 |
|
||||||
|
| 색상 팔레트 | `colorpalette.html`, `css/smartapp.min.css` | `App.razor`의 `MudTheme.Palette` (Primary/Secondary/Tertiary 등) |
|
||||||
|
| 카드형 위젯 | `dashboard-*.html` | `AdminMetricCard`, `MudPaper` 기반 카드 |
|
||||||
|
|
||||||
|
### 적용 규칙
|
||||||
|
|
||||||
|
- 새 어드민 화면을 만들 때: 레이아웃/동선/밀도는 `DOUZONE_UX_GUIDE.md` 상단 원칙을 따르고, 색상·카드 모서리·그림자·로그인류 화면의 톤은 `legacy/smartadmin/`을 참조해 `MudTheme`/CSS 변수로 반영한다.
|
||||||
|
- SmartAdmin 원본은 jQuery/Bootstrap 5 기반이므로 JS/DOM 구조를 그대로 이식하지 않는다. **시각적 토큰(색, 반경, 여백, 타이포그래피)만** 가져오고, 동작은 MudBlazor 컴포넌트로 구현한다.
|
||||||
|
- 기존 화면을 SmartAdmin 스타일로 일괄 재단장하는 작업은 별도 WBS로 `docs/ADMIN_PATTERN_CRITIQUE_WBS.md`에 등록한 뒤 진행한다(이번 범위 아님).
|
||||||
|
|
||||||
## Text And Labels
|
## Text And Labels
|
||||||
|
|
||||||
- 라벨은 짧게 쓴다.
|
- 라벨은 짧게 쓴다.
|
||||||
|
|||||||
@@ -1,57 +1,75 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
# Phase 8: WebAssembly 마이그레이션 - 기능 우선 아키텍처 (2026-07-03)
|
# Phase 9: 어드민 페이지별 렌더모드 정상화 (2026-07-03)
|
||||||
#
|
#
|
||||||
# DESIGN DECISION: prerender: false (not true)
|
# DESIGN: Router/Routes에는 렌더모드를 지정하지 않고, 페이지별로 개별 지정한다.
|
||||||
#
|
#
|
||||||
# RATIONALE:
|
# RATIONALE:
|
||||||
# - prerender: true = SSR (Server-Side Rendering) - 서버가 정적 HTML 미리 생성
|
# - Blazor Web App은 "전역 렌더모드"와 "페이지별 렌더모드" 중 하나만 선택할 수 있다.
|
||||||
# - InteractiveWebAssembly = CSR (Client-Side Rendering) - 클라이언트에서 동적 렌더링
|
# - Router/Routes에 @rendermode를 지정하면 하위 모든 페이지의 개별 @rendermode는 무시된다.
|
||||||
# - These are contradictory: cannot prerender interactive WebAssembly content
|
# - 로그인만 prerender: true가 필요하고(흰 화면 방지), 나머지 [Authorize] 페이지는
|
||||||
|
# prerender: false가 필요하다(인증 컨텍스트 없이 prerender 시 AuthorizeRouteView가
|
||||||
|
# 빈 화면을 그리는 문제를 피하기 위함). 이 둘을 동시에 만족하려면 전역 지정을 버리고
|
||||||
|
# 페이지별 지정으로 가야 한다.
|
||||||
#
|
#
|
||||||
# OBSERVED PROBLEM with prerender: true:
|
# See CLAUDE.md Phase 9 for architecture details.
|
||||||
# 1. Unauthenticated users see prerendered HTML (no auth context)
|
|
||||||
# 2. AuthorizeRouteView renders nothing (authorized content cannot prerender)
|
|
||||||
# 3. After login, WebAssembly tries to render same component differently
|
|
||||||
# 4. Conflict: static prerendered HTML vs. dynamic WASM runtime
|
|
||||||
#
|
|
||||||
# SOLUTION: prerender: false
|
|
||||||
# - App boots to blank screen briefly (WebAssembly loading ~0.5-2s)
|
|
||||||
# - After load: all protected pages render correctly with auth
|
|
||||||
# - No conflicting render modes
|
|
||||||
# - Functional > Performance optimization
|
|
||||||
#
|
|
||||||
# See CLAUDE.md Phase 8 for architecture details.
|
|
||||||
|
|
||||||
app_file="TaxBaik.Web.Client/Components/Admin/App.razor"
|
app_file="TaxBaik.Web.Client/Components/Admin/App.razor"
|
||||||
|
routes_file="TaxBaik.Web.Client/Components/Admin/Routes.razor"
|
||||||
login_file="TaxBaik.Web.Client/Components/Admin/Pages/Login.razor"
|
login_file="TaxBaik.Web.Client/Components/Admin/Pages/Login.razor"
|
||||||
|
|
||||||
for file in "$app_file" "$login_file"; do
|
for file in "$app_file" "$routes_file" "$login_file"; do
|
||||||
if [ ! -f "$file" ]; then
|
if [ ! -f "$file" ]; then
|
||||||
echo "Missing admin render file: $file" >&2
|
echo "Missing admin render file: $file" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
# Require WebAssemblyRenderMode (regardless of prerender value)
|
# Reject InteractiveServerRenderMode anywhere (Blazor Server architecture forbidden)
|
||||||
if ! grep -nE "InteractiveWebAssemblyRenderMode" "$app_file" >/dev/null; then
|
if grep -rnE "InteractiveServerRenderMode" "$app_file" "$routes_file" >/dev/null; then
|
||||||
echo "Admin shell must use InteractiveWebAssemblyRenderMode." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Reject InteractiveServerRenderMode (Blazor Server architecture forbidden)
|
|
||||||
if grep -nE "InteractiveServerRenderMode" "$app_file" >/dev/null; then
|
|
||||||
echo "Admin shell must NOT use InteractiveServerRenderMode (Blazor Server)." >&2
|
echo "Admin shell must NOT use InteractiveServerRenderMode (Blazor Server)." >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Login page also requires WebAssembly mode
|
# Router/Routes must NOT declare a global rendermode boundary.
|
||||||
# Accept both: @rendermode InteractiveWebAssembly OR @rendermode @(new InteractiveWebAssemblyRenderMode(...))
|
# A global rendermode on <Routes> or <Router> overrides every per-page @rendermode
|
||||||
if ! grep -nE "InteractiveWebAssembly" "$login_file" >/dev/null; then
|
# directive beneath it, which is exactly the bug this design fixes.
|
||||||
echo "Login page must use InteractiveWebAssemblyRenderMode or @rendermode InteractiveWebAssembly." >&2
|
if grep -nE "@rendermode" "$app_file" >/dev/null; then
|
||||||
|
echo "App.razor's <Routes> must not declare a global @rendermode (breaks per-page prerender control)." >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "✅ Admin render harness passed (WebAssembly mode verified)."
|
if grep -nE "@rendermode" "$routes_file" >/dev/null; then
|
||||||
echo " ℹ️ Note: prerender: false by design (protects @Authorize content from SSR conflicts)"
|
echo "Routes.razor's <Router> must not declare a global @rendermode (breaks per-page prerender control)." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Login page must explicitly prerender (shows the login form in the first HTML response).
|
||||||
|
if ! grep -nE "InteractiveWebAssemblyRenderMode\(\s*prerender:\s*true\s*\)" "$login_file" >/dev/null; then
|
||||||
|
echo "Login page must use InteractiveWebAssemblyRenderMode(prerender: true) so the form renders before WASM loads." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Only routable pages (files with a @page directive) need their own render mode.
|
||||||
|
# Child components (e.g. BlogForm.razor, FilingTable.razor) inherit the render
|
||||||
|
# mode of whichever page hosts them and must NOT declare their own.
|
||||||
|
missing_rendermode=0
|
||||||
|
while IFS= read -r -d '' page; do
|
||||||
|
[ "$page" = "$login_file" ] && continue
|
||||||
|
if ! grep -qE "^@page " "$page"; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
if ! grep -nE "@rendermode" "$page" >/dev/null; then
|
||||||
|
echo "⚠️ $page has no @rendermode directive (will not be interactive)." >&2
|
||||||
|
missing_rendermode=1
|
||||||
|
fi
|
||||||
|
done < <(find TaxBaik.Web.Client/Components/Admin/Pages -name "*.razor" -print0)
|
||||||
|
|
||||||
|
if [ "$missing_rendermode" -ne 0 ]; then
|
||||||
|
echo "One or more admin pages are missing an explicit @rendermode directive." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "✅ Admin render harness passed (per-page render mode verified)."
|
||||||
|
echo " ℹ️ Login: prerender: true (visible before WASM loads). Other pages: WebAssembly-interactive."
|
||||||
|
|||||||
@@ -54,4 +54,25 @@ test.describe('contact submit', () => {
|
|||||||
await navigateInBlazor(page, `${baseUrl}/admin/inquiries`);
|
await navigateInBlazor(page, `${baseUrl}/admin/inquiries`);
|
||||||
await expect(page.locator('.mud-main-content').getByText('문의 관리').first()).toBeVisible({ timeout: 20_000 });
|
await expect(page.locator('.mud-main-content').getByText('문의 관리').first()).toBeVisible({ timeout: 20_000 });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('submitting the public Contact.cshtml form shows the success message', async ({ page }) => {
|
||||||
|
// Regression test: Contact.cshtml's "Agree" checkbox had no value="true", so a
|
||||||
|
// checked box submitted the browser-default "on", which bool model binding
|
||||||
|
// cannot parse. ModelState.IsValid became false and OnPostAsync silently
|
||||||
|
// returned Page() with a blank form and no visible error - the "무한 작성 유도"
|
||||||
|
// bug. This drives the real form through a browser to catch that class of bug.
|
||||||
|
const stamp = Date.now();
|
||||||
|
|
||||||
|
await page.goto(`${baseUrl}/contact`, { waitUntil: 'networkidle' });
|
||||||
|
|
||||||
|
await page.fill('#name', `Contact-E2E-${stamp}`);
|
||||||
|
await page.fill('#phone', '010-1234-5678');
|
||||||
|
await page.fill('#email', `contact-e2e-${stamp}@example.com`);
|
||||||
|
await page.fill('#message', 'Playwright로 Contact.cshtml 폼을 직접 제출한 테스트입니다.');
|
||||||
|
await page.check('#agree');
|
||||||
|
await page.click('button[type="submit"]');
|
||||||
|
|
||||||
|
await expect(page.locator('#contact-success')).toBeVisible({ timeout: 15_000 });
|
||||||
|
await expect(page.locator('#contact-success')).toContainText('접수되었습니다');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,6 +4,21 @@ const baseUrl = 'https://www.taxbaik.com/taxbaik';
|
|||||||
const username = 'test_admin';
|
const username = 'test_admin';
|
||||||
const password = 'TestAdmin@123456';
|
const password = 'TestAdmin@123456';
|
||||||
|
|
||||||
|
test('Admin Login Page - prerendered HTML contains the form before JS runs', async ({ request }) => {
|
||||||
|
// Login.razor is @rendermode ...(prerender: true), so the raw HTTP response
|
||||||
|
// (no JS execution) must already contain the login form markup. This is the
|
||||||
|
// regression check for the WASM-boot white-screen bug: if Router/Routes ever
|
||||||
|
// regains a global @rendermode, this prerender is silently dropped and this
|
||||||
|
// test catches it without needing a browser.
|
||||||
|
const response = await request.get(`${baseUrl}/admin/login`);
|
||||||
|
expect(response.ok()).toBeTruthy();
|
||||||
|
|
||||||
|
const html = await response.text();
|
||||||
|
expect(html).toContain('name="username"');
|
||||||
|
expect(html).toContain('name="password"');
|
||||||
|
expect(html).toContain('admin-login-form');
|
||||||
|
});
|
||||||
|
|
||||||
test('Admin Login Page - Full Flow Test', async ({ page }) => {
|
test('Admin Login Page - Full Flow Test', async ({ page }) => {
|
||||||
// 콘솔 에러 캡처
|
// 콘솔 에러 캡처
|
||||||
page.on('console', msg => {
|
page.on('console', msg => {
|
||||||
|
|||||||
Reference in New Issue
Block a user