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