diff --git a/src/apis/client.ts b/src/apis/client.ts index 8ccefbf..04b37c4 100644 --- a/src/apis/client.ts +++ b/src/apis/client.ts @@ -34,7 +34,6 @@ const processQueue = (error: unknown, token: string | null = null) => { client.interceptors.request.use( (config) => { const accessToken = useAuthStore.getState().accessToken; - if (config.url !== '/auth/reissue' && accessToken) { config.headers.Authorization = `Bearer ${accessToken}`; } diff --git a/src/apis/letterDetail.ts b/src/apis/letterDetail.ts index 0091069..d321438 100644 --- a/src/apis/letterDetail.ts +++ b/src/apis/letterDetail.ts @@ -1,28 +1,23 @@ import client from './client'; -const getLetter = async ( - letterId: string, - setLetterState: React.Dispatch>, - callBack?: () => void, -) => { +const getLetter = async (letterId: string) => { try { const res = await client.get(`/api/letters/${letterId}`); if (!res) throw new Error('편지 데이터를 가져오는 도중 에러가 발생했습니다.'); - setLetterState(res.data.data); - if (callBack) callBack(); console.log(res); + return res; } catch (error) { console.error(error); } }; -const deleteLetter = async (letterId: string, callBack?: () => void) => { +const deleteLetter = async (letterId: string) => { try { console.log(`/api/letters/${letterId}`); const res = await client.delete(`/api/letters/${letterId}`); if (!res) throw new Error('편지 삭제 요청 도중 에러가 발생했습니다.'); - if (callBack) callBack(); console.log(res); + return res; } catch (error) { console.error(error); } diff --git a/src/apis/randomLetter.ts b/src/apis/randomLetter.ts index 4742f21..994aff6 100644 --- a/src/apis/randomLetter.ts +++ b/src/apis/randomLetter.ts @@ -1,17 +1,70 @@ import client from './client'; -const getRandomLetters = async ( - setRandomLettersState: React.Dispatch>, - category: string | null, -) => { +const getRandomLetters = async (category: string | null) => { try { - const res = await client.get(`/api/random/${category}`); + const res = await client.get(`/api/random-letters/${category}`); if (!res) throw new Error('랜덤 편지 데이터를 가져오는 도중 에러가 발생했습니다.'); - setRandomLettersState(res.data.data); console.log(res); + return res; } catch (error) { console.error(error); } }; -export { getRandomLetters }; +const postRandomLettersApprove = async (approveRequest: ApproveRequest, callBack?: () => void) => { + try { + console.log('엔드포인트 : /api/random-letters/approve'); + console.log('request', approveRequest); + const res = await client.post('/api/random-letters/approve', approveRequest); + if (!res) throw new Error('랜덤편지 매칭수락 도중 에러가 발생했습니다.'); + if (callBack) callBack(); + return res; + } catch (error) { + console.error(error); + } +}; + +const getRandomLetterMatched = async (callBack?: () => void) => { + try { + const res = await client.post('/api/random-letters/valid-table'); + if (!res) + throw new Error('랜덤 편지 최종 매칭 시간 검증 데이터를 가자오는 도중 에러가 발생했습니다.'); + if (callBack) callBack(); + console.log(res); + return res; + } catch (error) { + console.error(error); + } +}; + +const getRandomLetterCoolTime = async (callBack?: () => void) => { + try { + const res = await client.post('/api/random-letters/valid'); + if (!res) + throw new Error('랜덤 편지 최종 매칭 시간 검증 데이터를 가자오는 도중 에러가 발생했습니다.'); + if (callBack) callBack(); + console.log(res); + return res; + } catch (error) { + console.error(error); + } +}; + +const deleteRandomLetterMatching = async () => { + try { + const res = await client.delete('/api/random-letters/matching/cancel'); + if (!res) throw new Error('매칭 취소 도중 에러가 발생했습니다.'); + console.log(res); + return res; + } catch (error) { + console.log(error); + } +}; + +export { + getRandomLetters, + postRandomLettersApprove, + getRandomLetterCoolTime, + getRandomLetterMatched, + deleteRandomLetterMatching, +}; diff --git a/src/apis/write.ts b/src/apis/write.ts index 2c56f8b..7046e1f 100644 --- a/src/apis/write.ts +++ b/src/apis/write.ts @@ -1,28 +1,37 @@ +// import { AxiosResponse } from 'axios'; import client from './client'; -const postLetter = async (data: LetterRequest, callBack?: () => void) => { +const postLetter = async (data: LetterRequest) => { try { const res = await client.post('/api/letters', data); - if (callBack) callBack(); + if (!res) throw new Error('편지 전송과정중에서 오류가 발생했습니다.'); + console.log(`api 주소 : /api/letters, 전송타입 : post`); + return res; + } catch (error) { + console.error(error); + } +}; + +const postFirstReply = async (data: FirstReplyRequest) => { + try { + const res = await client.post('/api/random-letters/matching', data); + if (!res) throw new Error('최초 답장 전송과정중에서 오류가 발생했습니다.'); + console.log(`api 주소 : /api/random-letters/matching, 전송타입 : post`); console.log(res); + return res; } catch (error) { console.error(error); } }; -const getPrevLetter = async ( - letterId: string, - setPrevLetterState: React.Dispatch>, - callBack?: () => void, -) => { +const getPrevLetter = async (letterId: string) => { try { const res = await client.get(`/api/letters/${letterId}/previous`); - setPrevLetterState(res.data.data); - if (callBack) callBack(); console.log(res); + return res; } catch (error) { console.error(error); } }; -export { postLetter, getPrevLetter }; +export { postLetter, postFirstReply, getPrevLetter }; diff --git a/src/components/ResultLetter.tsx b/src/components/ResultLetter.tsx index 7f1b131..36980d5 100644 --- a/src/components/ResultLetter.tsx +++ b/src/components/ResultLetter.tsx @@ -9,13 +9,13 @@ export default function ResultLetter({ }: { categoryName: Category; title: string; - zipCode?: string; + zipCode: string; }) { const date = new Date(); const today = `${date.getFullYear()}년 ${date.getMonth() + 1}월 ${date.getDate()}일`; return ( - +
diff --git a/src/layouts/Header.tsx b/src/layouts/Header.tsx index 2946314..53af0c8 100644 --- a/src/layouts/Header.tsx +++ b/src/layouts/Header.tsx @@ -1,13 +1,16 @@ -import { Link } from 'react-router'; +import { Link, useNavigate } from 'react-router'; import { AlarmIcon, ArrowLeftIcon, PersonIcon } from '@/assets/icons'; const Header = () => { // TODO: 뒤로 가기 버튼이 보이는 조건 추가 // TODO: 스크롤 발생 시, 어떻게 보여져야 하는지 + const navigate = useNavigate(); return (
- +
diff --git a/src/pages/LetterDetail/index.tsx b/src/pages/LetterDetail/index.tsx index cd9da97..4a66a91 100644 --- a/src/pages/LetterDetail/index.tsx +++ b/src/pages/LetterDetail/index.tsx @@ -39,16 +39,38 @@ const LetterDetailPage = () => { } setDegreeModalOpen(false); }; + + const handleDeleteLetter = async (letterId: string) => { + const res = await deleteLetter(letterId); + if (res?.status === 200) { + navigate(-1); + } else { + alert('편지 삭제 도중 오류 발생(임시)'); + } + }; + useEffect(() => { document.body.addEventListener('click', handleOutsideClick); + + const handleGetLetter = async (letterId: string) => { + const res = await getLetter(letterId); + if (res?.status === 200) { + setLetterDetail(res.data.data); + } else { + alert( + '에러가 발생했거나 존재하지 않거나 따숨님의 편지가 아니에요(임시) - 이거 에러코드 따른 처리 달리해야할듯', + ); + navigate(-1); + } + }; if (params.id) { - getLetter(params.id, setLetterDetail); + handleGetLetter(params.id); } return () => { document.body.removeEventListener('click', handleOutsideClick); }; - }, [params.id]); + }, [params.id, navigate]); return ( <> {reportModalOpen && setReportModalOpen(false)} />} @@ -137,7 +159,7 @@ const LetterDetailPage = () => { setDeleteModalOpen(false); }} onConfirm={() => { - if (params.id) deleteLetter(params.id); + if (params.id) handleDeleteLetter(params.id); navigate(-1); }} /> diff --git a/src/pages/RandomLetters/components/CoolTime.tsx b/src/pages/RandomLetters/components/CoolTime.tsx index 68882da..f5017cf 100644 --- a/src/pages/RandomLetters/components/CoolTime.tsx +++ b/src/pages/RandomLetters/components/CoolTime.tsx @@ -3,17 +3,22 @@ import { useNavigate } from 'react-router'; import LetterWrapper from '@/components/LetterWrapper'; import { formatNumber } from '@/utils/formatNumber'; +import { timeFormatter } from '@/utils/timeFormatter'; // import letterPink from '@/assets/images/letter-pink.png'; export default function CoolTime({ - setCoolTime, + setIsCoolTime, + coolTime, }: { - setCoolTime: React.Dispatch>; + setIsCoolTime: React.Dispatch>; + coolTime: CoolTime; }) { const navigate = useNavigate(); - const TIME_STAMP = '2025-02-26T22:13:25.262045608'; + const TIME_STAMP = coolTime?.lastMatchedAt + ? coolTime.lastMatchedAt + : '2025-03-01T21:15:25.262045608'; const COMPLETED_DATE = new Date(TIME_STAMP); @@ -24,56 +29,36 @@ export default function CoolTime({ const endTime = END_DATE.getTime() - NOW_DATE.getTime(); - const [endTimes, setEndTimes] = useState({ - hours: Math.floor((endTime / (1000 * 60 * 60)) % 24), - minutes: Math.floor((endTime / (1000 * 60)) % 60), - seconds: Math.floor((endTime / 1000) % 60), - }); + const [endTimeSeconds, setEndTimeSeconds] = useState(endTime / 1000); + + const formatedEndTime = timeFormatter(endTimeSeconds); useEffect(() => { - if (endTimes.hours < 0 || endTimes.minutes < 0 || endTimes.seconds < 0) { - setEndTimes({ hours: 0, minutes: 0, seconds: 0 }); - } - if (endTimes.hours === 0 && endTimes.minutes === 0 && endTimes.seconds === 0) { - setCoolTime(false); - return; - } - const endTimeFlow = setInterval(() => { - setEndTimes((currentTime) => { - if (currentTime.seconds > 0) { - return { ...currentTime, seconds: currentTime.seconds - 1 }; - } // - else { - if (currentTime.minutes > 0) { - return { ...currentTime, minutes: currentTime.minutes - 1, seconds: 59 }; - } // - else { - if (currentTime.hours > 0) { - return { hours: currentTime.hours - 1, minutes: 59, seconds: 59 }; - } // - else { - setCoolTime(false); - return { ...currentTime }; - } - } - } - }); - if (endTimes.hours === 0 && endTimes.minutes === 0 && endTimes.seconds === 0) { - clearInterval(endTimeFlow); + const endTargetTime = Date.now() + endTime; + + const count = setInterval(() => { + const now = Date.now(); + const newEndTimeSeconds = Math.max(0, Math.floor((endTargetTime - now) / 1000)); + + if (endTimeSeconds <= 0) { + setIsCoolTime(false); } + + setEndTimeSeconds(newEndTimeSeconds); }, 1000); return () => { - clearInterval(endTimeFlow); + clearInterval(count); }; - }, [endTimes, setCoolTime]); + }); + return (

랜덤 편지 활성화 까지

- {formatNumber(endTimes.hours)} : {formatNumber(endTimes.minutes)} :{' '} - {formatNumber(endTimes.seconds)} + {formatNumber(formatedEndTime.hours)} : {formatNumber(formatedEndTime.minutes)} :{' '} + {formatNumber(formatedEndTime.seconds)}

diff --git a/src/pages/RandomLetters/components/Matched.tsx b/src/pages/RandomLetters/components/Matched.tsx index 5825da9..0bc3857 100644 --- a/src/pages/RandomLetters/components/Matched.tsx +++ b/src/pages/RandomLetters/components/Matched.tsx @@ -1,137 +1,118 @@ import { useEffect, useState } from 'react'; +import { useNavigate } from 'react-router'; +import { deleteRandomLetterMatching } from '@/apis/randomLetter'; import ResultLetter from '@/components/ResultLetter'; import { formatNumber } from '@/utils/formatNumber'; +import { timeFormatter } from '@/utils/timeFormatter'; export default function Matched({ - setMatched, - setCoolTime, + matchedLetter, + setIsMatched, + setIsCoolTime, + setOpenSelectedDetailModal, }: { - setMatched: React.Dispatch>; - setCoolTime: React.Dispatch>; + matchedLetter: MatchedLetter; + setIsMatched: React.Dispatch>; + setIsCoolTime: React.Dispatch>; + setOpenSelectedDetailModal: React.Dispatch>; }) { - const [isDisabled, setIsDisabled] = useState(false); + const navigate = useNavigate(); - const TIME_STAMP = '2025-02-25T21:52:25.262045608'; + const [isDisabled, setIsDisabled] = useState(false); - const MATCHED_DATE = new Date(TIME_STAMP); + const TIME_STAMP = matchedLetter.replyDeadLine; + const MATCH_DURATION = 1000 * 60 * 60 * 24; + const MATCH_GRACE = 1000 * 60 * 5; - const END_DATE = new Date(MATCHED_DATE); - END_DATE.setHours(MATCHED_DATE.getHours() + 24); + const END_DATE = new Date(TIME_STAMP); - const GRACE_DATE = new Date(MATCHED_DATE); - GRACE_DATE.setMinutes(MATCHED_DATE.getMinutes() + 5); + const MATCHED_DATE = new Date(END_DATE.getTime() - MATCH_DURATION + MATCH_GRACE); const NOW_DATE = new Date(); const endTime = END_DATE.getTime() - NOW_DATE.getTime(); - const graceTime = GRACE_DATE.getTime() - NOW_DATE.getTime(); + const graceTime = MATCHED_DATE.getTime() - NOW_DATE.getTime(); - const [endTimes, setEndTimes] = useState({ - hours: Math.floor((endTime / (1000 * 60 * 60)) % 24), - minutes: Math.floor((endTime / (1000 * 60)) % 60), - seconds: Math.floor((endTime / 1000) % 60), - }); + const [endTimeSeconds, setEndTimeSeconds] = useState(Math.floor(endTime / 1000)); + const [graceTimeSeconds, setGraceTimeSeconds] = useState(Math.floor(graceTime / 1000)); - const [graceTimes, setGraceTimes] = useState({ - minutes: Math.floor((graceTime / (1000 * 60)) % 60), - seconds: Math.floor((graceTime / 1000) % 60), - }); + const FormatedEndTimes = timeFormatter(endTimeSeconds); + const FormatedGraceTimes = timeFormatter(graceTimeSeconds); - useEffect(() => { - if (endTimes.hours < 0 || endTimes.minutes < 0 || endTimes.seconds < 0) { - setEndTimes({ hours: 0, minutes: 0, seconds: 0 }); + const handleDeleteRandomLetterMatching = async () => { + const res = await deleteRandomLetterMatching(); + if (res?.status === 200) { + alert('매칭이 취소되었습니다.'); + navigate(-1); } - if (endTimes.hours === 0 && endTimes.minutes === 0 && endTimes.seconds === 0) { - setMatched(false); - setCoolTime(true); - return; - } - const endTimeFlow = setInterval(() => { - setEndTimes((currentTime) => { - if (currentTime.seconds > 0) { - return { ...currentTime, seconds: currentTime.seconds - 1 }; - } // - else { - if (currentTime.minutes > 0) { - return { ...currentTime, minutes: currentTime.minutes - 1, seconds: 59 }; - } // - else { - if (currentTime.hours > 0) { - return { hours: currentTime.hours - 1, minutes: 59, seconds: 59 }; - } // - else { - setMatched(false); - setCoolTime(true); - return { ...currentTime }; - } - } - } - }); - if (endTimes.hours === 0 && endTimes.minutes === 0 && endTimes.seconds === 0) { - clearInterval(endTimeFlow); - } - }, 1000); - - return () => { - clearInterval(endTimeFlow); - }; - }, [endTimes, setMatched, setCoolTime]); + }; useEffect(() => { - if (graceTimes.minutes < 0 || graceTimes.seconds < 0) { - setGraceTimes({ minutes: 0, seconds: 0 }); + if (endTime <= 0) { + setIsMatched(false); + setIsCoolTime(true); } - if (graceTimes.minutes === 0 && graceTimes.seconds === 0) { - return setIsDisabled(true); + if (graceTime <= 0) { + setIsDisabled(true); } - const graceTimeFlow = setInterval(() => { - setGraceTimes((currentTime) => { - if (currentTime.seconds > 0) { - return { ...currentTime, seconds: currentTime.seconds - 1 }; - } // - else { - if (currentTime.minutes > 0) { - return { minutes: currentTime.minutes - 1, seconds: 59 }; - } // - else { - setIsDisabled(true); - return { ...currentTime }; - } - } - }); - if (graceTimes.minutes === 0 && graceTimes.seconds === 0) { - clearInterval(graceTimeFlow); + + const endTargetTime = Date.now() + endTime; + const graceTargetTime = Date.now() + graceTime; + + const counting = setInterval(() => { + const now = Date.now(); + const newEndTimeSeconds = Math.max(0, Math.floor((endTargetTime - now) / 1000)); + const newGraceTimeSeconds = Math.max(0, Math.floor((graceTargetTime - now) / 1000)); + + if (newEndTimeSeconds <= 0) { + setIsMatched(false); + setIsCoolTime(true); + } + if (newGraceTimeSeconds <= 0) { + setIsDisabled(true); } + + setEndTimeSeconds(newEndTimeSeconds); + setGraceTimeSeconds(newGraceTimeSeconds); }, 1000); return () => { - clearInterval(graceTimeFlow); + clearInterval(counting); }; - }, [graceTimes]); + }, [endTime, graceTime, setIsMatched, setIsCoolTime]); return (

답장까지 남은 시간

- {formatNumber(endTimes.hours)} : {formatNumber(endTimes.minutes)} :{' '} - {formatNumber(endTimes.seconds)} + {formatNumber(FormatedEndTimes.hours)} : {formatNumber(FormatedEndTimes.minutes)} :{' '} + {formatNumber(FormatedEndTimes.seconds)}

-
- +
{ + setOpenSelectedDetailModal(true); + }} + > +
diff --git a/src/pages/RandomLetters/components/MatchedLetter.tsx b/src/pages/RandomLetters/components/MatchedLetter.tsx index bda4395..1e05d46 100644 --- a/src/pages/RandomLetters/components/MatchedLetter.tsx +++ b/src/pages/RandomLetters/components/MatchedLetter.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import { useNavigate } from 'react-router'; import { twMerge } from 'tailwind-merge'; @@ -6,20 +6,20 @@ import BackButton from '@/components/BackButton'; import ReportModal from '@/components/ReportModal'; import { FONT_TYPE_OBJ, PAPER_TYPE_OBJ } from '@/pages/Write/constants'; -const MatchedLetter = ({ selectedLetter }: { selectedLetter: RandomLetters }) => { +const MatchedLetter = ({ matchedLetter }: { matchedLetter: MatchedLetter }) => { const navigate = useNavigate(); - // 상대방의 우편번호도 데이터에 포함되어야 할 거 같음!!! - const [letterDetail] = useState(null); const [reportModalOpen, setReportModalOpen] = useState(false); + useEffect(() => {}, [matchedLetter]); + return ( <> {reportModalOpen && setReportModalOpen(false)} />}
@@ -27,22 +27,22 @@ const MatchedLetter = ({ selectedLetter }: { selectedLetter: RandomLetters }) =>
TO. 따숨이 - {selectedLetter?.title} + {matchedLetter?.title}
- FROM. {selectedLetter.zipCode} + FROM. {matchedLetter.zipCode}
- {DUMMY_LIST.map((list, idx) => { + {randomLetters.map((list, idx) => { return (
{ - setSelectedCategory(category.category); + if (category.category) { + setSelectedCategory(category.category); + } }} className={twMerge( `body-b text-gray-60 rounded-full bg-white px-3 py-1.5`, diff --git a/src/pages/RandomLetters/components/MatchingSelectModal.tsx b/src/pages/RandomLetters/components/MatchingSelectModal.tsx index 9d634b4..c7d22c5 100644 --- a/src/pages/RandomLetters/components/MatchingSelectModal.tsx +++ b/src/pages/RandomLetters/components/MatchingSelectModal.tsx @@ -1,17 +1,29 @@ // import { useNavigate } from 'react-router'; +import { postRandomLettersApprove } from '@/apis/randomLetter'; import ModalOverlay from '@/components/ModalOverlay'; import ResultLetter from '@/components/ResultLetter'; function MatchingSelectModal({ setOpenModal, selectedLetter, + setMatchedLetter, setOpenSelectedDetailModal, }: { setOpenModal: React.Dispatch>; selectedLetter: RandomLetters; + setMatchedLetter: React.Dispatch>; setOpenSelectedDetailModal: React.Dispatch>; }) { + const handlePostRandomLettersApprove = async (approveRequest: ApproveRequest) => { + const res = await postRandomLettersApprove(approveRequest); + if (res?.status === 200) { + setOpenModal(false); + // MEMO : 이제 랜덤 편지 승인하기 데이터에 랜덤 편지 최종 매칭 시간 검증과 동일한 response 값이 담겨서 그 값을 matchedLetter의 상태 업데이트 값으로 사용하면 됨 + setMatchedLetter(res.data.data); + setOpenSelectedDetailModal(true); + } + }; // const navigate = useNavigate(); return ( @@ -39,9 +51,10 @@ function MatchingSelectModal({