Skip to content

Commit

Permalink
Merge pull request #84 from DDD-Community/feat/#58
Browse files Browse the repository at this point in the history
[feat/#58] 알람 기능이 적절하게 동작하도록 수정
  • Loading branch information
G-hoon authored Sep 24, 2024
2 parents a1e9b6d + 451ea93 commit 8601243
Show file tree
Hide file tree
Showing 8 changed files with 174 additions and 148 deletions.
29 changes: 12 additions & 17 deletions src/api/notification.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,10 @@ export interface notification {
duration?: duration
}

export const getNotification = async (): Promise<notification | null> => {
export const getNotification = async (): Promise<{ data: notification }> => {
try {
const res = await axiosInstance.get(`/pose-notifications`)

if (!res.data?.data) return null

const { id, duration } = res.data.data
return { id, duration }
return res.data
} catch (e) {
throw e
}
Expand All @@ -24,19 +20,18 @@ export const getNotification = async (): Promise<notification | null> => {
export const registerNotification = async (notification: notification): Promise<notification> => {
try {
const res = await axiosInstance.post(`/pose-notifications`, { ...notification })
const { id, duration } = res.data.data
return { id, duration }
return res.data.data
} catch (e) {
throw e
}
}

export const updateNotification = async (notification: notification): Promise<notification> => {
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
}
}
// export const updateNotification = async (notification: notification): Promise<notification> => {
// 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
// }
// }
30 changes: 19 additions & 11 deletions src/components/PoseDetector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import Camera from "./Camera"
import Controls from "./Posture/Controls"
import GuidePopupModal from "./Posture/GuidePopup/GuidePopupModal"
import PostureMessage from "./Posture/PostureMessage"
import useNotification from "@/hooks/useNotification"

const PoseDetector: React.FC = () => {
const [isScriptLoaded, setIsScriptLoaded] = useState<boolean>(false)
Expand Down Expand Up @@ -53,7 +54,8 @@ const PoseDetector: React.FC = () => {
const createSnapMutation = useCreateSnaphot()
const sendPoseMutation = useSendPose()

const userNoti = useNotificationStore((state) => state.notification)
// const userNoti = useNotificationStore((state) => state.notification)
const { notification } = useNotification()

const { requestNotificationPermission } = usePushNotification()
const { hasPermission } = useCameraPermission()
Expand Down Expand Up @@ -152,7 +154,7 @@ const PoseDetector: React.FC = () => {
const _isTextNeck = detectTextNeck(snapRef.current, results, true, 0.88)
const _isHandOnChin = detectHandOnChin(snapRef.current, results)
const _isTailboneSit = detectTailboneSit(snapRef.current, results)
const _isShowNoti = userNoti?.duration === "IMMEDIATELY" && userNoti?.isActive
const _isShowNoti = notification?.duration === "IMMEDIATELY" && notification?.isActive

if (_isShoulderTwist !== null) setIsShoulderTwist(_isShoulderTwist)
if (_isTextNeck !== null) setIsTextNeck(_isTextNeck)
Expand All @@ -177,7 +179,15 @@ const PoseDetector: React.FC = () => {
if (canvasRef.current) drawPose(results, canvasRef.current)
}
},
[setIsShoulderTwist, setIsTextNeck, setIsHandOnChin, setIsTailboneSit, isSnapShotSaved, managePoseTimer, userNoti]
[
setIsShoulderTwist,
setIsTextNeck,
setIsHandOnChin,
setIsTailboneSit,
isSnapShotSaved,
managePoseTimer,
notification,
]
)

const detectStart = useCallback(
Expand Down Expand Up @@ -306,22 +316,20 @@ const PoseDetector: React.FC = () => {
}, [snapshot])

useEffect(() => {
if (!isSnapShotSaved || !userNoti) return
if (!isSnapShotSaved || !notification) return

clearCnt()
clearInterval(notificationTimer.current)
notificationTimer.current = null

if (userNoti.isActive && userNoti.duration && userNoti.duration !== "IMMEDIATELY") {
const t = getDurationInMinutes(userNoti?.duration)
if (notification.isActive && notification.duration && notification.duration !== "IMMEDIATELY") {
const t = getDurationInMinutes(notification.duration)
notificationTimer.current = setInterval(() => {
if (userNoti.duration) {
sendNotification()
clearCnt()
}
sendNotification()
clearCnt()
}, 1000 * 60 * t)
}
}, [userNoti, isSnapShotSaved])
}, [notification, isSnapShotSaved])

// 팝업 열기
const handleShowPopup = (): void => {
Expand Down
150 changes: 77 additions & 73 deletions src/components/Posture/PostrueCrew.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { duration, notification } from "@/api/notification"
import { useModals } from "@/hooks/useModals"
import { usePatchNoti } from "@/hooks/useNotiMutation"
import useNotification from "@/hooks/useNotification"
import { useModifyNoti } from "@/hooks/useNotiMutation"
import usePushNotification from "@/hooks/usePushNotification"
import { useCreateSnaphot } from "@/hooks/useSnapshotMutation"
import { useAuthStore } from "@/store"
import { useNotificationStore } from "@/store/NotificationStore"
import { useSnapShotStore } from "@/store/SnapshotStore"
import CloseCrewPanelIcon from "@assets/icons/crew-panel-close-button.svg?react"
import PostureGuide from "@assets/icons/posture-guide-button-icon.svg?react"
import PostureRetakeIcon from "@assets/icons/posture-snapshot-retake-icon.svg?react"
Expand All @@ -12,8 +14,6 @@ import RankingGuideToolTip from "@assets/images/ranking-guide.png"
import SelectBox from "@components/SelectBox"
import { ReactElement, useCallback, useEffect, useRef, useState } from "react"
import { modals } from "../Modal/Modals"
import { useSnapShotStore } from "@/store/SnapshotStore"
import { useCreateSnaphot } from "@/hooks/useSnapshotMutation"

interface IPostureCrew {
groupUserId: number
Expand All @@ -40,118 +40,120 @@ const NOTI_OPTIONS: NotiOption[] = [
{ value: "MIN_60", label: "1시간 간격" },
]

const MAX_RECONNECT_ATTEMPTS = 5
const INITIAL_RECONNECT_DELAY = 1000
const UPDATE_INTERVAL = 1000 // 1초마다 상태 업데이트
const NOTI_VALUE_MAP = (value: string | undefined) => {
switch (value) {
case "IMMEDIATELY":
return "틀어진 즉시"
case "MIN_15":
return "15분 간격"
case "MIN_30":
return "30분 간격"
case "MIN_45":
return "45분 간격"
case "MIN_60":
return "1시간 간격"
}
return "틀어진 즉시"
}

export default function PostrueCrew(props: PostureCrewProps): ReactElement {
const { toggleSidebar } = props
const accessToken = useAuthStore((state) => state.accessToken)
const { resetSnapShot } = useSnapShotStore()
const { openModal } = useModals()
const createSnapMutation = useCreateSnaphot()
const [crews, setCrews] = useState<IPostureCrew[]>([])
const useWebSocket = (url: string) => {
const [isConnected, setIsConnected] = useState<"loading" | "success" | "disconnected">("loading")
const [socket, setSocket] = useState<WebSocket | null>(null)
const [reconnectAttempts, setReconnectAttempts] = useState(0)
const latestCrewsRef = useRef<IPostureCrew[]>([])
const updateTimeoutRef = useRef<NodeJS.Timeout | null>(null)
const [crews, setCrews] = useState<IPostureCrew[]>([])
const socketRef = useRef<WebSocket | null>(null)
const reconnectTimeoutRef = useRef<NodeJS.Timeout | null>(null)

const throttledUpdateCrews = useCallback(() => {
if (!updateTimeoutRef.current) {
updateTimeoutRef.current = setTimeout(() => {
setCrews(latestCrewsRef.current)
updateTimeoutRef.current = null
}, UPDATE_INTERVAL)
const connect = useCallback(() => {
if (socketRef.current?.readyState === WebSocket.OPEN) {
return
}
}, [])

const userNoti = useNotificationStore((state) => state.notification)
const setUserNoti = useNotificationStore((state) => state.setNotification)
const patchNotiMutation = usePatchNoti()
const { hasPermission } = usePushNotification()

const [isEnabled, setIsEnabled] = useState(userNoti?.isActive)
const [notiAlarmTime, setNotiAlarmTime] = useState(NOTI_OPTIONS.find((n) => n.value === userNoti?.duration)?.label)
socketRef.current = new WebSocket(url)

const connectWebSocket = useCallback(() => {
const newSocket = new WebSocket(`wss://api.alignlab.site/ws/v1/groups/1/users?X-HERO-AUTH-TOKEN=${accessToken}`)

newSocket.onopen = () => {
socketRef.current.onopen = () => {
console.log("WebSocket connected")
setIsConnected("success")
setReconnectAttempts(0)
}

newSocket.onmessage = (event) => {
socketRef.current.onmessage = (event) => {
const data = JSON.parse(event.data)
console.log("Received message:", data)
if (data.groupUsers) {
latestCrewsRef.current = data.groupUsers
throttledUpdateCrews()
setCrews(data.groupUsers)
}
}

newSocket.onerror = (error) => {
socketRef.current.onerror = (error) => {
console.error("WebSocket error:", error)
}

newSocket.onclose = (event) => {
socketRef.current.onclose = (event) => {
console.log("WebSocket disconnected. Code:", event.code, "Reason:", event.reason)
setIsConnected("disconnected")

if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
const delay = INITIAL_RECONNECT_DELAY * Math.pow(2, reconnectAttempts)
console.log(`Attempting to reconnect in ${delay}ms...`)
setTimeout(() => {
setReconnectAttempts((prev) => prev + 1)
connectWebSocket()
}, delay)
} else {
console.log("Max reconnection attempts reached. Please try again later.")
}
reconnect()
}
}, [url])

setSocket(newSocket)
}, [accessToken, reconnectAttempts, throttledUpdateCrews])
const reconnect = useCallback(() => {
if (reconnectTimeoutRef.current) {
clearTimeout(reconnectTimeoutRef.current)
}
reconnectTimeoutRef.current = setTimeout(() => {
console.log("Attempting to reconnect...")
connect()
}, 5000) // 5초 후 재연결 시도
}, [connect])

useEffect(() => {
connectWebSocket()
connect()

return () => {
if (socket) {
socket.close()
if (socketRef.current) {
socketRef.current.close()
}
if (updateTimeoutRef.current) {
clearTimeout(updateTimeoutRef.current)
if (reconnectTimeoutRef.current) {
clearTimeout(reconnectTimeoutRef.current)
}
}
}, [connectWebSocket])
}, [connect])

return { isConnected, crews }
}

export default function PostrueCrew(props: PostureCrewProps): ReactElement {
const { toggleSidebar } = props
const accessToken = useAuthStore((state) => state.accessToken)
const { resetSnapShot } = useSnapShotStore()
const { openModal } = useModals()
const createSnapMutation = useCreateSnaphot()
const wsUrl = `wss://api.alignlab.site/ws/v1/groups/1/users?X-HERO-AUTH-TOKEN=${accessToken}`
const { isConnected, crews } = useWebSocket(wsUrl)

const { notification, setNotification } = useNotification()
const updateNotiMutation = useModifyNoti()
const { hasPermission } = usePushNotification()

const onClickCloseSideNavButton = (): void => {
toggleSidebar()
}

const onClickNotiAlarmTime = (option: NotiOption): void => {
setNotiAlarmTime(option.label)
patchNotiMutation.mutate(
{ id: userNoti?.id, duration: option.value },
updateNotiMutation.mutate(
{ isActive: notification?.isActive, duration: option.value },
{
onSuccess: (data: notification) => {
setNotiAlarmTime(option.label)
setUserNoti(data)
setNotification(data)
},
}
)
}

const onClickNotiAlarm = (): void => {
patchNotiMutation.mutate(
{ id: userNoti?.id, isActive: !userNoti?.isActive },
updateNotiMutation.mutate(
{ isActive: !notification?.isActive, duration: notification?.duration },
{
onSuccess: (data: notification) => {
setIsEnabled(data.isActive)
setUserNoti(data)
console.log("#### : ", data)
setNotification(data)
},
}
)
Expand All @@ -168,6 +170,8 @@ export default function PostrueCrew(props: PostureCrewProps): ReactElement {
})
}

console.log("notification: ", notification)

return (
<div className="flex h-full flex-col rounded-lg bg-[#FAFAFA] p-4">
<button onClick={onClickCloseSideNavButton} className="mb-8 p-1">
Expand All @@ -183,7 +187,7 @@ export default function PostrueCrew(props: PostureCrewProps): ReactElement {
<input
type="checkbox"
className="peer sr-only"
checked={isEnabled && hasPermission}
checked={notification ? notification?.isActive && hasPermission : false}
onChange={onClickNotiAlarm}
/>
<div className="peer h-6 w-11 rounded-full bg-gray-200 after:absolute after:left-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:border after:border-gray-300 after:bg-white after:transition-all after:content-[''] peer-checked:bg-blue-600 peer-checked:after:translate-x-full peer-checked:after:border-white"></div>
Expand All @@ -192,9 +196,9 @@ export default function PostrueCrew(props: PostureCrewProps): ReactElement {

<div className="pb-8 pl-2 pr-2">
<SelectBox
isDisabled={!userNoti?.isActive || !hasPermission}
isDisabled={!notification?.isActive || !hasPermission}
options={NOTI_OPTIONS}
value={notiAlarmTime}
value={NOTI_VALUE_MAP(notification?.duration)}
onClick={onClickNotiAlarmTime}
/>
</div>
Expand Down
11 changes: 5 additions & 6 deletions src/components/SideNav.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import { clearAccessToken } from "@/api/axiosInstance"
import { useAuthStore } from "@/store/AuthStore"
import { useSnapShotStore } from "@/store/SnapshotStore"
import MainCraftIcon from "@assets/icons/posture-craft-side-nav-icon.svg?react"
import AnalysisIcon from "@assets/icons/side-nav-analysis-icon.svg?react"
import CrewIcon from "@assets/icons/side-nav-crew-icon.svg?react"
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"
import { useNotificationStore } from "@/store/NotificationStore"
import { Link, useLocation, useNavigate } from "react-router-dom"

const navItems = [
{
Expand Down Expand Up @@ -36,12 +35,12 @@ export default function SideNav(): React.ReactElement {
const logoutHandler = (): void => {
const clearUser = useAuthStore.persist.clearStorage
const clearSnapshot = useSnapShotStore.persist.clearStorage
const clearNotification = useNotificationStore.persist.clearStorage
// const clearNotification = useNotificationStore.persist.clearStorage

clearUser()
clearSnapshot()
clearAccessToken()
clearNotification()
// clearNotification()

logout(() => {
navigate("/")
Expand Down
Loading

0 comments on commit 8601243

Please sign in to comment.