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
86 changes: 72 additions & 14 deletions frontend/src/components/Header.js
Original file line number Diff line number Diff line change
@@ -1,23 +1,81 @@
import { useState, useEffect, useCallback } from 'react';
import { NavLink } from 'react-router-dom';
import styles from './Header.module.css';

function Header({ type }) {
const [menuOpen, setMenuOpen] = useState(false);

const closeMenu = useCallback(() => setMenuOpen(false), []);

useEffect(() => {
const mq = window.matchMedia('(min-width: 1025px)');
const handler = (e) => { if (e.matches) closeMenu(); };
mq.addEventListener('change', handler);
return () => mq.removeEventListener('change', handler);
}, [closeMenu]);

useEffect(() => {
document.body.style.overflow = menuOpen ? 'hidden' : '';
return () => { document.body.style.overflow = ''; };
}, [menuOpen]);

const handleLogout = () => {
localStorage.removeItem('token');
localStorage.removeItem('role');
window.location.href = '/login';
};

const themeClass = type === 'dark' ? styles.dark : styles.light;

return (
<header className={`${styles.header} ${type === "dark" ? styles.dark : styles.light}`}>
<NavLink to="/" className={styles.logo}>PIROIN</NavLink>
<nav className={styles.nav}>
<NavLink to="/pirocheck" className={({ isActive }) => isActive ? styles.active : ''}>PIROCHECK</NavLink>
<NavLink to="/sessions" className={({ isActive }) => isActive ? styles.active : ''}>Q&A</NavLink>
<NavLink to="/curriculum" className={({ isActive }) => isActive ? styles.active : ''}>커리큘럼</NavLink>
<>
<header className={`${styles.header} ${themeClass}`}>
<NavLink to="/" className={styles.logo}>PIROIN</NavLink>

<nav className={styles.nav}>
<NavLink to="/pirocheck" className={({ isActive }) => isActive ? styles.active : ''}>PIROCHECK</NavLink>
<NavLink to="/sessions" className={({ isActive }) => isActive ? styles.active : ''}>Q&A</NavLink>
<NavLink to="/curriculum" className={({ isActive }) => isActive ? styles.active : ''}>커리큘럼</NavLink>
</nav>

<button className={styles.logoutBtn} onClick={handleLogout}>
로그아웃
</button>

<button
className={`${styles.hamburger} ${menuOpen ? styles.hamburgerOpen : ''}`}
onClick={() => setMenuOpen((prev) => !prev)}
aria-label={menuOpen ? '메뉴 닫기' : '메뉴 열기'}
aria-expanded={menuOpen}
>
<span />
<span />
<span />
</button>
</header>

{/* 오버레이 */}
<div
className={`${styles.overlay} ${menuOpen ? styles.overlayVisible : ''}`}
onClick={closeMenu}
aria-hidden="true"
/>

{/* 드로어: themeClass 추가로 CSS 변수 상속 */}
<nav
className={`${styles.drawer} ${themeClass} ${menuOpen ? styles.drawerOpen : ''}`}
aria-hidden={!menuOpen}
>
<button className={styles.drawerCloseBtn} onClick={closeMenu} aria-label="메뉴 닫기">✕</button>
<NavLink to="/pirocheck" className={({ isActive }) => isActive ? styles.active : ''} onClick={closeMenu}>PIROCHECK</NavLink>
<NavLink to="/sessions" className={({ isActive }) => isActive ? styles.active : ''} onClick={closeMenu}>Q&A</NavLink>
<NavLink to="/curriculum" className={({ isActive }) => isActive ? styles.active : ''} onClick={closeMenu}>커리큘럼</NavLink>

<button className={styles.drawerLogoutBtn} onClick={handleLogout}>
로그아웃
</button>
</nav>
<button className={styles.logoutBtn} onClick={() => {
localStorage.removeItem('token');
localStorage.removeItem('role');
window.location.href = '/login';
}}>
로그아웃
</button>
</header>
</>
);
}

Expand Down
165 changes: 162 additions & 3 deletions frontend/src/components/Header.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
--header-bg: var(--black);
--header-color: var(--white);
--logo-color: var(--main);
border-bottom: none; /* 추가 */
border-bottom: none;
}

.header {
Expand All @@ -21,7 +21,6 @@
align-items: center;
padding: 0 80px;
box-sizing: border-box;
position: relative;
position: sticky;
top: 0;
z-index: 100;
Expand All @@ -33,8 +32,10 @@
font-size: 2.8rem;
font-weight: 800;
text-decoration: none;
flex-shrink: 0;
}

/* ── 데스크탑 nav ── */
.nav {
display: flex;
gap: 6rem;
Expand All @@ -51,6 +52,7 @@
font-size: 1.4rem;
font-weight: 500;
text-decoration: none;
white-space: nowrap;
}

.nav a:hover {
Expand All @@ -72,6 +74,163 @@
font-weight: 500;
cursor: pointer;
opacity: 0.7;
flex-shrink: 0;
white-space: nowrap;
}

.logoutBtn:hover { opacity: 1; transition: all ease-in-out 0.2s; }
.logoutBtn:hover {
opacity: 1;
transition: all ease-in-out 0.2s;
}

/* ── 햄버거 버튼 ── */
.hamburger {
display: none;
flex-direction: column;
justify-content: center;
gap: 5px;
margin-left: auto;
background: transparent;
border: none;
cursor: pointer;
padding: 4px;
z-index: 200;
}

.hamburger span {
display: block;
width: 24px;
height: 2px;
background: var(--header-color);
border-radius: 2px;
transition: transform 0.3s ease, opacity 0.3s ease, width 0.3s ease;
transform-origin: center;
}

.hamburgerOpen span:nth-child(1) {
transform: translateY(7px) rotate(45deg);
}
.hamburgerOpen span:nth-child(2) {
opacity: 0;
width: 0;
}
.hamburgerOpen span:nth-child(3) {
transform: translateY(-7px) rotate(-45deg);
}

/* ── 오버레이 (항상 DOM에 존재, visibility로 제어) ── */
.overlay {
position: fixed;
inset: 0;
z-index: 140;
opacity: 0;
visibility: hidden;
transition: opacity 0.35s ease, visibility 0.35s ease;
backdrop-filter: blur(2px);
}

.overlayVisible {
opacity: 1;
visibility: visible;
}

/* ── 드로어 (항상 DOM에 존재, transform으로 제어) ── */
.drawer {
position: fixed;
top: 0;
right: 0;
width: 260px;
height: 100vh;
z-index: 150;
display: flex;
flex-direction: column;
padding: 90px 36px 40px;
box-sizing: border-box;
transform: translateX(100%);
visibility: hidden;
transition: transform 0.35s cubic-bezier(0.4, 0, 0.2, 1),
visibility 0.35s ease;
box-shadow: -4px 0 24px rgba(0, 0, 0, 0.15);
}

.drawerOpen {
transform: translateX(0);
visibility: visible;
}

.drawer a {
color: var(--header-color);
font-family: var(--font-main);
font-size: 1.2rem;
font-weight: 500;
text-decoration: none;
padding: 16px 0;
border-bottom: 1px solid rgba(128, 128, 128, 0.15);
transition: color 0.2s ease;
}

.drawer a:last-of-type {
border-bottom: none;
}

.drawer a:hover {
color: var(--logo-color);
}

.drawerLogoutBtn {
margin-top: auto;
background: transparent;
border: 1px solid rgba(128, 128, 128, 0.3);
border-radius: 8px;
color: var(--dark);
font-family: var(--font-main);
font-size: 1rem;
font-weight: 500;
cursor: pointer;
opacity: 1;
padding: 10px 0;
transition: opacity 0.2s ease;
}

.drawerLogoutBtn:hover {
background: var(--dark);
color: var(--white);
transition: all ease-in-out 0.2s;
}

/* ── 반응형: 1024px 이하에서 햄버거로 전환 ── */
@media (max-width: 1024px) {
.header {
padding: 0 24px;
}

.nav,
.logoutBtn {
display: none;
}

.hamburger {
display: flex;
}
}

/* ── 드로어 닫기 버튼 ── */
.drawerCloseBtn {
position: absolute;
top: 20px;
right: 20px;
background: transparent;
border: none;
color: var(--header-color);
font-size: 1.8rem;
cursor: pointer;
opacity: 0.6;
line-height: 1;
padding: 4px 8px;
transition: opacity 0.2s ease;
}

.drawerCloseBtn:hover {
opacity: 1;
}

30 changes: 30 additions & 0 deletions frontend/src/pages/OnboardingPage.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,34 @@
font-size: 18px;
margin: 4px 0;
font-weight: 550;
text-align: center;
}

/* ── 모바일 ── */
@media (max-width: 480px) {
.title {
font-size: 60px;
margin-bottom: 32px;
}

.logoWrap {
width: 200px;
height: 200px;
margin-bottom: 32px;
}

.logoWrap img {
width: 200px;
height: 200px;
}

.circle {
width: 86px;
height: 86px;
}

.sub {
font-size: 14px;
padding: 0 24px;
}
}
17 changes: 13 additions & 4 deletions frontend/src/pages/curriculum/CurriculumPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,15 @@ const DAY_LABEL = { TUESDAY: '화요일', THURSDAY: '목요일', SATURDAY: '토
const STATUS_OPTIONS = ['BEFORE', 'ONGOING', 'AFTER'];
const STATUS_LABEL = { BEFORE: '세션 전', ONGOING: '세션 중', AFTER: '세션 후' };

// sessionDate(yyyy-mm-dd)에서 요일 계산
function getWeekDayFromDate(dateStr) {
if (!dateStr) return '';
const [year, month, day] = dateStr.split('-').map(Number);
const date = new Date(year, month - 1, day);
const map = { 2: '화요일', 4: '목요일', 6: '토요일' };
return map[date.getDay()] || '';
}

// ── 세션 정보 렌더 (공통) ─────────────────────────────
function SessionInfo({ session, isAdmin }) {
const icon = session.dayPart === 'AM' ? AmImg : PmImg;
Expand Down Expand Up @@ -47,7 +56,7 @@ function MemberSessionCard({ day }) {
const [isOpen, setIsOpen] = useState(false);
const amSession = day.sessions?.find(s => s.dayPart === 'AM');
const pmSession = day.sessions?.find(s => s.dayPart === 'PM');
const weekDay = DAY_LABEL[day.dayOfWeek] || '';
const weekDay = getWeekDayFromDate(day.sessionDate) || DAY_LABEL[day.dayOfWeek] || '';

return (
<div className={styles.sessionCard}>
Expand All @@ -56,7 +65,7 @@ function MemberSessionCard({ day }) {
<span className={styles.cardTitle}>{day.week}주차 {weekDay} 세션</span>
<span className={styles.cardDate}>{day.sessionDate}</span>
</div>
<img src={Toggle1} className={`${styles.toggleIcon} ${isOpen ? styles.toggleOpen : ''}`} alt="toggle" />
<img src={Toggle1} className={`${styles.toggleIcon} ${isOpen ? styles.toggleOpen : ''}`} alt="toggle" />
</div>
<hr className={styles.divider}/>

Expand All @@ -81,10 +90,10 @@ function MemberSessionCard({ day }) {

// ── 운영진용 세션 카드 ────────────────────────────────
function AdminSessionCard({ day, onEdit, onDelete }) {
const [isOpen, setIsOpen] = useState(true);
const [isOpen, setIsOpen] = useState(false);
const amSession = day.sessions?.find(s => s.dayPart === 'AM');
const pmSession = day.sessions?.find(s => s.dayPart === 'PM');
const weekDay = DAY_LABEL[day.dayOfWeek] || '';
const weekDay = getWeekDayFromDate(day.sessionDate) || DAY_LABEL[day.dayOfWeek] || '';

return (
<div className={styles.sessionCard}>
Expand Down
Loading
Loading