diff --git a/src/TaxBaik.Portal.Client/Components/Portal/App.razor b/src/TaxBaik.Portal.Client/Components/Portal/App.razor
new file mode 100644
index 0000000..d1bf472
--- /dev/null
+++ b/src/TaxBaik.Portal.Client/Components/Portal/App.razor
@@ -0,0 +1,160 @@
+@using Microsoft.AspNetCore.Components.Web
+
+
+
+
+
+ 백원숙 세무회계 - 관리자
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 연결 재설정 중...
+ 새로운 버전으로 업데이트되었습니다.
+ 자동으로 페이지를 새로고침합니다. 잠시만 기다려주세요.
+
+
+
+
+
+
+
+
+
+
+
+
+
+@code {
+ private bool isDarkMode = false;
+ private MudTheme mudTheme = new()
+ {
+ Palette = new PaletteLight()
+ {
+ Primary = "#1976D2",
+ PrimaryContrastText = "#FFFFFF",
+ Secondary = "#2D9F7E",
+ SecondaryContrastText = "#FFFFFF",
+ Tertiary = "#FF8A50",
+ TertiaryContrastText = "#FFFFFF",
+ Surface = "#F5F7FA",
+ Background = "#FFFFFF",
+ BackgroundGrey = "#F8F9FB",
+ DrawerBackground = "#FFFFFF",
+ DrawerText = "#424242",
+ AppbarBackground = "#FFFFFF",
+ AppbarText = "#424242",
+ TextPrimary = "#1A1A1A",
+ TextSecondary = "#64748B",
+ TextDisabled = "#94A3B8",
+ ActionDefault = "#1976D2",
+ ActionDisabled = "#BDBDBD",
+ Divider = "#E2E8F0",
+ DividerLight = "#F1F5F9",
+ Error = "#DC2626",
+ ErrorContrastText = "#FFFFFF",
+ Warning = "#F59E0B",
+ WarningContrastText = "#FFFFFF",
+ Info = "#06B6D4",
+ InfoContrastText = "#FFFFFF",
+ Success = "#16A34A",
+ SuccessContrastText = "#FFFFFF",
+ },
+ LayoutProperties = new LayoutProperties()
+ {
+ DefaultBorderRadius = "6px"
+ },
+ Typography = new Typography()
+ {
+ Default = new Default()
+ {
+ FontSize = ".8125rem",
+ FontWeight = 400,
+ LineHeight = 1.5
+ },
+ H1 = new H1()
+ {
+ FontSize = "1.75rem",
+ FontWeight = 600,
+ LineHeight = 1.2
+ },
+ H2 = new H2()
+ {
+ FontSize = "1.5rem",
+ FontWeight = 600,
+ LineHeight = 1.3
+ },
+ H3 = new H3()
+ {
+ FontSize = "1.25rem",
+ FontWeight = 600,
+ LineHeight = 1.3
+ },
+ H4 = new H4()
+ {
+ FontSize = "1.1rem",
+ FontWeight = 600,
+ LineHeight = 1.4
+ },
+ H5 = new H5()
+ {
+ FontSize = "0.95rem",
+ FontWeight = 500,
+ LineHeight = 1.4
+ },
+ H6 = new H6()
+ {
+ FontSize = "0.85rem",
+ FontWeight = 500,
+ LineHeight = 1.5
+ }
+ }
+ };
+}
diff --git a/src/TaxBaik.Portal.Client/Components/Portal/Layout/BlankLayout.razor b/src/TaxBaik.Portal.Client/Components/Portal/Layout/BlankLayout.razor
new file mode 100644
index 0000000..4f3f76d
--- /dev/null
+++ b/src/TaxBaik.Portal.Client/Components/Portal/Layout/BlankLayout.razor
@@ -0,0 +1,3 @@
+@inherits LayoutComponentBase
+
+@Body
diff --git a/src/TaxBaik.Portal.Client/Components/Portal/Layout/MainLayout.razor b/src/TaxBaik.Portal.Client/Components/Portal/Layout/MainLayout.razor
new file mode 100644
index 0000000..9ce4f48
--- /dev/null
+++ b/src/TaxBaik.Portal.Client/Components/Portal/Layout/MainLayout.razor
@@ -0,0 +1,6 @@
+@inherits LayoutComponentBase
+
+
+
+ @Body
+
diff --git a/src/TaxBaik.Portal.Client/Components/Portal/Pages/Login.razor b/src/TaxBaik.Portal.Client/Components/Portal/Pages/Login.razor
new file mode 100644
index 0000000..9e21ffe
--- /dev/null
+++ b/src/TaxBaik.Portal.Client/Components/Portal/Pages/Login.razor
@@ -0,0 +1,164 @@
+@page "/login"
+@page "/"
+@attribute [AllowAnonymous]
+@rendermode InteractiveWebAssembly
+
+@inject NavigationManager Navigation
+@inject HttpClient Http
+
+고객 포탈 로그인
+
+
+
+
고객 포탈
+
이메일과 비밀번호로 로그인하세요
+
+ @if (!string.IsNullOrEmpty(ErrorMessage))
+ {
+
@ErrorMessage
+ }
+
+
+
+
+
+
+
+
+
또는 소셜 계정으로 로그인
+
+
+
+
+
+
+
+
+
+@code {
+ private string Email = "";
+ private string Password = "";
+ private string? ErrorMessage;
+ private bool IsLoading = false;
+
+ private async Task HandleLogin()
+ {
+ IsLoading = true;
+ ErrorMessage = null;
+
+ try
+ {
+ var loginRequest = new { email = Email, password = Password };
+ var response = await Http.PostAsJsonAsync("/api/portal/login", loginRequest);
+
+ if (response.IsSuccessStatusCode)
+ {
+ Navigation.NavigateTo("/dashboard", forceLoad: true);
+ }
+ else
+ {
+ ErrorMessage = "로그인 정보를 확인할 수 없습니다.";
+ }
+ }
+ catch
+ {
+ ErrorMessage = "로그인 중 오류가 발생했습니다.";
+ }
+ finally
+ {
+ IsLoading = false;
+ }
+ }
+
+ private void LoginWithGoogle() => Navigation.NavigateTo("/api/portal/login/google");
+ private void LoginWithNaver() => Navigation.NavigateTo("/api/portal/login/naver");
+ private void LoginWithKakao() => Navigation.NavigateTo("/api/portal/login/kakao");
+}
diff --git a/src/TaxBaik.Portal.Client/Components/Portal/Pages/Logout.razor b/src/TaxBaik.Portal.Client/Components/Portal/Pages/Logout.razor
new file mode 100644
index 0000000..76b80e5
--- /dev/null
+++ b/src/TaxBaik.Portal.Client/Components/Portal/Pages/Logout.razor
@@ -0,0 +1,31 @@
+@page "/logout"
+@attribute [Authorize]
+
+@inject NavigationManager Navigation
+@inject HttpClient Http
+
+로그아웃 중...
+
+
+
+@code {
+ protected override async Task OnInitializedAsync()
+ {
+ try
+ {
+ await Http.PostAsync("/api/portal/logout", null);
+ }
+ catch
+ {
+ // 로그아웃 실패해도 무조건 로그인 페이지로
+ }
+ finally
+ {
+ Navigation.NavigateTo("/login", forceLoad: true);
+ }
+ }
+}
diff --git a/src/TaxBaik.Portal.Client/Components/Portal/Routes.razor b/src/TaxBaik.Portal.Client/Components/Portal/Routes.razor
new file mode 100644
index 0000000..9f7eb77
--- /dev/null
+++ b/src/TaxBaik.Portal.Client/Components/Portal/Routes.razor
@@ -0,0 +1,19 @@
+@namespace TaxBaik.PortalClient.Components.Portal
+@using Microsoft.AspNetCore.Components.Routing
+
+
+
+
+
+
+
+
+
+
+
+ 찾을 수 없음
+
+ 요청한 페이지를 찾을 수 없습니다.
+
+
+
diff --git a/src/TaxBaik.Portal.Client/PortalAuthenticationStateProvider.cs b/src/TaxBaik.Portal.Client/PortalAuthenticationStateProvider.cs
new file mode 100644
index 0000000..feda8b3
--- /dev/null
+++ b/src/TaxBaik.Portal.Client/PortalAuthenticationStateProvider.cs
@@ -0,0 +1,48 @@
+using System.Security.Claims;
+using Microsoft.AspNetCore.Components.Authorization;
+
+namespace TaxBaik.PortalClient;
+
+///
+/// Portal 사용자 인증 상태 (쿠키 기반)
+/// 서버에서 PortalAuthDefaults.AuthenticationScheme으로 설정한 쿠키를 읽음
+///
+public class PortalAuthenticationStateProvider : AuthenticationStateProvider
+{
+ private readonly HttpClient _httpClient;
+
+ public PortalAuthenticationStateProvider(HttpClient httpClient)
+ {
+ _httpClient = httpClient;
+ }
+
+ public override async Task GetAuthenticationStateAsync()
+ {
+ try
+ {
+ // 서버의 쿠키 인증 상태 확인 엔드포인트
+ var response = await _httpClient.GetAsync("/api/portal/auth/me");
+
+ if (response.IsSuccessStatusCode)
+ {
+ var json = await response.Content.ReadAsStringAsync();
+ // TODO: JSON 파싱 후 ClaimsIdentity 생성
+ var claims = new List
+ {
+ new Claim(ClaimTypes.NameIdentifier, "user-id"),
+ new Claim(ClaimTypes.Name, "User Name")
+ };
+
+ var identity = new ClaimsIdentity(claims, "Portal");
+ var user = new ClaimsPrincipal(identity);
+ return new AuthenticationState(user);
+ }
+ }
+ catch
+ {
+ // 인증 상태 확인 실패 → 미인증 상태
+ }
+
+ return new AuthenticationState(new ClaimsPrincipal());
+ }
+}
diff --git a/src/TaxBaik.Portal.Client/Program.cs b/src/TaxBaik.Portal.Client/Program.cs
new file mode 100644
index 0000000..8e36d98
--- /dev/null
+++ b/src/TaxBaik.Portal.Client/Program.cs
@@ -0,0 +1,22 @@
+using Microsoft.AspNetCore.Components.Authorization;
+using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
+using MudBlazor.Services;
+using TaxBaik.PortalClient;
+
+var builder = WebAssemblyHostBuilder.CreateDefault(args);
+
+builder.Services.AddMudServices();
+
+builder.Services.AddAuthorizationCore();
+
+// Portal 인증 (쿠키 기반)
+builder.Services.AddScoped();
+
+// HTTP 클라이언트 (쿠키 자동 포함)
+builder.Services.AddScoped(sp =>
+ new HttpClient
+ {
+ BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)
+ });
+
+await builder.Build().RunAsync();
diff --git a/src/TaxBaik.Portal.Client/TaxBaik.Portal.Client.csproj b/src/TaxBaik.Portal.Client/TaxBaik.Portal.Client.csproj
new file mode 100644
index 0000000..170d608
--- /dev/null
+++ b/src/TaxBaik.Portal.Client/TaxBaik.Portal.Client.csproj
@@ -0,0 +1,24 @@
+
+
+
+ net10.0
+ enable
+ enable
+ TaxBaik.PortalClient
+
+ portal
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/TaxBaik.Portal.Client/_Imports.razor b/src/TaxBaik.Portal.Client/_Imports.razor
new file mode 100644
index 0000000..27e9c7e
--- /dev/null
+++ b/src/TaxBaik.Portal.Client/_Imports.razor
@@ -0,0 +1,12 @@
+@using System.Net.Http
+@using System.Net.Http.Json
+@using Microsoft.AspNetCore.Authorization
+@using Microsoft.AspNetCore.Components.Authorization
+@using Microsoft.AspNetCore.Components.Forms
+@using Microsoft.AspNetCore.Components.Routing
+@using Microsoft.AspNetCore.Components.Web
+@using Microsoft.AspNetCore.Components.Web.Virtualization
+@using Microsoft.JSInterop
+@using MudBlazor
+@using TaxBaik.PortalClient
+@using static Microsoft.AspNetCore.Components.Web.RenderMode
diff --git a/src/TaxBaik.Portal.Client/wwwroot/index.html b/src/TaxBaik.Portal.Client/wwwroot/index.html
new file mode 100644
index 0000000..3f450f8
--- /dev/null
+++ b/src/TaxBaik.Portal.Client/wwwroot/index.html
@@ -0,0 +1,19 @@
+
+
+
+
+
+ TaxBaik - 고객 포탈
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/TaxBaik.Web/Program.cs b/src/TaxBaik.Web/Program.cs
index 3f557e1..8354afb 100644
--- a/src/TaxBaik.Web/Program.cs
+++ b/src/TaxBaik.Web/Program.cs
@@ -385,12 +385,15 @@ app.MapHealthChecks("/healthz");
app.MapRazorPages();
app.MapStaticAssets();
-// Admin Blazor WebAssembly SPA 호스팅 (/admin)
+// Admin & Portal Blazor WebAssembly SPA 호스팅
app.UseBlazorFrameworkFiles("/admin");
app.UseStaticFiles("/admin");
+app.UseBlazorFrameworkFiles("/portal");
+app.UseStaticFiles("/portal");
-// /admin 라우팅 폴백 (SPA 라우트 처리)
+// SPA 라우팅 폴백 (각 경로에서 index.html 제공)
app.MapFallbackToFile("admin/{*path:nonfile}", "admin/index.html");
+app.MapFallbackToFile("portal/{*path:nonfile}", "portal/index.html");
// 애플리케이션 시작/종료 로깅
try
diff --git a/src/TaxBaik.Web/TaxBaik.Web.csproj b/src/TaxBaik.Web/TaxBaik.Web.csproj
index 8ea8c78..99b33fb 100644
--- a/src/TaxBaik.Web/TaxBaik.Web.csproj
+++ b/src/TaxBaik.Web/TaxBaik.Web.csproj
@@ -4,6 +4,7 @@
+