diff --git a/frontend/src/components/Sidebar.jsx b/frontend/src/components/Sidebar.jsx index 5f9ab057..2671a619 100644 --- a/frontend/src/components/Sidebar.jsx +++ b/frontend/src/components/Sidebar.jsx @@ -114,7 +114,7 @@ const Sidebar = ({ isOpen, isRoot, onClose }) => { } onClick={handleNavLinkClick} > - 출석하기 + 출석조회
  • diff --git a/frontend/src/components/attendance/SessionManage.jsx b/frontend/src/components/attendance/SessionManage.jsx index 2561c193..55eb009a 100644 --- a/frontend/src/components/attendance/SessionManage.jsx +++ b/frontend/src/components/attendance/SessionManage.jsx @@ -1,40 +1,96 @@ -import { useEffect, useState } from 'react'; +import { useMemo } from 'react'; import styles from './SessionManage.module.css'; import { ClipboardCheck } from 'lucide-react'; -import { attendanceList } from '../../utils/attendanceList'; - -const SessionManage = () => { - const [sessions, setSessions] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - - useEffect(() => { - const fetchAttendance = async () => { - try { - const data = await attendanceList(); - const normalizedSessions = Array.isArray(data) - ? data.filter((item) => item && typeof item === 'object') - : []; - setSessions(normalizedSessions); - } catch (err) { - setError('데이터를 불러오는 중 오류가 발생했습니다.'); - setSessions([]); - } finally { - setLoading(false); + +const getRoundKey = (session) => session.roundId || `${session.roundDate || ''}-${session.roundStartAt || ''}`; + +const getTimestamp = (session) => { + const dateSource = session.roundStartAt || session.roundDate || session.createdAt || session.checkedAt; + const timestamp = Date.parse(dateSource); + return Number.isNaN(timestamp) ? 0 : timestamp; +}; + +const formatDate = (dateValue) => { + if (!dateValue) return '-'; + const date = new Date(dateValue); + return Number.isNaN(date.getTime()) ? '-' : date.toLocaleDateString(); +}; + +const formatTime = (dateValue) => { + if (!dateValue) return '-'; + const date = new Date(dateValue); + return Number.isNaN(date.getTime()) + ? '-' + : date.toLocaleTimeString('ko-KR', { + hour: '2-digit', + minute: '2-digit', + hour12: true, + }); +}; + +const formatAttendanceStatus = (attendanceStatus) => { + const status = (attendanceStatus || '').toUpperCase(); + + if (status === 'PRESENT' || status === 'ATTENDED') return '출석'; + if (status === 'LATE') return '지각'; + if (status === 'ABSENT') return '결석'; + if (status === 'EXCUSED') return '공결'; + if (status === 'PENDING') return '대기'; + + return attendanceStatus || '-'; +}; + +const SessionManage = ({ sessions = [], selectedSession = '', loading, error }) => { + const roundIndexMapBySession = useMemo(() => { + const roundMapBySession = new Map(); + sessions.forEach((session) => { + const sessionTitle = session.sessionTitle || '기타'; + if (!roundMapBySession.has(sessionTitle)) { + roundMapBySession.set(sessionTitle, new Map()); + } + + const roundMap = roundMapBySession.get(sessionTitle); + const roundKey = getRoundKey(session); + if (!roundMap.has(roundKey)) { + roundMap.set(roundKey, getTimestamp(session)); } - }; + }); + + const indexedMapBySession = new Map(); + + roundMapBySession.forEach((roundMap, sessionTitle) => { + const sortedRoundKeys = Array.from(roundMap.entries()) + .sort((a, b) => a[1] - b[1]) + .map(([roundKey]) => roundKey); + + const indexedRoundMap = new Map(); + sortedRoundKeys.forEach((roundKey, index) => { + indexedRoundMap.set(roundKey, index + 1); + }); - fetchAttendance(); - }, []); + indexedMapBySession.set(sessionTitle, indexedRoundMap); + }); + + return indexedMapBySession; + }, [sessions]); + + const visibleSessions = useMemo(() => { + const filtered = selectedSession + ? sessions.filter((session) => session.sessionTitle === selectedSession) + : sessions; + + return [...filtered].sort((a, b) => getTimestamp(a) - getTimestamp(b)); + }, [sessions, selectedSession]); - if (loading) return
    로딩 중...
    ; if (error) return
    {error}
    ; + const rows = loading ? [] : visibleSessions; + return (
    - 세션 관리 + 출석 목록
    @@ -42,26 +98,28 @@ const SessionManage = () => { - - + - {(Array.isArray(sessions) ? sessions : []).map((s) => ( + {rows.map((s) => { + const sessionTitle = s.sessionTitle || '기타'; + const roundKey = getRoundKey(s); + const roundIndex = roundIndexMapBySession.get(sessionTitle)?.get(roundKey) ?? '-'; + + return ( - - - - + + + - + - ))} + ); + })}
    일자 출석시작시간출석가능시간 회차 이름출석 상태
    {new Date(s.createdAt).toLocaleDateString()}{new Date(s.checkedAt).toLocaleTimeString()}30분{s.roundId}{formatDate(s.roundDate)}{formatTime(s.roundStartAt)}{roundIndex} {s.userName} - - {formatAttendanceStatus(s.attendanceStatus)}
    diff --git a/frontend/src/components/attendance/SessionManage.module.css b/frontend/src/components/attendance/SessionManage.module.css index f70872c7..1650e2a8 100644 --- a/frontend/src/components/attendance/SessionManage.module.css +++ b/frontend/src/components/attendance/SessionManage.module.css @@ -1,40 +1,35 @@ .card { width: 100%; max-width: 955px; - min-height: 320px; + min-height: 280px; box-sizing: border-box; - border: 1px solid #c9bebe; + border: 1px solid #d9d9d9; background: #fafbfc; border-radius: 12px; padding: 20px; display: flex; flex-direction: column; - gap: 30px; + gap: 16px; } .title { display: flex; align-items: center; gap: 10px; - font-weight: 400; + font-weight: 500; font-size: 18px; - line-height: 146%; - letter-spacing: 0%; + line-height: 1.4; color: #000000; } .table { width: 100%; border-collapse: collapse; - table-layout: auto; + table-layout: fixed; overflow-x: auto; display: block; } -.table td:last-child { - text-align: right; -} - .table thead, .table tbody { display: table; @@ -45,38 +40,17 @@ .table thead th, .table tbody td { text-align: left; - padding: 8px 7px; + padding: 10px 8px; white-space: nowrap; - border-bottom: 0.4px solid #6a6a6a; - font-family: Inter; + border-bottom: 1px solid #e5e5e5; font-weight: 400; font-size: 14px; - line-height: 100%; - letter-spacing: 0%; + line-height: 1.3; color: #000000; overflow: hidden; text-overflow: ellipsis; } -.button { - background: linear-gradient( - 92.89deg, - #d8e8ff 6.95%, - #d1d8ff 74.81%, - #dddeff 107.39% - ); - width: 60px; - height: 30px; - border-radius: 8px; - border: none; - font-weight: 400; - font-size: 14px; - line-height: 100%; - letter-spacing: 0%; - color: #000000; - cursor: pointer; -} - /* 태블릿 이상 */ @media (min-width: 768px) { .card { @@ -90,24 +64,19 @@ .table { display: table; + overflow-x: visible; } .table thead th, .table tbody td { font-size: 16px; } - - .button { - width: 70px; - height: 35px; - font-size: 16px; - } } /* 데스크톱 */ @media (min-width: 1024px) { .card { - padding: 20px 40px; - gap: 50px; + padding: 30px 40px; + gap: 20px; } } diff --git a/frontend/src/components/attendance/SessionSelectBox.jsx b/frontend/src/components/attendance/SessionSelectBox.jsx index 0d7f8301..0dbb5693 100644 --- a/frontend/src/components/attendance/SessionSelectBox.jsx +++ b/frontend/src/components/attendance/SessionSelectBox.jsx @@ -1,24 +1,33 @@ import styles from './SessionSelectBox.module.css'; import { ClipboardCheck } from 'lucide-react'; -const sessionList = [ - '증권 1팀', - '증권 2팀', - '증권 3팀', - '자신운용팀', - '금융 IT팀', - '매크로팀', - '트레이딩팀', -]; +const SessionSelectBox = ({ + sessions = [], + selectedSession = '', + onChange, + disabled = false, +}) => { + const sessionList = Array.from( + new Set( + sessions + .map((session) => session.sessionTitle) + .filter((sessionTitle) => typeof sessionTitle === 'string' && sessionTitle.trim() !== ''), + ), + ); -const SessionSelectBox = () => { return (
    - onChange?.(event.target.value)} + disabled={disabled} + > {sessionList.map((item) => (