- Initial setup and all features from develop branch - Includes: auth, deploy, docker, style fixes - K3S deployment configuration
244 lines
10 KiB
TypeScript
244 lines
10 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useEffect } from "react";
|
|
import Link from "next/link";
|
|
import Image from "next/image";
|
|
import { usePathname, useRouter } from "next/navigation";
|
|
import { ChevronDown } from "lucide-react";
|
|
import iconBlack from "@/public/icon_black.webp";
|
|
import iconWhite from "@/public/icon_white.webp";
|
|
// import AuthButton from "@/components/widgets/AuthButton";
|
|
import tabs from "@/const/tabs";
|
|
|
|
export default function Header() {
|
|
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
|
const [isScrolled, setIsScrolled] = useState(false);
|
|
const [hoveredTab, setHoveredTab] = useState<number | null>(null);
|
|
const [expandedTabs, setExpandedTabs] = useState<Set<number>>(new Set());
|
|
const pathname = usePathname();
|
|
const router = useRouter();
|
|
|
|
// 로그인/회원가입 페이지인지 확인
|
|
const isAuthPage = pathname === "/login" || pathname === "/signup";
|
|
|
|
useEffect(() => {
|
|
const handleScroll = () => {
|
|
setIsScrolled(window.scrollY > 0);
|
|
};
|
|
|
|
window.addEventListener("scroll", handleScroll);
|
|
return () => window.removeEventListener("scroll", handleScroll);
|
|
}, []);
|
|
|
|
// 로그인/회원가입 페이지 또는 모바일 메뉴가 열렸을 때 항상 스크롤된 상태로 표시
|
|
// hover는 CSS로만 처리 (데스크톱에서만 작동)
|
|
const shouldShowScrolled = isAuthPage || isScrolled || isMenuOpen || hoveredTab !== null;
|
|
|
|
// 모바일 메뉴에서 탭 확장/축소 토글
|
|
const toggleTab = (index: number) => {
|
|
setExpandedTabs((prev) => {
|
|
const newSet = new Set(prev);
|
|
if (newSet.has(index)) {
|
|
newSet.delete(index);
|
|
} else {
|
|
newSet.add(index);
|
|
}
|
|
return newSet;
|
|
});
|
|
};
|
|
|
|
return (
|
|
<header
|
|
className={`group pr-4 fixed top-0 left-0 right-0 z-50 transition-all duration-300 ${
|
|
shouldShowScrolled || isMenuOpen
|
|
? "bg-white shadow-md"
|
|
: "bg-transparent pc:hover:bg-white pc:hover:shadow-md"
|
|
}`}
|
|
>
|
|
<nav className="max-w-[1400px] mx-auto">
|
|
<div className="flex justify-between items-stretch h-[56px] pc:h-[70px] relative z-10">
|
|
{/* 로고 */}
|
|
<div className="shrink-0">
|
|
<Link href="/" className="flex items-center gap-3 group h-full px-6">
|
|
{/* 아이콘 - 모바일은 작게, 데스크톱은 크게 */}
|
|
<div className="relative w-8 h-8 pc:w-10 pc:h-10">
|
|
{/* 흰색 아이콘 */}
|
|
<Image
|
|
src={iconWhite}
|
|
alt="제자들교회 로고"
|
|
width={40}
|
|
height={40}
|
|
className={`absolute inset-0 w-full h-full transition-opacity duration-300 ${
|
|
shouldShowScrolled ? "opacity-0" : "opacity-100 pc:group-hover:opacity-0"
|
|
}`}
|
|
placeholder="blur"
|
|
priority
|
|
/>
|
|
{/* 검은색 아이콘 */}
|
|
<Image
|
|
src={iconBlack}
|
|
alt="제자들교회 로고"
|
|
width={40}
|
|
height={40}
|
|
className={`absolute inset-0 w-full h-full transition-opacity duration-300 ${
|
|
shouldShowScrolled ? "opacity-100" : "opacity-0 pc:group-hover:opacity-100"
|
|
}`}
|
|
placeholder="blur"
|
|
priority
|
|
/>
|
|
</div>
|
|
<div className={shouldShowScrolled || isMenuOpen ? "text-black pc:hover:text-black" : "text-white pc:group-hover:text-black"}>
|
|
<div className="text-xl pc:text-2xl font-bold tracking-wide">제자들교회</div>
|
|
{/* 데스크톱: 영어 이름 표시 */}
|
|
<div className="hidden pc:block text-xs opacity-90">DISCIPLES CHURCH</div>
|
|
</div>
|
|
</Link>
|
|
</div>
|
|
|
|
{/* 데스크톱 네비게이션 */}
|
|
<div className="hidden pc:flex">
|
|
{tabs.map((tab, index) => (
|
|
<div
|
|
key={tab.label}
|
|
className="relative flex items-stretch"
|
|
onMouseEnter={() => setHoveredTab(index)}
|
|
onMouseLeave={() => setHoveredTab(null)}
|
|
>
|
|
<Link
|
|
href={tab.href || tab.submenu[0]?.href || "#"}
|
|
className={`${shouldShowScrolled ? "text-black hover:text-black" : "text-white/90 pc:group-hover:text-black/90"} font-medium transition-colors flex items-center px-6`}
|
|
>
|
|
{tab.label}
|
|
</Link>
|
|
|
|
{/* 말풍선 스타일 드롭다운 */}
|
|
{tab.submenu.length > 1 && (
|
|
<div
|
|
className={`absolute top-full left-1/2 -translate-x-1/2 pt-4 transition-all duration-300 ease-in-out ${
|
|
hoveredTab === index
|
|
? "opacity-100 translate-y-0 pointer-events-auto"
|
|
: "opacity-0 -translate-y-2 pointer-events-none"
|
|
}`}
|
|
>
|
|
{/* 말풍선 본체 */}
|
|
<div className="bg-gray-500 text-white rounded-3xl shadow-xl overflow-visible min-w-[180px] relative">
|
|
{/* 말풍선 꼬리 (위쪽 삼각형) - 박스 안쪽 최상단에 위치 */}
|
|
<div className="absolute left-1/2 -translate-x-1/2 -top-3 w-0 h-0 border-l-14 border-l-transparent border-r-14 border-r-transparent border-b-14 border-b-gray-500"/>
|
|
|
|
{tab.submenu.map((item) => (
|
|
<Link
|
|
key={item.href}
|
|
href={item.href}
|
|
className="block px-6 py-3 hover:bg-white/10 transition-colors text-center border-b border-gray-600 last:border-b-0"
|
|
>
|
|
{item.label}
|
|
</Link>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
{/* <AuthButton shouldShowScrolled={shouldShowScrolled} /> */}
|
|
</div>
|
|
|
|
{/* 햄버거 메뉴 */}
|
|
<div className="pc:hidden flex items-center">
|
|
<button
|
|
onClick={() => setIsMenuOpen(!isMenuOpen)}
|
|
className={`${shouldShowScrolled || isMenuOpen ? "text-black" : "text-white pc:group-hover:text-black"} flex items-center px-6 transition-colors duration-300`}
|
|
>
|
|
<svg className="w-7 h-7" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth={2}
|
|
d={isMenuOpen ? "M6 18L18 6M6 6l12 12" : "M4 6h16M4 12h16M4 18h16"}
|
|
className="transition-all duration-300"
|
|
/>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 모바일 메뉴 */}
|
|
{isMenuOpen && (
|
|
<div className="pc:hidden fixed inset-0 z-30">
|
|
{/* 배경 오버레이 */}
|
|
<div
|
|
className="absolute inset-0 transition-opacity duration-300"
|
|
onClick={() => setIsMenuOpen(false)}
|
|
/>
|
|
|
|
{/* 모달 메뉴 */}
|
|
<div className="absolute top-[56px] left-0 right-0 border-b border-gray-300 bg-white max-h-[calc(100vh-56px)] overflow-y-auto pt-4 px-6 pb-6 transition-all z-30 animate-fade-in-fast">
|
|
<div className="space-y-1">
|
|
{tabs.map((tab, index) => {
|
|
const isExpanded = expandedTabs.has(index);
|
|
const firstSubmenuHref = tab.submenu[0]?.href || "#";
|
|
|
|
const handleMainTabClick = () => {
|
|
if (tab.submenu.length > 1) {
|
|
// 서브메뉴가 있는 경우 토글
|
|
toggleTab(index);
|
|
} else {
|
|
// 서브메뉴가 없는 경우 바로 이동
|
|
const targetHref = tab.href || firstSubmenuHref;
|
|
if (targetHref !== "#") {
|
|
router.push(targetHref);
|
|
setIsMenuOpen(false);
|
|
}
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div key={tab.label} className="border-b border-gray-100 last:border-b-0">
|
|
{/* 메인 탭 */}
|
|
<div
|
|
className="w-full flex justify-between items-center font-semibold py-3 text-lg cursor-pointer"
|
|
onClick={handleMainTabClick}
|
|
>
|
|
<span className="flex-1 text-left py-2">
|
|
{tab.label}
|
|
</span>
|
|
{tab.submenu.length > 1 && (
|
|
<span className="ml-2 p-2">
|
|
<ChevronDown
|
|
className={`w-5 h-5 transition-transform duration-200 ${
|
|
isExpanded ? "rotate-180" : ""
|
|
}`}
|
|
/>
|
|
</span>
|
|
)}
|
|
</div>
|
|
{/* 서브 탭들 */}
|
|
{isExpanded && (
|
|
<div className="ml-4 space-y-1 pb-2 animate-fade-in-fast">
|
|
{tab.submenu.map((item) => (
|
|
<Link
|
|
key={item.href}
|
|
href={item.href}
|
|
className="block py-3 text-base text-gray-700 hover:bg-gray-50 rounded-lg px-3"
|
|
onClick={() => setIsMenuOpen(false)}
|
|
>
|
|
{item.label}
|
|
</Link>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
{/* <div className="mt-4 text-center pb-6">
|
|
<AuthButton shouldShowScrolled={true} onLinkClick={() => setIsMenuOpen(false)} />
|
|
</div> */}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</nav>
|
|
</header>
|
|
);
|
|
}
|
|
|