83 lines
2.7 KiB
TypeScript
83 lines
2.7 KiB
TypeScript
import { View, Text, FlatList, StyleSheet } from 'react-native'
|
|
import { COLORS } from '../constants/Config'
|
|
|
|
export interface TimelineEvent {
|
|
timestamp: string
|
|
actor: string
|
|
action: string
|
|
detail?: string
|
|
}
|
|
|
|
interface Props {
|
|
events: TimelineEvent[]
|
|
}
|
|
|
|
const ACTION_COLOR: Record<string, string> = {
|
|
created: COLORS.accent,
|
|
assigned: COLORS.blue,
|
|
status: '#4f6ef7',
|
|
escalated: COLORS.danger,
|
|
comment: COLORS.muted,
|
|
resolved: COLORS.success,
|
|
closed: COLORS.success,
|
|
}
|
|
|
|
/**
|
|
* 기능 #6 — 인시던트 타임라인
|
|
* 수직 타임라인 UI (점 + 연결선 + 이벤트 카드)
|
|
*/
|
|
export default function IncidentTimeline({ events }: Props) {
|
|
if (!events || events.length === 0) {
|
|
return <Text style={s.empty}>타임라인 이벤트가 없습니다.</Text>
|
|
}
|
|
|
|
return (
|
|
<FlatList
|
|
data={events}
|
|
scrollEnabled={false}
|
|
keyExtractor={(_, i) => String(i)}
|
|
renderItem={({ item, index }) => {
|
|
const color = ACTION_COLOR[item.action?.toLowerCase()] ?? COLORS.accent
|
|
const isLast = index === events.length - 1
|
|
return (
|
|
<View style={s.row}>
|
|
<View style={s.gutter}>
|
|
<View style={[s.dot, { backgroundColor: color }]} />
|
|
{!isLast && <View style={s.line} />}
|
|
</View>
|
|
<View style={s.content}>
|
|
<View style={s.head}>
|
|
<Text style={s.action}>{item.action}</Text>
|
|
<Text style={s.time}>{formatTime(item.timestamp)}</Text>
|
|
</View>
|
|
<Text style={s.actor}>{item.actor}</Text>
|
|
{!!item.detail && <Text style={s.detail}>{item.detail}</Text>}
|
|
</View>
|
|
</View>
|
|
)
|
|
}}
|
|
/>
|
|
)
|
|
}
|
|
|
|
function formatTime(ts: string): string {
|
|
try {
|
|
const d = new Date(ts)
|
|
return `${d.getMonth() + 1}/${d.getDate()} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`
|
|
} catch { return ts }
|
|
}
|
|
|
|
const s = StyleSheet.create({
|
|
empty: { textAlign: 'center', color: COLORS.muted, paddingVertical: 24, fontSize: 13 },
|
|
row: { flexDirection: 'row' },
|
|
gutter: { width: 24, alignItems: 'center' },
|
|
dot: { width: 12, height: 12, borderRadius: 6, marginTop: 4 },
|
|
line: { flex: 1, width: 2, backgroundColor: COLORS.border, marginTop: 2 },
|
|
content: { flex: 1, paddingBottom: 18, paddingLeft: 8 },
|
|
head: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' },
|
|
action: { fontSize: 13, fontWeight: '700', color: COLORS.text, textTransform: 'capitalize' },
|
|
time: { fontSize: 11, color: COLORS.muted },
|
|
actor: { fontSize: 12, color: COLORS.blue, marginTop: 2 },
|
|
detail: { fontSize: 12, color: COLORS.muted, marginTop: 4, lineHeight: 17 },
|
|
})
|