diff --git a/app.config.js b/app.config.js index d9eca1a..0596a1f 100644 --- a/app.config.js +++ b/app.config.js @@ -76,7 +76,7 @@ const expoConfig = { backgroundColor: '#ffffff', }, package: 'com.safezone.onestep', - versionCode: 7, + versionCode: 5, softwareKeyboardLayoutMode: 'pan', }, web: { @@ -125,7 +125,7 @@ const expoConfig = { ], './plugins/withStaticFrameworks', ], - runtimeVersion: '1.0.1', + runtimeVersion: '1.0.0', updates: { url: 'https://u.expo.dev/63f6bbd9-1594-44b3-b161-0e0051413ef0', }, diff --git a/app/(tabs)/_layout.jsx b/app/(tabs)/_layout.jsx index bee5029..cd7d929 100644 --- a/app/(tabs)/_layout.jsx +++ b/app/(tabs)/_layout.jsx @@ -1,4 +1,3 @@ -import TextInputProvider from '@/contexts/textInputContext'; import '@/locales/index'; import { Tabs } from 'expo-router'; import React from 'react'; @@ -19,38 +18,35 @@ const inboxIcon = ({ color, size }) => ( const TabLayout = () => { const { t } = useTranslation(); return ( - - + - - - - + /> + + ); }; diff --git a/app/(tabs)/index.jsx b/app/(tabs)/index.jsx index 5fd1f90..7facb97 100644 --- a/app/(tabs)/index.jsx +++ b/app/(tabs)/index.jsx @@ -28,6 +28,8 @@ import { scale, verticalScale } from 'react-native-size-matters'; const TodayView = () => { const { i18n } = useTranslation(); const { userId } = useContext(LoginContext); + const [isDragging, setIsDragging] = React.useState(false); + const getLoadingText = () => { if (i18n.language === 'ko') { return `투두`; @@ -40,11 +42,18 @@ const TodayView = () => { userId: userId, }); - const renderCategoriesTodo = ({ item }) => { + const renderCategoriesTodo = ({ item, index }) => { + const isLastItem = index === categoriesData.length - 1; + return ( - - - + + + setIsDragging(true)} + onDragEnd={() => setIsDragging(false)} + /> ); }; @@ -56,14 +65,14 @@ const TodayView = () => { - - + + { renderItem={renderCategoriesTodo} keyExtractor={item => item.id} contentContainerStyle={styles.flatList} + scrollEnabled={!isDragging} /> - - - - + + + + @@ -104,6 +114,9 @@ const styles = StyleSheet.create({ paddingHorizontal: scale(20), paddingTop: verticalScale(20), }, + lastCategoryContainer: { + paddingBottom: verticalScale(90), + }, }); export default TodayView; diff --git a/app/categoryView/categoryListView.jsx b/app/categoryView/categoryListView.jsx index 9890adb..d610f79 100644 --- a/app/categoryView/categoryListView.jsx +++ b/app/categoryView/categoryListView.jsx @@ -44,11 +44,12 @@ const styles = StyleSheet.create({ container: { flex: 1, justifyContent: 'center', - paddingHorizontal: scale(20), backgroundColor: 'white', }, list: { backgroundColor: 'white', + paddingHorizontal: scale(20), + flex: 1, }, }); diff --git a/app/settingsView/settingsAccountView.tsx b/app/settingsView/settingsAccountView.tsx index 18dc24d..a77cd8f 100644 --- a/app/settingsView/settingsAccountView.tsx +++ b/app/settingsView/settingsAccountView.tsx @@ -1,5 +1,6 @@ import '@/locales/index'; import { IndexPath, Layout, Menu, MenuItem } from '@ui-kitten/components'; +import React from 'react'; import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { SafeAreaView, StyleSheet } from 'react-native'; diff --git a/app/settingsView/settingsView.jsx b/app/settingsView/settingsView.jsx index 89c2c59..54354fd 100644 --- a/app/settingsView/settingsView.jsx +++ b/app/settingsView/settingsView.jsx @@ -8,7 +8,7 @@ import { useTheme, } from '@ui-kitten/components'; import { useRouter } from 'expo-router'; -import React, { useState } from 'react'; +import React, { useContext, useState } from 'react'; import { useTranslation } from 'react-i18next'; import ConfirmModal from '@/components/common/molecules/ConfirmModal'; import { api as Api } from '@/utils/api'; @@ -25,6 +25,7 @@ import { } from 'react-native'; import { heightPercentage, widthPercentage } from '@/utils/responsiveSize'; import fontStyles from '@/theme/fontStyles'; +import { LoginContext } from '@/contexts/LoginContext'; const privacyPolicyUrl = 'https://swm-onestep.github.io/posts/%EA%B0%9C%EC%9D%B8%EC%A0%95%EB%B3%B4%EC%B2%98%EB%A6%AC%EB%B0%A9%EC%B9%A8-copy/'; const termsOfServiceUrl = @@ -45,6 +46,7 @@ const renderCurrentStatus = currentStatus => () => ( ); const SettingsView = () => { + const { userName } = useContext(LoginContext); const { t } = useTranslation(); const theme = useTheme(); const { clear: clearStorage } = useStorage(); @@ -175,7 +177,7 @@ const SettingsView = () => { {t('views.settingsView.greeting')} - user + {userName} diff --git a/components/InboxSubTodo.jsx b/components/InboxSubTodo.jsx deleted file mode 100644 index 2a1033d..0000000 --- a/components/InboxSubTodo.jsx +++ /dev/null @@ -1,93 +0,0 @@ -import { TextInputContext } from '@/contexts/textInputContext'; -import { useSubTodoUpdateMutation } from '@/hooks/api/useSubTodoMutations'; -import { Icon, Input, ListItem } from '@ui-kitten/components'; -import React, { useContext, useState } from 'react'; -import { Text, TouchableOpacity } from 'react-native'; -import { useTheme } from 'react-native-elements'; -import TodoModal from './TodoModal'; - -const InboxSubTodo = ({ item }) => { - const [isEditing, setIsEditing] = useState(false); - const [content, setContent] = useState(item.content); - const theme = useTheme(); - const [modalVisible, setModalVisible] = useState(false); - const { mutate: updateInboxSubTodo } = useSubTodoUpdateMutation(); - const { setInboxTextInputOpen } = useContext(TextInputContext); - - const handleEdit = () => { - setIsEditing(true); - setInboxTextInputOpen(false); - setModalVisible(false); - }; - - const handleInboxSubTodoUpdate = () => { - const updatedData = { - todoId: item.id, - content: content, - }; - updateInboxSubTodo(updatedData); - }; - - const outlineIcon = props => { - return ( - setModalVisible(true)}> - - - ); - }; - - const settingIcon = props => { - return ( - setModalVisible(true)}> - - - ); - }; - - return ( - <> - setContent(value)} - onSubmitEditing={() => { - handleInboxSubTodoUpdate(); - setIsEditing(false); - setInboxTextInputOpen(true); - }} - autoFocus={true} - /> - ) : ( - {item.content} - ) - } - key={item.id} - accessoryLeft={props => outlineIcon(props)} - accessoryRight={props => settingIcon(props)} - onPress={() => setModalVisible(true)} - style={{ paddingLeft: 40 }} - /> - - - ); -}; - -export default InboxSubTodo; diff --git a/components/WeeklyCalendar.jsx b/components/WeeklyCalendar.jsx index da98a56..28671ea 100644 --- a/components/WeeklyCalendar.jsx +++ b/components/WeeklyCalendar.jsx @@ -13,79 +13,203 @@ import { Icon, Layout, Text, useTheme } from '@ui-kitten/components'; import { View } from 'react-native'; import moment from 'moment'; import 'moment/locale/ko'; -import React, { useContext, useEffect, useState } from 'react'; +import React, { + useContext, + useEffect, + useState, + useMemo, + useCallback, + memo, +} from 'react'; import { useTranslation } from 'react-i18next'; import { TouchableOpacity } from 'react-native'; import fontStyles from '../theme/fontStyles'; import { moderateScale, scale, verticalScale } from 'react-native-size-matters'; import useTodosQuery from '@/hooks/api/useTodoQuery'; +import useCategoriesQuery from '@/hooks/api/useCategoriesQuery'; -const WeeklyCalendar = () => { +// 날짜 변환 딕셔너리를 컴포넌트 외부로 이동 +const convertDictionary = { + 월: 'Mon', + 화: 'Tue', + 수: 'Wed', + 목: 'Thu', + 금: 'Fri', + 토: 'Sat', + 일: 'Sun', +}; + +// 날짜 아이템 컴포넌트 분리 및 메모이제이션 +const DayItem = memo( + ({ date, selectedDate, theme, todoCount, onDateSelect, userId, i18n }) => { + const convertWeekDates = useCallback( + convertedDate => { + if (i18n.language === 'ko') { + return convertedDate.format('ddd'); + } + return convertDictionary[convertedDate.format('ddd')]; + }, + [i18n.language], + ); + + const handlePress = useCallback(() => { + handleLogEvent(WEEKLYCALENDAR_DAYITEM_CLICK_EVENT, { + time: new Date().toISOString(), + userId, + day: date.format('YYYY-MM-DD'), + }); + onDateSelect(date); + }, [date, userId, onDateSelect]); + + return ( + + + {convertWeekDates(date)} + + + + + {todoCount} + + + + {date.format('D')} + + + + ); + }, +); + +const WeeklyCalendar = memo(() => { const { selectedDate, setSelectedDate } = useContext(DateContext); - const [currentDate, setcurrentDate] = useState(moment()); + const [currentDate, setCurrentDate] = useState(moment()); const theme = useTheme(); const { userId } = useContext(LoginContext); - const todos = useTodosQuery().data; - // const [todos, setTodos] = useState(fedchedTodos.data); - const getWeekDates = date => { + const { data: todos = [] } = useTodosQuery(); + const { data: categories = [] } = useCategoriesQuery(); + const { i18n } = useTranslation(); + + // 주간 날짜 계산 메모이제이션 + const getWeekDates = useCallback(date => { const start = date.clone().startOf('ISOWeek'); - const r = Array.from({ length: 7 }, (_, i) => + return Array.from({ length: 7 }, (_, i) => moment(convertGmtToKst(new Date(start.clone().add(i, 'days')))), ); - return r; - }; - const [weekDates, setwWeekDates] = useState(getWeekDates(currentDate)); - const { i18n } = useTranslation(); + }, []); - const navigateWeek = direction => { - setcurrentDate(prevDate => + const weekDates = useMemo( + () => getWeekDates(currentDate), + [currentDate, getWeekDates], + ); + + const navigateWeek = useCallback(direction => { + setCurrentDate(prevDate => direction === 'next' ? prevDate.clone().add(7, 'd') : prevDate.clone().subtract(7, 'd'), ); - }; - useEffect(() => { - setwWeekDates(getWeekDates(currentDate)); - }, [currentDate]); - useEffect(() => { - moment().isBetween(weekDates[0], weekDates[6]) - ? setSelectedDate(currentDate) - : setSelectedDate(weekDates[0]); - }, [weekDates, setSelectedDate, currentDate]); + }, []); - const handleDateSelect = date => { - setSelectedDate(date); - }; + const handleDateSelect = useCallback( + date => { + setSelectedDate(date); + }, + [setSelectedDate], + ); - const convertMonthAndYear = date => { + const convertMonthAndYear = useCallback(date => { return date.format('yyyy.MM'); - }; + }, []); - const convertDictionary = { - 월: 'Mon', - 화: 'Tue', - 수: 'Wed', - 목: 'Thu', - 금: 'Fri', - 토: 'Sat', - 일: 'Sun', - }; + const convertCalendarType = useCallback(() => { + return i18n.language === 'ko' ? '주' : 'W'; + }, [i18n.language]); - const convertWeekDates = date => { - if (i18n.language === 'ko') { - return date.format('ddd'); - } else if (i18n.language === 'en') { - return convertDictionary[date.format('ddd')]; - } - }; + // 주간 이동 버튼 핸들러 + const handleNavigateWeek = useCallback( + direction => { + handleLogEvent(WEEKLYCALENDAR_NAVIGATEWEEK_CLICK_EVENT, { + time: new Date().toISOString(), + userId, + week: selectedDate.format('YYYY-MM-DD'), + direction, + }); + navigateWeek(direction); + }, + [navigateWeek, userId, selectedDate], + ); - const convertCalendarType = () => { - if (i18n.language === 'ko') { - return '주'; - } else if (i18n.language === 'en') { - return 'W'; + useEffect(() => { + if (moment().isBetween(weekDates[0], weekDates[6])) { + setSelectedDate(currentDate); + } else { + setSelectedDate(weekDates[0]); } - }; + }, [weekDates, setSelectedDate, currentDate]); + + // 일주일치 할일 필터링을 위한 메모이제이션 추가 + const filteredWeekTodos = useMemo(() => { + const startDate = weekDates[0].format('YYYY-MM-DD'); + const endDate = weekDates[6].format('YYYY-MM-DD'); + + return todos.filter(todo => { + // 카테고리 체크 + const isCategoryValid = categories.some( + category => category.id === todo.categoryId, + ); + // 날짜가 해당 주에 포함되는지 체크 + const isDateInRange = moment(todo.date).isBetween( + startDate, + endDate, + 'day', + '[]', + ); + // 완료되지 않은 할일만 필터링 + return !todo.isCompleted && isCategoryValid && isDateInRange; + }); + }, [todos, categories, weekDates]); + + // DayItem 컴포넌트에 전달할 할일 개수 계산 함수 + const getTodoCountForDate = useCallback( + date => { + return filteredWeekTodos.filter(todo => + isTodoIncludedInTodayView(todo.date, date.format('YYYY-MM-DD')), + ).length; + }, + [filteredWeekTodos], + ); return ( @@ -98,35 +222,14 @@ const WeeklyCalendar = () => { - { - handleLogEvent(WEEKLYCALENDAR_NAVIGATEWEEK_CLICK_EVENT, { - time: new Date().toISOString(), - userId: userId, - week: selectedDate.format('YYYY-MM-DD'), - direction: 'prev', - }); - navigateWeek('prev'); - }} - > + handleNavigateWeek('prev')}> - { - navigateWeek('next'); - - handleLogEvent(WEEKLYCALENDAR_NAVIGATEWEEK_CLICK_EVENT, { - time: new Date().toISOString(), - userId: userId, - week: selectedDate.format('YYYY-MM-DD'), - direction: 'next', - }); - }} - > + handleNavigateWeek('next')}> { {weekDates.map((date, index) => ( - - - {convertWeekDates(date)} - - { - handleLogEvent(WEEKLYCALENDAR_DAYITEM_CLICK_EVENT, { - time: new Date().toISOString(), - userId: userId, - day: date.format('YYYY-MM-DD'), - }); - handleDateSelect(date); - }} - style={{ - ...styles.touchBox, - backgroundColor: date.isSame(selectedDate, 'day') - ? theme.Blue01 - : 'transparent', - }} - > - - - { - todos.filter( - todo => - isTodoIncludedInTodayView( - todo.date, - date.format('YYYY-MM-DD'), - ) && !todo.isCompleted, - ).length - } - - - - {date.format('D')} - - - + ))} ); -}; +}); +// 스타일은 동일하게 유지 const styles = StyleSheet.create({ background: { display: 'flex', diff --git a/components/categoryView/CategoryMainItem.jsx b/components/categoryView/CategoryMainItem.jsx index e26fbba..30fab8a 100644 --- a/components/categoryView/CategoryMainItem.jsx +++ b/components/categoryView/CategoryMainItem.jsx @@ -1,7 +1,6 @@ -import { TextInputContext } from '@/contexts/textInputContext'; +import useTextInputStore from '@/contexts/textInputStore'; import colors from '@/theme/theme.json'; import { Icon } from '@ui-kitten/components'; -import { useContext } from 'react'; import { Pressable, StyleSheet, View } from 'react-native'; import { scale, verticalScale } from 'react-native-size-matters'; import CategoryButton from './CategoryButton'; @@ -12,7 +11,13 @@ const CategoryMainItem = ({ item, isToday = true }) => { setTodayActivatedCategoryId, setInboxTextInputOpen, setInboxActivatedCategoryId, - } = useContext(TextInputContext); + } = useTextInputStore(state => ({ + setTodayTextInputOpen: state.setTodayTextInputOpen, + setTodayActivatedCategoryId: state.setTodayActivatedCategoryId, + setInboxTextInputOpen: state.setInboxTextInputOpen, + setInboxActivatedCategoryId: state.setInboxActivatedCategoryId, + })); + const handlePress = () => { if (isToday) { setTodayTextInputOpen(true); diff --git a/components/common/molecules/EditableTextField.tsx b/components/common/molecules/EditableTextField.tsx index 3d6aa46..982b20f 100644 --- a/components/common/molecules/EditableTextField.tsx +++ b/components/common/molecules/EditableTextField.tsx @@ -1,38 +1,104 @@ import { Text } from '@ui-kitten/components'; -import React, { useState } from 'react'; +import React, { + useState, + useCallback, + useContext, + useEffect, + useRef, +} from 'react'; import { StyleProp, StyleSheet, TextInput, TextStyle } from 'react-native'; import { Todo } from '../../../types/todo'; import { moderateScale, scale } from 'react-native-size-matters'; +import { memo } from 'react'; +import { useTodoUpdateMutation } from '@/hooks/api/useTodoMutations'; +import { + NativeSyntheticEvent, + TextInputSubmitEditingEventData, +} from 'react-native'; +import { AIBottomSheetContext } from '@/contexts/AIBottomSheetProvider'; +import { useSubTodoUpdateMutation } from '@/hooks/api/useSubTodoMutations'; interface EditableListItemTitleProps { isEditing: boolean; - handleSubmitEditing: (content) => void; + setIsEditing: (isEditing: boolean) => void; item: Todo; inputStyles?: StyleProp | null; textStyles?: StyleProp | null; + isTodo?: boolean; + handleSubmitEditing?: ( + e: NativeSyntheticEvent, + ) => void; } -const EditableTextField = ({ - isEditing, - handleSubmitEditing, - item, - inputStyles = styles.input, - textStyles = styles.text, -}: EditableListItemTitleProps) => { - const [content, setContent] = useState(item.content); - return isEditing ? ( - handleSubmitEditing(content)} - autoFocus={true} - style={inputStyles} - /> - ) : ( - {item.content} - ); +const handleTodoContentUpdate = (content, item, updateTodo) => { + const updatedData = { + todoId: item.id, + content: content, + }; + updateTodo(updatedData); }; +const EditableTextField = memo( + ({ + isEditing, + setIsEditing, + item, + inputStyles = styles.input, + textStyles = styles.text, + isTodo = true, + }: EditableListItemTitleProps) => { + const [content, setContent] = useState(item.content); + const { mutate: updateTodo } = useTodoUpdateMutation(); + const { mutate: updateSubTodo } = useSubTodoUpdateMutation(); + const { bottomSheetRef } = useContext(AIBottomSheetContext); + const inputRef = useRef(null); + + useEffect(() => { + if (isEditing) { + bottomSheetRef.current?.close(); + const timer = setTimeout(() => { + inputRef.current?.focus(); + }, 100); + return () => clearTimeout(timer); + } + }, [isEditing, bottomSheetRef]); + + const handleTodoListItemSubmitEditing = useCallback( + async (e: NativeSyntheticEvent) => { + if (isTodo) { + handleTodoContentUpdate(e.nativeEvent.text, item, updateTodo); + } else { + const updatedData = { + subtodoId: item.id, + content: e.nativeEvent.text, + }; + updateSubTodo(updatedData); + } + await new Promise(resolve => setTimeout(resolve, 100)); // 0.1초 대기 + setIsEditing(false); + }, + [item, setIsEditing, updateTodo, updateSubTodo, isTodo], + ); + + const onChangeText = useCallback((text: string) => { + setContent(text); + }, []); + return isEditing ? ( + await handleTodoListItemSubmitEditing(e)} + style={inputStyles} + autoFocus={true} + onFocus={() => bottomSheetRef.current?.close()} + /> + ) : ( + {content} + ); + }, +); + export default EditableTextField; const styles = StyleSheet.create({ diff --git a/components/common/molecules/InboxEditableTextField.tsx b/components/common/molecules/InboxEditableTextField.tsx new file mode 100644 index 0000000..3d6aa46 --- /dev/null +++ b/components/common/molecules/InboxEditableTextField.tsx @@ -0,0 +1,49 @@ +import { Text } from '@ui-kitten/components'; +import React, { useState } from 'react'; +import { StyleProp, StyleSheet, TextInput, TextStyle } from 'react-native'; +import { Todo } from '../../../types/todo'; +import { moderateScale, scale } from 'react-native-size-matters'; + +interface EditableListItemTitleProps { + isEditing: boolean; + handleSubmitEditing: (content) => void; + item: Todo; + inputStyles?: StyleProp | null; + textStyles?: StyleProp | null; +} + +const EditableTextField = ({ + isEditing, + handleSubmitEditing, + item, + inputStyles = styles.input, + textStyles = styles.text, +}: EditableListItemTitleProps) => { + const [content, setContent] = useState(item.content); + return isEditing ? ( + handleSubmitEditing(content)} + autoFocus={true} + style={inputStyles} + /> + ) : ( + {item.content} + ); +}; + +export default EditableTextField; + +const styles = StyleSheet.create({ + text: { + paddingLeft: scale(8), + fontSize: moderateScale(14), + }, + input: { + justifyContent: 'center', + alignItems: 'center', + paddingLeft: scale(8), + fontSize: moderateScale(14), + }, +}); diff --git a/components/inboxView/inboxTodos/InboxTodos.tsx b/components/inboxView/inboxTodos/InboxTodos.tsx index 088076e..8899780 100644 --- a/components/inboxView/inboxTodos/InboxTodos.tsx +++ b/components/inboxView/inboxTodos/InboxTodos.tsx @@ -1,6 +1,5 @@ import { LoginContext } from '@/contexts/LoginContext'; -import { TextInputContext } from '@/contexts/textInputContext'; -import { useTodoUpdateMutation } from '@/hooks/api/useTodoMutations'; +import useTextInputStore from '@/contexts/textInputStore'; import useCreateInboxTodo from '@/hooks/todo/useCreateInboxTodo'; import useFilteredInboxTodos from '@/hooks/todo/useFilteredInboxTodo'; import '@/locales/index'; @@ -14,58 +13,37 @@ import { INBOXVIEW_SCROLL_EVENT, TODAYVIEW_TEXTINPUT_SUBMIT_EVENT, } from '@/utils/logEvent'; -import { Icon } from '@ui-kitten/components'; -import { LexoRank } from 'lexorank'; + import React, { Fragment, useContext } from 'react'; -import { useTranslation } from 'react-i18next'; -import { - KeyboardAvoidingView, - StyleSheet, - TextInput, - View, -} from 'react-native'; -import DraggableFlatList, { - ScaleDecorator, -} from 'react-native-draggable-flatlist'; -import { GestureHandlerRootView } from 'react-native-gesture-handler'; -import { KeyboardAccessoryView } from 'react-native-keyboard-accessory'; +import { KeyboardAvoidingView, StyleSheet } from 'react-native'; + +import { FlatList, GestureHandlerRootView } from 'react-native-gesture-handler'; import { moderateScale, scale, verticalScale } from 'react-native-size-matters'; import InboxTodo from './inboxTodo/InboxTodo'; +import TodoInput from '@/components/todayView/TextInput'; const InboxTodos = ({ todosData, categoryId }) => { - const { t } = useTranslation(); const { userId } = useContext(LoginContext); const inboxCurrentTodos = useFilteredInboxTodos(todosData, categoryId); - const { mutate: updateInboxTodo } = useTodoUpdateMutation(); const { isInboxTextInputOpen, inboxActivatedCategoryId, setInboxTextInputOpen, - } = useContext(TextInputContext); + } = useTextInputStore(state => ({ + isInboxTextInputOpen: state.isInboxTextInputOpen, + inboxActivatedCategoryId: state.inboxActivatedCategoryId, + setInboxTextInputOpen: state.setInboxTextInputOpen, + })); const { input, setInput, handleSubmit } = useCreateInboxTodo( userId, categoryId, ); - const renderTodo = ({ item, drag, isActive }) => { - return ( - - - - ); + const renderTodo = ({ item }) => { + return ; }; - const handleDragEnd = async ({ data }) => { - const updatedData = data.map(item => { - return { - todoId: item.id, - order: LexoRank.parse(item.order).toString(), - }; - }); - updateInboxTodo(updatedData); - }; - //TODO: order 순서 생각해보니까 이제 서버에서 하잖아? 일단 전체 투두에서 계속 마지막으로 붙이는 식으로 구현 const handleInputSubmit = async () => { handleLogEvent(TODAYVIEW_TEXTINPUT_SUBMIT_EVENT, { time: new Date().toISOString(), @@ -77,42 +55,27 @@ const InboxTodos = ({ todosData, categoryId }) => { }; return ( - + - renderTodo({ item })} keyExtractor={item => item.id.toString()} onScroll={() => handleScroll(INBOXVIEW_SCROLL_EVENT, userId)} scrollEventThrottle={DEFAULT_SCROLL_EVENT_THROTTLE} /> {isInboxTextInputOpen && categoryId === inboxActivatedCategoryId ? ( - - - - - - + ) : null} @@ -123,21 +86,19 @@ const styles = StyleSheet.create({ container: { flex: 1, }, - keyboardInputContainer: { - backgroundColor: 'white', - borderTopWidth: 0, - }, - mainContainer: { + keyboardAvoidingView: { flex: 1, backgroundColor: 'white', }, flatListContainer: { flex: 1, }, - keyboardAvoidingView: { - width: '100%', - position: 'absolute', - bottom: 0, + keyboardInputContainer: { + backgroundColor: 'white', + borderTopWidth: 0, + }, + mainContainer: { + flex: 1, backgroundColor: 'white', }, inputWrapper: { diff --git a/components/inboxView/inboxTodos/inboxTodo/InboxTodo.jsx b/components/inboxView/inboxTodos/inboxTodo/InboxTodo.jsx index 4082d04..031fa4d 100644 --- a/components/inboxView/inboxTodos/inboxTodo/InboxTodo.jsx +++ b/components/inboxView/inboxTodos/inboxTodo/InboxTodo.jsx @@ -1,16 +1,14 @@ -import SubTodoGenerateModal from '@/components/SubTodoGenerateModal'; -import GeneratedSubTodoList from '@/components/todayView/dailyTodos/dailyTodo/generatedSubTodoList/GeneratedSubTodoList'; -import { TextInputContext } from '@/contexts/textInputContext'; import '@/locales/index'; -import React, { useContext } from 'react'; +import React from 'react'; import { View } from 'react-native'; import useModal from '../../../../hooks/common/useModal'; import SubTodoInfo from './subTodoInfo/SubTodoInfo'; import SubTodoList from './subTodoList/SubTodoList'; import TodoListItem from './todoListItem/TodoListItem'; import useInboxTodo from './useInboxTodo'; +import useTextInputStore from '@/contexts/textInputStore'; -const InboxTodo = ({ item, drag, isActive }) => { +const InboxTodo = ({ item }) => { const { isEditing, setIsEditing, @@ -18,16 +16,11 @@ const InboxTodo = ({ item, drag, isActive }) => { setIsSubTodoToggleActivated, subTodoInputActivated, setSubTodoInputActivated, - generatedSubTodos, - setGeneratedSubTodos, } = useInboxTodo(); - const { setInboxTextInputOpen } = useContext(TextInputContext); - - const { - isVisible: isSubTodoGenerateModalVisible, - setIsVisible: setIsSubTodoGenerateModalVisible, - } = useModal(); + const setInboxTextInputOpen = useTextInputStore( + state => state.setInboxTextInputOpen, + ); const { setIsVisible: setIsTodoModalVisible } = useModal(); @@ -41,11 +34,8 @@ const InboxTodo = ({ item, drag, isActive }) => { 0} onEdit={handleEdit} @@ -63,16 +53,6 @@ const InboxTodo = ({ item, drag, isActive }) => { setSubTodoInputActivated={setSubTodoInputActivated} /> ) : null} - - ); }; diff --git a/components/inboxView/inboxTodos/inboxTodo/subTodoList/InboxSubTodo.jsx b/components/inboxView/inboxTodos/inboxTodo/subTodoList/InboxSubTodo.jsx index 226b07c..236cfa3 100644 --- a/components/inboxView/inboxTodos/inboxTodo/subTodoList/InboxSubTodo.jsx +++ b/components/inboxView/inboxTodos/inboxTodo/subTodoList/InboxSubTodo.jsx @@ -1,5 +1,5 @@ import { LoginContext } from '@/contexts/LoginContext'; -import { TextInputContext } from '@/contexts/textInputContext'; +import useTextInputStore from '@/contexts/textInputStore'; import { useSubTodoUpdateMutation } from '@/hooks/api/useSubTodoMutations'; import { handleLogEvent, @@ -17,7 +17,9 @@ const InboxSubTodo = ({ item }) => { const theme = useTheme(); const { mutate: updateSubTodo } = useSubTodoUpdateMutation(); const { userId } = useContext(LoginContext); - const { setInboxTextInputOpen } = useContext(TextInputContext); + const { setInboxTextInputOpen } = useTextInputStore( + state => state.setInboxTextInputOpen, + ); const handleEdit = () => { setIsEditing(true); diff --git a/components/inboxView/inboxTodos/inboxTodo/todoListItem/TodoListItem.tsx b/components/inboxView/inboxTodos/inboxTodo/todoListItem/TodoListItem.tsx index d325777..197439c 100644 --- a/components/inboxView/inboxTodos/inboxTodo/todoListItem/TodoListItem.tsx +++ b/components/inboxView/inboxTodos/inboxTodo/todoListItem/TodoListItem.tsx @@ -1,5 +1,4 @@ -import EditableTextField from '@/components/common/molecules/EditableTextField'; -import { heightPercentage, widthPercentage } from '@/utils/responsiveSize'; +import EditableTextField from '@/components/common/molecules/InboxEditableTextField'; import { Icon, ListItem } from '@ui-kitten/components'; import React from 'react'; import { Pressable, StyleSheet, Text } from 'react-native'; @@ -7,14 +6,11 @@ import { RenderItemParams } from 'react-native-draggable-flatlist'; import { Todo } from '../../../../../types/todo'; import TodoMoreMenu from './todoMoreMenu/TodoMoreMenu'; import useTodoListItem from './useTodoListItem'; +import { scale, verticalScale } from 'react-native-size-matters'; interface TodoListItemProps extends RenderItemParams { isEditing: boolean; setIsEditing: React.Dispatch>; - setIsSubTodoGenerateModalVisible: React.Dispatch< - React.SetStateAction - >; - setIsTodoModalVisible: React.Dispatch>; onEdit: () => void; // eslint-disable-next-line @typescript-eslint/no-explicit-any bottomSheetRef: React.MutableRefObject; @@ -30,7 +26,6 @@ const TodoListItem: React.FC = ({ isActive, isEditing, setIsEditing, - setIsSubTodoGenerateModalVisible, onEdit, setSubTodoInputActivated, }) => { @@ -44,7 +39,6 @@ const TodoListItem: React.FC = ({ item, isEditing, setIsEditing, - setIsSubTodoGenerateModalVisible, }); const accessoryLeft = (props?) => { @@ -63,7 +57,6 @@ const TodoListItem: React.FC = ({ const accessoryRight = () => { return ( = ({ onLongPress={drag} disabled={isActive} title={title} + style={{ paddingHorizontal: 0 }} /> ); @@ -107,8 +101,7 @@ const styles = StyleSheet.create({ container: { flexDirection: 'row', justifyContent: 'space-between', - paddingBottom: heightPercentage(8), - paddingTop: heightPercentage(8), + paddingVertical: verticalScale(10), marginRight: 0, paddingRight: 0, }, @@ -119,13 +112,13 @@ const styles = StyleSheet.create({ flex: 1, }, checkIcon: { - width: widthPercentage(20), - height: heightPercentage(20), + width: scale(20), + height: verticalScale(20), }, settingIcon: { - width: widthPercentage(20), - height: heightPercentage(20), - marginRight: widthPercentage(4), + width: scale(20), + height: verticalScale(20), + marginRight: scale(4), }, touchableCheck: { paddingTop: 0, @@ -133,7 +126,7 @@ const styles = StyleSheet.create({ }, text: { paddingTop: 0, - paddingLeft: widthPercentage(4), + paddingLeft: scale(4), }, input: { paddingTop: 0, diff --git a/components/inboxView/inboxTodos/inboxTodo/todoListItem/todoMoreMenu/TodoMoreMenu.tsx b/components/inboxView/inboxTodos/inboxTodo/todoListItem/todoMoreMenu/TodoMoreMenu.tsx index 4246f11..7321beb 100644 --- a/components/inboxView/inboxTodos/inboxTodo/todoListItem/todoMoreMenu/TodoMoreMenu.tsx +++ b/components/inboxView/inboxTodos/inboxTodo/todoListItem/todoMoreMenu/TodoMoreMenu.tsx @@ -46,25 +46,19 @@ const MenuIconButton = onPress => ( ); -const TodoMoreMenu = ({ - setIsSubTodoGenerateModalVisible, - onEdit, - item, - setSubTodoInputActivated, -}) => { +const TodoMoreMenu = ({ onEdit, item, setSubTodoInputActivated }) => { const { handleEditPress, handleDeletePress, - handleGenerateSubTodoPress, handleCreateSubTodoPress: handleAddSubTodoPress, // eslint-disable-next-line @typescript-eslint/no-unused-vars handlePutTodoToInboxPress, } = useTodoMoreMenu({ - setIsSubTodoGenerateModalVisible, onEdit, item, setSubTodoInputActivated, }); + const [visible, setVisible] = useState(false); const { t } = useTranslation(); const { openBottomSheet } = useContext(CalendarBottomSheetContext); @@ -119,12 +113,12 @@ const TodoMoreMenu = ({ disabled={item.children.length > 0} accessoryLeft={GenerateSubtodoIcon} title= 0} + disabled={true} titleText={t('components.todoMoreMenu.createSubTodoWithAi')} /> onPress={() => { setSelectedTodo(item); - handleGenerateSubTodoPress(); + setVisible(false); }} style={styles.middleMenuItem} /> @@ -136,6 +130,7 @@ const TodoMoreMenu = ({ /> onPress={() => { handleAddSubTodoPress(); + setVisible(false); }} style={styles.middleMenuItem} /> diff --git a/components/inboxView/inboxTodos/inboxTodo/todoListItem/todoMoreMenu/useTodoMoreMenu.tsx b/components/inboxView/inboxTodos/inboxTodo/todoListItem/todoMoreMenu/useTodoMoreMenu.tsx index dea7a21..614952b 100644 --- a/components/inboxView/inboxTodos/inboxTodo/todoListItem/todoMoreMenu/useTodoMoreMenu.tsx +++ b/components/inboxView/inboxTodos/inboxTodo/todoListItem/todoMoreMenu/useTodoMoreMenu.tsx @@ -18,7 +18,6 @@ import { useContext } from 'react'; const useTodoMoreMenu = ({ item, onEdit = () => {}, - setIsSubTodoGenerateModalVisible, setSubTodoInputActivated, }) => { const { selectedCategory } = useContext(CategoryContext); @@ -38,8 +37,8 @@ const useTodoMoreMenu = ({ const updatedTodo = { date: kstDate, - todo_id: item.id, - category_id: selectedCategory, + todoId: item.id, + categoryId: selectedCategory, }; updateTodoDate(updatedTodo); }; @@ -70,10 +69,6 @@ const useTodoMoreMenu = ({ }); }; - const handleGenerateSubTodoPress = () => { - setIsSubTodoGenerateModalVisible(true); - }; - const handleCreateSubTodoPress = () => { handleLogEvent(TODOMODAL_CREATESUBTODO_CLICK_EVENT, { time: new Date().toISOString(), @@ -96,7 +91,6 @@ const useTodoMoreMenu = ({ handleEditPress, handleDeletePress, handleChaneDatePress, - handleGenerateSubTodoPress, handleCreateSubTodoPress, handlePutTodoToInboxPress, }; diff --git a/components/inboxView/inboxTodos/inboxTodo/todoListItem/useTodoListItem.ts b/components/inboxView/inboxTodos/inboxTodo/todoListItem/useTodoListItem.ts index b454cc4..d831e8f 100644 --- a/components/inboxView/inboxTodos/inboxTodo/todoListItem/useTodoListItem.ts +++ b/components/inboxView/inboxTodos/inboxTodo/todoListItem/useTodoListItem.ts @@ -1,4 +1,4 @@ -import { TextInputContext } from '@/contexts/textInputContext'; +import useTextInputStore from '@/contexts/textInputStore'; import { useTheme } from '@ui-kitten/components'; import { useContext } from 'react'; import { LoginContext } from '../../../../../contexts/LoginContext'; @@ -10,16 +10,13 @@ import { handleLogEvent, } from '../../../../../utils/logEvent'; -const useTodoListItem = ({ - item, - isEditing, - setIsEditing, - setIsSubTodoGenerateModalVisible, -}) => { +const useTodoListItem = ({ item, isEditing, setIsEditing }) => { const { mutate: updateTodo } = useTodoUpdateMutation(); const { userId } = useContext(LoginContext); const theme = useTheme(); - const { setInboxTextInputOpen } = useContext(TextInputContext); + const setInboxTextInputOpen = useTextInputStore( + state => state.setInboxTextInputOpen, + ); const checkIconlogPressEvent = () => { handleLogEvent(DAILYTODO_TODOCOMPLETE_CLICK_EVENT, { @@ -62,10 +59,6 @@ const useTodoListItem = ({ setInboxTextInputOpen(false); }; - const handleGenerateIconPress = () => { - setIsSubTodoGenerateModalVisible(true); - }; - const handleSettingIconPress = () => { handleLogEvent(DAILYTODO_MEATBALLMENU_CLICK_EVENT, { time: new Date().toISOString(), @@ -81,7 +74,6 @@ const useTodoListItem = ({ setIsEditing, handleTodoListItemPress, handleTodoListItemSubmitEditing, - handleGenerateIconPress, handleSettingIconPress, }; }; diff --git a/components/todayView/TextInput.tsx b/components/todayView/TextInput.tsx new file mode 100644 index 0000000..3473971 --- /dev/null +++ b/components/todayView/TextInput.tsx @@ -0,0 +1,64 @@ +import React, { memo } from 'react'; +import { TextInput as RNTextInput, View, StyleSheet } from 'react-native'; +import { Icon } from '@ui-kitten/components'; +import { scale, verticalScale, moderateScale } from 'react-native-size-matters'; +import colors from '@/theme/theme.json'; +import { useTranslation } from 'react-i18next'; + +interface TodoInputProps { + input: string; + setInput: (text: string) => void; + onSubmit: () => void; +} + +const TodoInput = memo(({ input, setInput, onSubmit }: TodoInputProps) => { + const { t } = useTranslation(); + + return ( + + + + + + + ); +}); + +export default TodoInput; + +const styles = StyleSheet.create({ + inputWrapper: { + width: '100%', + borderTopWidth: StyleSheet.hairlineWidth, + borderTopColor: colors.Gray02, + backgroundColor: 'white', + }, + inputContainer: { + flexDirection: 'row', + alignItems: 'center', + paddingHorizontal: scale(7), + paddingVertical: verticalScale(8), + }, + checkIcon: { + width: scale(24), + height: verticalScale(24), + }, + textInput: { + flex: 1, + backgroundColor: colors.Gray02, + borderRadius: moderateScale(4), + paddingVertical: verticalScale(8), + paddingHorizontal: scale(10), + }, +}); diff --git a/components/todayView/TodoList.tsx b/components/todayView/TodoList.tsx new file mode 100644 index 0000000..89895e6 --- /dev/null +++ b/components/todayView/TodoList.tsx @@ -0,0 +1,79 @@ +import { + DEFAULT_SCROLL_EVENT_THROTTLE, + handleScroll, +} from '@/utils/handleScroll'; +import { TODAYVIEW_SCROLL_EVENT } from '@/utils/logEvent'; +import React, { memo, forwardRef } from 'react'; +import { FlatList } from 'react-native-gesture-handler'; +import DraggableFlatList from 'react-native-draggable-flatlist'; +import { Todo } from '@/types/todo'; +import { StyleSheet } from 'react-native'; + +const TodoList = memo( + forwardRef< + FlatList, + { + todos: Todo[]; + renderItem: (info: { + item: Todo; + drag: () => void; + isActive: boolean; + }) => React.ReactNode; + onDragEnd: (params: { from: number; to: number; data: Todo[] }) => void; + onDragStart: () => void; + userId: string; + setTodos: (todos: Todo[]) => void; + } + >( + ( + { + todos, + renderItem, + onDragEnd, + onDragStart, + userId, + setTodos, + }: { + todos: Todo[]; + renderItem: (info: { + item: Todo; + drag: () => void; + isActive: boolean; + }) => React.ReactNode; + onDragEnd: (params: { from: number; to: number; data: Todo[] }) => void; + onDragStart: () => void; + userId: string; + setTodos: (todos: Todo[]) => void; + }, + ref, + ) => { + return ( + { + onDragEnd({ from, to, data }); + setTodos(data); + }} + onDragBegin={onDragStart} + keyExtractor={item => item.id.toString()} + onScroll={() => handleScroll(TODAYVIEW_SCROLL_EVENT, userId)} + scrollEventThrottle={DEFAULT_SCROLL_EVENT_THROTTLE} + simultaneousHandlers={[]} + activationDistance={20} + containerStyle={styles.flatListContainer} + extraData={todos} + /> + ); + }, + ), +); + +export default TodoList; + +const styles = StyleSheet.create({ + flatListContainer: { + flex: 1, + }, +}); diff --git a/components/todayView/dailyTodos/DailyTodos.tsx b/components/todayView/dailyTodos/DailyTodos.tsx index 61a8e45..8ee1069 100644 --- a/components/todayView/dailyTodos/DailyTodos.tsx +++ b/components/todayView/dailyTodos/DailyTodos.tsx @@ -1,35 +1,28 @@ import { DateContext } from '@/contexts/DateContext'; import { LoginContext } from '@/contexts/LoginContext'; -import { TextInputContext } from '@/contexts/textInputContext'; +import useTextInputStore from '@/contexts/textInputStore'; import useCreateTodo from '@/hooks/todo/useCreateTodo'; import useFilteredTodos from '@/hooks/todo/useFilteredTodo'; import useHandleDrag from '@/hooks/todo/useHandleDrag'; import '@/locales/index'; -import colors from '@/theme/theme.json'; -import { - DEFAULT_SCROLL_EVENT_THROTTLE, - handleScroll, -} from '@/utils/handleScroll'; import { handleLogEvent, - TODAYVIEW_SCROLL_EVENT, TODAYVIEW_TEXTINPUT_SUBMIT_EVENT, } from '@/utils/logEvent'; -import { Icon } from '@ui-kitten/components'; -import React, { useContext } from 'react'; -import { useTranslation } from 'react-i18next'; -import { StyleSheet, TextInput, View } from 'react-native'; -import DraggableFlatList from 'react-native-draggable-flatlist'; +import React, { useCallback, useContext, useEffect, useState } from 'react'; +import { StyleSheet, View } from 'react-native'; +import { ScaleDecorator } from 'react-native-draggable-flatlist'; import { GestureHandlerRootView } from 'react-native-gesture-handler'; -import { moderateScale, scale, verticalScale } from 'react-native-size-matters'; import DailyTodo from './dailyTodo/DailyTodo'; +import TodoList from '../TodoList'; +import TodoInput from '../TextInput'; +import useListRefStore from '@/contexts/listRefStore'; -const DailyTodos = ({ todosData, categoryId }) => { +const DailyTodos = ({ todosData, categoryId, onDragStart, onDragEnd }) => { const { userId } = useContext(LoginContext); const { selectedDate } = useContext(DateContext); - const { t } = useTranslation(); - const currentTodos = useFilteredTodos(todosData, categoryId, selectedDate); + const [todos, setTodos] = useState(currentTodos); const { input, setInput, handleSubmit } = useCreateTodo( userId, categoryId, @@ -40,63 +33,72 @@ const DailyTodos = ({ todosData, categoryId }) => { isTodayTextInputOpen, todayActivatedCategoryId, setTodayTextInputOpen, - } = useContext(TextInputContext); + } = useTextInputStore(); const handleDragEnd = useHandleDrag(); - const handleInputSubmit = () => { + const { setTodoListRef } = useListRefStore(); + const todoListRef = React.useRef(null); + + useEffect(() => { + setTodoListRef(todoListRef); + }, [setTodoListRef]); + + const handleInputSubmit = useCallback(() => { handleLogEvent(TODAYVIEW_TEXTINPUT_SUBMIT_EVENT, { time: new Date().toISOString(), - userId: userId, + userId, }); handleSubmit(); setTodayTextInputOpen(false); - }; + todoListRef.current?.scrollToEnd(); + }, [userId, handleSubmit, setTodayTextInputOpen]); + + const handleDragEndWithCallback = useCallback( + ({ from, to, data }) => { + handleDragEnd({ from, to, data }); + onDragEnd?.(); + }, + [handleDragEnd, onDragEnd], + ); - const renderTodo = ({ item, drag, isActive }) => { - return ( - - ); - }; + const renderTodo = useCallback( + ({ item, drag, isActive }) => ( + + + + ), + [categoryId], + ); + + useEffect(() => { + setTodos(currentTodos); + }, [currentTodos]); return ( - item.id.toString()} - onScroll={() => handleScroll(TODAYVIEW_SCROLL_EVENT, userId)} - scrollEventThrottle={DEFAULT_SCROLL_EVENT_THROTTLE} - simultaneousHandlers={[]} - activationDistance={20} - containerStyle={styles.flatListContainer} + onDragEnd={handleDragEndWithCallback} + onDragStart={onDragStart} + userId={userId} /> {isTodayTextInputOpen && categoryId === todayActivatedCategoryId && ( - - - - - - + )} @@ -111,40 +113,6 @@ const styles = StyleSheet.create({ flex: 1, backgroundColor: 'white', }, - flatListContainer: { - flex: 1, - }, - keyboardAvoidingView: { - width: '100%', - position: 'absolute', - bottom: 0, - backgroundColor: 'white', - }, - inputWrapper: { - width: '100%', - borderTopWidth: StyleSheet.hairlineWidth, - borderTopColor: colors.Gray02, - backgroundColor: 'white', - }, - inputContainer: { - flexDirection: 'row', - alignItems: 'center', - paddingHorizontal: scale(7), - paddingVertical: verticalScale(8), - }, - checkIcon: { - width: scale(24), - height: verticalScale(24), - }, - textInput: { - flex: 1, - backgroundColor: colors.Gray02, - borderRadius: moderateScale(4), - marginLeft: scale(4), - paddingHorizontal: scale(8), - height: scale(24), - fontSize: moderateScale(14), - }, }); export default DailyTodos; diff --git a/components/todayView/dailyTodos/calendarBottomSheet/CalendarBottomSheet.tsx b/components/todayView/dailyTodos/calendarBottomSheet/CalendarBottomSheet.tsx index 7c1638a..8f8facd 100644 --- a/components/todayView/dailyTodos/calendarBottomSheet/CalendarBottomSheet.tsx +++ b/components/todayView/dailyTodos/calendarBottomSheet/CalendarBottomSheet.tsx @@ -1,9 +1,6 @@ import { View, Text, Pressable, StyleSheet } from 'react-native'; -import React, { useCallback, useContext, useMemo } from 'react'; -import BottomSheet, { - BottomSheetBackdrop, - BottomSheetView, -} from '@gorhom/bottom-sheet'; +import React, { useContext, useMemo } from 'react'; +import BottomSheet, { BottomSheetView } from '@gorhom/bottom-sheet'; import { DateContext } from '@/contexts/DateContext'; import { CalendarBottomSheetContext } from '@/contexts/CalendarBottomSheetProvider'; import { @@ -29,7 +26,7 @@ const CalendarBottomSheet = ({ isTodo, item }) => { ); const { mutate: updateTodoDate } = useTodoUpdateMutation(); const { mutate: updateSubTodoDate } = useSubTodoUpdateMutation(); - const snapPoints = useMemo(() => ['75%'], []); + const snapPoints = useMemo(() => ['90%'], []); const { i18n } = useTranslation(); const handleDateUpdate = date => { @@ -99,25 +96,12 @@ const CalendarBottomSheet = ({ isTodo, item }) => { startDayOfWeek: 1, }); - const renderBackdrop = useCallback( - props => ( - - ), - [], - ); - return ( { const { - isEditing, - setIsEditing, isSubtodoToggleActivated, setIsSubTodoToggleActivated, subTodoInputActivated, setSubTodoInputActivated, - generatedSubTodos, - setGeneratedSubTodos, } = useDailyTodo(); - const { setTodayTextInputOpen } = useContext(TextInputContext); - const { - isVisible: isSubTodoGenerateModalVisible, - setIsVisible: setIsSubTodoGenerateModalVisible, - } = useModal(); - - const { setIsVisible: setIsTodoModalVisible } = useModal(); - - const handleEdit = () => { - setIsEditing(true); - setTodayTextInputOpen(false); - setIsTodoModalVisible(false); - }; - return ( 0} - onEdit={handleEdit} + item={item} setSubTodoInputActivated={setSubTodoInputActivated} categoryId={categoryId} /> @@ -63,16 +34,6 @@ const DailyTodo = ({ item, drag, isActive, categoryId }) => { setSubTodoInputActivated={setSubTodoInputActivated} /> ) : null} - - ); }; diff --git a/components/todayView/dailyTodos/dailyTodo/subTodoInfo/SubTodoInfo.tsx b/components/todayView/dailyTodos/dailyTodo/subTodoInfo/SubTodoInfo.tsx index c3c3e62..d13629e 100644 --- a/components/todayView/dailyTodos/dailyTodo/subTodoInfo/SubTodoInfo.tsx +++ b/components/todayView/dailyTodos/dailyTodo/subTodoInfo/SubTodoInfo.tsx @@ -48,6 +48,7 @@ const SubTodoInfo: React.FC = ({ const styles = StyleSheet.create({ container: { paddingTop: 0, + paddingRight: scale(2), }, bottomContainer: { flexDirection: 'row', diff --git a/components/todayView/dailyTodos/dailyTodo/subTodoList/DailySubTodo.jsx b/components/todayView/dailyTodos/dailyTodo/subTodoList/DailySubTodo.jsx index ddb437e..1c16016 100644 --- a/components/todayView/dailyTodos/dailyTodo/subTodoList/DailySubTodo.jsx +++ b/components/todayView/dailyTodos/dailyTodo/subTodoList/DailySubTodo.jsx @@ -1,6 +1,6 @@ import EditableTextField from '@/components/common/molecules/EditableTextField'; import { LoginContext } from '@/contexts/LoginContext'; -import { TextInputContext } from '@/contexts/textInputContext'; +import useTextInputStore from '@/contexts/textInputStore'; import { useSubTodoUpdateMutation } from '@/hooks/api/useSubTodoMutations'; import getIconFillColor from '@/utils/getIconFillColor'; import { @@ -18,7 +18,9 @@ const DailySubTodo = ({ item }) => { const [isEditing, setIsEditing] = useState(false); const { mutate: updateSubTodo } = useSubTodoUpdateMutation(); const { userId } = useContext(LoginContext); - const { setTodayTextInputOpen } = useContext(TextInputContext); + const setTodayTextInputOpen = useTextInputStore( + state => state.setTodayTextInputOpen, + ); const handleEdit = () => { setIsEditing(true); @@ -34,15 +36,6 @@ const DailySubTodo = ({ item }) => { updateSubTodo(updatedData); }; - const handleSubTodoUpdate = content => { - const updatedData = { - subtodoId: item.id, - content: content, - }; - updateSubTodo(updatedData); - setIsEditing(false); - }; - const handleCheckIconPress = () => { handleLogEvent(DAILYTODO_SUBTODOCOMPLETE_CLICK_EVENT, { time: new Date().toISOString(), @@ -75,10 +68,11 @@ const DailySubTodo = ({ item }) => { <> ); diff --git a/components/todayView/dailyTodos/dailyTodo/subTodoList/SubTodoList.tsx b/components/todayView/dailyTodos/dailyTodo/subTodoList/SubTodoList.tsx index 451ccae..2fe560f 100644 --- a/components/todayView/dailyTodos/dailyTodo/subTodoList/SubTodoList.tsx +++ b/components/todayView/dailyTodos/dailyTodo/subTodoList/SubTodoList.tsx @@ -1,6 +1,6 @@ import colors from '@/theme/theme.json'; import { Icon, List } from '@ui-kitten/components'; -import React from 'react'; +import React, { useEffect, useRef } from 'react'; import { useTranslation } from 'react-i18next'; import { StyleSheet, TextInput, View } from 'react-native'; import { Todo } from '../../../../../types/todo'; @@ -27,6 +27,13 @@ const SubTodoList: React.FC = ({ const { handleSubtodoSubmit, subTodoInput, setSubtodoInput } = useSubTodoList( { item, setSubTodoInputActivated }, ); + const inputRef = useRef(null); + + useEffect(() => { + if (subTodoInputActivated) { + inputRef.current?.focus(); + } + }, [subTodoInputActivated]); return ( = ({ fill={colors.Gray02} /> { diff --git a/components/todayView/dailyTodos/dailyTodo/todoListItem/TodoListItem.tsx b/components/todayView/dailyTodos/dailyTodo/todoListItem/TodoListItem.tsx index a16e3ba..d4a9b03 100644 --- a/components/todayView/dailyTodos/dailyTodo/todoListItem/TodoListItem.tsx +++ b/components/todayView/dailyTodos/dailyTodo/todoListItem/TodoListItem.tsx @@ -1,6 +1,6 @@ import getIconFillColor from '@/utils/getIconFillColor'; import { Icon, ListItem, Text } from '@ui-kitten/components'; -import React from 'react'; +import React, { useCallback } from 'react'; import { Pressable, StyleSheet, View } from 'react-native'; import { RenderItemParams } from 'react-native-draggable-flatlist'; import { moderateScale, scale, verticalScale } from 'react-native-size-matters'; @@ -24,80 +24,72 @@ interface TodoListItemProps extends RenderItemParams { categoryId: number; } -const TodoListItem: React.FC = ({ - item, - drag, +const TodoListItem: React.FC = React.memo( + ({ item, drag, isActive, setSubTodoInputActivated, categoryId }) => { + const { + isEditing, + setIsEditing, + theme, + handleCheckIconPress, + handleTodoListItemPress, + } = useTodoListItem({ + item, + }); - isActive, - isEditing, - setIsEditing, - setIsSubTodoGenerateModalVisible, - onEdit, - setSubTodoInputActivated, - categoryId, -}) => { - const { - theme, - handleCheckIconPress, - handleTodoListItemPress, - handleTodoListItemSubmitEditing, - } = useTodoListItem({ - item, - isEditing, - setIsEditing, - setIsSubTodoGenerateModalVisible, - }); + const accessoryLeft = useCallback( + (props?) => { + return ( + + + + ); + }, + [handleCheckIconPress, item.isCompleted], + ); - const accessoryLeft = (props?) => { - return ( - handleCheckIconPress()} - style={styles.touchableCheck} - > - { + return ( + - - ); - }; + ); + }, [item, setSubTodoInputActivated, categoryId, setIsEditing]); - const accessoryRight = () => { - return ( - + const title = useCallback( + () => ( + + + {item.dueTime && ( + + {item.dueTime.split(':').slice(0, 2).join(':')} + + )} + + ), + [isEditing, item, setIsEditing, theme], ); - }; - const title = () => ( - - - {item.dueTime && ( - - {item.dueTime.split(':').slice(0, 2).join(':')} - - )} - - ); - - return ( - <> + return ( = ({ disabled={isActive} title={title} /> - - ); -}; + ); + }, +); const styles = StyleSheet.create({ checkIcon: { @@ -132,4 +124,4 @@ const styles = StyleSheet.create({ }, }); -export default TodoListItem; +export default React.memo(TodoListItem); diff --git a/components/todayView/dailyTodos/dailyTodo/todoListItem/todoMoreMenu/TodoMoreMenu.tsx b/components/todayView/dailyTodos/dailyTodo/todoListItem/todoMoreMenu/TodoMoreMenu.tsx index f5e308a..c6ff9bc 100644 --- a/components/todayView/dailyTodos/dailyTodo/todoListItem/todoMoreMenu/TodoMoreMenu.tsx +++ b/components/todayView/dailyTodos/dailyTodo/todoListItem/todoMoreMenu/TodoMoreMenu.tsx @@ -49,11 +49,10 @@ const MenuIconButton = onPress => ( ); const TodoMoreMenu = ({ - setIsSubTodoGenerateModalVisible, - onEdit, item, setSubTodoInputActivated, categoryId, + setIsEditing, }) => { const { handleEditPress, @@ -61,11 +60,10 @@ const TodoMoreMenu = ({ handleCreateSubTodoPress: handleAddSubTodoPress, handlePutTodoToInboxPress, } = useTodoMoreMenu({ - setIsSubTodoGenerateModalVisible, - onEdit, item, setSubTodoInputActivated, categoryId, + setIsEditing, }); const [visible, setVisible] = useState(false); const { t } = useTranslation(); @@ -73,7 +71,6 @@ const TodoMoreMenu = ({ const setSelectedTodo = useTodoStore(state => state.setSelectedTodo); const { openBottomSheet: openAIBottomSheet } = useContext(AIBottomSheetContext); - useContext(AIBottomSheetContext); const toggleMenu = useCallback(() => { setVisible(true); diff --git a/components/todayView/dailyTodos/dailyTodo/todoListItem/todoMoreMenu/useTodoMoreMenu.tsx b/components/todayView/dailyTodos/dailyTodo/todoListItem/todoMoreMenu/useTodoMoreMenu.tsx index 7578ca8..b720696 100644 --- a/components/todayView/dailyTodos/dailyTodo/todoListItem/todoMoreMenu/useTodoMoreMenu.tsx +++ b/components/todayView/dailyTodos/dailyTodo/todoListItem/todoMoreMenu/useTodoMoreMenu.tsx @@ -16,10 +16,9 @@ import { useContext } from 'react'; const useTodoMoreMenu = ({ item, - onEdit = () => {}, - setIsSubTodoGenerateModalVisible, setSubTodoInputActivated, categoryId, + setIsEditing, }) => { const { userId } = useContext(LoginContext); @@ -37,7 +36,7 @@ const useTodoMoreMenu = ({ const updatedTodo = { date: kstDate, - todo_id: item.id, + todoId: item.id, category_id: categoryId, }; updateTodoDate(updatedTodo); @@ -49,7 +48,7 @@ const useTodoMoreMenu = ({ userId: userId, todoId: item.id, }); - onEdit(); + setIsEditing(true); }; const handleDeletePress = () => { @@ -69,10 +68,6 @@ const useTodoMoreMenu = ({ }); }; - const handleGenerateSubTodoPress = () => { - setIsSubTodoGenerateModalVisible(true); - }; - const handleCreateSubTodoPress = () => { handleLogEvent(TODOMODAL_CREATESUBTODO_CLICK_EVENT, { time: new Date().toISOString(), @@ -95,7 +90,6 @@ const useTodoMoreMenu = ({ handleEditPress, handleDeletePress, handleChaneDatePress, - handleGenerateSubTodoPress, handleCreateSubTodoPress, handlePutTodoToInboxPress, }; diff --git a/components/todayView/dailyTodos/dailyTodo/todoListItem/useTodoListItem.ts b/components/todayView/dailyTodos/dailyTodo/todoListItem/useTodoListItem.ts index cb309ca..fd794c0 100644 --- a/components/todayView/dailyTodos/dailyTodo/todoListItem/useTodoListItem.ts +++ b/components/todayView/dailyTodos/dailyTodo/todoListItem/useTodoListItem.ts @@ -1,6 +1,5 @@ -import { TextInputContext } from '@/contexts/textInputContext'; import { useTheme } from '@ui-kitten/components'; -import { useContext } from 'react'; +import { useContext, useState } from 'react'; import { LoginContext } from '../../../../../contexts/LoginContext'; import { useTodoUpdateMutation } from '../../../../../hooks/api/useTodoMutations'; import { @@ -10,16 +9,11 @@ import { handleLogEvent, } from '../../../../../utils/logEvent'; -const useTodoListItem = ({ - item, - isEditing, - setIsEditing, - setIsSubTodoGenerateModalVisible, -}) => { +const useTodoListItem = ({ item }) => { const { mutate: updateTodo } = useTodoUpdateMutation(); const { userId } = useContext(LoginContext); const theme = useTheme(); - const { setTodayTextInputOpen } = useContext(TextInputContext); + const [isEditing, setIsEditing] = useState(false); const checkIconlogPressEvent = () => { handleLogEvent(DAILYTODO_TODOCOMPLETE_CLICK_EVENT, { @@ -48,24 +42,6 @@ const useTodoListItem = ({ }); }; - const handleTodoContentUpdate = content => { - const updatedData = { - todoId: item.id, - content: content, - }; - updateTodo(updatedData); - }; - - const handleTodoListItemSubmitEditing = content => { - handleTodoContentUpdate(content); - setIsEditing(false); - setTodayTextInputOpen(false); - }; - - const handleGenerateIconPress = () => { - setIsSubTodoGenerateModalVisible(true); - }; - const handleSettingIconPress = () => { handleLogEvent(DAILYTODO_MEATBALLMENU_CLICK_EVENT, { time: new Date().toISOString(), @@ -80,8 +56,6 @@ const useTodoListItem = ({ isEditing, setIsEditing, handleTodoListItemPress, - handleTodoListItemSubmitEditing, - handleGenerateIconPress, handleSettingIconPress, }; }; diff --git a/components/todayView/dailyTodos/subtodoGenerateBottomSheet/SubTodoGenerateBottomSheet.tsx b/components/todayView/dailyTodos/subtodoGenerateBottomSheet/SubTodoGenerateBottomSheet.tsx index 723cd94..44514e9 100644 --- a/components/todayView/dailyTodos/subtodoGenerateBottomSheet/SubTodoGenerateBottomSheet.tsx +++ b/components/todayView/dailyTodos/subtodoGenerateBottomSheet/SubTodoGenerateBottomSheet.tsx @@ -1,15 +1,6 @@ -import React, { - useCallback, - useContext, - useMemo, - useState, - useEffect, -} from 'react'; +import React, { useContext, useMemo, useState, useEffect } from 'react'; import { View, StyleSheet, Pressable } from 'react-native'; -import BottomSheet, { - BottomSheetBackdrop, - BottomSheetView, -} from '@gorhom/bottom-sheet'; +import BottomSheet, { BottomSheetView } from '@gorhom/bottom-sheet'; import { LoginContext } from '@/contexts/LoginContext'; import { Button, @@ -29,6 +20,7 @@ import axios from 'axios'; import { API_PATH } from '@/utils/config'; import * as Sentry from '@sentry/react-native'; import { AIBottomSheetContext } from '@/contexts/AIBottomSheetProvider'; +import useListRefStore from '@/contexts/listRefStore'; const SubTodoGenerateBottomSheet = ({ item }) => { const { accessToken } = useContext(LoginContext); @@ -38,23 +30,12 @@ const SubTodoGenerateBottomSheet = ({ item }) => { const theme = useTheme(); const { t } = useTranslation(); const { mutate: addSubTodo } = useSubTodoAddMutation(); + const { todoListRef } = useListRefStore(); const snapPoints = useMemo(() => ['75%'], []); const { bottomSheetRef } = useContext(AIBottomSheetContext); - const renderBackdrop = useCallback( - props => ( - - ), - [], - ); - useEffect(() => { if (generatedSubTodos.length > 0) { setSelectedIndexes(generatedSubTodos.map((_, index) => index)); @@ -95,7 +76,21 @@ const SubTodoGenerateBottomSheet = ({ item }) => { date: generatedSubTodos[index].date, todoId: generatedSubTodos[index].todo, })); - addSubTodo({ todoData: newSubTodos }); + + addSubTodo( + { todoData: newSubTodos }, + { + onSuccess: () => { + requestAnimationFrame(() => { + requestAnimationFrame(() => { + if (todoListRef?.current) { + todoListRef.current?.scrollToEnd({ animated: true }); + } + }); + }); + }, + }, + ); handleClose(); }; @@ -220,7 +215,6 @@ const SubTodoGenerateBottomSheet = ({ item }) => { snapPoints={snapPoints} index={-1} enablePanDownToClose={true} - backdropComponent={renderBackdrop} > {renderContent()} diff --git a/contexts/LoginContext.js b/contexts/LoginContext.js index b5929bd..31d5d82 100755 --- a/contexts/LoginContext.js +++ b/contexts/LoginContext.js @@ -10,6 +10,7 @@ const LoginProvider = ({ children }) => { const [accessToken, setAccessToken] = useState(); const [refreshToken, setRefreshToken] = useState(); const [loginType, setLoginType] = useState(); + const [userName, setUserName] = useState(); return ( { setRefreshToken, loginType, setLoginType, + userName, + setUserName, }} > {children} diff --git a/contexts/listRefStore.ts b/contexts/listRefStore.ts new file mode 100644 index 0000000..66d0e34 --- /dev/null +++ b/contexts/listRefStore.ts @@ -0,0 +1,18 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { create } from 'zustand'; + +type ListRefStore = { + categoryListRef: React.RefObject | null; + setCategoryListRef: (ref: React.RefObject) => void; + todoListRef: React.RefObject | null; + setTodoListRef: (ref: React.RefObject) => void; +}; + +const useListRefStore = create(set => ({ + categoryListRef: null, + setCategoryListRef: ref => set({ categoryListRef: ref }), + todoListRef: null, + setTodoListRef: ref => set({ todoListRef: ref }), +})); + +export default useListRefStore; diff --git a/contexts/textInputContext.js b/contexts/textInputContext.js deleted file mode 100644 index bfc925c..0000000 --- a/contexts/textInputContext.js +++ /dev/null @@ -1,31 +0,0 @@ -import { createContext, useState } from 'react'; - -export const TextInputContext = createContext(); - -const TextInputProvider = ({ children }) => { - const [isTodayTextInputOpen, setTodayTextInputOpen] = useState(false); - const [todayActivatedCategoryId, setTodayActivatedCategoryId] = - useState(null); - const [isInboxTextInputOpen, setInboxTextInputOpen] = useState(false); - const [inboxActivatedCategoryId, setInboxActivatedCategoryId] = - useState(null); - - return ( - - {children} - - ); -}; - -export default TextInputProvider; diff --git a/contexts/textInputStore.js b/contexts/textInputStore.js new file mode 100644 index 0000000..8f6d0be --- /dev/null +++ b/contexts/textInputStore.js @@ -0,0 +1,17 @@ +import { create } from 'zustand'; + +const useTextInputStore = create(set => ({ + isTodayTextInputOpen: false, + setTodayTextInputOpen: isOpen => set({ isTodayTextInputOpen: isOpen }), + + todayActivatedCategoryId: null, + setTodayActivatedCategoryId: id => set({ todayActivatedCategoryId: id }), + + isInboxTextInputOpen: false, + setInboxTextInputOpen: isOpen => set({ isInboxTextInputOpen: isOpen }), + + inboxActivatedCategoryId: null, + setInboxActivatedCategoryId: id => set({ inboxActivatedCategoryId: id }), +})); + +export default useTextInputStore; diff --git a/contexts/todoEditStore.ts b/contexts/todoEditStore.ts new file mode 100644 index 0000000..d36b15e --- /dev/null +++ b/contexts/todoEditStore.ts @@ -0,0 +1,18 @@ +import { create } from 'zustand'; + +interface TodoEditStore { + isEditing: boolean; + editingTodoId: number | null; + isSubTodoModalVisible: boolean; + setIsEditing: (isEditing: boolean) => void; + setEditingTodoId: (todoId: number | null) => void; +} + +export const useTodoEditStore = create(set => ({ + isEditing: false, + editingTodoId: null, + isSubTodoModalVisible: false, + isTodayTextInputOpen: false, + setIsEditing: isEditing => set({ isEditing }), + setEditingTodoId: todoId => set({ editingTodoId: todoId }), +})); diff --git a/hooks/api/useCategoriesQuery.js b/hooks/api/useCategoriesQuery.js index 5f00f42..ae285eb 100644 --- a/hooks/api/useCategoriesQuery.js +++ b/hooks/api/useCategoriesQuery.js @@ -15,6 +15,8 @@ const useCategoriesQuery = userId => { suspense: true, refetchInterval: 60000, refetchIntervalInBackground: true, + cacheTime: 180000, + staleTime: 30000, }); }; diff --git a/hooks/api/useInboxTodoQuery.js b/hooks/api/useInboxTodoQuery.js index faa94a6..a29a4d2 100644 --- a/hooks/api/useInboxTodoQuery.js +++ b/hooks/api/useInboxTodoQuery.js @@ -14,6 +14,8 @@ const useInboxTodoQuery = userId => { queryKey: [INBOX_QUERY_KEY], queryFn: () => fetcher(userId), suspense: true, + cacheTime: 180000, + staleTime: 30000, }); }; diff --git a/hooks/api/useTodoMutations.js b/hooks/api/useTodoMutations.js deleted file mode 100644 index 3ef1b5a..0000000 --- a/hooks/api/useTodoMutations.js +++ /dev/null @@ -1,55 +0,0 @@ -import { api as Api } from '@/utils/api'; -import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { INBOX_QUERY_KEY } from './useInboxTodoQuery'; -import { TODO_QUERY_KEY } from './useTodoQuery'; - -// 생성 (Add Todo) -const addTodoFetcher = async todoData => { - const data = await Api.addTodo(todoData); - return data; -}; - -export const useTodoAddMutation = () => { - const queryClient = useQueryClient(); - return useMutation({ - mutationFn: addTodoFetcher, - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: [TODO_QUERY_KEY] }); - queryClient.invalidateQueries({ queryKey: [INBOX_QUERY_KEY] }); - }, - }); -}; - -// 수정 (Update Todo) -const updateTodoFetcher = async updatedData => { - const data = await Api.updateTodo(updatedData); - return data; -}; - -export const useTodoUpdateMutation = () => { - const queryClient = useQueryClient(); - return useMutation({ - mutationFn: updateTodoFetcher, - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: [TODO_QUERY_KEY] }); - queryClient.invalidateQueries({ queryKey: [INBOX_QUERY_KEY] }); - }, - }); -}; - -// 삭제 (Delete Todo) -const deleteTodoFetcher = async todoId => { - const data = await Api.deleteTodo(todoId.todoId); - return data; -}; - -export const useTodoDeleteMutation = () => { - const queryClient = useQueryClient(); - return useMutation({ - mutationFn: deleteTodoFetcher, - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: [TODO_QUERY_KEY] }); - queryClient.invalidateQueries({ queryKey: [INBOX_QUERY_KEY] }); - }, - }); -}; diff --git a/hooks/api/useTodoMutations.ts b/hooks/api/useTodoMutations.ts new file mode 100644 index 0000000..c7ccddb --- /dev/null +++ b/hooks/api/useTodoMutations.ts @@ -0,0 +1,157 @@ +import { api as Api } from '@/utils/api'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { INBOX_QUERY_KEY } from './useInboxTodoQuery'; +import { TODO_QUERY_KEY } from './useTodoQuery'; +import { Todo } from '@/types/todo'; + +const addTodoFetcher = async (todoData: Omit) => { + const data = await Api.addTodo(todoData); + return data; +}; + +export const useTodoAddMutation = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: addTodoFetcher, + onMutate: async newTodo => { + await queryClient.cancelQueries({ queryKey: [TODO_QUERY_KEY] }); + await queryClient.cancelQueries({ queryKey: [INBOX_QUERY_KEY] }); + + const previousTodos = queryClient.getQueryData([TODO_QUERY_KEY]); + const previousInbox = queryClient.getQueryData([INBOX_QUERY_KEY]); + + const optimisticTodo: Todo = { + ...newTodo, + id: Date.now(), + children: [], + }; + + queryClient.setQueryData([TODO_QUERY_KEY], old => [ + ...(old || []), + optimisticTodo, + ]); + queryClient.setQueryData([INBOX_QUERY_KEY], old => [ + ...(old || []), + optimisticTodo, + ]); + + return { previousTodos, previousInbox }; + }, + onError: (_err, _newTodo, context) => { + queryClient.setQueryData([TODO_QUERY_KEY], context?.previousTodos); + queryClient.setQueryData([INBOX_QUERY_KEY], context?.previousInbox); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: [TODO_QUERY_KEY] }); + queryClient.invalidateQueries({ queryKey: [INBOX_QUERY_KEY] }); + }, + }); +}; + +const updateTodoFetcher = async ( + updatedData: Partial & { + todoId: number; + patchRank?: { prevId: number | null }; + }, +) => { + const data = await Api.updateTodo(updatedData); + return data; +}; + +export const useTodoUpdateMutation = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: updateTodoFetcher, + onMutate: async updatedTodo => { + await queryClient.cancelQueries({ queryKey: [TODO_QUERY_KEY] }); + await queryClient.cancelQueries({ queryKey: [INBOX_QUERY_KEY] }); + + const previousTodos = queryClient.getQueryData([TODO_QUERY_KEY]); + const previousInbox = queryClient.getQueryData([INBOX_QUERY_KEY]); + + const updateTodoOrder = (old: Todo[] | undefined) => { + if (!old) return []; + + if ('patchRank' in updatedTodo) { + const movedTodo = old.find(todo => todo.id === updatedTodo.todoId); + if (!movedTodo) return old; + + const filteredTodos = old.filter( + todo => todo.id !== updatedTodo.todoId, + ); + + if (updatedTodo.patchRank.prevId === null) { + return [movedTodo, ...filteredTodos]; + } + + const targetIndex = filteredTodos.findIndex( + todo => todo.id === updatedTodo.patchRank!.prevId, + ); + + if (targetIndex === -1) return old; + + return [ + ...filteredTodos.slice(0, targetIndex + 1), + movedTodo, + ...filteredTodos.slice(targetIndex + 1), + ]; + } + + return old.map(todo => { + if (todo.id === updatedTodo.todoId) { + return { ...todo, ...updatedTodo }; + } + return todo; + }); + }; + + queryClient.setQueryData([TODO_QUERY_KEY], updateTodoOrder); + queryClient.setQueryData([INBOX_QUERY_KEY], updateTodoOrder); + + return { previousTodos, previousInbox }; + }, + onError: (_err, _updatedTodo, context) => { + queryClient.setQueryData([TODO_QUERY_KEY], context?.previousTodos); + queryClient.setQueryData([INBOX_QUERY_KEY], context?.previousInbox); + }, + onSuccess: () => { + queryClient.refetchQueries({ queryKey: [TODO_QUERY_KEY] }); + queryClient.refetchQueries({ queryKey: [INBOX_QUERY_KEY] }); + }, + }); +}; + +const deleteTodoFetcher = async ({ todoId }: { todoId: number }) => { + const data = await Api.deleteTodo(todoId); + return data; +}; + +export const useTodoDeleteMutation = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: deleteTodoFetcher, + onMutate: async ({ todoId }) => { + await queryClient.cancelQueries({ queryKey: [TODO_QUERY_KEY] }); + await queryClient.cancelQueries({ queryKey: [INBOX_QUERY_KEY] }); + + const previousTodos = queryClient.getQueryData([TODO_QUERY_KEY]); + const previousInbox = queryClient.getQueryData([INBOX_QUERY_KEY]); + + const deleteTodo = (old: Todo[] | undefined) => + old?.filter(todo => todo.id !== todoId) || []; + + queryClient.setQueryData([TODO_QUERY_KEY], deleteTodo); + queryClient.setQueryData([INBOX_QUERY_KEY], deleteTodo); + + return { previousTodos, previousInbox }; + }, + onError: (_err, _variables, context) => { + queryClient.setQueryData([TODO_QUERY_KEY], context?.previousTodos); + queryClient.setQueryData([INBOX_QUERY_KEY], context?.previousInbox); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: [TODO_QUERY_KEY] }); + queryClient.invalidateQueries({ queryKey: [INBOX_QUERY_KEY] }); + }, + }); +}; diff --git a/hooks/api/useTodoQuery.js b/hooks/api/useTodoQuery.js index 63a760d..5081d57 100644 --- a/hooks/api/useTodoQuery.js +++ b/hooks/api/useTodoQuery.js @@ -1,4 +1,3 @@ -// useTodosQuery.js import { api as Api } from '@/utils/api'; import { useQuery } from '@tanstack/react-query'; @@ -16,6 +15,9 @@ const useTodosQuery = userId => { suspense: true, refetchInterval: 60000, refetchIntervalInBackground: true, + cacheTime: 180000, + staleTime: 30000, + keepPreviousData: true, }); }; diff --git a/hooks/auth/useGoogleAuth.js b/hooks/auth/useGoogleAuth.js index 0ccd86d..6ae5f4a 100644 --- a/hooks/auth/useGoogleAuth.js +++ b/hooks/auth/useGoogleAuth.js @@ -1,5 +1,4 @@ import { api as Api } from '@/utils/api'; -import messaging from '@react-native-firebase/messaging'; import * as Google from 'expo-auth-session/providers/google'; import { useEffect, useState } from 'react'; import { Platform } from 'react-native'; @@ -45,9 +44,6 @@ const useGoogleAuth = () => { useEffect(() => { getClientId(); handleLocalToken(); - (async () => { - await messaging().requestPermission(); - })(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); diff --git a/hooks/auth/useLogin.js b/hooks/auth/useLogin.js index 18c817d..3f5de38 100644 --- a/hooks/auth/useLogin.js +++ b/hooks/auth/useLogin.js @@ -17,7 +17,8 @@ const useLogin = () => { const storage = useStorage(); const { deviceToken } = useDeviceToken(); const router = useRouter(); - const { setIsLoggedIn, setUserId, setAccessToken } = useContext(LoginContext); + const { setIsLoggedIn, setUserId, setAccessToken, setUserName } = + useContext(LoginContext); const { mutate: addCategory } = useCategoryAddMutation(); const { t } = useTranslation(); @@ -42,6 +43,7 @@ const useLogin = () => { const user = await Api.getUserInfo(); await setAsyncStorageLoginInfo(jwtTokenData, user); setUserId(jwtTokenData.userId); + setUserName(jwtTokenData.email); setIsLoggedIn(true); if (jwtTokenData.isNew) { handleAddCategory({ categoryName: t('views.categoryAddView.init') }); diff --git a/hooks/todo/useFilteredTodo.js b/hooks/todo/useFilteredTodo.js index b6d9261..6d41cb3 100644 --- a/hooks/todo/useFilteredTodo.js +++ b/hooks/todo/useFilteredTodo.js @@ -1,17 +1,14 @@ -import { useEffect, useState } from 'react'; +import { useMemo } from 'react'; const useFilteredTodos = (todos, selectedCategory, selectedDate) => { - const [filteredTodos, setFilteredTodos] = useState([]); + const filteredTodos = useMemo(() => { + if (!todos) return []; - useEffect(() => { - if (todos) { - const filtered = todos.filter( - todo => - todo.categoryId === selectedCategory && - todo.date === selectedDate.format('YYYY-MM-DD'), - ); - setFilteredTodos(filtered); - } + return todos.filter( + todo => + todo.categoryId === selectedCategory && + todo.date === selectedDate.format('YYYY-MM-DD'), + ); }, [todos, selectedCategory, selectedDate]); return filteredTodos; diff --git a/hooks/todo/useHandleDrag.js b/hooks/todo/useHandleDrag.js index 8e28dec..d2bc410 100644 --- a/hooks/todo/useHandleDrag.js +++ b/hooks/todo/useHandleDrag.js @@ -5,9 +5,7 @@ const useHandleDrag = () => { const handleDragEnd = ({ from, to, data: newData }) => { if (!newData || newData.length === 0) return; - if (from === to) { - return; - } + if (from === to) return; let prevTodoId = null; let nextTodoId = null; @@ -22,7 +20,7 @@ const useHandleDrag = () => { } const updatedData = { - todo_id: newData[to].id, + todoId: newData[to].id, patchRank: { prevId: prevTodoId, nextId: nextTodoId,