PHP-parity: anon mb_level=1 + mega-menu separator-grouped columns
Board access (gnuboard common.php parity):
- gnuboard sets default mb_level=1 for anonymous visitors (common.php:675)
- React was treating anon as level 0, so bo_read_level=1 boards were blocking
anonymous reads when PHP allowed them. Fix: checkBoardAccess() now coerces
userLevel<1 to 1 before comparison
- Verified externally: anon /free /review /humor → 200 (read=1), /notice → 307 (read=2)
Mega-menu (포인트게임 layout fix):
- gnuboard's g5_eyoom_menu uses leaf separator labels like "─ 스포츠 ─" /
"─ 미니게임 ─" / "─ 슬롯/릴 ─" with href='#' to visually group sub-items.
- MegaPanel now recognises these as section breaks: each separator starts a
new column with the cleaned label as the column title, and following
leaves attach to that section until the next separator.
- Fallback: items that have actual children still render as titled groups.
- Result: 포인트게임 hover now lays out as
포인트 바카라 (loose) | 스포츠 | 미니게임 | 슬롯/릴 | (loose tail)
instead of one giant column + scattered group headers.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -164,23 +164,36 @@ function IconLink({ href, Icon, emoji, label, badge }: { href: string; Icon?: Re
|
||||
|
||||
/** Group submenu items: items with children render as section headers with their kids inline below. */
|
||||
function MegaPanel({ items }: { items: MenuItem[] }) {
|
||||
// Group leaves into a "MAIN" section then add each titled child group as its own column.
|
||||
const looseLeaves: MenuItem[] = [];
|
||||
const groupSections: { title: string; href?: string; links: MenuItem[] }[] = [];
|
||||
// Two grouping strategies — pick whichever gnuboard's data hints at:
|
||||
// (a) child-grouped: items with `children` become titled section columns
|
||||
// (b) separator-grouped: a leaf with href='#' or label like "─ X ─" / "── X ──"
|
||||
// starts a new section with that label as the column title (used by the
|
||||
// gnuboard 슬생 data, which flattens 포인트게임 sub-menu)
|
||||
const sections: { title?: string; href?: string; links: MenuItem[] }[] = [];
|
||||
let cur: { title?: string; href?: string; links: MenuItem[] } = { links: [] };
|
||||
function flush() { if (cur.links.length > 0 || cur.title) sections.push(cur); cur = { links: [] }; }
|
||||
|
||||
const isSeparator = (it: MenuItem) => {
|
||||
const lab = (it.label || '').replace(/[─\-=ㅡ]/g, '').trim();
|
||||
return (it.href === '#' || !it.href) && lab.length <= 8 && (it.label || '').match(/[─\-=ㅡ]/);
|
||||
};
|
||||
|
||||
for (const it of items) {
|
||||
if (it.children && it.children.length > 0) {
|
||||
groupSections.push({ title: it.label, href: it.href, links: it.children });
|
||||
flush();
|
||||
sections.push({ title: it.label, href: it.href, links: it.children });
|
||||
} else if (isSeparator(it)) {
|
||||
flush();
|
||||
const label = (it.label || '').replace(/[─\-=ㅡ\s]/g, '').trim();
|
||||
cur = { title: label || it.label, links: [] };
|
||||
} else {
|
||||
looseLeaves.push(it);
|
||||
cur.links.push(it);
|
||||
}
|
||||
}
|
||||
// Always show loose leaves first as a single "전체" column, then group columns.
|
||||
const sections: { title?: string; href?: string; links: MenuItem[] }[] = [];
|
||||
if (looseLeaves.length > 0) sections.push({ links: looseLeaves });
|
||||
sections.push(...groupSections);
|
||||
flush();
|
||||
|
||||
// Layout: 1 col when 1-2 sections, 2 col when 3, 3 col when 4+ but never more than 4 cols.
|
||||
const colCount = sections.length <= 2 ? 1 : sections.length === 3 ? 2 : Math.min(4, sections.length);
|
||||
// Layout: pick column count by total sections; cap 4
|
||||
const colCount = sections.length <= 1 ? 1 : sections.length === 2 ? 2 : sections.length === 3 ? 3 : Math.min(4, sections.length);
|
||||
const colClass = colCount === 1 ? 'grid-cols-1' : colCount === 2 ? 'grid-cols-2' : colCount === 3 ? 'grid-cols-3' : 'grid-cols-4';
|
||||
|
||||
return (
|
||||
|
||||
@@ -76,17 +76,24 @@ export async function getBoardMeta(slug: string): Promise<LegacyBoardMeta | null
|
||||
return rows[0] ? rowToMeta(rows[0]) : null;
|
||||
}
|
||||
|
||||
/** Throws-friendly access check: returns 'ok' or a redirect URL. */
|
||||
/** Throws-friendly access check: returns 'ok' or a redirect URL.
|
||||
* Mirrors gnuboard common.php (line 675): anonymous visitors get mb_level=1
|
||||
* by default, so reading a level-1 board is allowed without login.
|
||||
* Pass userLevel=0 only if the caller has already authenticated and is
|
||||
* certain the user is below mb_level=1 (which never happens in stock g5).
|
||||
*/
|
||||
export function checkBoardAccess(meta: LegacyBoardMeta, userLevel: number, action: 'read' | 'write' | 'comment'): { ok: true } | { ok: false; reason: string; needsLogin: boolean; required: number } {
|
||||
const required =
|
||||
action === 'read' ? meta.readLevel :
|
||||
action === 'write' ? meta.writeLevel :
|
||||
meta.commentLevel;
|
||||
if (userLevel >= required) return { ok: true };
|
||||
// Anonymous visitors are level 1 in gnuboard common.php — treat 0 as 1
|
||||
const effective = userLevel >= 1 ? userLevel : 1;
|
||||
if (effective >= required) return { ok: true };
|
||||
return {
|
||||
ok: false,
|
||||
reason: `이 게시판은 레벨 ${required} 이상만 ${action === 'read' ? '열람' : action === 'write' ? '작성' : '댓글 작성'} 가능합니다`,
|
||||
needsLogin: userLevel === 0,
|
||||
needsLogin: userLevel === 0 && required > 1,
|
||||
required,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user