diff --git a/src/api/notification.ts b/src/api/notification.ts new file mode 100644 index 0000000..667a23a --- /dev/null +++ b/src/api/notification.ts @@ -0,0 +1,42 @@ +import axiosInstance from "./axiosInstance" + +export type duration = "IMMEDIATELY" | "MIN_15" | "MIN_30" | "MIN_45" | "MIN_60" + +export interface notification { + id?: number + isActive?: boolean + duration?: duration +} + +export const getNotification = async (): Promise => { + try { + const res = await axiosInstance.get(`/pose-notifications`) + + if (!res.data?.data) return null + + const { id, duration } = res.data.data + return { id, duration } + } catch (e) { + throw e + } +} + +export const modifyNotification = async (notification: notification): Promise => { + try { + const res = await axiosInstance.post(`/pose-notifications`, { ...notification }) + const { id, duration } = res.data.data + return { id, duration } + } catch (e) { + throw e + } +} + +export const patchNotification = async (notification: notification): Promise => { + try { + const res = await axiosInstance.patch(`/pose-notifications/${notification.id}`, { ...notification }) + const { id, isActive, duration } = res.data.data + return { id, isActive, duration } + } catch (e) { + throw e + } +} diff --git a/src/components/Camera.tsx b/src/components/Camera.tsx index 59b6705..2a07b42 100644 --- a/src/components/Camera.tsx +++ b/src/components/Camera.tsx @@ -40,7 +40,7 @@ export default function Camera(props: CameraProps): React.ReactElement { useEffect(() => { startVideo() - }) + }, []) return (
{ const [isScriptLoaded, setIsScriptLoaded] = useState(false) @@ -23,16 +25,23 @@ const PoseDetector: React.FC = () => { const [isHandOnChin, setIsHandOnChin] = useState(null) const [isModelLoaded, setIsModelLoaded] = useState(false) const [isSnapSaved, setIsSnapSaved] = useState(false) - const [isPopupVisible, setIsPopupVisible] = useState(false) + const [isPopupVisible, setIsPopupVisible] = useState(true) + + const { showNotification } = usePushNotification() const modelRef = useRef(null) const snapRef = useRef(null) const resultRef = useRef(null) - + const notificationTimer = useRef(null) const turtleNeckTimer = useRef(null) const shoulderTwistTimer = useRef(null) const chinUtpTimer = useRef(null) const tailboneSitTimer = useRef(null) + const turtleNeckCnt = useRef(0) + const shoulderTwistCnt = useRef(0) + const chinUtpCnt = useRef(0) + const tailboneSitCnt = useRef(0) + const canvasRef = useRef(null) const snapshot = useSnapshotStore((state) => state.snapshot) @@ -40,8 +49,11 @@ const PoseDetector: React.FC = () => { const sendPoseMutation = useSendPose() const setSnap = useSnapshotStore((state) => state.setSnapshot) + const userNoti = useNotificationStore((state) => state.notification) + + const random = Math.random() < 0.5 - const { requestNotificationPermission, showNotification } = usePushNotification() + const { requestNotificationPermission } = usePushNotification() // webgl 설정 const initializeBackend = async (): Promise => { @@ -82,28 +94,49 @@ const PoseDetector: React.FC = () => { document.body.appendChild(script) } - const managePoseTimer = ( - condition: boolean | null, - timerRef: React.MutableRefObject, - poseType: poseType, - isSnapSaved: boolean - ): void => { - if (condition && isSnapSaved) { - if (!timerRef.current) { - timerRef.current = setInterval(() => { - if (resultRef.current) { - const { keypoints, score } = resultRef.current[0] - const req = { snapshot: { keypoints, score }, type: poseType } - sendPoseMutation.mutate(req) - } - }, 5000) - } - } else { - clearInterval(timerRef.current) - timerRef.current = null + const getPoseName = (poseType: poseType): string => { + switch (poseType) { + case "TURTLE_NECK": + return "거북목" + case "SHOULDER_TWIST": + return "어깨 틀어짐" + case "CHIN_UTP": + return "턱 괴기" + case "TAILBONE_SIT": + return "꼬리뼈 앉기" } + return "" } + const managePoseTimer = useCallback( + ( + condition: boolean | null, + timerRef: React.MutableRefObject, + poseType: poseType, + isSnapSaved: boolean, + cntRef: React.MutableRefObject, + isShowNoti: boolean | undefined + ): void => { + if (condition && isSnapSaved) { + if (!timerRef.current) { + timerRef.current = setInterval(() => { + if (resultRef.current) { + const { keypoints, score } = resultRef.current[0] + const req = { snapshot: { keypoints, score }, type: poseType } + sendPoseMutation.mutate(req) + cntRef.current = cntRef.current + 1 + if (isShowNoti) showNotification(`${getPoseName(poseType)} 감지! 자세를 바르게 앉아주세요.`) + } + }, 30 * 1000) + } + } else { + clearInterval(timerRef.current) + timerRef.current = null + } + }, + [sendPoseMutation, showNotification] + ) + const detect = useCallback( (results: pose[]): void => { resultRef.current = results @@ -117,6 +150,7 @@ const PoseDetector: React.FC = () => { const _isTextNeck = detectTextNeck(snapRef.current, results, true) const _isHandOnChin = detectHandOnChin(results) const _isTailboneSit = detectTailboneSit(snapRef.current, results) + const _isShowNoti = userNoti?.duration === "IMMEDIATELY" && userNoti?.isActive if (_isShoulderTwist !== null) setIsShoulderTwist(_isShoulderTwist) if (_isTextNeck !== null) setIsTextNeck(_isTextNeck) @@ -124,13 +158,20 @@ const PoseDetector: React.FC = () => { if (_isTailboneSit !== null) setIsTailboneSit(_isTailboneSit) // 공통 타이머 관리 함수 호출 - managePoseTimer(_isTextNeck, turtleNeckTimer, "TURTLE_NECK", isSnapSaved) - managePoseTimer(_isShoulderTwist, shoulderTwistTimer, "SHOULDER_TWIST", isSnapSaved) - managePoseTimer(_isTailboneSit, tailboneSitTimer, "TAILBONE_SIT", isSnapSaved) - managePoseTimer(_isHandOnChin, chinUtpTimer, "CHIN_UTP", isSnapSaved) + managePoseTimer(_isTextNeck, turtleNeckTimer, "TURTLE_NECK", isSnapSaved, turtleNeckCnt, _isShowNoti) + managePoseTimer( + _isShoulderTwist, + shoulderTwistTimer, + "SHOULDER_TWIST", + isSnapSaved, + shoulderTwistCnt, + _isShowNoti + ) + managePoseTimer(_isTailboneSit, tailboneSitTimer, "TAILBONE_SIT", isSnapSaved, tailboneSitCnt, _isShowNoti) + managePoseTimer(_isHandOnChin, chinUtpTimer, "CHIN_UTP", isSnapSaved, chinUtpCnt, _isShowNoti) } }, - [setIsShoulderTwist, setIsTextNeck, setIsHandOnChin, setIsTailboneSit, isSnapSaved, showNotification] + [setIsShoulderTwist, setIsTextNeck, setIsHandOnChin, setIsTailboneSit, isSnapSaved, managePoseTimer, userNoti] ) const detectStart = useCallback( @@ -144,7 +185,7 @@ const PoseDetector: React.FC = () => { [detect] ) - const getInitSnap = (): void => { + const getInitSnap = useCallback((): void => { if (modelRef && modelRef.current) { snapRef.current = resultRef.current if (snapshot === null) { @@ -168,7 +209,7 @@ const PoseDetector: React.FC = () => { } } } - } + }, [createSnapMutation, snapshot, setSnap]) const getUserSnap = (): void => { if (snapshot) { @@ -177,16 +218,17 @@ const PoseDetector: React.FC = () => { } } - const clearTimers = () => { + const clearTimers = (): void => { clearInterval(turtleNeckTimer.current) clearInterval(shoulderTwistTimer.current) clearInterval(tailboneSitTimer.current) clearInterval(chinUtpTimer.current) - + clearInterval(notificationTimer.current) turtleNeckTimer.current = null shoulderTwistTimer.current = null tailboneSitTimer.current = null chinUtpTimer.current = null + notificationTimer.current = null } const clearSnap = (): void => { @@ -198,24 +240,60 @@ const PoseDetector: React.FC = () => { } } - const getIsRight = ( - _isShoulderTwist: boolean | null, - _isTextNeck: boolean | null, - _isTailboneSit: boolean | null, - _isHandOnChin: boolean | null - ): boolean => { - if (!_isShoulderTwist && !_isTextNeck && !_isTailboneSit && !_isHandOnChin) return true - return false + const clearCnt = (): void => { + turtleNeckCnt.current = 0 + shoulderTwistCnt.current = 0 + tailboneSitCnt.current = 0 + chinUtpCnt.current = 0 + } + + const getDurationInMinutes = (duration: duration): number => { + switch (duration) { + case "MIN_15": + return 15 + case "MIN_30": + return 30 + case "MIN_45": + return 45 + default: + return 60 + } + } + + const sendNotification = (minutes: number | null): void => { + const total = turtleNeckCnt.current + shoulderTwistCnt.current + chinUtpCnt.current + tailboneSitCnt.current + if (random) { + showNotification( + `지난 ${minutes}분 동안 총 ${total}회 감지! ${ + total > 0 ? "자세를 바르게 앉아주세요." : "좋은 자세를 유지해주세요." + }` + ) + } else { + showNotification( + `지난 ${minutes}분 동안 거북목 ${turtleNeckCnt.current}회, 어깨틀어짐 ${shoulderTwistCnt.current}회, 턱 괴기 ${ + chinUtpCnt.current + }회, 꼬리뼈 앉기 ${tailboneSitCnt.current}회 감지! ${ + total > 0 ? "자세를 바르게 앉아주세요." : "좋은 자세를 유지해주세요." + }` + ) + } } useEffect(() => { requestNotificationPermission() getScript() + clearTimers() + return () => { + clearTimers() + clearCnt() + worker.postMessage({ type: "terminate", data: {} }) + } }, []) useEffect(() => { if (!isSnapSaved) { clearTimers() // 스냅샷이 저장되지 않았을 때 타이머들을 초기화 + clearCnt() // 횟수도 초기화 } }, [isSnapSaved]) @@ -232,6 +310,21 @@ const PoseDetector: React.FC = () => { if (snapshot) getUserSnap() }, [snapshot]) + useEffect(() => { + if (!isSnapSaved || !userNoti) return + + clearCnt() + clearInterval(notificationTimer.current) + notificationTimer.current = null + + if (userNoti.isActive && userNoti.duration && userNoti.duration !== "IMMEDIATELY") { + const t = getDurationInMinutes(userNoti?.duration) + notificationTimer.current = setInterval(() => { + if (userNoti.duration) sendNotification(t) + }, 1000 * 60 * t) + } + }, [userNoti, isSnapSaved]) + // 팝업 열기 const handleShowPopup = (): void => { setIsPopupVisible(true) @@ -253,50 +346,22 @@ const PoseDetector: React.FC = () => { {isModelLoaded && ( <> -
- {!isSnapSaved - ? "바른 자세를 취한 후, 하단의 버튼을 눌러주세요." - : getIsRight(isShoulderTwist, isTextNeck, isHandOnChin, isTailboneSit) - ? "올바른 자세입니다." - : "올바르지 않은 자세입니다."} -
-
- {!isSnapSaved ? ( - <> - - - - ) : ( - - )} -
+ + )} - {isPopupVisible && } {/* 팝업 표시 */} + {isPopupVisible && }
)} diff --git a/src/components/Posture/Controls.tsx b/src/components/Posture/Controls.tsx new file mode 100644 index 0000000..446a52b --- /dev/null +++ b/src/components/Posture/Controls.tsx @@ -0,0 +1,48 @@ +import PostureCheckIcon from "@assets/icons/good-posture-check-button-icon.svg?react" +import GuideIcon from "@assets/icons/posture-guide-button-icon.svg?react" + +const Controls: React.FC<{ + isSnapSaved: boolean + getInitSnap: () => void + clearSnap: () => void + handleShowPopup: () => void +}> = ({ isSnapSaved, getInitSnap, clearSnap, handleShowPopup }) => { + return ( +
+ {!isSnapSaved ? ( + <> + + + + ) : ( + + )} +
+ ) +} + +export default Controls diff --git a/src/components/Posture/GuidePopup.tsx b/src/components/Posture/GuidePopup.tsx index 3a86be8..0558aad 100644 --- a/src/components/Posture/GuidePopup.tsx +++ b/src/components/Posture/GuidePopup.tsx @@ -1,6 +1,7 @@ import GuideImage from "@assets/images/posture-guide-2x.png" +import { ReactElement } from "react" -const GuidePopup = ({ onClose }: { onClose: () => void }) => { +const GuidePopup = ({ onClose }: { onClose: () => void }): ReactElement => { return (
{/* blur 처리 */} diff --git a/src/components/Posture/PostrueCrew.tsx b/src/components/Posture/PostrueCrew.tsx index d5ba39d..9e1a40f 100644 --- a/src/components/Posture/PostrueCrew.tsx +++ b/src/components/Posture/PostrueCrew.tsx @@ -2,9 +2,12 @@ import CloseCrewPanelIcon from "@assets/icons/crew-panel-close-button.svg?react" import QuestionIcon from "@assets/icons/question-info-icon.svg?react" import PostureGuide from "@assets/icons/posture-guide-button-icon.svg?react" import RankingGuideToolTip from "@assets/images/ranking-guide.png" -import { useEffect, useState } from "react" +import { ReactElement, useEffect, useState } from "react" import SelectBox from "@components/SelectBox" import { useAuthStore } from "@/store" +import { duration, notification } from "@/api/notification" +import { useNotificationStore } from "@/store/NotificationStore" +import { usePatchNoti } from "@/hooks/useNotiMutation" interface IPostureCrew { groupUserId: number @@ -18,13 +21,31 @@ interface PostureCrewProps { toggleSidebar: () => void } -export default function PostrueCrew(props: PostureCrewProps) { +interface NotiOption { + value: duration + label: string +} + +const NOTI_OPTIONS: NotiOption[] = [ + { value: "IMMEDIATELY", label: "틀어진 즉시" }, + { value: "MIN_15", label: "15분 간격" }, + { value: "MIN_30", label: "30분 간격" }, + { value: "MIN_45", label: "45분 간격" }, + { value: "MIN_60", label: "1시간 간격" }, +] + +export default function PostrueCrew(props: PostureCrewProps): ReactElement { const { toggleSidebar } = props const accessToken = useAuthStore((state) => state.accessToken) const [crews, setCrews] = useState([]) const [isConnected, setIsConnected] = useState<"loading" | "success" | "disconnected">("loading") - const [isEnabled, setIsEnabled] = useState(true) - const [notiAlarmTime, setNotiAlarmTime] = useState("틀어진 즉시") + + const userNoti = useNotificationStore((state) => state.notification) + const setUserNoti = useNotificationStore((state) => state.setNotification) + const patchNotiMutation = usePatchNoti() + + const [isEnabled, setIsEnabled] = useState(userNoti?.isActive) + const [notiAlarmTime, setNotiAlarmTime] = useState(NOTI_OPTIONS.find((n) => n.value === userNoti?.duration)?.label) useEffect(() => { const socket = new WebSocket(`wss://api.alignlab.site/ws/v1/groups/1/users?X-HERO-AUTH-TOKEN=${accessToken}`) @@ -53,12 +74,33 @@ export default function PostrueCrew(props: PostureCrewProps) { } }, []) - const onClickCloseSideNavButton = () => { + const onClickCloseSideNavButton = (): void => { toggleSidebar() } - const onClickNotiAlarmTime = (value: string) => { - setNotiAlarmTime(value) + const onClickNotiAlarmTime = (option: NotiOption): void => { + setNotiAlarmTime(option.label) + patchNotiMutation.mutate( + { id: userNoti?.id, duration: option.value }, + { + onSuccess: (data: notification) => { + setNotiAlarmTime(option.label) + setUserNoti(data) + }, + } + ) + } + + const onClickNotiAlarm = (): void => { + patchNotiMutation.mutate( + { id: userNoti?.id, isActive: !userNoti?.isActive }, + { + onSuccess: (data: notification) => { + setIsEnabled(data.isActive) + setUserNoti(data) + }, + } + ) } return ( @@ -73,19 +115,15 @@ export default function PostrueCrew(props: PostureCrewProps) {
diff --git a/src/components/Posture/PostureMessage.tsx b/src/components/Posture/PostureMessage.tsx new file mode 100644 index 0000000..66eb23c --- /dev/null +++ b/src/components/Posture/PostureMessage.tsx @@ -0,0 +1,28 @@ +const PostureMessage: React.FC<{ + isSnapSaved: boolean + isShoulderTwist: boolean | null + isTextNeck: boolean | null + isHandOnChin: boolean | null + isTailboneSit: boolean | null +}> = ({ isSnapSaved, isShoulderTwist, isTextNeck, isHandOnChin, isTailboneSit }) => { + const getIsRight = ( + _isShoulderTwist: boolean | null, + _isTextNeck: boolean | null, + _isTailboneSit: boolean | null, + _isHandOnChin: boolean | null + ): boolean => { + return !_isShoulderTwist && !_isTextNeck && !_isTailboneSit && !_isHandOnChin + } + + return ( +
+ {!isSnapSaved + ? "바른 자세를 취한 후, 하단의 버튼을 눌러주세요." + : getIsRight(isShoulderTwist, isTextNeck, isHandOnChin, isTailboneSit) + ? "올바른 자세입니다." + : "올바르지 않은 자세입니다."} +
+ ) +} + +export default PostureMessage diff --git a/src/components/SelectBox.tsx b/src/components/SelectBox.tsx index d400de2..e87ae01 100644 --- a/src/components/SelectBox.tsx +++ b/src/components/SelectBox.tsx @@ -1,18 +1,27 @@ import { useState } from "react" +interface SelectBoxOption { + label: string + value: any +} + interface SelectBoxProps { - value: string - options: string[] - onClick: (selectedValue: string) => void + value: string | undefined + options: SelectBoxOption[] + isDisabled: boolean + onClick: (selectedOption: SelectBoxOption) => void } -export default function SelectBox(props: SelectBoxProps) { - const { value, options, onClick } = props +export default function SelectBox(props: SelectBoxProps): React.ReactElement { + const { value, options, isDisabled, onClick } = props const [isOpen, setIsOpen] = useState(false) - const toggleDropdown = () => setIsOpen(!isOpen) + const toggleDropdown = (): void => { + if (isDisabled) return + setIsOpen(!isOpen) + } - const handleOptionClick = (option: string) => { + const handleOptionClick = (option: SelectBoxOption): void => { onClick(option) setIsOpen(false) } @@ -20,13 +29,17 @@ export default function SelectBox(props: SelectBoxProps) { return (
- {value} + {value} {options.map((option) => (
handleOptionClick(option)} > - {option} + {option.label}
))}
diff --git a/src/components/SideNav.tsx b/src/components/SideNav.tsx index 620b912..369dac9 100644 --- a/src/components/SideNav.tsx +++ b/src/components/SideNav.tsx @@ -6,6 +6,7 @@ import MonitoringIcon from "@assets/icons/side-nav-monitor-icon.svg?react" import { Link, useLocation, useNavigate } from "react-router-dom" import { useSnapshotStore } from "@/store/SnapshotStore" import { useMemo } from "react" +import { clearAccessToken } from "@/api/axiosInstance" const navItems = [ { @@ -25,7 +26,7 @@ const navItems = [ }, ] -export default function SideNav() { +export default function SideNav(): React.ReactElement { const nickname = useAuthStore((state) => state.user?.nickname) const location = useLocation() const logout = useAuthStore((state) => state.logout) @@ -37,6 +38,7 @@ export default function SideNav() { clearUser() clearSnapshot() + clearAccessToken() logout(() => { navigate("/") diff --git a/src/hooks/useNotiMutation.ts b/src/hooks/useNotiMutation.ts new file mode 100644 index 0000000..3145d6c --- /dev/null +++ b/src/hooks/useNotiMutation.ts @@ -0,0 +1,35 @@ +import { getNotification, modifyNotification, notification, patchNotification } from "@/api/notification" +import { useMutation, UseMutationResult } from "@tanstack/react-query" + +export const useGetNoti = (): UseMutationResult => { + return useMutation({ + mutationFn: () => { + return getNotification() + }, + onSuccess: (data) => { + console.log(data) + }, + }) +} + +export const useModifyNoti = (): UseMutationResult => { + return useMutation({ + mutationFn: (notification: notification) => { + return modifyNotification(notification) + }, + onSuccess: (data) => { + console.log(data) + }, + }) +} + +export const usePatchNoti = (): UseMutationResult => { + return useMutation({ + mutationFn: (notification: notification) => { + return patchNotification(notification) + }, + onSuccess: (data) => { + console.log(data) + }, + }) +} diff --git a/src/hooks/usePushNotification.ts b/src/hooks/usePushNotification.ts index bf0f660..cc26b46 100644 --- a/src/hooks/usePushNotification.ts +++ b/src/hooks/usePushNotification.ts @@ -16,10 +16,10 @@ const usePushNotification = (): any => { } } - const showNotification = (): void => { + const showNotification = (body: string): void => { if (Notification.permission === "granted") { - new Notification("Hello!", { - body: "거북목 상태입니다. 자세를 바르게 하세요.", + new Notification("자세 공작소", { + body: body, }) } } diff --git a/src/pages/AuthPage.tsx b/src/pages/AuthPage.tsx index 95c45dd..640c714 100644 --- a/src/pages/AuthPage.tsx +++ b/src/pages/AuthPage.tsx @@ -6,6 +6,9 @@ import RoutePath from "@/constants/routes.json" import { useAuthStore } from "@/store/AuthStore" import { useSnapshotStore } from "@/store/SnapshotStore" import { useGetRecentSnapshot } from "@/hooks/useSnapshotMutation" +import { useGetNoti, useModifyNoti } from "@/hooks/useNotiMutation" +import { useNotificationStore } from "@/store/NotificationStore" +import { duration, modifyNotification, notification } from "@/api/notification" const AuthPage: React.FC = () => { const navigate = useNavigate() @@ -15,12 +18,14 @@ const AuthPage: React.FC = () => { const signUpMutation = useSignUp() const signInMutation = useSignIn() const getRecentSnapMutation = useGetRecentSnapshot() - + const getNotiMutation = useGetNoti() + const modifyNotiMutation = useModifyNoti() const [isLoading, setIsLoading] = useState(true) const [isError, setIsError] = useState(false) const setUser = useAuthStore((state) => state.setUser) const setSnap = useSnapshotStore((state) => state.setSnapshot) + const setNoti = useNotificationStore((state) => state.setNotification) useEffect(() => { const authenticate = async (): Promise => { @@ -34,8 +39,6 @@ const AuthPage: React.FC = () => { const _accessToken = await oauthMutation.mutateAsync(code) - console.log("_accessToken: ", _accessToken) - const isUserSignedUp = await getIsSignUpMutation.mutateAsync(_accessToken) if (!isUserSignedUp) { @@ -55,6 +58,14 @@ const AuthPage: React.FC = () => { setSnap(userSnap.points.map((p) => ({ name: p.position.toLocaleLowerCase(), x: p.x, y: p.y, confidence: 1 }))) } + const notification = await getNotiMutation.mutateAsync() + // notification 설정 없으면 기본값(틀어진 즉시)로 설정 + if (!notification) { + setNoti({ isActive: false, duration: "IMMEDIATELY" }) + } else { + setNoti({ isActive: true, ...notification }) + } + setIsLoading(false) navigate(RoutePath.MONITORING) } catch (error) { diff --git a/src/pages/MonitoringPage.tsx b/src/pages/MonitoringPage.tsx index 45defda..4b1a6f8 100644 --- a/src/pages/MonitoringPage.tsx +++ b/src/pages/MonitoringPage.tsx @@ -10,7 +10,7 @@ const MonitoringPage: React.FC = () => { const setSnap = useSnapshotStore((state) => state.setSnapshot) const snapshot = useSnapshotStore((state) => state.snapshot) - const [isSidebarOpen, setIsSidebarOpen] = useState(false) + const [isSidebarOpen, setIsSidebarOpen] = useState(true) const toggleSidebar = (): void => { setIsSidebarOpen((prev) => !prev) diff --git a/src/store/NotificationStore.ts b/src/store/NotificationStore.ts new file mode 100644 index 0000000..7d84760 --- /dev/null +++ b/src/store/NotificationStore.ts @@ -0,0 +1,18 @@ +import { notification } from "@/api/notification" +import { create } from "zustand" +import { persist } from "zustand/middleware" + +interface NotificationState { + notification: notification | null + setNotification: (notification: notification | null) => void +} + +export const useNotificationStore = create( + persist( + (set) => ({ + notification: null, + setNotification: (notification: notification | null) => set({ notification }), + }), + { name: "notificationStorage" } + ) +) diff --git a/src/workers/worker.ts b/src/workers/worker.ts index a6bb209..450cda9 100644 --- a/src/workers/worker.ts +++ b/src/workers/worker.ts @@ -1,3 +1,5 @@ +let timer: string | number | NodeJS.Timeout | undefined + interface e { type: string data: any @@ -7,8 +9,12 @@ self.onmessage = (e) => { const { type } = e.data switch (type) { case "init": - setInterval(() => { + timer = setInterval(() => { postMessage("do it") }, 100) + break + case "terminate": + clearTimeout(timer) + break } }