ERP-node/frontend/app/(main)/main/layout.tsx

161 lines
5.2 KiB
TypeScript

"use client";
import { useState, useEffect } from "react";
import { useAuth } from "@/hooks/useAuth";
import { useMenu } from "@/hooks/useMenu";
import { useProfile } from "@/hooks/useProfile";
import { MainHeader } from "@/components/layout/MainHeader";
import { MainSidebar } from "@/components/layout/MainSidebar";
import { ProfileModal } from "@/components/layout/ProfileModal";
import { LAYOUT_CONFIG } from "@/constants/layout";
/**
* 메인 레이아웃 컴포넌트
* 비즈니스 로직은 커스텀 훅들에서 처리하고, UI 컴포넌트들을 조합하여 구성
*/
export default function MainLayout({ children }: { children: React.ReactNode }) {
const { user, logout, refreshUserData, loading: authLoading } = useAuth();
const [isSidebarOpen, setIsSidebarOpen] = useState(false); // 사이드바 상태
const [isMobile, setIsMobile] = useState(false); // 모바일 여부
// 화면 크기 감지
useEffect(() => {
const checkMobile = () => {
const mobile = window.innerWidth < 768; // md breakpoint
setIsMobile(mobile);
// 모바일에서는 사이드바를 기본적으로 닫아둠
if (mobile) {
setIsSidebarOpen(false);
}
// PC에서는 사이드바 상태를 건드리지 않음 (항상 표시)
};
checkMobile();
window.addEventListener("resize", checkMobile);
return () => window.removeEventListener("resize", checkMobile);
}, []);
// 메뉴 관련 로직
const { menuList, expandedMenus, handleMenuClick } = useMenu(user, authLoading);
// 프로필 관련 로직
const {
isModalOpen,
formData,
selectedImage,
isSaving,
alertModal,
closeAlert,
openProfileModal,
closeProfileModal,
updateFormData,
selectImage,
removeImage,
saveProfile,
} = useProfile(user, refreshUserData);
/**
* 사이드바 토글 처리 (모바일에서만 동작)
*/
const handleSidebarToggle = () => {
if (isMobile) {
// 모바일에서만 토글 동작
setIsSidebarOpen(!isSidebarOpen);
}
// PC에서는 아무것도 하지 않음 (항상 표시)
};
/**
* 로그아웃 처리
*/
const handleLogout = async () => {
await logout();
};
return (
<div className="bg-background flex h-screen flex-col">
<MainHeader
user={user}
onSidebarToggle={handleSidebarToggle}
onProfileClick={openProfileModal}
onLogout={handleLogout}
/>
<div className="relative flex-1">
{/* 데스크톱 고정 사이드바 - md 이상에서 항상 표시 */}
<aside
className={`fixed top-14 left-0 z-40 hidden md:block ${LAYOUT_CONFIG.SIDEBAR.WIDTH} bg-background h-[calc(100vh-3.5rem)] border-r`}
>
<MainSidebar
menuList={menuList}
expandedMenus={expandedMenus}
onMenuClick={handleMenuClick}
className="h-full p-4"
/>
</aside>
{/* 모바일 오버레이 사이드바 - md 미만에서만 표시 */}
{isMobile && (
<div
className={`fixed top-14 right-0 bottom-0 left-0 z-50 flex transition-all duration-300 ease-in-out md:hidden ${
isSidebarOpen ? "opacity-100" : "pointer-events-none opacity-0"
}`}
>
{/* 배경 오버레이 */}
<div
className={`fixed top-14 right-0 bottom-0 left-0 bg-black/50 transition-opacity duration-300 ${
isSidebarOpen ? "opacity-100" : "opacity-0"
}`}
onClick={() => setIsSidebarOpen(false)}
/>
{/* 사이드바 */}
<aside
className={`relative ${LAYOUT_CONFIG.SIDEBAR.WIDTH} bg-background flex h-full transform flex-col shadow-lg transition-transform duration-300 ease-in-out ${
isSidebarOpen ? "translate-x-0" : "-translate-x-full"
}`}
>
{/* 메뉴 리스트 */}
<div className="flex-1 overflow-y-auto">
<MainSidebar
menuList={menuList}
expandedMenus={expandedMenus}
onMenuClick={(menu) => {
handleMenuClick(menu);
// 모바일에서 메뉴 클릭 시 사이드바 닫기
if (isMobile && (!menu.children || menu.children.length === 0)) {
setIsSidebarOpen(false);
}
}}
className="p-4"
/>
</div>
</aside>
</div>
)}
{/* Main Content - 반응형 여백 적용 */}
<main className="ml-0 w-full transition-all duration-300 md:ml-64 md:w-auto">
<div className="h-full p-6">{children}</div>
</main>
</div>
{/* 프로필 수정 모달 */}
<ProfileModal
isOpen={isModalOpen}
user={user}
formData={formData}
selectedImage={selectedImage}
isSaving={isSaving}
alertModal={alertModal}
onClose={closeProfileModal}
onFormChange={updateFormData}
onImageSelect={selectImage}
onImageRemove={removeImage}
onSave={saveProfile}
onAlertClose={closeAlert}
/>
</div>
);
}