## GUARDiA Manager (frontend) - pages/DrConsole.tsx — DR 재해복구 관제 (시나리오/RTO-RPO/테스트 실행) - pages/NetworkConsole.tsx — 네트워크 장비 관제 (백업/diff/상태) - pages/CsapConsole.tsx — CSAP 준수율 대시보드 (점검/Excel 다운로드) - App.tsx — 3개 라우트 추가 (/dr, /network, /csap) - Sidebar.tsx — '운영 관제' 그룹 메뉴 추가 - AppLayout.tsx — 페이지 타이틀 3개 추가 ## GUARDiA Messenger (React Native) - app/(tabs)/dr.tsx — DR 모니터링 화면 (M-01) - app/(tabs)/network.tsx — 네트워크 장비 현황 화면 (M-02) - app/(tabs)/_layout.tsx — DR·네트워크 탭 추가 - services/api.ts — DR/네트워크/CSAP API 함수 추가 - hooks/useBiometric.ts — 생체인증 훅 (M-03) - hooks/useOfflineCache.ts — 오프라인 캐시 훅 (M-04) ## 매뉴얼 - 16_API_명세서.md — v2.2.0 업데이트 - 39_DR_네트워크장비_CSAP_운영가이드.md — Manager/Messenger UI 연동 현황 추가 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
63 lines
1.9 KiB
TypeScript
63 lines
1.9 KiB
TypeScript
import { useState, useEffect, useCallback } from 'react'
|
|
import * as SecureStore from 'expo-secure-store'
|
|
import NetInfo from '@react-native-community/netinfo'
|
|
|
|
interface CacheOptions {
|
|
ttlMs?: number // 캐시 유효 시간 (ms), 기본 5분
|
|
}
|
|
|
|
export function useOfflineCache<T>(
|
|
cacheKey: string,
|
|
fetcher: () => Promise<{ data: T }>,
|
|
options: CacheOptions = {}
|
|
) {
|
|
const { ttlMs = 5 * 60 * 1000 } = options
|
|
const [data, setData] = useState<T | null>(null)
|
|
const [isOffline, setOffline] = useState(false)
|
|
const [loading, setLoading] = useState(true)
|
|
const [cachedAt, setCachedAt] = useState<Date | null>(null)
|
|
|
|
useEffect(() => {
|
|
const unsub = NetInfo.addEventListener(state => {
|
|
setOffline(!(state.isConnected ?? true))
|
|
})
|
|
return () => unsub()
|
|
}, [])
|
|
|
|
const load = useCallback(async () => {
|
|
setLoading(true)
|
|
try {
|
|
if (!isOffline) {
|
|
const result = await fetcher()
|
|
setData(result.data)
|
|
const now = new Date()
|
|
setCachedAt(now)
|
|
await SecureStore.setItemAsync(cacheKey, JSON.stringify({
|
|
data: result.data, ts: now.toISOString(),
|
|
}))
|
|
} else {
|
|
const raw = await SecureStore.getItemAsync(cacheKey)
|
|
if (raw) {
|
|
const { data: cached, ts } = JSON.parse(raw)
|
|
const age = Date.now() - new Date(ts).getTime()
|
|
setData(cached)
|
|
setCachedAt(new Date(ts))
|
|
if (age > ttlMs) {
|
|
console.warn(`[OfflineCache] ${cacheKey} 캐시 만료 (${Math.floor(age / 60000)}분 전)`)
|
|
}
|
|
}
|
|
}
|
|
} catch {
|
|
const raw = await SecureStore.getItemAsync(cacheKey)
|
|
if (raw) {
|
|
const { data: cached, ts } = JSON.parse(raw)
|
|
setData(cached); setCachedAt(new Date(ts))
|
|
}
|
|
} finally { setLoading(false) }
|
|
}, [cacheKey, fetcher, isOffline, ttlMs])
|
|
|
|
useEffect(() => { load() }, [load])
|
|
|
|
return { data, isOffline, loading, cachedAt, reload: load }
|
|
}
|