diff --git a/CLAUDE.md b/CLAUDE.md index 4a913cd..5357d21 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 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(); +builder.Services.AddScoped(); +builder.Services.AddScoped( + sp => sp.GetRequiredService()); +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 테스트