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
5 changes: 5 additions & 0 deletions frontend/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ import SignUp from './pages/SignUp';
import QuantTradingDashboard from './pages/QuantTradingDashboard';
import BacktestResult from './pages/BacktestResult.jsx';

import CheckInPage from './components/attendancemanage/qrmanagement/CheckInPage.jsx';
import QrRenderPage from './components/attendancemanage/qrmanagement/QrRenderPage.jsx';

import OAuthSuccess from './pages/OAuthSuccess.jsx';

import Main from './pages/external/Main.jsx';
Expand Down Expand Up @@ -62,6 +65,8 @@ function App() {
<Route path="/backtest" element={<BackTest />} />
<Route path="/backtest/result" element={<BacktestResult />} />
<Route path="/mypage" element={<Mypage />} />
<Route path="/attendance/admin/qr" element={<QrRenderPage />} />
<Route path="/attendance/check-in" element={<CheckInPage />} />
</Route>
</Route>
</Routes>
Expand Down
107 changes: 70 additions & 37 deletions frontend/src/components/attendancemanage/RoundDayPicker.jsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
import { useState, useEffect } from 'react';
import styles from '../VerificationModal.module.css';

import { DayPicker } from 'react-day-picker';
import 'react-day-picker/style.css';
import { useAttendance } from '../../contexts/AttendanceContext';

const RoundDayPicker = () => {
const { sessions, selectedSessionId, handleAddRounds, closeAddRoundsModal } =
const { selectedSessionId, handleAddRounds, closeAddRoundsModal } =
useAttendance();

const [selectedRounds, setSelectedRounds] = useState([]);
const [selectedDate, setSelectedDate] = useState();
const [roundName, setRoundName] = useState('');
const [startTime, setStartTime] = useState('');
const [endTime, setEndTime] = useState('');
const [locationName, setLocationName] = useState('');

const today = new Date();

// ESC 키로 모달을 닫는 기능
useEffect(() => {
const handleKeyDown = (event) => {
if (event.key === 'Escape') {
Expand All @@ -21,44 +24,49 @@ const RoundDayPicker = () => {
};

document.addEventListener('keydown', handleKeyDown);

return () => {
document.removeEventListener('keydown', handleKeyDown);
};
}, [closeAddRoundsModal]);

const handleComplete = () => {
const currentSession = sessions.find(
(s) => s.attendanceSessionId === selectedSessionId
);

if (!currentSession) {
const handleComplete = async () => {
if (!selectedSessionId) {
alert('세션을 먼저 선택해주세요.');
return;
}
if (selectedRounds.length === 0) {
alert('추가할 날짜를 1개 이상 선택해주세요.');

if (
!selectedDate ||
!roundName ||
!startTime ||
!endTime ||
!locationName
) {
alert('모든 항목을 입력해주세요.');
return;
}

const newRounds = selectedRounds.map((date) => {
const timeZoneOffset = date.getTimezoneOffset() * 60000;
const dateWithoutOffset = new Date(date.getTime() - timeZoneOffset);
const dateString = dateWithoutOffset.toISOString().split('T')[0];
// 날짜 문자열 (YYYY-MM-DD)
const roundDate = selectedDate.toLocaleDateString('sv-SE');

return {
// id: `round-${uuid()}`,
roundDate: dateString,
startTime: currentSession.defaultStartTime,
availableMinutes: currentSession.defaultAvailableMinutes,
// status: 'opened',
// participants: [],
};
});
const startAt = `${roundDate}T${startTime}:00`;
const closeAt = `${roundDate}T${endTime}:00`;

handleAddRounds(selectedSessionId, newRounds);
const newRound = {
roundDate,
startAt,
closeAt,
roundName,
locationName,
};

closeAddRoundsModal();
try {
await handleAddRounds(selectedSessionId, [newRound]);
console.log('새로운 라운드 데이터:', newRound);
closeAddRoundsModal();
} catch (err) {
alert('라운드 생성에 실패했습니다.');
}
};

return (
Expand All @@ -69,23 +77,48 @@ const RoundDayPicker = () => {
<button
type="button"
className={styles.closeButton}
onClick={() => {
closeAddRoundsModal();
}}
onClick={closeAddRoundsModal}
>
&times;
</button>
</div>

<DayPicker
animate
mode="multiple"
mode="single"
disabled={{ before: today }}
selected={selectedRounds}
onSelect={setSelectedRounds}
selected={selectedDate}
onSelect={setSelectedDate}
/>

<div style={{ marginTop: '20px' }}>
<input
type="text"
placeholder="라운드 이름"
value={roundName}
onChange={(e) => setRoundName(e.target.value)}
/>

<input
type="time"
value={startTime}
onChange={(e) => setStartTime(e.target.value)}
/>

<input
type="time"
value={endTime}
onChange={(e) => setEndTime(e.target.value)}
/>

<input
type="text"
placeholder="장소 이름"
value={locationName}
onChange={(e) => setLocationName(e.target.value)}
/>
</div>

<hr />
<p>세션에 추가하고 싶은 날짜를 선택하세요.</p>
<p>(출석 시작 시간 & 인정 시간은 세션의 디폴트 값으로 설정됨)</p>
<div className={styles.modifyButtonGroup}>
<button
className={`${styles.button} ${styles.submitButton}`}
Expand Down
120 changes: 80 additions & 40 deletions frontend/src/components/attendancemanage/SessionManagementCard.jsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import styles from './SessionManagementCard.module.css';
import calendarAddIcon from '../../assets/calendar-icon.svg';

import { getAttendanceSessions } from '../../utils/attendanceManage';
import { useEffect, useState } from 'react';
import { useContext, useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useAttendance } from '../../contexts/AttendanceContext';
import { getRounds, addRound } from '../../utils/attendanceManage';
import RoundDayPicker from './RoundDayPicker';

// 날짜 포맷 함수
const formatDate = (dateStr) => {
Expand All @@ -15,39 +17,55 @@ const formatDate = (dateStr) => {
};

const SessionManagementCard = ({ styles: commonStyles }) => {
const [sessionList, setSessionList] = useState([]); // 세션 목록
const [selectedSessionId, setSelectedSessionId] = useState(''); // 선택된 세션
const [currentDisplayedRounds, setCurrentDisplayedRounds] = useState([]); // 나중에 API 연결
const {
sessions,
roundsVersion,
handleAddRounds,
openAddRoundsModal,
selectedSessionId,
setSelectedSessionId,
} = useAttendance();
const [currentDisplayedRounds, setCurrentDisplayedRounds] = useState([]);

const sessionList = sessions || [];

const currentSession = sessionList.find(
(session) => String(session.sessionId) === String(selectedSessionId)
);

// 최초 렌더: 세션 목록 불러오기
useEffect(() => {
const fetchSessions = async () => {
const fetchRounds = async () => {
if (!selectedSessionId) {
setCurrentDisplayedRounds([]);
return;
}

try {
const sessions = await getAttendanceSessions();
setSessionList(sessions || []);
const rounds = await getRounds(selectedSessionId);
setCurrentDisplayedRounds(rounds || []);
} catch (e) {
toast.error('세션 목록을 불러오지 못했습니다.');
setSessionList([]);
toast.error('라운드를 불러오지 못했습니다.');
setCurrentDisplayedRounds([]);
}
};
fetchSessions();
}, []);

// 현재 선택된 세션
const currentSession = sessionList.find(
(session) => session.attendanceSessionId === selectedSessionId
);
fetchRounds();
}, [selectedSessionId, roundsVersion]);

// *라운드 조회 API 연결필요
useEffect(() => {
const handleAddRound = async (newRoundData) => {
if (!selectedSessionId) {
setCurrentDisplayedRounds([]);
toast.error('세션을 먼저 선택해주세요.');
return;
}

// TODO: 추후 getRounds(selectedSessionId) 연결
setCurrentDisplayedRounds([]); // 임시
}, [selectedSessionId]);
try {
await handleAddRounds(selectedSessionId, [newRoundData]);

toast.success('라운드가 추가되었습니다.');
} catch (err) {
toast.error('라운드 추가에 실패했습니다.');
}
};

return (
<div className={styles.sessionManagementCardContainer}>
Expand All @@ -62,7 +80,7 @@ const SessionManagementCard = ({ styles: commonStyles }) => {
toast.error('세션을 먼저 선택해주세요.');
return;
}
// openAddRoundsModal();
openAddRoundsModal();
}}
>
<div className={commonStyles.iconGroup}>
Expand All @@ -76,20 +94,16 @@ const SessionManagementCard = ({ styles: commonStyles }) => {
{/*세션 선택 드롭다운 */}
<div className={styles.selectGroup}>
<select
id="sessionSelect"
className={styles.sessionSelect}
value={selectedSessionId}
onChange={(e) => setSelectedSessionId(e.target.value)}
>
<option value="" disabled>
------ 세션을 선택하세요 ------
</option>

{sessionList.map((session) => (
<option
key={session.attendanceSessionId}
value={session.attendanceSessionId}
>
{session.title}
<option key={session.sessionId} value={session.sessionId}>
{session.session.title}
</option>
))}
</select>
Expand All @@ -104,21 +118,47 @@ const SessionManagementCard = ({ styles: commonStyles }) => {
<th>시간</th>
<th>가능(분)</th>
<th>회차</th>
<th>QR 코드</th>
</tr>
</thead>
<tbody>
{currentDisplayedRounds.length > 0 ? (
currentDisplayedRounds.map((round, index) => (
<tr key={round.id}>
<td>{formatDate(round.date)}</td>
<td>{round.startTime?.substring(0, 5)}</td>
<td>{round.availableMinutes}</td>
<td>{index + 1}</td>
</tr>
))
currentDisplayedRounds.map((round, index) => {
const startTime = new Date(round.startAt);
const closeTime = new Date(round.closeAt);

const minutes = Math.floor((closeTime - startTime) / 60000);

return (
<tr key={round.roundId}>
<td>{formatDate(round.roundDate)}</td>
<td>
{startTime.toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit',
})}
</td>
<td>{minutes}</td>
<td>{index + 1}</td>
<td>
<button
className={styles.qrButton}
onClick={() =>
window.open(
`/attendance/admin/qr?roundId=${round.roundId}`,
'_blank'
)
}
>
QR 생성
</button>
</td>
</tr>
);
})
) : (
<tr>
<td colSpan="4" className={styles.noData}>
<td colSpan="5" className={styles.noData}>
회차 정보가 없습니다.
</td>
</tr>
Expand Down
Loading