docs: 통합 Web 앱 아키텍처로 CLAUDE.md 업데이트
TaxBaik CI/CD / build-and-deploy (push) Failing after 47s

- 포트 배치: 5001로 통합 (5002 제거)
- 배포 절차: 단일 Web 앱 빌드로 단순화
- 서비스: taxbaik만 관리 (taxbaik-admin 제거)
- Nginx: /taxbaik 블록 하나로 통합
- 파일 구조: Web/Components/Admin으로 명시
- 인증: JWT + localStorage 패턴 문서화

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-06-26 22:39:31 +09:00
parent 57269e281d
commit 713fbde5e4
+103 -75
View File
@@ -11,42 +11,60 @@
## 2. 아키텍처
### 2.1 프로젝트 구조
### 2.1 프로젝트 구조 (통합)
**단일 앱 구조** (소규모 프로젝트 최적화):
```
TaxBaik.Domain 클래스 라이브러리 (엔티티, 인터페이스, enum — 외부 의존성 없음)
TaxBaik.Domain 클래스 라이브러리 (엔티티, 인터페이스, enum)
TaxBaik.Infrastructure 클래스 라이브러리 (Dapper repository, DB 마이그레이션)
TaxBaik.Application 클래스 라이브러리 (서비스, DTO, 비즈니스 로직)
TaxBaik.Web ASP.NET Core 앱 (Razor Pages SSR, port 5001)
TaxBaik.Admin ASP.NET Core 앱 (Blazor Server, port 5002)
TaxBaik.Web ASP.NET Core 앱 (포트 5001)
├─ Pages/ Razor Pages (공개 홈페이지, 블로그, 문의폼)
├─ Components/
│ ├─ (Web pages)
│ └─ Admin/ Blazor Server (관리자 백오피스)
│ ├─ Pages/
│ ├─ Layout/
│ └─ App.razor
└─ Services/ 인증, 블로그, 문의 등
```
**경로:**
- 홈페이지: `/taxbaik` (Razor Pages)
- 관리자: `/taxbaik/admin` (Blazor)
- 로그인: `/taxbaik/admin/login`
### 2.2 계층 책임
- **Domain**: 비즈니스 규칙, 엔티티 정의
- **Infrastructure**: DB 접근, Dapper 구현체, 마이그레이션 실행
- **Application**: 서비스, DTO 매핑, 비즈니스 워크플로우
- **Web**: 공개 사이트 (SEO 최적화, Razor Pages SSR)
- **Admin**: 관리자 백오피스 (실시간 UI, Blazor Server)
- **Web (Pages/)**: 공개 홈페이지 (SEO 최적화, Razor Pages SSR)
- **Web (Components/Admin)**: 관리자 백오피스 (실시간 UI, Blazor Server)
- **Web (Services/)**: 인증(JWT), 블로그, 문의 관리 등
### 2.3 기술 결정 이유
**왜 Razor Pages (Web)인가?**
- Razor Pages는 서버에서 HTML을 렌더링 → Google, Naver가 즉시 크롤 가능
- Blazor Server는 초기 응답이 shell HTML → SEO 불리 (블로그는 구글 검색이 핵심)
**왜 Razor Pages (공개 사이트)인가?**
- 서버에서 HTML을 렌더링 → Google, Naver가 즉시 크롤 가능
- Blazor는 초기 응답이 shell HTML → SEO 불리 (블로그는 검색 유입이 핵심)
**왜 Blazor Server (Admin)인가?**
**왜 Blazor Server (관리자)인가?**
- 관리자는 SEO 불필요 → WebSocket으로 실시간 UI 업데이트 가능
- 기존 QuantEngine 패턴과 일치
- 복잡한 관리 UI를 쉽게 구현
**왜 단일 앱 (통합 Web)인가?**
- 소규모 프로젝트 → 분리의 이점 < 개발 복잡도
- **개발**: 터미널 1개, 포트 1개 (5001)
- **배포**: 앱 1개, DB 마이그레이션 1회
- **유지보수**: 모든 비즈니스 로직 한 곳 (Application)
- **장점**: 기존 분리 구조의 모든 기능 + 간단한 개발 경험
**왜 Dapper인가?**
- QuantEngine에서 이미 사용 중 (팀 지식)
- 팀 기존 지식 (QuantEngine에서 사용)
- 복잡한 조인, 페이징, 성능 제어 용이
- EF Core 대비 SQL 완전 제어 가능
**왜 두 개의 ASP.NET Core 앱인가?**
- Web: 정적 콘텐츠 + Razor Pages
- Admin: 동적 Blazor + WebSocket
- 미들웨어 파이프라인 다름 → 독립 배포로 한쪽 문제가 다른 쪽에 영향 안 함
---
## 3. 로컬 개발 환경 설정
@@ -88,24 +106,27 @@ psql -h localhost -U taxbaik -d taxbaikdb -c "\dt"
dotnet run -p TaxBaik.Web
```
#### 단계 3: 개발 워크플로우
#### 단계 3: 개발 워크플로우 (단일 앱 통합)
```bash
# 터미널 1: SSH 터널 유지
ssh -L 5432:127.0.0.1:5432 kjh2064@178.104.200.7
# 터미널 2: Web 사이트 (Razor Pages)
# 터미널 2: 통합 Web (Razor Pages + Blazor)
cd TaxBaik.Web
dotnet run
# 접속: http://localhost:5001/taxbaik
# 터미널 3: Admin 앱 (Blazor)
cd TaxBaik.Admin
dotnet run
# 접속: https://localhost:5002
# 로그인: admin / admin123
# 접속:
# - 홈페이지: http://localhost:5001/taxbaik
# - 관리자: http://localhost:5001/taxbaik/admin/login
# - 로그인: admin / admin123
```
**장점**:
- ✅ 한 개의 포트 (5001)
- ✅ 한 개의 터미널에서 실행
- ✅ 한 번의 DB 마이그레이션
- ✅ 모든 기능 유지 (JWT 인증, Blazor UI, Razor Pages SEO)
### 3.2 appsettings.json (로컬)
```json
@@ -173,8 +194,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)
5002 : TaxBaik.Admin (localhost, proxy via /taxbaik/admin)
5001 : TaxBaik.Web (공개 사이트 + 관리자 통합, localhost, proxy via /taxbaik)
5432 : PostgreSQL (localhost 바인드)
```
@@ -182,11 +202,10 @@ ssh kjh2064@178.104.200.7
**핵심 전략**: .NET Core shadow copy로 배포 중 무중단 실행
1. **로컬 빌드**:
1. **로컬 빌드** (단일 앱 통합):
```bash
dotnet clean TaxBaik.sln
dotnet publish TaxBaik.Web -c Release -o ./publish/web
dotnet publish TaxBaik.Admin -c Release -o ./publish/admin
dotnet publish TaxBaik.Web -c Release -o ./publish
```
2. **CI/CD 배포** (Gitea Actions):
@@ -213,7 +232,6 @@ ssh kjh2064@178.104.200.7
```bash
# 최근 5개 배포만 유지
ls -dt ~/deployments/taxbaik_* | tail -n +6 | xargs -r rm -rf
ls -dt ~/deployments/taxbaik_admin_* | tail -n +6 | xargs -r rm -rf
```
**systemd 서비스 graceful shutdown 설정**:
@@ -225,19 +243,16 @@ KillMode=mixed # SIGTERM → 30초 대기 → SIGKILL
### 3.4 서비스 파일 위치
```
/etc/systemd/system/taxbaik.service ← Web
/etc/systemd/system/taxbaik-admin.service ← Admin
/etc/systemd/system/taxbaik.service ← 통합 Web 앱 (공개 사이트 + 관리자)
```
### 5.5 배포 디렉토리 구조 (서버)
```
/home/kjh2064/
├── taxbaik_active → ./deployments/taxbaik_20260626_150000/
├── taxbaik_admin_active → ./deployments/taxbaik_admin_20260626_150000/
└── deployments/
├── taxbaik_20260626_150000/ (Web publish 출력)
├── taxbaik_20260626_150000/ (통합 Web publish 출력)
├── taxbaik_20260626_140000/ (이전 버전)
├── taxbaik_admin_20260626_150000/ (Admin publish 출력)
└── ...
```
@@ -253,27 +268,17 @@ KillMode=mixed # SIGTERM → 30초 대기 → SIGKILL
location /taxbaik {
proxy_pass http://127.0.0.1:5001;
proxy_http_version 1.1;
proxy_set_header Connection keep-alive;
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;
proxy_read_timeout 120s;
}
location /taxbaik/admin {
proxy_pass http://127.0.0.1:5002;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
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;
proxy_read_timeout 120s;
}
```
**더 구체적인 경로가 우선**: `/taxbaik` 블록이 `/` Gitea 블록보다 먼저 매칭됨.
**참고**: 단일 `/taxbaik` 블록이 공개 사이트와 관리자 (Blazor WebSocket)를 모두 처리합니다.
---
@@ -341,7 +346,7 @@ public async Task<BlogPost?> GetBySlugAsync(string slug, CancellationToken ct)
- 비동기 메서드: **Async** 접미사 (GetBySlugAsync)
- 비공개 메서드: **Async 접미사 생략 가능**
### 6.2 파일 구조
### 6.2 파일 구조 (통합 Web 앱)
```
Domain/
Entities/BlogPost.cs
@@ -359,12 +364,17 @@ Application/
Web/
Pages/Blog/Index.cshtml
Pages/Blog/Index.cshtml.cs ← PageModel
Pages/Blog/Index.cshtml.cs ← PageModel (공개 사이트)
Components/
Admin/
Pages/Blog/BlogList.razor ← Blazor 관리자 페이지
Layout/MainLayout.razor
App.razor
Services/
AuthService.cs ← JWT 인증
CustomAuthenticationStateProvider.cs
LocalStorageService.cs
wwwroot/css/site.css
Admin/
Components/Pages/Blog/BlogList.razor
Services/AdminAuthStateProvider.cs
```
### 6.3 모든 UI는 한국어
@@ -437,41 +447,60 @@ catch
---
## 8. Blazor Admin 패턴
## 8. Blazor Admin 패턴 (통합 Web 앱)
### 8.1 PathBase
Admin은 `/taxbaik/admin/` 경로에서 실행:
전체 앱은 `/taxbaik/` 경로에서 실행:
```csharp
// Program.cs
app.UsePathBase("/taxbaik/admin");
app.UsePathBase("/taxbaik");
```
`@page` 지시문의 경로는 이 기본값에 상대적. 예:
```razor
@page "/login" ← 실제 URL: /taxbaik/admin/login
@page "/blog" ← 실제 URL: /taxbaik/admin/blog
@page "/admin/login" ← 실제 URL: /taxbaik/admin/login
@page "/admin/blog" ← 실제 URL: /taxbaik/admin/blog
@page "/blog" ← 실제 URL: /taxbaik/blog (Razor Pages)
```
### 8.2 Cookie 인증
### 8.2 JWT 인증 (LocalStorage + Bearer Token)
```csharp
// Program.cs
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(opts => opts.LoginPath = "/login");
app.UseAuthentication();
app.UseAuthorization();
builder.Services.AddScoped<AuthService>();
builder.Services.AddScoped<CustomAuthenticationStateProvider>();
builder.Services.AddScoped<AuthenticationStateProvider>(
sp => sp.GetRequiredService<CustomAuthenticationStateProvider>());
builder.Services.AddCascadingAuthenticationState();
builder.Services.AddAuthorizationCore();
```
### 8.3 모든 페이지에 [Authorize] 추가
토큰은 localStorage에 저장되며, `CustomAuthenticationStateProvider`가 자동으로 복원:
```csharp
// CustomAuthenticationStateProvider.cs
public async Task LoginAsync(string token)
{
await _localStorage.SetItemAsStringAsync("authToken", token);
StateHasChanged(); // Blazor 상태 갱신
}
public async Task LogoutAsync()
{
await _localStorage.RemoveItemAsync("authToken");
StateHasChanged();
}
```
### 8.3 모든 Admin 페이지에 [Authorize] 추가
```razor
@* _Imports.razor *@
@* Components/Admin/_Imports.razor *@
@attribute [Authorize]
```
Login 페이지만 [AllowAnonymous]:
Admin 로그인 페이지만 [AllowAnonymous]:
```razor
@page "/login"
@page "/admin/login"
@attribute [AllowAnonymous]
```
@@ -606,16 +635,15 @@ ssh kjh2064@178.104.200.7
# DB 확인
psql -U kjh2064 -d taxbaikdb -c "\dt"
# 서비스 상태
systemctl status taxbaik taxbaik-admin
# 서비스 상태 (통합 Web 앱만)
systemctl status taxbaik
# 엔드포인트 확인
curl http://127.0.0.1:5001/health
curl http://127.0.0.1:5002/health
curl http://127.0.0.1:5001/taxbaik
# Nginx 라우팅 확인
curl http://127.0.0.1/taxbaik
curl http://127.0.0.1/taxbaik/admin
curl http://127.0.0.1/taxbaik/admin/login
```
### E2E 테스트