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' },
+})