/* * 민감화면 보호 컨텍스트 — B4(화면캡처 차단) + B7(백그라운드 스냅샷 마스킹) 통합. * 근거: docs/security/mobile-security-audit.md 백로그 B4·B7, checklist DP-4·DP-5. * * 동작: * · 민감화면이 focus되면 useSecureScreen 훅이 등록(activeCount++) + preventCapture. * · 앱이 background/inactive(태스크 스위처·홈 전환)로 가고 activeCount>0이면 * 루트에 브랜드 오버레이를 덮어 스냅샷에 민감 정보가 남지 않게 마스킹. * · 이탈/포그라운드 복귀 시 자동 해제. * 방어적: 캡처 모듈 부재여도 오버레이(B7)는 순수 RN(AppState)로 동작. */ import { useFocusEffect } from 'expo-router'; import React, { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, } from 'react'; import { AppState, type AppStateStatus, StyleSheet, Text, View } from 'react-native'; import { allowCapture, preventCapture } from '../lib/screenCapture'; import { colors, spacing, type } from '../theme'; interface SecureScreenContextValue { register: () => void; unregister: () => void; } const SecureScreenContext = createContext(undefined); export function SecureScreenProvider({ children }: { children: React.ReactNode }) { const [activeCount, setActiveCount] = useState(0); const [appState, setAppState] = useState(AppState.currentState); const register = useCallback(() => setActiveCount((n) => n + 1), []); const unregister = useCallback(() => setActiveCount((n) => Math.max(0, n - 1)), []); useEffect(() => { const sub = AppState.addEventListener('change', (next) => setAppState(next)); return () => sub.remove(); }, []); const value = useMemo(() => ({ register, unregister }), [register, unregister]); // 민감화면 활성 + 비활성 상태(태스크 스위처/백그라운드) → 마스킹. const masked = activeCount > 0 && appState !== 'active'; return ( {children} {masked ? ( KINTEX AI 전시·행사시스템 ) : null} ); } /** * 민감화면 보호 훅 — 화면 컴포넌트 본문에서 1회 호출. * @param key 캡처 스택 구분용 고유 키(화면별 상수). * focus 시 캡처 차단 + 백그라운드 마스킹 등록, blur 시 해제. */ export function useSecureScreen(key: string): void { const ctx = useContext(SecureScreenContext); const registered = useRef(false); useFocusEffect( useCallback(() => { preventCapture(key); if (ctx && !registered.current) { ctx.register(); registered.current = true; } return () => { allowCapture(key); if (ctx && registered.current) { ctx.unregister(); registered.current = false; } }; // ctx는 프로바이더 수명 내 안정적 — key 변경 시에만 재바인딩 }, [ctx, key]), ); } const styles = StyleSheet.create({ mask: { ...StyleSheet.absoluteFillObject, backgroundColor: colors.primary700, alignItems: 'center', justifyContent: 'center', gap: spacing.xs, }, markTop: { color: colors.white, fontSize: 30, fontWeight: '800', letterSpacing: 2 }, markSub: { color: 'rgba(255,255,255,0.8)', fontSize: type.body.fontSize, fontWeight: '600' }, });