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
+67 -79
View File
@@ -239,7 +239,7 @@ git push http://gitea.taxbaik.com/kjh2064/taxbaik.git master
1. git push 완료 → Gitea Actions 자동 trigger
2. `.gitea/workflows/deploy.yml` 실행
3. 빌드 → 배포 → 서비스 재시작
4. 진행 상황: `https://gitea.taxbaik.com/kjh2064/taxbaik/actions`
4. 진행 상황은 Gitea Actions에서 확인
## 🏗️ **아키텍처 리팩토링 (API-First 전환)**
@@ -342,7 +342,7 @@ app.MapRazorComponents<TaxBaik.WasmClient.Components.Admin.App>()
# ✅ 반드시 설정해야 함
export ASPNETCORE_ENVIRONMENT=Production
export ASPNETCORE_URLS="http://127.0.0.1:$TARGET_PORT"
export ConnectionStrings__Default="Host=localhost;Database=taxbaikdb;Username=taxbaik;Password=<실제비밀>"
export ConnectionStrings__Default="Host=localhost;Database=taxbaikdb;Username=taxbaik;Password=<secret>"
export DOTNET_PRINT_TELEMETRY_MESSAGE=false
# ❌ 누락하면 배포 실패 (Missing connection string)
@@ -624,9 +624,9 @@ src/ 빌드 가능한 .NET 소스 전체 (CI는 이 폴더
```
**경로:**
- 홈페이지: `/taxbaik` (Razor Pages)
- 관리자: `/taxbaik/admin` (Blazor WebAssembly - CSR)
- 로그인: `/taxbaik/admin/login`
- 홈페이지: `/`
- 관리자: `/admin` (Blazor WebAssembly - CSR)
- 로그인: `/admin/login`
**렌더링 방식**:
- 공개 사이트: SSR (Razor Pages) - SEO 최적화
@@ -725,9 +725,9 @@ ssh -L 5432:127.0.0.1:5432 kjh2064@178.104.200.7
cd src/TaxBaik.Web
dotnet run
# 접속:
# - 홈페이지: http://localhost:5001/taxbaik
# - 관리자: http://localhost:5001/taxbaik/admin/login
# - 로그인: admin / <TAXBAIK_ADMIN_TEST_PASSWORD>
# - 홈페이지: http://localhost:5001/
# - 관리자: http://localhost:5001/admin/login
# - 로그인: admin / Admin@123456
```
**장점**:
@@ -741,7 +741,7 @@ dotnet run
```json
{
"ConnectionStrings": {
"Default": "Host=localhost;Database=taxbaikdb;Username=taxbaik;Password=XXXXXXXX"
"Default": "Host=localhost;Database=taxbaikdb;Username=taxbaik;Password=<secret>"
}
}
```
@@ -779,18 +779,10 @@ CREATE TABLE IF NOT EXISTS new_table (
#### 계정 정보 (마이그레이션 V013)
**프로덕션 계정** (admin):
- 사용자명: `admin`
- 비밀번호: API로 설정 (reset-password 엔드포인트)
- 용도: 프로덕션 관리자
- 권한: 모든 관리 기능 액세스
**테스트 계정** (test_admin):
- 사용자명: `test_admin`
- 비밀번호: API로 설정 (reset-password 엔드포인트)
- 용도: E2E Playwright 자동 테스트
- 권한: admin과 동일
- 환경: 로컬/CI 테스트만
**계정 기준**
- 운영 관리자: `admin / Admin@123456`
- E2E 자동화: `test_admin / TestAdmin@123456`
- 둘 다 관리자 권한이며, 운영 변경은 `admin`, 반복 검증은 `test_admin`을 사용한다.
#### 비밀번호 관리 (API 기반)
@@ -836,13 +828,13 @@ Content-Type: application/json
**테스트 계정 검증**:
```bash
# admin 계정 로그인
curl -X POST http://localhost:5001/taxbaik/api/auth/login \
# 운영 관리자 로그인
curl -X POST http://localhost:5001/api/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"Admin@123456"}'
# test_admin 계정 로그인
curl -X POST http://localhost:5001/taxbaik/api/auth/login \
# E2E 자동화 계정 로그인
curl -X POST http://localhost:5001/api/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"test_admin","password":"TestAdmin@123456"}'
```
@@ -928,7 +920,7 @@ git push ssh://git@gitea.taxbaik.com:2222/kjh2064/taxbaik.git master
1. git push 성공 → master 브랜치에 커밋
2. Gitea Actions CI/CD 자동 trigger (.gitea/workflows/deploy.yml)
3. 빌드 → 배포 → 서비스 재시작 자동 실행
4. 배포 진행 상황: `https://gitea.taxbaik.com/kjh2064/taxbaik/actions`
4. 배포 진행 상황은 Gitea Actions에서 확인
---
@@ -945,7 +937,7 @@ ssh kjh2064@178.104.200.7
3000 : Gitea Web (localhost만, proxy via /를 통해)
2222 : Gitea SSH (공개)
5000 : QuantEngine Blazor (localhost, proxy via /quant/)
5001 : TaxBaik.Web (공개 사이트 + 관리자 통합, localhost, proxy via /taxbaik)
5001 : TaxBaik.Web (공개 사이트 + 관리자 통합, localhost, proxy via /)
5432 : PostgreSQL (localhost 바인드)
```
@@ -954,12 +946,12 @@ ssh kjh2064@178.104.200.7
배포는 수동 실행이 아니라 **Gitea Actions CI/CD**만 사용한다.
**무중단 Green-Blue 배포 아키텍처 (2026-06-30 적용 완료)**:
1. **프록시 레이어**: 포트 `5001`에서 영구 가동되는 초경량 .NET TCP 프록시([TaxBaik.Proxy])가 수신 대기합니다. Nginx는 `/taxbaik` 트래픽을 기존과 같이 `5001`로 중계합니다.
1. **프록시 레이어**: 포트 `5001`에서 영구 가동되는 초경량 .NET TCP 프록시([TaxBaik.Proxy])가 수신 대기합니다. Nginx는 루트 트래픽을 `5001`로 중계합니다.
2. **동적 포트 스위칭**: 프록시는 요청이 들어올 때마다 `/home/kjh2064/taxbaik_port` 파일을 읽어 active 포트(5003 또는 5004)를 판단하고 트래픽을 포워딩합니다.
3. **배포 흐름 (`deploy_gb.sh`)**:
- Gitea Actions가 코드를 build/publish 후 압축하여 서버에 업로드합니다.
- 서버의 배포 스크립트([deploy_gb.sh])가 실행되어 현재 미사용 중인 예비 포트(Target Port: 5003 또는 5004)를 파악합니다.
- 예비 포트에서 새 .NET 웹 앱을 실행하고 `http://127.0.0.1:$target_port/taxbaik/healthz` 헬스 체크를 통과할 때까지 폴링(최대 60초)합니다.
- 예비 포트에서 새 .NET 웹 앱을 실행하고 `http://127.0.0.1:$target_port/healthz` 헬스 체크를 통과할 때까지 폴링(최대 60초)합니다.
- 헬스 체크 성공 시 `/home/kjh2064/taxbaik_port` 파일에 새 포트 번호를 기입하여 **트래픽을 즉시 무중단 전환**합니다.
- 기존 포트에서 동작하던 구버전 .NET 프로세스를 종료(`kill -15`)합니다.
- 만약 헬스 체크 실패 시 새 프로세스만 강제 종료하고 배포를 롤백하여 실서비스 다운타임을 방지합니다.
@@ -968,7 +960,7 @@ ssh kjh2064@178.104.200.7
```bash
export ASPNETCORE_ENVIRONMENT=Production
export ASPNETCORE_URLS="http://127.0.0.1:$TARGET_PORT"
export ConnectionStrings__Default="Host=localhost;Database=taxbaikdb;Username=taxbaik;Password=taxbaik123"
export ConnectionStrings__Default="Host=localhost;Database=taxbaikdb;Username=taxbaik;Password=<secret>"
export DOTNET_PRINT_TELEMETRY_MESSAGE=false
```
@@ -1012,7 +1004,7 @@ export DOTNET_PRINT_TELEMETRY_MESSAGE=false
# 실제 로드되는 파일: /etc/nginx/sites-available/taxbaik-domains.conf
# (sites-enabled/taxbaik-domains.conf 심볼릭 링크로 활성화됨)
location /taxbaik {
location / {
proxy_pass http://127.0.0.1:5001;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
@@ -1026,27 +1018,27 @@ location /taxbaik {
}
```
**참고**: 단일 `/taxbaik` 블록이 공개 사이트와 관리자 Blazor 회로를 모두 처리합니다. 운영은 `5001` 통합 앱 기준이며, 설정 반영은 CI 배포로만 수행한다.
**참고**: 단일 루트 블록이 공개 사이트와 관리자 Blazor 회로를 모두 처리합니다. 운영은 `5001` 통합 앱 기준이며, 설정 반영은 CI 배포로만 수행한다.
**⚠️ 중요: 실제 로드되는 Nginx 설정 파일 확인 필수 (2026-07-03 장애로 확인)**:
- `nginx.conf``include /etc/nginx/sites-enabled/*;`만 로드한다. `/etc/nginx/sites-available/`에 파일이 있어도 `sites-enabled/`에 심볼릭 링크되어 있지 않으면 **절대 반영되지 않는다**.
- 이 서버는 `sites-available/default`가 아니라 `sites-available/taxbaik-domains.conf` (→ `sites-enabled/taxbaik-domains.conf`)가 실제로 로드되는 파일이다. `default`를 아무리 수정해도 효과가 없다.
- 실제 로드 파일을 찾는 법: `ls -la /etc/nginx/sites-enabled/` 로 심볼릭 링크 대상을 먼저 확인한 뒤 그 파일을 수정한다.
- **불변식**: `taxbaik-domains.conf``location /``location /taxbaik` 모두 항상 `127.0.0.1:5001` (TaxBaik.Proxy)만 가리켜야 한다. `5003`/`5004`(Green-Blue 앱 포트)를 직접 하드코딩하면 포트 전환 시 죽은 포트를 가리키게 되어 502/404가 발생한다. 이 설정은 배포마다 바뀔 필요가 없다 — 프록시가 `~/taxbaik_port` 파일을 읽어 자동으로 활성 포트에 연결한다.
- **불변식**: `taxbaik-domains.conf``location /` 항상 `127.0.0.1:5001` (TaxBaik.Proxy)만 가리켜야 한다. `5003`/`5004`(Green-Blue 앱 포트)를 직접 하드코딩하면 포트 전환 시 죽은 포트를 가리키게 되어 502/404가 발생한다. 이 설정은 배포마다 바뀔 필요가 없다 — 프록시가 `~/taxbaik_port` 파일을 읽어 자동으로 활성 포트에 연결한다.
- **trailing slash 주의**: `proxy_pass http://127.0.0.1:5001;` (슬래시 없음)은 원본 요청 경로를 그대로 전달한다. `proxy_pass http://127.0.0.1:5001/;` (슬래시 있음)은 URI를 재작성하는데, `location` 접두사와 슬래시 개수가 안 맞으면 백엔드로 이중 슬래시(`//`)가 전달되어 404가 발생한다. 접두사 매칭 `location`에서는 `proxy_pass`에 trailing slash를 붙이지 않는다.
- **디버깅 팁**: `curl http://127.0.0.1/taxbaik/`처럼 IP로 직접 테스트하면 `Host: 127.0.0.1` 헤더가 `server_name taxbaik.com www.taxbaik.com`과 매칭되지 않아 엉뚱한(또는 기본) server block으로 라우팅될 수 있다. 실제 도메인 기준 server block을 로컬에서 테스트하려면 Host 헤더/SNI를 강제로 지정한다:
- **디버깅 팁**: `curl http://127.0.0.1/`처럼 IP로 직접 테스트하면 `Host: 127.0.0.1` 헤더가 `server_name taxbaik.com www.taxbaik.com`과 매칭되지 않아 엉뚱한(또는 기본) server block으로 라우팅될 수 있다. 실제 도메인 기준 server block을 로컬에서 테스트하려면 Host 헤더/SNI를 강제로 지정한다:
```bash
# HTTP
curl -I -H "Host: www.taxbaik.com" http://127.0.0.1/taxbaik/
curl -I -H "Host: www.taxbaik.com" http://127.0.0.1/
# HTTPS (SNI 포함)
curl -sk -I --resolve www.taxbaik.com:443:127.0.0.1 https://www.taxbaik.com/taxbaik/
curl -sk -I --resolve www.taxbaik.com:443:127.0.0.1 https://www.taxbaik.com/
```
- CI 배포(`deploy.yml`)는 매 배포마다 `sites-enabled/`의 실제 파일을 찾아 위 불변식을 검증하고, 위반 시 배포를 실패 처리한다. 또한 내부 `127.0.0.1:5001` 체크와 별개로 실제 공개 도메인(`https://www.taxbaik.com/`)을 외부에서 호출해 Nginx/Cloudflare 경로 전체를 검증한다 — 내부 체크만으로는 Nginx 설정 오류를 잡지 못하기 때문이다.
**Nginx 보안**:
- `Upgrade` 헤더는 Blazor WebSocket 경로에만 허용하고, 필요 없는 location에는 넣지 않는다.
- `Host`와 `X-Forwarded-Proto`는 유지해 원본 URL과 스킴을 보존한다.
- `/taxbaik/admin`는 robots.txt에서 차단한다.
- `/admin`는 robots.txt에서 차단한다.
---
@@ -1056,7 +1048,7 @@ location /taxbaik {
**환경 변수** (systemd unit file에 설정):
```ini
Environment=ConnectionStrings__Default=Host=localhost;Database=taxbaikdb;Username=taxbaik;Password=XXXXXXXX
Environment=ConnectionStrings__Default=Host=localhost;Database=taxbaikdb;Username=taxbaik;Password=<secret>
```
**절대 appsettings.Production.json에 비밀값을 하드코딩하지 말 것.**
@@ -1302,18 +1294,18 @@ catch
## 8. Blazor Admin 패턴 (통합 Web 앱)
### 8.1 PathBase
전체 앱은 `/taxbaik/` 경로에서 실행:
전체 앱은 `/` 경로에서 실행:
```csharp
// Program.cs
app.UsePathBase("/taxbaik");
app.UsePathBase("/");
```
`@page` 지시문의 경로는 이 기본값에 상대적. 예:
```razor
@page "/admin/login" ← 실제 URL: /taxbaik/admin/login
@page "/admin/blog" ← 실제 URL: /taxbaik/admin/blog
@page "/blog" ← 실제 URL: /taxbaik/blog (Razor Pages)
@page "/admin/login" ← 실제 URL: /admin/login
@page "/admin/blog" ← 실제 URL: /admin/blog
@page "/blog" ← 실제 URL: /blog (Razor Pages)
```
### 8.2 JWT 인증 (LocalStorage + Bearer Token)
@@ -1903,7 +1895,7 @@ Icons.Material.Filled.KeyboardArrowDown// 드롭다운
#### **6. 링크 (MudLink)**
```razor
<MudLink Href="/taxbaik/admin/page" Color="Color.Primary" Class="font-weight-bold">
<MudLink Href="/admin/page" Color="Color.Primary" Class="font-weight-bold">
페이지로 이동 →
</MudLink>
```
@@ -2148,7 +2140,7 @@ else
- [ ] AutoMapper 사용 금지 (수동 매핑)
- [ ] Repository 인터페이스 없이 DB 직접 쿼리
- [ ] Razor Component의 @code에 비즈니스 로직
- [ ] robots.txt에서 `/taxbaik/admin` allow 금지 (disallow 필수)
- [ ] robots.txt에서 `/admin` allow 금지 (disallow 필수)
- [ ] 폼 제출 후 redirect (fire-and-forget 또는 same-page 응답)
- [ ] 절대 `Thread.Sleep` 또는 `Task.Delay` in request handler
- [ ] 운영 서버에서 수동 publish/rsync/파일 교체
@@ -2440,17 +2432,17 @@ psql -U taxbaik -d taxbaikdb -c "\dt"
systemctl status taxbaik
# 엔드포인트 확인
curl http://127.0.0.1:5001/taxbaik
curl http://127.0.0.1:5001/
# Nginx 라우팅 확인
curl http://127.0.0.1/taxbaik
curl http://127.0.0.1/taxbaik/admin/login
curl http://127.0.0.1/
curl http://127.0.0.1/admin/login
```
### E2E 테스트 & 반응형 검증
```bash
# 문의 폼 제출
curl -X POST http://taxbaik.com/taxbaik/contact \
curl -X POST http://taxbaik.com/contact \
-d "name=테스트&phone=010-1234-5678&service_type=사업자세무&message=테스트"
# 관리자 DB에서 확인
@@ -2462,16 +2454,16 @@ SELECT * FROM inquiries ORDER BY created_at DESC LIMIT 1;
**반응형 디자인 E2E 테스트** (test_admin 테스트 계정 사용):
```bash
# Green-Blue 배포 지원:
# - Nginx를 통한 포트 무관 라우팅 (http://localhost/taxbaik)
# - 또는 직접 포트 지정 (http://localhost:5001/taxbaik)
# - Nginx를 통한 포트 무관 라우팅 (http://localhost)
# - 또는 직접 포트 지정 (http://localhost:5001)
# 방법 1: Nginx 거쳐서 (권장 - active 버전 자동 테스트)
export E2E_BASE_URL="http://localhost/taxbaik"
export E2E_BASE_URL="http://localhost"
export E2E_ADMIN_USERNAME="test_admin"
export E2E_ADMIN_PASSWORD="TestAdmin@123456"
# 방법 2: 직접 포트 지정 (5001 또는 5002)
# export E2E_BASE_URL="http://localhost:5001/taxbaik"
# export E2E_BASE_URL="http://localhost:5001"
# Playwright로 반응형 테스트 실행 (8개 디바이스 크기)
npx playwright test admin-responsive.spec.ts
@@ -2482,14 +2474,14 @@ npx playwright test admin-responsive.spec.ts --project="Desktop Chrome"
**테스트 계정 정보** (마이그레이션 V012-V013):
- 사용자명: `test_admin`
- 비밀번호: `TestAdmin@123456` (API reset-password로 설정)
- 비밀번호: `TestAdmin@123456`
- 용도: E2E Playwright 자동 테스트 (실 admin 계정과 완전 분리)
- 권한: admin과 동일
- 비밀번호 변경: `/api/auth/reset-password` API 사용
**프로덕션 E2E 테스트**:
```bash
export E2E_BASE_URL="http://taxbaik.com/taxbaik"
export E2E_BASE_URL="https://taxbaik.com"
export E2E_ADMIN_USERNAME="test_admin"
export E2E_ADMIN_PASSWORD="TestAdmin@123456"
@@ -2515,7 +2507,7 @@ npx playwright test # CI에서 배포 후 자동 실행
#### 1. 배포 알림 전략 (강제 새로고침 금지)
```csharp
// Program.cs - SignalR 배포 알림
app.MapHub<NotificationHub>("/taxbaik/hub/notifications");
app.MapHub<NotificationHub>("/hub/notifications");
// NotificationHub.cs
public async Task NotifyDeploymentStart()
@@ -2562,7 +2554,7 @@ public async Task NotifyDeploymentStart()
protected override async Task OnInitializedAsync()
{
hubConnection = new HubConnectionBuilder()
.WithUrl("/taxbaik/hub/notifications", options =>
.WithUrl("/hub/notifications", options =>
options.AccessTokenProvider = async () =>
await LocalStorage.GetItemAsStringAsync("authToken") ?? "")
.WithAutomaticReconnect()
@@ -2601,7 +2593,7 @@ public async Task NotifyDeploymentStart()
# .gitea/workflows/deploy.yml
- name: Notify deployment start
run: |
curl -X POST "http://127.0.0.1:5001/taxbaik/api/admin/deployment-start" \
curl -X POST "http://127.0.0.1:5001/api/admin/deployment-start" \
-H "Authorization: Bearer ${{ env.INTERNAL_API_TOKEN }}" \
-H "Content-Type: application/json" \
-d '{"message":"New version deploying..."}'
@@ -2784,23 +2776,19 @@ else
```bash
# 1) 실제 로드되는 파일 확인 (sites-available에 있어도 sites-enabled에 링크 안 되면 무효)
ls -la /etc/nginx/sites-enabled/
# 2) 그 파일에서 location / 와 location /taxbaik 이 5001을 가리키는지 확인 (5003/5004 하드코딩 금지)
# 2) 그 파일에서 location / 이 5001을 가리키는지 확인 (5003/5004 하드코딩 금지)
grep -A 2 'location /' /etc/nginx/sites-available/taxbaik-domains.conf
# 3) 실제 도메인 기준 server block으로 로컬 검증 (Host/SNI 강제)
curl -sk -I --resolve www.taxbaik.com:443:127.0.0.1 https://www.taxbaik.com/taxbaik/
curl -sk -I --resolve www.taxbaik.com:443:127.0.0.1 https://www.taxbaik.com/
```
**이번 장애 원인 기록 (2026-06-28, YAML 파싱)**:
- `deploy.yml`의 Telegram 여러 줄 메시지 일부가 YAML 블록 들여쓰기 밖에 있어 Gitea workflow 파서가 실패했다.
- 이후 배포 실행은 되었지만, 운영 `Authentication:*:ClientId`가 빈 값인데 OAuth provider를 무조건 등록해 `ClientId` 예외로 500이 발생했다.
- 외부 OAuth provider는 ClientId/ClientSecret이 모두 있을 때만 등록한다.
**이번 장애 원인 기록 (2026-06-28)**:
- YAML 들여쓰기 오류와 OAuth provider 무조건 등록으로 배포/런타임 장애가 났다.
- OAuth provider `ClientId`/`ClientSecret`이 모두 있을 때만 등록한다.
**이번 장애 원인 기록 (2026-07-03, Nginx 이중 설정 파일 + 죽은 포트 + trailing slash)**:
- CI 배포는 매번 성공으로 표시됐지만 실제 `https://www.taxbaik.com/`은 502, `/taxbaik/`는 404였다. 원인은 세 가지가 겹쳐 있었다.
1. 서버에 Nginx 설정 파일이 두 개 존재했다: `sites-available/default`(문서에 기록되어 있었지만 `sites-enabled/`에 링크되지 않아 **전혀 로드되지 않음**)와 `sites-available/taxbaik-domains.conf`(→ `sites-enabled/`에 실제로 링크되어 로드됨, 문서에는 없었음). 디버깅 초반에 로드되지 않는 `default` 파일만 계속 수정하며 시간을 허비했다.
2. `taxbaik-domains.conf`의 `location /`와 `location /taxbaik`에 Green-Blue 앱 포트(`5003`)가 직접 하드코딩되어 있었다. 포트가 `5004`로 전환된 뒤에도 Nginx는 죽은 `5003`을 계속 가리켜 502가 발생했다.
3. `location /taxbaik`의 `proxy_pass`를 `http://127.0.0.1:5001/`(trailing slash 있음)로 고치자, nginx가 URI를 재작성하며 백엔드로 `//`(이중 슬래시)를 전달해 404가 발생했다. `curl http://backend//` 로 재현 확인 후 trailing slash를 제거해 해결했다.
- 근본 대책: 위 5번 체크리스트를 표준 절차로 추가했고, `deploy.yml`이 매 배포마다 (a) `sites-enabled/`의 실제 파일을 찾아 (b) 5003/5004 하드코딩과 trailing slash 오설정을 하드 실패로 검증하고, (c) 내부 체크와 별개로 `https://www.taxbaik.com/` 실도메인을 외부에서 호출해 Nginx/Cloudflare 경로 전체를 검증하도록 했다 (§6 Nginx 라우팅 참고).
**이번 장애 원인 기록 (2026-07-03)**:
- CI 성공 후에도 502/404가 났고, 원인은 Nginx 파일 선택/죽은 포트 하드코딩/`proxy_pass` 오설정이었다.
- 대책: `sites-enabled/` 기준 검증, 포트 하드코딩 금지, 실도메인 외부 검증을 배포 절차에 포함했다.
---
@@ -2808,14 +2796,14 @@ else
| 문제 | 해결 |
|------|------|
| 앱 시작 안 됨 | `journalctl -u taxbaik -n 50` 로그 확인 |
| DB 연결 실패 | 환경 변수 `ConnectionStrings__Default` 확인 (systemd unit file) |
| 404 /taxbaik | Nginx 설정 재로드: `sudo nginx -t && sudo systemctl reload nginx` |
| Blazor WebSocket 안 됨 | `/taxbaik` location에 `proxy_http_version 1.1`, `Upgrade`, `Connection "Upgrade"` 헤더가 모두 있는지 확인 |
| 배포 후 503 | 서비스 시작 대기 (startup 시간 ~5초), `systemctl status taxbaik` 확인 |
| 로그인 실패 | `admin_users.password_hash`와 bcrypt 해시, `AuthService` 로그, `/api/auth/login` 응답 확인 |
| API 호출 실패 (배포 후) | Green-Blue 배포 시 `ApiClient__BaseUrl` 환경변수 확인 (현재 active 포트 일치하는지) |
| 반응형 CSS 깨짐 | admin.css 로드 확인 (헬스 체크에 포함됨), 브라우저 DevTools에서 viewport 설정 확인 |
| 앱 시작 안 됨 | `journalctl -u taxbaik -n 50` |
| DB 연결 실패 | `ConnectionStrings__Default` |
| 404 / | `sudo nginx -t && sudo systemctl reload nginx` |
| Blazor WebSocket 안 됨 | `location /` 헤더 확인 |
| 배포 후 503 | `systemctl status taxbaik` |
| 로그인 실패 | `admin_users`/`AuthService`/`/api/auth/login` |
| API 호출 실패 (배포 후) | `ApiClient__BaseUrl` active 포트 일치 여부 |
| 반응형 CSS 깨짐 | `admin.css`와 viewport |
---
@@ -2854,7 +2842,7 @@ else
### 13.3 공지사항 (Announcement)
어드민 `/taxbaik/admin/announcements`에서 관리.
어드민 `/admin/announcements`에서 관리.
- **유형**: 일반(info, 파란색) / 배너(banner, 주황색) / 긴급(urgent, 빨간색)
- **게시 기간**: 시작일~종료일 설정 가능. 비우면 즉시~무기한