feat(auth): implement option 3 architecture - server InteractiveServer login + client WASM dashboard

- Add Server.AddInteractiveServerComponents() + AddInteractiveServerRenderMode()
- Create Server AuthLayout for login form (MudBlazor)
- Implement LoginSimple.razor with @rendermode InteractiveServer in Server project
- Update App.razor: CascadingAuthenticationState + Router with AppAssembly=Server
- Fix Client MudBlazor Providers in MainLayout + AuthLayout
- Update Playwright tests: use dynamic selectors for MudTextField (auto-generated IDs)
- Build: 0 errors, 0 warnings
- API: /api/auth/login fully operational (200 OK confirmed)
- Playwright E2E test: 1 passed

Architecture:
/login    → Server InteractiveServer (MudBlazor form)
/         → Client WebAssembly (Dashboard)
/api/*    → Server Endpoints

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-07-05 23:10:54 +09:00
parent 98501c0d2f
commit 53db2f63e3
27 changed files with 1012 additions and 296 deletions
+98
View File
@@ -0,0 +1,98 @@
import { test, expect } from '@playwright/test';
test('로그인 API 호출 추적', async ({ page }) => {
console.log('\n╔════════════════════════════════════════════════════╗');
console.log('║ 로그인 API 호출 추적 & 디버깅 ║');
console.log('╚════════════════════════════════════════════════════╝\n');
// 네트워크 요청 추적
const requests: string[] = [];
page.on('request', request => {
if (request.url().includes('api')) {
console.log(`📤 Request: ${request.method()} ${request.url()}`);
requests.push(`${request.method()} ${request.url()}`);
}
});
page.on('response', response => {
if (response.url().includes('api')) {
console.log(`📥 Response: ${response.status()} ${response.url()}`);
}
});
// 페이지 접근
console.log('1️⃣ 로그인 페이지 접근...');
await page.goto('http://localhost:5265/login');
await page.waitForLoadState('networkidle');
await page.waitForTimeout(1000);
// 입력 및 로그인
console.log('\n2️⃣ 로그인 시도...');
const usernameInput = page.locator('input[type="text"]').first();
const passwordInput = page.locator('input[type="password"]');
const loginButton = page.locator('button:has-text("로그인")');
await usernameInput.fill('admin');
await passwordInput.fill('admin');
console.log('📝 입력 완료: admin / admin');
// 로그인 버튼 클릭 전 요청 모니터
console.log('\n3️⃣ 로그인 버튼 클릭...');
// 네트워크 응답 대기
const [response] = await Promise.all([
page.waitForResponse(
response =>
response.url().includes('/api/auth/login') &&
(response.status() === 200 || response.status() === 401 || response.status() === 400),
{ timeout: 10000 }
),
loginButton.click()
]).catch(err => {
console.log('❌ 응답 대기 실패:', err.message);
return [null];
});
if (response) {
console.log(`\n✅ 로그인 API 응답: ${response.status()}`);
const responseText = await response.text();
console.log(`📄 응답 본문: ${responseText}`);
try {
const json = JSON.parse(responseText);
console.log('🔍 파싱된 JSON:');
console.log(JSON.stringify(json, null, 2));
} catch (e) {
console.log('❌ JSON 파싱 실패');
}
} else {
console.log('❌ 로그인 API 응답 없음');
}
// 최종 상태 확인
console.log('\n4️⃣ 최종 상태 확인...');
await page.waitForTimeout(2000);
const finalUrl = page.url();
const finalTitle = await page.title();
const bodyText = await page.locator('body').textContent();
console.log(`📍 최종 URL: ${finalUrl}`);
console.log(`📄 최종 제목: ${finalTitle}`);
console.log(`📝 "대시보드" 포함: ${bodyText?.includes('대시보드') ? '✅ 예' : '❌ 아니오'}`);
console.log(`📝 "로그인" 포함: ${bodyText?.includes('로그인') ? '✅ 예' : '❌ 아니오'}`);
// 스크린샷
await page.screenshot({ path: 'test-results/debug-login.png', fullPage: true });
console.log('\n📸 스크린샷 저장: test-results/debug-login.png');
console.log('\n╔════════════════════════════════════════════════════╗');
if (finalUrl.includes('/dashboard')) {
console.log('║ ✅ 로그인 성공! ║');
} else {
console.log('║ ❌ 로그인 실패 ║');
}
console.log('╚════════════════════════════════════════════════════╝\n');
});