fromis_9/app/src/screens/ScheduleScreen.tsx

283 lines
8.2 KiB
TypeScript

import React, { useEffect, useState, useCallback } from 'react';
import {
View,
Text,
FlatList,
TextInput,
TouchableOpacity,
ActivityIndicator,
RefreshControl,
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { Ionicons } from '@expo/vector-icons';
import { getSchedules, getCategories, Schedule, ScheduleCategory } from '../api/schedules';
import { colors } from '../constants/colors';
export default function ScheduleScreen() {
const [schedules, setSchedules] = useState<Schedule[]>([]);
const [categories, setCategories] = useState<ScheduleCategory[]>([]);
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
const [loadingMore, setLoadingMore] = useState(false);
const [searchQuery, setSearchQuery] = useState('');
const [selectedCategory, setSelectedCategory] = useState<number | null>(null);
const [offset, setOffset] = useState(0);
const [hasMore, setHasMore] = useState(true);
const LIMIT = 20;
const fetchCategories = async () => {
try {
const data = await getCategories();
setCategories(data);
} catch (error) {
console.error('카테고리 로드 오류:', error);
}
};
const fetchSchedules = async (isRefresh = false) => {
if (loadingMore && !isRefresh) return;
try {
const currentOffset = isRefresh ? 0 : offset;
const { schedules: data, total } = await getSchedules({
q: searchQuery || undefined,
category: selectedCategory || undefined,
limit: LIMIT,
offset: currentOffset,
});
if (isRefresh) {
setSchedules(data);
setOffset(LIMIT);
} else {
setSchedules(prev => [...prev, ...data]);
setOffset(prev => prev + LIMIT);
}
setHasMore(currentOffset + data.length < total);
} catch (error) {
console.error('일정 로드 오류:', error);
} finally {
setLoading(false);
setRefreshing(false);
setLoadingMore(false);
}
};
useEffect(() => {
fetchCategories();
}, []);
useEffect(() => {
setLoading(true);
setOffset(0);
setHasMore(true);
fetchSchedules(true);
}, [searchQuery, selectedCategory]);
const onRefresh = () => {
setRefreshing(true);
fetchSchedules(true);
};
const onEndReached = () => {
if (!loadingMore && hasMore) {
setLoadingMore(true);
fetchSchedules();
}
};
// 날짜 포맷
const formatDate = (dateStr: string) => {
const date = new Date(dateStr);
const month = date.getMonth() + 1;
const day = date.getDate();
const weekdays = ['일', '월', '화', '수', '목', '금', '토'];
const weekday = weekdays[date.getDay()];
return `${month}/${day} (${weekday})`;
};
const renderScheduleItem = ({ item }: { item: Schedule }) => (
<View style={{
backgroundColor: colors.backgroundSecondary,
borderRadius: 12,
padding: 14,
marginBottom: 10,
borderLeftWidth: 4,
borderLeftColor: item.category_color || colors.primary,
}}>
<View style={{ flexDirection: 'row', justifyContent: 'space-between' }}>
<Text style={{
fontSize: 12,
color: item.category_color || colors.textSecondary,
fontWeight: '500',
}}>
{item.category_name}
</Text>
<Text style={{ fontSize: 12, color: colors.textTertiary }}>
{formatDate(item.date)}
</Text>
</View>
<Text
style={{
fontSize: 15,
fontWeight: '600',
color: colors.textPrimary,
marginTop: 6,
}}
numberOfLines={2}
>
{item.title}
</Text>
{item.time && (
<Text style={{
fontSize: 12,
color: colors.textSecondary,
marginTop: 4,
}}>
{item.time.slice(0, 5)}
</Text>
)}
{item.members && item.members.length > 0 && (
<View style={{ flexDirection: 'row', flexWrap: 'wrap', marginTop: 8, gap: 4 }}>
{item.members.map(m => (
<View
key={m.id}
style={{
backgroundColor: colors.primary + '20',
paddingHorizontal: 8,
paddingVertical: 2,
borderRadius: 10,
}}
>
<Text style={{ fontSize: 11, color: colors.primary }}>
{m.name}
</Text>
</View>
))}
</View>
)}
</View>
);
return (
<SafeAreaView style={{ flex: 1, backgroundColor: colors.background }}>
{/* 헤더 */}
<View style={{
paddingHorizontal: 16,
paddingVertical: 12,
borderBottomWidth: 1,
borderBottomColor: colors.borderLight,
}}>
<Text style={{
fontSize: 20,
fontWeight: 'bold',
color: colors.textPrimary,
textAlign: 'center',
}}>
</Text>
</View>
{/* 검색 */}
<View style={{ paddingHorizontal: 16, paddingVertical: 12 }}>
<View style={{
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.backgroundSecondary,
borderRadius: 10,
paddingHorizontal: 12,
}}>
<Ionicons name="search" size={20} color={colors.textTertiary} />
<TextInput
style={{
flex: 1,
paddingVertical: 10,
paddingHorizontal: 10,
fontSize: 15,
color: colors.textPrimary,
}}
placeholder="일정 검색..."
placeholderTextColor={colors.textTertiary}
value={searchQuery}
onChangeText={setSearchQuery}
/>
{searchQuery && (
<TouchableOpacity onPress={() => setSearchQuery('')}>
<Ionicons name="close-circle" size={20} color={colors.textTertiary} />
</TouchableOpacity>
)}
</View>
</View>
{/* 카테고리 필터 */}
<View style={{ paddingHorizontal: 16, marginBottom: 12 }}>
<FlatList
horizontal
showsHorizontalScrollIndicator={false}
data={[{ id: null, name: '전체', color: colors.textSecondary }, ...categories]}
keyExtractor={(item) => String(item.id)}
renderItem={({ item }) => (
<TouchableOpacity
onPress={() => setSelectedCategory(item.id)}
style={{
paddingHorizontal: 14,
paddingVertical: 6,
borderRadius: 16,
marginRight: 8,
backgroundColor: selectedCategory === item.id
? colors.primary
: colors.backgroundSecondary,
}}
>
<Text style={{
fontSize: 13,
color: selectedCategory === item.id ? 'white' : colors.textSecondary,
fontWeight: selectedCategory === item.id ? '600' : '400',
}}>
{item.name}
</Text>
</TouchableOpacity>
)}
/>
</View>
{/* 일정 목록 */}
{loading ? (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<ActivityIndicator size="large" color={colors.primary} />
</View>
) : (
<FlatList
data={schedules}
keyExtractor={(item) => String(item.id)}
renderItem={renderScheduleItem}
contentContainerStyle={{ paddingHorizontal: 16, paddingBottom: 20 }}
refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
}
onEndReached={onEndReached}
onEndReachedThreshold={0.5}
ListEmptyComponent={
<View style={{ padding: 40, alignItems: 'center' }}>
<Text style={{ color: colors.textTertiary }}>
.
</Text>
</View>
}
ListFooterComponent={
loadingMore ? (
<ActivityIndicator style={{ padding: 20 }} color={colors.primary} />
) : null
}
/>
)}
</SafeAreaView>
);
}