diff --git a/frontend/src/components/attendancemanage/AbsenceSummaryModal.jsx b/frontend/src/components/attendancemanage/AbsenceSummaryModal.jsx index 9e6da871..29c12e09 100644 --- a/frontend/src/components/attendancemanage/AbsenceSummaryModal.jsx +++ b/frontend/src/components/attendancemanage/AbsenceSummaryModal.jsx @@ -1,14 +1,14 @@ -import React from 'react'; +import { createPortal } from 'react-dom'; import styles from './AbsenceSummaryModal.module.css'; const AbsenceSummaryModal = ({ isOpen, onClose, userRows }) => { if (!isOpen) return null; // 결석한 기록이 있는 유저들만 필터링하고 결석 횟수 계산 - const absenceData = userRows + const absenceData = (userRows || []) .map(user => { - const totalAbsences = user.attendances.filter(att => att.status === 'ABSENT').length; - const totalLates = user.attendances.filter(att => att.status === 'LATE').length; + const totalAbsences = (user.attendances || []).filter(att => att.status === 'ABSENT').length; + const totalLates = (user.attendances || []).filter(att => att.status === 'LATE').length; return { ...user, totalAbsences, @@ -18,7 +18,7 @@ const AbsenceSummaryModal = ({ isOpen, onClose, userRows }) => { .filter(user => user.totalAbsences > 0 || user.totalLates > 0) .sort((a, b) => b.totalAbsences - a.totalAbsences || b.totalLates - a.totalLates); - return ( + const modalContent = (
{
); + + return createPortal(modalContent, document.body); }; export default AbsenceSummaryModal; diff --git a/frontend/src/components/attendancemanage/AbsenceSummaryModal.module.css b/frontend/src/components/attendancemanage/AbsenceSummaryModal.module.css index 343a8dd8..ab2d12ad 100644 --- a/frontend/src/components/attendancemanage/AbsenceSummaryModal.module.css +++ b/frontend/src/components/attendancemanage/AbsenceSummaryModal.module.css @@ -67,6 +67,7 @@ padding: 24px 28px; overflow-y: auto; flex: 1; + max-height: 100%; } .summaryTable { @@ -75,6 +76,8 @@ } .summaryTable th { + position: sticky; + top: -24px; /* modalBody padding-top 보정 */ text-align: left; padding: 12px 14px; font-size: 14px; @@ -82,6 +85,7 @@ color: #64748b; background: #f8fafc; border-bottom: 2px solid #e2e8f0; + z-index: 10; } .summaryTable td { diff --git a/frontend/src/components/attendancemanage/AttendanceManagementCard.jsx b/frontend/src/components/attendancemanage/AttendanceManagementCard.jsx index 11062bde..a00352af 100644 --- a/frontend/src/components/attendancemanage/AttendanceManagementCard.jsx +++ b/frontend/src/components/attendancemanage/AttendanceManagementCard.jsx @@ -157,8 +157,8 @@ const AttendanceManagementCard = ({ styles: commonStyles }) => { const fetchRequestIdRef = useRef(0); const sortedUserRows = useMemo(() => { - if (!attendanceData.rounds.length) { - return attendanceData.userRows + if (!(attendanceData?.rounds || []).length) { + return (attendanceData?.userRows || []) .map((user, index) => ({ user, index })) .sort((a, b) => { const priorityDiff = @@ -170,9 +170,9 @@ const AttendanceManagementCard = ({ styles: commonStyles }) => { } // 로컬 시간대 기준으로 오늘 날짜(YYYY-MM-DD)와 정렬된 회차 목록 생성 - const sortedRounds = [...attendanceData.rounds].sort( + const sortedRounds = [...(attendanceData?.rounds || [])].sort( (a, b) => - a.roundDate.localeCompare(b.roundDate) || a.roundNumber - b.roundNumber + (a.roundDate || '').localeCompare(b.roundDate || '') || (a.roundNumber || 0) - (b.roundNumber || 0) ); const todayStr = new Date().toLocaleDateString('sv-SE'); @@ -181,10 +181,9 @@ const AttendanceManagementCard = ({ styles: commonStyles }) => { sortedRounds[sortedRounds.length - 1]; const targetRoundId = targetRound?.roundId; - - return [...attendanceData.userRows].sort((a, b) => { - const aAtt = a.attendances.find((att) => att.roundId === targetRoundId); - const bAtt = b.attendances.find((att) => att.roundId === targetRoundId); + return [...(attendanceData?.userRows || [])].sort((a, b) => { + const aAtt = (a?.attendances || []).find((att) => att.roundId === targetRoundId); + const bAtt = (b?.attendances || []).find((att) => att.roundId === targetRoundId); const aStatus = aAtt?.status || 'PENDING'; const bStatus = bAtt?.status || 'PENDING'; @@ -513,7 +512,7 @@ const AttendanceManagementCard = ({ styles: commonStyles }) => { 이름 역할 학번 - {attendanceData.rounds.map((round) => ( + {(attendanceData?.rounds || []).map((round) => ( {round.roundNumber}회차 @@ -540,7 +539,7 @@ const AttendanceManagementCard = ({ styles: commonStyles }) => { {user.userName} {getRoleDisplayLabel(user.role)} {user.studentId} - {user.attendances.map((att) => { + {(user.attendances || []).map((att) => { const statusClass = styles[ ATTENDANCE_CONFIG[att.status]?.className || @@ -585,7 +584,7 @@ const AttendanceManagementCard = ({ styles: commonStyles }) => { ) : ( 데이터가 존재하지 않습니다. diff --git a/frontend/src/components/attendancemanage/SessionManagementCard.jsx b/frontend/src/components/attendancemanage/SessionManagementCard.jsx index 0a3a4463..7a2f182c 100644 --- a/frontend/src/components/attendancemanage/SessionManagementCard.jsx +++ b/frontend/src/components/attendancemanage/SessionManagementCard.jsx @@ -77,7 +77,12 @@ const SessionManagementCard = ({ styles: commonStyles }) => { const rounds = await getRounds(selectedSessionId); setCurrentDisplayedRounds(rounds || []); } catch (e) { - toast.error('라운드를 불러오지 못했습니다.'); + const status = e?.response?.status ?? e?.status; + if (status === 403) { + toast.error('세션 멤버가 아니거나 조회 권한이 없습니다.'); + } else { + toast.error('라운드를 불러오지 못했습니다.'); + } setCurrentDisplayedRounds([]); } }; @@ -214,8 +219,8 @@ const SessionManagementCard = ({ styles: commonStyles }) => { - {currentDisplayedRounds.length > 0 ? ( - currentDisplayedRounds.map((round, index) => { + {(currentDisplayedRounds || []).length > 0 ? ( + (currentDisplayedRounds || []).map((round, index) => { const startTime = new Date(round.startAt); const closeTime = new Date(round.closeAt); diff --git a/frontend/src/components/attendancemanage/SessionManagementCard.module.css b/frontend/src/components/attendancemanage/SessionManagementCard.module.css index 0780be4e..702149af 100644 --- a/frontend/src/components/attendancemanage/SessionManagementCard.module.css +++ b/frontend/src/components/attendancemanage/SessionManagementCard.module.css @@ -515,7 +515,6 @@ width: 100%; min-width: 0; overflow-x: auto; - overflow-y: hidden; padding-bottom: 2px; margin-left: 0; }