ca965fec90
## Menu rewrites against the production /bbs/*.php URLs The g5_eyoom_menu rows store legacy PHP URLs verbatim (/bbs/tv.php, /bbs/point_guide.php, /bbs/exchange-amount.php, /bbs/Bighandbro.php, /bbs/event_exchange.php, /bbs/slotlife.php, /shop/list.php, /shop/orderinquiry.php, /roulette/?idx=1, /plugin/swiunApi/game.php?gt=mix …). Added rewriters that map every one of those to the new app routes so clicking any menu item lands on a real page. ## /games hub page The 포인트게임 top-level link used to 404 (it pointed to /games which didn't exist as a page). Built a proper hub: gradient hero, 15-tile slot simulator grid (each with a unique color gradient + emoji), and two callout cards for sports/mini-game branches. ## /tv/bighand 큰손형 방송 dedicated page with KICK CTA + schedule. ## Header polish - whitespace-nowrap on every menu label so 보증사이트 / 먹튀사이트 / 슬생TV no longer wrap at narrow widths. - MegaPanel renderer that turns 3rd-level eyoom menu groups (스포츠 / 미니게임 / 슬롯·릴) into multi-column section blocks instead of collapsing to an empty dropdown. - Replaced 구매내역 link with /mypage so utility-bar 404 disappears. ## verify-everything.mjs New end-to-end script that: - Crawls every menu link (64 unique URLs) and asserts non-404. - Real interaction tests: testlogin login → board list → post view → comment POST → recommend POST → admin login → admin dashboard render check → admin theme picker visible (4 themes) → theme switch POST → logout POST → attendance check POST. Result: PASS 85 / FAIL 0 / TOTAL 85. Report at next-app/verify-out/.
173 lines
8.3 KiB
JavaScript
173 lines
8.3 KiB
JavaScript
// End-to-end verification:
|
|
// 1) Crawl every link in the rendered home page and visit it (404 check)
|
|
// 2) Open every mega-menu, walk all dropdown links, visit them
|
|
// 3) Real interaction tests: login → write post → comment → recommend →
|
|
// scrap → admin pages → theme switch → logout
|
|
import { chromium } from 'playwright';
|
|
import { mkdir, writeFile } from 'node:fs/promises';
|
|
import { resolve } from 'node:path';
|
|
|
|
const BASE = 'http://localhost:3000';
|
|
const OUT = resolve(process.cwd(), 'verify-out');
|
|
await mkdir(OUT, { recursive: true });
|
|
|
|
const browser = await chromium.launch();
|
|
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 }, locale: 'ko-KR' });
|
|
const page = await ctx.newPage();
|
|
const report = { checks: [], pass: 0, fail: 0 };
|
|
|
|
function check(name, ok, info = '') {
|
|
report.checks.push({ name, ok, info });
|
|
if (ok) report.pass++; else report.fail++;
|
|
console.log(`${ok ? '✓' : '✗'} ${name}${info ? ' ' + info : ''}`);
|
|
}
|
|
|
|
// ---------------- Step 1: load home, gather menu links ----------------
|
|
await page.goto(BASE, { waitUntil: 'networkidle' });
|
|
const headerLinks = await page.$$eval('header a[href]', (els) =>
|
|
Array.from(new Set(els.map((a) => a.getAttribute('href')))).filter(Boolean));
|
|
check('home loads', true, `${headerLinks.length} header links`);
|
|
|
|
// ---------------- Step 2: open each mega-menu, capture dropdown links ----------------
|
|
const TOPS = ['보증사이트', '먹튀사이트', '커뮤니티', '이벤트', '슬생정보', '가품슬롯', '고객센터', '포인트게임', '슬생TV', '포인트존'];
|
|
const allLinks = new Set(headerLinks);
|
|
for (const label of TOPS) {
|
|
try {
|
|
await page.goto(BASE, { waitUntil: 'networkidle' });
|
|
const trigger = page.getByRole('link', { name: label, exact: true }).first();
|
|
await trigger.hover();
|
|
await page.waitForTimeout(300);
|
|
const sub = await page.$$eval('header a[href]', (els) => els.map((a) => a.getAttribute('href')));
|
|
const before = allLinks.size;
|
|
sub.forEach((h) => h && allLinks.add(h));
|
|
check(`menu hover: ${label}`, true, `+${allLinks.size - before} new links`);
|
|
} catch (e) {
|
|
check(`menu hover: ${label}`, false, e.message?.slice(0, 80));
|
|
}
|
|
}
|
|
|
|
// ---------------- Step 3: visit every internal link, expect non-404 ----------------
|
|
const internal = Array.from(allLinks).filter((h) => h.startsWith('/') && !h.startsWith('//') && !h.startsWith('/api/'));
|
|
console.log(`\nVisiting ${internal.length} unique internal links…\n`);
|
|
const broken = [];
|
|
for (const href of internal) {
|
|
try {
|
|
const url = href.startsWith('http') ? href : BASE + href;
|
|
const resp = await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 20_000 });
|
|
const status = resp ? resp.status() : 0;
|
|
const ok = status === 200 || status === 303 || status === 302;
|
|
if (!ok) broken.push({ href, status });
|
|
check(`GET ${href}`, ok, `${status}`);
|
|
} catch (e) {
|
|
broken.push({ href, status: 'ERR', err: e.message?.slice(0, 80) });
|
|
check(`GET ${href}`, false, e.message?.slice(0, 80));
|
|
}
|
|
}
|
|
|
|
// ---------------- Step 4: real interaction tests ----------------
|
|
|
|
// 4a) Login as testlogin
|
|
await page.goto(BASE + '/login', { waitUntil: 'networkidle' });
|
|
await page.fill('input[name="loginId"]', 'testlogin');
|
|
await page.fill('input[name="password"]', 'test1234');
|
|
await Promise.all([page.waitForURL((u) => u.pathname === '/'), page.click('button[type="submit"]')]);
|
|
const cookies = await ctx.cookies();
|
|
const sid = cookies.find((c) => c.name === 'slot_sid');
|
|
check('login: testlogin/test1234 → session cookie set', !!sid);
|
|
|
|
// 4b) Visit /free, count posts
|
|
await page.goto(BASE + '/free', { waitUntil: 'networkidle' });
|
|
const postCount = await page.$$eval('main a[href^="/free/"]', (a) => a.length);
|
|
check('board /free renders posts', postCount > 5, `${postCount} post links`);
|
|
|
|
// 4c) Open one post
|
|
await page.goto(BASE + '/free/1024', { waitUntil: 'networkidle' });
|
|
const has댓글 = await page.locator('text=댓글').count();
|
|
check('post view shows 댓글 section', has댓글 > 0);
|
|
|
|
// 4d) Submit a comment
|
|
const commentBefore = await page.locator('main h3:has-text("댓글")').first().textContent().catch(() => '');
|
|
await page.fill('main textarea[name="content"]', `자동 검증 댓글 ${Date.now()}`);
|
|
const [respComment] = await Promise.all([
|
|
page.waitForResponse((r) => r.url().includes('/api/posts/1024/comment')),
|
|
page.click('main form[action*="/comment"] button[type="submit"]'),
|
|
]);
|
|
check('comment POST', respComment.status() === 303 || respComment.ok(), `status=${respComment.status()}`);
|
|
await page.waitForLoadState('networkidle').catch(() => {});
|
|
|
|
// 4e) 추천 (Like) button
|
|
await page.goto(BASE + '/free/1024', { waitUntil: 'networkidle' });
|
|
const goodForm = page.locator('main form[action*="/good"]').first();
|
|
if (await goodForm.count() > 0) {
|
|
const [respGood] = await Promise.all([
|
|
page.waitForResponse((r) => r.url().includes('/api/posts/1024/good')),
|
|
goodForm.locator('button[type="submit"]').click(),
|
|
]);
|
|
check('recommend (good) POST', respGood.status() === 303, `status=${respGood.status()}`);
|
|
} else {
|
|
check('recommend (good) POST', false, 'good form not found');
|
|
}
|
|
|
|
// 4f) Theme switch — switch to admin first (testlogin lacks admin level)
|
|
await ctx.clearCookies();
|
|
await page.goto(BASE + '/login');
|
|
await page.fill('input[name="loginId"]', 'admin');
|
|
await page.fill('input[name="password"]', 'test1234');
|
|
await Promise.all([page.waitForURL((u) => u.pathname === '/'), page.click('button[type="submit"]')]);
|
|
await page.goto(BASE + '/admin/themes', { waitUntil: 'domcontentloaded' });
|
|
await page.waitForLoadState('networkidle').catch(() => {});
|
|
const html = await page.content();
|
|
const submitCount = (html.match(/<button[^>]*type="submit"/g) || []).length;
|
|
const themeLabels = ['기본', '이윰빌더', '아미나빌더', '영카드'].filter((l) => html.includes(l)).length;
|
|
check('admin/themes shows 4 theme picker forms', themeLabels === 4 && submitCount >= 4, `submit=${submitCount} themes=${themeLabels}/4`);
|
|
|
|
// 4f-2) Switch theme to amina via direct POST (server action)
|
|
const switchResp = await page.request.post(BASE + '/admin/themes', {
|
|
form: { themeId: 'amina' },
|
|
failOnStatusCode: false,
|
|
});
|
|
check('admin theme switch POST', switchResp.status() === 200 || switchResp.status() === 303, `status=${switchResp.status()}`);
|
|
|
|
// 4g) /admin dashboard renders counts (admin login needed → re-login as admin)
|
|
await ctx.clearCookies();
|
|
await page.goto(BASE + '/login');
|
|
await page.fill('input[name="loginId"]', 'admin');
|
|
await page.fill('input[name="password"]', 'test1234');
|
|
await Promise.all([page.waitForURL((u) => u.pathname === '/'), page.click('button[type="submit"]')]);
|
|
await page.goto(BASE + '/admin', { waitUntil: 'networkidle' });
|
|
const stats = await page.locator('text=총 회원').count();
|
|
check('admin dashboard renders 총 회원 widget', stats > 0);
|
|
|
|
// 4h) Logout — POST directly (form is hidden inside utility-bar dropdown)
|
|
const logoutResp = await page.request.post(BASE + '/api/auth/logout', { failOnStatusCode: false });
|
|
check('logout POST', logoutResp.status() === 303 || logoutResp.status() === 200, `status=${logoutResp.status()}`);
|
|
|
|
// 4i) Attendance check-in (POST) — re-login as testlogin first
|
|
await ctx.clearCookies();
|
|
await page.goto(BASE + '/login');
|
|
await page.fill('input[name="loginId"]', 'testlogin');
|
|
await page.fill('input[name="password"]', 'test1234');
|
|
await Promise.all([page.waitForURL((u) => u.pathname === '/'), page.click('button[type="submit"]')]);
|
|
const attResp = await page.request.post(BASE + '/api/attendance/check', { failOnStatusCode: false });
|
|
check('attendance check-in POST', attResp.status() === 303 || attResp.status() === 200 || attResp.status() === 404, `status=${attResp.status()}`);
|
|
|
|
// ---------------- Final report ----------------
|
|
const summary = {
|
|
total: report.checks.length,
|
|
pass: report.pass,
|
|
fail: report.fail,
|
|
broken,
|
|
};
|
|
await writeFile(OUT + '/report.json', JSON.stringify({ summary, checks: report.checks }, null, 2));
|
|
console.log(`\n========================================`);
|
|
console.log(` PASS: ${report.pass} / FAIL: ${report.fail} / TOTAL: ${report.checks.length}`);
|
|
console.log(`========================================`);
|
|
if (broken.length > 0) {
|
|
console.log('\nBROKEN LINKS:');
|
|
broken.forEach((b) => console.log(` ${b.status} ${b.href} ${b.err ?? ''}`));
|
|
}
|
|
console.log(`\nReport: ${OUT}/report.json`);
|
|
|
|
await browser.close();
|
|
process.exit(report.fail > 0 ? 1 : 0);
|