This commit is contained in:
@@ -149,3 +149,16 @@ jobs:
|
|||||||
echo "Service verification failed (home: $HOME_STATUS, login: $LOGIN_STATUS, auth: $AUTH_BODY)" >&2
|
echo "Service verification failed (home: $HOME_STATUS, login: $LOGIN_STATUS, auth: $AUTH_BODY)" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
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
|
||||||
|
|||||||
@@ -33,6 +33,9 @@ artifacts/
|
|||||||
# Test results
|
# Test results
|
||||||
TestResults/
|
TestResults/
|
||||||
*.trx
|
*.trx
|
||||||
|
playwright-report/
|
||||||
|
test-results/
|
||||||
|
.playwright-cli/
|
||||||
|
|
||||||
# IDE
|
# IDE
|
||||||
.vscode/
|
.vscode/
|
||||||
@@ -46,6 +49,9 @@ Thumbs.db
|
|||||||
packages/
|
packages/
|
||||||
.nuget/
|
.nuget/
|
||||||
|
|
||||||
|
# Node / Playwright
|
||||||
|
node_modules/
|
||||||
|
|
||||||
# Publish
|
# Publish
|
||||||
publish/
|
publish/
|
||||||
PublishProfiles/
|
PublishProfiles/
|
||||||
|
|||||||
@@ -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 기준 갱신
|
||||||
|
- [ ] 오래된 최종 보고 문서의 허위 완료 표현 정정
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
@page "/admin/blog"
|
@page "/admin/blog"
|
||||||
@attribute [Authorize]
|
@attribute [Authorize]
|
||||||
@inject IApiClient ApiClient
|
@inject IApiClient ApiClient
|
||||||
@inject DialogService DialogService
|
|
||||||
@inject ISnackbar Snackbar
|
@inject ISnackbar Snackbar
|
||||||
|
|
||||||
<PageTitle>블로그 관리</PageTitle>
|
<PageTitle>블로그 관리</PageTitle>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
@page "/admin/settings"
|
@page "/admin/settings"
|
||||||
@using TaxBaik.Domain.Interfaces
|
@using TaxBaik.Domain.Interfaces
|
||||||
@attribute [Authorize]
|
@attribute [Authorize]
|
||||||
@inject Snackbar Snackbar
|
@inject ISnackbar Snackbar
|
||||||
|
|
||||||
<PageTitle>설정</PageTitle>
|
<PageTitle>설정</PageTitle>
|
||||||
|
|
||||||
@@ -35,5 +35,7 @@
|
|||||||
private async Task SaveSettings()
|
private async Task SaveSettings()
|
||||||
{
|
{
|
||||||
// TODO: Save settings to database
|
// TODO: Save settings to database
|
||||||
|
Snackbar.Add("설정 저장 기능은 아직 구현되지 않았습니다.", Severity.Info);
|
||||||
|
await Task.CompletedTask;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+75
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"scripts": {
|
||||||
|
"test:e2e": "playwright test",
|
||||||
|
"test:e2e:headed": "playwright test --headed"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@playwright/test": "1.57.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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'] }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
@@ -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([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user