From 409695a34948c2cc7918c3030c524536bf3a5e08 Mon Sep 17 00:00:00 2001 From: plumbestie Date: Mon, 1 Jun 2026 23:23:11 +0900 Subject: [PATCH 1/6] =?UTF-8?q?[Feat]=20Header=20=EB=B0=98=EC=9D=91?= =?UTF-8?q?=ED=98=95=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/Header.js | 86 +++++++++-- frontend/src/components/Header.module.css | 165 +++++++++++++++++++++- 2 files changed, 234 insertions(+), 17 deletions(-) diff --git a/frontend/src/components/Header.js b/frontend/src/components/Header.js index d2c815c..ce10952 100644 --- a/frontend/src/components/Header.js +++ b/frontend/src/components/Header.js @@ -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 ( -
- PIROIN -
+ ); } diff --git a/frontend/src/components/Header.module.css b/frontend/src/components/Header.module.css index fb275f0..9b3c98e 100644 --- a/frontend/src/components/Header.module.css +++ b/frontend/src/components/Header.module.css @@ -10,7 +10,7 @@ --header-bg: var(--black); --header-color: var(--white); --logo-color: var(--main); - border-bottom: none; /* 추가 */ + border-bottom: none; } .header { @@ -21,7 +21,6 @@ align-items: center; padding: 0 80px; box-sizing: border-box; - position: relative; position: sticky; top: 0; z-index: 100; @@ -33,8 +32,10 @@ font-size: 2.8rem; font-weight: 800; text-decoration: none; + flex-shrink: 0; } +/* ── 데스크탑 nav ── */ .nav { display: flex; gap: 6rem; @@ -51,6 +52,7 @@ font-size: 1.4rem; font-weight: 500; text-decoration: none; + white-space: nowrap; } .nav a:hover { @@ -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; } \ No newline at end of file +.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; +} + \ No newline at end of file From 1e6b6df5b13c0af73b138a4c4e0c49563efe602b Mon Sep 17 00:00:00 2001 From: plumbestie Date: Mon, 1 Jun 2026 23:44:20 +0900 Subject: [PATCH 2/6] =?UTF-8?q?[Feat]=20Curriculum=20=EB=B0=98=EC=9D=91?= =?UTF-8?q?=ED=98=95=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../curriculum/CurriculumPage.module.css | 196 +++++++++++++----- 1 file changed, 147 insertions(+), 49 deletions(-) diff --git a/frontend/src/pages/curriculum/CurriculumPage.module.css b/frontend/src/pages/curriculum/CurriculumPage.module.css index c899b66..4b1f2a3 100644 --- a/frontend/src/pages/curriculum/CurriculumPage.module.css +++ b/frontend/src/pages/curriculum/CurriculumPage.module.css @@ -31,7 +31,7 @@ .weekTitle { font-family: var(--font-main); font-size: 1.6rem; - font-weight: 700; + font-weight: 650; color: var(--black); } @@ -63,7 +63,7 @@ gap: 20px; flex-wrap: wrap; align-items: flex-start; - justify-content: flex-start; + justify-content: flex-start; } /* 세션 카드 */ @@ -71,10 +71,15 @@ background: var(--white); border: 1px solid #eee; border-radius: 20px; - padding: 30px; + padding: 30px 30px 24px 30px; min-width: 200px; width: calc(28% - 14px); + height: 380px; + display: flex; + flex-direction: column; box-shadow: 0 1px 4px rgba(0,0,0,0.06); + box-sizing: border-box; + overflow: hidden; } .cardHeader { @@ -82,13 +87,14 @@ align-items: center; justify-content: space-between; cursor: pointer; - margin-bottom: 0; + margin-bottom: 0; } .cardHeaderLeft { display: flex; align-items: center; gap: 10px; + flex-wrap: wrap; } .cardTitle { @@ -114,12 +120,15 @@ display: flex; flex-direction: column; gap: 12px; + overflow-y: auto; + flex: 1; + padding-bottom: 4px; } .divider { - border: none; + border: none; border-top: 1px solid var(--gray200); - margin: 20px 0 20px 0; + margin: 20px 0; } /* 세션 정보 */ @@ -144,13 +153,17 @@ } .sessionTitleRow { - margin: 5px 0; + display: flex; + align-items: center; + gap: 8px; + margin: 5px 0; } .sessionIcon { width: 18px; height: 18px; object-fit: contain; + flex-shrink: 0; filter: brightness(0) saturate(100%) invert(44%) sepia(98%) saturate(500%) hue-rotate(90deg) brightness(95%) contrast(110%); } @@ -160,6 +173,11 @@ font-weight: 550; color: var(--black); padding: 5px 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + flex: 1; + min-width: 0; } .sessionHost { @@ -167,12 +185,16 @@ font-size: 0.9rem; color: var(--gray600); margin-left: auto; + flex-shrink: 0; + white-space: nowrap; } .sessionDetailRow { - display: flex; - justify-content: space-between; - padding: 3px 0; + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + padding: 3px 0 3px 26px; } .sessionLink { @@ -180,12 +202,16 @@ font-size: 0.9rem; color: var(--black); text-decoration: none; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + text-align: right; } .sessionLink:hover { - color: var(--dark); - transition: all ease-in-out 0.2s; - cursor: pointer; + color: var(--dark); + transition: all ease-in-out 0.2s; + cursor: pointer; } .sessionLinkName { @@ -206,6 +232,24 @@ color: var(--black); } +.sessionDetailLabel { + font-family: var(--font-main); + font-size: 0.9rem; + color: var(--black); + min-width: 50px; + flex-shrink: 0; +} + +.sessionDetailVal { + font-family: var(--font-main); + font-size: 0.9rem; + color: var(--black); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + text-align: right; +} + /* 과제 */ .assignmentRow { display: flex; @@ -214,6 +258,7 @@ gap: 8px; padding-top: 4px; margin-left: 22px; + flex-wrap: wrap; } .assignmentLabel { @@ -249,9 +294,9 @@ } .editBtn:hover { - background: var(--dark); - color: var(--white); - transition: all ease-in-out 0.2s; + background: var(--dark); + color: var(--white); + transition: all ease-in-out 0.2s; } .deleteBtn { @@ -266,9 +311,9 @@ } .deleteBtn:hover { - background: var(--dark); - color: var(--white); - transition: all ease-in-out 0.2s; + background: var(--dark); + color: var(--white); + transition: all ease-in-out 0.2s; } /* 세션 생성/수정 폼 */ @@ -289,9 +334,11 @@ border-radius: 16px; padding: 40px; width: 560px; + max-width: 100%; display: flex; flex-direction: column; gap: 16px; + box-sizing: border-box; } .formTitle { @@ -313,6 +360,7 @@ align-items: center; gap: 8px; margin-top: 8px; + flex-wrap: wrap; } .amLabel { @@ -333,6 +381,7 @@ display: flex; gap: 6px; margin-left: auto; + flex-wrap: wrap; } .statusBtn { @@ -347,9 +396,9 @@ } .statusBtn:hover { - background: var(--dark); - color: var(--white); - transition: all ease-in-out 0.2s; + background: var(--dark); + color: var(--white); + transition: all ease-in-out 0.2s; } .statusActive { @@ -412,10 +461,10 @@ text-align: center; } -/* 추가 스타일 */ .toggleIcon { width: 14px; height: 14px; + flex-shrink: 0; transition: transform 0.3s ease; filter: brightness(0) saturate(100%) invert(44%) sepia(60%) saturate(1693%) hue-rotate(89deg) brightness(107%) contrast(95%); } @@ -424,34 +473,83 @@ transform: rotate(180deg); } -.sessionTitleRow { - display: flex; - align-items: center; - gap: 8px; -} - -.sessionDetailRow { - display: flex; - align-items: center; - gap: 8px; - padding-left: 26px; -} - -.sessionDetailLabel { - font-family: var(--font-main); - font-size: 0.9rem; - color: var(--black); - min-width: 50px; -} - -.sessionDetailVal { - font-family: var(--font-main); - font-size: 0.9rem; - color: var(--black); -} - .formRow2 { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; +} + +/* ── 태블릿: 카드 2열 ── */ +@media (max-width: 1100px) { + .container { + padding: 32px 32px; + } + + .sessionCard { + width: calc(50% - 10px); + } +} + +/* ── 모바일: 카드 1열, 폼 풀스크린 ── */ +@media (max-width: 640px) { + .container { + padding: 24px 16px; + } + + .sessionCard { + width: 100%; + min-width: unset; + padding: 20px; + height: auto; + overflow: visible; + } + + .cardBody { + overflow-y: visible; + flex: none; + } + + .cardTitle { + font-size: 1.1rem; + } + + .cardDate { + margin-left: 0; + } + + /* 폼 */ + .formOverlay { + padding: 0; + align-items: flex-start; + } + + .formCard { + width: 100%; + min-height: 100vh; + border-radius: 0; + padding: 28px 20px; + } + + .formGrid { + grid-template-columns: 1fr; + } + + .formRow2 { + grid-template-columns: 1fr; + } + + .saveFormBtn { + width: 100%; + } + + .statusBtns { + margin-left: 0; + margin-top: 6px; + width: 100%; + } + + .formSectionTitle { + flex-direction: column; + align-items: flex-start; + } } \ No newline at end of file From abbae7839d64676e5e77bed4c75208744d88473d Mon Sep 17 00:00:00 2001 From: plumbestie Date: Mon, 1 Jun 2026 23:48:27 +0900 Subject: [PATCH 3/6] =?UTF-8?q?[Feat]=20Login=20=EB=B0=98=EC=9D=91?= =?UTF-8?q?=ED=98=95=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/pages/login/LoginPage.js | 17 ++++++++++------- frontend/src/pages/login/LoginPage.module.css | 17 +++++++++++++++++ 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/frontend/src/pages/login/LoginPage.js b/frontend/src/pages/login/LoginPage.js index 7fbd50d..6e7840d 100644 --- a/frontend/src/pages/login/LoginPage.js +++ b/frontend/src/pages/login/LoginPage.js @@ -1,6 +1,5 @@ import { useState, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; -import { authFetch } from '../../utils/Api'; import styles from './LoginPage.module.css'; function LoginPage() { @@ -8,12 +7,10 @@ function LoginPage() { const [focused, setFocused] = useState(''); const [form, setForm] = useState({ name: '', password: '' }); - const handleChange = (e) => { setForm({ ...form, [e.target.name]: e.target.value }); }; - const handleLogin = async () => { try { const response = await fetch('/api/auth/login', { @@ -27,9 +24,8 @@ function LoginPage() { localStorage.setItem('token', data.token); localStorage.setItem('role', data.role); localStorage.setItem('name', data.name); - navigate('/sessions'); // 로그인 성공 시 이동할 페이지 + navigate('/sessions'); } else { - const errData = await response.json(); alert('이름 또는 비밀번호가 올바르지 않습니다.'); } } catch (error) { @@ -37,13 +33,18 @@ function LoginPage() { } }; - useEffect(() => { + // 엔터키 로그인 + const handleKeyDown = (e) => { + if (e.key === 'Enter') handleLogin(); + }; + + useEffect(() => { document.title = "로그인 | PIROIN"; }, []); return (
-

PIROIN

+

navigate('/')}>PIROIN

setFocused('name')} onBlur={() => setFocused('')} @@ -61,6 +63,7 @@ function LoginPage() { placeholder="비밀번호" value={form.password} onChange={handleChange} + onKeyDown={handleKeyDown} className={`${styles.input} ${focused === 'pw' ? styles.inputFocused : ''}`} onFocus={() => setFocused('pw')} onBlur={() => setFocused('')} diff --git a/frontend/src/pages/login/LoginPage.module.css b/frontend/src/pages/login/LoginPage.module.css index 3a146cd..bf710c8 100644 --- a/frontend/src/pages/login/LoginPage.module.css +++ b/frontend/src/pages/login/LoginPage.module.css @@ -13,6 +13,8 @@ font-size: 56px; font-weight: 900; margin-bottom: 48px; + cursor: pointer; + text-decoration: none; } .form { @@ -30,6 +32,7 @@ font-size: 16px; outline: none; transition: border 0.2s; + box-sizing: border-box; } .inputFocused { @@ -51,4 +54,18 @@ .button:hover { background-color: var(--main); transition: all 0.2s ease; +} + +/* ── 모바일 ── */ +@media (max-width: 480px) { + .title { + font-size: 40px; + margin-bottom: 32px; + } + + .form { + width: 100%; + padding: 0 24px; + box-sizing: border-box; + } } \ No newline at end of file From 923701ae15be650c4f49e25808429356c149f4cb Mon Sep 17 00:00:00 2001 From: plumbestie Date: Mon, 1 Jun 2026 23:51:55 +0900 Subject: [PATCH 4/6] =?UTF-8?q?[Feat]=20Onboarding=20=EB=B0=98=EC=9D=91?= =?UTF-8?q?=ED=98=95=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/pages/OnboardingPage.module.css | 30 ++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/frontend/src/pages/OnboardingPage.module.css b/frontend/src/pages/OnboardingPage.module.css index 890b008..f25dab2 100644 --- a/frontend/src/pages/OnboardingPage.module.css +++ b/frontend/src/pages/OnboardingPage.module.css @@ -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; + } } \ No newline at end of file From b73821f8607a3e99e4167d65405eef144cdf5505 Mon Sep 17 00:00:00 2001 From: plumbestie Date: Tue, 2 Jun 2026 01:04:06 +0900 Subject: [PATCH 5/6] =?UTF-8?q?[Feat]=20PiroCheck(=EB=A9=94=EC=9D=B8,=20?= =?UTF-8?q?=EC=B6=9C=EC=84=9D,=20=EA=B3=BC=EC=A0=9C)=20=EB=B0=98=EC=9D=91?= =?UTF-8?q?=ED=98=95=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/pages/curriculum/CurriculumPage.js | 17 +- .../curriculum/CurriculumPage.module.css | 3 +- .../pages/pirocheck/PIroCheckMain.module.css | 11 +- .../pages/pirocheck/assignment/Assignment.js | 37 +++-- .../assignment/Assignment.module.css | 156 ++++++++++++++++-- .../pages/pirocheck/attendance/Attendance.js | 74 ++++++--- .../attendance/Attendance.module.css | 60 ++++++- 7 files changed, 292 insertions(+), 66 deletions(-) diff --git a/frontend/src/pages/curriculum/CurriculumPage.js b/frontend/src/pages/curriculum/CurriculumPage.js index d369939..e65f1ce 100644 --- a/frontend/src/pages/curriculum/CurriculumPage.js +++ b/frontend/src/pages/curriculum/CurriculumPage.js @@ -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; @@ -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 (
@@ -56,7 +65,7 @@ function MemberSessionCard({ day }) { {day.week}주차 {weekDay} 세션 {day.sessionDate}
- toggle + toggle

@@ -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 (
diff --git a/frontend/src/pages/curriculum/CurriculumPage.module.css b/frontend/src/pages/curriculum/CurriculumPage.module.css index 4b1f2a3..57633ed 100644 --- a/frontend/src/pages/curriculum/CurriculumPage.module.css +++ b/frontend/src/pages/curriculum/CurriculumPage.module.css @@ -74,12 +74,13 @@ padding: 30px 30px 24px 30px; min-width: 200px; width: calc(28% - 14px); - height: 380px; + max-height: 380px; display: flex; flex-direction: column; box-shadow: 0 1px 4px rgba(0,0,0,0.06); box-sizing: border-box; overflow: hidden; + transition: max-height 0.3s ease; } .cardHeader { diff --git a/frontend/src/pages/pirocheck/PIroCheckMain.module.css b/frontend/src/pages/pirocheck/PIroCheckMain.module.css index 9a0663a..856186b 100644 --- a/frontend/src/pages/pirocheck/PIroCheckMain.module.css +++ b/frontend/src/pages/pirocheck/PIroCheckMain.module.css @@ -4,7 +4,7 @@ align-items: center; justify-content: center; gap: 20px; - height: calc(100vh - 100px); + height: calc(100vh - 100px); background: var(--black); } @@ -25,4 +25,13 @@ .menuBtn:hover { background: var(--dark); color: var(--white); +} + +/* ── 모바일 ── */ +@media (max-width: 480px) { + .menuBtn { + width: calc(100% - 48px); + font-size: 1.5rem; + padding: 20px 0; + } } \ No newline at end of file diff --git a/frontend/src/pages/pirocheck/assignment/Assignment.js b/frontend/src/pages/pirocheck/assignment/Assignment.js index 81a0f13..27786c7 100644 --- a/frontend/src/pages/pirocheck/assignment/Assignment.js +++ b/frontend/src/pages/pirocheck/assignment/Assignment.js @@ -56,13 +56,14 @@ function AssignmentModal({ item, onClose, onSave }) { headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title: form.title, week: form.week, day: form.day }), }); - onSave(); + onSave(form.title); onClose(); }; return (
+ logo
ASSIGNMENT
@@ -113,21 +114,21 @@ function WeekBlock({ weekData, role, onEdit, onDelete }) { {dayMap[session.day]} {session.sessionDate && {session.sessionDate}}
- {role === 'ADMIN' && ( -
- - -
- )}
{session.items.map((item, k) => (
{item.title} {role === 'MEMBER' && } + {role === 'ADMIN' && ( +
+ + +
+ )}
))} {j < grouped.length - 1 &&
} @@ -156,6 +157,18 @@ function Assignment() { setWeeks(results); }; + // 수정 시 로컬 state만 업데이트 (순서 유지) + const handleEditSave = (updatedItem) => { + setWeeks(prev => prev.map(w => ({ + ...w, + assignments: w.assignments.map(a => + a.assignmentId === updatedItem.assignmentId + ? { ...a, ...updatedItem } + : a + ) + }))); + }; + useEffect(() => { fetchAll(); }, []); const handleDelete = async (assignmentId) => { @@ -186,7 +199,7 @@ function Assignment() { setModalItem(undefined)} - onSave={fetchAll} + onSave={modalItem ? (updated) => handleEditSave({ ...modalItem, title: updated }) : fetchAll} /> )}
diff --git a/frontend/src/pages/pirocheck/assignment/Assignment.module.css b/frontend/src/pages/pirocheck/assignment/Assignment.module.css index 5ca20b9..8621bcf 100644 --- a/frontend/src/pages/pirocheck/assignment/Assignment.module.css +++ b/frontend/src/pages/pirocheck/assignment/Assignment.module.css @@ -36,6 +36,7 @@ border-radius: 16px; margin-bottom: 20px; overflow: hidden; + box-sizing: border-box; } .weekHeader { @@ -153,6 +154,7 @@ .statusIcon { width: 20px; height: 20px; + flex-shrink: 0; } .divider { @@ -195,25 +197,44 @@ } .modal { + position: relative; background: #3a3a3a; border-radius: 20px; padding: 40px 60px; - width: 420px; + width: clamp(360px, 40vw, 520px); display: flex; flex-direction: column; align-items: center; - gap: 16px; + gap: 16px; + box-sizing: border-box; } +/* X 닫기 버튼 */ +.modalCloseBtn { + position: absolute; + top: 16px; + right: 20px; + background: transparent; + border: none; + color: #aaa; + font-size: 1.4rem; + cursor: pointer; + line-height: 1; + padding: 4px; + transition: color 0.2s; +} + +.modalCloseBtn:hover { color: var(--white); } + .modalLogo { - width: 200px; - height: 200px; + width: 160px; + height: 160px; object-fit: contain; } .modalTitle { - font-family: var(--font-main); - font-size: 3rem; + font-family: var(--font-title); + font-size: 2.6rem; font-weight: 800; color: var(--main); letter-spacing: 0; @@ -223,36 +244,33 @@ display: flex; align-items: center; gap: 12px; - width: 85%; + width: 100%; margin-top: 10px; } .select { - padding: 10px 36px 10px 20px; + padding: 10px 36px 10px 20px; background-color: var(--pale); - -webkit-appearance: none; -moz-appearance: none; appearance: none; - background-image: url("data:image/svg+xml;utf8,"); background-repeat: no-repeat; background-position: right 16px center; - border: none; border-radius: 8px; font-family: var(--font-main); font-size: 1rem; cursor: pointer; flex: 1; + min-width: 0; } -.select::-ms-expand { - display: none; -} +.select::-ms-expand { display: none; } + .modalInput { - width: 85%; - padding: 12px 20px; + width: 100%; + padding: 12px 20px; background: var(--pale); border: none; border-radius: 8px; @@ -265,6 +283,7 @@ color: var(--white); font-family: var(--font-main); font-size: 1.2rem; + white-space: nowrap; } .saveBtn { @@ -284,4 +303,109 @@ .saveBtn:hover { background: var(--main); color: var(--black); +} + +/* ── 모바일 ── */ +@media (max-width: 640px) { + .container { + padding: 40px 24px; + } + + .title { + font-size: 3rem; + margin-bottom: 28px; + text-align: center; + word-break: keep-all; + line-height: 1.2; + } + + .mockBanner { + width: 100%; + } + + .weekBlock { + width: 100%; + } + + .weekHeader { + padding: 20px; + } + + .weekBody { + padding: 0 20px 20px; + } + + /* 모달 */ + .modalOverlay { + align-items: center; + padding: 24px; + box-sizing: border-box; + } + + .modal { + width: 100%; + max-width: 100%; + border-radius: 20px; + padding: 40px 24px 40px; + } + + .modalLogo { + width: 120px; + height: 120px; + } + + .modalTitle { + font-size: 2rem; + } + + .modalRow { + width: 100%; + } + + .modalInput { + width: 100%; + } + + .addBtn { + bottom: 24px; + right: 20px; + } + + .assignmentTitle { + font-size: 0.9rem; + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + padding-right: 8px; + } + + .sessionTitle { + font-size: 0.85rem; + } + + .dayLabel { + font-size: 1.1rem; + } +} +/* ── 태블릿 ── */ +@media (min-width: 641px) and (max-width: 1024px) { + .container { + padding: 48px 32px; + } + + .weekBlock { + width: 100%; + max-width: 720px; + } + + .mockBanner { + width: 100%; + max-width: 720px; + } + + .modal { + width: clamp(400px, 70vw, 560px); + } } \ No newline at end of file diff --git a/frontend/src/pages/pirocheck/attendance/Attendance.js b/frontend/src/pages/pirocheck/attendance/Attendance.js index fe8f350..032bbba 100644 --- a/frontend/src/pages/pirocheck/attendance/Attendance.js +++ b/frontend/src/pages/pirocheck/attendance/Attendance.js @@ -15,9 +15,19 @@ function cloverForSlot(status) { return 미정; } -function historyIcon(slots) { +// 세션 1회 출석 결과 → 코인/화남 아이콘 +// status: true(출석성공) / false(결석) / null(미정) +// successCount: 해당 세션에서 출석 성공한 횟수 (AM/PM 등 슬롯 수) +function sessionIcon(slot) { + if (slot.status === true) return 출석; + if (slot.status === false) return 결석; + return 미정; +} + +// 주차 전체 슬롯(3개) → 코인 합산 +function weekCoinIcon(slots) { const successCount = slots.filter(s => s.status === true).length; - if (successCount === 3) return 3회 출석; + if (successCount >= 3) return 3회 출석; if (successCount === 2) return 2회 출석; if (successCount === 1) return 1회 출석; return 결석; @@ -90,29 +100,34 @@ function MemberView() { const [history, setHistory] = useState([]); useEffect(() => { - // 1~5주차 기본값 세팅 - const defaultHistory = [1, 2, 3, 4, 5].map(week => ({ - week, - slots: [ - { status: false }, - { status: false }, - { status: false }, - ] - })); - - authFetch('/api/attendance/user') - .then(r => r.json()) - .then(data => { - const apiData = data.data || []; - // API 데이터로 해당 주차 덮어씌우기 - const merged = defaultHistory.map(def => { - const found = apiData.find(d => d.week === def.week); - return found || def; - }); - setHistory(merged); - }) - .catch(() => setHistory(defaultHistory)); -}, []); + const defaultHistory = [1, 2, 3, 4, 5].map(week => ({ + week, + slots: [ + { status: null }, + { status: null }, + { status: null }, + ] + })); + + // 오늘 출석 현황 초기 fetch (새로고침 시 유지) + const today = new Date().toISOString().split('T')[0]; + authFetch(`/api/attendance/user/date?date=${today}`) + .then(r => r.json()) + .then(d => setTodaySlots(d.data || [])) + .catch(() => {}); + + authFetch('/api/attendance/user') + .then(r => r.json()) + .then(data => { + const apiData = data.data || []; + const merged = defaultHistory.map(def => { + const found = apiData.find(d => d.week === def.week); + return found || def; + }); + setHistory(merged); + }) + .catch(() => setHistory(defaultHistory)); + }, []); const handleSubmit = async () => { if (!inputCode.trim()) return; @@ -133,7 +148,7 @@ function MemberView() { } else if (result.statusCode === 'INVALID_CODE') { setMessage('출석 코드를 확인해주세요.'); } else { - setMessage(result.message); + setMessage(result.message); } setInputCode(''); @@ -159,18 +174,23 @@ function MemberView() { {message &&
{message}
} + {/* 오늘 출석 현황 클로버 */}
{displaySlots.map((slot, i) => (
{cloverForSlot(slot.status)}
))}
+ {/* 주차별 출석 히스토리 */}
{history.map((row, i) => (
{row.week}주차
- {historyIcon(row.slots)} + {/* 슬롯 3개 각각 코인/화남으로 표시 */} + {row.slots.map((slot, j) => ( +
{sessionIcon(slot)}
+ ))}
))} diff --git a/frontend/src/pages/pirocheck/attendance/Attendance.module.css b/frontend/src/pages/pirocheck/attendance/Attendance.module.css index 7e72e9a..6f542a0 100644 --- a/frontend/src/pages/pirocheck/attendance/Attendance.module.css +++ b/frontend/src/pages/pirocheck/attendance/Attendance.module.css @@ -3,7 +3,7 @@ flex-direction: column; align-items: center; padding: 60px 20px; - min-height: calc(80vh - 100px); + min-height: calc(80vh - 100px); background: var(--black); justify-content: center; } @@ -14,7 +14,7 @@ font-weight: 800; color: var(--main); margin-bottom: 50px; - letter-spacing: 0.05em; + text-align: center; } /* ── ADMIN ── */ @@ -60,7 +60,7 @@ transition: opacity 0.2s; } -.createBtn:hover { background: var(--main); transition: all ease-in-out 0.2s;} +.createBtn:hover { background: var(--main); transition: all ease-in-out 0.2s; } .manageLink { color: var(--white); @@ -78,11 +78,13 @@ display: flex; align-items: center; background: var(--gray600); - border-radius: 10px; + border-radius: 10px; overflow: hidden; width: 480px; + max-width: calc(100vw - 56px); margin-bottom: 16px; padding: 3px 3px 3px 20px; + box-sizing: border-box; } .codeInput { @@ -101,7 +103,7 @@ padding: 10px 20px; background: var(--dark); border: none; - border-radius: 10px; + border-radius: 10px; color: var(--white); font-family: var(--font-main); font-size: 1rem; @@ -164,4 +166,52 @@ display: flex; flex-direction: column; gap: 16px; +} + +/* ── 모바일 ── */ +@media (max-width: 480px) { + .container { + padding: 48px 28px; + } + + .title { + font-size: 2.8rem; + margin-bottom: 36px; + word-break: keep-all; + line-height: 1.2; + } + + /* ADMIN */ + .code { + width: 68px; + height: 84px; + font-size: 2.2rem; + } + + .codebox { + gap: 10px; + } + + .createBtn { + width: calc(100vw - 56px); + max-width: 320px; + } + + /* MEMBER */ + .cloverSvg { + width: 63px; + height: 63px; + } + + .cloverRow { + gap: 28px; + margin-top: 28px; + margin-bottom: 28px; + } + + .historyBox { + width: calc(100vw - 56px); + padding: 24px 28px; + box-sizing: border-box; + } } \ No newline at end of file From f9ae938c240519cab851c542495f1d2795e2dcbc Mon Sep 17 00:00:00 2001 From: plumbestie Date: Tue, 2 Jun 2026 23:54:42 +0900 Subject: [PATCH 6/6] =?UTF-8?q?[Feat]=20PiroCheck(=EB=B3=B4=EC=A6=9D?= =?UTF-8?q?=EA=B8=88,=20StudentDetail&List)=20=EB=B0=98=EC=9D=91=ED=98=95?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pirocheck/deposit/Deposit.module.css | 49 +++++-- .../pages/pirocheck/students/StudentDetail.js | 4 +- .../students/StudentDetail.module.css | 125 +++++++++++++----- .../pirocheck/students/StudentList.module.css | 38 ++++-- 4 files changed, 152 insertions(+), 64 deletions(-) diff --git a/frontend/src/pages/pirocheck/deposit/Deposit.module.css b/frontend/src/pages/pirocheck/deposit/Deposit.module.css index 298bf8b..d941723 100644 --- a/frontend/src/pages/pirocheck/deposit/Deposit.module.css +++ b/frontend/src/pages/pirocheck/deposit/Deposit.module.css @@ -7,19 +7,6 @@ background: var(--black); } -.mockBanner { - background: #5a3e00; - color: #ffd166; - font-family: var(--font-main); - font-size: 0.85rem; - padding: 10px 20px; - border-radius: 8px; - margin-bottom: 20px; - text-align: center; - width: 400px; - box-sizing: border-box; -} - .title { font-family: var(--font-title); font-size: 3rem; @@ -32,6 +19,7 @@ /* 잔여 보증금 박스 */ .amountBox { width: 400px; + max-width: calc(100vw - 56px); background: #e8f5e9; border-radius: 16px; padding: 36px 20px; @@ -40,6 +28,7 @@ align-items: center; gap: 12px; margin-bottom: 24px; + box-sizing: border-box; } .amountLabel { @@ -59,9 +48,11 @@ /* 차감 목록 */ .itemList { width: 400px; + max-width: calc(100vw - 56px); display: flex; flex-direction: column; gap: 12px; + box-sizing: border-box; } .item { @@ -85,4 +76,36 @@ font-size: 1.1rem; font-weight: 600; color: var(--white); +} + +/* ── 모바일 ── */ +@media (max-width: 480px) { + .container { + padding: 48px 28px; + margin: 0 30px; + } + + .title { + font-size: 2.8rem; + margin-bottom: 28px; + word-break: keep-all; + line-height: 1.2; + text-align: center; + } + + .amountLabel { + font-size: 1.2rem; + } + + .amountValue { + font-size: 2rem; + } + + .item { + padding: 16px 20px; + } + + .itemLabel, .itemValue { + font-size: 1rem; + } } \ No newline at end of file diff --git a/frontend/src/pages/pirocheck/students/StudentDetail.js b/frontend/src/pages/pirocheck/students/StudentDetail.js index 37eb34a..d3e5c17 100644 --- a/frontend/src/pages/pirocheck/students/StudentDetail.js +++ b/frontend/src/pages/pirocheck/students/StudentDetail.js @@ -37,10 +37,10 @@ function WeekBlock({ weekData, onChange }) { {isOpen && (
- {weekData.days.length === 0 && ( + {(!weekData.days || weekData.days.length === 0) && (
데이터가 없습니다.
)} - {weekData.days.map((day, i) => ( + {(weekData.days || []).map((day, i) => (
toggleDay(day.day)}>
diff --git a/frontend/src/pages/pirocheck/students/StudentDetail.module.css b/frontend/src/pages/pirocheck/students/StudentDetail.module.css index e073df1..0d15987 100644 --- a/frontend/src/pages/pirocheck/students/StudentDetail.module.css +++ b/frontend/src/pages/pirocheck/students/StudentDetail.module.css @@ -7,27 +7,16 @@ background: var(--black); } -.mockBanner { - background: #5a3e00; - color: #ffd166; - font-family: var(--font-main); - font-size: 0.85rem; - padding: 10px 20px; - border-radius: 8px; - margin-bottom: 20px; - width: 480px; - box-sizing: border-box; - text-align: center; -} - .card { width: 480px; + max-width: calc(100vw - 56px); background: var(--gray600); border-radius: 20px; - padding: 40px 40px; + padding: 40px; display: flex; flex-direction: column; gap: 20px; + box-sizing: border-box; } /* 프로필 */ @@ -66,6 +55,7 @@ display: flex; flex-direction: column; gap: 8px; + min-width: 0; } .depositBoxGray { @@ -76,6 +66,7 @@ display: flex; flex-direction: column; gap: 8px; + min-width: 0; } .depositLabel { @@ -99,7 +90,8 @@ } .defenceInput { - width: 80px; + flex: 1; + min-width: 0; border: none; background: transparent; font-family: var(--font-main); @@ -126,7 +118,7 @@ font-size: 0.75rem; font-weight: 550; cursor: pointer; - margin-left: 30px; + flex-shrink: 0; } .saveBtn:hover { background: var(--gray600); color: var(--white); transition: all ease-in-out 0.2s; } @@ -167,7 +159,6 @@ margin-left: 5px; } -/* 토글 애니메이션 */ .toggleIcon { width: 15px; height: 15px; @@ -200,7 +191,6 @@ } /* 요일 블록 */ - .dayHeader { display: flex; align-items: center; @@ -236,7 +226,6 @@ gap: 8px; } -/* 출석/과제 그룹 - 레이블 옆에 아이템들 */ .statusGroup { display: flex; align-items: flex-start; @@ -274,34 +263,29 @@ } .select { - padding: 3px 28px 3px 12px; - width: 80px; + padding: 3px 28px 3px 12px; + width: 80px; box-sizing: border-box; - background: var(--gray600); border: 1px solid var(--white); border-radius: 6px; color: var(--white); font-family: var(--font-main); font-size: 0.85rem; - cursor: pointer; - + cursor: pointer; -webkit-appearance: none; -moz-appearance: none; appearance: none; - background-image: url("data:image/svg+xml;utf8,"); background-repeat: no-repeat; - - background-position: right 10px center; + background-position: right 10px center; + flex-shrink: 0; } -.select::-ms-expand { - display: none; -} +.select::-ms-expand { display: none; } .saveWeekBtn { - margin: 20px 0; + margin: 20px auto; padding: 5px 30px; background: transparent; border: 1.5px solid var(--main); @@ -312,8 +296,6 @@ font-weight: 600; cursor: pointer; display: block; - margin-left: auto; - margin-right: auto; } .saveWeekBtn:hover { background: var(--main); color: var(--white); transition: all ease-in-out 0.2s; } @@ -324,7 +306,6 @@ margin: 10px 0; } -/* 전체 저장 */ .saveAllBtn { width: 60%; margin: 0 auto; @@ -340,4 +321,78 @@ transition: opacity 0.2s; } -.saveAllBtn:hover { background: var(--main); transition: all ease-in-out 0.2s; } \ No newline at end of file +.saveAllBtn:hover { background: var(--main); transition: all ease-in-out 0.2s; } + +/* ── 태블릿 ── */ +@media (min-width: 481px) and (max-width: 1024px) { + .depositEditRow { + flex-wrap: wrap; + } + + .saveBtn { + margin-left: 0; + align-self: flex-start; + } +} + +/* ── 모바일 ── */ +@media (max-width: 480px) { + .container { + padding: 48px 28px; + } + + .card { + padding: 28px 20px; + } + + .profileName { + font-size: 1.5rem; + } + + .depositLabel { + font-size: 0.9rem; + } + + .depositValue { + font-size: 1rem; + } + + .defenceInput { + font-size: 1rem; + } + + .won { + font-size: 1rem; + } + + .depositEditRow { + flex-wrap: wrap; + } + + .saveBtn { + width: 100%; + margin-left: 0; + font-size: 0.7rem; + padding: 6px 8px; + text-align: center; + } + + .toggleIcon { + width: 12px; + height: 12px; + } + + .toggleIcon2 { + width: 16px; + height: 16px; + } + + .weekBody { + margin-left: 24px; + margin-right: 12px; + } + + .saveAllBtn { + width: 100%; + } +} \ No newline at end of file diff --git a/frontend/src/pages/pirocheck/students/StudentList.module.css b/frontend/src/pages/pirocheck/students/StudentList.module.css index 4eea6e6..4c17539 100644 --- a/frontend/src/pages/pirocheck/students/StudentList.module.css +++ b/frontend/src/pages/pirocheck/students/StudentList.module.css @@ -7,19 +7,6 @@ background: var(--black); } -.mockBanner { - background: #5a3e00; - color: #ffd166; - font-family: var(--font-main); - font-size: 0.85rem; - padding: 10px 20px; - border-radius: 8px; - margin-bottom: 20px; - width: 480px; - box-sizing: border-box; - text-align: center; -} - .title { font-family: var(--font-title); font-size: 3rem; @@ -36,8 +23,10 @@ border-radius: 10px; overflow: hidden; width: 480px; + max-width: calc(100vw - 56px); margin-bottom: 30px; padding: 6px 6px 6px 20px; + box-sizing: border-box; } .searchInput { @@ -62,6 +51,7 @@ font-size: 0.95rem; font-weight: 600; cursor: pointer; + white-space: nowrap; } .searchBtn:hover { @@ -71,9 +61,11 @@ .list { width: 450px; + max-width: calc(100vw - 56px); display: flex; flex-direction: column; gap: 12px; + box-sizing: border-box; } .studentItem { @@ -87,6 +79,7 @@ cursor: pointer; transition: background 0.2s; width: 100%; + box-sizing: border-box; } .studentItem:hover { background: #4a4a4a; } @@ -99,5 +92,22 @@ } .arrow { - width: 25px; height: 25px; + width: 25px; + height: 25px; + flex-shrink: 0; +} + +/* ── 모바일 ── */ +@media (max-width: 480px) { + .container { + padding: 48px 28px; + } + + .title { + font-size: 2.4rem; + margin-bottom: 28px; + text-align: center; + word-break: keep-all; + line-height: 1.2; + } } \ No newline at end of file