Skip to content
2 changes: 1 addition & 1 deletion frontend/src/components/Sidebar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ const Sidebar = ({ isOpen, isRoot, onClose }) => {
}
onClick={handleNavLinkClick}
>
출석하기
출석조회
</NavLink>
</li>
<li>
Expand Down
132 changes: 95 additions & 37 deletions frontend/src/components/attendance/SessionManage.jsx
Original file line number Diff line number Diff line change
@@ -1,67 +1,125 @@
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 <div>로딩 중...</div>;
if (error) return <div>{error}</div>;

const rows = loading ? [] : visibleSessions;

return (
<div className={styles.card}>
<div className={styles.title}>
<ClipboardCheck />
세션 관리
출석 목록
</div>

<table className={styles.table} role="grid">
<thead>
<tr>
<th>일자</th>
<th>출석시작시간</th>
<th>출석가능시간</th>
<th>회차</th>
<th>이름</th>
<th></th>
<th>출석 상태</th>
</tr>
</thead>

<tbody>
{(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 (
<tr key={s.attendanceId}>
<td>{new Date(s.createdAt).toLocaleDateString()}</td>
<td>{new Date(s.checkedAt).toLocaleTimeString()}</td>
<td>30분</td>
<td>{s.roundId}</td>
<td>{formatDate(s.roundDate)}</td>
<td>{formatTime(s.roundStartAt)}</td>
<td>{roundIndex}</td>
<td>{s.userName}</td>
<td>
<button className={styles.button}>출석</button>
</td>
<td>{formatAttendanceStatus(s.attendanceStatus)}</td>
</tr>
))}
);
})}
</tbody>
</table>
</div>
Expand Down
55 changes: 12 additions & 43 deletions frontend/src/components/attendance/SessionManage.module.css
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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 {
Expand All @@ -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;
}
}
33 changes: 21 additions & 12 deletions frontend/src/components/attendance/SessionSelectBox.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className={styles.box}>
<div className={styles.title}>
<ClipboardCheck aria-hidden="true" />
<label htmlFor="session-select">출석하기</label>
<label htmlFor="session-select">세션선택</label>
</div>
<select id="session-select" className={styles.session}>
<select
id="session-select"
className={styles.session}
value={selectedSession}
onChange={(event) => onChange?.(event.target.value)}
disabled={disabled}
>
<option value="">세션선택</option>
{sessionList.map((item) => (
<option key={item} value={item}>
Expand Down
34 changes: 19 additions & 15 deletions frontend/src/components/attendance/SessionSelectBox.module.css
Original file line number Diff line number Diff line change
@@ -1,39 +1,39 @@
.box {
border: 1px solid #c9bebe;
border: 1px solid #d9d9d9;
background: #fafbfc;
width: 100%;
max-width: 600px;
min-height: 160px;
max-width: 955px;
min-height: 140px;
border-radius: 12px;
padding: 20px;
display: flex;
flex-direction: column;
gap: 20px;
gap: 16px;
box-sizing: border-box;
}

.title {
display: flex;
align-items: center;
gap: 5px;
font-weight: 700;
gap: 10px;
font-weight: 500;
font-size: 18px;
line-height: 146%;
letter-spacing: 0%;
color: #171717;
line-height: 1.4;
color: #000000;
}

.session {
width: 100%;
max-width: 280px;
height: 40px;
padding: 10px 20px;
border: 0.8px solid #afafaf;
max-width: 360px;
height: 42px;
padding: 10px 12px;
border: 1px solid #c9c9c9;
border-radius: 8px;
font-weight: 400;
font-size: 16px;
line-height: 100%;
font-size: 15px;
line-height: 1.2;
color: #171717;
background: #ffffff;
box-sizing: border-box;
}

Expand All @@ -47,4 +47,8 @@
.title {
font-size: 20px;
}

.session {
font-size: 16px;
}
}
Loading