diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index 8e4e8ed..e87d3d9 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -149,3 +149,16 @@ jobs: echo "Service verification failed (home: $HOME_STATUS, login: $LOGIN_STATUS, auth: $AUTH_BODY)" >&2 exit 1 fi + + - name: Install Playwright dependencies + run: | + set -e + npm ci + npx playwright install chromium --with-deps + + - name: Browser E2E verification + env: + E2E_BASE_URL: http://${{ secrets.DEPLOY_HOST }}/taxbaik + E2E_ADMIN_USERNAME: admin + E2E_ADMIN_PASSWORD: ${{ secrets.TAXBAIK_ADMIN_TEST_PASSWORD }} + run: npm run test:e2e diff --git a/.gitignore b/.gitignore index f9c4f50..7f87f78 100644 --- a/.gitignore +++ b/.gitignore @@ -33,6 +33,9 @@ artifacts/ # Test results TestResults/ *.trx +playwright-report/ +test-results/ +.playwright-cli/ # IDE .vscode/ @@ -46,6 +49,9 @@ Thumbs.db packages/ .nuget/ +# Node / Playwright +node_modules/ + # Publish publish/ PublishProfiles/ diff --git a/ROADMAP_WBS.md b/ROADMAP_WBS.md new file mode 100644 index 0000000..3f75b0b --- /dev/null +++ b/ROADMAP_WBS.md @@ -0,0 +1,92 @@ +# TaxBaik 개선 로드맵 WBS + +이 문서는 "완료 보고"가 아니라 검증 가능한 작업 목록이다. 각 WBS는 성공 기준을 통과해야 완료로 본다. + +## 완료 판정 원칙 + +- 코드 변경만으로 완료 처리하지 않는다. +- 서버 배포 대상 기능은 CI/CD 성공과 Playwright 브라우저 테스트 통과를 모두 요구한다. +- API 기능은 단위 테스트 또는 통합 테스트와 함께 실제 HTTP 호출 결과를 확인한다. +- DB 변경은 마이그레이션과 롤백 위험을 문서화한다. +- 비밀값은 Gitea Secrets 또는 서버 환경변수로만 관리한다. + +## WBS-OPS-01 배포 검증 고도화 + +목표: curl/API 검증만으로 "완료" 처리하지 않고, 실제 브라우저 사용자 흐름을 CI 게이트로 만든다. + +성공 기준: +- `dotnet build TaxBaik.sln -c Release` 경고 0, 오류 0 +- `dotnet test TaxBaik.sln -c Release --no-build` 전체 통과 +- CI 배포 후 Playwright가 `/taxbaik/admin/login`에서 실제 로그인 수행 +- 로그인 후 `/taxbaik/admin/dashboard` 도달 +- `localStorage.auth_token` 저장 확인 +- 브라우저 console error 및 page error 0개 + +Todo: +- [x] Playwright Test 프로젝트 추가 +- [x] 관리자 로그인 E2E 추가 +- [x] CI 배포 후 Playwright 실행 단계 추가 +- [x] Playwright가 발견한 Blazor DI 결함 수정 +- [ ] CI run에서 Playwright 통과 확인 + +## WBS-AUTH-01 인증/비밀번호 운영 안정화 + +목표: DB 직접 수정 대신 API로 관리자 인증 운영 작업을 수행한다. + +성공 기준: +- 비밀번호 변경 API가 현재 비밀번호를 요구한다. +- 비밀번호 재설정 API는 운영 secret 없이는 동작하지 않는다. +- 실패 응답은 민감 정보를 노출하지 않는다. +- Playwright 로그인 테스트가 변경 후에도 통과한다. + +Todo: +- [x] 로그인 API 검증 +- [x] 비밀번호 변경 API 추가 +- [x] 재설정 API 추가 +- [ ] 관리자 UI에 비밀번호 변경 화면 추가 +- [ ] 비밀번호 변경 Playwright E2E 추가 + +## WBS-ADMIN-01 관리자 Blazor 안정화 + +목표: 관리자 화면을 일반 웹페이지처럼 명시적 사용자 액션에만 갱신하고, circuit 예외를 배포 전 차단한다. + +성공 기준: +- 관리자 주요 메뉴 대시보드/블로그/문의/설정 접근 시 circuit error 0개 +- 잘못된 DI 타입 주입 0건 +- 저장/삭제/상태 변경 액션은 성공/실패 메시지를 표시한다. +- Playwright가 주요 메뉴 smoke test를 수행한다. + +Todo: +- [x] 중복 `/admin` 라우트 제거 +- [x] MudBlazor DI 타입 오류 수정 +- [ ] 관리자 메뉴 smoke E2E 추가 +- [ ] 설정 저장 TODO를 실제 DB 기반 기능으로 전환 + +## WBS-UX-01 공개 홈페이지 UX/SEO 검증 + +목표: 공개 홈페이지가 검색 유입과 상담 전환에 맞는 구조인지 검증한다. + +성공 기준: +- 홈/블로그 목록/블로그 상세/상담 문의 페이지 200 +- 주요 페이지 title/description 존재 +- 모바일 viewport에서 주요 CTA가 보인다. +- 상담 문의 제출 Playwright E2E가 통과한다. + +Todo: +- [ ] 공개 페이지 Playwright smoke E2E 추가 +- [ ] 상담 문의 제출 E2E 추가 +- [ ] 블로그 상세 SEO 메타 검증 추가 + +## WBS-MAINT-01 유지보수성/파편화 축소 + +목표: 문서와 실제 구조의 불일치를 줄이고 단일 앱 운영 기준을 유지한다. + +성공 기준: +- README/CLAUDE/DEPLOYMENT_GUIDE의 .NET 버전, 앱 구조, 테스트 기준이 실제 코드와 일치 +- 배포 문서에 Playwright 검증 절차 포함 +- 오래된 분리 Admin 서비스 문서 제거 또는 명확히 deprecated 처리 + +Todo: +- [ ] README 테스트/배포 섹션 갱신 +- [ ] CLAUDE.md E2E 기준 갱신 +- [ ] 오래된 최종 보고 문서의 허위 완료 표현 정정 diff --git a/TaxBaik.Web/Components/Admin/Pages/Blog/BlogList.razor b/TaxBaik.Web/Components/Admin/Pages/Blog/BlogList.razor index b439879..9ba4ac2 100644 --- a/TaxBaik.Web/Components/Admin/Pages/Blog/BlogList.razor +++ b/TaxBaik.Web/Components/Admin/Pages/Blog/BlogList.razor @@ -1,7 +1,6 @@ @page "/admin/blog" @attribute [Authorize] @inject IApiClient ApiClient -@inject DialogService DialogService @inject ISnackbar Snackbar 블로그 관리 diff --git a/TaxBaik.Web/Components/Admin/Pages/Settings/SiteSettings.razor b/TaxBaik.Web/Components/Admin/Pages/Settings/SiteSettings.razor index 49df900..46c42c9 100644 --- a/TaxBaik.Web/Components/Admin/Pages/Settings/SiteSettings.razor +++ b/TaxBaik.Web/Components/Admin/Pages/Settings/SiteSettings.razor @@ -1,7 +1,7 @@ @page "/admin/settings" @using TaxBaik.Domain.Interfaces @attribute [Authorize] -@inject Snackbar Snackbar +@inject ISnackbar Snackbar 설정 @@ -35,5 +35,7 @@ private async Task SaveSettings() { // TODO: Save settings to database + Snackbar.Add("설정 저장 기능은 아직 구현되지 않았습니다.", Severity.Info); + await Task.CompletedTask; } } diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..2128239 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,75 @@ +{ + "name": "taxbaik", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "devDependencies": { + "@playwright/test": "1.57.0" + } + }, + "node_modules/@playwright/test": { + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.57.0.tgz", + "integrity": "sha512-6TyEnHgd6SArQO8UO2OMTxshln3QMWBtPGrOCgs3wVEmQmwyuNtB10IZMfmYDE0riwNR1cu4q+pPcxMVtaG3TA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.57.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/playwright": { + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.57.0.tgz", + "integrity": "sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.57.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.57.0.tgz", + "integrity": "sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..c960e2f --- /dev/null +++ b/package.json @@ -0,0 +1,9 @@ +{ + "scripts": { + "test:e2e": "playwright test", + "test:e2e:headed": "playwright test --headed" + }, + "devDependencies": { + "@playwright/test": "1.57.0" + } +} diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..1d495b5 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,25 @@ +import { defineConfig, devices } from '@playwright/test'; + +export default defineConfig({ + testDir: './tests/e2e', + timeout: 30_000, + expect: { + timeout: 10_000 + }, + fullyParallel: false, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 1 : 0, + reporter: process.env.CI ? [['list'], ['html', { open: 'never' }]] : 'list', + use: { + baseURL: process.env.E2E_BASE_URL ?? 'http://178.104.200.7/taxbaik', + trace: 'retain-on-failure', + screenshot: 'only-on-failure', + video: 'retain-on-failure' + }, + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] } + } + ] +}); diff --git a/tests/e2e/admin-login.spec.ts b/tests/e2e/admin-login.spec.ts new file mode 100644 index 0000000..75a045b --- /dev/null +++ b/tests/e2e/admin-login.spec.ts @@ -0,0 +1,36 @@ +import { expect, test } from '@playwright/test'; + +const username = process.env.E2E_ADMIN_USERNAME ?? 'admin'; +const password = process.env.E2E_ADMIN_PASSWORD; +const baseUrl = (process.env.E2E_BASE_URL ?? 'http://178.104.200.7/taxbaik').replace(/\/$/, ''); + +test.describe('admin authentication', () => { + test('logs in through the real browser UI and reaches dashboard', async ({ page }) => { + test.skip(!password, 'E2E_ADMIN_PASSWORD is required.'); + + const consoleErrors: string[] = []; + page.on('console', message => { + if (message.type() === 'error') { + consoleErrors.push(message.text()); + } + }); + page.on('pageerror', error => { + consoleErrors.push(error.message); + }); + + await page.goto(`${baseUrl}/admin/login`); + + await expect(page.getByRole('heading', { name: '관리자 로그인' })).toBeVisible(); + await page.getByRole('textbox', { name: '사용자명' }).fill(username); + await page.getByRole('textbox', { name: '비밀번호' }).fill(password); + await page.getByRole('button', { name: '로그인' }).click(); + + await expect(page).toHaveURL(/\/taxbaik\/admin\/dashboard$/); + await expect(page.getByRole('heading', { name: /대시보드/ })).toBeVisible(); + await expect(page.getByRole('link', { name: /로그아웃/ })).toBeVisible(); + + const token = await page.evaluate(() => localStorage.getItem('auth_token')); + expect(token, 'auth_token should be stored after login').toBeTruthy(); + expect(consoleErrors, 'browser console/page errors').toEqual([]); + }); +});