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
91 changes: 72 additions & 19 deletions front-end/src/pages/student/course/StudentCourse.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,47 @@ import StudentRequest from './request/StudentRequest';
import StudentReact from './react/StudentReact';
import StudentQuestion from './question/StudentQuestion';
import useModal from '@/hooks/useModal';
import { getStudentPopup, PopupType } from '@/utils/studentPopupUtils';
import {
getStudentPopup,
handleStudentError,
PopupType,
} from '@/utils/studentPopupUtils';
import { useNavigate } from 'react-router';
import { Question } from '@/core/model';
import { classroomRepository } from '@/di';

const TAB_OPTIONS = [
{ key: 'request', label: '요청하기' },
{ key: 'react', label: '반응하기' },
{ key: 'question', label: '질문하기' },
];

const SSE_URL = import.meta.env.VITE_API_URL;

const StudentCourse = () => {
const navigate = useNavigate();
const [selectedTab, setSelectedTab] = useState(TAB_OPTIONS[0].key);
const [underlineStyle, setUnderlineStyle] = useState({ left: 0, width: 0 });
const [questions, setQuestions] = useState<Question[]>([]);
const [modalType, setModalType] = useState<PopupType | null>(null);

const tabRefs = useRef<{ [key: string]: HTMLButtonElement | null }>({});
const eventSourceRef = useRef<EventSource | null>(null);

const { openModal, closeModal, Modal } = useModal();

useEffect(() => {
async function fetchQuestions() {
try {
const questionList = await classroomRepository.getQuestions();
setQuestions(questionList);
} catch (error) {
handleStudentError({ error, setModalType, openModal });
}
}
fetchQuestions();
}, []);

useEffect(() => {
updateUnderline();
window.addEventListener('resize', updateUnderline);
Expand All @@ -33,27 +55,50 @@ const StudentCourse = () => {
};
}, [selectedTab]);

// useEffect(() => {
// const eventSource = new EventSource('/api/sse/connection/student');
useEffect(() => {
const connectSSE = () => {
if (eventSourceRef.current) {
eventSourceRef.current.close();
}

// eventSource.onopen = () => {
// console.log('🔗 SSE 연결됨');
// };
const eventSource = new EventSource(`${SSE_URL}/sse/connection/student`, {
withCredentials: true,
});

// eventSource.onmessage = (event) => {
// console.log('📩 받은 데이터:', event.data);
// };
eventSource.onmessage = (event) => {
const parsedData = JSON.parse(event.data); // JSON 형식으로 변환

// messageType에 따라 분기 처리
switch (parsedData.messageType) {
case 'QUESTION_CHECK':
setQuestions((prevQuestions) =>
prevQuestions.filter((q) => q.id !== parsedData.data.id)
);
break;

case 'COURSE_CLOSED':
setModalType('closedCourse');
openModal();
break;

default:
break;
}
};

eventSource.onerror = () => {
eventSource.close();
connectSSE();
};
eventSourceRef.current = eventSource;
};
Comment on lines +68 to +94
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

SSE message handling is correct, but watch out for potential infinite reconnect loops.

Implementing a maximum reconnection limit or exponential backoff can prevent continual reconnection attempts when the server is permanently unreachable.


// eventSource.onerror = (error) => {
// console.error('❌ SSE 오류:', error);
// eventSource.close();
// };
connectSSE(); // 최초 연결

// return () => {
// console.log('🔌 SSE 연결 종료');
// eventSource.close();
// };
// }, []);
return () => {
eventSourceRef.current?.close();
};
}, []);
Comment on lines +58 to +101
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

SSE reconnection might need backoff logic.
The dynamic reconnection in onerror is useful, but re-calling connectSSE() immediately on every error can cause high retry frequency if the server is down. Consider adding a retry limit or exponential backoff to prevent potential infinite loops and reduce server load.

Below is an example diff introducing a simple exponential backoff mechanism:

 useEffect(() => {
   let retryDelay = 1000;

   const connectSSE = () => {
     if (eventSourceRef.current) {
       eventSourceRef.current.close();
     }

     const eventSource = new EventSource(`${SSE_URL}/sse/connection/student`, {
       withCredentials: true,
     });

     eventSource.onmessage = (event) => {
       const parsedData = JSON.parse(event.data);
       switch (parsedData.messageType) {
         case 'QUESTION_CHECK':
           setQuestions((prevQuestions) =>
             prevQuestions.filter((q) => q.id !== parsedData.data.id)
           );
           break;
         case 'COURSE_CLOSED':
           setModalType('closedCourse');
           openModal();
           break;
         default:
           break;
       }
       // Reset delay upon successful message
+      retryDelay = 1000;
     };

     eventSource.onerror = () => {
       eventSource.close();
+      setTimeout(() => {
+        connectSSE();
+        // Increase the delay for the next retry
+        retryDelay = Math.min(retryDelay * 2, 30000);
+      }, retryDelay);
     };

     eventSourceRef.current = eventSource;
   };

   connectSSE();

   return () => {
     eventSourceRef.current?.close();
   };
 }, []);
📝 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 connectSSE = () => {
if (eventSourceRef.current) {
eventSourceRef.current.close();
}
// eventSource.onopen = () => {
// console.log('🔗 SSE 연결됨');
// };
const eventSource = new EventSource(`${SSE_URL}/sse/connection/student`, {
withCredentials: true,
});
// eventSource.onmessage = (event) => {
// console.log('📩 받은 데이터:', event.data);
// };
eventSource.onmessage = (event) => {
const parsedData = JSON.parse(event.data); // JSON 형식으로 변환
// messageType에 따라 분기 처리
switch (parsedData.messageType) {
case 'QUESTION_CHECK':
setQuestions((prevQuestions) =>
prevQuestions.filter((q) => q.id !== parsedData.data.id)
);
break;
case 'COURSE_CLOSED':
setModalType('closedCourse');
openModal();
break;
default:
break;
}
};
eventSource.onerror = () => {
eventSource.close();
connectSSE();
};
eventSourceRef.current = eventSource;
};
// eventSource.onerror = (error) => {
// console.error('❌ SSE 오류:', error);
// eventSource.close();
// };
connectSSE(); // 최초 연결
// return () => {
// console.log('🔌 SSE 연결 종료');
// eventSource.close();
// };
// }, []);
return () => {
eventSourceRef.current?.close();
};
}, []);
useEffect(() => {
let retryDelay = 1000;
const connectSSE = () => {
if (eventSourceRef.current) {
eventSourceRef.current.close();
}
const eventSource = new EventSource(`${SSE_URL}/sse/connection/student`, {
withCredentials: true,
});
eventSource.onmessage = (event) => {
const parsedData = JSON.parse(event.data); // JSON 형식으로 변환
// messageType에 따라 분기 처리
switch (parsedData.messageType) {
case 'QUESTION_CHECK':
setQuestions((prevQuestions) =>
prevQuestions.filter((q) => q.id !== parsedData.data.id)
);
break;
case 'COURSE_CLOSED':
setModalType('closedCourse');
openModal();
break;
default:
break;
}
// Reset delay upon successful message
retryDelay = 1000;
};
eventSource.onerror = () => {
eventSource.close();
setTimeout(() => {
connectSSE();
// Increase the delay for the next retry
retryDelay = Math.min(retryDelay * 2, 30000);
}, retryDelay);
};
eventSourceRef.current = eventSource;
};
connectSSE(); // 최초 연결
return () => {
eventSourceRef.current?.close();
};
}, []);


const selectedIndex = TAB_OPTIONS.findIndex((tab) => tab.key === selectedTab);

Expand All @@ -76,7 +121,7 @@ const StudentCourse = () => {
switch (modalType) {
case 'notFound':
return getStudentPopup(
'notfound',
'notFound',
handleErrorModalClick,
handleErrorModalClick
);
Expand All @@ -94,6 +139,12 @@ const StudentCourse = () => {
handleErrorModalClick,
handleErrorModalClick
);
case 'closedCourse':
return getStudentPopup(
'closedCourse',
handleErrorModalClick,
handleErrorModalClick
);

default:
return getStudentPopup(
Expand Down Expand Up @@ -146,6 +197,8 @@ const StudentCourse = () => {
</div>
<div className={S.tabLayout}>
<StudentQuestion
questions={questions}
setQuestions={setQuestions}
setModalType={setModalType}
openModal={openModal}
/>
Expand Down
25 changes: 10 additions & 15 deletions front-end/src/pages/student/course/question/StudentQuestion.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useEffect, useState } from 'react';
import { useState } from 'react';
import SuccessPopup from '../components/SuccessPopup';
import S from './StudentQuestion.module.css';
import QuestionForm from './components/QuestionForm';
Expand All @@ -10,24 +10,19 @@ import { handleStudentError } from '@/utils/studentPopupUtils';
type StudentQuestionProps = {
setModalType: React.Dispatch<React.SetStateAction<string | null>>;
openModal: () => void;
setQuestions: React.Dispatch<React.SetStateAction<Question[]>>;
questions: Question[];
};

const StudentQuestion = ({ setModalType, openModal }: StudentQuestionProps) => {
const StudentQuestion = ({
setModalType,
openModal,
setQuestions,
questions,
}: StudentQuestionProps) => {
const [successPopup, setSuccessPopup] = useState(false);
const [questions, setQuestions] = useState<Question[]>([]);
const [inputValue, setInputValue] = useState('');

useEffect(() => {
async function fetchQuestions() {
try {
const questionList = await classroomRepository.getQuestions();
setQuestions(questionList);
} catch (error) {
handleStudentError({ error, setModalType, openModal });
}
}
fetchQuestions();
}, []);
const [inputValue, setInputValue] = useState('');

const handleInputSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
Expand Down
2 changes: 1 addition & 1 deletion front-end/src/pages/student/home/StudentHome.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ const StudentHome = () => {
}

case 'notFound':
return getStudentPopup('notfound', closeModal, closeModal);
return getStudentPopup('notFound', closeModal, closeModal);

case 'notStart':
return getStudentPopup('notStart', closeModal, closeModal);
Expand Down
4 changes: 4 additions & 0 deletions front-end/src/utils/studentPopupUtils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ const popupConfigs: Record<string, PopupConfig> = {
title: '아직 수업이 시작하지 않았습니다',
content: '잠시만 기다려주세요',
},
closedCourse: {
title: '교수님이 강의를 종료하셨어요',
content: '확인을 누르시면 처음화면으로 돌아갑니다',
},
server: {
title: '서버에 오류가 발생했습니다',
content: '다시 한번 시도해주세요',
Expand Down