From 5c0ceae3cb8e03704a7bcdd8f2891821f969030e Mon Sep 17 00:00:00 2001 From: Hyeona01 Date: Tue, 3 Dec 2024 13:43:26 +0900 Subject: [PATCH 1/6] =?UTF-8?q?FE:=20[fix]=20=EC=82=AC=ED=9B=84=20?= =?UTF-8?q?=EB=A0=88=ED=8F=AC=ED=8A=B8=20UI=20=EC=88=98=EC=A0=95=20#70?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/report/ReportSection.tsx | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/frontend/eyesee-admin/src/components/report/ReportSection.tsx b/src/frontend/eyesee-admin/src/components/report/ReportSection.tsx index d665790..a55740f 100644 --- a/src/frontend/eyesee-admin/src/components/report/ReportSection.tsx +++ b/src/frontend/eyesee-admin/src/components/report/ReportSection.tsx @@ -57,15 +57,17 @@ const ReportSection = ({ reportData }: ReportSectionType) => {
부정행위 탐지율
{reportData.cheatingRate}%
-
+
부정행위 유형별 통계
- {Object.entries(reportData.cheatingTypeStatistics).map( - ([type, count]) => ( - - {type}: {count}건{", "} - - ) - )} +
+ {Object.entries(reportData.cheatingTypeStatistics).map( + ([type, count]) => ( + + {type}: {count}건{", "} + + ) + )} +
From 59d42fb0d18cd6d4cdfff3fdcc60271ef45e979c Mon Sep 17 00:00:00 2001 From: Hyeona01 Date: Tue, 3 Dec 2024 13:58:29 +0900 Subject: [PATCH 2/6] =?UTF-8?q?FE:=20[fix]=20=EC=A2=85=EB=A3=8C=EB=90=9C?= =?UTF-8?q?=20=EC=8B=9C=ED=97=98=20=EB=AC=B8=EA=B5=AC=20ui=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0=20#70?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../eyesee-admin/src/components/report/ReportSection.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/frontend/eyesee-admin/src/components/report/ReportSection.tsx b/src/frontend/eyesee-admin/src/components/report/ReportSection.tsx index a55740f..089d538 100644 --- a/src/frontend/eyesee-admin/src/components/report/ReportSection.tsx +++ b/src/frontend/eyesee-admin/src/components/report/ReportSection.tsx @@ -28,7 +28,7 @@ const ReportSection = ({ reportData }: ReportSectionType) => {

엑셀 다운로드

-
+
종료된 시험입니다.
From f67300790e9965a301d8a84f91a3193a89974c25 Mon Sep 17 00:00:00 2001 From: Hyeona01 Date: Tue, 3 Dec 2024 14:38:15 +0900 Subject: [PATCH 3/6] =?UTF-8?q?FE:=20[feat]=20=EC=88=98=ED=97=98=EC=9E=90?= =?UTF-8?q?=20=EB=8D=B0=EC=9D=B4=ED=84=B0=EC=97=90=20exam=20id=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20#53?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../eyesee-user/src/app/exam-room/page.tsx | 6 ++--- .../eyesee-user/src/app/information/page.tsx | 10 ++++++-- .../eyesee-user/src/store/useUserIdStore.ts | 19 --------------- .../eyesee-user/src/store/useUserStore.ts | 23 +++++++++++++++++++ src/frontend/eyesee-user/src/types/exam.ts | 1 + 5 files changed, 35 insertions(+), 24 deletions(-) delete mode 100644 src/frontend/eyesee-user/src/store/useUserIdStore.ts create mode 100644 src/frontend/eyesee-user/src/store/useUserStore.ts diff --git a/src/frontend/eyesee-user/src/app/exam-room/page.tsx b/src/frontend/eyesee-user/src/app/exam-room/page.tsx index f9aa53b..862e1e0 100644 --- a/src/frontend/eyesee-user/src/app/exam-room/page.tsx +++ b/src/frontend/eyesee-user/src/app/exam-room/page.tsx @@ -2,7 +2,7 @@ import React, { useEffect, useRef, useState } from "react"; import NextButton from "@/components/common/NextButton"; -import { useUserIdStore } from "@/store/useUserIdStore"; +import { useUserStore } from "@/store/useUserStore"; import { useStore } from "@/store/useStore"; const RealTimeVideoPage = () => { @@ -18,8 +18,8 @@ const RealTimeVideoPage = () => { const [isProcessing, setIsProcessing] = useState(false); // 부정행위 감지 처리 중 여부 상태 - const userId = useStore(useUserIdStore, (state) => state.userId); - const examId = 1; + const userId = useStore(useUserStore, (state) => state.userId); + const examId = useStore(useUserStore, (state) => state.examId); const setupWebSocket = () => { console.log(process.env.NEXT_PUBLIC_WEBSOCKET_KEY); console.log(userId); diff --git a/src/frontend/eyesee-user/src/app/information/page.tsx b/src/frontend/eyesee-user/src/app/information/page.tsx index cb5cf71..330fe17 100644 --- a/src/frontend/eyesee-user/src/app/information/page.tsx +++ b/src/frontend/eyesee-user/src/app/information/page.tsx @@ -5,7 +5,7 @@ import NextButton from "@/components/common/NextButton"; import SubHeader from "@/components/common/SubHeader"; import InformationSection from "@/components/information/InformationSection"; import { useExamStore } from "@/store/useExamStore"; -import { useUserIdStore } from "@/store/useUserIdStore"; +import { useUserStore } from "@/store/useUserStore"; import { UserInfoRequest } from "@/types/exam"; import { setAccessToken, setRefreshToken } from "@/utils/auth"; import { informationValidation } from "@/utils/validation"; @@ -14,8 +14,11 @@ import { useEffect, useState } from "react"; const InformationPage = () => { const router = useRouter(); - const { setUserId } = useUserIdStore(); + + const { setUserId } = useUserStore(); + const { setExamId } = useUserStore(); const { exam } = useExamStore(); + const [isAvailable, setIsAvailable] = useState(false); const [information, setInformation] = useState({ // TODO: 서버 데이터 타입 통일 필요 @@ -34,6 +37,9 @@ const InformationPage = () => { setAccessToken(response.data.access_token); setRefreshToken(response.data.refresh_token); setUserId(response.data.userId); + setExamId(response.data.examId); + console.log(response.data.userId, response.data.examId); + router.push("/camera"); } catch (error) { console.error("수험 정보 입력 실패:", error); // 실패 시 에러 처리 diff --git a/src/frontend/eyesee-user/src/store/useUserIdStore.ts b/src/frontend/eyesee-user/src/store/useUserIdStore.ts deleted file mode 100644 index cb42831..0000000 --- a/src/frontend/eyesee-user/src/store/useUserIdStore.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { create } from "zustand"; -import { persist } from "zustand/middleware"; - -type UserIdStore = { - userId: number | null; - setUserId: (newUserId: number) => void; -}; - -export const useUserIdStore = create( - persist( - (set) => ({ - userId: null, - setUserId: (newUserId) => set({ userId: newUserId }), - }), - { - name: "userId-storage", // localStorage에 저장될 키 이름 - } - ) -); diff --git a/src/frontend/eyesee-user/src/store/useUserStore.ts b/src/frontend/eyesee-user/src/store/useUserStore.ts new file mode 100644 index 0000000..1546f0d --- /dev/null +++ b/src/frontend/eyesee-user/src/store/useUserStore.ts @@ -0,0 +1,23 @@ +import { create } from "zustand"; +import { persist } from "zustand/middleware"; + +type UserStore = { + userId: number | null; + examId: number | null; + setUserId: (newUserId: number) => void; + setExamId: (newExamId: number) => void; +}; + +export const useUserStore = create( + persist( + (set) => ({ + userId: null, + examId: null, + setUserId: (newUserId) => set({ userId: newUserId }), + setExamId: (newExamId) => set({ examId: newExamId }), + }), + { + name: "user-storage", // localStorage에 저장될 키 이름 + } + ) +); diff --git a/src/frontend/eyesee-user/src/types/exam.ts b/src/frontend/eyesee-user/src/types/exam.ts index 834914b..338cdf6 100644 --- a/src/frontend/eyesee-user/src/types/exam.ts +++ b/src/frontend/eyesee-user/src/types/exam.ts @@ -48,6 +48,7 @@ export type UserInfoRequest = { export type UserInfoResponse = { userId: number; + examId: number; access_token: string; refresh_token: string; }; From 786861492c8cce893e0451ece3601d451db8b35b Mon Sep 17 00:00:00 2001 From: Hyeona01 Date: Thu, 5 Dec 2024 16:10:03 +0900 Subject: [PATCH 4/6] =?UTF-8?q?FE:=20[feat]=20=EB=B6=80=EC=A0=95=ED=96=89?= =?UTF-8?q?=EC=9C=84=20=EC=98=81=EC=83=81=20=EC=A0=80=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../eyesee-user/src/app/exam-room/page.tsx | 66 +++++++++++++++---- 1 file changed, 53 insertions(+), 13 deletions(-) diff --git a/src/frontend/eyesee-user/src/app/exam-room/page.tsx b/src/frontend/eyesee-user/src/app/exam-room/page.tsx index 862e1e0..b940b09 100644 --- a/src/frontend/eyesee-user/src/app/exam-room/page.tsx +++ b/src/frontend/eyesee-user/src/app/exam-room/page.tsx @@ -6,26 +6,38 @@ import { useUserStore } from "@/store/useUserStore"; import { useStore } from "@/store/useStore"; const RealTimeVideoPage = () => { + // 수험자 실시간 화면이 담기는 공간 const videoRef = useRef(null); + + // 캠버스에서 프레임을 캡쳐 const canvasRef = useRef(null); + + // 웹소켓 연결 관리 const socketRef = useRef(null); + + // 실시간 비디오 녹화 const mediaRecorderRef = useRef(null); + + // 녹화된 비디오 데이터를 청크 단위로 저장 const recordedChunksRef = useRef<{ timestamp: number; data: Blob }[]>([]); // 청크와 타임스탬프 저장 + const captureIntervalRef = useRef(null); const CHUNK_SIZE = 1000; // 1초마다 녹화 데이터를 저장 const BUFFER_DURATION = 20 * 1000; // 20초 간의 데이터를 저장 - const [isProcessing, setIsProcessing] = useState(false); // 부정행위 감지 처리 중 여부 상태 + // 부정행위 감지 처리 중 여부 + const [isProcessing, setIsProcessing] = useState(false); const userId = useStore(useUserStore, (state) => state.userId); const examId = useStore(useUserStore, (state) => state.examId); + const setupWebSocket = () => { console.log(process.env.NEXT_PUBLIC_WEBSOCKET_KEY); - console.log(userId); + console.log(userId, examId); if (!userId) { - console.error("userId가 설정되지 않았습니다."); + console.log("userId가 설정되지 않았습니다."); return; } @@ -83,21 +95,27 @@ const RealTimeVideoPage = () => { // Canvas를 사용해 비디오 프레임을 WebSocket으로 전송 const startRecording = (stream: MediaStream) => { mediaRecorderRef.current = new MediaRecorder(stream, { - mimeType: "video/webm; codecs=vp8", + mimeType: "video/webm; codecs=vp9", }); mediaRecorderRef.current.ondataavailable = (event) => { if (event.data.size > 0) { const timestamp = Date.now(); + + // 새로 들어온 데이터 추가 recordedChunksRef.current.push({ timestamp: timestamp, data: event.data, }); + // if (isProcessing) return; // 부정행위 비디오 처리 및 저장 중에는 비디오 프레임 업데이트 정지 // 슬라이딩 윈도우 방식으로 오래된 데이터 삭제 - const currentTime = Date.now(); recordedChunksRef.current = recordedChunksRef.current.filter( - (chunk) => chunk.timestamp >= currentTime - BUFFER_DURATION + (chunk) => chunk.timestamp >= timestamp - BUFFER_DURATION + ); + + console.log( + `현재 청크 수: ${recordedChunksRef.current.length}, 유지 시간: ${BUFFER_DURATION}ms` ); } }; @@ -117,16 +135,16 @@ const RealTimeVideoPage = () => { } }; - // 부정행위 비디오 처리 및 저장 - const sendCheatingVideo = async ( - cheatingTimestamp: string | number | Date - ) => { + // ===== AI단에서 웹소켓 메시지를 수신하면 부정행위 비디오 처리 및 저장 ===== + const sendCheatingVideo = async (cheatingTimestamp: string) => { try { setIsProcessing(true); // 부정행위 감지 시작 console.log("부정행위 발생 타임스탬프:", cheatingTimestamp); const cheatingDate = new Date(cheatingTimestamp); + console.log("cheatingDate", cheatingDate); + if (isNaN(cheatingDate.getTime())) { throw new Error("Invalid cheatingTimestamp: " + cheatingTimestamp); } @@ -142,34 +160,55 @@ const RealTimeVideoPage = () => { // MediaRecorder가 멈춘 후 데이터를 처리 mediaRecorderRef.current!.onstop = () => { + console.log("recordedChunksRef.current :", recordedChunksRef.current); + const previousChunks = recordedChunksRef.current.filter( (chunk) => chunk.timestamp >= startTime && chunk.timestamp <= cheatingTime ); console.log(`탐지 이전 데이터: ${previousChunks.length} 청크`); + console.log(`탐지 이전 데이터: ${previousChunks}`); const postCheatingChunks = recordedChunksRef.current.filter( (chunk) => chunk.timestamp > cheatingTime && chunk.timestamp <= endTime ); console.log(`탐지 이후 데이터: ${postCheatingChunks.length} 청크`); + console.log(`탐지 이후 데이터: ${postCheatingChunks}`); const allChunks = [...previousChunks, ...postCheatingChunks]; + console.log(`allChunks: ${allChunks.length} 청크`); + console.log(`allChunks: ${allChunks}`); + allChunks.forEach((chunk, index) => { + console.log(`손실 탐지 Chunk ${index}:`, chunk.timestamp, chunk.data); + }); + const blob = new Blob( allChunks.map((chunk) => chunk.data), { - type: "video/webm", + type: "video/webm; codecs=vp9", } ); - + // 디버깅 + console.log("blob: ", blob); console.log(`최종 Blob 크기: ${blob.size / 1024} KB`); + // -- const url = URL.createObjectURL(blob); + // 디버깅 + console.log("Generated Blob URL:", url); + + // 비디오 태그 생성으로 디버깅 + const video = document.createElement("video"); + video.src = url; + video.controls = true; + document.body.appendChild(video); + // -- + const a = document.createElement("a"); a.href = url; a.download = `cheating_${new Date().toISOString()}.webm`; a.click(); - URL.revokeObjectURL(url); console.log("부정행위 비디오 저장 완료"); // 이 부분은 녹화가 중지된 후, 데이터가 완전히 처리된 후에 실행되어야 합니다. @@ -183,6 +222,7 @@ const RealTimeVideoPage = () => { } }; + // AI 단으로 실시간 영상 송신 const captureAndSendFrame = () => { if ( canvasRef.current && From 9f893f73d826e66a496c640a2f76ff3a01468ab4 Mon Sep 17 00:00:00 2001 From: Hyeona01 Date: Thu, 5 Dec 2024 17:34:04 +0900 Subject: [PATCH 5/6] =?UTF-8?q?FE:=20[fix]=20examLotion=20=EC=98=A4?= =?UTF-8?q?=ED=83=80=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/frontend/eyesee-admin/src/components/addExam/Step1.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/frontend/eyesee-admin/src/components/addExam/Step1.tsx b/src/frontend/eyesee-admin/src/components/addExam/Step1.tsx index ba5ad60..9ea1b9c 100644 --- a/src/frontend/eyesee-admin/src/components/addExam/Step1.tsx +++ b/src/frontend/eyesee-admin/src/components/addExam/Step1.tsx @@ -62,8 +62,8 @@ const Step1 = ({ examData, setExamData }: Step1Props) => { handleChange("examLocatoin", e.target.value)} + value={examData.examLocation} + onChange={(e) => handleChange("examLocation", e.target.value)} placeholder="입력해주세요" className={inputClassName} /> From 9b3808b82877e8610a599d7225dfbcdcf66cbd90 Mon Sep 17 00:00:00 2001 From: Hyeona01 Date: Thu, 5 Dec 2024 17:40:10 +0900 Subject: [PATCH 6/6] =?UTF-8?q?FE:=20[fix]=20examLocation=20=EC=98=A4?= =?UTF-8?q?=ED=83=80=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/frontend/eyesee-admin/src/types/exam.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/frontend/eyesee-admin/src/types/exam.ts b/src/frontend/eyesee-admin/src/types/exam.ts index 0ec1a56..44472e0 100644 --- a/src/frontend/eyesee-admin/src/types/exam.ts +++ b/src/frontend/eyesee-admin/src/types/exam.ts @@ -4,7 +4,7 @@ export type ExamRequest = { examName: string; examSemester: string; examStudentNumber: number; - examLocatoin: string; + examLocation: string; examDate: string; examStartTime: string; examDuration: number; @@ -18,7 +18,7 @@ export const initialExamData: ExamRequest = { examName: "", examSemester: "", examStudentNumber: 0, - examLocatoin: "", + examLocation: "", examDate: "", examStartTime: "", examDuration: 0,