Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 26 additions & 4 deletions frontend/src/components/attendance/SessionManage.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useMemo } from 'react';
import styles from './SessionManage.module.css';
import { ClipboardCheck } from 'lucide-react';
import { normalizeSessionTitle } from '../../utils/normalizeSessionTitle';

const getRoundKey = (session) => session.roundId || `${session.roundDate || ''}-${session.roundStartAt || ''}`;

Expand Down Expand Up @@ -44,7 +45,7 @@ const SessionManage = ({ sessions = [], selectedSession = '', loading, error })
const roundIndexMapBySession = useMemo(() => {
const roundMapBySession = new Map();
sessions.forEach((session) => {
const sessionTitle = session.sessionTitle || '기타';
const sessionTitle = normalizeSessionTitle(session.sessionTitle);
if (!roundMapBySession.has(sessionTitle)) {
roundMapBySession.set(sessionTitle, new Map());
}
Expand Down Expand Up @@ -75,11 +76,32 @@ const SessionManage = ({ sessions = [], selectedSession = '', loading, error })
}, [sessions]);

const visibleSessions = useMemo(() => {
const selectedSessionTitle = normalizeSessionTitle(selectedSession);

const filtered = selectedSession
? sessions.filter((session) => session.sessionTitle === selectedSession)
? sessions.filter((session) => normalizeSessionTitle(session.sessionTitle) === selectedSessionTitle)
: sessions;

return [...filtered].sort((a, b) => getTimestamp(a) - getTimestamp(b));
const seenAttendanceIds = new Set();
const deduplicated = [];

filtered.forEach((session) => {
const attendanceId = session?.attendanceId;

if (!attendanceId) {
deduplicated.push(session);
return;
}

if (seenAttendanceIds.has(attendanceId)) {
return;
}

seenAttendanceIds.add(attendanceId);
deduplicated.push(session);
});

return [...deduplicated].sort((a, b) => getTimestamp(a) - getTimestamp(b));
}, [sessions, selectedSession]);

if (error) return <div>{error}</div>;
Expand All @@ -106,7 +128,7 @@ const SessionManage = ({ sessions = [], selectedSession = '', loading, error })

<tbody>
{rows.map((s) => {
const sessionTitle = s.sessionTitle || '기타';
const sessionTitle = normalizeSessionTitle(s.sessionTitle);
const roundKey = getRoundKey(s);
const roundIndex = roundIndexMapBySession.get(sessionTitle)?.get(roundKey) ?? '-';

Expand Down
10 changes: 5 additions & 5 deletions frontend/src/components/attendance/SessionSelectBox.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import styles from './SessionSelectBox.module.css';
import { ClipboardCheck } from 'lucide-react';
import { normalizeSessionTitle } from '../../utils/normalizeSessionTitle';

const SessionSelectBox = ({
sessions = [],
Expand All @@ -9,12 +10,12 @@ const SessionSelectBox = ({
}) => {
const sessionList = Array.from(
new Set(
sessions
.map((session) => session.sessionTitle)
.filter((sessionTitle) => typeof sessionTitle === 'string' && sessionTitle.trim() !== ''),
sessions.map((session) => normalizeSessionTitle(session.sessionTitle)),
),
);

const currentValue = sessionList.includes(selectedSession) ? selectedSession : sessionList[0] || '';

return (
<div className={styles.box}>
<div className={styles.title}>
Expand All @@ -24,11 +25,10 @@ const SessionSelectBox = ({
<select
id="session-select"
className={styles.session}
value={selectedSession}
value={currentValue}
onChange={(event) => onChange?.(event.target.value)}
disabled={disabled}
>
<option value="">세션선택</option>
{sessionList.map((item) => (
<option key={item} value={item}>
{item}
Expand Down
28 changes: 26 additions & 2 deletions frontend/src/pages/Attendance.jsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import styles from './Attendance.module.css';
import { useEffect, useState } from 'react';
import { useEffect, useMemo, useState } from 'react';
import SessionSelectBox from '../components/attendance/SessionSelectBox';
// import ExcusedTime from '../components/attendance/ExcusedTime';
import SessionManage from '../components/attendance/SessionManage';
import { attendanceList } from '../utils/attendanceList';
import { normalizeSessionTitle } from '../utils/normalizeSessionTitle';

import { useAuthGuard } from '../hooks/useAuthGuard';
import { useCheckIn } from '../hooks/useCheckIn';
Expand All @@ -17,6 +18,29 @@ const Attendance = () => {
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);

const sessionOptions = useMemo(() => {
const uniqueSessionTitles = new Set();

attendanceSessions.forEach((session) => {
uniqueSessionTitles.add(normalizeSessionTitle(session?.sessionTitle));
});

return Array.from(uniqueSessionTitles);
}, [attendanceSessions]);

useEffect(() => {
if (sessionOptions.length === 0) {
if (selectedSession !== '') {
setSelectedSession('');
}
return;
}

if (!selectedSession || !sessionOptions.includes(selectedSession)) {
setSelectedSession(sessionOptions[0]);
}
}, [selectedSession, sessionOptions]);

useEffect(() => {
const fetchAttendance = async () => {
try {
Expand All @@ -40,7 +64,7 @@ const Attendance = () => {
sessions={attendanceSessions}
selectedSession={selectedSession}
onChange={setSelectedSession}
disabled={loading || !!error}
disabled={loading || !!error || sessionOptions.length === 0}
/>
{/* <ExcusedTime /> */}
</div>
Expand Down
64 changes: 62 additions & 2 deletions frontend/src/utils/attendanceList.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,70 @@
import { api } from './axios.js';
import { getAttendanceSessions, getRounds } from './attendanceManage';

const getSessionId = (session) =>
session?.sessionId || session?.attendanceSessionId || session?.id || null;

const getSessionTitle = (session) =>
session?.session?.title || session?.title || session?.sessionTitle || '';

const buildRoundMetaMap = async () => {
const sessions = await getAttendanceSessions();
if (!Array.isArray(sessions) || sessions.length === 0) return new Map();

const perSessionRoundMeta = await Promise.all(
sessions.map(async (session) => {
const sessionId = getSessionId(session);
if (!sessionId) return [];

try {
const rounds = await getRounds(sessionId);
if (!Array.isArray(rounds)) return [];

const sessionTitle = getSessionTitle(session);
return rounds
.filter((round) => !!round?.roundId)
.map((round) => [
round.roundId,
{
sessionTitle,
roundDate: round.roundDate,
roundStartAt: round.startAt || round.roundStartAt,
},
]);
} catch {
return [];
}
}),
);

return new Map(perSessionRoundMeta.flat());
};

export const attendanceList = async () => {
try {
const res = await api.get('/api/attendance/me');
// console.log('API BASE URL:', import.meta.env.VITE_API_URL);
return res.data;

const records = Array.isArray(res.data) ? res.data : [];
if (records.length === 0) return records;

try {
const roundMetaMap = await buildRoundMetaMap();

return records.map((record) => {
const roundMeta = roundMetaMap.get(record?.roundId);
if (!roundMeta) return record;

return {
...record,
sessionTitle: record?.sessionTitle || roundMeta.sessionTitle,
roundDate: record?.roundDate || roundMeta.roundDate,
roundStartAt: record?.roundStartAt || roundMeta.roundStartAt,
};
});
} catch {
// Keep attendance view available even if metadata enrichment fails.
return records;
}
} catch (err) {
console.error('출석 세션 불러오기 중 오류 발생: ', err);
throw err;
Expand Down
10 changes: 10 additions & 0 deletions frontend/src/utils/normalizeSessionTitle.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
const SESSION_TITLE_FALLBACK = '기타';

export const normalizeSessionTitle = (sessionTitle) => {
if (typeof sessionTitle !== 'string') return SESSION_TITLE_FALLBACK;

const normalizedTitle = sessionTitle.trim();
return normalizedTitle !== '' ? normalizedTitle : SESSION_TITLE_FALLBACK;
};

export const getSessionTitleFallback = () => SESSION_TITLE_FALLBACK;