refactor admin UX and stabilize shell
TaxBaik CI/CD / build-and-deploy (push) Failing after 2m20s

This commit is contained in:
2026-07-09 00:03:33 +09:00
parent a2f94e2b5e
commit 89fa51efe2
91 changed files with 1633 additions and 1030 deletions
+4 -4
View File
@@ -123,12 +123,12 @@ sudo systemctl status taxbaik-admin --no-pager -l
- 콤보 값은 [COMMON_CODE_POLICY.md](./COMMON_CODE_POLICY.md)를 1차 기준으로 삼는다.
- 로그인 화면은 배포 전 브라우저 실증이 필수다. `dotnet build`만으로 로그인 화면 정상 표시를 완료로 선언하지 않는다.
- 로그인 화면 실증 기준은 최소 1회 실제 브라우저 응답, 로그인 폼 렌더, 입력 포커스 가능 여부 확인이다.
- 공개 루트와 관리자 루트는 반드시 분리 검증한다. `https://www.taxbaik.com/` 은 공용 홈페이지 제목을 가져야 하고, `https://www.taxbaik.com/taxbaik/admin/login` 은 관리자 제목을 가져야 한다.
- 공개 루트와 관리자 루트는 반드시 분리 검증한다. `https://www.taxbaik.com/` 은 공용 홈페이지 제목을 가져야 하고, `https://www.taxbaik.com/admin/login` 은 관리자 제목을 가져야 한다.
- DB 연결 문자열 검증은 배포 전에 먼저 실패해야 한다. `scripts/validate_locked_db_connection.py`가 실패하면 어떤 브라우저 테스트도 진행하지 않는다.
- 최종 완료 검증은 실제 서비스 도메인에서만 수행한다. 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` 순서로 확인하고 결과를 기록한다.
- 루트 리다이렉트를 추가할 때는 반드시 루프 여부를 `curl`로 먼저 확인하고 나서만 반영한다. `/``/admin/login` 서로를 다시 가리키면 안 된다.
- nginx의 `location /`를 건드린 뒤에는, 변경 직후 `curl -I https://www.taxbaik.com/`, `curl -I https://www.taxbaik.com/admin/login` 순서로 확인하고 결과를 기록한다.
- 클라이언트 로그와 장애 진단 로그는 운영 데이터가 아니라 관측 데이터로 본다. 저장 실패는 사용자 흐름을 막지 않으며, 수집 실패 자체를 재시도 루프로 증폭하지 않는다.
- 동일 오류의 텔레그램 재알림은 일정 기간 1회로 제한하고, 재전송 목적의 루프는 금지한다.
- 데이터가 오류 재현에 필요하면 `entity`, `entityId`, `dataKey` 같은 최소 식별자만 남기고, 원문 데이터 전체를 로그에 싣지 않는다.
@@ -253,6 +253,6 @@ public class GetBlogEndpoint : Endpoint<GetBlogRequest, GetBlogResponse>
- UI가 저장한다고 보이는 필드를 API/Application이 저장하지 않으면 릴리스하지 않는다.
- 운영 배포 검증이 CI 밖에서만 가능하면 완료로 보지 않는다.
- 빌드, Playwright 시나리오, 화면 캡처, DOM 캡처, 데이터 일치 증빙이 없는 상태에서 완료라고 쓰지 않는다.
- `UsePathBase` 아래에서 루트 기본값을 만들려면 리다이렉트보다 실제 최종 콘텐츠가 나오는지 먼저 확인한다. 루트가 `/taxbaik/`를 다시 반환하면 즉시 중단한다.
- 루트 기본값을 만들려면 리다이렉트보다 실제 최종 콘텐츠가 나오는지 먼저 확인한다. 루트가 관리자 하위 경로를 다시 반환하면 즉시 중단한다.
- lint가 필요하면 build보다 먼저, 그리고 배포보다 훨씬 앞단에서 실패시키고, lint가 없는 프로젝트에 억지로 새 lint 체계를 만들지 않는다.
- 데이터 모델을 추측해서 세무 규칙이나 더존 UX 관습을 왜곡해 구현하지 않는다.
+5 -5
View File
@@ -19,12 +19,12 @@
| 영역 | 라우트/파일 | 기준 |
| --- | --- | --- |
| Public Home/Blog/Contact | `/taxbaik/`, `/taxbaik/blog`, `/taxbaik/contact` | 서버 사이드 렌더링, SEO 우선, WASM 의존 금지 |
| Admin Blog | `/taxbaik/admin/blog`, `/taxbaik/admin/blog/create`, `/taxbaik/admin/blog/{id}/edit` | 클라이언트 사이드 Blazor WebAssembly, API-first 클라이언트 경유, JS 최소화 |
| Admin Inquiry | `/taxbaik/admin/inquiries`, `/taxbaik/admin/inquiries/create`, `/taxbaik/admin/inquiries/{id}/edit` | 클라이언트 사이드 Blazor WebAssembly, 공개 접수/관리자 등록/상태 변경 분리 |
| Public API | `/taxbaik/api/*` | JWT 인증, ProblemDetails 오류, DTO 입출력 |
| Public Home/Blog/Contact | `/`, `/blog`, `/contact` | 서버 사이드 렌더링, SEO 우선, WASM 의존 금지 |
| Admin Blog | `/admin/blog`, `/admin/blog/create`, `/admin/blog/{id}/edit` | 클라이언트 사이드 Blazor WebAssembly, API-first 클라이언트 경유, JS 최소화 |
| Admin Inquiry | `/admin/inquiries`, `/admin/inquiries/create`, `/admin/inquiries/{id}/edit` | 클라이언트 사이드 Blazor WebAssembly, 공개 접수/관리자 등록/상태 변경 분리 |
| Public API | `/api/*` | JWT 인증, ProblemDetails 오류, DTO 입출력 |
| CI/CD | `.gitea/workflows/deploy.yml`, `.gitea/workflows/browser-e2e.yml` | 수동 배포 금지, 배포본 E2E 통과 후 완료 |
| Smoke | `scripts/taxbaik-smoke.sh` | 루트는 공용 홈, `/taxbaik/admin/login` 은 관리자 화면이어야 함 |
| Smoke | `scripts/taxbaik-smoke.sh` | 루트는 공용 홈, `/admin/login` 은 관리자 화면이어야 함 |
## Shared Component Map
+16 -203
View File
@@ -2,7 +2,7 @@
## 📋 개요
**상대경로 기반 Nginx 설정**으로 PathBase 제거 후 경로를 올바르게 매핑하는 하네스.
**root `/`와 `/admin` 경로를 검증하는 Nginx 하네스**.
---
@@ -11,8 +11,7 @@
```
공개 URL: https://www.taxbaik.com/
백엔드: http://127.0.0.1:5001/
Nginx: 경로 변환 없음 (상대경로 그대로 전달)
Nginx: 경로 변환 없음
```
---
@@ -31,16 +30,14 @@ Nginx: 경로 변환 없음 (상대경로 그대로 전달)
### 필수 server 블록
```nginx
# 1. TaxBaik (HTTPS 443)
server {
server_name taxbaik.com www.taxbaik.com;
listen 443 ssl;
ssl_certificate /etc/letsencrypt/live/taxbaik.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/taxbaik.com/privkey.pem;
location / {
proxy_pass http://127.0.0.1:5001/;
# 헤더 설정 (필수)
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
@@ -50,27 +47,6 @@ server {
proxy_set_header X-Forwarded-Proto $scheme;
}
}
# 2. HTTP → HTTPS 리다이렉트
server {
listen 80;
server_name taxbaik.com www.taxbaik.com;
return 301 https://$host$request_uri;
}
# 3. Gitea (선택사항, SSL 없으면 HTTP만)
server {
server_name gitea.taxbaik.com;
listen 80; # SSL 인증서 없으면 HTTP만
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
```
---
@@ -95,7 +71,7 @@ server {
```nginx
# ✅ 올바름 (슬래시 있음)
proxy_pass http://127.0.0.1:5001/;
# 결과: /taxbaik/admin/login → http://127.0.0.1:5001/taxbaik/admin/login
# 결과: /admin/login → http://127.0.0.1:5001/admin/login
```
**규칙 2: 경로 변환 (필요시만)**
@@ -120,34 +96,9 @@ location /api {
# 결과: /api/users → http://127.0.0.1:5001/api//users (이중 슬래시)
```
### 필수 헤더 설정
### 필수 헤더
```nginx
location / {
# 1. HTTP 버전 업그레이드 (HTTP/1.1로 유지)
proxy_http_version 1.1;
# 2. WebSocket 지원 (Blazor SignalR)
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
# 3. 호스트 정보
proxy_set_header Host $host;
# 4. 클라이언트 IP
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# 5. 프로토콜 (HTTP vs HTTPS)
proxy_set_header X-Forwarded-Proto $scheme;
# 6. 캐시 바이패스
proxy_cache_bypass $http_upgrade;
# 7. 타임아웃 (길게)
proxy_read_timeout 120s;
}
```
`location /`에는 `proxy_http_version 1.1`, `Upgrade`, `Connection`, `Host`, `X-Real-IP`, `X-Forwarded-For`, `X-Forwarded-Proto`를 유지한다.
---
@@ -155,24 +106,11 @@ location / {
### Let's Encrypt 인증서
```nginx
server {
listen 443 ssl;
ssl_certificate /etc/letsencrypt/live/taxbaik.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/taxbaik.com/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
}
```
`/etc/letsencrypt/live/taxbaik.com/fullchain.pem``privkey.pem`을 사용한다.
### 보안 헤더
```nginx
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
```
`HSTS`, `nosniff`, `SAMEORIGIN`은 유지한다.
---
@@ -180,149 +118,24 @@ add_header X-XSS-Protection "1; mode=block" always;
### 포트 전환 원리
```
Nginx (고정)
/etc/nginx/sites-available/taxbaik-domains.conf
proxy_pass http://127.0.0.1:5001/; (절대 변경하지 말 것!)
TaxBaik.Proxy (포트 5001)
/home/kjh2064/taxbaik_port 파일 읽기
Active Port: 5003 또는 5004
실제 앱 (http://127.0.0.1:5003 또는 5004)
```
### 중요: Nginx는 수정하지 않음
```bash
# ❌ 절대 하지 말 것: Nginx 설정에 5003/5004 하드코딩
# 이렇게 하면 포트 전환이 작동하지 않음
proxy_pass http://127.0.0.1:5003/; # 위험!
# ✅ 올바름: 프록시 포트 고정
proxy_pass http://127.0.0.1:5001/; # 프록시가 실제 포트로 포워딩
```
Nginx는 `5001` 프록시에만 연결하고, 프록시가 `~/taxbaik_port`를 읽어 `5003/5004` 중 활성 포트로 보낸다. Nginx에 `5003/5004`를 직접 하드코딩하지 않는다.
---
## 🔍 검증 체크리스트
### 설정 적용 전
- [ ] 백업 생성: `sudo cp /etc/nginx/sites-available/taxbaik-domains.conf ...backup`
- [ ] 편집기로 수정: `sudo nano /etc/nginx/sites-available/taxbaik-domains.conf`
- [ ] 문법 검증: `sudo nginx -t`
- 예상: "syntax is ok" + "test is successful"
### 설정 적용 후
- [ ] Nginx 재로드: `sudo systemctl reload nginx`
- [ ] 상태 확인: `sudo systemctl status nginx`
- 예상: "active (running)"
### 실제 동작 확인
```bash
# 1. HTTP → HTTPS 리다이렉트 확인
curl -I http://www.taxbaik.com/
# 예상: HTTP 301 + Location: https://
# 2. HTTPS 접근 확인
curl -I https://www.taxbaik.com/
# 예상: HTTP/2 200
# 3. 로컬호스트 경유 확인
curl -H "Host: www.taxbaik.com" http://127.0.0.1/
# 예상: HTML 응답 (프록시 정상)
# 4. API 엔드포인트 확인
curl https://www.taxbaik.com/api/blog?limit=1
# 예상: JSON 응답
```
---
## 📋 설정 변경 이력
| 날짜 | 변경 사항 | 이유 |
|------|---------|------|
| 2026-07-08 | PathBase 제거 후 상대경로로 통일 | 경로 정규화 |
| 2026-07-08 | proxy_pass 슬래시 명시 | 이중 슬래시 방지 |
| 2026-07-08 | 프록시 포트 5001 고정 | Green-Blue 배포 대응 |
- `sudo nginx -t``sudo systemctl reload nginx`
- `http://www.taxbaik.com/`은 301, `https://www.taxbaik.com/`은 200
- `curl -H "Host: www.taxbaik.com" http://127.0.0.1/`은 HTML 응답
- `https://www.taxbaik.com/api/blog?limit=1`은 JSON 응답
---
## 🆘 문제 해결
### 문제 1: 404 응답
**증상:**
```
curl https://www.taxbaik.com/
# 결과: HTTP 404
```
**진단:**
```bash
# 1. Nginx가 올바른 백엔드 포트로 프록싱하는지 확인
curl -H "Host: www.taxbaik.com" http://127.0.0.1/
# 같은 404? → 백엔드 문제
# 다른 응답? → Nginx 설정 문제
# 2. 포트 5001 확인
ss -tlnp | grep :5001
# 없음? → TaxBaik.Proxy 실행 안 함
# 3. 활성 포트 확인
cat /home/kjh2064/taxbaik_port
# 예상: 5003 또는 5004
```
### 문제 2: 502 Bad Gateway
**증상:**
```
Nginx가 백엔드에 연결 불가
```
**진단:**
```bash
# 1. Nginx 로그 확인
sudo tail -50 /var/log/nginx/error.log
# 찾기: "upstream connect failed"
# 2. 프록시 로그 확인
tail -50 /home/kjh2064/taxbaik_proxy.log
# 3. 앱 포트 확인
ss -tlnp | grep :{5003,5004}
# 없음? → 앱 실행 안 함
```
### 문제 3: SSL 인증서 오류
**증상:**
```
NET::ERR_CERT_AUTHORITY_INVALID
```
**진단:**
```bash
# 1. 인증서 경로 확인
sudo ls -la /etc/letsencrypt/live/taxbaik.com/
# 파일 존재? → OK
# 없음? → certbot으로 새 인증서 생성
# 2. 인증서 유효 기간
sudo openssl x509 -in /etc/letsencrypt/live/taxbaik.com/fullchain.pem -noout -dates
# 3. Nginx 설정에서 경로 확인
sudo grep "ssl_certificate" /etc/nginx/sites-available/taxbaik-domains.conf
```
- 404: `http://127.0.0.1/` 응답, `:5001` 프록시, `taxbaik_port` 값 확인
- 502: Nginx 에러 로그, 프록시 로그, `5003/5004` 앱 포트 확인
- SSL 오류: `letsencrypt` 인증서 경로와 만료일 확인
---
+3 -4
View File
@@ -25,7 +25,7 @@
- 관리자 진단은 `AdminOnly`만 노출한다.
- 브라우저 오류 수집은 사용자 흐름을 막지 않는다.
- 중복 로그는 억제하고, 재시도 루프를 만들지 않는다.
- 하위 경로 `/taxbaik/admin` 기준을 유지한다.
- 하위 경로 `/admin` 기준을 유지한다.
- `App.razor`에 HTML 호스트와 `blazor.webassembly.js`를 다시 넣지 않는다.
## Verification
@@ -34,6 +34,5 @@
1. `dotnet build src/TaxBaik.Web/TaxBaik.Web.csproj -c Debug`
2. `node scripts/route_lint.mjs`
3. 로컬 브라우저에서 `/taxbaik/admin/login` -> `/taxbaik/admin/dashboard`
4. `/taxbaik/admin/client-logs`에서 요약, 그룹, 필터가 보이는지 확인
3. 로컬 브라우저에서 `/admin/login` -> `/admin/dashboard`
4. `/admin/client-logs`에서 요약, 그룹, 필터가 보이는지 확인
+2 -2
View File
@@ -76,7 +76,7 @@ scripts/
| 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 성공 |
| P3 Domain Verify | 실제 도메인 최종 검증 | `https://www.taxbaik.com` 기준 브라우저 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 규칙 확정 |
@@ -128,7 +128,7 @@ WBS 항목은 다음 데이터가 모두 있어야 완료다.
- 결과는 화면 캡처만으로 끝내지 않고 DOM 캡처를 함께 남긴다.
- API 응답 또는 화면 텍스트를 기준으로 기대값과 실제값을 assert한다.
- 실패 시에는 어떤 데이터가 달랐는지 로그로 남긴다.
- 최종 완료 검증은 `https://www.taxbaik.com/taxbaik` 도메인에서 수행한다. IP 직결 주소는 로컬/임시 점검용으로만 쓴다.
- 최종 완료 검증은 `https://www.taxbaik.com` 도메인에서 수행한다. IP 직결 주소는 로컬/임시 점검용으로만 쓴다.
## Deployment and Trouble FAQ