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 (
-
+
-