# Engineering Harness

이 문서는 TaxBaik 코드가 매번 흔들리지 않도록 막는 최소 하네스다. 여기에 없는 내용은 추측하지 않고 코드, 테스트, 운영 로그, DB 스키마 중 하나로 확인한다.

## Non-Negotiables

| 항목 | 기준 | 실패 판정 |
| --- | --- | --- |
| Runtime | ASP.NET Core `net10.0` 기준 유지 | 프로젝트별 TargetFramework 불일치 |
| Public UI | 홈페이지/공개 페이지는 서버 사이드 렌더링 기준 | 공개 페이지가 불필요하게 WASM 번들에 의존 |
| Admin UI | 어드민은 클라이언트 사이드 Blazor WebAssembly + MudBlazor + API-first | 어드민 컴포넌트가 Application/Repository를 직접 주입 |
| API | 모든 운영 기능은 `/api/*` DTO 경유 | UI 전용 서비스 호출만 존재 |
| Auth | JWT 인증, 관리자 API는 `[Authorize]` | 익명으로 관리자 데이터 접근 가능 |
| Deploy | Gitea Actions CI/CD만 배포 경로 | 수동 SSH/복사로 운영 반영 |
| Evidence | 빌드, 테스트, E2E, API smoke 로그 | "확인함", "될 것" 같은 진술 |
| Admin Render | Router/Routes에는 전역 `@rendermode`를 두지 않고 페이지별로 지정한다. 로그인 페이지만 `prerender: true`로 최초 HTML에 폼을 포함시키고, 나머지 `[Authorize]` 페이지는 `prerender: false`를 유지한다 | Router/Routes에 전역 렌더모드가 다시 생기거나, 로그인 폼이 최초 HTML에 없다 |
| KST Timestamp | CI/배포/백업 폴더명과 추적 일시는 `TZ=Asia/Seoul` | `date`가 기본 UTC 또는 서버 로캘에 종속 |
| Repo Root | 소스는 `src/`, 문서는 `docs/`, 테스트는 `tests/`, 스크립트는 `scripts/`, 마이그레이션은 `db/`, 배포 참조 설정은 `deploy/`에 둔다. 루트에는 진입점 설정(`CLAUDE.md`, `README.md`, `.gitignore`, `package.json` 등)만 남긴다 | 루트에 스크린샷/로그/1회성 디버그 스크립트/빌드 산출물이 커밋된다 |

## Architecture Guardrails

- Domain은 엔티티, enum, repository interface만 가진다.
- Application은 use case와 검증 규칙을 가진다. HTTP, JS, MudBlazor, DB 연결 세부를 모른다.
- Infrastructure는 Dapper SQL과 외부 시스템 구현을 가진다.
- Web은 Controller, 공개 Razor Pages SSR, Blazor host, 인증/서빙 설정을 가진다.
- Web.Client/Admin UI는 클라이언트 사이드 Blazor WebAssembly로 본다. 서버 DI 서비스에 의존하지 않고 API client만 호출한다.
- 관리자 호스트가 prerender를 사용하더라도 데이터 접근 원칙은 WASM + API-first다. prerender는 초기 마크업용이며 비즈니스 로직의 근거가 아니다.
- 어드민 기본 렌더는 WASM이다. 다만 초기 흰 화면 방지 목적의 셸 프리렌더와 로그인 화면의 서버 프리렌더는 허용한다. 비즈니스 로직은 여전히 API-first다.
- 로그인 화면은 예외적으로 “먼저 보여야 하는 화면”이다. JS 바인딩/텔레메트리/하이드레이션이 실패해도 로그인 폼 자체는 화면에 남아 있어야 하며, 실패 시 흰 화면이나 빈 본문을 허용하지 않는다.
- 로그인 화면은 공통 추적보다 가시성을 우선한다. 추적은 보조이며, 로그인 폼 렌더를 가로막는 코드는 금지한다.
- 로그인 화면의 JS는 `try/catch`로 감싸고, 실패해도 사용자 입력과 화면 표시를 막지 않아야 한다.
- JavaScript는 최소화한다. 브라우저 API, 인증 토큰 저장, 서드파티 편집기처럼 Blazor/MudBlazor만으로 해결하기 부적절한 경우에만 JS module로 격리한다.
- 상속은 프레임워크 요구 또는 명확한 다형성 모델에만 사용한다. 폼/테이블/CRUD 재사용은 기본적으로 컴포넌트 합성과 작은 service/client로 처리한다.
- 과유불급을 지킨다. 실제 재사용이 2곳 미만이면 새 추상화를 만들지 말고 기존 컴포넌트를 직접 조합한다.
- CI, 배포 폴더명, 백업명, 버전 추적에 쓰는 시간 문자열은 `TZ=Asia/Seoul`을 기본으로 한다.
- 클라이언트 오류 수집은 서버/브라우저를 보호하는 목적의 제한형 수집으로만 운영한다. 건당 비동기 전송, 중복 억제, 분당 상한, 서버 rate limit, 실패 시 조용히 폐기, 재시도 폭주 금지.
- 브라우저에서 발생한 JS 오류는 운영 장애 탐지를 위한 샘플 데이터로만 취급하고, 전체 이벤트 스트림을 보존하려는 설계는 금지한다.
- 텔레그램 알림은 운영자의 주의 채널이지 이벤트 버스가 아니다. 같은 원인/같은 기간의 중복 알림은 억제하고, 리포트/오류/문의/시작 장애는 종류별 시간창을 분리한다.
- 오류 알림에는 재현성 6요소를 포함한다: 화면, 기능, 액션, 단계, 데이터 식별자, 현재 라우트. 이 정보가 없으면 운영 대응이 끝나지 않은 것으로 본다.
- 루트에 새 파일을 직접 추가하지 않는다. 소스는 `src/`, 문서는 `docs/`, 테스트는 `tests/`, 스크립트는 `scripts/`, 마이그레이션은 `db/`, 배포 참조 설정은 `deploy/`에 둔다.
- 임시/스크래치 작업(스크린샷, 1회성 디버그 스크립트, 로그)은 저장소 밖(OS/세션 임시 폴더)에서 하고 절대 커밋하지 않는다. 저장소 안에서 꼭 필요하면 `.gitignore`에 등록된 `.scratch/`만 사용한다.
- 커밋 전 `git status`로 루트에 낯선 파일이 생기지 않았는지 확인한다. 빌드 산출물(runtimeconfig.json, deps.json, wwwroot 산출물 등)이 루트나 프로젝트 폴더 밖에 커밋되면 안 된다.
- 재현 맥락은 페이지별 수동 JS 호출이 아니라 `AdminTelemetryContext` 같은 공통 컴포넌트가 담당한다. 새 어드민 화면은 레이아웃 경유 기본값을 자동 상속해야 하며, 예외만 명시적으로 덮어쓴다.

## Code Quality Harness

| 원칙 | 적용 방식 |
| --- | --- |
| SOLID | 페이지는 orchestration만, 검증은 Application, 저장은 Repository, HTTP 계약은 DTO |
| 유지보수 | Blog/Inquiry 같은 CRUD는 `List`, `Form`, `Client`, `Dto`, `Validator` 패턴으로 고정 |
| 리팩토링 | 동작 보존 테스트를 먼저 추가하고 작은 단위로 이동 |
| 일관성 | 오류 응답은 ProblemDetails, 페이징은 `{ data, total, page, pageSize }` |
| 파편화 방지 | 같은 필드/상태/서비스유형 문자열은 enum/상수/공통 코드 중 하나로 단일화 |
| 과유불급 | 추상화는 2개 이상 실제 사용처가 생긴 뒤 도입 |
| 정규화 | 고객, 문의, 상담, 계약, 세금신고는 원천 테이블을 분리 |
| 역정규화 | 대시보드/검색/운영 요약용 스냅샷만 허용하고 원천 id와 갱신 시점을 저장 |
| 충돌방지 | 수정 API는 가능하면 `updatedAt` 또는 row version 기반 충돌 감지를 둔다 |
| 더존 UX 정신 | 더존 세무회계프로그램처럼 고밀도, 표준 동선, 빠른 입력, 상태 가시성, 회귀 최소화를 기본 UX 원칙으로 삼는다 |
| 추측금지 | 세법, 세율, 더존 필드, 운영 계정, 배포 결과는 공식 자료/코드/DB/로그 없이는 단정하지 않는다 |
| JS 최소화 | Blazor/MudBlazor 우선, 불가피한 JS는 module + dispose + 테스트 가능한 얇은 wrapper |
| 공통코드 | 상태/유형/출처/위험도는 `common_codes`를 우선 소스로 사용하고 화면 하드코딩을 금지 |

## Data Integrity Harness

- DB 제약 조건이 1차 방어선이다: NOT NULL, UNIQUE, FK, CHECK, index.
- Application validation은 사용자 메시지와 use case 규칙을 담당한다.
- UI validation은 빠른 피드백일 뿐이며 유일한 검증으로 보지 않는다.
- 관리자 수정 화면에 노출한 필드는 실제 저장되어야 한다. 저장하지 않는 필드는 read-only로 표시한다.
- 상태 전이는 허용 목록을 둔다. 임의 문자열 저장을 금지한다.
- 삭제는 운영 데이터 손실 위험이 있으면 soft delete 또는 archive를 우선 검토한다.
- 콤보 값은 [COMMON_CODE_POLICY.md](./COMMON_CODE_POLICY.md)를 1차 기준으로 삼는다.
- 로그인 화면은 배포 전 브라우저 실증이 필수다. `dotnet build`만으로 로그인 화면 정상 표시를 완료로 선언하지 않는다.
- 로그인 화면 실증 기준은 최소 1회 실제 브라우저 응답, 로그인 폼 렌더, 입력 포커스 가능 여부 확인이다.
- 클라이언트 로그와 장애 진단 로그는 운영 데이터가 아니라 관측 데이터로 본다. 저장 실패는 사용자 흐름을 막지 않으며, 수집 실패 자체를 재시도 루프로 증폭하지 않는다.
- 동일 오류의 텔레그램 재알림은 일정 기간 1회로 제한하고, 재전송 목적의 루프는 금지한다.
- 데이터가 오류 재현에 필요하면 `entity`, `entityId`, `dataKey` 같은 최소 식별자만 남기고, 원문 데이터 전체를 로그에 싣지 않는다.

## API-First Admin Pattern

새 어드민 기능은 클라이언트 사이드 Blazor WebAssembly를 기준으로 아래 구조를 기본 템플릿으로 따른다.

| Layer | Naming | 책임 |
| --- | --- | --- |
| DTO | `CreateXRequest`, `UpdateXRequest`, `XResponse` | HTTP 계약 |
| Controller | `XController` | 인증, 라우팅, status code, ProblemDetails |
| Client | `IXBrowserClient`, `XBrowserClient` | JWT 포함 HTTP 호출 |
| Page | `XList.razor`, `XCreate.razor`, `XEdit.razor` | 화면 상태와 navigation |
| Form | `XForm.razor` | 입력 컴포넌트와 UI validation |
| Tests | unit + Playwright/API smoke | 회귀 방지 |

## FastEndpoints Framework

새 API 엔드포인트는 Controllers 대신 **FastEndpoints**로 구현한다.

| 규칙 | 실행 |
| --- | --- |
| Library | `FastEndpoints` v5.30.0 이상 |
| 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 내장) |
| Registration | `builder.Services.AddFastEndpoints()` + `app.MapFastEndpoints()` |
| Coexistence | Controllers와 FastEndpoints는 PathBase 내에서 병행 가능 (URL 충돌만 피함) |

**주의**: 기존 Controllers에서 FastEndpoints로 마이그레이션 시, 기존 엔드포인트 URL(`/api/*`)이 변경되지 않도록 명시적 라우팅을 지정한다.

```csharp
public class GetBlogEndpoint : Endpoint<GetBlogRequest, GetBlogResponse>
{
    public override void Configure()
    {
        Get("/api/blog/{id}");
        AllowAnonymous();
    }

    public override async Task HandleAsync(GetBlogRequest req, CancellationToken ct)
    {
        // Logic here
    }
}
```

## Rendering Boundary

| 영역 | 렌더링 | 데이터 접근 |
| --- | --- | --- |
| Public Home/Blog/Contact | 서버 사이드 렌더링 | 서버 Application Service 직접 사용 가능 |
| Admin | 클라이언트 사이드 Blazor WebAssembly | JWT 포함 HTTP API만 사용 |
| Shared DTO | 서버/클라이언트 공유 가능 | UI 전용 상태와 DB 엔티티를 섞지 않음 |

공개 페이지의 SEO와 초기 로딩은 SSR로 최적화한다. 어드민은 앱처럼 동작해야 하므로 WebAssembly와 API 계약을 기준으로 설계한다.

## CI Harness

완료는 로컬 성공이 아니라 CI와 배포본 성공이다.

| Gate | Command/Check | Target |
| --- | --- | --- |
| Build | `dotnet build src/TaxBaik.sln -c Release --no-restore` | error 0 |
| Unit | `dotnet test src/TaxBaik.sln -c Release --no-build` | failed 0 |
| Browser | `npx playwright test --project="Desktop Chrome"` | failed 0 |
| API Smoke | login + protected admin API curl | HTTP 2xx |
| Deploy | `.gitea/workflows/deploy.yml` | success |
| Post Deploy | `.gitea/workflows/browser-e2e.yml` | success |

### Gitea Auth Harness

- Gitea API와 workflow dispatch에는 `GITEA_TOKEN_TAXBAIK`만 사용한다.
- `GITEA_TOKEN`은 쓰지 않는다.
- 인증 헤더는 `Authorization: token <GITEA_TOKEN_TAXBAIK>`를 기본으로 한다.
- 토큰 검증은 먼저 `GET /api/v1/user`로 확인하고, 그 다음 `workflow_dispatch`를 실행한다.
- `401 invalid username, password or token`이 나오면 토큰 이름, 공백, 환경 변수 scope를 먼저 확인한다.

## Stop Conditions

- 동일 개념이 3곳 이상 다른 이름/계약으로 구현되면 기능 추가를 중단하고 정리한다.
- UI가 저장한다고 보이는 필드를 API/Application이 저장하지 않으면 릴리스하지 않는다.
- 운영 배포 검증이 CI 밖에서만 가능하면 완료로 보지 않는다.
- 데이터 모델을 추측해서 세무 규칙이나 더존 UX 관습을 왜곡해 구현하지 않는다.
