메가메뉴 닫힘 딜레이이

This commit is contained in:
mota
2025-10-13 14:41:51 +09:00
parent 4b1c60a9bf
commit 30ffadec5e
3 changed files with 28 additions and 7 deletions

View File

@@ -5,7 +5,7 @@ const prisma = new PrismaClient();
async function upsertCategories() {
// 카테고리 트리 (projectmemo 기준 상위 그룹)
const categories = [
{ name: "암실소문 (메인)", slug: "main", sortOrder: 1, status: "active" },
{ name: "암실소문", slug: "main", sortOrder: 1, status: "active" },
{ name: "명예의 전당", slug: "hall-of-fame", sortOrder: 2, status: "active" },
{ name: "주변 제휴업체", slug: "nearby-partners", sortOrder: 3, status: "active" },
{ name: "제휴업소 정보", slug: "partner-info", sortOrder: 4, status: "active" },
@@ -128,8 +128,11 @@ async function upsertBoards(admin, categoryMap) {
{ name: "회원랭킹", slug: "ranking", description: "랭킹", type: "special", sortOrder: 14 },
{ name: "무료쿠폰", slug: "free-coupons", description: "쿠폰", type: "special", sortOrder: 15 },
{ name: "월간집계", slug: "monthly-stats", description: "월간 통계", type: "special", sortOrder: 16 },
// 제휴업소 일반(사진)
{ name: "제휴업소 일반(사진)", slug: "partners-photos", description: "사진 전용 게시판", type: "general", sortOrder: 17, requiredFields: { imageOnly: true, minImages: 1, maxImages: 10 } },
// 제휴업소 일반
{ name: "제휴업소", slug: "partners-photos", description: "사진 전용 게시판", type: "general", sortOrder: 17, requiredFields: { imageOnly: true, minImages: 1, maxImages: 10 } },
// 광고/제휴
{ name: "제휴문의", slug: "partner-contact", description: "제휴문의", type: "general", sortOrder: 18, requiredFields: { imageOnly: true, minImages: 1, maxImages: 10 } },
{ name: "제휴업소 요청", slug: "partner-req", description: "제휴업소 요청", type: "general", sortOrder: 19, requiredFields: { imageOnly: true, minImages: 1, maxImages: 10 } },
];
const created = [];
@@ -159,6 +162,9 @@ async function upsertBoards(admin, categoryMap) {
anonymous: "community",
"find-therapist": "community",
"blue-house": "community",
// 광고/제휴
"partner-contact": "ads-affiliates",
"partner-req": "ads-affiliates",
};
const categorySlug = mapBySlug[b.slug] || "community";
const category = categoryMap[categorySlug];

View File

@@ -18,6 +18,19 @@ export function AppHeader() {
const [leftPositions, setLeftPositions] = React.useState<Record<string, number>>({});
const [panelHeight, setPanelHeight] = React.useState<number>(0);
const [blockWidths, setBlockWidths] = React.useState<Record<string, number>>({});
const closeTimer = React.useRef<number | null>(null);
const cancelClose = React.useCallback(() => {
if (closeTimer.current) {
window.clearTimeout(closeTimer.current);
closeTimer.current = null;
}
}, []);
const scheduleClose = React.useCallback(() => {
cancelClose();
closeTimer.current = window.setTimeout(() => setMegaOpen(false), 150);
}, [cancelClose]);
// 카테고리 로드
React.useEffect(() => {
fetch("/api/categories", { cache: "no-store" })
@@ -120,8 +133,8 @@ export function AppHeader() {
{/* 데스크톱 메가메뉴 */}
<div
className="relative hidden xl:block pl-10"
onMouseEnter={() => setMegaOpen(true)}
onMouseLeave={() => setMegaOpen(false)}
onMouseEnter={() => { cancelClose(); setMegaOpen(true); }}
onMouseLeave={() => { scheduleClose(); }}
onFocusCapture={() => setMegaOpen(true)}
onBlurCapture={(e) => {
const next = (e as unknown as React.FocusEvent<HTMLDivElement>).relatedTarget as Node | null;
@@ -153,8 +166,10 @@ export function AppHeader() {
megaOpen ? "opacity-100" : "pointer-events-none opacity-0"
}`}
style={{ top: headerBottom }}
onMouseEnter={() => { cancelClose(); setMegaOpen(true); }}
onMouseLeave={() => { scheduleClose(); }}
>
<div className="px-4 py-4 w-screen">
<div className="px-4 py-4 w-full max-w-7xl mx-auto overflow-x-hidden">
<div ref={panelRef} className="relative">
{categories.map((cat) => (
<div

View File

@@ -23,7 +23,7 @@ export default function RootLayout({
<ToastProvider>
<div className="min-h-screen flex flex-col">
<div className="sticky top-0 z-50 border-b bg-white/80 backdrop-blur">
<div className="mx-auto">
<div className="mx-auto max-w-7xl w-full">
<AppHeader />
</div>
</div>