/* * SCR-01 로그인 — 이메일/비번 + 아이디 기억 + 비밀번호 찾기·회원가입 링크 + 2FA(OTP) 대응. * 브랜드 패널(딥블루 그라디언트 대체 카피) + AI 이미지 고지 캡션. * 비밀번호는 저장하지 않는다. 아이디(이메일)만 AsyncStorage에 기억. */ import { Link, router } from 'expo-router'; import React, { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { KeyboardAvoidingView, Platform, Pressable, ScrollView, StyleSheet, Text, View, } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { Banner } from '../components/Banner'; import { Button } from '../components/Button'; import { Field } from '../components/Field'; import { useAuth } from '../context/AuthContext'; import { useSecureScreen } from '../context/SecureScreenContext'; import { ApiRequestError } from '../lib/api'; import { isOtpRequired, login } from '../lib/auth'; import { useDeviceIntegrity } from '../lib/integrity'; import { landingPathFor } from '../lib/roleTrack'; import { prefDelete, prefGet, prefSet } from '../lib/secureStore'; import { colors, radius, spacing, touch, type } from '../theme'; const REMEMBER_KEY = 'kintex.rememberedEmail'; export default function LoginScreen() { const insets = useSafeAreaInsets(); const { t } = useTranslation(); const { signIn } = useAuth(); // 2FA/자격증명 입력 화면 — 캡처 차단 + 백그라운드 마스킹(B4/B7). useSecureScreen('login'); // 위변조(루팅·후킹·디버거) 단말 경고(B1/B9) — 비차단 고지. const integrity = useDeviceIntegrity(); const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const [otp, setOtp] = useState(''); const [otpRequired, setOtpRequired] = useState(false); const [remember, setRemember] = useState(true); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); useEffect(() => { (async () => { const saved = await prefGet(REMEMBER_KEY); if (saved) { setEmail(saved); setRemember(true); } })(); }, []); async function onSubmit() { setError(null); if (!email.trim() || !password) { setError(t('login.errEmpty')); return; } if (otpRequired && !otp.trim()) { setError(t('login.errOtpEmpty')); return; } setLoading(true); try { const res = await login({ email, password, otp: otpRequired ? otp : undefined }); if (remember) await prefSet(REMEMBER_KEY, email.trim()); else await prefDelete(REMEMBER_KEY); await signIn(res); // 역할 우선순위로 랜딩 결정(웹 roleTrack 패리티) — visitor=관람객 티켓, agency=현장, 그 외 홈. const landing = landingPathFor({ user: res.user, workspaces: res.workspaces }); router.replace(landing as never); } catch (e) { if (isOtpRequired(e)) { setOtpRequired(true); setError(t('login.errOtpRequired')); } else if (e instanceof ApiRequestError && e.code === 'OTP_INVALID') { setError(t('login.errOtpInvalid')); } else if (e instanceof ApiRequestError && e.code === 'ACCOUNT_LOCKED') { setError(t('login.errLocked')); } else if (e instanceof ApiRequestError) { setError(e.message); } else { setError(t('login.errGeneric')); } } finally { setLoading(false); } } return ( {/* 브랜드 패널 */} KINTEX {t('login.brandTitle')} {t('login.brandCaption')} {/* 위변조 단말 경고(비차단) — 공공 대민 정책: 경고 후 이용 제한 안내 */} {integrity.compromised ? ( {t('login.compromised')} ) : null} {/* 로그인 카드 */} {t('login.title')} {error ? ( {error} ) : null} {otpRequired ? ( ) : null} {/* 아이디 기억 */} setRemember((v) => !v)} accessibilityRole="checkbox" accessibilityState={{ checked: remember }} > {remember ? : null} {t('login.remember')}