From 6e736046c48a8cd9634ca288967aa5df7d913145 Mon Sep 17 00:00:00 2001 From: sangkyu39 Date: Mon, 23 Mar 2026 16:30:42 +0900 Subject: [PATCH 01/12] =?UTF-8?q?[FE]=ED=94=BC=EB=93=9C=EB=B0=B1=20?= =?UTF-8?q?=EB=AA=A8=EB=8B=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/Sidebar.jsx | 2 +- frontend/src/components/Sidebar.module.css | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/frontend/src/components/Sidebar.jsx b/frontend/src/components/Sidebar.jsx index 66707077..279b765f 100644 --- a/frontend/src/components/Sidebar.jsx +++ b/frontend/src/components/Sidebar.jsx @@ -432,7 +432,7 @@ const Sidebar = ({ isOpen, isRoot, onClose }) => {

피드백 작성

- 솔직한 피드백은 금융IT팀에게 큰 도움이 됩니다. + 솔직한 피드백은 금융IT팀에게 큰 도움이 됩니다. 피드백은 익명으로 저장됩니다.

+ +
+ {absenceData.length > 0 ? ( + + + + + + + + + + + {absenceData.map(user => ( + + + + + + + ))} + +
이름학번총 결석총 지각
{user.userName}{user.studentId}{user.totalAbsences}회{user.totalLates}회
+ ) : ( +

결석 또는 지각 기록이 있는 학생이 없습니다.

+ )} +
+
+ +
+ + + ); +}; + +export default AbsenceSummaryModal; diff --git a/frontend/src/components/attendancemanage/AbsenceSummaryModal.module.css b/frontend/src/components/attendancemanage/AbsenceSummaryModal.module.css new file mode 100644 index 00000000..343a8dd8 --- /dev/null +++ b/frontend/src/components/attendancemanage/AbsenceSummaryModal.module.css @@ -0,0 +1,142 @@ +.modalOverlay { + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + background-color: rgba(0, 0, 0, 0.45); + display: flex; + align-items: center; + justify-content: center; + z-index: 9999; + backdrop-filter: blur(4px); +} + +.modalContent { + background-color: #ffffff; + border-radius: 16px; + width: 90%; + max-width: 540px; + max-height: 85vh; + box-shadow: 0 12px 32px rgba(0, 0, 0, 0.15); + display: flex; + flex-direction: column; + animation: modalAppear 0.25s ease-out; +} + +@keyframes modalAppear { + from { + opacity: 0; + transform: translateY(15px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.modalHeader { + padding: 24px 28px; + border-bottom: 1px solid #f1f3f7; + display: flex; + justify-content: space-between; + align-items: center; +} + +.modalHeader h2 { + margin: 0; + font-size: 20px; + font-weight: 700; + color: #1e293b; +} + +.closeButton { + background: none; + border: none; + font-size: 28px; + color: #64748b; + cursor: pointer; + transition: color 0.15s; +} + +.closeButton:hover { + color: #1e293b; +} + +.modalBody { + padding: 24px 28px; + overflow-y: auto; + flex: 1; +} + +.summaryTable { + width: 100%; + border-collapse: collapse; +} + +.summaryTable th { + text-align: left; + padding: 12px 14px; + font-size: 14px; + font-weight: 600; + color: #64748b; + background: #f8fafc; + border-bottom: 2px solid #e2e8f0; +} + +.summaryTable td { + padding: 14px; + font-size: 15px; + color: #334155; + border-bottom: 1px solid #f1f3f7; +} + +.absentCount { + color: #ef4444; + font-weight: 700; +} + +.lateCount { + color: #9333ea; + font-weight: 700; +} + +.noData { + text-align: center; + color: #64748b; + font-size: 15px; + margin: 40px 0; +} + +.modalFooter { + padding: 20px 28px; + border-top: 1px solid #f1f3f7; + display: flex; + justify-content: flex-end; +} + +.confirmButton { + background-color: #2563eb; + color: #ffffff; + padding: 10px 24px; + border: none; + border-radius: 8px; + font-size: 15px; + font-weight: 600; + cursor: pointer; + transition: background-color 0.15s; +} + +.confirmButton:hover { + background-color: #1d4ed8; +} + +@media (max-width: 640px) { + .modalContent { + width: 95%; + } + + .modalHeader, .modalBody, .modalFooter { + padding: 16px 20px; + } +} diff --git a/frontend/src/components/attendancemanage/AttendanceManagementCard.jsx b/frontend/src/components/attendancemanage/AttendanceManagementCard.jsx index c270f236..3638f7f8 100644 --- a/frontend/src/components/attendancemanage/AttendanceManagementCard.jsx +++ b/frontend/src/components/attendancemanage/AttendanceManagementCard.jsx @@ -9,6 +9,7 @@ import profileIcon from '../../assets/profile-icon.svg'; import binIcon from '../../assets/bin-icon.svg'; import slashProfileIcon from '../../assets/slash-profile-icon.svg'; import ConfirmationToast from './ConfirmationToast'; +import AbsenceSummaryModal from './AbsenceSummaryModal'; // 상태별 텍스트 및 스타일 통합 관리 const ATTENDANCE_CONFIG = { @@ -151,20 +152,54 @@ const AttendanceManagementCard = ({ styles: commonStyles }) => { const [activeToastId, setActiveToastId] = useState(null); const [openDropdownKey, setOpenDropdownKey] = useState(null); const [currentPage, setCurrentPage] = useState(1); + const [isAbsenceModalOpen, setIsAbsenceModalOpen] = useState(false); const cardRef = useRef(null); const fetchRequestIdRef = useRef(0); const sortedUserRows = useMemo(() => { - return attendanceData.userRows - .map((user, index) => ({ user, index })) - .sort((a, b) => { - const priorityDiff = - getRoleSortPriority(a.user.role) - getRoleSortPriority(b.user.role); - if (priorityDiff !== 0) return priorityDiff; - return a.index - b.index; - }) - .map((item) => item.user); - }, [attendanceData.userRows]); + if (!attendanceData.rounds.length) { + return attendanceData.userRows + .map((user, index) => ({ user, index })) + .sort((a, b) => { + const priorityDiff = + getRoleSortPriority(a.user.role) - getRoleSortPriority(b.user.role); + if (priorityDiff !== 0) return priorityDiff; + return a.index - b.index; + }) + .map((item) => item.user); + } + + const todayStr = new Date().toISOString().split('T')[0]; + const targetRound = + attendanceData.rounds.find((r) => r.roundDate === todayStr) || + attendanceData.rounds[attendanceData.rounds.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); + + const aStatus = aAtt?.status || 'PENDING'; + const bStatus = bAtt?.status || 'PENDING'; + + const getStatusPriority = (status) => { + if (status === 'ABSENT') return 0; + if (status === 'LATE') return 1; + return 2; + }; + + const statusDiff = getStatusPriority(aStatus) - getStatusPriority(bStatus); + if (statusDiff !== 0) return statusDiff; + + // Secondary sort by role + const priorityDiff = + getRoleSortPriority(a.role) - getRoleSortPriority(b.role); + if (priorityDiff !== 0) return priorityDiff; + + return a.userName.localeCompare(b.userName, 'ko'); + }); + }, [attendanceData.userRows, attendanceData.rounds]); const totalUsers = sortedUserRows.length; const totalPages = Math.max(1, Math.ceil(totalUsers / USERS_PER_PAGE)); @@ -446,6 +481,12 @@ const AttendanceManagementCard = ({ styles: commonStyles }) => { 유저 삭제 + @@ -475,64 +516,66 @@ const AttendanceManagementCard = ({ styles: commonStyles }) => { {paginatedUserRows.length > 0 ? ( - paginatedUserRows.map((user) => ( - - - toggleUserSelection(user.userId)} - /> - - {user.userName} - {getRoleDisplayLabel(user.role)} - {user.studentId} - {user.attendances.map((att) => { - const statusClass = - styles[ - ATTENDANCE_CONFIG[att.status]?.className || - ATTENDANCE_CONFIG.PENDING.className - ]; - const dropdownKey = `${user.userId}-${att.roundId}`; - const isOpen = openDropdownKey === dropdownKey; - - return ( - - - setOpenDropdownKey((prev) => - prev === dropdownKey ? null : dropdownKey - ) - } - onSelect={(nextStatus) => { - setOpenDropdownKey(null); - if (nextStatus !== att.status) { - handleAttendanceChange( - user.userId, - att.roundId, - nextStatus - ); + paginatedUserRows.map((user) => { + return ( + + + toggleUserSelection(user.userId)} + /> + + {user.userName} + {getRoleDisplayLabel(user.role)} + {user.studentId} + {user.attendances.map((att) => { + const statusClass = + styles[ + ATTENDANCE_CONFIG[att.status]?.className || + ATTENDANCE_CONFIG.PENDING.className + ]; + const dropdownKey = `${user.userId}-${att.roundId}`; + const isOpen = openDropdownKey === dropdownKey; + + return ( + + + setOpenDropdownKey((prev) => + prev === dropdownKey ? null : dropdownKey + ) } - }} - /> - - ); - })} - - )) + onSelect={(nextStatus) => { + setOpenDropdownKey(null); + if (nextStatus !== att.status) { + handleAttendanceChange( + user.userId, + att.roundId, + nextStatus + ); + } + }} + /> + + ); + })} + + ); + }) ) : ( { })} )} + {isAbsenceModalOpen && ( + setIsAbsenceModalOpen(false)} + userRows={attendanceData.userRows} + /> + )} ); }; diff --git a/frontend/src/components/attendancemanage/AttendanceManagementCard.module.css b/frontend/src/components/attendancemanage/AttendanceManagementCard.module.css index 9406960a..1bb6c08f 100644 --- a/frontend/src/components/attendancemanage/AttendanceManagementCard.module.css +++ b/frontend/src/components/attendancemanage/AttendanceManagementCard.module.css @@ -625,6 +625,28 @@ height: 18px; } +.absenceSummaryButton { + margin-left: 12px; + padding: 8px 16px; + background-color: #ff4d2d; + color: #ffffff; + border: none; + border-radius: 8px; + font-size: 14px; + font-weight: 600; + cursor: pointer; + transition: background-color 0.2s, transform 0.1s; + white-space: nowrap; +} + +.absenceSummaryButton:hover { + background-color: #e6391d; +} + +.absenceSummaryButton:active { + transform: scale(0.98); +} + .tableGroup { margin-top: 4px; max-height: none; From 9d1490534f0ea23785fd11fb62cf0c12472cf69a Mon Sep 17 00:00:00 2001 From: yyunee Date: Fri, 3 Apr 2026 15:56:27 +0900 Subject: [PATCH 08/12] [FIX] Rabbit Review --- frontend/src/components/AdminRoute.jsx | 5 ++- .../attendancemanage/AbsenceSummaryModal.jsx | 37 +++++++++++++++---- .../AttendanceManagementCard.jsx | 12 ++++-- frontend/src/hooks/useAuthGuard.js | 7 +++- 4 files changed, 48 insertions(+), 13 deletions(-) diff --git a/frontend/src/components/AdminRoute.jsx b/frontend/src/components/AdminRoute.jsx index ddf8c748..b54591f6 100644 --- a/frontend/src/components/AdminRoute.jsx +++ b/frontend/src/components/AdminRoute.jsx @@ -25,11 +25,14 @@ const AdminRoute = () => { setIsAuthorized(isAdminRole(role)); } catch (error) { setIsAuthorized(false); + const isUnauthorized = + error?.status === 401 || error?.response?.status === 401; + const returnUrl = encodeURIComponent( location.pathname + location.search ); setRedirectPath( - error?.status === 401 ? `/login?returnUrl=${returnUrl}` : '/' + isUnauthorized ? `/login?returnUrl=${returnUrl}` : '/' ); } finally { setLoading(false); diff --git a/frontend/src/components/attendancemanage/AbsenceSummaryModal.jsx b/frontend/src/components/attendancemanage/AbsenceSummaryModal.jsx index f98b04aa..9e6da871 100644 --- a/frontend/src/components/attendancemanage/AbsenceSummaryModal.jsx +++ b/frontend/src/components/attendancemanage/AbsenceSummaryModal.jsx @@ -20,10 +20,23 @@ const AbsenceSummaryModal = ({ isOpen, onClose, userRows }) => { return (
-
e.stopPropagation()}> +
e.stopPropagation()} + role="dialog" + aria-modal="true" + aria-labelledby="absence-modal-title" + >
-

결석 및 지각 집계

- +

결석 및 지각 집계

+
{absenceData.length > 0 ? ( @@ -37,22 +50,32 @@ const AbsenceSummaryModal = ({ isOpen, onClose, userRows }) => { - {absenceData.map(user => ( + {absenceData.map((user) => ( {user.userName} {user.studentId} - {user.totalAbsences}회 + + {user.totalAbsences}회 + {user.totalLates}회 ))} ) : ( -

결석 또는 지각 기록이 있는 학생이 없습니다.

+

+ 결석 또는 지각 기록이 있는 학생이 없습니다. +

)}
- +
diff --git a/frontend/src/components/attendancemanage/AttendanceManagementCard.jsx b/frontend/src/components/attendancemanage/AttendanceManagementCard.jsx index 3638f7f8..11062bde 100644 --- a/frontend/src/components/attendancemanage/AttendanceManagementCard.jsx +++ b/frontend/src/components/attendancemanage/AttendanceManagementCard.jsx @@ -169,10 +169,16 @@ const AttendanceManagementCard = ({ styles: commonStyles }) => { .map((item) => item.user); } - const todayStr = new Date().toISOString().split('T')[0]; + // 로컬 시간대 기준으로 오늘 날짜(YYYY-MM-DD)와 정렬된 회차 목록 생성 + const sortedRounds = [...attendanceData.rounds].sort( + (a, b) => + a.roundDate.localeCompare(b.roundDate) || a.roundNumber - b.roundNumber + ); + const todayStr = new Date().toLocaleDateString('sv-SE'); + const targetRound = - attendanceData.rounds.find((r) => r.roundDate === todayStr) || - attendanceData.rounds[attendanceData.rounds.length - 1]; + sortedRounds.find((r) => r.roundDate === todayStr) || + sortedRounds[sortedRounds.length - 1]; const targetRoundId = targetRound?.roundId; diff --git a/frontend/src/hooks/useAuthGuard.js b/frontend/src/hooks/useAuthGuard.js index b159d7b9..9fc4591a 100644 --- a/frontend/src/hooks/useAuthGuard.js +++ b/frontend/src/hooks/useAuthGuard.js @@ -14,8 +14,11 @@ export const useAuthGuard = () => { await api.get('/api/user/details'); // 성공하면 아무것도 하지 않음 } catch (error) { - // 401 에러만 로그인 페이지로 리다이렉트 - if (error.status === 401) { + // 401 에러(인증 실패)만 로그인 페이지로 리다이렉트 + const isUnauthorized = + error?.status === 401 || error?.response?.status === 401; + + if (isUnauthorized) { toast.error('로그인 후 이용하실 수 있습니다.'); const returnUrl = encodeURIComponent( location.pathname + location.search From 568195aface696ec71ef3b6bddf40859c57bc13d Mon Sep 17 00:00:00 2001 From: yyunee Date: Sun, 5 Apr 2026 15:40:51 +0900 Subject: [PATCH 09/12] [FIX] Error AttendanceMangement Page --- .../attendancemanage/AbsenceSummaryModal.jsx | 5 ++++- .../AbsenceSummaryModal.module.css | 4 ++++ .../AttendanceManagementCard.jsx | 21 +++++++++---------- .../SessionManagementCard.jsx | 11 +++++++--- 4 files changed, 26 insertions(+), 15 deletions(-) diff --git a/frontend/src/components/attendancemanage/AbsenceSummaryModal.jsx b/frontend/src/components/attendancemanage/AbsenceSummaryModal.jsx index 9e6da871..d89123a9 100644 --- a/frontend/src/components/attendancemanage/AbsenceSummaryModal.jsx +++ b/frontend/src/components/attendancemanage/AbsenceSummaryModal.jsx @@ -1,4 +1,5 @@ import React from 'react'; +import { createPortal } from 'react-dom'; import styles from './AbsenceSummaryModal.module.css'; const AbsenceSummaryModal = ({ isOpen, onClose, userRows }) => { @@ -18,7 +19,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); From d0b3d341ff70e90694b5095f10fc96e1b24e2b30 Mon Sep 17 00:00:00 2001 From: yyunee Date: Sun, 5 Apr 2026 15:58:51 +0900 Subject: [PATCH 10/12] [FIX] rabbit review --- .../src/components/attendancemanage/AbsenceSummaryModal.jsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/attendancemanage/AbsenceSummaryModal.jsx b/frontend/src/components/attendancemanage/AbsenceSummaryModal.jsx index d89123a9..c307c04e 100644 --- a/frontend/src/components/attendancemanage/AbsenceSummaryModal.jsx +++ b/frontend/src/components/attendancemanage/AbsenceSummaryModal.jsx @@ -6,10 +6,10 @@ 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, From 1d1b3dd40345bbc63252345cc195f3e4d4782f45 Mon Sep 17 00:00:00 2001 From: yyunee Date: Sun, 5 Apr 2026 16:02:36 +0900 Subject: [PATCH 11/12] fix: restore missing guard clauses, React Portal, and sticky headers lost during merge --- .../attendancemanage/AbsenceSummaryModal.jsx | 6 ++++-- .../AbsenceSummaryModal.module.css | 4 ++++ .../AttendanceManagementCard.jsx | 17 ++++++++--------- 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/frontend/src/components/attendancemanage/AbsenceSummaryModal.jsx b/frontend/src/components/attendancemanage/AbsenceSummaryModal.jsx index 33a02bee..29c12e09 100644 --- a/frontend/src/components/attendancemanage/AbsenceSummaryModal.jsx +++ b/frontend/src/components/attendancemanage/AbsenceSummaryModal.jsx @@ -1,4 +1,4 @@ -import React from 'react'; +import { createPortal } from 'react-dom'; import styles from './AbsenceSummaryModal.module.css'; const AbsenceSummaryModal = ({ isOpen, onClose, userRows }) => { @@ -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 66793dd8..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'; @@ -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 || From f24fc307bb9a293c021f09685e7e1470e86dc1f5 Mon Sep 17 00:00:00 2001 From: yyunee Date: Mon, 6 Apr 2026 00:16:07 +0900 Subject: [PATCH 12/12] [FIX] session menu button and dropdown visibility --- .../components/attendancemanage/SessionManagementCard.module.css | 1 - 1 file changed, 1 deletion(-) 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; }