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
17 changes: 17 additions & 0 deletions .github/ISSUE_TEMPLATE/issue_template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
name: "🚀 이슈"
about: "[Festabook] 프로젝트 이슈 템플릿"
title: "[Tag] 이슈 제목"
labels: ''
assignees: ''

---

## 📝 개요

이슈에 대한 간략한 설명을 작성해주세요.

## ✅ 할 일

- [ ] 첫 번째 할 일
- [ ] 두 번째 할 일
21 changes: 21 additions & 0 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
## #️⃣ 이슈 번호

> ex) #이슈번호, #이슈번호

<br>

## 🛠️ 작업 내용

- 구현한 기능을 작성해주세요.

<br>

## 🙇🏻 중점 리뷰 요청

- 특히 확인이 필요한 부분, 고민했던 부분 등을 적어주세요.

<br>

## 📸 이미지 첨부 (Optional)

<img src="파일주소" width="50%" height="50%"/>
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
51 changes: 43 additions & 8 deletions src/components/common/Modal.jsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,21 @@
import React, { useState, useEffect, useRef, useCallback } from 'react';
import { useIsMobile } from '../layout/UseIsMobile';

const Modal = ({ isOpen, onClose, children, maxWidth = 'max-w-lg', showCloseButton = true }) => {
const Modal = ({
isOpen,
onClose,
children,
maxWidth = 'max-w-lg',
showCloseButton = true,
closeOnBackdropClick = false,
// 모바일에서 공통 모달 레이아웃을 적용할지 여부
enableMobileLayout = false,
}) => {
const [visible, setVisible] = useState(false);
const [showContent, setShowContent] = useState(false);
const modalRef = useRef(null);
const backdropRef = useRef(null);
const isMobile = useIsMobile();

useEffect(() => {
if (isOpen) {
Expand All @@ -15,31 +26,55 @@ const Modal = ({ isOpen, onClose, children, maxWidth = 'max-w-lg', showCloseButt
}
}, [isOpen]);

// ESC 키로 닫기
useEffect(() => {
if (!isOpen) return;

const handleKeyPress = (e) => {
if (e.key === 'Escape') {
onClose();
}
};

document.addEventListener('keydown', handleKeyPress);
return () => {
document.removeEventListener('keydown', handleKeyPress);
};
}, [isOpen, onClose]);

// 트랜지션이 끝났을 때만 visible을 false로 변경
const handleBackdropTransitionEnd = useCallback((e) => {
if (!showContent && e.target === backdropRef.current && e.propertyName === 'opacity') {
setVisible(false);
}
}, [showContent]);

// 바깥 클릭으로 닫히지 않도록 제거
// const handleBackdropClick = (e) => {
// if (modalRef.current && !modalRef.current.contains(e.target)) {
// onClose();
// }
// };
// 바깥 클릭으로 닫기
const handleBackdropClick = (e) => {
if (closeOnBackdropClick && modalRef.current && !modalRef.current.contains(e.target)) {
onClose();
}
};

if (!visible) return null;

const useMobileModalLayout = isMobile && enableMobileLayout;

const paddingClass = useMobileModalLayout ? 'px-4 py-4' : 'p-6';
const mobileSizeClasses = useMobileModalLayout
? 'w-[92vw] max-w-[420px] max-h-[80vh] overflow-y-auto'
: `w-full ${maxWidth}`;

return (
<div
ref={backdropRef}
className={`modal-backdrop fixed inset-0 flex justify-center items-center transition-opacity duration-300 z-50 ${showContent ? 'bg-black/50' : 'bg-black/0'}`}
onTransitionEnd={handleBackdropTransitionEnd}
onClick={handleBackdropClick}
>
<div
ref={modalRef}
className={`modal-content ${showContent ? 'modal-content-end' : 'modal-content-start'} bg-white rounded-lg shadow-xl w-full ${maxWidth} p-6 relative`}
className={`modal-content ${showContent ? 'modal-content-end' : 'modal-content-start'} bg-white rounded-lg shadow-xl ${mobileSizeClasses} ${paddingClass} relative`}
onClick={e => e.stopPropagation()}
>
{showCloseButton && (
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;
}
13 changes: 9 additions & 4 deletions src/components/modals/OtherPlaceEditModal.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, { useState, useEffect } from 'react';
import Modal from '../common/Modal';
import { placeAPI, timeTagAPI } from '../../utils/api';
import { useIsMobile } from '../layout/UseIsMobile';

const OtherPlaceEditModal = ({ place, onSave, onClose, showToast }) => {
const [form, setForm] = useState({
Expand All @@ -11,6 +12,7 @@ const OtherPlaceEditModal = ({ place, onSave, onClose, showToast }) => {
const [loading, setLoading] = useState(false);
const [timeTags, setTimeTags] = useState([]);
const [selectedTimeTags, setSelectedTimeTags] = useState([]);
const isMobile = useIsMobile();

useEffect(() => {
if (place) {
Expand Down Expand Up @@ -114,7 +116,9 @@ const OtherPlaceEditModal = ({ place, onSave, onClose, showToast }) => {
onClose={onClose}
title="기타 시설 수정"
maxWidth="max-w-md"
enableMobileLayout={true}
>
<div className={isMobile ? 'p-4 max-h-[70vh] overflow-y-auto' : ''}>
<form onSubmit={handleSubmit} className="space-y-6">
{/* 플레이스 이름 */}
<div>
Expand Down Expand Up @@ -156,19 +160,19 @@ const OtherPlaceEditModal = ({ place, onSave, onClose, showToast }) => {
)}

{/* 버튼 */}
<div className="mt-6 flex justify-end w-full relative z-10">
<div className="space-x-3">
<div className={`mt-6 w-full relative z-10 ${isMobile ? 'flex flex-col gap-3' : 'flex justify-end'}`}>
<div className={isMobile ? 'flex flex-col gap-3 w-full' : 'space-x-3'}>
<button
type="button"
onClick={onClose}
className="bg-gray-200 hover:bg-gray-300 text-gray-800 font-bold py-2 px-4 rounded-lg transition-colors duration-200"
className={`${isMobile ? 'w-full' : ''} bg-gray-200 hover:bg-gray-300 text-gray-800 font-bold py-2 px-4 rounded-lg transition-colors duration-200`}
disabled={loading}
>
취소
</button>
<button
type="submit"
className={`${
className={`${isMobile ? 'w-full mt-2 md:mt-0' : ''} ${
loading ? 'bg-gray-500' : 'bg-gray-800 hover:bg-gray-900'
} text-white font-bold py-2 px-4 rounded-lg transition-colors duration-200`}
disabled={loading}
Expand All @@ -178,6 +182,7 @@ const OtherPlaceEditModal = ({ place, onSave, onClose, showToast }) => {
</div>
</div>
</form>
</div>
</Modal>
);
};
Expand Down
Loading