import { useState, useEffect } from 'react' import { View, Text, TextInput, TouchableOpacity, StyleSheet, KeyboardAvoidingView, Platform, ActivityIndicator, Alert, ScrollView, } from 'react-native' import { useRouter } from 'expo-router' import * as LocalAuthentication from 'expo-local-authentication' import * as SecureStore from 'expo-secure-store' import { useAuth } from '../../hooks/useAuth' import { COLORS } from '../../constants/Config' import { recordActivity } from '../../hooks/useSessionExpiry' export default function LoginScreen() { const { login } = useAuth() const router = useRouter() const [username, setUsername] = useState('') const [password, setPassword] = useState('') const [loading, setLoading] = useState(false) const [bioAvailable, setBioAvailable] = useState(false) // #29 토큰이 있을 때만 노출 /* #29 SecureStore에 토큰이 있고 생체 하드웨어가 등록된 경우에만 버튼 표시 */ useEffect(() => { ;(async () => { try { const token = await SecureStore.getItemAsync('grd_token') const hasHardware = await LocalAuthentication.hasHardwareAsync() const isEnrolled = await LocalAuthentication.isEnrolledAsync() setBioAvailable(!!token && hasHardware && isEnrolled) } catch { setBioAvailable(false) } })() }, []) /* #29 생체인증 로그인 */ const biometricLogin = async () => { const hasHardware = await LocalAuthentication.hasHardwareAsync() const isEnrolled = await LocalAuthentication.isEnrolledAsync() if (!hasHardware || !isEnrolled) { Alert.alert('생체인증 불가', '지문/Face ID가 등록되지 않았습니다.') return } const result = await LocalAuthentication.authenticateAsync({ promptMessage: 'GUARDiA 로그인', cancelLabel: '취소', fallbackLabel: '비밀번호 사용', }) if (result.success) { const token = await SecureStore.getItemAsync('grd_token') if (token) { await recordActivity() router.replace('/(tabs)') } else { Alert.alert('재로그인 필요', '저장된 인증 정보가 없습니다. 비밀번호로 로그인해주세요.') } } } const handleLogin = async () => { if (!username.trim() || !password.trim()) { Alert.alert('입력 오류', '아이디와 비밀번호를 입력해주세요.') return } setLoading(true) try { await login(username.trim(), password) await recordActivity() // #31 로그인 시점을 마지막 활동으로 기록 } catch (e: any) { const msg = e.response?.data?.detail ?? '로그인에 실패했습니다.' Alert.alert('로그인 실패', msg) } finally { setLoading(false) } } return ( {/* 로고 영역 */} 🛡️ GUARDiA AI 인프라 자율 운영 플랫폼 (주)지오정보기술 {/* 로그인 카드 */} 로그인 아이디 비밀번호 {loading ? : 로그인 } {bioAvailable && ( 👆 생체인증 로그인 )} GUARDiA ITSM 계정으로 로그인합니다 v1.0.0 · zioinfo.co.kr ) } /* Variant 스타일 로그인 — 딥네이비 배경 + 흰 카드 */ const s = StyleSheet.create({ container: { flex: 1, backgroundColor: '#001530' }, inner: { flexGrow: 1, justifyContent: 'center', padding: 28 }, logoBox: { alignItems: 'center', marginBottom: 36 }, logoIcon: { fontSize: 52, marginBottom: 10 }, logoTitle: { fontSize: 34, fontWeight: '900', color: '#fff', letterSpacing: 1.5 }, logoSub: { fontSize: 13, color: 'rgba(0,160,200,.85)', marginTop: 5 }, badge: { marginTop: 10, backgroundColor: 'rgba(0,160,200,.15)', borderWidth: 1, borderColor: 'rgba(0,160,200,.3)', paddingHorizontal: 14, paddingVertical: 5, borderRadius: 20, }, badgeText: { color: '#00A0C8', fontSize: 11, fontWeight: '700' }, card: { backgroundColor: '#fff', borderRadius: 20, padding: 28, shadowColor: '#001530', shadowOffset: { width: 0, height: 12 }, shadowOpacity: 0.25, shadowRadius: 32, elevation: 10, }, cardTitle: { fontSize: 18, fontWeight: '800', color: '#003366', marginBottom: 22, textAlign: 'center', letterSpacing: -0.3, }, field: { marginBottom: 16 }, label: { fontSize: 11, fontWeight: '700', color: '#00A0C8', marginBottom: 6, textTransform: 'uppercase', letterSpacing: .6 }, input: { borderWidth: 1.5, borderColor: '#E2E8F0', borderRadius: 12, padding: 14, fontSize: 15, color: '#1E293B', backgroundColor: '#F8FAFC', }, btn: { backgroundColor: '#00A0C8', borderRadius: 12, padding: 16, alignItems: 'center', marginTop: 8, shadowColor: '#00A0C8', shadowOffset: { width: 0, height: 4 }, shadowOpacity: .3, shadowRadius: 10, elevation: 4, }, btnDisabled: { opacity: .6 }, btnText: { color: '#fff', fontSize: 16, fontWeight: '800', letterSpacing: 0.3 }, bioBtn: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 8, marginTop: 12, padding: 14, borderRadius: 12, borderWidth: 1.5, borderColor: '#00A0C8', backgroundColor: 'rgba(0,160,200,.06)', }, bioIcon: { fontSize: 18 }, bioText: { color: '#00A0C8', fontSize: 15, fontWeight: '700' }, hint: { textAlign: 'center', color: '#64748B', fontSize: 12, marginTop: 16 }, version: { textAlign: 'center', color: 'rgba(0,160,200,.4)', fontSize: 11, marginTop: 24 }, })