diff --git a/package-lock.json b/package-lock.json
index 43065e5..600e17a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -10,6 +10,8 @@
"dependencies": {
"@aws-sdk/client-s3": "^3.685.0",
"@aws-sdk/s3-request-presigner": "^3.685.0",
+ "@mediapipe/camera_utils": "^0.3.1675466862",
+ "@mediapipe/face_detection": "^0.4.1646425229",
"axios": "^1.7.7",
"chart.js": "^4.4.6",
"firebase": "^9.10.0",
@@ -2136,6 +2138,16 @@
"resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz",
"integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w=="
},
+ "node_modules/@mediapipe/camera_utils": {
+ "version": "0.3.1675466862",
+ "resolved": "https://registry.npmjs.org/@mediapipe/camera_utils/-/camera_utils-0.3.1675466862.tgz",
+ "integrity": "sha512-siuXBoUxWo9WL0MeAxIxvxY04bvbtdNl7uCxoJxiAiRtNnCYrurr7Vl5VYQ94P7Sq0gVq6PxIDhWWeZ/pLnSzw=="
+ },
+ "node_modules/@mediapipe/face_detection": {
+ "version": "0.4.1646425229",
+ "resolved": "https://registry.npmjs.org/@mediapipe/face_detection/-/face_detection-0.4.1646425229.tgz",
+ "integrity": "sha512-aeCN+fRAojv9ch3NXorP6r5tcGVLR3/gC1HmtqB0WEZBRXrdP6/3W/sGR0dHr1iT6ueiK95G9PVjbzFosf/hrg=="
+ },
"node_modules/@next/env": {
"version": "15.0.1",
"resolved": "https://registry.npmjs.org/@next/env/-/env-15.0.1.tgz",
diff --git a/package.json b/package.json
index 97b0c94..1eb90ce 100644
--- a/package.json
+++ b/package.json
@@ -11,6 +11,8 @@
"dependencies": {
"@aws-sdk/client-s3": "^3.685.0",
"@aws-sdk/s3-request-presigner": "^3.685.0",
+ "@mediapipe/camera_utils": "^0.3.1675466862",
+ "@mediapipe/face_detection": "^0.4.1646425229",
"axios": "^1.7.7",
"chart.js": "^4.4.6",
"firebase": "^9.10.0",
diff --git a/src/app/interview/ongoing/prepare/page.tsx b/src/app/interview/ongoing/prepare/page.tsx
index 1d4c2be..fa1160f 100644
--- a/src/app/interview/ongoing/prepare/page.tsx
+++ b/src/app/interview/ongoing/prepare/page.tsx
@@ -10,6 +10,7 @@ import { getMessaging, isSupported, onMessage } from "firebase/messaging";
import { firebaseApp } from "@/utils/firebaseConfig";
import useInterviewStore from "@/stores/useInterviewStore";
import MicTest from "@/components/mic-test";
+import VideoTest from "@/components/video-test-2";
const apiUrl = `${setUrl}`;
@@ -25,6 +26,7 @@ const InterviewOngoingPreparePage = () => {
let hasFetched = false;
const requestQuestionList = async () => {
+ console.log("questionRequest: ", questionRequest);
if (!hasFetched) {
hasFetched = true;
await axios.post(`${apiUrl}/question`, questionRequest, {
@@ -140,13 +142,21 @@ const InterviewOngoingPreparePage = () => {
return (
- {interview.interviewMethod === "CHAT" ? (
+ {interview.interviewMethod === "CHAT" && (
- ) : isReady ? (
+ )}
+ {interview.interviewMethod === "VOICE" && isReady && (
- ) : (
+ )}
+ {interview.interviewMethod === "VOICE" && !isReady && (
)}
+ {interview.interviewMethod === "VIDEO" && isReady && (
+
+ )}
+ {interview.interviewMethod === "VIDEO" && !isReady && (
+
+ )}
);
};
diff --git a/src/components/RecordingIndicator.tsx b/src/components/RecordingIndicator.tsx
index 9da0731..90fde8f 100644
--- a/src/components/RecordingIndicator.tsx
+++ b/src/components/RecordingIndicator.tsx
@@ -1,13 +1,111 @@
+// "use client";
+
+// import { motion } from "framer-motion";
+// import { BsMicFill } from "react-icons/bs";
+
+// const RecordingIndicator = ({ isRecording }: { isRecording: boolean }) => {
+// const ringVariants = {
+// active: {
+// scale: [1, 1.3, 1],
+// opacity: [0.5, 0.2, 0.5],
+// borderColor: ["#60a5fa", "#c084fc"],
+// transition: {
+// duration: 1.2,
+// repeat: Infinity,
+// ease: "easeInOut",
+// },
+// },
+// inactive: {
+// scale: 1,
+// opacity: 0.3,
+// borderColor: "#cbd5e1",
+// },
+// };
+
+// const highlightVariants = {
+// active: {
+// rotate: [0, 360],
+// transition: {
+// duration: 2,
+// repeat: Infinity,
+// ease: "linear",
+// },
+// },
+// inactive: { rotate: 0 },
+// };
+
+// return (
+//
+//
+//
+
+//
+
+//
+//
+//
+// {isRecording ? "답변 중" : "답변을 준비해주세요"}
+//
+//
+// );
+// };
+
+// export default RecordingIndicator;
+
"use client";
import { motion } from "framer-motion";
+import { useEffect, useRef, useState } from "react";
import { BsMicFill } from "react-icons/bs";
-const RecordingIndicator = ({ isRecording }: { isRecording: boolean }) => {
- const ringVariants = {
+const CameraIndicator = ({ isRecording }: { isRecording: boolean }) => {
+ const videoRef = useRef(null);
+ const [isCameraOn, setIsCameraOn] = useState(false);
+
+ useEffect(() => {
+ console.log("isRecording", isRecording);
+ const enableCamera = async () => {
+ try {
+ const stream = await navigator.mediaDevices.getUserMedia({
+ video: true,
+ });
+ if (videoRef.current) {
+ videoRef.current.srcObject = stream;
+ setIsCameraOn(true);
+ }
+ } catch (error) {
+ console.error("Error accessing camera: ", error);
+ setIsCameraOn(false);
+ }
+ };
+
+ enableCamera();
+ console.log("isCameraOn", isCameraOn);
+ // else {
+ // if (videoRef.current && videoRef.current.srcObject) {
+ // const tracks = (videoRef.current.srcObject as MediaStream).getTracks();
+ // tracks.forEach((track) => track.stop());
+ // videoRef.current.srcObject = null;
+ // setIsCameraOn(false);
+ // }
+ // }
+ }, []);
+
+ const borderVariants = {
active: {
- scale: [1, 1.3, 1],
- opacity: [0.5, 0.2, 0.5],
borderColor: ["#60a5fa", "#c084fc"],
transition: {
duration: 1.2,
@@ -16,46 +114,27 @@ const RecordingIndicator = ({ isRecording }: { isRecording: boolean }) => {
},
},
inactive: {
- scale: 1,
- opacity: 0.3,
borderColor: "#cbd5e1",
},
};
- const highlightVariants = {
- active: {
- rotate: [0, 360],
- transition: {
- duration: 2,
- repeat: Infinity,
- ease: "linear",
- },
- },
- inactive: { rotate: 0 },
- };
-
return (
-
-
-
-
-
-
+
+
+
{isRecording ? "답변 중" : "답변을 준비해주세요"}
@@ -63,4 +142,4 @@ const RecordingIndicator = ({ isRecording }: { isRecording: boolean }) => {
);
};
-export default RecordingIndicator;
+export default CameraIndicator;
diff --git a/src/components/interview/step/check-info-step.tsx b/src/components/interview/step/check-info-step.tsx
index 5cfdf78..f8d9458 100644
--- a/src/components/interview/step/check-info-step.tsx
+++ b/src/components/interview/step/check-info-step.tsx
@@ -115,7 +115,8 @@ const CheckInfoStep = ({ onPrev, onNext, onSubmit }: StepSubmitProps) => {
{
interviewTitle: getInterviewTitle(interview),
interviewType: interview.interviewType,
- interviewMethod: interview.interviewMethod,
+ interviewMethod:
+ interview.interviewMethod === "CHAT" ? "CHAT" : "VOICE",
interviewMode: interview.interviewMode,
jobId: interview.jobId,
questionCount: selectedQuestionCount,
@@ -186,7 +187,8 @@ const CheckInfoStep = ({ onPrev, onNext, onSubmit }: StepSubmitProps) => {
interviewTitle: data.data.interviewTitle,
interviewStatus: data.data.interviewStatus,
interviewType: data.data.interviewType,
- interviewMethod: data.data.interviewMethod,
+ interviewMethod:
+ data.data.interviewMethod === "CHAT" ? "CHAT" : "VOICE",
interviewMode: data.data.interviewMode,
questionCount: data.data.questionCount,
files:
diff --git a/src/components/interview/step/method-step.tsx b/src/components/interview/step/method-step.tsx
index 179a6ec..eafeece 100644
--- a/src/components/interview/step/method-step.tsx
+++ b/src/components/interview/step/method-step.tsx
@@ -104,10 +104,9 @@ const MethodStep = ({ onPrev, onNext }: StepProps) => {
/>
setSelectedMethod("VIDEO")}
- disabled={true}
/>
diff --git a/src/components/video-test-2.tsx b/src/components/video-test-2.tsx
new file mode 100644
index 0000000..aa6ddca
--- /dev/null
+++ b/src/components/video-test-2.tsx
@@ -0,0 +1,320 @@
+import React, { useEffect, useRef, useState } from "react";
+
+interface VideoTestProps {
+ handleSetReady: (isVideoChecked: boolean) => void;
+}
+
+const VideoTest = ({ handleSetReady }: VideoTestProps) => {
+ const [microphonePermission, setMicrophonePermission] = useState(false);
+ const [volumeLevel, setVolumeLevel] = useState(0);
+ const [isMicrophoneChecked, setIsMicrophoneChecked] = useState(false);
+ const [audioProgress, setAudioProgress] = useState(0);
+ const [isPlaying, setIsPlaying] = useState(false);
+ const [isAudioChecked, setIsAudioChecked] = useState(false);
+ const [intervalId, setIntervalId] = useState(null);
+ const [audioInstance, setAudioInstance] = useState(
+ null
+ );
+
+ const videoRef = useRef(null);
+ const [hasWebcam, setHasWebcam] = useState(false);
+ const [isVideoChecked, setIsVideoChecked] = useState(false);
+ const [isVideoActive, setIsVideoActive] = useState(false);
+
+ useEffect(() => {
+ if (!hasWebcam) return;
+
+ const checkInterval = setInterval(checkVideoActive, 1000);
+ return () => clearInterval(checkInterval);
+ }, [hasWebcam]);
+
+ useEffect(() => {
+ const requestMicrophoneAccess = async () => {
+ try {
+ const stream = await navigator.mediaDevices.getUserMedia({
+ audio: true,
+ });
+ setMicrophonePermission(true);
+
+ const audioContext = new (window.AudioContext ||
+ (window as unknown as { webkitAudioContext: typeof AudioContext })
+ .webkitAudioContext)();
+ const source = audioContext.createMediaStreamSource(stream);
+ const analyser = audioContext.createAnalyser();
+
+ analyser.fftSize = 256;
+ const dataArray = new Uint8Array(analyser.frequencyBinCount);
+
+ const updateVolume = () => {
+ analyser.getByteFrequencyData(dataArray);
+ const volume =
+ dataArray.reduce((a, b) => a + b, 0) / dataArray.length;
+
+ const scaledVolume = Math.min(100, volume * 1.5);
+ setVolumeLevel(Math.floor(scaledVolume));
+
+ // if (scaledVolume > 20) {
+ // setIsMicrophoneChecked(true);
+ // }
+
+ requestAnimationFrame(updateVolume);
+ };
+
+ source.connect(analyser);
+ updateVolume();
+ } catch (error) {
+ console.error("마이크 접근 오류:", error);
+ setMicrophonePermission(false);
+ }
+ };
+
+ requestMicrophoneAccess();
+ }, []);
+
+ useEffect(() => {
+ const setupWebcam = async () => {
+ try {
+ const stream = await navigator.mediaDevices.getUserMedia({
+ video: true,
+ });
+
+ if (videoRef.current) {
+ videoRef.current.srcObject = stream;
+ setHasWebcam(true);
+ console.log("turn on the camera");
+
+ // 비디오 스트림이 실제로 재생되는지 확인
+ videoRef.current.onplaying = () => {
+ setIsVideoActive(true);
+ };
+ }
+ } catch (error) {
+ console.error("웹캠 접근 오류:", error);
+ setHasWebcam(false);
+ }
+ };
+
+ setupWebcam();
+
+ return () => {
+ if (videoRef.current?.srcObject) {
+ const tracks = (videoRef.current.srcObject as MediaStream).getTracks();
+ tracks.forEach((track) => track.stop());
+ }
+ };
+ }, [handleSetReady]);
+
+ const togglePlayAudio = () => {
+ if (isPlaying && audioInstance) {
+ audioInstance.pause();
+ setAudioProgress(0);
+ setIsPlaying(false);
+ setAudioInstance(null);
+ if (intervalId) {
+ clearInterval(intervalId);
+ }
+ return;
+ }
+
+ const audio = new Audio("/audiotest.mp3");
+ setAudioInstance(audio);
+ setIsPlaying(true);
+ setAudioProgress(0);
+
+ audio.play();
+ const newIntervalId = setInterval(() => {
+ if (audio.ended) {
+ clearInterval(newIntervalId);
+ setAudioProgress(100);
+ setIsPlaying(false);
+ } else {
+ setAudioProgress((audio.currentTime / audio.duration) * 100);
+ }
+ }, 100);
+
+ setIntervalId(newIntervalId);
+
+ audio.onended = () => {
+ clearInterval(newIntervalId);
+ setAudioProgress(100);
+ setIsPlaying(false);
+ setAudioInstance(null);
+ };
+
+ audio.onerror = () => {
+ setIsPlaying(false);
+ setAudioProgress(0);
+ alert("오디오 파일 재생에 실패했습니다.");
+ };
+ };
+
+ const handleClickStartButton = () => {
+ if (audioInstance) {
+ audioInstance.pause();
+ audioInstance.currentTime = 0;
+ setAudioInstance(null);
+ }
+
+ handleSetReady(true);
+ };
+
+ // 비디오 프레임이 어두운지 확인하는 함수
+ const checkVideoActive = () => {
+ if (!videoRef.current) return;
+
+ const canvas = document.createElement("canvas");
+ const context = canvas.getContext("2d");
+ if (!context) return;
+
+ canvas.width = videoRef.current.videoWidth;
+ canvas.height = videoRef.current.videoHeight;
+
+ context.drawImage(videoRef.current, 0, 0);
+ const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
+ const pixels = imageData.data;
+
+ // 프레임의 평균 밝기 계산
+ let totalBrightness = 0;
+ for (let i = 0; i < pixels.length; i += 4) {
+ const r = pixels[i];
+ const g = pixels[i + 1];
+ const b = pixels[i + 2];
+ totalBrightness += (r + g + b) / 3;
+ }
+ const averageBrightness = totalBrightness / (pixels.length / 4);
+
+ // 평균 밝기가 특정 임계값보다 높으면 활성 상태로 간주
+ setIsVideoActive(averageBrightness > 30);
+ };
+
+ return (
+
+
+
+
음성 테스트
+ {!microphonePermission ? (
+
+ 마이크 접근 권한이 필요합니다. 설정을 확인해주세요.
+
+ ) : (
+
+
+ {[...Array(10)].map((_, i) => (
+
= (i + 1) * 10
+ ? "bg-blue-500"
+ : "bg-gray-300"
+ }`}
+ style={{ width: `${20 + i * 5}px` }}
+ >
+ ))}
+
+ {!isMicrophoneChecked ? (
+
음성 인식 중...
+ ) : (
+
+ )}
+
+
+ )}
+
+
+
+
+
영상 테스트
+
+
+ {!isVideoChecked ? (
+
영상 확인 중...
+ ) : (
+
+ )}
+
+
+
+
+
+
+
오디오 테스트
+
+
+ {!isAudioChecked ? (
+
오디오 확인 중...
+ ) : (
+
+ )}
+
+
+
+
+
+
+
+ );
+};
+
+export default VideoTest;
diff --git a/src/components/video-test.tsx b/src/components/video-test.tsx
new file mode 100644
index 0000000..6576aa7
--- /dev/null
+++ b/src/components/video-test.tsx
@@ -0,0 +1,148 @@
+import React, { useEffect, useRef, useState } from "react";
+
+interface VideoTestProps {
+ handleSetReady: (isReady: boolean) => void;
+}
+
+const VideoTest = ({ handleSetReady }: VideoTestProps) => {
+ const videoRef = useRef(null);
+ const [hasWebcam, setHasWebcam] = useState(false);
+ const [isActive, setIsActive] = useState(false);
+ const [isReady, setIsReady] = useState(false);
+ const activeTimerRef = useRef(null);
+
+ useEffect(() => {
+ const setupWebcam = async () => {
+ try {
+ const stream = await navigator.mediaDevices.getUserMedia({
+ video: true,
+ });
+
+ if (videoRef.current) {
+ videoRef.current.srcObject = stream;
+ setHasWebcam(true);
+ console.log("turn on the camera");
+
+ // 비디오 스트림이 실제로 재생되는지 확인
+ videoRef.current.onplaying = () => {
+ setIsActive(true);
+ // 2초 동안 스트림이 활성화되어 있으면 준비 완료로 간주
+ activeTimerRef.current = setTimeout(() => {
+ setIsReady(true);
+ // handleSetReady(true);
+ }, 2000);
+ };
+ }
+ } catch (error) {
+ console.error("웹캠 접근 오류:", error);
+ setHasWebcam(false);
+ }
+ };
+
+ setupWebcam();
+
+ return () => {
+ if (activeTimerRef.current) {
+ clearTimeout(activeTimerRef.current);
+ }
+ if (videoRef.current?.srcObject) {
+ const tracks = (videoRef.current.srcObject as MediaStream).getTracks();
+ tracks.forEach((track) => track.stop());
+ }
+ };
+ }, [handleSetReady]);
+
+ // 비디오 프레임이 어두운지 확인하는 함수
+ const checkVideoActive = () => {
+ if (!videoRef.current) return;
+
+ const canvas = document.createElement("canvas");
+ const context = canvas.getContext("2d");
+ if (!context) return;
+
+ canvas.width = videoRef.current.videoWidth;
+ canvas.height = videoRef.current.videoHeight;
+
+ context.drawImage(videoRef.current, 0, 0);
+ const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
+ const pixels = imageData.data;
+
+ // 프레임의 평균 밝기 계산
+ let totalBrightness = 0;
+ for (let i = 0; i < pixels.length; i += 4) {
+ const r = pixels[i];
+ const g = pixels[i + 1];
+ const b = pixels[i + 2];
+ totalBrightness += (r + g + b) / 3;
+ }
+ const averageBrightness = totalBrightness / (pixels.length / 4);
+
+ // 평균 밝기가 특정 임계값보다 높으면 활성 상태로 간주
+ setIsActive(averageBrightness > 30);
+ };
+
+ // 주기적으로 비디오 활성 상태 체크
+ useEffect(() => {
+ if (!hasWebcam) return;
+
+ const checkInterval = setInterval(checkVideoActive, 1000);
+ return () => clearInterval(checkInterval);
+ }, [hasWebcam]);
+
+ return (
+
+
+
+
영상 테스트
+ {/* {!hasWebcam ? (
+
+ 카메라 접근 권한이 필요합니다. 설정을 확인해주세요.
+
+ ) : ( */}
+
+
+
+ {isActive
+ ? isReady
+ ? "화면 테스트 완료 ✓"
+ : "영상 확인 중... 잠시만 기다려주세요"
+ : "카메라를 확인해주세요"}
+
+
+
+ {/* )} */}
+
+
+
+
+
+
+
+
+ );
+};
+
+export default VideoTest;