From b8f51021d86ce37e14e002b3bc1a3f8e1ae0c53e Mon Sep 17 00:00:00 2001 From: bada308 <203190@jnu.ac.kr> Date: Tue, 30 Jul 2024 16:32:55 +0900 Subject: [PATCH 1/8] =?UTF-8?q?refactor:=20=EC=98=A4=EB=8A=98=20=EB=82=A0?= =?UTF-8?q?=EC=A7=9C=20=EB=B3=B4=EB=82=B4=EB=8A=94=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/apis/auth/logout.ts | 2 - src/apis/challenge/getChallengeList.ts | 6 +-- src/apis/challenge/getChallengeStartDate.ts | 28 ----------- src/apis/challenge/getTodayChallenge.ts | 4 +- src/apis/challenge/postCreateChallenge.ts | 15 ++---- src/apis/lucky/getLucky.ts | 13 ++--- src/apis/lucky/postLuckyBubble.ts | 8 +--- src/components/main/Navbar.tsx | 53 --------------------- 8 files changed, 10 insertions(+), 119 deletions(-) delete mode 100644 src/apis/challenge/getChallengeStartDate.ts diff --git a/src/apis/auth/logout.ts b/src/apis/auth/logout.ts index 7af91a7..2fb5b61 100644 --- a/src/apis/auth/logout.ts +++ b/src/apis/auth/logout.ts @@ -18,8 +18,6 @@ export const useLogout = () => { mutationFn: () => postLogout(), onSettled: () => { deleteCookie('authorization'); - localStorage.removeItem('challengeDate'); - localStorage.removeItem('challengeStartDate'); navigate(routes.login, { replace: true }); }, onError: (error) => { diff --git a/src/apis/challenge/getChallengeList.ts b/src/apis/challenge/getChallengeList.ts index deff1ed..2001009 100644 --- a/src/apis/challenge/getChallengeList.ts +++ b/src/apis/challenge/getChallengeList.ts @@ -6,11 +6,7 @@ import { nextDate } from '../../utils/nextDate'; import { ChallengeListUnitDto } from '../dtos/challengeDtos'; const getChallengeList = async () => { - const challengeDate = localStorage.getItem('challengeDate'); - - const { data } = await authorizedApi.get( - `${API.CHALLENGE.LIST}?challengeDate=${challengeDate ? challengeDate : new Date().getTime()}`, - ); + const { data } = await authorizedApi.get(API.CHALLENGE.LIST); return { challenges: data.data.map( diff --git a/src/apis/challenge/getChallengeStartDate.ts b/src/apis/challenge/getChallengeStartDate.ts deleted file mode 100644 index a41abad..0000000 --- a/src/apis/challenge/getChallengeStartDate.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { useQuery } from '@tanstack/react-query'; -import { authorizedApi } from '..'; -import API from '../../constants/API'; - -const getChallengeStartDate = async () => { - const { data } = await authorizedApi.get(API.CHALLENGE.STARTDATE); - - return { - challengeStartDate: data.data.challengeStartDate, - }; -}; - -export const useGetChallengeStartDate = () => { - const { data, isLoading } = useQuery({ - queryKey: [API.CHALLENGE.STARTDATE], - queryFn: () => getChallengeStartDate(), - enabled: !localStorage.getItem('challengeStartDate'), - }); - - if (data) { - localStorage.setItem('challengeStartDate', data.challengeStartDate); - localStorage.setItem('challengeDate', data.challengeStartDate); - } - - return { - isLoading, - }; -}; diff --git a/src/apis/challenge/getTodayChallenge.ts b/src/apis/challenge/getTodayChallenge.ts index 2c50c92..57ae0f5 100644 --- a/src/apis/challenge/getTodayChallenge.ts +++ b/src/apis/challenge/getTodayChallenge.ts @@ -6,10 +6,8 @@ import { ChallengeToday } from '../../types/challenge'; import { ChallengeInfoDto } from '../dtos/challengeDtos'; const getTodayChallenge = async () => { - const challengeDate = localStorage.getItem('challengeDate'); - const { data } = await authorizedApi.get>( - `${API.CHALLENGE.TODAY}?challengeDate=${challengeDate ? challengeDate : new Date().getTime().toString()}`, + API.CHALLENGE.TODAY, ); if (data.status === '200') diff --git a/src/apis/challenge/postCreateChallenge.ts b/src/apis/challenge/postCreateChallenge.ts index 0cd7d62..f7d3b01 100644 --- a/src/apis/challenge/postCreateChallenge.ts +++ b/src/apis/challenge/postCreateChallenge.ts @@ -2,25 +2,18 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'; import { authorizedApi } from '..'; import API from '../../constants/API'; -const postCreateChallenge = async (challengeDate: number) => { - const { data } = await authorizedApi.post(API.CHALLENGE.CREATE, { - challengeDate, - }); +const postCreateChallenge = async () => { + const { data } = await authorizedApi.post(API.CHALLENGE.CREATE); return data; }; export const useCreateChallenge = () => { const queryClient = useQueryClient(); - const storageDate = localStorage.getItem('challengeDate'); - - const challengeDate = storageDate - ? parseInt(storageDate) - : new Date().getTime(); return useMutation({ - mutationKey: [API.CHALLENGE.CREATE, challengeDate], - mutationFn: () => postCreateChallenge(challengeDate), + mutationKey: [API.CHALLENGE.CREATE], + mutationFn: () => postCreateChallenge(), onSettled: () => { queryClient.invalidateQueries({ queryKey: [API.CHALLENGE.TODAY], diff --git a/src/apis/lucky/getLucky.ts b/src/apis/lucky/getLucky.ts index c6a2b46..3948615 100644 --- a/src/apis/lucky/getLucky.ts +++ b/src/apis/lucky/getLucky.ts @@ -3,11 +3,9 @@ import { authorizedApi } from '..'; import API from '../../constants/API'; import { LuckyDto } from '../dtos/luckyDtos'; -const getLucky = async (challengeDate: string) => { +const getLucky = async () => { try { - const { data } = await authorizedApi.get( - `${API.LUCKY.STATUS}?challengeDate=${challengeDate}`, - ); + const { data } = await authorizedApi.get(API.LUCKY.STATUS); if (data.data === null) return { luckyStatus: 0, isBubble: false } as LuckyDto; @@ -22,13 +20,8 @@ const getLucky = async (challengeDate: string) => { }; export const useGetLucky = () => { - const storageDate = localStorage.getItem('challengeDate'); - const challengeDate = storageDate - ? storageDate - : new Date().getTime().toString(); - return useSuspenseQuery({ queryKey: [API.LUCKY.STATUS], - queryFn: () => getLucky(challengeDate), + queryFn: () => getLucky(), }); }; diff --git a/src/apis/lucky/postLuckyBubble.ts b/src/apis/lucky/postLuckyBubble.ts index 5d54631..72fa92b 100644 --- a/src/apis/lucky/postLuckyBubble.ts +++ b/src/apis/lucky/postLuckyBubble.ts @@ -3,13 +3,7 @@ import { authorizedApi } from '..'; import API from '../../constants/API'; const postLuckyBubble = async () => { - const challengeDate = localStorage.getItem('challengeDate'); - - const { data } = await authorizedApi.post(`${API.LUCKY.BUBBLE}`, { - challengeDate: challengeDate - ? challengeDate - : new Date().getTime().toString(), - }); + const { data } = await authorizedApi.post(API.LUCKY.BUBBLE); return data; }; diff --git a/src/components/main/Navbar.tsx b/src/components/main/Navbar.tsx index a4f3a2f..f625959 100644 --- a/src/components/main/Navbar.tsx +++ b/src/components/main/Navbar.tsx @@ -1,6 +1,4 @@ import { FaListUl } from 'react-icons/fa'; -import { GrPowerReset } from 'react-icons/gr'; -import { TbPlayerTrackNextFilled } from 'react-icons/tb'; import { useNavigate } from 'react-router-dom'; import routes from '../../constants/routes'; import IconButton from '../common/IconButton'; @@ -10,45 +8,6 @@ const Navbar = () => { navigate(routes.challenge); }; - const handleDayChange = () => { - const ONE_DAY_MILI_SEC = 86400000; - const challengeStartDate = localStorage.getItem('challengeStartDate'); - const challengeDate = localStorage.getItem('challengeDate'); - - if (challengeDate && challengeStartDate) { - const startDate = parseInt(challengeStartDate); - const nowDate = parseInt(challengeDate); - - const passDays = (nowDate - startDate) / ONE_DAY_MILI_SEC; - - console.log(passDays); - - if (passDays == 0) { - localStorage.setItem( - 'challengeDate', - `${nowDate + ONE_DAY_MILI_SEC * 14}`, - ); - } else if (passDays == 14) { - localStorage.setItem( - 'challengeDate', - `${nowDate + ONE_DAY_MILI_SEC * 15}`, - ); - } else { - localStorage.setItem('challengeDate', `${nowDate + ONE_DAY_MILI_SEC}`); - } - - window.location.reload(); - } - }; - - const handleResetDay = () => { - const challengeStartDate = localStorage.getItem('challengeStartDate'); - - if (challengeStartDate) - localStorage.setItem('challengeDate', challengeStartDate); - window.location.reload(); - }; - return (
{ label="챌린지" onClick={handleChallengeBtn} /> - } - label="다음" - onClick={handleDayChange} - /> - } - label="초기화" - onClick={handleResetDay} - />
); }; From 0085cfbbba4946ae83c0ce4fa84f61422e1803c5 Mon Sep 17 00:00:00 2001 From: bada308 <203190@jnu.ac.kr> Date: Wed, 31 Jul 2024 15:50:12 +0900 Subject: [PATCH 2/8] =?UTF-8?q?feat:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=EC=B1=8C=EB=A6=B0=EC=A7=80=20=EC=8B=9C=EC=9E=91=20?= =?UTF-8?q?=EB=82=A0=EC=A7=9C=20=EC=A1=B0=ED=9A=8C=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/MainPage.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/pages/MainPage.tsx b/src/pages/MainPage.tsx index 20bc891..34b3165 100644 --- a/src/pages/MainPage.tsx +++ b/src/pages/MainPage.tsx @@ -1,7 +1,6 @@ import QueryString from 'qs'; import { Suspense, useEffect } from 'react'; import { Navigate } from 'react-router-dom'; -import { useGetChallengeStartDate } from '../apis/challenge/getChallengeStartDate'; import { useGetMyFamilyInfo } from '../apis/family/getMyFamilyInfo'; import Loader from '../components/common/Loader'; import ChallengeSection from '../components/main/ChallengeSection'; @@ -15,7 +14,6 @@ const MainPage = () => { ignoreQueryPrefix: true, }); const { data: familyList, isLoading: familyLoading } = useGetMyFamilyInfo(); - const { isLoading: startDateLoading } = useGetChallengeStartDate(); const { openModal, isOpen } = useModalStore(); useEffect(() => { @@ -32,7 +30,7 @@ const MainPage = () => { } }, [familyLoading, familyList]); - if (familyLoading || startDateLoading) return
가족 정보 Loading...
; + if (familyLoading) return
가족 정보 Loading...
; if (!familyList) return ; return ( From 68ef9d8b248fd6c14ebad2b9c89e86a0d19efb53 Mon Sep 17 00:00:00 2001 From: Parksangwoo Date: Wed, 31 Jul 2024 16:01:17 +0900 Subject: [PATCH 3/8] =?UTF-8?q?chore:=20=EC=8B=9C=EC=97=B0=EC=9D=84=20?= =?UTF-8?q?=EC=9C=84=ED=95=B4=20=EC=88=98=EC=A0=95=ED=95=9C=20=EB=94=94?= =?UTF-8?q?=EC=9E=90=EC=9D=B8=20=EB=A1=A4=EB=B0=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/challengelist/ChallengeListUnit.tsx | 2 +- src/components/main/ChallengeButton.tsx | 8 ++++---- src/components/qa-challenge/ChallengeHeader.tsx | 4 ++-- src/components/qa-challenge/PhotoChallengeContainer.tsx | 3 +++ src/global.css | 2 +- src/pages/ChallengeListPage.tsx | 4 +--- 6 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/components/challengelist/ChallengeListUnit.tsx b/src/components/challengelist/ChallengeListUnit.tsx index 9f2e0ac..3e372bf 100644 --- a/src/components/challengelist/ChallengeListUnit.tsx +++ b/src/components/challengelist/ChallengeListUnit.tsx @@ -35,7 +35,7 @@ const ChallengeListUnit = ({ return (
-

{`#${parseInt(challengeNumber) > 9 ? challengeNumber : '0' + challengeNumber}`}

+

{`#${parseInt(challengeNumber) > 9 ? challengeNumber : '0' + challengeNumber}`}

diff --git a/src/components/main/ChallengeButton.tsx b/src/components/main/ChallengeButton.tsx index 410f6e0..7148785 100644 --- a/src/components/main/ChallengeButton.tsx +++ b/src/components/main/ChallengeButton.tsx @@ -33,8 +33,8 @@ type ChallengeButtonProps = const adjustFontSize = (text: string) => { return text.length > 10 - ? 'xs:text-ryurue-md text-ryurue-base' - : ' xs:text-ryurue-lg text-ryurue-md'; + ? 'xs:text-ryurue-base text-ryurue-sm' + : ' xs:text-ryurue-md text-ryurue-base'; }; const renderTypeInfo = (props: ChallengeButtonProps) => { @@ -63,7 +63,7 @@ const ChallengeButton = (props: ChallengeButtonProps) => { const { mutate } = useCreateChallenge(); const layoutClass = clsx( - 'shadow h-32 w-full items-center justify-center gap-4 rounded-md bg-paper bg-contain px-9 py-5 font-ryurue', + 'shadow h-36 w-full items-center justify-center gap-4 rounded-md bg-paper bg-contain px-9 py-5 font-ryurue', { flex: type === 'active', 'grid grid-rows-[24px_1fr]': type !== 'active', @@ -71,7 +71,7 @@ const ChallengeButton = (props: ChallengeButtonProps) => { }, ); const textClass = clsx( - 'line-clamp-1 text-pretty break-keep', + 'line-clamp-2 text-pretty break-keep', adjustFontSize(text), { 'text-gray-40': type === 'disabled' }, ); diff --git a/src/components/qa-challenge/ChallengeHeader.tsx b/src/components/qa-challenge/ChallengeHeader.tsx index 13523ad..744765c 100644 --- a/src/components/qa-challenge/ChallengeHeader.tsx +++ b/src/components/qa-challenge/ChallengeHeader.tsx @@ -12,12 +12,12 @@ const ChallengeHeader = ({ return (

{challengeNumber && challengeDate && ( -
+
# {challengeNumber}번째 질문 {challengeDate}
)} - + {challengeTitle}
diff --git a/src/components/qa-challenge/PhotoChallengeContainer.tsx b/src/components/qa-challenge/PhotoChallengeContainer.tsx index e562a6b..9165b17 100644 --- a/src/components/qa-challenge/PhotoChallengeContainer.tsx +++ b/src/components/qa-challenge/PhotoChallengeContainer.tsx @@ -23,6 +23,9 @@ const PhotoChallengeContainer = () => { challengeDate={formatDate(challengeInfo.challengeDate)} challengeTitle={challengeInfo.challengeTitle} /> +

+ 가로 스크롤을 통해 가족들의 사진을 구경하세요! +

{ return (
-

- 챌린지 -

+

챌린지

{challenges.length > 0 ? (
{challenges.map((challenge) => ( From 55b487b30fe90056508f08c580da9f76fac37c67 Mon Sep 17 00:00:00 2001 From: Parksangwoo Date: Wed, 31 Jul 2024 16:04:13 +0900 Subject: [PATCH 4/8] =?UTF-8?q?chore:=20fcm=20=EC=8B=9C=EC=97=B0=20?= =?UTF-8?q?=EB=B2=84=ED=8A=BC=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/mypage/UserInfo.tsx | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/src/components/mypage/UserInfo.tsx b/src/components/mypage/UserInfo.tsx index 8c69405..70dffdd 100644 --- a/src/components/mypage/UserInfo.tsx +++ b/src/components/mypage/UserInfo.tsx @@ -1,6 +1,3 @@ -import { useState } from 'react'; -import CopyToClipboard from 'react-copy-to-clipboard'; -import { getFcmToken } from '../../apis/auth/fcmToken'; import { useLogout } from '../../apis/auth/logout'; import { useGetUserInfo } from '../../apis/user/getUserInfo'; import { SYS_MESSAGE } from '../../constants/message'; @@ -15,7 +12,6 @@ const UserInfo = () => { const { userInfo, isLoading } = useGetUserInfo(); const { openModal } = useModalStore(); const { mutate } = useLogout(); - const [fcm, setFcm] = useState(''); if (isLoading) return
Loading...
; if (!userInfo) return
{SYS_MESSAGE.NO_DATA}
; @@ -38,19 +34,6 @@ const UserInfo = () => { mutate(); }; - const requestPermission = async () => { - try { - const token = await getFcmToken(); - if (token) { - setFcm(token); - // await postFcmToken(token); - // Send the token to your server and update the UI if necessary - } - } catch (err) { - console.error('An error occurred while retrieving token. ', err); - } - }; - return (
@@ -63,10 +46,6 @@ const UserInfo = () => { src={userInfo.userImg} isText /> - - -
From 00132f55b5b698effd632024b094bcad761c8446 Mon Sep 17 00:00:00 2001 From: bada308 <203190@jnu.ac.kr> Date: Wed, 31 Jul 2024 18:45:19 +0900 Subject: [PATCH 5/8] =?UTF-8?q?feat:=20web=20worker=20=EC=9D=B4=EC=9A=A9?= =?UTF-8?q?=ED=95=B4=EC=84=9C=20=EC=96=BC=EA=B5=B4=20=EC=9D=B8=EC=8B=9D=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/useFaceFilterWithModel.ts | 144 +++++++++++++++------------- src/utils/faceDetectionWorker.ts | 36 +++++++ 2 files changed, 113 insertions(+), 67 deletions(-) create mode 100644 src/utils/faceDetectionWorker.ts diff --git a/src/hooks/useFaceFilterWithModel.ts b/src/hooks/useFaceFilterWithModel.ts index f767a85..2010c6d 100644 --- a/src/hooks/useFaceFilterWithModel.ts +++ b/src/hooks/useFaceFilterWithModel.ts @@ -1,8 +1,11 @@ import { useCallback, useEffect, useRef, useState } from 'react'; import FILTER from '../constants/FILTER'; -import { useFaceLandmarker } from '../store/faceLandmarkerStore'; import { useFilterTypeStore } from '../store/filterTypeStore'; -import { FilterPosition, VideoSize } from '../types/challenge'; +import { + FilterPosition, + FilterTypeWithoutNone, + VideoSize, +} from '../types/challenge'; const videoSize = { width: 640, @@ -14,7 +17,6 @@ const useFaceFilterWithModel = ( isFilterActive: boolean, ) => { const { filterType } = useFilterTypeStore(); - const { faceLandmarker, isLoaded, loadFaceLandmarker } = useFaceLandmarker(); const canvasRef = useRef(null); const [filterImage, setFilterImage] = useState(null); const [ctx, setCtx] = useState(null); @@ -22,7 +24,9 @@ const useFaceFilterWithModel = ( const [actualVideoSize, setActualVideoSize] = useState(videoSize); const animationFrameId = useRef(null); const lastProcessedTimeRef = useRef(0); - const processInterval = 100; // 100ms 간격으로 얼굴 필터 업데이트 + const processInterval = 60; // 100ms 간격으로 얼굴 필터 업데이트 + + const workerRef = useRef(null); const estimateFacesLoop = useCallback(() => { const now = performance.now(); @@ -32,12 +36,11 @@ const useFaceFilterWithModel = ( } lastProcessedTimeRef.current = now; if (filterType === 'none' || !isFilterActive) return; - if (!video || !filterImage || !ctx || !faceLandmarker) return; + if (!video || !filterImage || !ctx) return; const actualHeight = video.getBoundingClientRect().height; const actualWidth = actualHeight * (video.videoWidth / video.videoHeight); setActualVideoSize({ width: actualWidth, height: actualHeight }); - const padding = (actualWidth - video.getBoundingClientRect().width) / 2; if (actualWidth === 0 || actualHeight === 0) { console.error('비디오 크기가 0입니다.'); @@ -45,57 +48,23 @@ const useFaceFilterWithModel = ( return; } - const startTimeMs = performance.now(); - try { - const { faceLandmarks } = faceLandmarker.detectForVideo( - video, - startTimeMs, - ); - if (faceLandmarks && faceLandmarks.length > 0) { - // 캔버스 초기화 - ctx.clearRect(0, 0, actualWidth, actualHeight); - canvasRef.current!.width = actualWidth; - canvasRef.current!.height = actualHeight; - - // 필터 위치 계산 - const position = FILTER.CALCULATOR[filterType]( - faceLandmarks[0], - actualWidth, - actualHeight, - ); - - if (!position) { - animationFrameId.current = requestAnimationFrame(estimateFacesLoop); - return; - } - - const { x, y, width, height } = position; - setPosition({ x, y, width, height }); - - // 필터 그리기 - ctx.drawImage(filterImage, x - padding, y, width, height); - } else { - animationFrameId.current = requestAnimationFrame(estimateFacesLoop); - return; - } - } catch (error) { - // 오류 발생 시 초기화 후 몇 초 후 재시도 - setTimeout(() => { - loadFaceLandmarker(); - }, 1000); // 1초 후 재시도 - return; - } - - animationFrameId.current = requestAnimationFrame(estimateFacesLoop); - }, [ - ctx, - faceLandmarker, - filterImage, - loadFaceLandmarker, - filterType, - video, - isFilterActive, - ]); + // OffscreenCanvas를 이용해 비디오 프레임을 복사 + const offscreenCanvas = new OffscreenCanvas( + video.videoWidth, + video.videoHeight, + ); + const offscreenCtx = offscreenCanvas.getContext('2d'); + offscreenCtx?.drawImage(video, 0, 0, video.videoWidth, video.videoHeight); + const videoFrame = offscreenCanvas.transferToImageBitmap(); // OffscreenCanvas를 ImageBitmap으로 변환 + + // Worker로 비디오 프레임 전달 + workerRef.current?.postMessage({ + action: 'detect', + videoFrame, + videoWidth: actualWidth, + videoHeight: actualHeight, + }); + }, [ctx, filterImage, filterType, video, isFilterActive]); useEffect(() => { if (!canvasRef.current) return; @@ -116,12 +85,61 @@ const useFaceFilterWithModel = ( }; }, [filterType]); + useEffect(() => { + workerRef.current = new Worker( + new URL('../utils/faceDetectionWorker.ts', import.meta.url), + ); + + workerRef.current.postMessage({ action: 'init' }); + }, []); + + useEffect(() => { + if (!workerRef.current || !video) return; + workerRef.current.onmessage = (event) => { + const { status, faceLandmarks } = event.data; + if (status === 'initialized') { + console.log('FaceLandmarker 모델 초기화 완료'); + } + + if (faceLandmarks && faceLandmarks.length > 0) { + const { width, height } = actualVideoSize; + const padding = (width - video!.getBoundingClientRect().width) / 2; + + ctx?.clearRect(0, 0, width, height); + canvasRef.current!.width = width; + canvasRef.current!.height = height; + + const position = FILTER.CALCULATOR[filterType as FilterTypeWithoutNone]( + faceLandmarks[0], + width, + height, + ); + + if (position) { + setPosition(position); + ctx?.drawImage( + filterImage!, + position.x - padding, + position.y, + position.width, + position.height, + ); + } + } + + animationFrameId.current = requestAnimationFrame(estimateFacesLoop); + + return () => { + workerRef.current?.terminate(); + }; + }; + }, [actualVideoSize, ctx, estimateFacesLoop, filterImage, filterType, video]); + useEffect(() => { if ( isFilterActive && filterImage && ctx && - isLoaded && video && filterType !== 'none' ) { @@ -144,15 +162,7 @@ const useFaceFilterWithModel = ( cancelAnimationFrame(animationFrameId.current); } } - }, [ - estimateFacesLoop, - filterImage, - ctx, - isLoaded, - filterType, - video, - isFilterActive, - ]); + }, [estimateFacesLoop, filterImage, ctx, filterType, video, isFilterActive]); // 언마운트 시 애니메이션 프레임 정리 useEffect(() => { diff --git a/src/utils/faceDetectionWorker.ts b/src/utils/faceDetectionWorker.ts new file mode 100644 index 0000000..9af8881 --- /dev/null +++ b/src/utils/faceDetectionWorker.ts @@ -0,0 +1,36 @@ +// Web Worker을 이용해 얼굴 인식 로직 분리 +import { FaceLandmarker, FilesetResolver } from '@mediapipe/tasks-vision'; + +let faceLandmarker: FaceLandmarker | null = null; + +onmessage = async (event) => { + const { action, videoFrame } = event.data; + + if (action === 'init' && !faceLandmarker) { + const vision = await FilesetResolver.forVisionTasks( + 'https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision/wasm', + ); + const modelAssetPath = + 'https://storage.googleapis.com/mediapipe-models/face_landmarker/face_landmarker/float16/1/face_landmarker.task'; + faceLandmarker = await FaceLandmarker.createFromOptions(vision, { + baseOptions: { + modelAssetPath, + delegate: 'GPU', + }, + runningMode: 'VIDEO', + numFaces: 1, + }); + postMessage({ status: 'initialized' }); + return; + } + + if (action === 'detect' && faceLandmarker) { + const startTimeMs = performance.now(); + const { faceLandmarks } = faceLandmarker.detectForVideo( + videoFrame, + startTimeMs, + ); + postMessage({ faceLandmarks }); + return; + } +}; From 884fe1f8b9d2c8e513e020e2b6512b4b7e99d047 Mon Sep 17 00:00:00 2001 From: bada308 <203190@jnu.ac.kr> Date: Wed, 31 Jul 2024 18:46:01 +0900 Subject: [PATCH 6/8] =?UTF-8?q?fix:=20requestAnimationFrame=20=ED=98=B8?= =?UTF-8?q?=EC=B6=9C=20=EA=B0=84=EA=B2=A9=2060ms=EB=A1=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/useFaceFilter.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hooks/useFaceFilter.ts b/src/hooks/useFaceFilter.ts index 6451214..d0bf208 100644 --- a/src/hooks/useFaceFilter.ts +++ b/src/hooks/useFaceFilter.ts @@ -19,7 +19,7 @@ const useFaceFilter = ( const [ctx, setCtx] = useState(null); const animationFrameId = useRef(null); const lastProcessedTimeRef = useRef(0); - const processInterval = 100; // 100ms 간격으로 얼굴 필터 업데이트 + const processInterval = 60; // 100ms 간격으로 얼굴 필터 업데이트 const estimateFacesLoop = useCallback(() => { const now = performance.now(); From 646a54848eebbe920f659fb37eff78483f735ffd Mon Sep 17 00:00:00 2001 From: bada308 <203190@jnu.ac.kr> Date: Wed, 31 Jul 2024 18:46:15 +0900 Subject: [PATCH 7/8] chore: Update face filter process interval to 60ms --- src/hooks/useFaceFilter.ts | 2 +- src/hooks/useFaceFilterWithModel.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/hooks/useFaceFilter.ts b/src/hooks/useFaceFilter.ts index d0bf208..7d3926d 100644 --- a/src/hooks/useFaceFilter.ts +++ b/src/hooks/useFaceFilter.ts @@ -19,7 +19,7 @@ const useFaceFilter = ( const [ctx, setCtx] = useState(null); const animationFrameId = useRef(null); const lastProcessedTimeRef = useRef(0); - const processInterval = 60; // 100ms 간격으로 얼굴 필터 업데이트 + const processInterval = 60; // 60ms 간격으로 얼굴 필터 업데이트 const estimateFacesLoop = useCallback(() => { const now = performance.now(); diff --git a/src/hooks/useFaceFilterWithModel.ts b/src/hooks/useFaceFilterWithModel.ts index 2010c6d..b72b0d3 100644 --- a/src/hooks/useFaceFilterWithModel.ts +++ b/src/hooks/useFaceFilterWithModel.ts @@ -24,7 +24,7 @@ const useFaceFilterWithModel = ( const [actualVideoSize, setActualVideoSize] = useState(videoSize); const animationFrameId = useRef(null); const lastProcessedTimeRef = useRef(0); - const processInterval = 60; // 100ms 간격으로 얼굴 필터 업데이트 + const processInterval = 60; // 60ms 간격으로 얼굴 필터 업데이트 const workerRef = useRef(null); From e8c7cc942d5136018f5d5c97b4f5007fa5d6e8b3 Mon Sep 17 00:00:00 2001 From: bada308 <203190@jnu.ac.kr> Date: Wed, 31 Jul 2024 18:46:30 +0900 Subject: [PATCH 8/8] =?UTF-8?q?feat:=20FaceChallengeContent=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../face-challenge/FaceChallengeContainer.tsx | 21 ++---------- .../face-challenge/FaceChallengeContent.tsx | 33 +++++++++++++++++++ 2 files changed, 35 insertions(+), 19 deletions(-) create mode 100644 src/components/face-challenge/FaceChallengeContent.tsx diff --git a/src/components/face-challenge/FaceChallengeContainer.tsx b/src/components/face-challenge/FaceChallengeContainer.tsx index d25eb1d..0e3c7ae 100644 --- a/src/components/face-challenge/FaceChallengeContainer.tsx +++ b/src/components/face-challenge/FaceChallengeContainer.tsx @@ -1,15 +1,10 @@ -import { useState } from 'react'; import { useParams } from 'react-router-dom'; import { useGetFaceChallenge } from '../../apis/challenge/getFaceChallenge'; -import FacetimeContainer from './facetime/FacetimeContainer'; -import PrejoinContainer from './prejoin/PrejoinContainer'; +import FaceChallengeContent from './FaceChallengeContent'; import FaceChallengeResult from './result/FaceChallengeResult'; -import FaceLandmark from './utils/FaceLandmark'; const FaceChallengeContainer = () => { const { challengeId } = useParams(); - - const [isJoined, setIsJoined] = useState(false); const { data } = useGetFaceChallenge({ challengeId: challengeId || '', }); @@ -23,19 +18,7 @@ const FaceChallengeContainer = () => { {challengeInfo.isComplete ? ( ) : ( - - {isJoined ? ( - - ) : ( - <> - setIsJoined(isJoined)} - challengeInfo={challengeInfo} - code={code} - /> - - )} - + )} ); diff --git a/src/components/face-challenge/FaceChallengeContent.tsx b/src/components/face-challenge/FaceChallengeContent.tsx new file mode 100644 index 0000000..0e6d320 --- /dev/null +++ b/src/components/face-challenge/FaceChallengeContent.tsx @@ -0,0 +1,33 @@ +import { useState } from 'react'; +import { ChallengeDetailDto } from '../../apis/dtos/challengeDtos'; +import FacetimeContainer from './facetime/FacetimeContainer'; +import PrejoinContainer from './prejoin/PrejoinContainer'; +import FaceLandmark from './utils/FaceLandmark'; + +interface FaceChallengeContentProps { + challengeInfo: ChallengeDetailDto; + code: string; +} + +const FaceChallengeContent = ({ + challengeInfo, + code, +}: FaceChallengeContentProps) => { + const [isJoined, setIsJoined] = useState(false); + return ( + + {isJoined ? ( + + ) : ( + <> + setIsJoined(isJoined)} + challengeInfo={challengeInfo} + code={code} + /> + + )} + + ); +}; +export default FaceChallengeContent;