diff --git a/src/api/diary/get.ts b/src/api/diary/get.ts index 031c85e..f99093d 100644 --- a/src/api/diary/get.ts +++ b/src/api/diary/get.ts @@ -1,6 +1,6 @@ import React from 'react'; import instance from '@api/axios'; -import { DateStatus, IDate, IDiaryCount, IDiaryListResponse, NEW_DIARY } from '@type/Diary'; +import { IDate, IDiaryCount, IDiaryListResponse, newDiary } from '@type/Diary'; import { useQuery, UseQueryResult } from '@tanstack/react-query'; export const fetchDiaryList = async (targetDate: string): Promise => { @@ -8,11 +8,11 @@ export const fetchDiaryList = async (targetDate: string): Promise { +export const useDiaryList = (targetDate: string, isMarked: boolean) => { return useQuery({ queryKey: ['diary', 'list', targetDate], queryFn: () => fetchDiaryList(targetDate), - enabled: !!targetDate, + enabled: !!targetDate && isMarked, staleTime: Infinity, select: (data) => { const diaryList = data.diaries.map((item) => ({ @@ -20,12 +20,7 @@ export const useDiaryList = (targetDate: string, dateStatus: DateStatus) => { content: item.content, createdTime: item.createdDate, })); - diaryList.length < 3 && - diaryList.push({ - id: NEW_DIARY, - content: '이 날의 심심기록을 남겨보세요', - createdTime: '', - }); + diaryList.length < 3 && diaryList.push(newDiary); return { sendStatus: data.sendStatus, diaryList }; }, }); diff --git a/src/components/diary/calendar/CalendarSelectModal.tsx b/src/components/diary/calendar/CalendarSelectModal.tsx new file mode 100644 index 0000000..4386112 --- /dev/null +++ b/src/components/diary/calendar/CalendarSelectModal.tsx @@ -0,0 +1,139 @@ +import MyText from '@components/common/MyText'; +import { appColor3 } from '@utils/colors'; +import { kMonth } from '@utils/localeConfig'; +import { fontLarge } from '@utils/Sizing'; +import React from 'react'; +import { + View, + Modal, + FlatList, + Pressable, + TouchableWithoutFeedback, + StyleSheet, + ListRenderItem, +} from 'react-native'; + +interface IMonthItem { + month: string; + index: number; +} + +interface ICalendarSelectModalProps { + isModalVisible: boolean; + handleModalDismiss: () => void; + selectedMonth: number; + selectedYear: number; + setSelectedMonth: (month: number) => void; + setSelectedYear: (year: number) => void; +} + +const CalendarSelectModal = ({ + isModalVisible, + handleModalDismiss, + selectedMonth, + selectedYear, + setSelectedMonth, + setSelectedYear, +}: ICalendarSelectModalProps) => { + const renderMonthItem: ListRenderItem = ({ item }) => ( + setSelectedMonth(item.index + 1)}> + + + {item.month} + + + + ); + + const renderYearItem: ListRenderItem = ({ item }) => ( + setSelectedYear(item)}> + + + {item} + + + + ); + return ( + + + + + + + ({ month, index }))} + renderItem={renderMonthItem} + keyExtractor={(item) => item.index.toString()} + initialScrollIndex={Math.max(0, selectedMonth - 1)} + getItemLayout={(data, index) => ({ length: 50, offset: 50 * index, index })} + showsVerticalScrollIndicator={false} + /> + + + 2000 + i)} + renderItem={renderYearItem} + keyExtractor={(item) => item.toString()} + initialScrollIndex={Math.max(0, selectedYear - 2000)} + getItemLayout={(data, index) => ({ length: 50, offset: 50 * index, index })} + showsVerticalScrollIndicator={false} + /> + + + + + + + ); +}; + +export default CalendarSelectModal; + +const styles = StyleSheet.create({ + modalOverlay: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + backgroundColor: 'rgba(0, 0, 0, 0.5)', + }, + modalContent: { + width: 300, + maxHeight: 300, + backgroundColor: '#ffffff', + borderRadius: 10, + padding: 10, + flexDirection: 'row', + }, + modalListContainer: { + flexDirection: 'row', + justifyContent: 'space-between', + width: '100%', + }, + modalMonth: { + width: '50%', + }, + modalYear: { + width: '50%', + }, + modalItem: { + paddingVertical: 10, + paddingHorizontal: 20, + }, + modalText: { + fontSize: fontLarge, + textAlign: 'center', + }, + selectedText: { + fontSize: fontLarge, + textAlign: 'center', + color: appColor3, + }, + selectedStyle: { + borderColor: appColor3, + borderWidth: 1, + borderRadius: 15, + paddingVertical: 10, + paddingHorizontal: 19, + }, +}); diff --git a/src/components/diary/calendar/MyCalendar.tsx b/src/components/diary/calendar/MyCalendar.tsx index e991926..34f2867 100644 --- a/src/components/diary/calendar/MyCalendar.tsx +++ b/src/components/diary/calendar/MyCalendar.tsx @@ -1,22 +1,31 @@ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import CalendarArrow, { Direction } from '@components/diary/calendar/CalendarArrow'; -import { StyleSheet, View, Platform } from 'react-native'; -import { Calendar, DateData } from 'react-native-calendars'; -import setLocaleConfig from '@utils/localeConfig'; -import { IDiaryCount, IMarkedDates } from '@type/Diary'; +import { StyleSheet, View } from 'react-native'; +import { Calendar } from 'react-native-calendars'; +import setLocaleConfig, { kMonth } from '@utils/localeConfig'; +import { IDate, IMarkedDates } from '@type/Diary'; import { dotColors } from '@utils/colors'; import { fontLarge } from '@utils/Sizing'; -import { getToday } from '@utils/dateUtils'; +import MyText from '@components/common/MyText'; +import TextButton from '@components/common/TextButton'; +import CalendarSelectModal from './CalendarSelectModal'; +import useCalendarHook from '@hooks/diary/calendarHook'; setLocaleConfig(); -interface IMyCalendarProps { - selectedDate: string; - markedDates: IDiaryCount[]; - onDayPress: (date: DateData) => void; - onMonthChange: (date: DateData) => void; -} -const MyCalendar = ({ selectedDate, markedDates, onDayPress, onMonthChange }: IMyCalendarProps) => { +const MyCalendar = () => { + const { + onDayPress, + onMonthChange, + markedDates, + selectedDate, + setSelectedDate, + saveDateStatus, + setTargetMonth, + } = useCalendarHook(); + const [isModalVisible, setModalVisible] = useState(false); + const [selectedMonth, setSelectedMonth] = useState(parseInt(selectedDate.slice(5, 7), 10)); + const [selectedYear, setSelectedYear] = useState(parseInt(selectedDate.slice(0, 4), 10)); const markedDatesList: IMarkedDates = markedDates.reduce((acc, date) => { acc[date.markedDate] = { selected: date.markedDate === selectedDate, @@ -26,10 +35,36 @@ const MyCalendar = ({ selectedDate, markedDates, onDayPress, onMonthChange }: IM return acc; }, {} as IMarkedDates); + useEffect(() => { + setSelectedMonth(parseInt(selectedDate.slice(5, 7), 10)); + setSelectedYear(parseInt(selectedDate.slice(0, 4), 10)); + setTargetMonth({ + year: selectedYear.toString(), + month: selectedMonth.toString().padStart(2, '0') as IDate['month'], + }); + saveDateStatus(selectedDate); + }, [selectedDate]); + + const handleModalDismiss = () => { + setSelectedDate(`${selectedYear}-${selectedMonth.toString().padStart(2, '0')}-01`); + setModalVisible(false); + }; + + const onHeaderPress = () => { + setModalVisible(true); + }; + + const getDisplayDate = (date: Date) => { + const year = date.getFullYear(); + const month = date.getMonth(); + return `${kMonth[month]} ${year}`; + }; + return ( } + renderHeader={(date: string) => ( + <> + + {getDisplayDate(new Date(date))} + + + + )} /> ); diff --git a/src/components/diary/carousel/DiaryCard.tsx b/src/components/diary/carousel/DiaryCard.tsx index fe39ced..619a117 100644 --- a/src/components/diary/carousel/DiaryCard.tsx +++ b/src/components/diary/carousel/DiaryCard.tsx @@ -37,6 +37,7 @@ const DiaryCard = ({ useEffect(() => { setDiaryInput(id === NEW_DIARY ? '' : content); setIsEditing(false); + setTimeStartWriting(''); }, [id, content]); const addNewDiary = useMutation({ @@ -46,7 +47,7 @@ const DiaryCard = ({ setSnackbar('저장이 완료되었습니다.'); setTimeStartWriting(''); }, - onError: (error) => { + onError: (error: any) => { setSnackbar(error.response.data.message || '오류가 발생했습니다.'); }, onSettled: () => { @@ -59,7 +60,7 @@ const DiaryCard = ({ queryClient.invalidateQueries({ queryKey: ['diary'] }); setSnackbar('삭제가 완료되었습니다.'); }, - onError: (error) => { + onError: (error: any) => { setSnackbar(error.response.data.message || '오류가 발생했습니다.'); }, onSettled: () => { @@ -75,7 +76,7 @@ const DiaryCard = ({ queryClient.invalidateQueries({ queryKey: ['diary', 'list'] }); setSnackbar('수정이 완료되었습니다.'); }, - onError: (error) => { + onError: (error: any) => { setSnackbar(error.response.data.message || '오류가 발생했습니다.'); }, onSettled: () => { @@ -86,13 +87,12 @@ const DiaryCard = ({ const sendDiary = useMutation({ mutationFn: (data: IAiLetterRequest) => postAiLetters(data), onSuccess: (data) => { - console.log(data); queryClient.invalidateQueries({ queryKey: ['diary', 'list'] }); queryClient.invalidateQueries({ queryKey: ['userInfo'] }); queryClient.invalidateQueries({ queryKey: ['fetchAiLettersMonthSummary'] }); // setSnackbar('편지가 도착했습니다'); }, - onError: (error) => { + onError: (error: any) => { setSnackbar(error.response.data.message || '오류가 발생했습니다.'); }, }); @@ -127,8 +127,7 @@ const DiaryCard = ({ }; const sendDiaryData = () => { - const formattedTime = `${targetDate}T${timeStartWriting.split('T')[1]}`; - const cretatedDate = id === NEW_DIARY ? formattedTime : createdTime; + const cretatedDate = id === NEW_DIARY ? timeStartWriting : createdTime; const data = { content: diaryInput, createdDate: cretatedDate, @@ -163,6 +162,12 @@ const DiaryCard = ({ sendDiaryData(); }; + const onCancelSaveEdit = () => { + setInformModalVisible(false); + setIsEditing(false); + setDiaryInput(content); + }; + const onKeyboardDismiss = () => { setIsEditing(false); Keyboard.dismiss(); @@ -216,13 +221,14 @@ const DiaryCard = ({ visible={isSendModalVisible} setIsVisible={setSendModalVisible} onConfirm={onConfirmSend} - content="기록을 보내시겠습니까?" + content={`이 날의 기록을 모두 보내시겠습니까?\n편지는 하루에 한 번만 받을 수 있어요.`} confirmText="보내기" /> diff --git a/src/components/diary/carousel/DiaryCardHeader.tsx b/src/components/diary/carousel/DiaryCardHeader.tsx index 2a4d897..dcd579d 100644 --- a/src/components/diary/carousel/DiaryCardHeader.tsx +++ b/src/components/diary/carousel/DiaryCardHeader.tsx @@ -6,6 +6,8 @@ import { format } from 'date-fns'; import { ko } from 'date-fns/locale'; // import MyIconButton from '@components/common/MyIconButtons'; import TextButton from '@components/common/TextButton'; +import { useRecoilValue } from 'recoil'; +import { tense } from '@stores/tense'; interface DiaryCardHeaderProps { isNew: boolean; @@ -35,6 +37,8 @@ const DiaryCardHeader = ({ onDelete, onSend, }: DiaryCardHeaderProps) => { + const dateStatus = useRecoilValue(tense); + return ( {formatTime(createdTime) || formatTime(timeStartWriting)} @@ -63,13 +67,15 @@ const DiaryCardHeader = ({ ) : ( // <> - - 보내기 - + {dateStatus !== 'FUTURE' && ( + + 보내기 + + )} 삭제 diff --git a/src/components/diary/carousel/DiaryCarousel.native.tsx b/src/components/diary/carousel/DiaryCarousel.native.tsx index 83877cd..600e4cf 100644 --- a/src/components/diary/carousel/DiaryCarousel.native.tsx +++ b/src/components/diary/carousel/DiaryCarousel.native.tsx @@ -2,12 +2,15 @@ import React, { useCallback, useEffect, useRef, useState } from 'react'; import { StyleSheet, View, FlatList, ListRenderItem, ViewToken } from 'react-native'; import DiaryCard from '@components/diary/carousel/DiaryCard'; import DiaryPagination from '@components/diary/carousel/DiaryPagination'; -import { IDiary, IDiaryCarouselProps, NEW_DIARY } from '@type/Diary'; +import { IDiary, NEW_DIARY } from '@type/Diary'; import { CARD_WIDTH } from '@utils/Sizing'; import useDiaryHook from '@hooks/diary/diaryHook'; import CenterViewText from '@components/common/CenterViewText'; +import { useRecoilValue } from 'recoil'; +import { selectedDateStatus } from '@stores/tense'; -const DiaryCarousel = ({ selectedDate }: IDiaryCarouselProps) => { +const DiaryCarousel = () => { + const selectedDate = useRecoilValue(selectedDateStatus); const { data, isPending, isError, sendStatus } = useDiaryHook(selectedDate); const [activeIndex, setActiveIndex] = useState(0); const [isEditing, setIsEditing] = useState(false); diff --git a/src/components/diary/carousel/DiaryCarousel.web.tsx b/src/components/diary/carousel/DiaryCarousel.web.tsx index 0532239..690fbfd 100644 --- a/src/components/diary/carousel/DiaryCarousel.web.tsx +++ b/src/components/diary/carousel/DiaryCarousel.web.tsx @@ -2,11 +2,14 @@ import React, { useEffect, useState } from 'react'; import { StyleSheet, View } from 'react-native'; import DiaryCard from '@components/diary/carousel/DiaryCard'; import DiaryArrowIcons from '@components/diary/carousel/DiaryArrowIcons'; -import { IDiaryCarouselProps, NEW_DIARY } from '@type/Diary'; +import { NEW_DIARY } from '@type/Diary'; import useDiaryHook from '@hooks/diary/diaryHook'; import CenterViewText from '@components/common/CenterViewText'; +import { useRecoilValue } from 'recoil'; +import { selectedDateStatus } from '@stores/tense'; -const DiaryCarousel = ({ selectedDate }: IDiaryCarouselProps) => { +const DiaryCarousel = () => { + const selectedDate = useRecoilValue(selectedDateStatus); const [activeIndex, setActiveIndex] = useState(0); const { data, isPending, isError, isSuccess, sendStatus } = useDiaryHook(selectedDate); const [isEditing, setIsEditing] = useState(false); diff --git a/src/components/diary/carousel/DiaryInput.tsx b/src/components/diary/carousel/DiaryInput.tsx index dfbd165..60497ac 100644 --- a/src/components/diary/carousel/DiaryInput.tsx +++ b/src/components/diary/carousel/DiaryInput.tsx @@ -4,7 +4,6 @@ import MyTextInput from '@components/common/MyTextInput'; import LengthCheckView from '@components/common/LengthCheckView'; import { useRecoilValue } from 'recoil'; import { selectedDateStatus } from '@stores/tense'; -import { set } from 'date-fns'; interface IDiaryInputProps { id: number; @@ -34,13 +33,13 @@ const DiaryInput = ({ const onChangeText = (text: string) => { if (text.length > MAX_LENGTH) return; - if (text.length === 1 && !timeStartWriting) { + if (!timeStartWriting) { const now = new Date(); const [year, month, date] = targetDate.split('-').map(Number); - const newDate = set(now, { year, month: month - 1, date }); - setTimeStartWriting(newDate.toISOString()); + now.setFullYear(year, month - 1, date); + setTimeStartWriting(now.toISOString()); } - if (text.length === 0 && timeStartWriting) { + if (text.length === 0) { setTimeStartWriting(''); } setDiaryInput(text); diff --git a/src/hooks/diary/calendarHook.ts b/src/hooks/diary/calendarHook.ts index 0904d7f..f0f4053 100644 --- a/src/hooks/diary/calendarHook.ts +++ b/src/hooks/diary/calendarHook.ts @@ -1,39 +1,55 @@ -import React, { useState } from 'react'; -import { isSameDay } from 'date-fns'; +import React, { useEffect, useState } from 'react'; import { DateData } from 'react-native-calendars'; import { IDate } from '@type/Diary'; import { useDiaryCounts } from '@api/diary/get'; import { useRecoilState, useSetRecoilState } from 'recoil'; import { snackMessage } from '@stores/snackMessage'; -import { selectedDateStatus, tense } from '@stores/tense'; +import { markedDateStatus, selectedDateStatus, tense } from '@stores/tense'; import { getToday, getYear, getMonth } from '@utils/dateUtils'; +import { isPast, isSameDay } from 'date-fns'; const useCalendarHook = () => { - const [today] = useState(getToday); - const [selectedMonth, setSelectedMonth] = useState({ year: getYear(), month: getMonth() }); + const [targetMonth, setTargetMonth] = useState({ year: getYear(), month: getMonth() }); const [selectedDate, setSelectedDate] = useRecoilState(selectedDateStatus); const setDateStatus = useSetRecoilState(tense); const [snackbarText, setSnackbarText] = useRecoilState(snackMessage); - const { data, isPending, isError } = useDiaryCounts(selectedMonth); + const { data, isPending, isError } = useDiaryCounts(targetMonth); + const setMarkedDateSet = useSetRecoilState(markedDateStatus); + + useEffect(() => { + if (data) { + setMarkedDateSet(new Set(data.map((item) => item.markedDate))); + } + }, [data, setMarkedDateSet]); const onDayPress = (day: DateData) => { - isSameDay(day.dateString, new Date(today)) ? setDateStatus('TODAY') : setDateStatus('PAST'); setSelectedDate(day.dateString); }; const onMonthChange = (date: DateData) => { const year = date.year.toString(); const month = date.month.toString().padStart(2, '0') as IDate['month']; - setSelectedMonth({ year, month }); + setTargetMonth({ year, month }); + }; + + const saveDateStatus = (date: string) => { + if (isSameDay(date, new Date(getToday()))) { + setDateStatus('TODAY'); + } else if (isPast(new Date(date))) { + setDateStatus('PAST'); + } else { + setDateStatus('FUTURE'); + } }; return { - today, selectedDate, setSelectedDate, onDayPress, onMonthChange, setDateStatus, + saveDateStatus, + setTargetMonth, snackbarText, setSnackbarText, markedDates: data || [], diff --git a/src/hooks/diary/diaryHook.ts b/src/hooks/diary/diaryHook.ts index 61e9a1f..c5c35ab 100644 --- a/src/hooks/diary/diaryHook.ts +++ b/src/hooks/diary/diaryHook.ts @@ -1,17 +1,22 @@ import { useDiaryList } from '@api/diary/get'; -import { tense } from '@stores/tense'; +import { markedDateStatus } from '@stores/tense'; +import { newDiary } from '@type/Diary'; import { useRecoilValue } from 'recoil'; const useDiaryHook = (selectedDate: string) => { - const dateStatus = useRecoilValue(tense); - const { data, isPending, isError, isSuccess } = useDiaryList(selectedDate, dateStatus); + const markedDateSet = useRecoilValue(markedDateStatus); + const { data, isPending, isError, isSuccess } = useDiaryList( + selectedDate, + markedDateSet.has(selectedDate), + ); + const newDiaryData = [newDiary]; return { - data: data?.diaryList || [], + data: markedDateSet.has(selectedDate) ? data?.diaryList || newDiaryData : newDiaryData, isPending, isError, isSuccess, - sendStatus: data?.sendStatus || false, + sendStatus: markedDateSet.has(selectedDate) ? data?.sendStatus || false : false, }; }; diff --git a/src/hooks/useAxiosInterceptors.ts b/src/hooks/useAxiosInterceptors.ts index 8501714..707d168 100644 --- a/src/hooks/useAxiosInterceptors.ts +++ b/src/hooks/useAxiosInterceptors.ts @@ -45,7 +45,7 @@ const useAxiosInterceptors = () => { // auth/google 제외한 요청 401(토큰 만료) 에러 시 토큰 재발급, 403(권한 없음) 에러 시 로그아웃 if (error.response.status === 401 && !originalRequest._retry) { originalRequest._retry = true; - console.log('토큰 만료'); + // console.log('토큰 만료'); axios .post(`${process.env.BASE_URL}/auth/reissue`, null, { headers: { 'X-Custom-Header': 'foobar', 'Content-Type': 'application/json' }, @@ -58,12 +58,12 @@ const useAxiosInterceptors = () => { // return instance.request(originalRequest); }) .catch((err) => { - console.log('토큰 재발급 실패', err); + // console.log('토큰 재발급 실패', err); handleLogout(); }); } else if (error.response.status === 403 && !originalRequest._retry) { originalRequest._retry = true; - console.log('권한 없음'); + // console.log('권한 없음'); handleLogout(); } return Promise.reject(error); diff --git a/src/navigators/MainNavigator.tsx b/src/navigators/MainNavigator.tsx index 8844d72..39ec70d 100644 --- a/src/navigators/MainNavigator.tsx +++ b/src/navigators/MainNavigator.tsx @@ -16,8 +16,8 @@ const Stack = createStackNavigator(); const MainNavigator = () => { const isLoggedIn = useRecoilValue(isLoggedInState); - useUserSetup(); useAxiosInterceptors(); + useUserSetup(); return ( { }} > { }} /> { }} /> { }} /> { - const { selectedDate, onDayPress, onMonthChange, markedDates, snackbarText } = useCalendarHook(); - + const snackbarText = useRecoilValue(snackMessage); return ( <> - - + + diff --git a/src/stores/tense.ts b/src/stores/tense.ts index 9828bb7..c4cfeb6 100644 --- a/src/stores/tense.ts +++ b/src/stores/tense.ts @@ -11,3 +11,8 @@ export const selectedDateStatus = atom({ key: 'selectedDateStatus', default: format(new Date(), 'yyyy-MM-dd'), }); + +export const markedDateStatus = atom>({ + key: 'markedDateStatus', + default: new Set([format(new Date(), 'yyyy-MM-dd')]), +}); diff --git a/src/types/Diary.ts b/src/types/Diary.ts index 629fe75..1e296db 100644 --- a/src/types/Diary.ts +++ b/src/types/Diary.ts @@ -12,7 +12,13 @@ export interface IDiaryList { export const NEW_DIARY = -1; -export type DateStatus = 'TODAY' | 'PAST'; +export const newDiary = { + id: NEW_DIARY, + content: '이 날의 심심기록을 남겨보세요', + createdTime: '', +}; + +export type DateStatus = 'TODAY' | 'PAST' | 'FUTURE'; export interface IDiaryCarouselProps { selectedDate: string; diff --git a/src/utils/localeConfig.ts b/src/utils/localeConfig.ts index 0f12adc..095846c 100644 --- a/src/utils/localeConfig.ts +++ b/src/utils/localeConfig.ts @@ -1,35 +1,24 @@ import { LocaleConfig } from 'react-native-calendars'; +export const kMonth = [ + '일 월', + '이 월', + '삼 월', + '사 월', + '오 월', + '유 월', + '칠 월', + '팔 월', + '구 월', + '시 월', + '십일 월', + '십이 월', +]; + const setLocaleConfig = () => { LocaleConfig.locales['ko'] = { - monthNames: [ - '일 월', - '이 월', - '삼 월', - '사 월', - '오 월', - '유 월', - '칠 월', - '팔 월', - '구 월', - '시 월', - '십일 월', - '십이 월', - ], - monthNamesShort: [ - '일 월', - '이 월', - '삼 월', - '사 월', - '오 월', - '유 월', - '칠 월', - '팔 월', - '구 월', - '시 월', - '십일 월', - '십이 월', - ], + monthNames: kMonth, + monthNamesShort: kMonth, dayNames: ['일요일', '월요일', '화요일', '수요일', '목요일', '금요일', '토요일'], dayNamesShort: ['일', '월', '화', '수', '목', '금', '토'], today: '오늘',