From 1d89fb2b40af8d9456e167e6f468fd11891c5735 Mon Sep 17 00:00:00 2001 From: GGGGGangSub Date: Sat, 29 Nov 2025 02:57:15 +0900 Subject: [PATCH 1/4] =?UTF-8?q?fix:=20CSS=EB=94=94=EC=9E=90=EC=9D=B8=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=EB=B0=8F=20=EC=9C=A0=EC=A0=80=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20api=EB=A5=BC=20=EC=A0=9C=EC=99=B8=ED=95=98=EA=B3=A0?= =?UTF-8?q?=20=EB=AA=A8=EB=91=90=20=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/assets/calendar-icon.svg | 3 + .../AttendanceManagementCard.module.css | 23 +- .../attendancemanage/RoundDayPicker.jsx | 12 +- .../attendancemanage/RoundModifyModal.jsx | 11 +- .../SessionManagementCard.jsx | 44 ++- .../SessionManagementCard.module.css | 24 +- .../attendancemanage/SessionModifyModal.jsx | 13 +- .../SessionSettingCard.module.css | 14 +- frontend/src/contexts/AttendanceContext.jsx | 354 +++++++++++------- frontend/src/pages/AttendanceManage.jsx | 37 ++ .../src/pages/AttendanceManage.module.css | 57 ++- frontend/src/utils/attendanceManage.js | 143 +++++++ 12 files changed, 557 insertions(+), 178 deletions(-) create mode 100644 frontend/src/assets/calendar-icon.svg create mode 100644 frontend/src/utils/attendanceManage.js diff --git a/frontend/src/assets/calendar-icon.svg b/frontend/src/assets/calendar-icon.svg new file mode 100644 index 00000000..c9c6d563 --- /dev/null +++ b/frontend/src/assets/calendar-icon.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/frontend/src/components/attendancemanage/AttendanceManagementCard.module.css b/frontend/src/components/attendancemanage/AttendanceManagementCard.module.css index d692850d..f60e7c66 100644 --- a/frontend/src/components/attendancemanage/AttendanceManagementCard.module.css +++ b/frontend/src/components/attendancemanage/AttendanceManagementCard.module.css @@ -1,9 +1,9 @@ .attendanceManagementCardContainer { border-radius: 12px; - min-width: 300px; - max-width: 335px; + width: 100%; background: #fafbfc; - padding: 32px 20px; + padding: 26px 40px; + border: 1px solid #c9bebe; } .tableGroup { @@ -26,7 +26,18 @@ border-radius: 4px; border: 0.8px solid #afafaf; } -.table thead th, +.table thead th { + text-align: left; + color: var(--text); + padding: 8px 7px; + border-bottom: 1px solid var(--line); + white-space: nowrap; + font-family: Inter; + font-size: 20px; + font-style: normal; + font-weight: 400; + line-height: 100%; +} .table tbody td { text-align: left; color: var(--text); @@ -34,7 +45,7 @@ border-bottom: 1px solid var(--line); white-space: nowrap; font-family: Inter; - font-size: 16px; + font-size: 18px; font-style: normal; font-weight: 400; line-height: 100%; @@ -47,4 +58,4 @@ text-align: center; padding: 20px; color: #888; -} \ No newline at end of file +} diff --git a/frontend/src/components/attendancemanage/RoundDayPicker.jsx b/frontend/src/components/attendancemanage/RoundDayPicker.jsx index e142129a..d0e66f4f 100644 --- a/frontend/src/components/attendancemanage/RoundDayPicker.jsx +++ b/frontend/src/components/attendancemanage/RoundDayPicker.jsx @@ -29,7 +29,9 @@ const RoundDayPicker = () => { }, [closeAddRoundsModal]); const handleComplete = () => { - const currentSession = sessions.find((s) => s.id === selectedSessionId); + const currentSession = sessions.find( + (s) => s.attendanceSessionId === selectedSessionId + ); if (!currentSession) { alert('세션을 먼저 선택해주세요.'); @@ -46,12 +48,12 @@ const RoundDayPicker = () => { const dateString = dateWithoutOffset.toISOString().split('T')[0]; return { - id: `round-${uuid()}`, - date: dateString, + // id: `round-${uuid()}`, + roundDate: dateString, startTime: currentSession.defaultStartTime, availableMinutes: currentSession.defaultAvailableMinutes, - status: 'opened', - participants: [], + // status: 'opened', + // participants: [], }; }); diff --git a/frontend/src/components/attendancemanage/RoundModifyModal.jsx b/frontend/src/components/attendancemanage/RoundModifyModal.jsx index e3b4652f..dbfd3558 100644 --- a/frontend/src/components/attendancemanage/RoundModifyModal.jsx +++ b/frontend/src/components/attendancemanage/RoundModifyModal.jsx @@ -6,6 +6,7 @@ import ConfirmationToast from './ConfirmationToast'; const RoundModifyModal = ({ styles: commonStyles, onClose, + sessionId, round, onSave, onDelete, @@ -80,10 +81,12 @@ const RoundModifyModal = ({ if (!isFormValid(hour, minute, second, availableMinute)) return; // 상위 컴포넌트로 업데이트된 회차 데이터 전달 - onSave({ - id: round.id, + onSave(round.id, { + // roundId: round.id, + sessionId: sessionId, + roundDate: round.date, startTime: `${hh.padStart(2, '0')}:${mm.padStart(2, '0')}:${ss.padStart(2, '0')}`, - availableMinutes: availableMinute, + allowedMinutes: availableMinute, }); onClose(); @@ -127,7 +130,7 @@ const RoundModifyModal = ({
-
+
diff --git a/frontend/src/components/attendancemanage/SessionManagementCard.jsx b/frontend/src/components/attendancemanage/SessionManagementCard.jsx index 58a096cb..cd91217c 100644 --- a/frontend/src/components/attendancemanage/SessionManagementCard.jsx +++ b/frontend/src/components/attendancemanage/SessionManagementCard.jsx @@ -1,6 +1,9 @@ import { useAttendance } from '../../contexts/AttendanceContext'; import styles from './SessionManagementCard.module.css'; -import calendarAddIcon from '../../assets/calendar-add-icon.svg'; +import calendarAddIcon from '../../assets/calendar-icon.svg'; + +import { getRounds } from '../../utils/attendanceManage'; +import { useEffect, useState } from 'react'; const SessionManagementCard = ({ styles: commonStyles }) => { const { @@ -14,12 +17,33 @@ const SessionManagementCard = ({ styles: commonStyles }) => { openRoundModifyModal, openSessionModifyModal, openAddRoundsModal, + roundsVersion, } = useAttendance(); + const [currentDisplayedRounds, setCurrentDisplayedRounds] = useState([]); + const currentSession = sessions.find( - (session) => session.id === selectedSessionId + (session) => session.attendanceSessionId === selectedSessionId ); - const currentDisplayedRounds = currentSession ? currentSession.rounds : []; + + useEffect(() => { + const fetchRounds = async () => { + if (selectedSessionId) { + setCurrentDisplayedRounds([]); + const rounds = await getRounds(selectedSessionId); + + const sortedRounds = (rounds || []).sort( + (a, b) => + new Date(`${a.date}T${a.startTime}`) - + new Date(`${b.date}T${b.startTime}`) + ); + setCurrentDisplayedRounds(sortedRounds); + } else { + setCurrentDisplayedRounds([]); + } + }; + fetchRounds(); + }, [selectedSessionId, roundsVersion]); return (
@@ -36,7 +60,10 @@ const SessionManagementCard = ({ styles: commonStyles }) => { openAddRoundsModal(); }} > - 회차 추가 +
+ 회차 추가 +
출석일자 추가
+
@@ -50,10 +77,13 @@ const SessionManagementCard = ({ styles: commonStyles }) => { }} > {sessions.map((session) => ( - ))} @@ -80,6 +110,7 @@ const SessionManagementCard = ({ styles: commonStyles }) => { 일자 시간 가능(분) + 회차 메뉴 @@ -95,6 +126,7 @@ const SessionManagementCard = ({ styles: commonStyles }) => { {round.date} {round.startTime} {round.availableMinutes} + 1
-
+
diff --git a/frontend/src/components/attendancemanage/SessionSettingCard.module.css b/frontend/src/components/attendancemanage/SessionSettingCard.module.css index 8e0b5d91..974feed4 100644 --- a/frontend/src/components/attendancemanage/SessionSettingCard.module.css +++ b/frontend/src/components/attendancemanage/SessionSettingCard.module.css @@ -10,20 +10,23 @@ .SessionSettingCardContainer { border-radius: 12px; background: #fafbfc; - width: 335px; + width: 100%; + border: 1px solid #c9bebe; flex-shrink: 0; - padding: 32px 20px; + padding: 26px 40px; } .form { - display: grid; + display: flex; + gap: 20px; } .timeInputGroup { display: flex; - width: 100%; + width: auto; justify-content: space-between; + gap: 10px; } .timeInputGroup > input { width: var(--ctrl-w); @@ -31,8 +34,9 @@ .availableTimeInputGroup { display: grid; + width: auto; grid-template-columns: var(--ctrl-w) auto; - column-gap: 12px; + column-gap: 36px; align-items: center; justify-content: space-between; } diff --git a/frontend/src/contexts/AttendanceContext.jsx b/frontend/src/contexts/AttendanceContext.jsx index e395ca57..cb407dc0 100644 --- a/frontend/src/contexts/AttendanceContext.jsx +++ b/frontend/src/contexts/AttendanceContext.jsx @@ -1,55 +1,70 @@ -import { createContext, useContext, useState } from 'react'; +import { + createContext, + useCallback, + useContext, + useEffect, + useState, +} from 'react'; import { useImmer } from 'use-immer'; -import { v4 as uuid } from 'uuid'; +import { + addRound, + changeRoundData, + changeSessionData, + createAttendanceSession, + deleteRound, + deleteSession, + getAttendanceSessions, + getRounds, +} from '../utils/attendanceManage'; const AttendanceContext = createContext(null); // 세션 목 데이터 const sessionData = [ - { - id: 'session-1', - title: '금융 IT팀 세션', - // 세션의 기본 위치 정보 - location: { - lat: 37.5499, - lng: 127.0751, - }, - defaultStartTime: '18:30:00', // 세션의 기본 시간 설정 - defaultAvailableMinutes: 30, // 출석 인정 시간 (분 단위) - rewardPoints: 100, // 세션의 리워드 - isVisible: true, // 세션 공개 여부 - // 세션 회차들 - rounds: [ - { - id: 'round-1', - date: '2025-11-06', - startTime: '10:00:00', - availableMinutes: 20, - status: 'opened', - participants: [ - { memberId: 'member-1', name: '김민준', attendance: '출석' }, - { memberId: 'member-2', name: '이서연', attendance: '결석' }, - { memberId: 'member-3', name: '박도윤', attendance: '출석' }, - ], - }, - { - id: 'round-2', - date: '2025-11-06', - startTime: '11:00:00', - availableMinutes: 30, - status: 'opened', - participants: [ - { memberId: 'member-1', name: '김민준', attendance: '출석' }, - { memberId: 'member-2', name: '이서연', attendance: '출석' }, - { memberId: 'member-3', name: '박도윤', attendance: '결석' }, - ], - }, - ], - }, + // { + // id: 'session-1', + // title: '금융 IT팀 세션', + // // 세션의 기본 위치 정보 + // location: { + // lat: 37.5499, + // lng: 127.0751, + // }, + // defaultStartTime: '18:30:00', // 세션의 기본 시간 설정 + // defaultAvailableMinutes: 30, // 출석 인정 시간 (분 단위) + // rewardPoints: 100, // 세션의 리워드 + // isVisible: true, // 세션 공개 여부 + // // 세션 회차들 + // rounds: [ + // { + // id: 'round-1', + // date: '2025-11-06', + // startTime: '10:00:00', + // availableMinutes: 20, + // status: 'opened', + // participants: [ + // { memberId: 'member-1', name: '김민준', attendance: '출석' }, + // { memberId: 'member-2', name: '이서연', attendance: '결석' }, + // { memberId: 'member-3', name: '박도윤', attendance: '출석' }, + // ], + // }, + // { + // id: 'round-2', + // date: '2025-11-06', + // startTime: '11:00:00', + // availableMinutes: 30, + // status: 'opened', + // participants: [ + // { memberId: 'member-1', name: '김민준', attendance: '출석' }, + // { memberId: 'member-2', name: '이서연', attendance: '출석' }, + // { memberId: 'member-3', name: '박도윤', attendance: '결석' }, + // ], + // }, + // ], + // }, ]; export const AttendanceProvider = ({ children }) => { - const [sessions, setSessions] = useImmer(sessionData); + const [sessions, setSessions] = useImmer([]); const [selectedSessionId, setSelectedSessionId] = useState(null); const [selectedRound, setSelectedRound] = useState(null); const [editingRound, setEditingRound] = useState(null); @@ -59,6 +74,24 @@ export const AttendanceProvider = ({ children }) => { const [isAddRoundsModalOpen, setAddRoundsModalOpen] = useState(false); + const [sessionsVersion, setSessionsVersion] = useState(0); + const [roundsVersion, setRoundsVersion] = useState(0); + + // 최초, setSessions가 호출될때마다 모든 세션 불러오기 + + useEffect(() => { + const fetchSessions = async () => { + try { + const sessionData = await getAttendanceSessions(); + setSessions(sessionData); + } catch (error) { + console.error('모든 세션 데이터를 가져오는 데 실패했습니다: ', error); + setSessions([]); + } + }; + fetchSessions(); + }, [setSessions]); + const handleAttendanceChange = (memberId, newAttendance) => { setSessions((draft) => { const session = draft.find((s) => s.id === selectedSessionId); @@ -74,35 +107,52 @@ export const AttendanceProvider = ({ children }) => { }); }; - const handleRoundChange = (updateRoundData) => { - setSessions((draft) => { - const session = draft.find((s) => s.id === selectedSessionId); - if (!session) return; - const round = session.rounds.find((r) => r.id === updateRoundData.id); - if (round) { - round.startTime = updateRoundData.startTime; - round.availableMinutes = updateRoundData.availableMinutes; - } - // 회차 수정 후 정렬 - if (session.rounds) { - session.rounds.sort( - (a, b) => - new Date(`${a.date}T${a.startTime}`) - - new Date(`${b.date}T${b.startTime}`) - ); - } - }); + const handleRoundChange = async (roundId, updateRoundData) => { + // setSessions((draft) => { + // const session = draft.find((s) => s.id === selectedSessionId); + // if (!session) return; + // const round = session.rounds.find((r) => r.id === updateRoundData.id); + // if (round) { + // round.startTime = updateRoundData.startTime; + // round.availableMinutes = updateRoundData.availableMinutes; + // } + // // 회차 수정 후 정렬 + // if (session.rounds) { + // session.rounds.sort( + // (a, b) => + // new Date(`${a.date}T${a.startTime}`) - + // new Date(`${b.date}T${b.startTime}`) + // ); + // } + // }); + + try { + await changeRoundData(roundId, updateRoundData); + + setRoundsVersion((v) => v + 1); + } catch (error) { + console.error('회차 수정에 실패했습니다. ', error); + } }; - const handleSessionChange = (updateSessionData) => { - setSessions((draft) => { - const session = draft.find((s) => s.id === updateSessionData.id); - if (session) { - session.defaultStartTime = updateSessionData.defaultStartTime; - session.defaultAvailableMinutes = - updateSessionData.defaultAvailableMinutes; - } - }); + const handleSessionChange = async (updateSessionData) => { + // setSessions((draft) => { + // const session = draft.find((s) => s.id === updateSessionData.id); + // if (session) { + // session.defaultStartTime = updateSessionData.defaultStartTime; + // session.defaultAvailableMinutes = + // updateSessionData.defaultAvailableMinutes; + // } + // }); + + try { + await changeSessionData(updateSessionData); + + const updatedSessions = await getAttendanceSessions(); + setSessions(updatedSessions || []); + } catch (error) { + console.error('세션 수정에 실패했습니다. ', error); + } }; const openRoundModifyModal = () => setRoundModifyModalOpen(true); @@ -115,88 +165,133 @@ export const AttendanceProvider = ({ children }) => { const closeAddRoundsModal = () => setAddRoundsModalOpen(false); - const handleAddSession = (sessionTitle, roundDetails) => { + const handleAddSession = async (sessionTitle, sessionDetails) => { const newSession = { - id: `session-${uuid()}`, + // id: `session-${uuid()}`, title: sessionTitle, - defaultStartTime: `${roundDetails.hh}:${roundDetails.mm}:${roundDetails.ss}`, - defaultAvailableMinutes: parseInt(roundDetails.availableTimeMm, 10), + defaultStartTime: `${sessionDetails.hh}:${sessionDetails.mm}:${sessionDetails.ss}`, + // defaultAvailableMinutes: parseInt(roundDetails.availableTimeMm, 10), + allowedMinutes: sessionDetails.availableTimeMm, // 최소 5분 이상이여야 함 rewardPoints: 100, - isVisible: true, - rounds: [ - // { - // id: `round-${uuid()}`, - // date: new Date().toISOString().slice(0, 10), - // startTime: `${roundDetails.hh}:${roundDetails.mm}:${roundDetails.ss}`, - // availableMinutes: parseInt(roundDetails.availableTimeMm, 10), - // status: 'opened', - // participants: [], - // }, - ], + // 위도, 경도, 범위 미터는 임시로 지정 + latitude: 1, + longitude: 2, + radiusMeters: 3, + // isVisible: true, + // rounds: [ + // { + // id: `round-${uuid()}`, + // date: new Date().toISOString().slice(0, 10), + // startTime: `${roundDetails.hh}:${roundDetails.mm}:${roundDetails.ss}`, + // availableMinutes: parseInt(roundDetails.availableTimeMm, 10), + // status: 'opened', + // participants: [], + // }, + // ], }; - setSessions((draft) => { - draft.push(newSession); - }); - }; - const handleAddRounds = (sessionId, newRounds) => { - setSessions((draft) => { - const session = draft.find((session) => session.id === sessionId); - - if (session) { - session.rounds.push(...newRounds); - // 회차 추가 후 정렬 - session.rounds.sort( - (a, b) => - new Date(`${a.date}T${a.startTime}`) - - new Date(`${b.date}T${b.startTime}`) - ); - } - }); + // setSessions((draft) => { + // draft.push(newSession); + // }); + + try { + await createAttendanceSession(newSession); + + const updatedSessions = await getAttendanceSessions(); + setSessions(updatedSessions || []); + } catch (error) { + console.error('세션 추가에 실패했습니다. ', error); + } }; - const handleDeleteSession = (sessionId) => { - setSessions((draft) => { - const sessionIndex = draft.findIndex((session) => { - return session.id === sessionId; + const handleAddRounds = async (sessionId, newRounds) => { + // setSessions((draft) => { + // const session = draft.find((session) => session.id === sessionId); + // if (session) { + // session.rounds.push(...newRounds); + // // 회차 추가 후 정렬 + // session.rounds.sort( + // (a, b) => + // new Date(`${a.date}T${a.startTime}`) - + // new Date(`${b.date}T${b.startTime}`) + // ); + // } + // }); + try { + const addRoundsPromises = newRounds.map((newRound) => { + return addRound(sessionId, newRound); }); - if (sessionIndex !== -1) { - draft.splice(sessionIndex, 1); + await Promise.all(addRoundsPromises); + setRoundsVersion((v) => v + 1); + } catch (error) { + console.error('회차 추가에 실패했습니다. ', error); + } + }; + + const handleDeleteSession = async (sessionId) => { + // setSessions((draft) => { + // const sessionIndex = draft.findIndex((session) => { + // return session.id === sessionId; + // }); + // if (sessionIndex !== -1) { + // draft.splice(sessionIndex, 1); + // } + // }); + + // 세션 삭제 시 먼저 해당 세션의 회차들 삭제 + const roundsToDelete = await getRounds(sessionId); + if (roundsToDelete && roundsToDelete.length > 0) { + for (const round of roundsToDelete) { + await deleteRound(round.id); } - }); + } + // 세션 삭제 + await deleteSession(sessionId); + + const updatedSessions = await getAttendanceSessions(); + setSessions(updatedSessions || []); // 세션 선택 초기화 setSelectedSessionId(null); setSelectedRound(null); }; - const handleDeleteRound = (roundId) => { - setSessions((draft) => { - const session = draft.find((session) => { - return session.id === selectedSessionId; - }); - if (session) { - const roundIndex = session.rounds.findIndex( - (round) => round.id === roundId - ); - if (roundIndex !== -1) { - session.rounds.splice(roundIndex, 1); - } - } - }); + const handleDeleteRound = async (roundId) => { + // setSessions((draft) => { + // const session = draft.find((session) => { + // return session.id === selectedSessionId; + // }); + + // if (session) { + // const roundIndex = session.rounds.findIndex( + // (round) => round.id === roundId + // ); + // if (roundIndex !== -1) { + // session.rounds.splice(roundIndex, 1); + // } + // } + // }); + + try { + await deleteRound(roundId); + + setRoundsVersion((v) => v + 1); + } catch (error) { + console.error('회차 삭제에 실패했습니다. ', error); + } // 회차 선택 초기화 setSelectedRound(null); }; const selectedSession = sessions.find( - (session) => session.id === selectedSessionId + (session) => session.attendanceSessionId === selectedSessionId ); - const selectedRoundData = selectedSession?.rounds.find( - (round) => round.id === selectedRound - ); + // const selectedRoundData = selectedSession?.rounds.find( + // (round) => round.roundId === selectedRound + // ); - const participants = selectedRoundData?.participants || []; + const participants = []; // 공유할 값들을 객체로 묶기 const value = { @@ -226,6 +321,7 @@ export const AttendanceProvider = ({ children }) => { isAddRoundsModalOpen, openAddRoundsModal, closeAddRoundsModal, + roundsVersion, }; return ( diff --git a/frontend/src/pages/AttendanceManage.jsx b/frontend/src/pages/AttendanceManage.jsx index 002465dd..5025a670 100644 --- a/frontend/src/pages/AttendanceManage.jsx +++ b/frontend/src/pages/AttendanceManage.jsx @@ -12,9 +12,19 @@ import SessionModifyModal from '../components/attendancemanage/SessionModifyModa import RoundDayPicker from '../components/attendancemanage/RoundDayPicker'; import { ToastContainer } from 'react-toastify'; +import { + getAttendanceSessions, + deleteSession, + addRound, + deleteRound, + getRounds, + addUser, + getUsers, +} from '../utils/attendanceManage'; const AttendanceContent = () => { const { + sessions, isRoundModifyModalOpen, editingRound, closeRoundModifyModal, @@ -26,10 +36,36 @@ const AttendanceContent = () => { handleDeleteSession, handleDeleteRound, isAddRoundsModalOpen, + selectedSessionId, } = useAttendance(); return ( <> + + + +
@@ -42,6 +78,7 @@ const AttendanceContent = () => { { +// const now = new Date(); + +// const year = now.getFullYear(); +// const month = String(now.getMonth() + 1).padStart(2, '0'); +// const day = String(now.getDate()).padStart(2, '0'); +// const hours = String(now.getHours()).padStart(2, '0'); +// const minutes = String(now.getMinutes()).padStart(2, '0'); +// const seconds = String(now.getSeconds()).padStart(2, '0'); + +// return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}`; +// }; + +// 모든 세션 조회 +export const getAttendanceSessions = async () => { + try { + const res = await api.get('/api/attendance/sessions'); + return res.data; + } catch (err) { + console.error('출석 세션 불러오기 중 오류 발생: ', err); + throw err; + } +}; + +// 세션 생성 및 추가 +export const createAttendanceSession = async (sessionData) => { + try { + const res = await api.post('/api/attendance/sessions', sessionData); + return res.data; + } catch (err) { + console.error('출석 세션 생성 중 오류 발생: ', err); + throw err; + } +}; + +// 특정 세션 삭제 +export const deleteSession = async (sessionId) => { + try { + const res = await api.delete(`/api/attendance/sessions/${sessionId}`); + return res.data; + } catch (err) { + console.error('세션 삭제 중 오류 발생', err); + throw err; + } +}; + +// 세션 아이디로 모든 회차 조회 +export const getRounds = async (sessionId) => { + try { + const res = await api.get(`/api/attendance/sessions/${sessionId}/rounds`); + return res.data; + } catch (err) { + console.error('회차 조회 중 오류 발생', err); + throw err; + } +}; + +// 회차 추가 +export const addRound = async (sessionId, newRound) => { + const paylaod = { + sessionId, + roundDate: newRound.roundDate, + startTime: newRound.startTime, + allowedMinutes: newRound.availableMinutes, + }; + + try { + const res = await api.post( + `/api/attendance/sessions/${sessionId}/rounds`, + paylaod + ); + return res.data; + } catch (err) { + console.error('회차 추가 중 오류 발생', err); + throw err; + } +}; + +// 회차 삭제 +export const deleteRound = async (roundId) => { + try { + const res = await api.delete(`/api/attendance/rounds/${roundId}`); + return res.data; + } catch (err) { + console.error('회차 삭제 중 오류 발생', err); + throw err; + } +}; + +// 세션 정보 수정 +export const changeSessionData = async (updateSessionData) => { + try { + const res = await api.put( + `/api/attendance/sessions/${updateSessionData.attendanceSessionId}`, + updateSessionData + ); + return res.data; + } catch (err) { + console.error('세션 정보 수정 중 오류 발생', err); + throw err; + } +}; + +// 회차 정보 수정 +export const changeRoundData = async (roundId, updateRoundData) => { + try { + const res = await api.put( + `/api/attendance/rounds/${roundId}`, + updateRoundData + ); + return res.data; + } catch (err) { + console.error('회차 정보 수정 중 오류 발생', err); + throw err; + } +}; + +// 세션에 유저 추가 +export const addUser = async (sessionId, userId) => { + try { + const res = await api.post(`/api/attendance/sessions/${sessionId}/users`, { + userId: userId, + }); + return res.data; + } catch (err) { + console.error('유저 추가 중 오류 발생', err); + throw err; + } +}; + +// 세션 참여자 조회 +export const getUsers = async (sessionId) => { + try { + const res = await api.get(`/api/attendance/sessions/${sessionId}/users`); + return res.data; + } catch (err) { + console.error('유저 조회 중 오류 발생', err); + throw err; + } +}; From a11c36149a9c4ea35340464d68ec214c57d4fb80 Mon Sep 17 00:00:00 2001 From: GGGGGangSub Date: Mon, 1 Dec 2025 20:25:50 +0900 Subject: [PATCH 2/4] =?UTF-8?q?feat:=20=EC=84=B8=EC=85=98=EC=97=90=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=EC=9E=90=20=EC=B6=94=EA=B0=80=20=EB=AA=A8?= =?UTF-8?q?=EB=8B=AC=20=EA=B5=AC=ED=98=84=20=EB=B0=8F=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=EC=9E=90=20=EC=83=81=ED=83=9C=EB=B3=80=EA=B2=BD=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/VerificationModal.module.css | 69 +++++++++++ .../attendancemanage/AddUsersModal.jsx | 110 ++++++++++++++++++ .../AttendanceManagementCard.jsx | 63 +++++++--- .../attendancemanage/RoundDayPicker.jsx | 1 - .../SessionManagementCard.jsx | 48 +++++--- frontend/src/contexts/AttendanceContext.jsx | 71 ++++++----- frontend/src/pages/AttendanceManage.jsx | 60 ++++++++-- .../src/pages/AttendanceManage.module.css | 2 +- frontend/src/utils/attendanceManage.js | 60 ++++++++++ 9 files changed, 420 insertions(+), 64 deletions(-) create mode 100644 frontend/src/components/attendancemanage/AddUsersModal.jsx diff --git a/frontend/src/components/VerificationModal.module.css b/frontend/src/components/VerificationModal.module.css index e4e03f3c..ab4174ef 100644 --- a/frontend/src/components/VerificationModal.module.css +++ b/frontend/src/components/VerificationModal.module.css @@ -183,3 +183,72 @@ h1 { } .modifyButtonGroup button:disabled { } + +/* 세션에 사용자 추가 모달 디자인 */ +.modalContent { + padding: 0 24px 10px 24px; +} + +.inputGroup { + display: flex; + flex-direction: column; + gap: 8px; +} + +.label { + font-size: 0.9rem; + font-weight: 600; + color: #555; +} + +.selectInput { + width: 100%; + padding: 10px; + font-size: 1rem; + border: 1px solid #ddd; + border-radius: 6px; + outline: none; + background-color: #fff; + cursor: pointer; +} + +.selectInput:focus { + border-color: #3b82f6; /* 파란색 포커스 */ +} + +/* 버튼 그룹 스타일 */ +.modifyButtonGroup { + padding: 24px; + display: flex; + justify-content: flex-end; /* 오른쪽 정렬 */ +} + +.button { + padding: 10px 20px; + font-size: 1rem; + font-weight: 600; + border-radius: 6px; + cursor: pointer; + border: none; +} + +.submitButton { + background-color: #3b82f6; /* 디자인상의 파란색 */ + color: white; + width: 100%; /* 버튼을 꽉 채우고 싶으면 width 추가, 아니면 제거 */ +} + +.submitButton:hover { + background-color: #2563eb; +} + +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(-10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} diff --git a/frontend/src/components/attendancemanage/AddUsersModal.jsx b/frontend/src/components/attendancemanage/AddUsersModal.jsx new file mode 100644 index 00000000..6819d211 --- /dev/null +++ b/frontend/src/components/attendancemanage/AddUsersModal.jsx @@ -0,0 +1,110 @@ +import { useState, useEffect } from 'react'; +import styles from '../VerificationModal.module.css'; +import { useAttendance } from '../../contexts/AttendanceContext'; +import { getUserList } from '../../utils/attendanceManage'; + +const AddUsersModal = () => { + const { sessions, selectedSessionId, handleAddUsers, closeAddUsersModal } = + useAttendance(); + const [selectedUserId, setSelectedUserId] = useState(null); + const [users, setUsers] = useState([]); + + // ESC 키로 모달 또는 토스트를 닫는 기능 + useEffect(() => { + // 유저 리스트 가져오기 + const fetchUsers = async () => { + try { + const userList = await getUserList(); + setUsers(userList); + } catch (err) { + console.error('사용자 목록을 불러오는 데 실패했습니다:', err); + } + }; + if (selectedSessionId) { + fetchUsers(); + } + + const handleKeyDown = (event) => { + if (event.key === 'Escape') { + closeAddUsersModal(); + } + }; + document.addEventListener('keydown', handleKeyDown); + return () => { + document.removeEventListener('keydown', handleKeyDown); + }; + }, [closeAddUsersModal]); + + console.log(selectedUserId); + + const handleComplete = () => { + const currentSession = sessions.find( + (s) => s.attendanceSessionId === selectedSessionId + ); + + if (!currentSession) { + alert('세션을 먼저 선택해주세요.'); + return; + } + if (!selectedUserId) { + alert('추가할 인원를 1명 이상 선택해주세요.'); + return; + } + + handleAddUsers(selectedSessionId, selectedUserId); + closeAddUsersModal(); + }; + + return ( +
+
+
+

세션에 유저 추가하기

+ +
+ +
+
+ + +
+
+ +
+ +
+
+
+ ); +}; + +export default AddUsersModal; diff --git a/frontend/src/components/attendancemanage/AttendanceManagementCard.jsx b/frontend/src/components/attendancemanage/AttendanceManagementCard.jsx index f66fbdef..33c6d96e 100644 --- a/frontend/src/components/attendancemanage/AttendanceManagementCard.jsx +++ b/frontend/src/components/attendancemanage/AttendanceManagementCard.jsx @@ -1,10 +1,44 @@ import styles from './AttendanceManagementCard.module.css'; import { useAttendance } from '../../contexts/AttendanceContext'; +import { useEffect, useState } from 'react'; +import { getRoundUserAttendance } from '../../utils/attendanceManage'; + +const attendanceEnglishToKorean = { + PRESENT: '출석', + LATE: '지각', + ABSENT: '결석', + EXCUSED: '공결', + PENDING: '미정', +}; const AttendanceManagementCard = ({ styles: commonStyles }) => { - const { selectedRound, handleAttendanceChange, participants } = - useAttendance(); + const { + selectedSessionId, + selectedRound, + handleAttendanceChange, + roundAttendanceVersion, + } = useAttendance(); + + const [users, setUsers] = useState([]); + + useEffect(() => { + const fetchUsers = async () => { + if (selectedSessionId && selectedRound) { + const userList = await getRoundUserAttendance(selectedRound); + console.log(userList); + // const sortedUsers = (userList || []).sort( + // (a, b) => + // new Date(`${a.date}T${a.startTime}`) - + // new Date(`${b.date}T${b.startTime}`) + // ); + setUsers(userList); + } else { + setUsers([]); + } + }; + fetchUsers(); + }, [selectedSessionId, selectedRound, roundAttendanceVersion]); return (
@@ -28,24 +62,25 @@ const AttendanceManagementCard = ({ styles: commonStyles }) => { 회차를 선택해주세요. - ) : participants.length > 0 ? ( - participants.map((participant) => ( - - {participant.name} - {participant.attendance} + ) : users.length > 0 ? ( + users.map((user) => ( + + {user.userName} + {attendanceEnglishToKorean[user.attendanceStatus]} - diff --git a/frontend/src/components/attendancemanage/RoundDayPicker.jsx b/frontend/src/components/attendancemanage/RoundDayPicker.jsx index d0e66f4f..20b6619b 100644 --- a/frontend/src/components/attendancemanage/RoundDayPicker.jsx +++ b/frontend/src/components/attendancemanage/RoundDayPicker.jsx @@ -3,7 +3,6 @@ import styles from '../VerificationModal.module.css'; import { DayPicker } from 'react-day-picker'; import 'react-day-picker/style.css'; -import { v4 as uuid } from 'uuid'; import { useAttendance } from '../../contexts/AttendanceContext'; const RoundDayPicker = () => { diff --git a/frontend/src/components/attendancemanage/SessionManagementCard.jsx b/frontend/src/components/attendancemanage/SessionManagementCard.jsx index cd91217c..143b0549 100644 --- a/frontend/src/components/attendancemanage/SessionManagementCard.jsx +++ b/frontend/src/components/attendancemanage/SessionManagementCard.jsx @@ -18,6 +18,7 @@ const SessionManagementCard = ({ styles: commonStyles }) => { openSessionModifyModal, openAddRoundsModal, roundsVersion, + openAddUsersModal, } = useAttendance(); const [currentDisplayedRounds, setCurrentDisplayedRounds] = useState([]); @@ -50,21 +51,38 @@ const SessionManagementCard = ({ styles: commonStyles }) => {

세션 관리

- +
+ + +
setSelectedUserId(e.target.value)} > {users && users.map((user) => ( diff --git a/frontend/src/components/attendancemanage/AttendanceManagementCard.jsx b/frontend/src/components/attendancemanage/AttendanceManagementCard.jsx index 33c6d96e..136ce105 100644 --- a/frontend/src/components/attendancemanage/AttendanceManagementCard.jsx +++ b/frontend/src/components/attendancemanage/AttendanceManagementCard.jsx @@ -26,7 +26,6 @@ const AttendanceManagementCard = ({ styles: commonStyles }) => { const fetchUsers = async () => { if (selectedSessionId && selectedRound) { const userList = await getRoundUserAttendance(selectedRound); - console.log(userList); // const sortedUsers = (userList || []).sort( // (a, b) => // new Date(`${a.date}T${a.startTime}`) - @@ -52,7 +51,7 @@ const AttendanceManagementCard = ({ styles: commonStyles }) => { 이름 상태 변경 - 횟수 + 이메일 @@ -83,7 +82,7 @@ const AttendanceManagementCard = ({ styles: commonStyles }) => { - - + {user.email} )) ) : ( diff --git a/frontend/src/components/attendancemanage/AttendanceManagementCard.module.css b/frontend/src/components/attendancemanage/AttendanceManagementCard.module.css index f60e7c66..51adb6e1 100644 --- a/frontend/src/components/attendancemanage/AttendanceManagementCard.module.css +++ b/frontend/src/components/attendancemanage/AttendanceManagementCard.module.css @@ -37,6 +37,10 @@ font-style: normal; font-weight: 400; line-height: 100%; + + position: sticky; + top: 0; + background-color: #fafbfc; } .table tbody td { text-align: left; diff --git a/frontend/src/components/attendancemanage/SessionManagementCard.module.css b/frontend/src/components/attendancemanage/SessionManagementCard.module.css index 75f6ae25..0220e8fb 100644 --- a/frontend/src/components/attendancemanage/SessionManagementCard.module.css +++ b/frontend/src/components/attendancemanage/SessionManagementCard.module.css @@ -50,6 +50,10 @@ font-style: normal; font-weight: 400; line-height: 100%; + + position: sticky; + top: 0; + background-color: #fafbfc; } .row { margin-bottom: 10px; diff --git a/frontend/src/pages/AttendanceManage.jsx b/frontend/src/pages/AttendanceManage.jsx index d947d92f..e7c8ccee 100644 --- a/frontend/src/pages/AttendanceManage.jsx +++ b/frontend/src/pages/AttendanceManage.jsx @@ -54,59 +54,19 @@ const AttendanceContent = () => { - - - - {/* --------------------------------------------------------------- */} From 2f527f8e96f742b4b10630f5bf4c4647b56b82e1 Mon Sep 17 00:00:00 2001 From: GGGGGangSub Date: Mon, 8 Dec 2025 13:18:39 +0900 Subject: [PATCH 4/4] =?UTF-8?q?fix:=20=EC=B6=9C=EC=84=9D=EA=B4=80=EB=A6=AC?= =?UTF-8?q?(=EB=8B=B4=EB=8B=B9=EC=9E=90)=20=ED=8E=98=EC=9D=B4=EC=A7=80=20a?= =?UTF-8?q?lert=EB=AC=B8=20toast=EB=A1=9C=20=EB=B3=80=EA=B2=BD=20=EB=B0=8F?= =?UTF-8?q?=20=EB=94=94=EC=9E=90=EC=9D=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/attendancemanage/SessionManagementCard.jsx | 5 +++-- frontend/src/pages/AttendanceManage.jsx | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/attendancemanage/SessionManagementCard.jsx b/frontend/src/components/attendancemanage/SessionManagementCard.jsx index 143b0549..e651f25f 100644 --- a/frontend/src/components/attendancemanage/SessionManagementCard.jsx +++ b/frontend/src/components/attendancemanage/SessionManagementCard.jsx @@ -4,6 +4,7 @@ import calendarAddIcon from '../../assets/calendar-icon.svg'; import { getRounds } from '../../utils/attendanceManage'; import { useEffect, useState } from 'react'; +import { toast } from 'react-toastify'; const SessionManagementCard = ({ styles: commonStyles }) => { const { @@ -56,7 +57,7 @@ const SessionManagementCard = ({ styles: commonStyles }) => { className={commonStyles.iconButton} onClick={() => { if (!currentSession) { - alert('세션을 먼저 선택해주세요.'); + toast.error('세션을 먼저 선택해주세요.'); return; } openAddRoundsModal(); @@ -71,7 +72,7 @@ const SessionManagementCard = ({ styles: commonStyles }) => { className={commonStyles.iconButton} onClick={() => { if (!currentSession) { - alert('세션을 먼저 선택해주세요.'); + toast.error('세션을 먼저 선택해주세요.'); return; } openAddUsersModal(); diff --git a/frontend/src/pages/AttendanceManage.jsx b/frontend/src/pages/AttendanceManage.jsx index e7c8ccee..138e516c 100644 --- a/frontend/src/pages/AttendanceManage.jsx +++ b/frontend/src/pages/AttendanceManage.jsx @@ -45,7 +45,7 @@ const AttendanceContent = () => { return ( <> - + */} {/* --------------------------------------------------------------- */}