- 37개 파일 IP → zioinfo.co.kr 치환 (소스/매뉴얼/설정/하네스) - Manager DrConsole/NetworkConsole/CsapConsole 빌드 + /var/www/manager/ 배포 - 테스트: Manager HTTP 200, ITSM 신규 API 7개 전체 200 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
197 lines
8.7 KiB
TypeScript
197 lines
8.7 KiB
TypeScript
import { useEffect, useState } from 'react'
|
|
import {
|
|
View, Text, ScrollView, StyleSheet, TouchableOpacity,
|
|
RefreshControl, ActivityIndicator, TextInput, Modal, Alert,
|
|
} from 'react-native'
|
|
import { COLORS, PRIORITY_COLOR, STATUS_COLOR } from '../../constants/Config'
|
|
import { getSRList, createSR } from '../../services/api'
|
|
|
|
const PRIORITIES = ['CRITICAL','HIGH','MEDIUM','LOW']
|
|
const TYPES = ['DEPLOY','RESTART','LOG','INQUIRY','OTHER']
|
|
|
|
export default function SRScreen() {
|
|
const [items, setItems] = useState<any[]>([])
|
|
const [loading, setLoading] = useState(true)
|
|
const [refresh, setRefresh] = useState(false)
|
|
const [creating, setCreating] = useState(false)
|
|
const [form, setForm] = useState({ title: '', description: '', priority: 'MEDIUM', sr_type: 'OTHER' })
|
|
const [modal, setModal] = useState(false)
|
|
const [saving, setSaving] = useState(false)
|
|
|
|
const load = async (r = false) => {
|
|
r ? setRefresh(true) : setLoading(true)
|
|
try {
|
|
const res = await getSRList()
|
|
setItems(res.data.content ?? res.data.items ?? res.data ?? [])
|
|
} catch {}
|
|
setLoading(false); setRefresh(false)
|
|
}
|
|
|
|
useEffect(() => { load() }, [])
|
|
|
|
const submit = async () => {
|
|
if (!form.title.trim()) { Alert.alert('제목을 입력하세요.'); return }
|
|
setSaving(true)
|
|
try {
|
|
await createSR(form)
|
|
setModal(false)
|
|
setForm({ title:'', description:'', priority:'MEDIUM', sr_type:'OTHER' })
|
|
await load()
|
|
Alert.alert('등록 완료', 'SR이 접수되었습니다.')
|
|
} catch (e: any) {
|
|
Alert.alert('오류', e.response?.data?.detail ?? 'SR 등록 실패')
|
|
} finally { setSaving(false) }
|
|
}
|
|
|
|
return (
|
|
<View style={{ flex: 1, backgroundColor: COLORS.bg }}>
|
|
<View style={s.toolbar}>
|
|
<Text style={s.toolbarTitle}>서비스 요청 목록</Text>
|
|
<TouchableOpacity style={s.addBtn} onPress={() => setModal(true)}>
|
|
<Text style={s.addBtnText}>+ 새 SR</Text>
|
|
</TouchableOpacity>
|
|
</View>
|
|
|
|
{loading
|
|
? <ActivityIndicator style={{ marginTop: 60 }} color={COLORS.accent} />
|
|
: (
|
|
<ScrollView
|
|
refreshControl={<RefreshControl refreshing={refresh} onRefresh={() => load(true)} />}
|
|
>
|
|
{items.length === 0 && (
|
|
<Text style={{ textAlign:'center', color:COLORS.muted, marginTop:60 }}>
|
|
SR이 없습니다.
|
|
</Text>
|
|
)}
|
|
{items.map(sr => (
|
|
<View key={sr.id} style={s.card}>
|
|
<View style={s.cardHead}>
|
|
<Text style={s.srId}>{sr.sr_id}</Text>
|
|
<View style={[s.statusBadge, { backgroundColor: STATUS_COLOR[sr.status]+'22' }]}>
|
|
<Text style={[s.statusText, { color: STATUS_COLOR[sr.status] ?? COLORS.muted }]}>
|
|
{sr.status}
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
<Text style={s.srTitle} numberOfLines={2}>{sr.title}</Text>
|
|
<View style={s.cardFoot}>
|
|
<View style={[s.priBadge, { backgroundColor: PRIORITY_COLOR[sr.priority]+'22' }]}>
|
|
<Text style={[s.priText, { color: PRIORITY_COLOR[sr.priority] }]}>
|
|
{sr.priority}
|
|
</Text>
|
|
</View>
|
|
<Text style={s.metaText}>{sr.requested_by}</Text>
|
|
<Text style={s.metaText}>{sr.created_at?.slice(0,10)}</Text>
|
|
</View>
|
|
</View>
|
|
))}
|
|
<View style={{ height: 24 }} />
|
|
</ScrollView>
|
|
)
|
|
}
|
|
|
|
{/* SR 등록 모달 */}
|
|
<Modal visible={modal} animationType="slide" presentationStyle="pageSheet">
|
|
<View style={s.modal}>
|
|
<View style={s.modalHead}>
|
|
<Text style={s.modalTitle}>새 SR 등록</Text>
|
|
<TouchableOpacity onPress={() => setModal(false)}>
|
|
<Text style={{ fontSize: 24, color: COLORS.muted }}>✕</Text>
|
|
</TouchableOpacity>
|
|
</View>
|
|
<ScrollView style={{ padding: 20 }}>
|
|
{[
|
|
{ label: '제목 *', key: 'title', ph: 'SR 제목을 입력하세요', multi: false },
|
|
{ label: '설명', key: 'description', ph: '상세 내용을 입력하세요', multi: true },
|
|
].map(f => (
|
|
<View key={f.key} style={s.field}>
|
|
<Text style={s.label}>{f.label}</Text>
|
|
<TextInput
|
|
style={[s.input, f.multi && { height: 100, textAlignVertical: 'top' }]}
|
|
value={(form as any)[f.key]}
|
|
onChangeText={v => setForm(p => ({ ...p, [f.key]: v }))}
|
|
placeholder={f.ph}
|
|
placeholderTextColor={COLORS.muted}
|
|
multiline={f.multi}
|
|
/>
|
|
</View>
|
|
))}
|
|
|
|
<View style={s.field}>
|
|
<Text style={s.label}>우선순위</Text>
|
|
<View style={s.chips}>
|
|
{PRIORITIES.map(p => (
|
|
<TouchableOpacity key={p}
|
|
style={[s.chip, form.priority === p && { backgroundColor: PRIORITY_COLOR[p], borderColor: PRIORITY_COLOR[p] }]}
|
|
onPress={() => setForm(f => ({ ...f, priority: p }))}
|
|
>
|
|
<Text style={[s.chipText, form.priority === p && { color: '#fff' }]}>{p}</Text>
|
|
</TouchableOpacity>
|
|
))}
|
|
</View>
|
|
</View>
|
|
|
|
<View style={s.field}>
|
|
<Text style={s.label}>유형</Text>
|
|
<View style={s.chips}>
|
|
{TYPES.map(t => (
|
|
<TouchableOpacity key={t}
|
|
style={[s.chip, form.sr_type === t && s.chipActive]}
|
|
onPress={() => setForm(f => ({ ...f, sr_type: t }))}
|
|
>
|
|
<Text style={[s.chipText, form.sr_type === t && { color: '#fff' }]}>{t}</Text>
|
|
</TouchableOpacity>
|
|
))}
|
|
</View>
|
|
</View>
|
|
|
|
<TouchableOpacity style={[s.submitBtn, saving && { opacity: .6 }]} onPress={submit} disabled={saving}>
|
|
{saving
|
|
? <ActivityIndicator color="#fff" />
|
|
: <Text style={s.submitText}>SR 등록</Text>
|
|
}
|
|
</TouchableOpacity>
|
|
</ScrollView>
|
|
</View>
|
|
</Modal>
|
|
</View>
|
|
)
|
|
}
|
|
|
|
const s = StyleSheet.create({
|
|
toolbar: { flexDirection:'row', justifyContent:'space-between', alignItems:'center',
|
|
backgroundColor:'#fff', paddingHorizontal:16, paddingVertical:12,
|
|
borderBottomWidth:1, borderBottomColor:COLORS.border },
|
|
toolbarTitle:{ fontSize:15, fontWeight:'700', color:COLORS.text },
|
|
addBtn: { backgroundColor:COLORS.accent, paddingHorizontal:14, paddingVertical:7, borderRadius:8 },
|
|
addBtnText: { color:'#fff', fontSize:13, fontWeight:'600' },
|
|
card: { backgroundColor:'#fff', marginHorizontal:16, marginTop:10, borderRadius:10,
|
|
padding:14, elevation:1 },
|
|
cardHead: { flexDirection:'row', justifyContent:'space-between', marginBottom:6 },
|
|
srId: { fontSize:11, color:COLORS.accent, fontWeight:'600' },
|
|
statusBadge: { paddingHorizontal:8, paddingVertical:2, borderRadius:10 },
|
|
statusText: { fontSize:10, fontWeight:'600' },
|
|
srTitle: { fontSize:14, fontWeight:'600', color:COLORS.text, marginBottom:8 },
|
|
cardFoot: { flexDirection:'row', alignItems:'center', gap:8 },
|
|
priBadge: { paddingHorizontal:8, paddingVertical:2, borderRadius:10 },
|
|
priText: { fontSize:10, fontWeight:'700' },
|
|
metaText: { fontSize:11, color:COLORS.muted },
|
|
modal: { flex:1, backgroundColor:'#fff' },
|
|
modalHead: { flexDirection:'row', justifyContent:'space-between', alignItems:'center',
|
|
padding:20, borderBottomWidth:1, borderBottomColor:COLORS.border },
|
|
modalTitle: { fontSize:17, fontWeight:'700', color:COLORS.text },
|
|
field: { marginBottom:16 },
|
|
label: { fontSize:11, fontWeight:'600', color:COLORS.muted, marginBottom:6,
|
|
textTransform:'uppercase', letterSpacing:.5 },
|
|
input: { borderWidth:1.5, borderColor:COLORS.border, borderRadius:9,
|
|
padding:12, fontSize:14, color:COLORS.text },
|
|
chips: { flexDirection:'row', flexWrap:'wrap', gap:8 },
|
|
chip: { paddingHorizontal:12, paddingVertical:6, borderRadius:20,
|
|
borderWidth:1, borderColor:COLORS.border },
|
|
chipActive: { backgroundColor:COLORS.accent, borderColor:COLORS.accent },
|
|
chipText: { fontSize:12, color:COLORS.text },
|
|
submitBtn: { backgroundColor:COLORS.primary, borderRadius:10, padding:15,
|
|
alignItems:'center', marginTop:8, marginBottom:32 },
|
|
submitText: { color:'#fff', fontSize:15, fontWeight:'700' },
|
|
})
|