Refine admin login flow and verification harness
TaxBaik CI/CD / build-and-deploy (push) Successful in 2m21s

This commit is contained in:
2026-07-07 14:38:30 +09:00
parent b7cb442937
commit 35842b6765
60 changed files with 1043 additions and 495 deletions
+46 -2
View File
@@ -31,6 +31,7 @@
- Infrastructure는 Dapper SQL과 외부 시스템 구현을 가진다.
- Web은 Controller, 공개 Razor Pages SSR, Blazor host, 인증/서빙 설정을 가진다.
- Web.Client/Admin UI는 클라이언트 사이드 Blazor WebAssembly로 본다. 서버 DI 서비스에 의존하지 않고 API client만 호출한다.
- 로그인 화면은 정적 SSR 또는 Razor Page form으로 처리하고, 홈/대시보드 같은 상호작용 화면만 Interactive WebAssembly + MudBlazor를 사용한다.
- 관리자 호스트가 prerender를 사용하더라도 데이터 접근 원칙은 WASM + API-first다. prerender는 초기 마크업용이며 비즈니스 로직의 근거가 아니다.
- 어드민 기본 렌더는 WASM이다. 다만 초기 흰 화면 방지 목적의 셸 프리렌더와 로그인 화면의 서버 프리렌더는 허용한다. 비즈니스 로직은 여전히 API-first다.
- 로그인 화면은 예외적으로 “먼저 보여야 하는 화면”이다. JS 바인딩/텔레메트리/하이드레이션이 실패해도 로그인 폼 자체는 화면에 남아 있어야 하며, 실패 시 흰 화면이나 빈 본문을 허용하지 않는다.
@@ -58,6 +59,9 @@
| 리팩토링 | 동작 보존 테스트를 먼저 추가하고 작은 단위로 이동 |
| 일관성 | 오류 응답은 ProblemDetails, 페이징은 `{ data, total, page, pageSize }` |
| 파편화 방지 | 같은 필드/상태/서비스유형 문자열은 enum/상수/공통 코드 중 하나로 단일화 |
| UI 상태 | Blazor 화면의 폼/목록 상태는 MVVM 스타일의 ViewModel로 분리 |
| DTO 검증 | DTO에는 `System.ComponentModel.DataAnnotations` 어노테이션을 1차 기준으로 둔다 |
| 추가 검증 | 어노테이션만으로 표현이 부족할 때만 FluentValidation을 보완으로 쓴다 |
| 과유불급 | 추상화는 2개 이상 실제 사용처가 생긴 뒤 도입 |
| 정규화 | 고객, 문의, 상담, 계약, 세금신고는 원천 테이블을 분리 |
| 역정규화 | 대시보드/검색/운영 요약용 스냅샷만 허용하고 원천 id와 갱신 시점을 저장 |
@@ -71,6 +75,7 @@
- DB 제약 조건이 1차 방어선이다: NOT NULL, UNIQUE, FK, CHECK, index.
- Application validation은 사용자 메시지와 use case 규칙을 담당한다.
- DTO validation은 `DataAnnotations`를 1차 기준으로 하고, 표현력이 부족한 경우에만 FluentValidation으로 보완한다.
- UI validation은 빠른 피드백일 뿐이며 유일한 검증으로 보지 않는다.
- 관리자 수정 화면에 노출한 필드는 실제 저장되어야 한다. 저장하지 않는 필드는 read-only로 표시한다.
- 상태 전이는 허용 목록을 둔다. 임의 문자열 저장을 금지한다.
@@ -79,6 +84,7 @@
- 로그인 화면은 배포 전 브라우저 실증이 필수다. `dotnet build`만으로 로그인 화면 정상 표시를 완료로 선언하지 않는다.
- 로그인 화면 실증 기준은 최소 1회 실제 브라우저 응답, 로그인 폼 렌더, 입력 포커스 가능 여부 확인이다.
- 공개 루트와 관리자 루트는 반드시 분리 검증한다. `https://www.taxbaik.com/` 은 공용 홈페이지 제목을 가져야 하고, `https://www.taxbaik.com/taxbaik/admin/login` 은 관리자 제목을 가져야 한다.
- 최종 완료 검증은 실제 서비스 도메인에서만 수행한다. IP 직결 주소는 로컬/임시 확인용으로만 사용한다.
- 배포 후 smoke 기준은 `scripts/taxbaik-smoke.sh`를 1차 기준으로 사용한다. CI와 브라우저 E2E는 이 스크립트를 재사용해야 한다.
- `UsePathBase("/taxbaik")`가 있는 Web 앱에서 `MapGet("/")` 같은 루트 리다이렉트를 추가할 때는, 반드시 루프 여부를 `curl`로 먼저 확인하고 나서만 반영한다. `/``/taxbaik/`가 서로를 다시 가리키면 안 된다.
- nginx의 `location /``location /taxbaik`를 건드린 뒤에는, 변경 직후 `curl -I https://www.taxbaik.com/`, `curl -I https://www.taxbaik.com/taxbaik/`, `curl -I https://www.taxbaik.com/taxbaik/admin/login` 순서로 확인하고 결과를 기록한다.
@@ -109,7 +115,7 @@
| Naming | `Create[Entity]Endpoint`, `Get[Entity]Endpoint`, `List[Entity]Endpoint` 등 |
| Location | `src/TaxBaik.Web/Features/[DomainName]/` |
| Pattern | Request DTO → Endpoint class → Response DTO |
| Validation | FluentValidation (FastEndpoints 내장) |
| Validation | DTO는 `DataAnnotations` 우선, 부족할 때만 FluentValidation 보완 |
| Registration | `builder.Services.AddFastEndpoints()` + `app.MapFastEndpoints()` |
| Coexistence | Controllers와 FastEndpoints는 PathBase 내에서 병행 가능 (URL 충돌만 피함) |
@@ -143,7 +149,8 @@ public class GetBlogEndpoint : Endpoint<GetBlogRequest, GetBlogResponse>
## CI Harness
완료는 로컬 성공이 아니라 CI와 배포본 성공이다.
완료는 단순 로컬 성공이 아니라, 아래 필수 하네스를 모두 통과하고 그 증빙이 남아야 한다.
실행 순서는 항상 `로컬 1차 검증 -> CI 배포 -> 실제 도메인 최종 검증`이다.
| Gate | Command/Check | Target |
| --- | --- | --- |
@@ -155,6 +162,42 @@ public class GetBlogEndpoint : Endpoint<GetBlogRequest, GetBlogResponse>
| Deploy | `.gitea/workflows/deploy.yml` | success |
| Post Deploy | `.gitea/workflows/browser-e2e.yml` | success |
### Validation Order
1. 로컬에서 `dotnet build`와 필요한 unit/Playwright 시나리오를 먼저 통과시킨다.
1. 로컬 검증이 끝나야 CI 배포를 진행한다.
1. CI 배포가 성공해야 실제 도메인에서 브라우저 E2E와 smoke를 수행한다.
1. 실제 도메인 결과와 증빙이 있어야만 최종 완료로 기록한다.
### Definition Of Done
다음 조건을 모두 만족해야 `완료`로 본다.
1. `dotnet build`가 경고/오류 없이 성공한다.
1. Playwright가 실제 사용자 시나리오를 직접 호출한다.
1. 시나리오에서 기대한 데이터가 실제 응답 데이터로 검증된다.
1. 화면 캡처와 DOM 캡처를 남긴다.
1. 기대값이 실제 화면 결과와 일치한다는 증빙이 데이터로 남는다.
1. 위 증빙이 없으면 기능이 동작해 보여도 완료로 선언하지 않는다.
### Mandatory Harness
아래 하네스는 선택 사항이 아니라 필수다.
- 빌드 하네스: `dotnet build src/TaxBaik.sln -c Release --no-restore`
- 브라우저 하네스: Playwright로 실제 사용자 흐름 실행
- 데이터 하네스: API 응답, DB 값, 또는 화면 데이터가 기대값과 일치하는지 검증
- 증빙 하네스: 스크린샷, DOM 스냅샷, 테스트 로그를 함께 보관
- 판정 하네스: 사람의 육안 판단이 아니라 캡처와 로그로 성공 여부를 판정
### Evidence Rules
- Playwright는 단순 페이지 열기만으로 끝내지 않는다.
- 사용자 관점에서 버튼 클릭, 입력, 저장, 조회 같은 실제 시나리오를 실행해야 한다.
- 각 시나리오는 기대값과 실제값을 비교하는 assert를 포함해야 한다.
- 스크린샷은 결과 화면을 증빙하고, DOM 캡처는 화면이 그렇게 나온 구조적 근거를 제공해야 한다.
- 수동 확인만 있고 데이터 증빙이 없으면 실패다.
### Gitea Auth Harness
- Gitea API와 workflow dispatch에는 `GITEA_TOKEN_TAXBAIK`만 사용한다.
@@ -168,6 +211,7 @@ public class GetBlogEndpoint : Endpoint<GetBlogRequest, GetBlogResponse>
- 동일 개념이 3곳 이상 다른 이름/계약으로 구현되면 기능 추가를 중단하고 정리한다.
- UI가 저장한다고 보이는 필드를 API/Application이 저장하지 않으면 릴리스하지 않는다.
- 운영 배포 검증이 CI 밖에서만 가능하면 완료로 보지 않는다.
- 빌드, Playwright 시나리오, 화면 캡처, DOM 캡처, 데이터 일치 증빙이 없는 상태에서 완료라고 쓰지 않는다.
- `UsePathBase` 아래에서 루트 기본값을 만들려면 리다이렉트보다 실제 최종 콘텐츠가 나오는지 먼저 확인한다. 루트가 `/taxbaik/`를 다시 반환하면 즉시 중단한다.
- lint가 필요하면 build보다 먼저, 그리고 배포보다 훨씬 앞단에서 실패시키고, lint가 없는 프로젝트에 억지로 새 lint 체계를 만들지 않는다.
- 데이터 모델을 추측해서 세무 규칙이나 더존 UX 관습을 왜곡해 구현하지 않는다.
+1
View File
@@ -7,6 +7,7 @@
| 문서 | 용도 | 변경 조건 |
| --- | --- | --- |
| [ENGINEERING_HARNESS.md](./ENGINEERING_HARNESS.md) | 아키텍처, 코드 품질, 배포, 데이터 정합성 하네스 | 방향성 변경 또는 반복 위반 발견 |
| [STACK_TEMPLATE_WBS.md](./STACK_TEMPLATE_WBS.md) | MudBlazor + ASP.NET Core 10 + Dapper + PostgreSQL 18.4 + API-first + SSR/Interactive render mode 기준 템플릿, 로드맵, WBS | 새 스택 채택 또는 렌더/인증 기준 변경 |
| [DOUZONE_UX_GUIDE.md](./DOUZONE_UX_GUIDE.md) | 더존식 어드민 UX 원칙, 템플릿, 컴포넌트, 서빙 규칙 | 화면 패턴 변경 또는 신규 템플릿 추가 |
| [COMMON_CODE_POLICY.md](./COMMON_CODE_POLICY.md) | 공통코드, 저장값, 컬럼 길이, 하드코딩 금지 규칙 | 공통코드 또는 콤보 추가/수정 |
| [COMBO_POLICY.md](./COMBO_POLICY.md) | 콤보/선택/검색 입력 정책과 저장값 규칙 | 상태/유형/선택 입력 정책 변경 |
+160
View File
@@ -0,0 +1,160 @@
# Stack Template And WBS
이 문서는 `MudBlazor + ASP.NET Core 10 + Dapper + PostgreSQL 18.4 + API-first` 기준의 기본 템플릿과 작업 순서를 정의한다. 기준은 공식 문서로 확인한 다음 반영한다.
## Verified Assumptions
- MudBlazor는 static rendering을 지원하지 않는다. MudBlazor를 쓰는 화면은 interactive render mode가 필요하다.
- Blazor Web App은 static rendering과 interactive rendering을 구분한다.
- Interactive WebAssembly 컴포넌트는 `.Client` 프로젝트에 두는 것이 공식 구조다.
- ASP.NET Core 10은 `Microsoft.AspNetCore.OpenApi` 기반 OpenAPI 문서 생성을 지원한다.
- PostgreSQL 18.4는 2026-05-14 릴리스이며, 18.x 내 minor upgrade는 dump/restore가 필요하지 않다.
- Dapper는 SQL 중심의 micro ORM으로 repository layer에 맞는다.
- DTO 검증은 `DataAnnotations`를 기본으로 하고, 표현력이 부족한 경우에만 FluentValidation을 보완으로 둔다.
- Blazor UI의 화면 상태는 MVVM 스타일 ViewModel로 분리한다.
## Target Template
| 영역 | 권장 방식 | 비고 |
| --- | --- | --- |
| Public Home | Static SSR + 필요한 부분만 Interactive WebAssembly | 초기 로딩과 SEO 우선 |
| Login | Static SSR 또는 Razor Page form | MudBlazor 의존 금지 |
| Home/Dashboard | Interactive WebAssembly + MudBlazor | 사용자 인터랙션 중심 |
| Admin CRUD | Interactive WebAssembly + API-first | BrowserClient 경유 |
| API | Minimal API 또는 Controller + OpenAPI | 계약 우선 |
| Persistence | Dapper + `NpgsqlDataSource` singleton | connection pool 직접 관리 금지 |
| Auth | 브라우저는 HttpOnly Secure Cookie 우선, 외부 클라이언트만 JWT | localStorage 저장 금지 |
현재 저장소 기준 예외:
- 포털 로그인은 이미 Razor Page SSR로 구현돼 있다.
- 관리자 로그인은 아직 Interactive WebAssembly 경로를 사용한다.
- 따라서 본 템플릿의 로그인 SSR 기준은 포털/공개 사용자 흐름에 우선 적용하고, 관리자 로그인은 별도 마이그레이션 WBS로 분리한다.
## Authentication Rules
- 브라우저 사용자 흐름은 SSR 로그인 form으로 시작한다.
- 서버는 자격 증명을 검증한 뒤 HttpOnly + Secure + SameSite cookie를 발급한다.
- 같은 origin에서 WebAssembly 화면은 쿠키 기반 API 호출을 우선 사용한다.
- JWT는 외부 API 클라이언트, 모바일, 파트너용으로만 분리한다.
- 토큰 원문은 로그에 남기지 않는다.
## Render Mode Rules
- `/account/login` 은 Static SSR 또는 Razor Page로 구현한다.
- `/` 또는 홈 대시는 Interactive WebAssembly로 구현한다.
- MudBlazor 컴포넌트는 interactive render mode가 없는 페이지에 두지 않는다.
- 로그인 화면에서 `MudButton`, `MudTextField`, `MudDialog`, `MudSnackbar` 같은 interactive 의존은 금지한다.
## Architecture Skeleton
```text
src/
TaxBaik.Web/ # Host, SSR shell, auth, API, OpenAPI
TaxBaik.Web.Client/ # Interactive WebAssembly UI
TaxBaik.Contracts/ # DTO, request/response, validation contracts
TaxBaik.Application/ # Use cases, interfaces, policies
TaxBaik.Infrastructure/ # Dapper repositories, external integrations
TaxBaik.Migrations/ # SQL migration scripts
tests/
TaxBaik.UnitTests/
TaxBaik.ApiTests/
TaxBaik.Playwright/
scripts/
smoke/
deploy/
evidence/
```
## Roadmap
| Phase | Goal | Exit Criteria |
| --- | --- | --- |
| P0 Baseline | 공식 전제와 템플릿 기준 고정 | 본 문서와 `ENGINEERING_HARNESS.md``INDEX.md`에 연결 |
| P1 Local Verify | 로컬 빌드/테스트/증빙 먼저 통과 | 로컬 `dotnet build`, unit, Playwright 1차 통과 |
| P2 CI Deploy | CI 배포와 중간 smoke 통과 | 배포 workflow success |
| P3 Domain Verify | 실제 도메인 최종 검증 | `https://www.taxbaik.com/taxbaik` 기준 브라우저 E2E 성공 |
| P1 Shell | SSR login / interactive home / API host 분리 | 로그인 SSR, 홈 WASM, API 계약 분리 확인 |
| P2 Auth | Cookie/JWT 이중 경로 정착 | 브라우저 cookie, 외부 JWT 경로 분리 완료 |
| P3 Data | Dapper + PostgreSQL 18.4 안정화 | repository, migration, index, connection 규칙 확정 |
| P4 Verify | Playwright와 데이터 증빙 자동화 | 화면 캡처, DOM 캡처, 실제 데이터 비교 통과 |
| P5 Operate | 배포/장애/FAQ 표준화 | CI/CD, smoke, rollback, incident FAQ 정착 |
## Detailed WBS
WBS 항목은 다음 데이터가 모두 있어야 완료다.
- 변경 diff 링크 또는 파일명
- 로컬 빌드 결과
- 로컬 Playwright 결과
- Playwright 시나리오 이름
- 화면 캡처 파일 경로
- DOM 캡처 파일 경로
- 기대값과 실제값 비교 결과
- CI 배포 결과
- 실제 도메인 최종 검증 결과
- CI 또는 배포 로그 링크
| ID | 작업 | 산출물 | 완료 데이터 정의 |
| --- | --- | --- | --- |
| P0-01 | 공식 전제 검증 문서화 | Verified Assumptions | MudBlazor static rendering 비지원, Blazor render mode, OpenAPI, PostgreSQL 18.4, Dapper 근거 링크 |
| P0-02 | 엔지니어링 하네스 반영 | `ENGINEERING_HARNESS.md` | 완료 정의에 build + Playwright + screenshot + DOM + data proof 포함 |
| P0-03 | 인덱스 연결 | `INDEX.md` | 새 템플릿 문서가 canonical list에 포함 |
| P1-01 | 솔루션 구조 확정 | `src/`, `tests/`, `scripts/` tree | 호스트/클라이언트/계약/애플리케이션/인프라 분리 확인 |
| P1-02 | 로그인 SSR 구현 가이드 | `/account/login` 기준 | static SSR form, antiforgery, cookie issuance 흐름 문서화 |
| P1-03 | 홈 WASM 가이드 | `/` 또는 dashboard | interactive WebAssembly + MudBlazor 사용 경계 문서화 |
| P1-04 | 관리자 로그인 마이그레이션 분리 | `/admin/login` 또는 equivalent | 현재 WASM 로그인과 목표 SSR 경계를 문서화하고 후속 WBS로 분리 |
| P1-05 | API-first 계약 정의 | `/api/v1/*` | DTO, OpenAPI, ProblemDetails, auth policy 정의 |
| P1-06 | DTO/MVVM 규칙 확정 | 모든 Blazor form/list | DTO는 DataAnnotations, UI는 ViewModel, 보완 검증은 FluentValidation |
| P2-01 | Cookie auth 템플릿 | auth middleware | HttpOnly/Secure/SameSite settings, 401/403 behavior 확인 |
| P2-02 | JWT 분리 템플릿 | external auth endpoint | refresh rotation, revocation, expiry, no localStorage rule |
| P2-03 | CSRF/antiforgery 검증 | login POST flow | token present, POST accepted, stale tab failure case documented |
| P3-01 | Dapper repository pattern | singleton `NpgsqlDataSource` | open-conn-per-method, transaction short-lived, query mapping test |
| P3-02 | PostgreSQL 18.4 migration policy | SQL scripts | versioned migrations, restore drill evidence |
| P3-03 | schema/index baseline | core tables | PK/FK/unique/check/index list and explain output |
| P4-01 | Playwright user journeys | login/home/admin scenario suite | each test has screenshot, DOM snapshot, assert on real data |
| P4-02 | evidence capture harness | scripts/evidence | screenshot + DOM + response capture persisted per run |
| P4-03 | build gate | CI/local build | 로컬 `dotnet build` warning/error zero evidence |
| P5-01 | deployment smoke | deploy workflow | CI 배포 success + 실제 도메인 smoke success |
| P5-02 | incident FAQ | docs FAQ section | common failure, root cause, and recovery steps documented |
## Playwright Evidence Standard
- 시나리오는 실제 사용자 관점이어야 한다.
- 버튼 클릭, 입력, 저장, 조회 같은 동작을 포함해야 한다.
- 결과는 화면 캡처만으로 끝내지 않고 DOM 캡처를 함께 남긴다.
- API 응답 또는 화면 텍스트를 기준으로 기대값과 실제값을 assert한다.
- 실패 시에는 어떤 데이터가 달랐는지 로그로 남긴다.
- 최종 완료 검증은 `https://www.taxbaik.com/taxbaik` 도메인에서 수행한다. IP 직결 주소는 로컬/임시 점검용으로만 쓴다.
## Deployment and Trouble FAQ
### Q1. 로그인 화면에 MudBlazor를 쓰면 안 되나?
Static SSR 로그인이라면 쓰지 않는 것이 맞다. MudBlazor는 static rendering을 지원하지 않으므로 login은 HTML form 또는 Razor Page가 안전하다.
### Q2. 브라우저 앱에 JWT를 localStorage에 저장해도 되나?
피해야 한다. 브라우저 사용자 인증은 HttpOnly cookie가 기본이고, JWT는 외부 API 클라이언트용으로 분리한다.
### Q3. WebAssembly 홈에서 cookie 인증 API 호출이 복잡한가?
same-origin이면 단순하다. cross-origin이면 CORS, SameSite, credentials 정책이 추가되어 장애 포인트가 늘어난다.
### Q4. Dapper를 쓰면 ORM의 변경 추적이 없는데 괜찮나?
이 구조는 조회와 명령을 분리하는 쪽에 맞는다. SQL을 명시적으로 관리하고 repository 테스트와 migration으로 정합성을 유지한다.
### Q5. PostgreSQL 18.4로 업그레이드할 때 dump/restore가 필요한가?
같은 18.x minor 업그레이드는 dump/restore가 필요하지 않다. 다만 운영 적용 전 staging restore drill은 필수다.
## Stop Conditions
- MudBlazor를 static rendering 화면에 억지로 넣는 시도는 중단한다.
- 로그인 SSR과 WASM home 경계를 흐리는 구현은 중단한다.
- 관리자 로그인 WASM을 즉시 제거하지 말고, 먼저 migration WBS와 대체 SSR 경로를 정의한다.
- JWT를 브라우저 저장소에 넣는 설계는 중단한다.
- 완료 데이터 정의가 없는 WBS는 완료로 보지 않는다.