fix(sirsoft-gdpr): 정책 버전 본문 페이지 링크 404 수정

정책 버전 snapshot 모달의 본문 페이지 링크가 /{{slug}} 로 조합되어
페이지 모듈(sirsoft-page) URL 패턴 /page/{slug} 와 불일치해 404가
발생하던 문제를 수정. 쿠키 배너의 동일 링크와 조합 방식을 통일했다.

회귀 검증을 위해 GDPR 플러그인에 Playwright E2E 인프라를 최초 도입.
This commit is contained in:
chym1217 committed 2026-07-30 10:42:04 +09:00
1 parent d5b6f4b457
commit a0cc36a9db
8 files changed
+193 -3

No files matched your search

@@ -27,6 +27,7 @@
- 관리자 환경설정의 카테고리 「필수」 배지가 다크 모드에서 배경·글자색이 적용되지 않던 문제를 바로잡았습니다.
- 관리자 환경설정의 「운영 주체 / 개인정보처리방침」 섹션 제목 영역이 다른 카드와 다르게 표시되던 문제를 바로잡았습니다.
- 관리자 환경설정의 저장/취소 버튼을 다른 관리자 화면과 동일하게 화면 하단 고정 위치로 옮기고, 상단 탭 영역의 불필요한 구분선과 좌우 여백 어긋남을 바로잡았습니다.
- 관리자 환경설정 「정책 버전 보기」에서 본문 페이지 링크를 클릭하면 페이지를 찾을 수 없던 문제를 바로잡았습니다.
## [1.0.1] - 2026-07-16
+3 -1
View File
@@ -7,7 +7,9 @@
"build": "vite build",
"dev": "vite build --watch",
"test": "vitest",
"test:run": "vitest run"
"test:run": "vitest run",
"test:e2e": "playwright test --config=tests/Playwright/playwright.config.ts",
"test:e2e:ui": "playwright test --config=tests/Playwright/playwright.config.ts --ui"
},
"devDependencies": {
"@testing-library/jest-dom": "^6.5.0",
@@ -14,6 +14,7 @@
import { describe, it, expect } from 'vitest';
import layout from '../../../layouts/admin/plugin_settings.json';
import publishModal from '../../../layouts/admin/partials/plugin_settings/_policy_version_publish_modal.json';
import snapshotModal from '../../../layouts/admin/partials/_shared/_policy_version_snapshot_modal.json';
import { findById, type AnyNode } from './helpers';
describe('admin/plugin_settings.json — 3 카드 (operator / cookie_banner / auto_blocking_policy)', () => {
@@ -698,3 +699,26 @@ describe('admin/plugin_settings.json — 3 카드 (operator / cookie_banner / au
});
});
});
/**
* 이슈 #509 체크리스트 12번 회귀 테스트 — 정책 버전 snapshot 모달의 본문 페이지 링크가
* `/{{slug}}` 로 조합되어 페이지 모듈(sirsoft-page) 실제 URL 패턴 `/page/{slug}` 와
* 불일치, 클릭 시 404 발생하던 결함. 쿠키 배너의 '변경된 정책 본문 보기' 링크와
* 동일하게 `/page/{{slug}}` 로 조합되어야 한다.
*/
describe('admin/partials/_shared/_policy_version_snapshot_modal.json — 본문 페이지 링크', () => {
const layoutJson = JSON.stringify(snapshotModal);
it('본문 페이지 링크 href 가 /page/{{slug}} 형식으로 조합된다 (페이지 모듈 URL 패턴 일치)', () => {
expect(layoutJson).toContain('"href":"/page/{{gdprPolicyVersionSnapshot?.data?.data?.snapshot?.privacy_policy_slug ?? \'\'}}"');
});
it('href 에 /page/ 프리픽스 없이 슬러그만 조합하는 결함 패턴이 없다', () => {
expect(layoutJson).not.toContain('"href":"/{{gdprPolicyVersionSnapshot?.data?.data?.snapshot?.privacy_policy_slug}}"');
});
it('본문 페이지 링크는 새 탭(target=_blank) + noopener 로 연다', () => {
expect(layoutJson).toContain('"target":"_blank"');
expect(layoutJson).toContain('"rel":"noopener"');
});
});
@@ -172,11 +172,12 @@
"text": "{{gdprPolicyVersionSnapshot?.data?.data?.snapshot?.privacy_policy_slug ?? '-'}}"
},
{
"comment": "본문 페이지 새 탭 열기 — 슬러그를 그대로 path 로 사용 (예: 'privacy-policy' → '/privacy-policy'). 운영자가 검증한 슬러그라 사용자 입력 검증 불요.",
"id": "policy_version_snapshot_open_policy_page_link",
"comment": "본문 페이지 새 탭 열기 — 페이지 모듈(sirsoft-page) URL 패턴 /page/{slug} 로 조합 (예: 'privacy-policy' → '/page/privacy-policy'). 쿠키 배너의 '변경된 정책 본문 보기' 링크(cookie_banner.json)와 동일한 조합 방식. 운영자가 검증한 슬러그라 사용자 입력 검증 불요.",
"type": "basic",
"name": "A",
"props": {
"href": "/{{gdprPolicyVersionSnapshot?.data?.data?.snapshot?.privacy_policy_slug}}",
"href": "/page/{{gdprPolicyVersionSnapshot?.data?.data?.snapshot?.privacy_policy_slug ?? ''}}",
"target": "_blank",
"rel": "noopener",
"className": "text-info-soft inline-flex items-center gap-1 px-3 py-2 font-medium bg-blue-50 dark:bg-blue-900/30 border border-blue-200 dark:border-blue-700 rounded hover:bg-blue-100 dark:hover:bg-blue-900/50 transition-colors flex-shrink-0"
@@ -755,6 +755,7 @@
"text": "v{{gdprPolicyVersionCurrent?.data?.data?.version ?? '?'}}"
},
{
"id": "policy_version_current_snapshot_view_button",
"type": "basic",
"name": "Button",
"if": "{{gdprPolicyVersionCurrent?.data?.data?.version}}",
@@ -0,0 +1,24 @@
/**
* GDPR 플러그인 권한 fixture.
*
* 코어 `tests/Playwright/fixtures/auth.ts` 의 헬퍼를 재사용하되, 플러그인 권한 토큰 fixture 를
* 자체적으로 정의한다. 권한 식별자는 임의 string 이므로 코어 PlaywrightIssueToken 커맨드가
* 그대로 동작한다 (Permission::firstOrCreate 가 자동 생성).
*/
import { test as base } from '@playwright/test';
// 6단계 상위 = 코어 루트의 fixtures/auth.ts
import { issueToken, authenticatePage } from '../../../../../../tests/Playwright/fixtures/auth';
type GdprAuthFixtures = {
/** GDPR 개인정보 조회 + 설정 변경 권한 보유 토큰 (환경설정 화면 접근용) */
privacyManageToken: string;
};
export const test = base.extend<GdprAuthFixtures>({
privacyManageToken: async ({}, use) => {
await use(issueToken('sirsoft-gdpr.privacy.view', 'sirsoft-gdpr.privacy.update'));
},
});
export { authenticatePage };
export { expect } from '@playwright/test';
@@ -0,0 +1,84 @@
/**
* GDPR 플러그인 Playwright E2E 설정.
*
* 코어 `playwright.config.ts` 와 동일한 base URL 해석 우선순위를 따른다 — 플러그인도 활성 호스트가
* 가변(개발자/CI/PO 환경별로 다른 도메인)이므로 하드코딩 회피.
*
* Base URL 해석:
* 1. PLAYWRIGHT_BASE_URL 환경변수 (CI/명시적 오버라이드)
* 2. .env (코어 루트) 의 APP_URL — 단 localhost 류는 fallback 부적합
* 3. 그 외 — 명시 에러
*
* 실행 예시:
* PowerShell — $env:PLAYWRIGHT_BASE_URL='https://g7.dev'; npm run test:e2e
* Bash — PLAYWRIGHT_BASE_URL=https://g7.dev npm run test:e2e
*
* 플러그인은 코어 fixture 의 `issueToken` / `authenticatePage` 헬퍼를 재사용 — 권한 식별자는
* `sirsoft-gdpr.*` 등 임의 string.
*/
import { defineConfig, devices } from '@playwright/test';
import { readFileSync, existsSync } from 'node:fs';
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
// ESM 환경(package.json "type": "module")에서는 __dirname 이 정의되지 않으므로
// import.meta.url 로 재구성한다.
const __dirname = dirname(fileURLToPath(import.meta.url));
/**
* 코어 루트 (artisan / .env / Playwright 산출물의 기준 경로).
*
* 확장 config 는 확장 디렉토리에서 실행되지만, 산출물을 그 안에 쓰면 Windows 에서
* `plugin:update` 의 디렉토리 이동이 열린 핸들에 걸려 실패한다.
* 산출물은 코어 루트 아래로 모아 update 경로와 분리한다 (.gitignore 가 이미 덮는 위치).
*/
const CORE_ROOT = process.env.G7_ROOT || resolve(__dirname, '../../../../../');
/** 확장별 산출물 격리 — 확장끼리 리포트를 덮어쓰지 않도록 slug 로 네임스페이스. */
const ARTIFACT_SLUG = 'plugins/sirsoft-gdpr';
function readEnvFile(filePath: string, key: string): string | null {
if (!existsSync(filePath)) return null;
const content = readFileSync(filePath, { encoding: 'utf-8' });
const pattern = new RegExp(`^${key}=(.*)$`, 'm');
const match = content.match(pattern);
if (!match) return null;
let value = match[1].trim();
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
value = value.slice(1, -1);
}
return value || null;
}
function resolveBaseUrl(): string {
if (process.env.PLAYWRIGHT_BASE_URL) {
return process.env.PLAYWRIGHT_BASE_URL;
}
const appUrl = readEnvFile(resolve(CORE_ROOT, '.env'), 'APP_URL');
if (appUrl && !/^https?:\/\/localhost(:\d+)?\/?$/i.test(appUrl)) {
return appUrl;
}
throw new Error(
'GDPR 플러그인 E2E base URL 미설정. PLAYWRIGHT_BASE_URL 환경변수를 지정하거나 코어 .env 의 APP_URL 을 활성 호스트로 설정하세요.'
);
}
export default defineConfig({
testDir: './specs',
outputDir: resolve(CORE_ROOT, 'test-results', ARTIFACT_SLUG),
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: [
['html', { outputFolder: resolve(CORE_ROOT, 'playwright-report', ARTIFACT_SLUG), open: 'never' }],
['list'],
],
use: {
baseURL: resolveBaseUrl(),
trace: 'retain-on-failure',
screenshot: 'only-on-failure',
ignoreHTTPSErrors: true,
},
projects: [{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }],
});
@@ -0,0 +1,53 @@
/**
* E2E: 관리자 환경설정 > 쿠키 배너 > 정책 버전 > 보기 — 본문 페이지 링크 (#509 체크리스트 12번)
*
* @scenario admin_gdpr_policy_version_snapshot_open_policy_page_link
* @effects snapshot_modal_opens, policy_page_link_href_matches_page_module_url_pattern
*
* 배경: 정책 버전 snapshot 모달의 본문 페이지 링크가 `/{{slug}}` 로 조합되어 페이지 모듈
* (sirsoft-page) 의 실제 URL 패턴 `/page/{slug}` 와 불일치, 클릭 시 404 가 발생하던 결함.
* 마이그레이션이 시드하는 v1 initial 정책 버전의 snapshot 에는 항상 기본 슬러그 'privacy' 가
* 포함되므로 (GdprPolicyVersionMigrationSmokeTest 참조), 별도 설정 저장 없이도 v배지 클릭 →
* 모달의 본문 페이지 링크 href 를 검증할 수 있다.
*
* 검증:
* 1. 환경설정 화면에서 v배지 옆 "본문 보기" 버튼 클릭 시 snapshot 모달이 열린다
* 2. 모달의 본문 페이지 링크 href 가 `/page/{slug}` 형식으로 조합되어 페이지 모듈 URL 패턴과 일치한다
* (프리픽스 누락으로 `/{slug}` 형태가 되는 결함 패턴이 재발하지 않았는지 회귀 가드)
*/
import { test, expect, authenticatePage } from '../../fixtures/gdpr-auth';
const CARD_COOKIE_BANNER = '#card_cookie_banner';
const SNAPSHOT_VIEW_BUTTON = '#policy_version_current_snapshot_view_button';
const OPEN_POLICY_PAGE_LINK = '#policy_version_snapshot_open_policy_page_link';
/** 관리자 GDPR 환경설정 진입 후 쿠키 배너 탭(정책 버전 UI 포함)으로 스크롤 이동 */
async function gotoGdprSettings(page: import('@playwright/test').Page): Promise<void> {
await page.goto('/admin/plugins/sirsoft-gdpr/settings');
await page.waitForLoadState('domcontentloaded', { timeout: 30_000 });
await expect(page.locator('#settings_tab_navigation')).toBeAttached({ timeout: 20_000 });
// TabNavigationScroll 은 URL query 가 아니라 클릭 시 해당 카드로 스크롤하는 방식이다.
await page.locator(CARD_COOKIE_BANNER).scrollIntoViewIfNeeded();
await expect(page.locator(SNAPSHOT_VIEW_BUTTON)).toBeAttached({ timeout: 20_000 });
}
// @scenario tab=card_cookie_banner, permitted=yes
// @effects snapshot_modal_opens, policy_page_link_href_matches_page_module_url_pattern
test('#509 - 정책 버전 snapshot 모달의 본문 페이지 링크가 /page/{slug} 로 조합된다', async ({ page, privacyManageToken }) => {
await authenticatePage(page, privacyManageToken);
await gotoGdprSettings(page);
expect(page.url()).not.toMatch(/\/admin\/login/);
await page.locator(SNAPSHOT_VIEW_BUTTON).click();
const link = page.locator(OPEN_POLICY_PAGE_LINK);
await expect(link).toBeAttached({ timeout: 10_000 });
const href = await link.getAttribute('href');
expect(href).not.toBeNull();
// 결함 패턴(/{slug}, /page/ 프리픽스 누락) 재발 방지 — 페이지 모듈 URL 패턴 /page/{slug} 와 일치해야 한다.
expect(href).toMatch(/^\/page\/.+/);
expect(href).not.toMatch(/^\/(?!page\/)/);
});