itsm_ysm/mobile/app/assets.tsx
itms-merge-dev efc3e66635 feat(itms): 모바일 앱 신규 (Expo, 12화면, OAuth password grant)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 17:10:46 +09:00

82 lines
3.6 KiB
TypeScript

import { useCallback, useState } from 'react'
import { View, Text, FlatList, TouchableOpacity, StyleSheet, TextInput, RefreshControl } from 'react-native'
import { router, useFocusEffect } from 'expo-router'
import { Feather } from '@expo/vector-icons'
import { useTheme } from '../constants/ThemeContext'
import { type ThemeTokens } from '../constants/ItmsTheme'
import { EmptyState, StatusPill } from '../components/ui'
import { rowTitle, rowStatus, pick } from '../constants/rows'
import { itmsSearchAssets, listRows, itmsErrorMessage, type RowMap } from './itmsApi'
/*
* 자산 조회(ResourceManagerController /rem/searchAsset.do). 검색 + 새로고침.
* 상세는 목록 raw 를 넘겨 getAssetInfo 로 보강.
*/
export default function Assets() {
const { t } = useTheme()
const s = makeStyles(t)
const [rows, setRows] = useState<RowMap[]>([])
const [keyword, setKeyword] = useState('')
const [loading, setLoading] = useState(false)
const [err, setErr] = useState('')
const load = useCallback(async (kw?: string) => {
setLoading(true); setErr('')
try {
const filter: RowMap = {}
const k = (kw ?? keyword).trim()
if (k) { filter.searchKeyword = k; filter.searchWrd = k }
setRows(listRows(await itmsSearchAssets(filter)))
} catch (e) { setErr(itmsErrorMessage(e)); setRows([]) }
finally { setLoading(false) }
}, [keyword])
useFocusEffect(useCallback(() => { load() }, [load]))
const assetCode = (r: RowMap) => pick(r, ['assetId', 'resrceId', 'assetCode', 'resrceNo', 'mngNo', 'serialNo'])
return (
<View style={s.wrap}>
<View style={s.head}>
<View style={s.searchBar}>
<Feather name="search" size={16} color={t.muted} />
<TextInput
style={s.searchInput} value={keyword} onChangeText={setKeyword}
placeholder="자산명·자산번호 검색" placeholderTextColor={t.muted}
returnKeyType="search" onSubmitEditing={() => load()}
/>
</View>
</View>
<FlatList
data={rows}
keyExtractor={(_, i) => String(i)}
contentContainerStyle={rows.length === 0 ? { flexGrow: 1 } : { padding: 16 }}
refreshControl={<RefreshControl refreshing={loading} onRefresh={() => load()} tintColor={t.accent} />}
ListEmptyComponent={loading ? null : <EmptyState icon="server" text={err || '표시할 자산이 없습니다.'} />}
renderItem={({ item }) => (
<TouchableOpacity
style={s.card}
onPress={() => router.push({ pathname: '/asset-detail', params: { raw: JSON.stringify(item) } })}
>
<View style={{ flex: 1 }}>
<Text style={s.cardTitle} numberOfLines={1}>{rowTitle(item)}</Text>
<Text style={s.cardSub}>{assetCode(item)}</Text>
</View>
{!!rowStatus(item) && <StatusPill label={rowStatus(item)} />}
</TouchableOpacity>
)}
/>
</View>
)
}
const makeStyles = (t: ThemeTokens) => StyleSheet.create({
wrap: { flex: 1, backgroundColor: t.bg },
head: { paddingHorizontal: 16, paddingTop: 16, paddingBottom: 8 },
searchBar: { flexDirection: 'row', alignItems: 'center', gap: 8, backgroundColor: t.card, borderRadius: 12, borderWidth: 1, borderColor: t.border, paddingHorizontal: 12 },
searchInput: { flex: 1, color: t.text, fontSize: 14, paddingVertical: 10 },
card: { flexDirection: 'row', alignItems: 'center', gap: 10, backgroundColor: t.card, borderRadius: 12, borderWidth: 1, borderColor: t.border, padding: 14, marginBottom: 10 },
cardTitle: { color: t.text, fontSize: 15, fontWeight: '700' },
cardSub: { color: t.muted, fontSize: 12, marginTop: 4 },
})