kintex/mobile/context/SecureScreenContext.tsx
zio 214b6df1ef feat(public,security,data): visitor public site + AI guide, security hardening, V42-V45 migrations
- frontend: visitor public home (AiAssistant, AiPlanningBriefing, VisitorTrackPage),
  3-track public routing (PublicShell/App), i18n ko/en/zh/ja, favicon
- backend: public AI visitor-assistant + event calendar API (publicsite/*),
  profile avatar API (auth/profile/*), SecurityConfig CORS whitelist,
  SecretStartupValidator (B12 fail-fast, prod only), application-prod.yml,
  AppIntegrity, /api/auth/me expansion (MeResponse)
- db: V42 bulk demo seed, V43 visitor_guide/transport + event calendar view,
  V44 performance indexes, V45 app_user profile photo columns (all idempotent)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 19:05:00 +09:00

102 lines
3.6 KiB
TypeScript

/*
* 민감화면 보호 컨텍스트 — 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<SecureScreenContextValue | undefined>(undefined);
export function SecureScreenProvider({ children }: { children: React.ReactNode }) {
const [activeCount, setActiveCount] = useState(0);
const [appState, setAppState] = useState<AppStateStatus>(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<SecureScreenContextValue>(() => ({ register, unregister }), [register, unregister]);
// 민감화면 활성 + 비활성 상태(태스크 스위처/백그라운드) → 마스킹.
const masked = activeCount > 0 && appState !== 'active';
return (
<SecureScreenContext.Provider value={value}>
{children}
{masked ? (
<View style={styles.mask} pointerEvents="none" accessibilityElementsHidden>
<Text style={styles.markTop}>KINTEX</Text>
<Text style={styles.markSub}>AI ·</Text>
</View>
) : null}
</SecureScreenContext.Provider>
);
}
/**
* 민감화면 보호 훅 — 화면 컴포넌트 본문에서 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' },
});