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:
chpark
2026-04-30 12:08:35 +09:00
parent 57dccaf4a1
commit 2a258be3da
2 changed files with 34 additions and 14 deletions
@@ -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 (
+10 -3
View File
@@ -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,
};
}