메인페이지 작업
This commit is contained in:
@@ -21,6 +21,7 @@ export function AppHeader() {
|
||||
const [panelHeight, setPanelHeight] = React.useState<number>(0);
|
||||
const [blockWidths, setBlockWidths] = React.useState<Record<string, number>>({});
|
||||
const closeTimer = React.useRef<number | null>(null);
|
||||
const [navMinWidths, setNavMinWidths] = React.useState<Record<string, number>>({});
|
||||
|
||||
// 현재 경로 기반 활성 보드/카테고리 계산
|
||||
const pathname = usePathname();
|
||||
@@ -97,6 +98,97 @@ export function AppHeader() {
|
||||
};
|
||||
}, []);
|
||||
|
||||
// (요구) 상위 카테고리 폭을 키우지 않음: 측정은 하되 상위 폭 업데이트는 하지 않음
|
||||
React.useEffect(() => {
|
||||
if (!categories.length) return;
|
||||
const measure = () => {
|
||||
// 상위 폭 미변경: 측정만 수행하고 상태는 건드리지 않음
|
||||
};
|
||||
// 레이아웃 안정화 이후(다음 두 프레임)에 1회 측정
|
||||
const raf1 = window.requestAnimationFrame(() => {
|
||||
const raf2 = window.requestAnimationFrame(() => measure());
|
||||
(measure as any)._raf2 = raf2;
|
||||
});
|
||||
const onResize = () => measure();
|
||||
window.addEventListener("resize", onResize);
|
||||
return () => {
|
||||
window.cancelAnimationFrame(raf1);
|
||||
if ((measure as any)._raf2) window.cancelAnimationFrame((measure as any)._raf2);
|
||||
window.removeEventListener("resize", onResize);
|
||||
};
|
||||
}, [categories]);
|
||||
|
||||
// 초기 로드/리사이즈 시, 드롭다운 최종 폭을 선계산(상위 폭은 키우지 않음)
|
||||
React.useEffect(() => {
|
||||
if (!categories.length) return;
|
||||
const recomputeOnce = () => {
|
||||
const container = panelRef.current;
|
||||
if (!container) return;
|
||||
const containerRect = container.getBoundingClientRect();
|
||||
const nextPositions: Record<string, number> = {};
|
||||
const desiredWidths: Record<string, number> = {};
|
||||
const minWidthsBySlug: Record<string, number> = { community: 200 };
|
||||
categories.forEach((cat) => {
|
||||
const itemEl = navItemRefs.current[cat.slug];
|
||||
if (!itemEl) return;
|
||||
const r = itemEl.getBoundingClientRect();
|
||||
nextPositions[cat.slug] = Math.max(0, r.left - containerRect.left);
|
||||
const baseWidth = r.width; // 상위 아이템 실제 폭
|
||||
const minWidth = minWidthsBySlug[cat.slug] ?? baseWidth;
|
||||
const blockEl = blockRefs.current[cat.slug];
|
||||
let maxChildWidth = 0;
|
||||
if (blockEl) {
|
||||
const links = blockEl.querySelectorAll("a");
|
||||
links.forEach((a) => {
|
||||
const el = a as HTMLElement;
|
||||
const rectW = el.getBoundingClientRect().width;
|
||||
const cs = window.getComputedStyle(el);
|
||||
const ml = parseFloat(cs.marginLeft || "0");
|
||||
const mr = parseFloat(cs.marginRight || "0");
|
||||
const w = Math.ceil(rectW + ml + mr);
|
||||
if (w > maxChildWidth) maxChildWidth = w;
|
||||
});
|
||||
if (maxChildWidth === 0) {
|
||||
const contentEl = blockEl.firstElementChild as HTMLElement | null;
|
||||
maxChildWidth = Math.ceil(contentEl?.getBoundingClientRect().width ?? blockEl.getBoundingClientRect().width);
|
||||
}
|
||||
}
|
||||
// 하위 컨테이너 폭을 상위 폭으로 상한(clamp)
|
||||
const measuredSub = Math.max(maxChildWidth, 0);
|
||||
desiredWidths[cat.slug] = Math.min(baseWidth, Math.max(minWidth, measuredSub));
|
||||
});
|
||||
const finalWidths: Record<string, number> = {};
|
||||
const ordered = categories
|
||||
.filter((c) => typeof nextPositions[c.slug] === "number")
|
||||
.sort((a, b) => (nextPositions[a.slug]! - nextPositions[b.slug]!));
|
||||
ordered.forEach((cat, idx) => {
|
||||
const currentLeft = nextPositions[cat.slug]!;
|
||||
const desired = desiredWidths[cat.slug] ?? 0;
|
||||
const maxAvail = idx < ordered.length - 1
|
||||
? Math.max(0, nextPositions[ordered[idx + 1].slug]! - currentLeft)
|
||||
: Math.max(0, containerRect.width - currentLeft);
|
||||
finalWidths[cat.slug] = Math.min(desired, maxAvail);
|
||||
});
|
||||
|
||||
// 상태 업데이트는 변경된 경우에만 수행
|
||||
const eq = (a: Record<string, number>, b: Record<string, number>) => {
|
||||
const ka = Object.keys(a);
|
||||
const kb = Object.keys(b);
|
||||
if (ka.length !== kb.length) return false;
|
||||
for (const k of ka) { if (a[k] !== b[k]) return false; }
|
||||
return true;
|
||||
};
|
||||
setBlockWidths((prev) => (eq(prev, finalWidths) ? prev : finalWidths));
|
||||
};
|
||||
const raf = window.requestAnimationFrame(() => recomputeOnce());
|
||||
const onResize = () => recomputeOnce();
|
||||
window.addEventListener("resize", onResize);
|
||||
return () => {
|
||||
window.cancelAnimationFrame(raf);
|
||||
window.removeEventListener("resize", onResize);
|
||||
};
|
||||
}, [categories]);
|
||||
|
||||
// 헤더 항목과 드롭다운 블록 정렬: 좌표 측정 및 패널 높이 계산
|
||||
React.useEffect(() => {
|
||||
if (!megaOpen) return;
|
||||
@@ -112,12 +204,31 @@ export function AppHeader() {
|
||||
if (!itemEl) return;
|
||||
const r = itemEl.getBoundingClientRect();
|
||||
nextPositions[cat.slug] = Math.max(0, r.left - containerRect.left);
|
||||
const baseWidth = r.width; // 각 네비 항목의 실제 폭을 기본값으로 사용
|
||||
const baseWidth = r.width; // 상위 항목 실제 폭
|
||||
const minWidth = minWidthsBySlug[cat.slug] ?? baseWidth;
|
||||
desiredWidths[cat.slug] = Math.max(baseWidth, minWidth);
|
||||
// 하위 카테고리(드롭다운) 버튼들의 최대 실제 폭(패딩+마진 포함) 측정
|
||||
const blockEl = blockRefs.current[cat.slug];
|
||||
let maxChildWidth = 0;
|
||||
if (blockEl) {
|
||||
const links = blockEl.querySelectorAll("a");
|
||||
links.forEach((a) => {
|
||||
const el = a as HTMLElement;
|
||||
const rectW = el.getBoundingClientRect().width;
|
||||
const cs = window.getComputedStyle(el);
|
||||
const ml = parseFloat(cs.marginLeft || "0");
|
||||
const mr = parseFloat(cs.marginRight || "0");
|
||||
const w = Math.ceil(rectW + ml + mr);
|
||||
if (w > maxChildWidth) maxChildWidth = w;
|
||||
});
|
||||
if (maxChildWidth === 0) {
|
||||
const contentEl = blockEl.firstElementChild as HTMLElement | null;
|
||||
maxChildWidth = Math.ceil(contentEl?.getBoundingClientRect().width ?? blockEl.getBoundingClientRect().width);
|
||||
}
|
||||
}
|
||||
// 하위 컨테이너 폭을 상위 폭으로 상한(clamp)
|
||||
const measuredSub = Math.max(maxChildWidth, 0);
|
||||
desiredWidths[cat.slug] = Math.min(baseWidth, Math.max(minWidth, measuredSub));
|
||||
});
|
||||
setLeftPositions(nextPositions);
|
||||
|
||||
// 원하는 최소 폭을 기준으로 하되, 다음 항목의 시작점까지를 넘지 않도록 클램프
|
||||
const finalWidths: Record<string, number> = {};
|
||||
const ordered = categories
|
||||
@@ -133,6 +244,18 @@ export function AppHeader() {
|
||||
});
|
||||
setBlockWidths(finalWidths);
|
||||
|
||||
// 우측 경계 초과 시 좌측으로 이동하여 뷰포트 안에 맞춤
|
||||
const finalLefts: Record<string, number> = {};
|
||||
ordered.forEach((cat) => {
|
||||
const currentLeft = nextPositions[cat.slug]!;
|
||||
const width = finalWidths[cat.slug] ?? 0;
|
||||
const maxLeft = Math.max(0, containerRect.width - width);
|
||||
finalLefts[cat.slug] = Math.max(0, Math.min(currentLeft, maxLeft));
|
||||
});
|
||||
setLeftPositions(finalLefts);
|
||||
|
||||
// 상위 카테고리 최소 폭은 초기 측정(useEffect)에서만 설정
|
||||
|
||||
// 패널 높이 = 블록들 중 최대 높이
|
||||
let maxH = 0;
|
||||
categories.forEach((cat) => {
|
||||
@@ -146,6 +269,29 @@ export function AppHeader() {
|
||||
window.addEventListener("resize", onResize);
|
||||
return () => window.removeEventListener("resize", onResize);
|
||||
}, [megaOpen, categories]);
|
||||
|
||||
// xl 이상으로 뷰포트가 커지면 모바일 사이드바를 자동으로 닫음
|
||||
React.useEffect(() => {
|
||||
const mql = window.matchMedia("(min-width: 1280px)");
|
||||
const onChange = (e: any) => {
|
||||
if (e.matches) setMobileOpen(false);
|
||||
};
|
||||
// 초기 상태 체크 (이미 xl 이상인 경우 닫기)
|
||||
if (mql.matches) setMobileOpen(false);
|
||||
if (typeof mql.addEventListener === "function") {
|
||||
mql.addEventListener("change", onChange);
|
||||
} else {
|
||||
// Safari 등 구형 브라우저 호환
|
||||
mql.addListener(onChange);
|
||||
}
|
||||
return () => {
|
||||
if (typeof mql.removeEventListener === "function") {
|
||||
mql.removeEventListener("change", onChange);
|
||||
} else {
|
||||
mql.removeListener(onChange);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
return (
|
||||
<header ref={headerRef} className="relative flex items-center justify-between px-4 py-3 bg-white/80 backdrop-blur supports-[backdrop-filter]:bg-white/60">
|
||||
<div className="flex items-center gap-3 z-[100]">
|
||||
@@ -187,11 +333,11 @@ export function AppHeader() {
|
||||
ref={(el) => {
|
||||
navItemRefs.current[cat.slug] = el;
|
||||
}}
|
||||
style={{ minWidth: idx === categories.length - 1 ? 120 : undefined }}
|
||||
style={idx === categories.length - 1 ? { minWidth: 120 } : undefined}
|
||||
>
|
||||
<Link
|
||||
href={cat.boards?.[0]?.id ? `/boards/${cat.boards[0].id}` : `/boards?category=${cat.slug}`}
|
||||
className={`px-2 py-2 text-sm font-medium transition-colors duration-200 hover:text-neutral-900 whitespace-nowrap ${
|
||||
className={`block w-full px-2 py-2 text-sm font-medium transition-colors duration-200 hover:text-neutral-900 whitespace-nowrap ${
|
||||
activeCategorySlug === cat.slug ? "text-neutral-900" : "text-neutral-700"
|
||||
}`}
|
||||
ref={(el) => {
|
||||
@@ -230,7 +376,7 @@ export function AppHeader() {
|
||||
style={{ left: (leftPositions[cat.slug] ?? 0), width: blockWidths[cat.slug] ?? undefined }}
|
||||
>
|
||||
{/* <div className="mb-2 font-semibold text-neutral-800">{cat.name}</div> */}
|
||||
<div className="mx-auto w-max flex flex-col items-center gap-3">
|
||||
<div className="mx-auto flex flex-col items-center gap-3 w-full">
|
||||
{cat.boards.map((b) => (
|
||||
<Link
|
||||
key={b.id}
|
||||
@@ -252,7 +398,7 @@ export function AppHeader() {
|
||||
</div>
|
||||
</div>
|
||||
<div id="dummy" className="block"></div>
|
||||
<div className="hidden md:block">
|
||||
<div className="hidden xl:flex xl:flex-1 justify-end">
|
||||
<SearchBar/>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
Reference in New Issue
Block a user