Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 17 additions & 2 deletions src/App.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState, useCallback } from 'react';
import React, { useState, useCallback, useEffect } from 'react';

// Contexts & Providers
import { DataProvider } from './contexts/DataProvider';
Expand Down Expand Up @@ -41,6 +41,7 @@ import PasswordChangeModal from './components/modals/PasswordChangeModal';
import LostItemGuideModal from './components/modals/LostItemGuideModal';
import TimeTagAddModal from './components/modals/TimeTagAddModal';
import TimeTagEditModal from './components/modals/TimeTagEditModal';
import { useIsMobile } from './components/layout/UseIsMobile';

// Common Components
import Toast from './components/common/Toast';
Expand All @@ -50,6 +51,11 @@ function App() {
const [modalState, setModalState] = useState({ type: null, props: {} });
const [toasts, setToasts] = useState([]);
const [sidebarOpen, setSidebarOpen] = useState(true); // 사이드바 열림/닫힘 상태 추가
const isMobile = useIsMobile();

useEffect(() => {
setSidebarOpen(!isMobile); // 모바일에서는 기본 닫힘
}, [isMobile]);

const showToast = useCallback((message) => {
const id = Date.now() + Math.random();
Expand Down Expand Up @@ -111,7 +117,16 @@ function App() {
<DataProvider>
<PageContext.Provider value={{ page, setPage }}>
<ModalContext.Provider value={{ openModal, closeModal, showToast }}>
<div className="bg-gray-100 text-gray-800 flex h-screen">
<div className="bg-gray-100 text-gray-800 flex h-screen relative">
{isMobile && !sidebarOpen && (
<button
className="fixed bottom-6 right-6 z-40 md:hidden bg-white border border-gray-200 shadow-sm px-3 py-2 rounded-full text-gray-700 flex items-center gap-2"
onClick={() => setSidebarOpen(true)}
aria-label="메뉴 열기"
>
<i className="fas fa-bars text-lg" /> 메뉴
</button>
)}

<Sidebar open={sidebarOpen} setOpen={setSidebarOpen} />
<main className="flex-1 p-8 overflow-y-auto">
Expand Down
57 changes: 34 additions & 23 deletions src/components/layout/Sidebar.jsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import React, { useState, useRef } from 'react';
import { usePage } from '../../hooks/usePage';
import { useIsMobile } from './UseIsMobile';

const Sidebar = ({ open, setOpen }) => {
const { page, setPage } = usePage();
const [textVisible, setTextVisible] = useState(open);
const handleNav = (targetPage) => setPage(targetPage);
const isMobile = useIsMobile();
const handleNav = (targetPage) => {
setPage(targetPage);
if (isMobile) setOpen(false);
};

// 텍스트 표시 지연 처리
React.useEffect(() => {
Expand All @@ -18,27 +23,31 @@ const Sidebar = ({ open, setOpen }) => {
}
}, [open]);
// NavLink 컴포넌트도 open prop을 받도록 수정
const NavLink = ({ target, icon, children, open }) => (
<a
href="#"
onClick={(e) => { e.preventDefault(); handleNav(target); }}
className={`sidebar-link flex items-center py-3 rounded-lg transition duration-200 hover:bg-gray-700 hover:text-white ${page === target ? 'active' : 'text-gray-600'} px-4`}
style={{ justifyContent: 'flex-start' }}
>
<i className={`fas ${icon} w-6 text-gray-500`}></i>
{open && (
<span
className={`ml-2 transition-opacity duration-200 whitespace-nowrap overflow-hidden ${textVisible ? 'opacity-100' : 'opacity-0'}`}
>
{children}
</span>
)}
</a>
);
const NavLink = ({ target, icon, children, open }) => {
const isExpanded = open || isMobile;
return (
<a
href="#"
onClick={(e) => { e.preventDefault(); handleNav(target); }}
className={`sidebar-link flex items-center py-3 rounded-lg transition duration-200 hover:bg-gray-700 hover:text-white ${page === target ? 'active' : 'text-gray-600'} px-4`}
style={{ justifyContent: 'flex-start' }}
>
<i className={`fas ${icon} w-6 text-gray-500`}></i>
{isExpanded && (
<span
className={`ml-2 transition-opacity duration-200 whitespace-nowrap overflow-hidden ${textVisible ? 'opacity-100' : 'opacity-0'}`}
>
{children}
</span>
)}
</a>
);
};
// SubMenu는 사이드바가 닫혀있으면 아이콘만, 열려있으면 텍스트와 하위 메뉴까지 보이도록 수정
const SubMenu = ({ icon, title, links, sidebarOpen }) => {
const isActive = links.some(l => l.target === page);
const contentRef = useRef(null);
const isExpanded = sidebarOpen || isMobile;
return (
<div className="relative">
<a
Expand All @@ -60,7 +69,7 @@ const Sidebar = ({ open, setOpen }) => {
>
<div className="flex items-center">
<i className={`fas ${icon} w-6 text-gray-500`}></i>
{sidebarOpen && (
{isExpanded && (
<span
className={`ml-2 transition-opacity duration-200 whitespace-nowrap overflow-hidden ${textVisible ? 'opacity-100' : 'opacity-0'}`}
>
Expand All @@ -70,7 +79,7 @@ const Sidebar = ({ open, setOpen }) => {
</div>
</a>
{/* 사이드바가 열려있을 때만 하위 메뉴 렌더링 */}
{sidebarOpen && (
{isExpanded && (
<div
ref={contentRef}
style={{
Expand Down Expand Up @@ -98,11 +107,13 @@ const Sidebar = ({ open, setOpen }) => {
</div>
);
};
const sidebarClasses = isMobile
? `${open ? 'translate-x-0' : '-translate-x-full'} fixed inset-0 z-50 w-72 max-w-full p-6 bg-white flex flex-col h-full overflow-y-auto transform transition-transform duration-300 ease-in-out shadow-lg`
: `${open ? 'w-64 p-4' : 'w-16 p-2'} bg-gray-50 shrink-0 flex flex-col border-r border-gray-200 h-full transition-all duration-300 ease-in-out relative overflow-hidden`;

return (
<aside
className={
`${open ? 'w-64 p-4' : 'w-16 p-2'} bg-gray-50 shrink-0 flex flex-col border-r border-gray-200 h-full transition-all duration-300 relative overflow-hidden`
}
className={sidebarClasses}
style={{ minHeight: '100vh' }}
>
{/* 상단: Festabook, 자물쇠, 열기/닫기 버튼을 한 줄에 배치 */}
Expand Down
16 changes: 16 additions & 0 deletions src/components/layout/UseIsMobile.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { useEffect, useState } from "react";

export function useIsMobile() {
const [isMobile, setIsMobile] = useState(window.matchMedia("(max-width: 768px)").matches);

useEffect(() => {
const mediaQuery = window.matchMedia("(max-width: 768px)");

const handler = (e) => setIsMobile(e.matches);
mediaQuery.addEventListener("change", handler);

return () => mediaQuery.removeEventListener("change", handler);
}, []);

return isMobile;
}