66 lines
1.9 KiB
TypeScript
66 lines
1.9 KiB
TypeScript
import { useEffect, useRef, useState } from 'react'
|
|
import { Text, StyleSheet, Vibration } from 'react-native'
|
|
|
|
interface Props {
|
|
deadline: string // ISO 문자열
|
|
onExpire?: () => void
|
|
}
|
|
|
|
const URGENT_MS = 30 * 60 * 1000 // 30분
|
|
|
|
/**
|
|
* 기능 #3 — SLA 카운트다운 타이머
|
|
* 매 초 갱신, 임박(30분 이내) 시 빨간색 + 1회 진동
|
|
*/
|
|
export default function SlaTimer({ deadline, onExpire }: Props) {
|
|
const [remaining, setRemaining] = useState<number>(
|
|
() => new Date(deadline).getTime() - Date.now()
|
|
)
|
|
const buzzedRef = useRef(false)
|
|
const expiredRef = useRef(false)
|
|
|
|
useEffect(() => {
|
|
buzzedRef.current = false
|
|
expiredRef.current = false
|
|
const tick = () => {
|
|
const r = new Date(deadline).getTime() - Date.now()
|
|
setRemaining(r)
|
|
if (r < URGENT_MS && r > 0 && !buzzedRef.current) {
|
|
buzzedRef.current = true
|
|
try { Vibration.vibrate([0, 500, 200, 500]) } catch {}
|
|
}
|
|
if (r <= 0 && !expiredRef.current) {
|
|
expiredRef.current = true
|
|
onExpire?.()
|
|
}
|
|
}
|
|
tick()
|
|
const interval = setInterval(tick, 1000)
|
|
return () => clearInterval(interval)
|
|
}, [deadline, onExpire])
|
|
|
|
const overdue = remaining < 0
|
|
const abs = Math.abs(remaining)
|
|
const hours = Math.floor(abs / 3600000)
|
|
const minutes = Math.floor((abs % 3600000) / 60000)
|
|
const seconds = Math.floor((abs % 60000) / 1000)
|
|
const isUrgent = remaining < URGENT_MS
|
|
|
|
const pad = (n: number) => String(n).padStart(2, '0')
|
|
const label = overdue
|
|
? `초과 ${pad(hours)}:${pad(minutes)}:${pad(seconds)}`
|
|
: `${pad(hours)}:${pad(minutes)}:${pad(seconds)}`
|
|
|
|
return (
|
|
<Text style={[styles.timer, isUrgent && styles.urgent, overdue && styles.overdue]}>
|
|
{label}
|
|
</Text>
|
|
)
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
timer: { fontVariant: ['tabular-nums'], fontWeight: '700', fontSize: 15, color: '#64748B' },
|
|
urgent: { color: '#f59e0b' },
|
|
overdue: { color: '#ef4444' },
|
|
})
|