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}
/>
diff --git a/src/frontend/eyesee-admin/src/components/report/ReportSection.tsx b/src/frontend/eyesee-admin/src/components/report/ReportSection.tsx
index d665790..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) => {
엑셀 다운로드
-
+
종료된 시험입니다.
@@ -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}건{", "}
+
+ )
+ )}
+
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,
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..b940b09 100644
--- a/src/frontend/eyesee-user/src/app/exam-room/page.tsx
+++ b/src/frontend/eyesee-user/src/app/exam-room/page.tsx
@@ -2,30 +2,42 @@
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 = () => {
+ // 수험자 실시간 화면이 담기는 공간
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 userId = useStore(useUserIdStore, (state) => state.userId);
- const examId = 1;
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 &&
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;
};