diff --git a/app/(tabs)/_layout.tsx b/app/(tabs)/_layout.tsx index c272dc86..fb7c355f 100644 --- a/app/(tabs)/_layout.tsx +++ b/app/(tabs)/_layout.tsx @@ -91,6 +91,13 @@ export default function TabLayout() { tabBarIcon: ({ focused }) => , }} /> + , + }} + /> ('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' }, +})