diff --git a/src/Layout/PageLayout.jsx b/src/Layout/PageLayout.jsx index e96102f..c090cd4 100644 --- a/src/Layout/PageLayout.jsx +++ b/src/Layout/PageLayout.jsx @@ -7,10 +7,16 @@ const Container = styled.div` width: 100%; `; +const SideBarLayout = styled.div` + @media (max-width: 480px) { + display: none; + } +`; + export default function PageLayout({ sideBar, children }) { return ( - {sideBar} + {sideBar} {children} ); diff --git a/src/components/Modal/Modal.jsx b/src/components/Modal/Modal.jsx index 988cbad..cebbe7c 100644 --- a/src/components/Modal/Modal.jsx +++ b/src/components/Modal/Modal.jsx @@ -1,18 +1,19 @@ import PropTypes from 'prop-types'; import * as S from './Modal.style'; import { useNavigate } from 'react-router-dom'; -import { useState } from 'react'; import Portal from '../Portal/Portal'; -const Modal = ({ name, major, studentId, isOpen, onClose }) => { +const Modal = ({ isOpen, onClose, attendees }) => { const navigate = useNavigate(); - const handleCompletedButtonClick = async () => { + const handlePersonClick = (studentInfo) => { onClose(); - navigate('/attendance/sign'); + navigate('/attendance/sign', { + state: { studentInfo }, + }); }; - const handleCancelButtonClick = async () => { + const handleCancelButtonClick = () => { onClose(); navigate('/attendance/student-id'); }; @@ -24,26 +25,19 @@ const Modal = ({ name, major, studentId, isOpen, onClose }) => { return ( - - {name}님이 맞으십니까? - + 출석 체크 할 사람을 선택해주세요. - - 학과 - {major} - - - 학번 - {studentId} - + {attendees.map((attendee, index) => ( + handlePersonClick(attendee)}> + {attendee.name} + {attendee.major} + + ))} - 아니요 + 이전페이지로 돌아가기. - - 네, 서명하러 하기 - @@ -51,11 +45,15 @@ const Modal = ({ name, major, studentId, isOpen, onClose }) => { }; Modal.propTypes = { - name: PropTypes.string.isRequired, - major: PropTypes.string.isRequired, - studentId: PropTypes.string.isRequired, isOpen: PropTypes.bool.isRequired, onClose: PropTypes.func.isRequired, + attendees: PropTypes.arrayOf( + PropTypes.shape({ + name: PropTypes.string.isRequired, + major: PropTypes.string.isRequired, + studentId: PropTypes.string.isRequired, + }), + ).isRequired, }; export default Modal; diff --git a/src/components/Navigator/Navigator.jsx b/src/components/Navigator/Navigator.jsx index 81612bf..62c99dc 100644 --- a/src/components/Navigator/Navigator.jsx +++ b/src/components/Navigator/Navigator.jsx @@ -15,6 +15,14 @@ export default function Navigator() { const [isSidebarOpen, setIsSidebarOpen] = useState(false); const EVENT_ID = useRecoilValue(eventIDState); + const handleDimClick = () => { + setIsSidebarOpen(false); + }; + + const toggleSidebar = () => { + setIsSidebarOpen(!isSidebarOpen); + }; + useEffect(() => { console.log('USER_ID:', USER_ID); console.log('EVENT_ID:', EVENT_ID); @@ -55,9 +63,19 @@ export default function Navigator() { }; }, []); - const toggleSidebar = () => { - setIsSidebarOpen(!isSidebarOpen); - }; + useEffect(() => { + const handleScroll = () => { + if (window.scrollY > 0) { + setIsSidebarOpen(false); + } + }; + + window.addEventListener('scroll', handleScroll); + + return () => { + window.removeEventListener('scroll', handleScroll); + }; + }, []); return ( <> @@ -93,6 +111,7 @@ export default function Navigator() { + {isSidebarOpen && } ); diff --git a/src/components/Navigator/Navigator.style.jsx b/src/components/Navigator/Navigator.style.jsx index 9baf268..6bb9889 100644 --- a/src/components/Navigator/Navigator.style.jsx +++ b/src/components/Navigator/Navigator.style.jsx @@ -175,3 +175,14 @@ export const Sidebar = styled.div` transform: ${({ isOpen }) => (isOpen ? 'translateX(0)' : 'translateX(100%)')}; transition: transform 0.3s ease-in-out; `; + +export const Dim = styled.div` + opacity: 0.2; + position: fixed; + top: 55px; + z-index: 1; + left: 0; + width: 100%; + height: 100%; + background-color: black; +`; diff --git a/src/components/Navigator/Sidebar.jsx b/src/components/Navigator/Sidebar.jsx index 1c44415..8c49d11 100644 --- a/src/components/Navigator/Sidebar.jsx +++ b/src/components/Navigator/Sidebar.jsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import * as S from './Sidebar.style'; import { FaTableList, @@ -7,8 +7,13 @@ import { FaUsers, FaChartPie, } from 'react-icons/fa6'; -import { useLocation } from 'react-router-dom'; +import { USER_ID } from '../../constants'; +import { useLocation, useNavigate } from 'react-router-dom'; +import { useRecoilValue } from 'recoil'; +import { eventIDState } from '../../recoil/atoms/state'; +import { axiosInstance } from '../../axios'; +// 개별 메뉴 const menuItems = [ { to: '/event/dashboard', icon: , text: '대시보드' }, { @@ -40,8 +45,73 @@ function MenuItem({ to, icon, text, isActive }) { ); } +// 사이드바 전체 export default function Sidebar() { + const [parsedEvents, setParsedEvents] = useState(null); + const [isAttendanceButtonActive, setIsAttendanceButtonActive] = + useState(false); const location = useLocation(); + const EVENT_ID = useRecoilValue(eventIDState); + + useEffect(() => { + console.log('USER_ID:', USER_ID); + console.log('EVENT_ID:', EVENT_ID); + const fetchData = async () => { + try { + const response = await axiosInstance.get( + `/api/v1/events/${USER_ID}/${EVENT_ID}`, + ); + const eventData = response.data; + if (eventData) { + const now = new Date(); + + const schedules = eventData.eventSchedules.map((schedule) => ({ + date: schedule.eventDate, + startTime: schedule.eventStartTime, + endTime: schedule.eventEndTime, + attendanceList: schedule.attendanceListResponseDtos, + })); + + const firstSchedule = schedules[0]; + const lastSchedule = schedules[schedules.length - 1]; + + const firstScheduleStartDateTime = new Date( + `${firstSchedule.date}T${firstSchedule.startTime}`, + ); + const lastScheduleEndDateTime = new Date( + `${lastSchedule.date}T${lastSchedule.endTime}`, + ); + + // 현재 시간이 행사 기간 내에 있는지 확인 + if ( + now >= firstScheduleStartDateTime && + now <= lastScheduleEndDateTime + ) { + setIsAttendanceButtonActive(true); + } else { + setIsAttendanceButtonActive(false); + } + + const parsedEvent = { + title: eventData.eventTitle, + detail: eventData.eventDetail, + image: eventData.eventImage, + schedules, + totalSessions: eventData.eventSchedules.length, + totalParticipants: schedules[0].attendanceList.length, + eventType: eventData.eventType, + eventTarget: eventData.eventTarget, + }; + + setParsedEvents(parsedEvent); + } + } catch (error) { + console.error('이벤트 데이터를 가져오는 중 오류:', error); + } + }; + + fetchData(); + }, [EVENT_ID]); return ( @@ -76,8 +146,10 @@ export default function Sidebar() { - - + + 출석화면으로 이동 diff --git a/src/components/Navigator/Sidebar.style.jsx b/src/components/Navigator/Sidebar.style.jsx index 9d4b92b..1e81525 100644 --- a/src/components/Navigator/Sidebar.style.jsx +++ b/src/components/Navigator/Sidebar.style.jsx @@ -78,8 +78,15 @@ export const AttendanceBtn = styled.button` line-height: 100%; letter-spacing: -0.01em; color: #ffffff; + cursor: pointer; + transition: background 0.3s ease; &:hover { background: #2b90fc; } + + &:disabled { + background: #cccccc; + cursor: not-allowed; + } `; diff --git a/src/pages/AttendancePage/AttendanceSignPage.jsx b/src/pages/AttendancePage/AttendanceSignPage.jsx index a2905d4..989f818 100644 --- a/src/pages/AttendancePage/AttendanceSignPage.jsx +++ b/src/pages/AttendancePage/AttendanceSignPage.jsx @@ -21,6 +21,7 @@ const AttendanceSignPage = ({ name, major, studentId }) => { const [isSigned, setIsSigned] = useState(false); const [isOpen, setIsOpen] = useState(false); const [eventTitle, setEventTitle] = useState(''); + const [eventTarget, setEventTarget] = useState('INTERNAL'); const signatureRef = useRef(null); const { getSessionStorage } = useSessionStorages(); @@ -70,17 +71,19 @@ const AttendanceSignPage = ({ name, major, studentId }) => { }; useEffect(() => { - const fetchEventTitle = async () => { + const fetchEventDetails = async () => { try { const response = await axiosInstance.get( `/api/v1/events/${USER_ID}/${EVENT_ID}`, ); - setEventTitle(response.data.eventTitle); + const eventData = response.data; + setEventTitle(eventData.eventTitle); + setEventTarget(eventData.eventTarget); } catch (error) { console.error('이벤트 타이틀 에러', error); } }; - fetchEventTitle(); + fetchEventDetails(); }, []); return ( @@ -91,13 +94,15 @@ const AttendanceSignPage = ({ name, major, studentId }) => { - 학과 + 소속 {studentInfo.major} - - 학번 - {studentInfo.number} - + {eventTarget === 'INTERNAL' && ( + + 학번 + {studentInfo.number} + + )} {/* 서명 */} diff --git a/src/pages/AttendancePage/AttendanceSignPage.style.jsx b/src/pages/AttendancePage/AttendanceSignPage.style.jsx index 8c27831..ba6a4eb 100644 --- a/src/pages/AttendancePage/AttendanceSignPage.style.jsx +++ b/src/pages/AttendancePage/AttendanceSignPage.style.jsx @@ -36,7 +36,7 @@ export const ContentContainer = styled.div` width: 100%; max-width: 900px; gap: 16px; - padding: 20px 0; + padding-bottom: 20px; @media (max-width: ${BREAKPOINTS[0]}px) { flex-direction: column; @@ -119,9 +119,9 @@ export const CanvasPlaceholder = styled.p` export const SignatureCanvasContainer = styled.div` width: 100%; - max-width: 900px; - height: auto; - aspect-ratio: 900 / 400; + max-width: 800px; + height: inherit; + aspect-ratio: 900 / 300; canvas { width: 100%; diff --git a/src/pages/AttendancePage/AttendanceStudentIdPage.jsx b/src/pages/AttendancePage/AttendanceStudentIdPage.jsx index a173625..a230ce6 100644 --- a/src/pages/AttendancePage/AttendanceStudentIdPage.jsx +++ b/src/pages/AttendancePage/AttendanceStudentIdPage.jsx @@ -1,6 +1,7 @@ import React, { useEffect, useState } from 'react'; import * as S from './AttendanceStudentIdPage.style'; import { AttendanceHeader } from '../../components'; +import Modal from '../../components/Modal/Modal'; import { USER_ID } from '../../constants'; import { useSessionStorages } from '../../hooks'; import { axiosInstance } from '../../axios'; @@ -16,26 +17,43 @@ const AttendanceStudentIdPage = () => { const [eventDate, setEventDate] = useState(''); const [isAlreadyCompleted, setIsAlreadyCompleted] = useState(false); const [isNoMatch, setIsNoMatch] = useState(false); + const [eventTarget, setEventTarget] = useState('INTERNAL'); + const [attendees, setAttendees] = useState([]); + const [isModalOpen, setIsModalOpen] = useState(false); const EVENT_ID = useRecoilValue(eventIDState); const { setSessionStorage } = useSessionStorages(); const studentId = Array.from({ length: 7 }, (_, index) => index + 1); + const phoneId = Array.from({ length: 4 }, (_, index) => index + 1); const dialList = Array.from({ length: 9 }, (_, index) => index + 1); - const isSevenDigits = enteredDials.length === 7; + const isSevenDigits = + eventTarget === 'INTERNAL' + ? enteredDials.length === 7 + : enteredDials.length === 4; const isConfirmEnabled = isSevenDigits; const getAttendanceCheck = async (params) => { - const { data } = await axiosInstance.get( - `/api/v1/attendance/check/${USER_ID}/${EVENT_ID}`, - { - params: { - studentNumber: params.studentNumber, - eventDate: params.eventDate, - }, + const url = + eventTarget === 'INTERNAL' + ? `/api/v1/attendance/check/studentNumber/${USER_ID}/${EVENT_ID}` + : `/api/v1/attendance/check/phoneNumber/${USER_ID}/${EVENT_ID}`; + + console.log('API 호출 URL:', url); + console.log('API 호출 파라미터:', { + [eventTarget === 'INTERNAL' ? 'studentNumber' : 'phoneNumber']: + params.number, + eventDate: params.eventDate, + }); + + const { data } = await axiosInstance.get(url, { + params: { + [eventTarget === 'INTERNAL' ? 'studentNumber' : 'phoneNumber']: + params.number, + eventDate: params.eventDate, }, - ); + }); return data; }; @@ -44,22 +62,27 @@ const AttendanceStudentIdPage = () => { setEnteredDials(enteredDials.slice(0, -1)); } else if (dial === '서명하러 가기' && isConfirmEnabled) { try { + const numberString = enteredDials.join(''); const data = await getAttendanceCheck({ - studentNumber: Number(enteredDials.join('')), + number: numberString, eventDate, }); - if (!data) { + if (!data || (Array.isArray(data) && data.length === 0)) { setIsNoMatch(true); - alert('일치하는 학번이 없습니다'); + alert('일치하는 정보가 없습니다'); } else if (data.isAlreadyCompleted) { setIsAlreadyCompleted(true); alert('이미 출석을 완료하였습니다'); + } else if (Array.isArray(data) && data.length > 1) { + // 동일한 휴대폰 번호 뒷 4자리를 가진 사람이 여러 명인 경우 + setAttendees(data); + setIsModalOpen(true); } else { const parsedStudent = { - name: data.studentName, - number: data.studentNumber, - major: data.major, + name: data.studentName || data[0].studentName, + number: data.studentNumber || data[0].studentNumber, + major: data.major || data[0].major, }; setAttendanceCheck(data); @@ -71,13 +94,17 @@ const AttendanceStudentIdPage = () => { } catch (error) { setEnteredDials([]); if (error.response && error.response.status === 404) { - alert('일치하는 학번이 없습니다'); + alert('일치하는 정보가 없습니다'); } else { + console.error('API 에러 발생:', error.response || error); alert('API 에러 발생'); } } } else { - if (enteredDials.length < 7 && dial !== '서명하러 가기') { + if ( + enteredDials.length < (eventTarget === 'INTERNAL' ? 7 : 4) && + dial !== '서명하러 가기' + ) { setEnteredDials([...enteredDials, dial]); } } @@ -91,8 +118,23 @@ const AttendanceStudentIdPage = () => { const response = await axiosInstance.get( `/api/v1/events/${USER_ID}/${EVENT_ID}`, ); - setEventTitle(response.data.eventTitle); - setEventDate(response.data.eventSchedules[0].eventDate); // 첫 번째 스케줄의 날짜를 가져옴 + const eventData = response.data; + setEventTitle(eventData.eventTitle); + setEventTarget(eventData.eventTarget); + + const now = new Date(); + const today = now.toISOString().split('T')[0]; + const todaySchedule = eventData.eventSchedules.find( + (schedule) => schedule.eventDate === today, + ); + + if (todaySchedule) { + setEventDate(todaySchedule.eventDate); + console.log('출석 체크가 처리되는 날짜:', todaySchedule.eventDate); + } else { + setEventDate(''); + console.log('오늘 날짜에 해당하는 일정이 없습니다.'); + } } catch (error) { console.error('이벤트 정보를 가져오는 중 에러 발생:', error); } @@ -104,9 +146,13 @@ const AttendanceStudentIdPage = () => { return ( - 학번을 입력해주세요. + + {eventTarget === 'INTERNAL' + ? '학번을 입력해주세요.' + : '휴대폰 번호 뒷자리 4자리를 입력해주세요.'} + - {studentId.map((index) => ( + {(eventTarget === 'INTERNAL' ? studentId : phoneId).map((index) => ( {enteredDials[index - 1] || ''} ))} @@ -132,6 +178,13 @@ const AttendanceStudentIdPage = () => { {'서명하러 가기'} + {isModalOpen && ( + setIsModalOpen(false)} + attendees={attendees} + /> + )} ); }; diff --git a/src/pages/DashboardPage/DashboardPage.jsx b/src/pages/DashboardPage/DashboardPage.jsx index 4abfb15..0af44c3 100644 --- a/src/pages/DashboardPage/DashboardPage.jsx +++ b/src/pages/DashboardPage/DashboardPage.jsx @@ -257,7 +257,7 @@ export default function DashboardPage() { 담당자 - 해당 연락처로 참석자들에게 문자와 메일이 발송됩니다. + 해당 연락처로 참석자 명단이 발송됩니다.