Mega-menu re-layout + per-board level inline edit + 8-color accent picker
Mega-menu (Header MegaPanel):
- Was: 3-col grid for all sections, leading to broken-looking layout when
the loose-leaves count was low (1 leaf in column 1, group titles in 2/3)
- Now: leaves first as a single column, group sections after; column count
scales 1/2/3/4 by section count, capped at 4
/admin/boards (gnuboard parity):
- Inline read/write/comment Lv editor per row (3 number inputs + apply)
- Bulk "전체 적용" amber banner: set the same 3 levels across every g5_board
in one transaction (e.g. read=1/write=2/comment=2 → consistent per-site)
- /admin/boards/[bo_table]/edit still available via "상세" link for the
remaining 89 columns
Accent picker (like the localhost:9771 reference):
- /api/ui/accent?t={blue|teal|purple|rose|amber|emerald|sky|fuchsia}
- slot_accent cookie persists for 90d; layout reads it and overrides the
theme primary, plus rewrites .bg-mega gradient inline so the mega-nav
immediately reflects the chosen color
- Header utility-bar shows 8 color dots (current color highlighted ring)
- data-accent attribute on <html> for any future per-accent CSS rules
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
#!/usr/bin/env node
|
||||
// Playwright headless: log in as admin/test1234 and walk every admin path.
|
||||
// Then log in as testlogin/test1234 and walk every user-side page.
|
||||
// All against http://103.31.14.201:8088.
|
||||
import { chromium } from 'playwright';
|
||||
|
||||
const REACT = process.env.REACT_BASE || 'http://103.31.14.201:8088';
|
||||
|
||||
const ADMIN_PATHS = [
|
||||
'/admin', '/admin/members', '/admin/boards', '/admin/themes',
|
||||
'/admin/config/auth', '/admin/config/maintenance', '/admin/config/clean', '/admin/config/popups',
|
||||
'/admin/boards/groups', '/admin/boards/faq', '/admin/boards/contents', '/admin/boards/qa-config',
|
||||
'/admin/boards/popular', '/admin/boards/popular-rank', '/admin/boards/parsing',
|
||||
'/admin/boards/wrfixed', '/admin/boards/write-count', '/admin/boards/free/edit',
|
||||
'/admin/shop/items', '/admin/shop/config', '/admin/shop/categories', '/admin/shop/coupons',
|
||||
'/admin/shop/orders', '/admin/shop/sendcost', '/admin/shop/banners',
|
||||
'/admin/shop/brands', '/admin/shop/couponzone', '/admin/shop/buylist',
|
||||
'/admin/shop/item-options', '/admin/shop/item-events', '/admin/shop/personalpay',
|
||||
'/admin/shop/stocksms', '/admin/shop/examount', '/admin/shop/expoint',
|
||||
'/admin/eyoom/menu', '/admin/eyoom/yellowcard', '/admin/eyoom/managers', '/admin/eyoom/biz-info',
|
||||
'/admin/eyoom/main-layout', '/admin/eyoom/tags', '/admin/eyoom/attendance',
|
||||
'/admin/eyoom/themes', '/admin/eyoom/config', '/admin/eyoom/boards', '/admin/eyoom/shopmenu',
|
||||
'/admin/eyoom/ebslider', '/admin/eyoom/ebcontents', '/admin/eyoom/eblatest', '/admin/eyoom/ebbanner',
|
||||
'/admin/eyoom/level', '/admin/eyoom/memo', '/admin/eyoom/activity',
|
||||
'/admin/sms/config', '/admin/sms/write', '/admin/sms/history', '/admin/sms/history-num',
|
||||
'/admin/sms/hp', '/admin/sms/hp-group', '/admin/sms/hp-file', '/admin/sms/emoticon',
|
||||
'/admin/sms/emoticon-group', '/admin/sms/member-update',
|
||||
'/admin/members/visits', '/admin/members/poll', '/admin/members/visit-search',
|
||||
'/admin/members/funnels', '/admin/members/mail', '/admin/members/visit-delete',
|
||||
'/admin/members/point-compress',
|
||||
'/admin/plugin/sns', '/admin/plugin/recaptcha', '/admin/plugin/board-manage',
|
||||
'/admin/plugin/browscap', '/admin/plugin/visit-convert',
|
||||
'/admin/plugin/chatbot', '/admin/plugin/chatbot-feedback',
|
||||
'/admin/roulette', '/admin/roulette/rewards', '/admin/roulette/chances',
|
||||
'/admin/lottery/winners', '/admin/seo',
|
||||
];
|
||||
const USER_PATHS = [
|
||||
'/', '/free', '/review', '/mukti', '/humor', '/pick', '/notice', '/guarantee', '/lottery_ticket',
|
||||
'/mypage', '/mypage/posts', '/mypage/scrap', '/mypage/respond', '/mypage/profile',
|
||||
'/mypage/follower', '/mypage/following', '/mypage/activity', '/mypage/password',
|
||||
'/memo', '/shop', '/games', '/games/fortunes/play', '/wallet/charge', '/auth/cert',
|
||||
'/tag/' + encodeURIComponent('슬롯'),
|
||||
];
|
||||
|
||||
let pass = 0, fail = 0;
|
||||
const failures = [];
|
||||
|
||||
(async () => {
|
||||
const browser = await chromium.launch();
|
||||
const ctx = await browser.newContext();
|
||||
const page = await ctx.newPage();
|
||||
|
||||
console.log(`▶ Playwright headless against ${REACT}\n`);
|
||||
|
||||
// Admin login
|
||||
await page.goto(REACT + '/login');
|
||||
await page.fill('input[name="loginId"]', 'admin');
|
||||
await page.fill('input[name="password"]', 'test1234');
|
||||
await Promise.all([page.waitForURL((u) => !u.pathname.startsWith('/login'), { timeout: 12000 }).catch(() => {}), page.click('button[type="submit"]')]);
|
||||
console.log(' ✅ admin login → ' + page.url());
|
||||
|
||||
// Walk admin paths
|
||||
for (const p of ADMIN_PATHS) {
|
||||
try {
|
||||
const resp = await page.goto(REACT + p, { waitUntil: 'domcontentloaded', timeout: 15000 });
|
||||
const status = resp?.status() ?? 0;
|
||||
if (status < 200 || status >= 400) { fail++; failures.push(`${p} HTTP ${status}`); console.log(` ❌ admin ${p} HTTP ${status}`); continue; }
|
||||
const html = await page.content();
|
||||
const hasKr = /[가-힣]/.test(html);
|
||||
if (hasKr && html.length > 1500) { pass++; console.log(` ✅ admin ${p}`); }
|
||||
else { fail++; failures.push(`${p} no content`); console.log(` ❌ admin ${p} (${html.length} bytes, kr=${hasKr})`); }
|
||||
} catch (e) { fail++; failures.push(`${p} ${e.message.split('\n')[0]}`); console.log(` ❌ admin ${p} ${e.message.split('\n')[0]}`); }
|
||||
}
|
||||
|
||||
// Theme cookie round-trip (4 themes) via redirect API
|
||||
for (const t of ['basic', 'amina', 'youngcart', 'eyoom']) {
|
||||
try {
|
||||
await page.goto(REACT + `/api/ui/theme?t=${t}`, { waitUntil: 'domcontentloaded' });
|
||||
await page.goto(REACT + '/', { waitUntil: 'domcontentloaded' });
|
||||
const cnt = await page.locator(`html[data-theme="${t}"]`).count();
|
||||
if (cnt > 0) { pass++; console.log(` ✅ theme cookie applied: ${t}`); }
|
||||
else { fail++; failures.push('theme ' + t); console.log(` ❌ theme ${t}`); }
|
||||
} catch (e) { fail++; console.log(` ❌ theme ${t} ${e.message}`); }
|
||||
}
|
||||
|
||||
// Switch to user
|
||||
await ctx.clearCookies();
|
||||
await page.goto(REACT + '/login');
|
||||
await page.fill('input[name="loginId"]', 'testlogin');
|
||||
await page.fill('input[name="password"]', 'test1234');
|
||||
await Promise.all([page.waitForURL((u) => !u.pathname.startsWith('/login'), { timeout: 12000 }).catch(() => {}), page.click('button[type="submit"]')]);
|
||||
console.log(' ✅ user login → ' + page.url());
|
||||
|
||||
for (const p of USER_PATHS) {
|
||||
try {
|
||||
const resp = await page.goto(REACT + p, { waitUntil: 'domcontentloaded', timeout: 15000 });
|
||||
const status = resp?.status() ?? 0;
|
||||
if (status < 200 || status >= 400) { fail++; failures.push(`user ${p} HTTP ${status}`); console.log(` ❌ user ${p} HTTP ${status}`); continue; }
|
||||
const html = await page.content();
|
||||
const hasKr = /[가-힣]/.test(html);
|
||||
if (hasKr && html.length > 1500) { pass++; console.log(` ✅ user ${p}`); }
|
||||
else { fail++; failures.push(`user ${p} no content`); console.log(` ❌ user ${p} (${html.length} bytes)`); }
|
||||
} catch (e) { fail++; console.log(` ❌ user ${p} ${e.message.split('\n')[0]}`); }
|
||||
}
|
||||
|
||||
await browser.close();
|
||||
console.log(`\n=== TOTAL: ${pass} passed, ${fail} failed ===`);
|
||||
if (fail > 0) { console.log('Failures:'); for (const f of failures) console.log(' - ' + f); process.exit(1); }
|
||||
})();
|
||||
Reference in New Issue
Block a user