import { useState, useEffect } from 'react' import { View, Text, StyleSheet, TouchableOpacity, Alert, ScrollView, Platform, Linking, ActivityIndicator, } from 'react-native' import { COLORS, API_BASE } from '../../constants/Config' import { getToken } from '../../utils/auth' // expo-barcode-scanner는 EAS 빌드 환경에서만 실제 작동 // 개발/시뮬레이터에서는 수동 입력으로 대체 interface AssetInfo { server_id: number; server_name: string; ip_display: string os_name: string; location: string; status: string; last_checked: string qr_token: string } export default function ScanTab() { const [mode, setMode] = useState<'qr'|'manual'>('qr') const [manualToken, setManualToken] = useState('') const [scanning, setScanning] = useState(false) const [loading, setLoading] = useState(false) const [asset, setAsset] = useState(null) const [checkedIn, setCheckedIn] = useState(false) async function lookupToken(token: string) { if (!token.trim()) return setLoading(true); setAsset(null); setCheckedIn(false) try { const jwt = await getToken() const res = await fetch(`${API_BASE}/api/asset-qr/scan/${token.trim()}`, { headers: { Authorization: `Bearer ${jwt}` }, }) if (!res.ok) { const e = await res.json().catch(() => ({})) Alert.alert('조회 실패', e.detail || '자산을 찾을 수 없습니다') return } const data = await res.json() setAsset(data) } catch (e: any) { Alert.alert('오류', e.message || '서버 연결 실패') } finally { setLoading(false) } } async function doCheckin() { if (!asset) return setLoading(true) try { const jwt = await getToken() await fetch(`${API_BASE}/api/asset-qr/checkin/${asset.qr_token}`, { method: 'POST', headers: { Authorization: `Bearer ${jwt}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ note: '모바일 실사' }), }) setCheckedIn(true) Alert.alert('✅ 실사 완료', `${asset.server_name} 실사 완료 처리했습니다.`) } catch { Alert.alert('오류', '체크인 실패') } finally { setLoading(false) } } const statusColor = (s: string) => { if (s === 'ACTIVE') return '#166534' if (s === 'INACTIVE') return '#9a3412' return '#92400e' } return ( 📱 자산 QR 스캔 서버 라벨의 QR코드를 스캔하여 CMDB 정보를 조회합니다 {/* 모드 선택 */} {[{id:'qr',label:'📷 QR 스캔'},{id:'manual',label:'⌨️ 토큰 입력'}].map(t => ( setMode(t.id as any)} style={[S.tab, mode === t.id && S.tabActive]}> {t.label} ))} {mode === 'qr' ? ( 📷 카메라 QR 스캔{Platform.OS === 'android' ? ' (Android 지원)' : ' (iOS 지원)'} expo-barcode-scanner 모듈 필요 { Alert.alert( 'QR 스캔', 'QR 스캔은 EAS 빌드 앱에서 사용 가능합니다.\n토큰 직접 입력 탭을 이용하세요.', [{ text: '토큰 입력으로 이동', onPress: () => setMode('manual') }, { text: '확인' }] ) }}> 📷 QR 코드 스캔 시작 ) : ( QR 토큰 (UUID) {/* focus */}}> {manualToken || 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'} lookupToken(manualToken)}> 조회 라벨의 UUID를 입력하거나 QR 이미지 하단의 텍스트를 입력하세요 )} {/* 로딩 */} {loading && ( 조회 중... )} {/* 자산 정보 */} {asset && !loading && ( {asset.server_name} {asset.ip_display} {asset.status} {[ { label: 'OS', value: asset.os_name }, { label: '위치', value: asset.location || '미지정' }, { label: '마지막 점검', value: asset.last_checked ? new Date(asset.last_checked).toLocaleDateString('ko-KR') : '기록 없음' }, ].map(item => ( {item.label} {item.value} ))} {checkedIn ? '✅ 실사 완료됨' : '📋 실사 체크인'} )} {/* 앱 QR 다운로드 안내 */} 💡 앱 배포 QR GUARDiA Manager에서 최신 APK를 배포하면 QR코드가 생성됩니다.{'\n'} 다른 사용자에게 QR을 공유하여 앱스토어 없이 설치할 수 있습니다. Linking.openURL('https://zioinfo.co.kr:8443/api/app/landing')} style={{ marginTop: 8 }}> 앱 다운로드 페이지 열기 → ) } const S = StyleSheet.create({ root: { flex: 1, backgroundColor: '#f8fafc' }, header: { padding: 20, paddingBottom: 12, backgroundColor: '#003366' }, title: { fontSize: 20, fontWeight: '800', color: '#fff' }, subtitle: { fontSize: 12, color: 'rgba(255,255,255,0.7)', marginTop: 4 }, tabs: { flexDirection: 'row', backgroundColor: '#fff', borderBottomWidth: 1, borderColor: '#e2e8f0' }, tab: { flex: 1, padding: 12, alignItems: 'center', borderBottomWidth: 2, borderColor: 'transparent' }, tabActive: { borderColor: '#003366' }, tabText: { fontSize: 13, color: '#64748b' }, tabTextActive: { color: '#003366', fontWeight: '700' }, card: { margin: 12, marginBottom: 0, backgroundColor: '#fff', borderRadius: 12, padding: 16, borderWidth: 1, borderColor: '#e2e8f0', shadowColor: '#000', shadowOffset: { width: 0, height: 1 }, shadowOpacity: 0.05, shadowRadius: 4, elevation: 2 }, qrBox: { alignItems: 'center', paddingVertical: 24, backgroundColor: '#f8fafc', borderRadius: 8, marginBottom: 12 }, btn: { backgroundColor: '#003366', borderRadius: 8, padding: 12, alignItems: 'center', marginTop: 8 }, btnText: { color: '#fff', fontWeight: '700', fontSize: 14 }, row: { flexDirection: 'row', alignItems: 'center' }, input: { borderWidth: 1, borderColor: '#e2e8f0', borderRadius: 8, padding: 10, backgroundColor: '#f8fafc' }, fieldLabel: { fontSize: 12, fontWeight: '600', color: '#374151', marginBottom: 6 }, infoRow: { flexDirection: 'row', justifyContent: 'space-between', paddingVertical: 8, borderBottomWidth: 1, borderColor: '#f1f5f9' }, infoLabel: { fontSize: 12, color: '#64748b' }, infoValue: { fontSize: 13, fontWeight: '600', color: '#1e293b' }, })