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
3 changes: 3 additions & 0 deletions frontend/src/assets/calendar-icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
51 changes: 51 additions & 0 deletions frontend/src/components/VerificationModal.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -183,3 +183,54 @@ h1 {
}
.modifyButtonGroup button:disabled {
}

.modalContent {
padding: 10px 10px 30px;
}

.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;
}

.button {
padding: 10px 20px;
font-size: 1rem;
font-weight: 600;
border-radius: 6px;
cursor: pointer;
border: none;
}

@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
Comment on lines +186 to +236
Copy link

@coderabbitai coderabbitai bot Dec 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

CSS 규칙 중복 정의 - 스타일 충돌 위험

.inputGroup, .label, .button 클래스가 파일에서 여러 번 정의되어 있습니다(라인 57-59, 67-70, 100-121 그리고 다시 191-225). CSS 캐스케이딩으로 인해 나중의 정의가 이전 정의를 덮어쓰게 되므로, sendButton, submitButton, resetPasswordButton 등 기존 버튼 변수(라인 123-142)의 스타일이 예상치 못하게 변경될 수 있습니다.

특히 새로운 .button 정의(218-225)는 기존 정의에서 제공하는 중요한 속성들(display: inline-flex, width, height, font-family, line-height, letter-spacing 등)을 잃어버립니다.

제안: 중복을 제거하고 필요한 스타일을 기존 정의에 통합하거나, 새로운 선택자를 사용하여 스타일을 명확히 분리하세요.

다음은 권장 접근 방식입니다:

옵션 1: 기존 규칙 통합

 .inputGroup {
   margin-bottom: 21px;
 }
 .inputGroup label {
   display: block;
   margin-bottom: 12px;
   font-weight: 500;
   color: #555;
 }

-.label {
+/* 기존 .label을 제거하고 필요시 새 규칙과 병합 */
+.label {
   display: block;
-  margin-bottom: 0.5rem;
+  margin-bottom: 0.5rem;
+  font-size: 0.9rem;
+  font-weight: 600;
+  color: #555;
 }

옵션 2: 새로운 선택자 사용

-.button {
-  display: inline-flex;
-  ...
+.buttonPrimary {
+  display: inline-flex;
+  ...
 }
+
+.button {
+  padding: 10px 20px;
+  font-size: 1rem;
+  font-weight: 600;
+  border-radius: 6px;
+  cursor: pointer;
+  border: none;
+}

Committable suggestion skipped: line range outside the PR's diff.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이건 고쳐주시면 좋을 것 같습니다.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gxuoo, 네, CSS 클래스 중복 문제를 해결해드리겠습니다. 새로 추가된 스타일들이 기존 스타일과 충돌하지 않도록 더 구체적인 클래스명을 사용하는 것이 좋겠습니다.

다음과 같이 수정하시면 됩니다:

.modalContent {
  padding: 10px 10px 30px;
}

.addUserInputGroup {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.addUserLabel {
  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;
}

.addUserButton {
  padding: 10px 20px;
  font-size: 1rem;
  font-weight: 600;
  border-radius: 6px;
  cursor: pointer;
  border: none;
}

@keyframes fadeIn {
  from {
    opacity: 0;
    transform: translateY(-10px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

변경된 클래스명:

  • .inputGroup.addUserInputGroup
  • .label.addUserLabel
  • .button.addUserButton

이렇게 수정하신 후, AddUsersModal.jsx 파일에서도 해당 클래스명을 동일하게 업데이트해주셔야 합니다:

  • styles.inputGroupstyles.addUserInputGroup
  • styles.labelstyles.addUserLabel
  • styles.buttonstyles.addUserButton

이렇게 하면 기존 버튼 스타일(.sendButton, .submitButton, .resetPasswordButton 등)과 충돌 없이 새로운 모달의 스타일을 독립적으로 관리할 수 있습니다.

109 changes: 109 additions & 0 deletions frontend/src/components/attendancemanage/AddUsersModal.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
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]);

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

if (!currentSession) {
alert('세션을 먼저 선택해주세요.');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
alert('세션을 먼저 선택해주세요.');
toast.error('세션을 먼저 선택해주세요.');

alert 함수 대신에 toast를 사용하는 것이 좋을 것 같습니다. line5에 아래의 import문을 추가해주세요!

import { toast } from 'react-toastify';

return;
}
if (!selectedUserId) {
alert('추가할 인원를 1명 이상 선택해주세요.');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기도 그래야겠죠? 👀

return;
}

handleAddUsers(selectedSessionId, selectedUserId);
closeAddUsersModal();
};

return (
<div className={styles.overlay}>
<div className={styles.modal}>
<div className={styles.modalHeader}>
<h1>세션에 유저 추가하기</h1>
<button
type="button"
className={styles.closeButton}
onClick={() => {
closeAddUsersModal();
}}
>
&times;
</button>
</div>

<div className={styles.modalContent}>
<div className={styles.inputGroup}>
<label htmlFor="userSelect" className={styles.label}>
유저 선택
</label>
<select
id="userSelect"
className={styles.selectInput}
value={selectedUserId || ''}
onChange={(e) => setSelectedUserId(e.target.value)}
>
<option value="" disabled>
------ 유저를 선택하세요 ------
</option>
{users &&
users.map((user) => (
<option key={user.userId} value={user.userId}>
{user.name} ({user.email})
</option>
))}
</select>
</div>
</div>

<div className={styles.modifyButtonGroup}>
<button
className={`${styles.button} ${styles.submitButton}`}
onClick={handleComplete}
>
추가
</button>
</div>
</div>
</div>
);
};

export default AddUsersModal;
Original file line number Diff line number Diff line change
@@ -1,10 +1,43 @@
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);
// 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]);
Comment on lines +25 to +40
Copy link

@coderabbitai coderabbitai bot Dec 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

useEffect 내 에러 처리 누락

getRoundUserAttendance 호출이 실패할 경우 에러가 처리되지 않아 사용자에게 피드백이 없고 콘솔에 unhandled rejection이 발생할 수 있습니다.

  useEffect(() => {
    const fetchUsers = async () => {
      if (selectedSessionId && selectedRound) {
-        const userList = await getRoundUserAttendance(selectedRound);
-        setUsers(userList);
+        try {
+          const userList = await getRoundUserAttendance(selectedRound);
+          setUsers(userList || []);
+        } catch (error) {
+          console.error('출석 정보를 불러오는 데 실패했습니다:', error);
+          setUsers([]);
+        }
      } else {
        setUsers([]);
      }
    };
    fetchUsers();
  }, [selectedSessionId, selectedRound, roundAttendanceVersion]);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
useEffect(() => {
const fetchUsers = async () => {
if (selectedSessionId && selectedRound) {
const userList = await getRoundUserAttendance(selectedRound);
// 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]);
useEffect(() => {
const fetchUsers = async () => {
if (selectedSessionId && selectedRound) {
try {
const userList = await getRoundUserAttendance(selectedRound);
// const sortedUsers = (userList || []).sort(
// (a, b) =>
// new Date(`${a.date}T${a.startTime}`) -
// new Date(`${b.date}T${b.startTime}`)
// );
setUsers(userList || []);
} catch (error) {
console.error('출석 정보를 불러오는 데 실패했습니다:', error);
setUsers([]);
}
} else {
setUsers([]);
}
};
fetchUsers();
}, [selectedSessionId, selectedRound, roundAttendanceVersion]);
🤖 Prompt for AI Agents
In frontend/src/components/attendancemanage/AttendanceManagementCard.jsx around
lines 25 to 40, the async getRoundUserAttendance call inside useEffect lacks
error handling; wrap the fetchUsers logic in a try/catch, call
getRoundUserAttendance inside try, setUsers with the result on success, and in
catch log the error (console.error) and setUsers([]) (or set an existing error
state / show a user-facing notification) so rejections are handled and the UI
gets sensible feedback.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

에러 처리 추가해주시면 좋을 거 같아요!

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gxuoo 동의해 주셔서 감사합니다! 에러 처리를 추가하면 API 호출 실패 시 사용자 경험이 개선되고 디버깅도 훨씬 쉬워질 것입니다.

@NaturalSoda4552 님, 위의 제안된 코드를 참고하여 try/catch 블록을 추가해 주시면 감사하겠습니다! 🙏

(ᵔᴥᵔ)ʃ♡


return (
<div className={styles.attendanceManagementCardContainer}>
Expand All @@ -18,7 +51,7 @@ const AttendanceManagementCard = ({ styles: commonStyles }) => {
<th>이름</th>
<th>상태</th>
<th>변경</th>
<th>횟수</th>
<th>이메일</th>
</tr>
</thead>
<tbody>
Expand All @@ -28,27 +61,28 @@ const AttendanceManagementCard = ({ styles: commonStyles }) => {
회차를 선택해주세요.
</td>
</tr>
) : participants.length > 0 ? (
participants.map((participant) => (
<tr key={participant.memberId}>
<td>{participant.name}</td>
<td>{participant.attendance}</td>
) : users.length > 0 ? (
users.map((user) => (
<tr key={user.userId}>
<td>{user.userName}</td>
<td>{attendanceEnglishToKorean[user.attendanceStatus]}</td>
<td>
<select
className={styles.attendanceSelect}
value={participant.attendance}
value={user.attendanceStatus}
onChange={(e) =>
handleAttendanceChange(
participant.memberId,
e.target.value
)
handleAttendanceChange(user.userId, e.target.value)
}
>
<option value="출석">출석</option>
<option value="결석">결석</option>
{/* 출석(PRESENT), 지각(LATE), 결석(ABSENT), 공결(EXCUSED) */}
<option value="PRESENT">출석</option>
<option value="ABSENT">결석</option>
<option value="LATE">지각</option>
<option value="EXCUSED">공결</option>
<option value="PENDING">미정</option>
</select>
</td>
<td>-</td>
<td>{user.email}</td>
</tr>
))
) : (
Expand Down
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -26,15 +26,30 @@
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%;

position: sticky;
top: 0;
background-color: #fafbfc;
}
.table tbody td {
text-align: left;
color: var(--text);
padding: 8px 7px;
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%;
Expand All @@ -47,4 +62,4 @@
text-align: center;
padding: 20px;
color: #888;
}
}
13 changes: 7 additions & 6 deletions frontend/src/components/attendancemanage/RoundDayPicker.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = () => {
Expand All @@ -29,7 +28,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('세션을 먼저 선택해주세요.');
Expand All @@ -46,12 +47,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: [],
};
});

Expand Down
11 changes: 7 additions & 4 deletions frontend/src/components/attendancemanage/RoundModifyModal.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import ConfirmationToast from './ConfirmationToast';
const RoundModifyModal = ({
styles: commonStyles,
onClose,
sessionId,
round,
onSave,
onDelete,
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -127,7 +130,7 @@ const RoundModifyModal = ({
</div>

<div className={styles.form}>
<div className={commonStyles.inputGroup}>
<div className={commonStyles.modalInputGroup}>
<label htmlFor="sessionStartTime" className={commonStyles.label}>
출석 시작 시간
</label>
Expand Down
Loading