diff --git a/eslint.config.js b/eslint.config.js index 3cc2bdc..567dc87 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -31,6 +31,7 @@ export default tseslint.config( ...reactHooks.configs.recommended.rules, 'react-refresh/only-export-components': ['warn', { allowConstantExport: true }], '@tanstack/query/exhaustive-deps': 'error', + '@typescript-eslint/no-empty-object-type': off, 'import/order': [ 'error', { diff --git a/index.html b/index.html index ff314cf..3c08226 100644 --- a/index.html +++ b/index.html @@ -1,10 +1,10 @@ - + - + - Vite + React + TS + 36.5 =0.10.0'} + event-source-polyfill@1.0.31: + resolution: {integrity: sha512-4IJSItgS/41IxN5UVAVuAyczwZF7ZIEsM1XAoUzIHA6A+xzusEZUutdXz2Nr+MQPLxfTiCvqE79/C8HT8fKFvA==} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -3137,6 +3149,8 @@ snapshots: '@types/estree@1.0.6': {} + '@types/event-source-polyfill@1.0.5': {} + '@types/json-schema@7.0.15': {} '@types/json5@0.0.29': {} @@ -3741,6 +3755,8 @@ snapshots: esutils@2.0.3: {} + event-source-polyfill@1.0.31: {} + fast-deep-equal@3.1.3: {} fast-glob@3.3.3: diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..2eaefa2 Binary files /dev/null and b/public/favicon.ico differ diff --git a/public/vite.svg b/public/vite.svg deleted file mode 100644 index e7b8dfb..0000000 --- a/public/vite.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index 29e7dfa..5a958a3 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -8,6 +8,7 @@ import AdminPage from './pages/Admin'; import FilteredLetterManage from './pages/Admin/FilteredLetter'; import FilteringManage from './pages/Admin/Filtering'; import ReportManage from './pages/Admin/Report'; +import AdminRollingPaper from './pages/Admin/RollingPaper'; import AuthCallbackPage from './pages/Auth'; import Home from './pages/Home'; import Landing from './pages/Landing'; @@ -25,6 +26,7 @@ import OnboardingPage from './pages/Onboarding'; import RandomLettersPage from './pages/RandomLetters'; import RollingPaperPage from './pages/RollingPaper'; import WritePage from './pages/Write'; +import ShareApprovalPage from './pages/Share'; const App = () => { useViewport(); @@ -36,10 +38,10 @@ const App = () => { } /> } /> } /> - } /> } /> }> + } /> }> } /> @@ -55,6 +57,7 @@ const App = () => { } /> } /> + } /> }> } /> @@ -69,6 +72,7 @@ const App = () => { } /> } /> } /> + } /> diff --git a/src/apis/admin.ts b/src/apis/admin.ts index c5152e7..5eb1580 100644 --- a/src/apis/admin.ts +++ b/src/apis/admin.ts @@ -3,9 +3,8 @@ import client from './client'; const postReports = async (postReportRequest: PostReportRequest) => { try { const res = await client.post(`/api/reports`, postReportRequest); - if (res.status === 200) { - return res; - } + if (!res) throw new Error('신고 요청중 에러가 발생했습니다.'); + return res; } catch (error) { console.error(error); } @@ -51,25 +50,22 @@ const getBadWords = async (setBadWords: React.Dispatch void) => { +const postBadWords = async (badWordsRequest: BadWords) => { try { const res = await client.post('/api/bad-words', badWordsRequest); - if (callBack) callBack(); console.log(res); + if (!res) throw new Error('금칙어 등록 도중 에러가 발생했습니다.'); + return res; } catch (error) { console.error(error); } }; // 내 상상대로 만든 필터링 단어 취소 버튼 -const patchBadWords = async ( - badWordId: number, - badWordsRequest: BadWords, - callBack?: () => void, -) => { +const patchBadWords = async (badWordId: number) => { try { - const res = await client.patch(`/api/bad-words/${badWordId}/status`, badWordsRequest); - if (callBack) callBack(); + const res = await client.patch(`/api/bad-words/${badWordId}/status`, { isUsed: false }); + if (!res) throw new Error('검열 단어 삭제 도중 에러가 발생했습니다.'); console.log(res); } catch (error) { console.error(error); diff --git a/src/apis/auth.ts b/src/apis/auth.ts index 064fa5d..c963f64 100644 --- a/src/apis/auth.ts +++ b/src/apis/auth.ts @@ -33,6 +33,7 @@ export const getNewToken = async () => { try { const response = await client.post('/api/reissue', {}, { withCredentials: true }); if (!response) throw new Error('getNewToken: no response data'); + console.log(response.data); return response; } catch (error) { console.error(error); diff --git a/src/apis/client.ts b/src/apis/client.ts index df8b3b3..426b7c4 100644 --- a/src/apis/client.ts +++ b/src/apis/client.ts @@ -9,32 +9,13 @@ const client = axios.create({ headers: { 'Content-Type': 'application/json' }, }); -// type FailedRequest = { -// resolve: (token: string) => void; -// reject: (error: unknown) => void; -// }; - let isRefreshing = false; -// let failedQueue: FailedRequest[] = []; - -// const processQueue = (error: unknown, token: string | null = null) => { -// failedQueue.forEach((prom) => { -// if (error) { -// prom.reject(error); -// } else { -// if (token) { -// prom.resolve(token); -// } -// } -// }); - -// failedQueue = []; -// }; const callReissue = async () => { try { const response = await getNewToken(); - const newToken = response?.data.accessToken; + if(response?.status !== 200) throw new Error('error while fetching newToken'); + const newToken = response?.data.data.accessToken; return newToken; } catch (e) { return Promise.reject(e); @@ -45,11 +26,10 @@ let retry = false; client.interceptors.request.use( (config) => { - console.log('response again', config); - const accessToken = useAuthStore.getState().accessToken; - if (config.url !== '/auth/reissue' && accessToken) { + if (config.url !== '/api/reissue' && accessToken) { config.headers.Authorization = `Bearer ${accessToken}`; + console.log('interceptor', config); } return config; }, @@ -74,38 +54,21 @@ client.interceptors.response.use( retry = true; if (isRefreshing) { if (isLoggedIn) logout(); - // try { - // return new Promise((resolve, reject) => { - // failedQueue.push({ - // resolve: (token: string) => { - // originalRequest.headers.Authorization = `Bearer ${token}`; - // resolve(client(originalRequest)); - // }, - // reject: (err: unknown) => reject(err), - // }); - // }); - // } catch (e) { - // return Promise.reject(e); - // } } else { isRefreshing = true; try { const newToken = await callReissue(); setAccessToken(newToken); - // processQueue(null, newToken); isRefreshing = false; originalRequest.headers.Authorization = `Bearer ${newToken}`; return client(originalRequest); } catch (e) { - // processQueue(e, null); isRefreshing = false; if (isLoggedIn) logout(); return Promise.reject(e); } } } - if (isLoggedIn) logout(); - console.error('Failed to refresh token', error); return Promise.reject(error); }, ); diff --git a/src/apis/draftLetters.ts b/src/apis/draftLetters.ts index 424853e..02c43c0 100644 --- a/src/apis/draftLetters.ts +++ b/src/apis/draftLetters.ts @@ -2,32 +2,40 @@ import client from './client'; export interface DraftLetter { letterId: number; - writerId: number; + matchingId: number; receiverId: number; parentLetterId: number; - zipCode: string; title: string; content: string; category: string; paperType: string; fontType: string; - deliveryStartedAt: string; - deliveryCompletedAt: string; - matched: boolean; } -export const getDraftLetters = async () // token: string -: Promise => { +export const getDraftLetters = async (): Promise => { try { - const { data } = await client.get('/api/letters?status=draft', { - // headers: { - // Authorization: `Bearer ${token}`, - // }, - }); + const { data } = await client.get('/api/letters?status=draft'); console.log('임시저장된 편지 데이터', data); return data.data; } catch (error) { - console.error(`❌임시저장된 편지를 불러오던 중 에러가 발생했습니다`, error); + console.error('❌임시저장된 편지를 불러오던 중 에러가 발생했습니다', error); throw new Error('임시저장된 편지 불러오기 실패'); } }; + +export const deleteDraftLetters = async (letterId: number) => { + try { + const { data } = await client.delete(`/api/letters/${letterId}/temporary-save`); + + if (data.data?.letterId) { + console.log('삭제된 임시저장 편지 ID:', data.data.letterId); + } else { + console.error('❌서버 응답에 letterId가 존재하지 않습니다.'); + } + + return data.data.letterId; + } catch (error) { + console.error('❌임시저장된 편지를 삭제하던 중 에러가 발생했습니다:', error); + throw error; + } +}; diff --git a/src/apis/incomingLetters.ts b/src/apis/incomingLetters.ts index adde539..66a865e 100644 --- a/src/apis/incomingLetters.ts +++ b/src/apis/incomingLetters.ts @@ -1,13 +1,9 @@ import client from './client'; -export const getIncomingLetters = async (token: string) => { +export const getIncomingLetters = async () => { try { - const { data } = await client.get('/api/letters?status=delivery', { - headers: { - Authorization: `Bearer ${token}`, - }, - }); - console.log('불러온 데이터', data); + const { data } = await client.get('/api/letters?status=delivery'); + console.log('오고있는 편지 데이터', data); return data; } catch (error) { console.error('❌오고 있는 편지 목록을 불러오던 중 에러 발생', error); diff --git a/src/apis/letterDetail.ts b/src/apis/letterDetail.ts index d321438..6a72b66 100644 --- a/src/apis/letterDetail.ts +++ b/src/apis/letterDetail.ts @@ -23,4 +23,17 @@ const deleteLetter = async (letterId: string) => { } }; -export { getLetter, deleteLetter }; +const postEvaluateLetter = async (letterId: number, evaluation: LetterEvaluation) => { + try { + const res = await client.post(`/api/letters/${letterId}/evaluate`, { + evaluation: evaluation, + }); + if (!res) throw new Error('편지 삭제 요청 도중 에러가 발생했습니다.'); + console.log(res); + return res; + } catch (error) { + console.error(error); + } +}; + +export { getLetter, deleteLetter, postEvaluateLetter }; diff --git a/src/apis/mailBox.ts b/src/apis/mailBox.ts index 722e248..e0b9aca 100644 --- a/src/apis/mailBox.ts +++ b/src/apis/mailBox.ts @@ -13,7 +13,7 @@ export const getMailbox = async () => { export const getMailboxDetail = async (id: number, pageParam: number) => { try { const response = await client.get(`/api/mailbox/${id}/detail?page=${pageParam}&size=20`); - + console.log(response.data); if (!response) throw new Error('error while fetching mailbox detail data'); return response.data; } catch (error) { diff --git a/src/apis/myPage.ts b/src/apis/myPage.ts index 7d64cc9..638ebc1 100644 --- a/src/apis/myPage.ts +++ b/src/apis/myPage.ts @@ -12,7 +12,7 @@ export const fetchMyPageInfo = async () => { export const getMySharePostList = async () => { try { - const response = await client.get('/api/share-proposals/inbox'); + const response = await client.get('/api/share-posts/me'); if (!response) throw new Error('error while fetching my share post list'); return response.data; } catch (error) { diff --git a/src/apis/notification.ts b/src/apis/notification.ts new file mode 100644 index 0000000..869df0a --- /dev/null +++ b/src/apis/notification.ts @@ -0,0 +1,34 @@ +import client from './client'; + +const getTimeLines = async () => { + try { + const res = await client.get('/api/timelines'); + if (!res) throw new Error('타임라인을 받아오는 도중 오류가 발생했습니다.'); + console.log(res); + return res; + } catch (error) { + console.error(error); + } +}; + +const patchReadNotification = async (timelineId: number) => { + try { + const res = await client.patch(`/api/notifications/${timelineId}/read`); + if (!res) throw new Error('편지 개별 읽음 처리를 하는 도중 오류가 발생했습니다.'); + return res; + } catch (error) { + console.error(error); + } +}; + +const patchReadNotificationAll = async () => { + try { + const res = await client.patch(`/api/notifications/read`); + if (!res) throw new Error('편지 개별 읽음 처리를 하는 도중 오류가 발생했습니다.'); + return res; + } catch (error) { + console.error(error); + } +}; + +export { getTimeLines, patchReadNotification, patchReadNotificationAll }; diff --git a/src/apis/randomLetter.ts b/src/apis/randomLetter.ts index 994aff6..515a1d8 100644 --- a/src/apis/randomLetter.ts +++ b/src/apis/randomLetter.ts @@ -4,7 +4,6 @@ const getRandomLetters = async (category: string | null) => { try { const res = await client.get(`/api/random-letters/${category}`); if (!res) throw new Error('랜덤 편지 데이터를 가져오는 도중 에러가 발생했습니다.'); - console.log(res); return res; } catch (error) { console.error(error); @@ -13,8 +12,6 @@ const getRandomLetters = async (category: string | null) => { 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(); @@ -30,7 +27,6 @@ const getRandomLetterMatched = async (callBack?: () => void) => { if (!res) throw new Error('랜덤 편지 최종 매칭 시간 검증 데이터를 가자오는 도중 에러가 발생했습니다.'); if (callBack) callBack(); - console.log(res); return res; } catch (error) { console.error(error); @@ -43,7 +39,6 @@ const getRandomLetterCoolTime = async (callBack?: () => void) => { if (!res) throw new Error('랜덤 편지 최종 매칭 시간 검증 데이터를 가자오는 도중 에러가 발생했습니다.'); if (callBack) callBack(); - console.log(res); return res; } catch (error) { console.error(error); @@ -54,7 +49,6 @@ 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); diff --git a/src/apis/rolling.ts b/src/apis/rolling.ts index 8930c0d..0e60b6e 100644 --- a/src/apis/rolling.ts +++ b/src/apis/rolling.ts @@ -9,10 +9,18 @@ export const getCurrentRollingPaper = async (): Promise export const getRollingPaperDetail = async ( rollingPaperId: string | number, + page: number, + size: number, ): Promise => { const { data: { data }, - } = await client.get(`/api/event-posts/${rollingPaperId}`); + } = await client.get(`/api/event-posts/${rollingPaperId}`, { + params: { + page, + size, + }, + }); + console.log(data); return data; }; @@ -36,3 +44,58 @@ export const deleteRollingPaperComment = async (commentId: string | number) => { throw error; } }; + +export const postNewRollingPaper = async (title: string) => { + try { + const { + data: { data }, + } = await client.post('/api/admin/event-posts', { title }); + return data; + } catch (error) { + console.error(error); + throw error; + } +}; + +export const getRollingPaperList = async ( + page: string | number, + size: number, +): Promise => { + try { + const { + data: { data }, + } = await client.get('/api/admin/event-posts', { + params: { + page, + size, + }, + }); + return data; + } catch (error) { + console.error(error); + throw error; + } +}; + +export const deleteRollingPaper = async (eventPostId: number | string) => { + try { + const { data } = await client.delete(`/api/admin/event-posts/${eventPostId}`); + return data; + } catch (error) { + console.error(error); + throw error; + } +}; + +export const patchRollingPaper = async (eventPostId: number | string) => { + try { + const { + data: { data }, + } = await client.patch(`/api/admin/event-posts/${eventPostId}/status`); + console.log(data); + return data; + } catch (error) { + console.error(error); + throw error; + } +}; diff --git a/src/apis/share.ts b/src/apis/share.ts index 767b97e..37a3087 100644 --- a/src/apis/share.ts +++ b/src/apis/share.ts @@ -21,7 +21,7 @@ export interface SharePost { letters: ShareLetter[]; } -// 페이징 포함 +// 공유 게시글 목록 조회 - 페이징 포함 export interface SharePostResponse { content: SharePost[]; currentPage: number; @@ -30,8 +30,35 @@ export interface SharePostResponse { totalPages: number; } +// 편지 공유 요청 수신 조회 +export interface ShareProposal { + shareProposalId: number; + requesterZipCode: string; + recipientZipCode: string; + message: string; + status: 'REJECTED' | 'APPROVED' | 'PENDING'; +} + +//편지 공유 요청 상세 조회 +export interface ShareProposalLetter { + id: number; + content: string; + writerZipCode: string; + receiverZipCode: string; + createdAt: string; +} + +export interface ShareProposalDetail { + shareProposalId: number; + requesterZipCode: string; + recipientZipCode: string; + message: string; + status: 'PENDING' | 'ACCEPTED' | 'REJECTED'; + letters: ShareProposalLetter[]; +} + // 편지 공유 수락 / 거절 -export interface SharePostApproval { +export interface ShareProposalApproval { shareProposalId: number; status: 'APPROVED' | 'REJECTED'; sharePostId: number; @@ -53,7 +80,7 @@ export const getSharePostList = async (page: number = 1, size: number = 10) => { }; // 공유 게시글 상세 조회 -export const getSharePostDetail = async (sharePostId: number): Promise => { +export const getSharePostDetail = async (sharePostId: string): Promise => { try { const response = await client.get(`/api/share-posts/${sharePostId}`); console.log(`🔥공유 게시글 상세 데이터`, response.data); @@ -84,13 +111,40 @@ export const postShareProposals = async ( } }; +// 편지 공유 요청 수신 조회 +export const getShareProposalList = async () => { + try { + const response = await client.get('/api/share-proposals/inbox'); + console.log(`🌟공유 요청 목록`, response.data); + + return response.data.data; + } catch (error) { + console.error('❌ 편지 공유 요청을 조회하던 중 에러가 발생했습니다', error); + throw error; + } +}; + +// 편지 공유 요청 상세 조회 +export const getShareProposalDetail = async ( + shareProposalId: number, +): Promise => { + try { + const response = await client.get(`/api/share-proposals/${shareProposalId}`); + console.log(`😎공유 요청 상세 조회 데이터 `, response.data); + return response.data.data; + } catch (error) { + console.error('❌ 편지 공유 요청을 상세 조회하던 중 에러가 발생했습니다', error); + throw error; + } +}; + // 편지 공유 수락 / 거절 export const postShareProposalApproval = async ( shareProposalId: number, action: 'approve' | 'reject', -): Promise => { +): Promise => { try { - const response = await client.patch(`/api/share-proposal/${shareProposalId}/${action}`); + const response = await client.patch(`/api/share-proposals/${shareProposalId}/${action}`); return response.data; } catch (error) { console.error( @@ -102,7 +156,7 @@ export const postShareProposalApproval = async ( }; // 편지 좋아요 추가, 취소 -export const postSharePostLike = async (sharePostId: number) => { +export const postSharePostLike = async (sharePostId: string) => { try { const response = await client.post(`/api/share-posts/${sharePostId}/likes`); if (!response) throw new Error('error while posting like'); @@ -114,7 +168,7 @@ export const postSharePostLike = async (sharePostId: number) => { }; // 편지 좋아요 갯수 -export const getSharePostLikeCount = async (sharePostId: number) => { +export const getSharePostLikeCount = async (sharePostId: string) => { try { const response = await client.get(`/api/share-posts/${sharePostId}/likes`); if (!response) throw new Error('error while fetching likes'); diff --git a/src/apis/write.ts b/src/apis/write.ts index 7046e1f..2a93094 100644 --- a/src/apis/write.ts +++ b/src/apis/write.ts @@ -1,11 +1,10 @@ -// import { AxiosResponse } from 'axios'; import client from './client'; const postLetter = async (data: LetterRequest) => { + console.log('request', data); try { const res = await client.post('/api/letters', data); - if (!res) throw new Error('편지 전송과정중에서 오류가 발생했습니다.'); - console.log(`api 주소 : /api/letters, 전송타입 : post`); + if (!res) throw new Error('편지 전송과정에서 오류가 발생했습니다.'); return res; } catch (error) { console.error(error); @@ -13,11 +12,10 @@ const postLetter = async (data: LetterRequest) => { }; const postFirstReply = async (data: FirstReplyRequest) => { + console.log('Firstrequest', data); 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); + if (!res) throw new Error('최초 답장 전송과정에서 오류가 발생했습니다.'); return res; } catch (error) { console.error(error); @@ -27,11 +25,21 @@ const postFirstReply = async (data: FirstReplyRequest) => { const getPrevLetter = async (letterId: string) => { try { const res = await client.get(`/api/letters/${letterId}/previous`); - console.log(res); + if (!res) throw new Error('이전편지를 불러오는중 오류가 발생했습니다.'); return res; } catch (error) { console.error(error); } }; -export { postLetter, postFirstReply, getPrevLetter }; +const postTemporarySave = async (data: TemporaryRequest) => { + try { + const res = client.post(`/api/letters/temporary-save`, data); + if (!res) throw new Error('편지 임시저장과정에서 오류가 발생했습니다.'); + return res; + } catch (error) { + console.error(error); + } +}; + +export { postLetter, postFirstReply, getPrevLetter, postTemporarySave }; diff --git a/src/components/BackgroundBottom.tsx b/src/components/BackgroundBottom.tsx index 88389fd..6f8e830 100644 --- a/src/components/BackgroundBottom.tsx +++ b/src/components/BackgroundBottom.tsx @@ -6,7 +6,7 @@ const BackgroundBottom = () => { return ( ); diff --git a/src/components/ConfirmModal.tsx b/src/components/ConfirmModal.tsx index 2b4f8aa..194a6f5 100644 --- a/src/components/ConfirmModal.tsx +++ b/src/components/ConfirmModal.tsx @@ -22,7 +22,6 @@ const ConfirmModal = ({ onCancel, onConfirm, }: ConfirmModalProps) => { - // TODO: 전역 상태로 관리해야하는지 고민 return (
diff --git a/src/components/NoticeRollingPaper.tsx b/src/components/NoticeRollingPaper.tsx index b495b0e..7c80fe3 100644 --- a/src/components/NoticeRollingPaper.tsx +++ b/src/components/NoticeRollingPaper.tsx @@ -1,4 +1,5 @@ import { useQuery } from '@tanstack/react-query'; +import { useEffect, useRef, useState } from 'react'; import { Link } from 'react-router'; import { twMerge } from 'tailwind-merge'; @@ -6,13 +7,41 @@ import { getCurrentRollingPaper } from '@/apis/rolling'; import { NoticeIcon } from '@/assets/icons'; const NoticeRollingPaper = () => { - const { data } = useQuery({ + const { data, error } = useQuery({ queryKey: ['notice-rolling-paper'], queryFn: () => getCurrentRollingPaper(), }); + const [activeAnimate, setActiveAnimate] = useState(false); + const containerRef = useRef(null); + const textRef = useRef(null); + + useEffect(() => { + if (data?.title) { + const containerElement = containerRef.current; + const element = textRef.current; + + if (containerElement && element) { + const textWidth = element.scrollWidth; + const containerWidth = containerElement.offsetWidth; + + if (textWidth > containerWidth) { + const animationDuration = (textWidth / 10) * 0.3; + const totalDuration = Math.max(animationDuration, 10); + document.documentElement.style.setProperty('--marquee-duration', `${totalDuration}s`); + + setActiveAnimate(true); + } else { + setActiveAnimate(false); + } + } + } + }, [data?.title]); + const noticeText = data?.title; + if (error || !noticeText) return null; + return (
{ )} > -
-

{noticeText}

+
+

+ {noticeText} +

diff --git a/src/components/ReportModal.tsx b/src/components/ReportModal.tsx index be47d81..41ce17f 100644 --- a/src/components/ReportModal.tsx +++ b/src/components/ReportModal.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react'; +import { useState } from 'react'; import { twMerge } from 'tailwind-merge'; import { postReports } from '@/apis/admin'; @@ -8,7 +8,7 @@ import TextareaField from './TextareaField'; interface ReportModalProps { reportType: ReportType; - letterId: number | null; + letterId: number; onClose: () => void; } @@ -42,6 +42,7 @@ const ReportModal = ({ reportType, letterId, onClose }: ReportModalProps) => { const res = await postReports(postReportRequest); if (res?.status === 200) { alert('신고 처리되었습니다.'); + console.log(res); onClose(); } else if (res?.status === 409) { alert('신고한 이력이 있습니다.'); @@ -49,13 +50,6 @@ const ReportModal = ({ reportType, letterId, onClose }: ReportModalProps) => { } }; - useEffect(() => { - if (!postReportRequest.letterId) { - alert('신고 모달을 여는 과정에서 오류가 발생했습니다. 새로고침을 눌러주세요'); - onClose(); - } - }); - return ( state.toastObjects); + + if (toastObjects.length <= 0) return; + return ( + <> + {toastObjects.map((toastObj, index) => ( + + ))} + + ); +} diff --git a/src/components/ToastItem.tsx b/src/components/ToastItem.tsx new file mode 100644 index 0000000..2465e5b --- /dev/null +++ b/src/components/ToastItem.tsx @@ -0,0 +1,54 @@ +import useToastStore from '@/stores/toastStore'; +import { useEffect } from 'react'; +import { twMerge } from 'tailwind-merge'; + +interface ToastObj { + time: number; + toastType: 'Warning' | 'Success' | 'Error' | 'Info'; + position: 'Top' | 'Bottom'; + title: string; + onClick?: () => void; +} +export default function ToastItem({ toastObj, index }: { toastObj: ToastObj; index: number }) { + const setToastUnActive = useToastStore((state) => state.setToastUnActive); + + const TOAST_DESIGN = { + Warning: { style: 'bg-primary-4', imoji: '⚠️' }, + Success: { style: 'bg-[#DFFFDA] text-[#000000]', imoji: '✅' }, + Error: { style: 'bg-[#FFDCD8] text-[#FF0000]', imoji: '🚨' }, + Info: { style: 'bg-[#FFFFFF]', imoji: '📫' }, + }; + + const TOAST_POSITION = { + Top: 'top-20', + Bottom: 'bottom-5', + }; + + const animation = `toast-blink ${toastObj.time}s ease-in-out forwards`; + const toastStyle = twMerge( + 'fixed bottom-5 left-1/2 z-40 flex h-[40px] max-w-150 min-w-[300px] w-[100%] -translate-1/2 items-center justify-center rounded-lg caption-sb shadow-[0_1px_6px_rgba(200,200,200,0.2)]', + TOAST_POSITION[toastObj.position], + TOAST_DESIGN[toastObj.toastType].style, + ); + + const activeTime = toastObj.time * 1000; + useEffect(() => { + const closeToast = setTimeout(() => { + setToastUnActive(index); + }, activeTime); + + return () => clearTimeout(closeToast); + }); + return ( +
{ + setToastUnActive(index); + if (toastObj.onClick) toastObj.onClick(); + }} + > + {`${TOAST_DESIGN[toastObj.toastType].imoji} ${toastObj.title} ${TOAST_DESIGN[toastObj.toastType].imoji}`} +
+ ); +} diff --git a/src/hooks/useServerSentEvents.tsx b/src/hooks/useServerSentEvents.tsx new file mode 100644 index 0000000..1ad9850 --- /dev/null +++ b/src/hooks/useServerSentEvents.tsx @@ -0,0 +1,72 @@ +import { EventSourcePolyfill } from 'event-source-polyfill'; +import { useEffect, useRef } from 'react'; + +import useAuthStore from '@/stores/authStore'; +import useToastStore from '@/stores/toastStore'; +import { useNavigate } from 'react-router'; + +export const useServerSentEvents = () => { + const navigate = useNavigate(); + + const accessToken = useAuthStore((state) => state.accessToken); + const sourceRef = useRef(null); + + const setToastActive = useToastStore((state) => state.setToastActive); + + useEffect(() => { + if (!accessToken) { + console.log('로그인 정보 확인불가'); + return; + } + + const connectSSE = () => { + try { + console.log('구독 시작'); + sourceRef.current = new EventSourcePolyfill( + `${import.meta.env.VITE_API_URL}/api/notifications/sub`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }, + ); + + sourceRef.current.onmessage = (event) => { + console.log(event); + console.log('알림 수신'); + setToastActive({ + toastType: 'Info', + title: '새 알림이 도착했어요!', + position: 'Top', + time: 5, + onClick: () => navigate('/mypage/notifications'), + }); + }; + + sourceRef.current.onerror = (error) => { + console.log(error); + console.log('에러 발생함'); + closeSSE(); + // 재연결 로직 추가 가능 + setTimeout(connectSSE, 5000); // 5초 후 재연결 시도 + }; + } catch (error) { + console.error(error); + } + }; + + connectSSE(); + + return () => { + console.log('컴포넌트 언마운트로 인한 구독해제'); + closeSSE(); + }; + }, [accessToken]); + + const closeSSE = () => { + sourceRef.current?.close(); + sourceRef.current = null; + }; + + // return { closeSSE }; +}; diff --git a/src/layouts/PrivateRoute.tsx b/src/layouts/PrivateRoute.tsx index bc78b4a..a607485 100644 --- a/src/layouts/PrivateRoute.tsx +++ b/src/layouts/PrivateRoute.tsx @@ -2,8 +2,11 @@ import { useEffect, useState } from 'react'; import { useNavigate, Outlet } from 'react-router'; import useAuthStore from '@/stores/authStore'; +import { useServerSentEvents } from '@/hooks/useServerSentEvents'; +import Toast from '@/components/Toast'; export default function PrivateRoute() { + useServerSentEvents(); const isLoggedIn = useAuthStore((state) => state.isLoggedIn); const navigate = useNavigate(); const [shouldRender, setShouldRender] = useState(false); @@ -20,5 +23,10 @@ export default function PrivateRoute() { return null; } - return ; + return ( + <> + + + + ); } diff --git a/src/pages/Admin/Report.tsx b/src/pages/Admin/Report.tsx index 0838c4c..d107ef6 100644 --- a/src/pages/Admin/Report.tsx +++ b/src/pages/Admin/Report.tsx @@ -5,6 +5,7 @@ import { AlarmIcon } from '@/assets/icons'; import AdminPageTitle from './components/AdminPageTitle'; import ListHeaderFrame from './components/ListHeaderFrame'; +import PagenationNavigation from './components/PagenationNavigation'; import ReportDetailModal from './components/ReportDetailModal'; import ReportHandlingModal from './components/ReportHandlingModal'; import ReportListItem from './components/ReportListItem'; @@ -19,28 +20,37 @@ export default function ReportManage() { currentPage: '1', totalPages: '0', }); + const [selectedReport, setSelectReport] = useState(null); const [selectedReportId, setSelectedReportId] = useState(null); - // const [allReports, setAllReports] = useState(); - const [reportQueryString, setReportQueryString] = useState({ reportType: null, status: 'PENDING', page: '1', - size: '3', + size: '1', }); + const handleGetReports = async (reportQueryString: ReportQueryString) => { const res = await getReports(reportQueryString); if (res?.status === 200) { - console.log(res.data.data.content); - setReports(res.data.data.content); + const data = res.data.data; + setReports(data.content); setReportPages(() => ({ - currentPage: res.data.data.currentPage, - totalPages: res.data.data.totalPages, + currentPage: data.currentPage, + totalPages: data.totalPages, })); } }; + + const handleNowPage = (page: string) => { + setReportQueryString((cur) => ({ ...cur, page: page })); + }; + + const handleStatus = (status: Status) => { + setReportQueryString((cur) => ({ ...cur, status: status })); + }; + useEffect(() => { handleGetReports(reportQueryString); }, [reportQueryString]); @@ -48,7 +58,19 @@ export default function ReportManage() { <> 검열 관리 / 신고 편지 목록 - +
+ + +
@@ -68,39 +90,11 @@ export default function ReportManage() { setSelectReport={setSelectReport} /> ))} -
-
- - - {reportPages.currentPage}/{reportPages.totalPages} - - -
-
+
{detailModalOpen && ( diff --git a/src/pages/Admin/RollingPaper.tsx b/src/pages/Admin/RollingPaper.tsx index a855c2b..0c99dec 100644 --- a/src/pages/Admin/RollingPaper.tsx +++ b/src/pages/Admin/RollingPaper.tsx @@ -1,18 +1,36 @@ +import { useQuery } from '@tanstack/react-query'; import { useState } from 'react'; -import { AddIcon, AlarmIcon, DeleteIcon } from '@/assets/icons'; +import { getRollingPaperList } from '@/apis/rolling'; +import { AddIcon, AlarmIcon } from '@/assets/icons'; import AddRollingPaperModal from './components/AddRollingPaperModal'; import PageTitle from './components/AdminPageTitle'; +import RollingPaperItem from './components/RollingPaperItem'; import WrapperFrame from './components/WrapperFrame'; import WrapperTitle from './components/WrapperTitle'; +import PagenationNavigation from './components/PagenationNavigation'; + +const SIZE = 10; export default function AdminRollingPaper() { const [activeModal, setActiveModal] = useState(false); + const [currentPage, setCurrentPage] = useState('1'); + const { data, isLoading, isSuccess, refetch } = useQuery({ + queryKey: ['admin-rolling-paper', currentPage], + queryFn: () => getRollingPaperList(currentPage ?? 1, SIZE), + }); + + const handleNowPage = (page: string) => { + setCurrentPage(page); + refetch(); + }; return ( <> - {activeModal && setActiveModal(false)} />} + {activeModal && ( + setActiveModal(false)} /> + )} 게시판 관리 / 롤링 페이퍼 설정
@@ -26,55 +44,40 @@ export default function AdminRollingPaper() { 롤링페이퍼 생성
- - - - - - - - - - - - - - - - - - - - - - - - - - -
ID제목쌓인 편지 수상태
1 - 침수 피해를 복구중인 포스코 임직원 분들에게 응원의 메시지를 보내주세요! - 12 - - 진행 중 - -
2 - 침수 피해를 복구중인 포스코 임직원 분들에게 응원의 메시지를 보내주세요! - 12 - - - -
+ {isLoading &&

Loading...

} + {isSuccess && ( + <> + + + + + + + + + + + {data.content.map((rollingPaper) => ( + + ))} + +
ID제목상태
+ {data.content.length === 0 && ( + + 아직 생성된 롤링페이퍼가 없어요 + + )} + + )} +
); diff --git a/src/pages/Admin/components/AddInputButton.tsx b/src/pages/Admin/components/AddInputButton.tsx index 8b04232..72ac896 100644 --- a/src/pages/Admin/components/AddInputButton.tsx +++ b/src/pages/Admin/components/AddInputButton.tsx @@ -19,12 +19,13 @@ export default function AddInputButton({ target.style.width = `${target.scrollWidth}px`; }; - const handlePostBadWords = () => { + const handlePostBadWords = async () => { if (inputText.word === '') return setAddInputShow(false); - postBadWords(inputText, () => { + const res = await postBadWords(inputText); + if (res?.status === 200) { setBadWords((cur) => [...cur, inputText]); setAddInputShow(false); - }); + } }; useEffect(() => { diff --git a/src/pages/Admin/components/AddRollingPaperModal.tsx b/src/pages/Admin/components/AddRollingPaperModal.tsx index 2df17c3..e6f86bc 100644 --- a/src/pages/Admin/components/AddRollingPaperModal.tsx +++ b/src/pages/Admin/components/AddRollingPaperModal.tsx @@ -1,14 +1,31 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; import { ChangeEvent, FormEvent, useState } from 'react'; +import { postNewRollingPaper } from '@/apis/rolling'; import ModalOverlay from '@/components/ModalOverlay'; interface AddRollingPaperModalProps { + currentPage: number | string; onClose: () => void; } -export default function AddRollingPaperModal({ onClose }: AddRollingPaperModalProps) { +export default function AddRollingPaperModal({ currentPage, onClose }: AddRollingPaperModalProps) { const [title, setTitle] = useState(''); const [error, setError] = useState(''); + const queryClient = useQueryClient(); + + const { mutate } = useMutation({ + mutationFn: () => postNewRollingPaper(title), + onSuccess: () => { + setTitle(''); + setError(''); + onClose(); + queryClient.invalidateQueries({ queryKey: ['admin-rolling-paper', currentPage] }); + }, + onError: () => { + setError('편지 작성에 실패했어요. 다시 시도해주세요.'); + }, + }); const handleChange = (e: ChangeEvent) => { setTitle(e.target.value); @@ -21,7 +38,7 @@ export default function AddRollingPaperModal({ onClose }: AddRollingPaperModalPr return; } - console.log(title); + mutate(); }; return ( diff --git a/src/pages/Admin/components/PagenationNavigation.tsx b/src/pages/Admin/components/PagenationNavigation.tsx new file mode 100644 index 0000000..44a86a4 --- /dev/null +++ b/src/pages/Admin/components/PagenationNavigation.tsx @@ -0,0 +1,94 @@ +import { useState } from 'react'; +import { twMerge } from 'tailwind-merge'; + +interface PagenationNavigation { + totalPage: number; + buttonLength: number; + handlePageNumberButtonClick: (page: string) => void; +} +export default function PagenationNavigation({ + totalPage, + buttonLength, + handlePageNumberButtonClick, +}: PagenationNavigation) { + const totalSection = Math.ceil(totalPage / buttonLength) - 1; + const [nowSection, setNowSection] = useState(0); + const [nowPageNumberAt, setNowPageNumberAt] = useState(1); + + // 네비게이션 시작점, 끝점 + const navigationRange = { + start: nowSection * buttonLength + 1, + end: nowSection * buttonLength + buttonLength, + }; + + // 페이지 버튼 배열 + const pageNumberButtonArray = Array.from( + { length: navigationRange.end - navigationRange.start + 1 }, + (_, index) => navigationRange.start + index, + ); + + // 페이지 버튼 클릭시 해당 번호값이 파라미터에 담김 + const handlePageButtonClick = (page: number) => { + const pageString = page.toString(); + handlePageNumberButtonClick(pageString); + setNowPageNumberAt(page); + }; + + const handlePrevButtonClick = () => { + if (nowSection > 0) { + const prev = (nowSection - 1) * buttonLength + buttonLength; + setNowSection((cur) => cur - 1); + handlePageButtonClick(prev); + } + }; + + const handleNextButtonClick = () => { + if (nowSection < totalSection) { + const next = (nowSection + 1) * buttonLength + 1; + setNowSection((cur) => cur + 1); + handlePageButtonClick(next); + } + }; + + const buttonStyle = + 'rounded-full bg-white w-8 h-8 disabled:bg-gray-20 disabled:text-white disabled:cursor-auto'; + + return ( +
+
+ + {pageNumberButtonArray.map((num) => { + if (totalPage < num) return null; + return ( + + ); + })} + +
+
+ ); +} diff --git a/src/pages/Admin/components/ReportHandlingModal.tsx b/src/pages/Admin/components/ReportHandlingModal.tsx index bfe6562..b2c9f8b 100644 --- a/src/pages/Admin/components/ReportHandlingModal.tsx +++ b/src/pages/Admin/components/ReportHandlingModal.tsx @@ -21,7 +21,7 @@ export default function ReportHandlingModal({ ); }; - const [reportRequest, setReportRequest] = useState({ + const [reportRequest, setReportRequest] = useState({ status: 'RESOLVED', adminMemo: '', }); diff --git a/src/pages/Admin/components/RollingPaperItem.tsx b/src/pages/Admin/components/RollingPaperItem.tsx new file mode 100644 index 0000000..58cdfc2 --- /dev/null +++ b/src/pages/Admin/components/RollingPaperItem.tsx @@ -0,0 +1,90 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { AxiosError } from 'axios'; + +import { deleteRollingPaper, patchRollingPaper } from '@/apis/rolling'; +import { DeleteIcon } from '@/assets/icons'; +import { useState } from 'react'; +import ConfirmModal from '@/components/ConfirmModal'; + +interface RollingPaperItemProps { + information: AdminRollingPaperInformation; + currentPage: string | number; +} + +export default function RollingPaperItem({ information, currentPage }: RollingPaperItemProps) { + const [activeDeleteModal, setActiveDeleteModal] = useState(false); + const queryClient = useQueryClient(); + + const { mutate: deleteMutate } = useMutation({ + mutationFn: () => deleteRollingPaper(information.eventPostId), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['admin-rolling-paper', currentPage] }); + }, + onError: (err) => { + console.error(err); + }, + }); + + const { mutate: toggleStatus } = useMutation({ + mutationFn: () => patchRollingPaper(information.eventPostId), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['admin-rolling-paper', currentPage] }); + }, + onError: (err: AxiosError<{ code: string; message: string }>) => { + if (err.response?.data.code === 'EVENT-004') { + alert(err.response.data.message); + } + console.error(err); + }, + }); + + return ( + <> + {activeDeleteModal && ( + { + setActiveDeleteModal(false); + }} + onConfirm={deleteMutate} + /> + )} + + {information.eventPostId} + +
+ {information.used && ( + + 진행 중 + + )} + {information.title} +
+ + + + + + {!information.used && ( + + )} + + + + ); +} diff --git a/src/pages/Auth/index.tsx b/src/pages/Auth/index.tsx index 0596a77..cd892d5 100644 --- a/src/pages/Auth/index.tsx +++ b/src/pages/Auth/index.tsx @@ -7,6 +7,7 @@ import useAuthStore from '@/stores/authStore'; const AuthCallbackPage = () => { const stateToken = new URLSearchParams(window.location.search).get('state'); const redirectURL = new URLSearchParams(window.location.search).get('redirect'); + const error = new URLSearchParams(window.location.search).get('error'); const login = useAuthStore((state) => state.login); const logout = useAuthStore((state) => state.logout); @@ -32,6 +33,8 @@ const AuthCallbackPage = () => { login(); if (userInfo.accessToken) setAccessToken(userInfo.accessToken); + console.log(redirectURL); + switch (redirectURL) { case 'home': { @@ -67,6 +70,9 @@ const AuthCallbackPage = () => { useEffect(() => { if (!stateToken) { navigate('/notFound'); + if (error === 'deleted_member') { + alert('탈퇴한 회원입니다.'); + } return; } diff --git a/src/pages/Home/components/RandomCheer.tsx b/src/pages/Home/components/RandomCheer.tsx index 83e96c6..5d3ac5a 100644 --- a/src/pages/Home/components/RandomCheer.tsx +++ b/src/pages/Home/components/RandomCheer.tsx @@ -25,6 +25,7 @@ const RandomCheer = () => { src={randomCheerBird} alt="random cheer bird" className="h-[26.5px] w-[21px] opacity-80" + onClick={() => setRandomCheer(getRandomCheer())} />
); diff --git a/src/pages/Home/components/ShowDraftModal.tsx b/src/pages/Home/components/ShowDraftModal.tsx index 3cb9060..aa74e51 100644 --- a/src/pages/Home/components/ShowDraftModal.tsx +++ b/src/pages/Home/components/ShowDraftModal.tsx @@ -1,8 +1,8 @@ import DeleteOutlineRoundedIcon from '@mui/icons-material/DeleteOutlineRounded'; import React, { useEffect, useState } from 'react'; -// import { useNavigate } from 'react-router'; +import { useNavigate } from 'react-router'; -import { DraftLetter, getDraftLetters } from '@/apis/draftLetters'; +import { DraftLetter, getDraftLetters, deleteDraftLetters } from '@/apis/draftLetters'; import ModalBackgroundWrapper from '@/components/ModalBackgroundWrapper'; import ModalOverlay from '@/components/ModalOverlay'; @@ -14,15 +14,15 @@ interface ShowDraftModalProps { const ShowDraftModal = ({ onClose }: ShowDraftModalProps) => { const [draftLetters, setDraftLetters] = useState([]); - // const navigate = useNavigate(); + const navigate = useNavigate(); - // const handleNavigation = (incomingId: number) => { - // navigate(`/board/letter/${incomingId}`, { - // state: { isShareLetterPreview: false }, - // }); - // }; + const handleNavigation = (draft: DraftLetter) => { + navigate(`/letter/write/?letterId=${draft.letterId}`, { + state: { draft, isDraft: true }, + }); + }; - useEffect(() => { + const handleGetDraftLetters = () => { getDraftLetters() .then((data) => { setDraftLetters(data || []); @@ -30,6 +30,21 @@ const ShowDraftModal = ({ onClose }: ShowDraftModalProps) => { .catch((error) => { console.error('❌ 임시저장된 편지를 불러오는데 실패했습니다', error); }); + }; + + const handleDeleteDraftLetters = async (letterId: number) => { + //TODO: 정말로 삭제하시겠습니까? 모달창 + try { + await deleteDraftLetters(letterId); + setDraftLetters((prev) => prev.filter((letter) => letter.letterId !== letterId)); + console.log(`letterId는 `, letterId); + } catch (error) { + console.error(`❌임시저장된 편지를 삭제하던 중 에러가 발생했습니다.`, error); + } + }; + + useEffect(() => { + handleGetDraftLetters(); }, [onClose]); return ( @@ -42,21 +57,31 @@ const ShowDraftModal = ({ onClose }: ShowDraftModalProps) => {

임시저장 편지

-

로그아웃 시 임시 저장된 편지는 사라집니다

- {draftLetters.map((draft) => ( -
handleNavigation(draft.letterId)} - > -

{draft.title}

-
- + {draftLetters.length > 0 ? ( + draftLetters.map((draft) => ( +
handleNavigation(draft)} + > +

{draft.title}

+
{ + e.stopPropagation(); + handleDeleteDraftLetters(draft.letterId); + }} + > + +
-
- ))} + )) + ) : ( +

작성 중인 편지가 없어요

+ )}
diff --git a/src/pages/Home/components/ShowIncomingLettersModal.tsx b/src/pages/Home/components/ShowIncomingLettersModal.tsx index 219015b..27513c9 100644 --- a/src/pages/Home/components/ShowIncomingLettersModal.tsx +++ b/src/pages/Home/components/ShowIncomingLettersModal.tsx @@ -29,15 +29,19 @@ const ShowIncomingLettersModal = ({ onClose }: ShowIncomingLettersModalProps) =>

시간은 실제 시간을 기반으로 책정됩니다.

- {data.map((letter) => ( -
-

{letter.title}

-

{letter.remainingTime}

-
- ))} + {data.length > 0 ? ( + data.map((letter) => ( +
+

{letter.title}

+

{letter.remainingTime}

+
+ )) + ) : ( +

오고 있는 편지가 없어요

+ )}
diff --git a/src/pages/Home/components/ShowShareAccessModal.tsx b/src/pages/Home/components/ShowShareAccessModal.tsx index e1e40df..f66576e 100644 --- a/src/pages/Home/components/ShowShareAccessModal.tsx +++ b/src/pages/Home/components/ShowShareAccessModal.tsx @@ -1,8 +1,10 @@ import React, { useEffect, useState } from 'react'; import { useNavigate } from 'react-router'; -import { getSharePostDetail, getSharePostList } from '@/apis/share'; -import { SharePostResponse } from '@/apis/share'; +import { getShareProposalDetail } from '@/apis/share'; +import { getShareProposalList } from '@/apis/share'; +import { ShareProposal } from '@/apis/share'; + import ModalBackgroundWrapper from '@/components/ModalBackgroundWrapper'; import ModalOverlay from '@/components/ModalOverlay'; @@ -14,26 +16,23 @@ interface ShowShareAccessModalProps { const ShowShareAccessModal = ({ onClose }: ShowShareAccessModalProps) => { const navigate = useNavigate(); - const [sharePosts, setSharePosts] = useState(); + const [shareProposals, setShareProposals] = useState([]); useEffect(() => { - const fetchPosts = async () => { - try { - const data = await getSharePostList(1, 10); - setSharePosts(data); - } catch (error) { - console.error('❌ 게시글 목록을 불러오는 데 실패했습니다.', error); - } - }; - - fetchPosts(); + getShareProposalList() + .then((data) => { + setShareProposals(data || []); + }) + .catch((error) => { + console.error('❌ 공유 요청 목록을 불러오는 데 실패했습니다.', error); + }); }, []); - const handleNavigation = async (sharePostId: number) => { + const handleNavigation = async (shareProposalId: number) => { try { - const postDetail = await getSharePostDetail(sharePostId); - navigate(`/board/letter/${sharePostId}`, { - state: { postDetail, isShareLetterPreview: true }, + const proposalDetail = await getShareProposalDetail(shareProposalId); + navigate(`/board/share/${shareProposalId}`, { + state: { proposalDetail }, }); } catch (error) { console.error('❌ 게시글 상세 페이지로 이동하는 데에 실패했습니다.', error); @@ -56,15 +55,19 @@ const ShowShareAccessModal = ({ onClose }: ShowShareAccessModalProps) => {

- {sharePosts?.content.map((post) => ( - - ))} + {shareProposals.length > 0 ? ( + shareProposals.map((proposal) => ( + + )) + ) : ( +

새로운 공유 요청이 없어요

+ )}
diff --git a/src/pages/LetterBoard/index.tsx b/src/pages/LetterBoard/index.tsx index 80d26a7..b6d4511 100644 --- a/src/pages/LetterBoard/index.tsx +++ b/src/pages/LetterBoard/index.tsx @@ -17,11 +17,15 @@ const LetterBoardPage = () => { const fetchPostList = async (page: number = 1) => { try { const response = await getSharePostList(page); - if (!response) throw new Error('게시글 목록을 불러오는데 실패했습니다.'); + if (!response || !response.content) { + console.error('게시글 목록을 불러오는데 실패했습니다.'); + return { content: [], currentPage: page, totalPages: 1 }; + } console.log('page', response); return response as SharePostResponse; } catch (e) { console.error(e); + return { content: [], currentPage: page, totalPages: 1 }; } }; @@ -32,7 +36,7 @@ const LetterBoardPage = () => { enabled: true, initialPageParam: 1, getNextPageParam: (res) => { - if (!res || res.currentPage >= res.totalPages) { + if (!res || !res?.content || res?.currentPage >= res?.totalPages) { return undefined; } return res.currentPage + 1; @@ -41,7 +45,7 @@ const LetterBoardPage = () => { gcTime: 1000 * 60 * 10, }); - const postLists = data?.pages.flatMap((page) => page?.content) || []; + const postLists = data?.pages?.flatMap((page) => page?.content || []) || []; useEffect(() => { if (!hasNextPage) return; @@ -56,7 +60,7 @@ const LetterBoardPage = () => { return ( <> -
+
<> 게시판 @@ -65,22 +69,30 @@ const LetterBoardPage = () => {

{isLoading ? ( -

loading

+

로딩 중 입니다.

+ ) : postLists ? ( + postLists?.length > 0 ? ( +
+ {postLists?.map((item, index) => { + return ( + + ); + })} +
+ ) : ( +

게시글이 없습니다.

+ ) ) : ( -
- {postLists.map((item, index) => { - return ( - - ); - })} -
+

+ 오류가 발생했습니다. 다시 한 번 시도해주세요 +

)}
diff --git a/src/pages/LetterBoardDetail/index.tsx b/src/pages/LetterBoardDetail/index.tsx index 3f8292d..f106647 100644 --- a/src/pages/LetterBoardDetail/index.tsx +++ b/src/pages/LetterBoardDetail/index.tsx @@ -25,69 +25,85 @@ const LetterBoardDetailPage = ({ confirmDisabled }: ShareLetterPreviewProps) => const [isLike, setIsLike] = useState(false); const isWriter = false; const [activeReportModal, setActiveReportModal] = useState(false); + const sharePostId: string = location.pathname.split('/')[3]; + // const location = useLocation(); + const navigate = useNavigate(); + // const isShareLetterPreview = location.state?.isShareLetterPreview || false; + const isShareLetterPreview = false; + const [postDetail, setPostDetail] = useState(); + + const postLike = async () => { + try { + const response = await postSharePostLike(sharePostId); + if (!response) throw new Error('error while fetching like count'); + console.log('✅ 편지 좋아요 추가됨:', response); + } catch (error) { + console.error('❌ 편지 좋아요 추가 중 에러가 발생했습니다', error); + throw new Error('편지 좋아요 추가 실패'); + } + }; const handleToggleLike = () => { setLikeCount((prev) => prev + (isLike ? -1 : 1)); setIsLike((prev) => !prev); + postLike(); }; - const location = useLocation(); - const navigate = useNavigate(); + const handleProposalApproval = async ( + action: 'approve' | 'reject', + shareProposalId: number = location.state?.postDetail?.sharePostId, + ) => { + try { + const result = await postShareProposalApproval(shareProposalId, action); + console.log(`✅ 편지 공유 ${action === 'approve' ? '수락' : '거절'}됨:`, result); - const isShareLetterPreview = location.state?.isShareLetterPreview || false; - const [postDetail, setPostDetail] = useState(); + navigate('/'); + } catch (error) { + console.error(error); + } + }; useEffect(() => { - const { sharePostId } = location.state.postDetail; - const fetchPostDetail = async (postId: number) => { + const fetchPostDetail = async (postId: string) => { try { - console.log('sharePostId:', postId); - const data = await getSharePostDetail(postId); - setPostDetail(data); } catch (error) { console.error('❌ 공유 게시글 상세 조회에 실패했습니다.', error); } }; - const fetchLikeCounts = async (postId: number) => { + const fetchLikeCounts = async (postId: string) => { try { const response = await getSharePostLikeCount(postId); if (!response) throw new Error('error while fetching like count'); - console.log(response); - setLikeCount(response.data.likeCount); + console.log('✅ 편지 좋아요 갯수:', response); + setLikeCount(response.likeCount); + setIsLike(response.liked); } catch (error) { console.error('❌ 편지 좋아요 갯수를 가져오는 중 에러가 발생했습니다', error); throw new Error('편지 좋아요 갯수 가져오기 실패'); } }; - if (location.state?.postDetail) { - fetchPostDetail(sharePostId); - fetchLikeCounts(sharePostId); - } else { - console.warn('postDetail not found in location.state'); - } - }, [location.state]); - - const handleProposalApproval = async ( - action: 'approve' | 'reject', - shareProposalId: number = location.state?.postDetail?.sharePostId, - ) => { - try { - const result = await postShareProposalApproval(shareProposalId, action); - console.log(`✅ 편지 공유 ${action === 'approve' ? '수락' : '거절'}됨:`, result); - - navigate('/'); - } catch (error) { - console.error(error); - } - }; + // if (location.state?.postDetail) { + fetchPostDetail(sharePostId); + fetchLikeCounts(sharePostId); + // } else { + // console.warn('postDetail not found in location.state'); + // } + // }, [location.state]); + }, []); return ( <> - {activeReportModal && setActiveReportModal(false)} />} + {activeReportModal && ( + setActiveReportModal(false)} + /> + )}
{ isLoading, isError, } = useQuery({ - queryKey: ['mailbox'], + queryKey: ['mailBox'], queryFn: fetchMailLists, staleTime: 1000 * 60 * 5, gcTime: 1000 * 60 * 10, diff --git a/src/pages/LetterBoxDetail/index.tsx b/src/pages/LetterBoxDetail/index.tsx index 262cc03..ee02012 100644 --- a/src/pages/LetterBoxDetail/index.tsx +++ b/src/pages/LetterBoxDetail/index.tsx @@ -1,4 +1,4 @@ -import { useMutation, useInfiniteQuery } from '@tanstack/react-query'; +import { useMutation, useInfiniteQuery, useQueryClient } from '@tanstack/react-query'; import { ChangeEvent, useEffect, useState } from 'react'; import { useInView } from 'react-intersection-observer'; import { useLocation, useNavigate } from 'react-router'; @@ -11,6 +11,9 @@ import PageTitle from '@/components/PageTitle'; import InformationTooltip from './components/InformationTooltip'; import LetterPreview from './components/LetterPreview'; + +import useToastStore from '@/stores/toastStore'; + interface MailBoxDetailProps { letterId: number; title: string; @@ -28,6 +31,8 @@ const LetterBoxDetailPage = () => { const [isOpenShareModal, setIsOpenShareModal] = useState(false); const [selected, setSelected] = useState([]); const [shareComment, setShareComment] = useState(''); + const queryClient = useQueryClient(); + const setToastActive = useToastStore((state) => state.setToastActive); const navigate = useNavigate(); @@ -63,10 +68,19 @@ const LetterBoxDetailPage = () => { mutationFn: async () => await postMailboxDisconnect(userInfo.id), onSuccess: () => { navigate(-1); + setToastActive({ + toastType: 'Success', + title: '차단 완료 되었습니다.', + time: 5, + }); + queryClient.invalidateQueries({ queryKey: ['mailBox'] }); }, onError: (error) => { - // TODO: 차단 실패 toastUI 띄워주기 - // 요청이 실패했어요 잠시 후에 다시 시도해주세요. + setToastActive({ + toastType: 'Error', + title: '차단이 실패했습니다. 잠시 후에 다시 시도해주세요.', + time: 5, + }); console.error(error); }, }); @@ -76,10 +90,16 @@ const LetterBoxDetailPage = () => { onSuccess: () => { toggleShareMode(); setShareComment(''); + setToastActive({ + toastType: 'Success', + title: '공유 완료 되었습니다.', + }); }, onError: (error) => { - // TODO: 차단 실패 toastUI 띄워주기 - // 요청이 실패했어요 잠시 후에 다시 시도해주세요. + setToastActive({ + toastType: 'Error', + title: '공유가 실패했습니다. 잠시 후에 다시 시도해주세요.', + }); console.error(error); }, }); diff --git a/src/pages/LetterDetail/components/DegreeSelector.tsx b/src/pages/LetterDetail/components/DegreeSelector.tsx new file mode 100644 index 0000000..86ae0b7 --- /dev/null +++ b/src/pages/LetterDetail/components/DegreeSelector.tsx @@ -0,0 +1,61 @@ +import { postEvaluateLetter } from '@/apis/letterDetail'; +import { CloudIcon, SnowIcon, WarmIcon } from '@/assets/icons'; + +interface DegreeSelector { + letterDetail: LetterDetail | null; + setLetterDetail: React.Dispatch>; +} +export default function DegreeSelector({ letterDetail, setLetterDetail }: DegreeSelector) { + const handlePostEvaluateLetter = async ( + letterId: number | undefined, + evaluation: LetterEvaluation, + ) => { + if (!letterId) return alert('편지id값이 담겨있지 않습니다.'); + const res = await postEvaluateLetter(letterId, evaluation); + if (res?.status === 200) { + console.log('평가완료'); + setLetterDetail((cur) => ({ ...cur, evaluated: true })); + } + }; + const DEGREES = [ + { + icon: , + title: '따뜻해요', + onClick: () => { + handlePostEvaluateLetter(letterDetail?.letterId, 'GOOD'); + }, + }, + { + icon: , + title: '그럭저럭', + onClick: () => { + handlePostEvaluateLetter(letterDetail?.letterId, 'SOSO'); + }, + }, + { + icon: , + title: '앗! 차가워', + onClick: () => { + handlePostEvaluateLetter(letterDetail?.letterId, 'BAD'); + }, + }, + ]; + return ( +
+ {DEGREES.map((degree, idx) => { + return ( + + ); + })} +
+ ); +} diff --git a/src/pages/LetterDetail/components/LetterDetailContent.tsx b/src/pages/LetterDetail/components/LetterDetailContent.tsx new file mode 100644 index 0000000..4e7761d --- /dev/null +++ b/src/pages/LetterDetail/components/LetterDetailContent.tsx @@ -0,0 +1,26 @@ +import { twMerge } from 'tailwind-merge'; + +import { FONT_TYPE_OBJ } from '@/pages/Write/constants'; + +interface LetterDetailContent { + letterDetail: LetterDetail; +} +export default function LetterDetailContent({ letterDetail }: LetterDetailContent) { + return ( + <> +
+ TO. 따숨이 + {letterDetail.title} +
+ + FROM. {letterDetail.zipCode} + + ); +} diff --git a/src/pages/LetterDetail/components/LetterDetailDegreeButton.tsx b/src/pages/LetterDetail/components/LetterDetailDegreeButton.tsx new file mode 100644 index 0000000..769aa51 --- /dev/null +++ b/src/pages/LetterDetail/components/LetterDetailDegreeButton.tsx @@ -0,0 +1,50 @@ +import { useEffect, useRef } from 'react'; + +import { ThermostatIcon } from '@/assets/icons'; + +interface LetterDetailDegreeButton { + letterDetail: LetterDetail | null; + setDegreeModalOpen: React.Dispatch>; +} +export default function LetterDetailDegreeButton({ + letterDetail, + setDegreeModalOpen, +}: LetterDetailDegreeButton) { + const degreeButtonRef = useRef(null); + + useEffect(() => { + const handleOutsideClick = (event: MouseEvent) => { + const target = event.target as Node; + if (!target || degreeButtonRef.current?.contains(target)) { + return; + } + setDegreeModalOpen(false); + }; + + document.body.addEventListener('click', handleOutsideClick); + + return () => { + document.body.removeEventListener('click', handleOutsideClick); + }; + }, [setDegreeModalOpen]); + return ( + <> + {letterDetail?.evaluated ? ( +
+ 온도 측정된 편지에요! +
+ ) : ( + + )} + + ); +} diff --git a/src/pages/LetterDetail/components/LetterDetailHeader.tsx b/src/pages/LetterDetail/components/LetterDetailHeader.tsx new file mode 100644 index 0000000..17cc18c --- /dev/null +++ b/src/pages/LetterDetail/components/LetterDetailHeader.tsx @@ -0,0 +1,60 @@ +import { useState } from 'react'; + +import { DeleteIcon, SirenOutlinedIcon } from '@/assets/icons'; +import BackButton from '@/components/BackButton'; +import useAuthStore from '@/stores/authStore'; + +import DegreeSelector from './DegreeSelector'; +import LetterDetailDegreeButton from './LetterDetailDegreeButton'; + +interface LetterDetailHeader { + letterDetail: LetterDetail; + setLetterDetail: React.Dispatch>; + setDeleteModalOpen: React.Dispatch>; + setReportModalOpen: React.Dispatch>; +} +export default function LetterDetailHeader({ + letterDetail, + setLetterDetail, + setDeleteModalOpen, + setReportModalOpen, +}: LetterDetailHeader) { + const [degreeModalOpen, setDegreeModalOpen] = useState(false); + + const userZipCode = useAuthStore((state) => state.zipCode); + + return ( +
+ +
+ {userZipCode !== letterDetail?.zipCode && ( + + )} + {userZipCode === letterDetail?.zipCode && ( + + )} + {userZipCode !== letterDetail?.zipCode && ( + + )} + {degreeModalOpen && ( + + )} +
+
+ ); +} diff --git a/src/pages/LetterDetail/components/LetterDetailReplyButton.tsx b/src/pages/LetterDetail/components/LetterDetailReplyButton.tsx new file mode 100644 index 0000000..60c8ad3 --- /dev/null +++ b/src/pages/LetterDetail/components/LetterDetailReplyButton.tsx @@ -0,0 +1,19 @@ +import { useNavigate } from 'react-router'; + +interface LetterDetailReplyButton { + letterDetail: LetterDetail; +} +export default function LetterDetailReplyButton({ letterDetail }: LetterDetailReplyButton) { + const navigate = useNavigate(); + return ( + + ); +} diff --git a/src/pages/LetterDetail/index.tsx b/src/pages/LetterDetail/index.tsx index 88a40a4..2a34606 100644 --- a/src/pages/LetterDetail/index.tsx +++ b/src/pages/LetterDetail/index.tsx @@ -1,61 +1,47 @@ -import { useEffect, useRef, useState } from 'react'; +import { useEffect, useState } from 'react'; import { useNavigate, useParams } from 'react-router'; import { twMerge } from 'tailwind-merge'; import { deleteLetter, getLetter } from '@/apis/letterDetail'; -import { - CloudIcon, - DeleteIcon, - SirenOutlinedIcon, - SnowIcon, - ThermostatIcon, - WarmIcon, -} from '@/assets/icons'; -import BackButton from '@/components/BackButton'; import ConfirmModal from '@/components/ConfirmModal'; import ReportModal from '@/components/ReportModal'; -import { FONT_TYPE_OBJ, PAPER_TYPE_OBJ } from '@/pages/Write/constants'; +import { PAPER_TYPE_OBJ } from '@/pages/Write/constants'; +import useAuthStore from '@/stores/authStore'; + +import LetterDetailContent from './components/LetterDetailContent'; +import LetterDetailHeader from './components/LetterDetailHeader'; +import LetterDetailReplyButton from './components/LetterDetailReplyButton'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; const LetterDetailPage = () => { const params = useParams(); const navigate = useNavigate(); - // 상대방의 우편번호도 데이터에 포함되어야 할 거 같음!!! - const [letterDetail, setLetterDetail] = useState(null); + const queryClient = useQueryClient(); + + const [letterDetail, setLetterDetail] = useState({} as LetterDetail); + const userZipCode = useAuthStore((state) => state.zipCode); - const DEGREES = [ - { icon: , title: '따뜻해요' }, - { icon: , title: '그럭저럭' }, - { icon: , title: '앗! 차가워' }, - ]; - const [degreeModalOpen, setDegreeModalOpen] = useState(false); const [reportModalOpen, setReportModalOpen] = useState(false); const [deleteModalOpen, setDeleteModalOpen] = useState(false); - const degreeButtonRef = useRef(null); - const handleOutsideClick = (event: MouseEvent) => { - const target = event.target as Node; - if (!target || degreeButtonRef.current?.contains(target)) { - return; - } - setDegreeModalOpen(false); - }; - - const handleDeleteLetter = async (letterId: string) => { - const res = await deleteLetter(letterId); - if (res?.status === 200) { + const { mutate: handleDeleteLetter } = useMutation({ + mutationFn: (letterId: string) => deleteLetter(letterId), + onSuccess: () => { navigate(-1); - } else { + queryClient.invalidateQueries({ queryKey: ['mailBoxDetail'] }); + queryClient.invalidateQueries({ queryKey: ['mailBox'] }); + }, + onError: () => { 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); + const data: LetterDetail = res.data.data; + setLetterDetail(data); } else { alert( '에러가 발생했거나 존재하지 않거나 따숨님의 편지가 아니에요(임시) - 이거 에러코드 따른 처리 달리해야할듯', @@ -66,17 +52,15 @@ const LetterDetailPage = () => { if (params.id) { handleGetLetter(params.id); } - - return () => { - document.body.removeEventListener('click', handleOutsideClick); - }; }, [params.id, navigate]); + + if (!letterDetail) return <>; return ( <> {reportModalOpen && ( setReportModalOpen(false)} /> )} @@ -86,75 +70,16 @@ const LetterDetailPage = () => { letterDetail && PAPER_TYPE_OBJ[letterDetail.paperType], )} > -
- -
- - - - {degreeModalOpen && ( -
- {DEGREES.map((degree, idx) => { - return ( - - ); - })} -
- )} -
-
-
- TO. 따숨이 - {letterDetail?.title} -
- - FROM. {'12E12'} - + + + {userZipCode !== letterDetail?.zipCode && ( + + )} {deleteModalOpen && ( { }} onConfirm={() => { if (params.id) handleDeleteLetter(params.id); - navigate(-1); }} /> )} diff --git a/src/pages/MyPage/components/MyBoardPage.tsx b/src/pages/MyPage/components/MyBoardPage.tsx index 388934f..a4c3759 100644 --- a/src/pages/MyPage/components/MyBoardPage.tsx +++ b/src/pages/MyPage/components/MyBoardPage.tsx @@ -2,7 +2,7 @@ import { useQuery } from '@tanstack/react-query'; import { useNavigate } from 'react-router'; import { twMerge } from 'tailwind-merge'; -import { getMySharePostList } from '@/apis/mypage'; +import { getMySharePostList } from '@/apis/myPage'; import BackgroundBottom from '@/components/BackgroundBottom'; import PageTitle from '@/components/PageTitle'; @@ -14,11 +14,14 @@ const MyBoardPage = () => { const fetchMyPostList = async () => { try { const response = await getMySharePostList(); - if (!response) throw new Error('게시글 목록을 불러오는데 실패했습니다.'); - console.log(response); - return response.data; + if (!response) { + throw new Error('게시글 목록을 불러오는데 실패했습니다.'); + } + console.log('myPostList', response); + return response.data as SharePost[]; } catch (e) { console.error(e); + return []; } }; @@ -39,23 +42,24 @@ const MyBoardPage = () => { } return ( <> -
+
내가 올린 게시물 {isLoading ? ( -

loading

- ) : ( +

로딩 중 입니다.

+ ) : postLists && postLists?.length > 0 ? (
- {postLists.map((item, index) => ( + {postLists?.map((item, index) => ( ))}
+ ) : ( +

게시글이 없습니다.

)}
diff --git a/src/pages/MyPage/constants/index.ts b/src/pages/MyPage/constants/index.ts index b605b37..7028ff8 100644 --- a/src/pages/MyPage/constants/index.ts +++ b/src/pages/MyPage/constants/index.ts @@ -5,5 +5,5 @@ export const TEMPERATURE_RANGE = [ { min: 40, max: 55, description: '마음이 따뜻한 따숨님' }, { min: 55, max: 70, description: '훈훈한 따숨님' }, { min: 70, max: 80, description: '정말 따뜻한 따숨님' }, - { min: 85, max: 100, description: '사랑이 넘치는 따숨님' }, + { min: 85, max: 105, description: '사랑이 넘치는 따숨님' }, ]; diff --git a/src/pages/MyPage/index.tsx b/src/pages/MyPage/index.tsx index 2ed0b5c..226eb36 100644 --- a/src/pages/MyPage/index.tsx +++ b/src/pages/MyPage/index.tsx @@ -7,6 +7,7 @@ import useAuthStore from '@/stores/authStore'; import useMyPageStore from '@/stores/myPageStore'; import { TEMPERATURE_RANGE } from './constants'; +import useToastStore from '@/stores/toastStore'; const MyPage = () => { useEffect(() => { @@ -16,6 +17,7 @@ const MyPage = () => { const { data, fetchMyPageInfo } = useMyPageStore(); const [isOpenModal, setIsOpenModal] = useState(false); const logout = useAuthStore((state) => state.logout); + const setToastActive = useToastStore((state) => state.setToastActive); const getDescriptionByTemperature = (temp: number) => { const range = TEMPERATURE_RANGE.find((range) => temp >= range.min && temp < range.max); @@ -28,9 +30,14 @@ const MyPage = () => { try { const response = await deleteUserInfo(); if (!response) throw new Error('deletioning failed'); - console.log(response); + return response; } catch (error) { console.error(error); + setToastActive({ + toastType: 'Error', + title: '서버오류로 탈퇴처리가 되지 않았습니다. 잠시 후에 다시 시도해주세요.', + time: 5, + }); } }; @@ -43,9 +50,13 @@ const MyPage = () => { cancelText="되돌아가기" confirmText="탈퇴하기" onCancel={() => setIsOpenModal(false)} - onConfirm={() => { - handleLeave(); + onConfirm={async () => { + const response = await handleLeave(); setIsOpenModal(false); + if (response?.status === 200) { + logout(); + alert('탈퇴가 완료 되었습니다.'); + } }} /> )} diff --git a/src/pages/Notifications/components/NotificationItem.tsx b/src/pages/Notifications/components/NotificationItem.tsx index ff9428e..52f0db5 100644 --- a/src/pages/Notifications/components/NotificationItem.tsx +++ b/src/pages/Notifications/components/NotificationItem.tsx @@ -4,12 +4,12 @@ import { NOTIFICATION_ICON } from '../constants'; interface NotificationItemProps { type: string; - message: string; - isRead: boolean; + title: string; + read: boolean; onClick: () => void; } -const NotificationItem = ({ type, message, isRead, onClick }: NotificationItemProps) => { +const NotificationItem = ({ type, title, read, onClick }: NotificationItemProps) => { const Icon = NOTIFICATION_ICON[type]; const handleClick = (e: React.MouseEvent) => { @@ -18,11 +18,11 @@ const NotificationItem = ({ type, message, isRead, onClick }: NotificationItemPr }; return ( - +
- {isRead &&
} + {read &&
} -

{message}

+

{title}

); diff --git a/src/pages/Notifications/components/SendingModal.tsx b/src/pages/Notifications/components/SendingModal.tsx new file mode 100644 index 0000000..341532d --- /dev/null +++ b/src/pages/Notifications/components/SendingModal.tsx @@ -0,0 +1,36 @@ +import LetterWrapper from '@/components/LetterWrapper'; +import ModalOverlay from '@/components/ModalOverlay'; +import { useNavigate } from 'react-router'; + +export default function SendingModal({ + isOpenSendingModal, + setIsOpenSendingModal, +}: { + isOpenSendingModal: boolean; + setIsOpenSendingModal: React.Dispatch>; +}) { + const navigate = useNavigate(); + if (!isOpenSendingModal) return null; + const onClose = () => { + setIsOpenSendingModal(false); + }; + return ( + <> + + +
+

편지 도착

+ 편지는 작성된 시점으로 1시간 이후에 도착합니다. + 남은시간은 홈 화면의 편지 도착 시간 버튼을 눌러 확인 가능합니다. + +
+
+
+ + ); +} diff --git a/src/pages/Notifications/components/WarningModal.tsx b/src/pages/Notifications/components/WarningModal.tsx index 8e7e922..4470eb3 100644 --- a/src/pages/Notifications/components/WarningModal.tsx +++ b/src/pages/Notifications/components/WarningModal.tsx @@ -4,12 +4,13 @@ import ModalOverlay from '@/components/ModalOverlay'; interface WarningModalProps { isOpen: boolean; + reportContent: string; onClose: () => void; } -const WarningModal = ({ isOpen, onClose }: WarningModalProps) => { +const WarningModal = ({ isOpen, reportContent, onClose }: WarningModalProps) => { + const divideContents = reportContent.split('§'); if (!isOpen) return null; - return (
{ 따사로운 서비스 이용을 위해, 부적절하다고 판단되는 편지는 반려하고 있어요. 서로를 존중하는 따뜻한 공간을 만들기 위해 협조 부탁드립니다.

+ +

관리자 코멘트

+

{divideContents[0]}

+ +

현재 경고 누적

+

{`${divideContents[1]} 회`}

+

경고 규칙

1회 경고: 주의 안내 diff --git a/src/pages/Notifications/constants/index.ts b/src/pages/Notifications/constants/index.ts index 1134831..0ab3d29 100644 --- a/src/pages/Notifications/constants/index.ts +++ b/src/pages/Notifications/constants/index.ts @@ -4,7 +4,9 @@ export const NOTIFICATION_ICON: Record< string, React.ComponentType> > = { - letter: EnvelopeIcon, - warning: SirenFilledIcon, - board: BoardIcon, + SENDING: EnvelopeIcon, + LETTER: EnvelopeIcon, + REPORT: SirenFilledIcon, + SHARE: BoardIcon, + POSTED: BoardIcon, }; diff --git a/src/pages/Notifications/index.tsx b/src/pages/Notifications/index.tsx index bef3d9c..a3c0cdc 100644 --- a/src/pages/Notifications/index.tsx +++ b/src/pages/Notifications/index.tsx @@ -1,50 +1,120 @@ -import { useState } from 'react'; +import { useEffect, useState } from 'react'; +import { useNavigate } from 'react-router'; +import { getTimeLines, patchReadNotification, patchReadNotificationAll } from '@/apis/notification'; import PageTitle from '@/components/PageTitle'; import NotificationItem from './components/NotificationItem'; import WarningModal from './components/WarningModal'; - -const DUMMY_NOTI = [ - { id: 1, type: 'letter', message: '12E31님이 편지를 보냈습니다.', isRead: false }, - { id: 2, type: 'warning', message: '따숨님, 욕설로 인해 경고를 받으셨어요.', isRead: false }, - { id: 3, type: 'letter', message: '12E31님이 편지를 보냈습니다.', isRead: false }, - { id: 4, type: 'letter', message: '12E31님이 편지를 보냈습니다.', isRead: true }, - { id: 5, type: 'letter', message: '12E31님이 편지를 보냈습니다.', isRead: false }, - { id: 6, type: 'board', message: '12E31님과의 대화가 게시판에 공유되었어요.', isRead: false }, - { - id: 7, - type: 'board', - message: '12E31님과의 게시글에 대한 공유요청을 보냈어요.', - isRead: false, - }, -]; +import SendingModal from './components/SendingModal'; const NotificationsPage = () => { + const navigate = useNavigate(); + + const [noti, setNoti] = useState([]); + const [isOpenWarningModal, setIsOpenWarningModal] = useState(false); + const [isOpenSendingModal, setIsOpenSendingModal] = useState(false); - const handleClickItem = (type: string) => { - if (type === 'warning') { + const [reportContent, setReportContent] = useState(''); + + // MEMO : 편지 데이터 전송중 데이터도 추가될건데 나중에 데이터 추가되면 코드 업데이트 하긔 + const handleClickItem = (alarmType: string, content?: string | number) => { + if (alarmType === 'SENDING') { + setIsOpenSendingModal(true); + } + if (alarmType === 'LETTER') { + navigate(`/letter/${content}`); + } + if (alarmType === 'REPORT') { setIsOpenWarningModal(true); + if (typeof content === 'string') setReportContent(content); + } + if (alarmType === 'SHARE') { + navigate(`/board/letter/${content}`, { state: { isShareLetterPreview: true } }); + } + if (alarmType === 'POSTED') { + navigate(`/board/letter/${content}`); + } + }; + + const handleGetTimeLines = async () => { + const res = await getTimeLines(); + if (res?.status === 200) { + console.log(res); + setNoti(res.data.data.content); + } + }; + + const handlePatchReadNotification = async (timelineId: number) => { + const res = await patchReadNotification(timelineId); + if (res?.status === 200) { + setNoti((curNoti) => + curNoti.map((noti) => { + if (noti.timelineId === timelineId) { + return { ...noti, read: true }; + } + return noti; + }), + ); + } else { + console.log('읽음처리 에러 발생'); } }; + const handlePatchReadNotificationAll = async () => { + const res = await patchReadNotificationAll(); + if (res?.status === 200) { + setNoti((currentNoti) => { + return currentNoti.map((noti) => { + if (!noti.read) { + return { ...noti, read: true }; + } + return noti; + }); + }); + } else { + console.log('모두 읽음처리 에러 발생'); + } + }; + + useEffect(() => { + handleGetTimeLines(); + }, []); + return ( <> - setIsOpenWarningModal(false)} /> + setIsOpenWarningModal(false)} + /> +

알림 -
    - {DUMMY_NOTI.map((notification) => ( -
  • + {noti.map((notification) => ( +
  • handleClickItem(notification.type)} + type={notification.alarmType} + title={notification.title} + read={notification.read} + onClick={() => { + handleClickItem(notification.alarmType, notification.content); + handlePatchReadNotification(notification.timelineId); + }} />
  • ))} diff --git a/src/pages/Onboarding/WelcomeLetter.tsx b/src/pages/Onboarding/WelcomeLetter.tsx index 50b7e4e..49a1716 100644 --- a/src/pages/Onboarding/WelcomeLetter.tsx +++ b/src/pages/Onboarding/WelcomeLetter.tsx @@ -5,7 +5,7 @@ export default function index() { const navigate = useNavigate(); return (
    -
    +

    To.따숨이

    환영합니다! 우리 함께 마음을 나누어 보아요

    @@ -22,7 +22,7 @@ export default function index() {

    3. 고민 편지에 대한 답장은 검수 후에 전달됩니다.

    -

    From.9황작물

    +

    From.9황작물

    - - {randomLetters.map((list, idx) => { - return ( - -
    { - setOpenModal(true); - setSelectedLetter(list); - }} - > - -
    -
    - ); - })} -
    + {randomLetters.length === 0 ? ( + +
    +
    + 편지가 없습니다. + 따숨님의 편지를 작성해보시겠어요? +
    + +
    +
    + ) : ( + + {randomLetters.map((list, idx) => { + return ( + +
    { + setOpenModal(true); + setSelectedLetter(list); + }} + > + +
    +
    + ); + })} +
    + )}
diff --git a/src/pages/RandomLetters/constants/index.ts b/src/pages/RandomLetters/constants/index.ts index 47fa7f5..b10f948 100644 --- a/src/pages/RandomLetters/constants/index.ts +++ b/src/pages/RandomLetters/constants/index.ts @@ -1,5 +1,5 @@ -const CATEGORY_LIST: { title: string; category: Category | null }[] = [ - { title: '전체', category: null }, +const CATEGORY_LIST: { title: string; category: Category | 'ALL' }[] = [ + { title: '전체', category: 'ALL' }, { title: '위로와 공감', category: 'CONSOLATION' }, { title: '축하와 응원', category: 'CELEBRATION' }, { title: '고민 상담', category: 'CONSULT' }, diff --git a/src/pages/RollingPaper/components/CommentDetailModal.tsx b/src/pages/RollingPaper/components/CommentDetailModal.tsx index 4769892..cbabb52 100644 --- a/src/pages/RollingPaper/components/CommentDetailModal.tsx +++ b/src/pages/RollingPaper/components/CommentDetailModal.tsx @@ -20,7 +20,7 @@ const CommentDetailModal = ({ comment, isWriter, onClose, onDelete }: CommentDet
-

{comment.content}

+

{comment.content}

From. {comment.zipCode}

diff --git a/src/pages/RollingPaper/components/WriteCommentButton.tsx b/src/pages/RollingPaper/components/WriteCommentButton.tsx index 201037f..21cc29c 100644 --- a/src/pages/RollingPaper/components/WriteCommentButton.tsx +++ b/src/pages/RollingPaper/components/WriteCommentButton.tsx @@ -4,8 +4,7 @@ import { useState } from 'react'; import { postRollingPaperComment } from '@/apis/rolling'; import EnvelopeImg from '@/assets/images/closed-letter.png'; import MessageModal from '@/components/MessageModal'; - -const DUMMY_USER_ZIP_CODE = '1DR41'; +import useAuthStore from '@/stores/authStore'; interface WriteCommentButtonProps { rollingPaperId: string; @@ -15,12 +14,12 @@ const WriteCommentButton = ({ rollingPaperId }: WriteCommentButtonProps) => { const [activeMessageModal, setActiveMessageModal] = useState(false); const [newMessage, setNewMessage] = useState(''); const [error, setError] = useState(null); + const zipCode = useAuthStore((props) => props.zipCode); const queryClient = useQueryClient(); const { mutate } = useMutation({ mutationFn: (content: string) => postRollingPaperComment(rollingPaperId, content), - onSuccess: (data) => { - console.log(data); + onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['rolling-paper', rollingPaperId] }); setNewMessage(''); setError(null); @@ -37,7 +36,6 @@ const WriteCommentButton = ({ rollingPaperId }: WriteCommentButtonProps) => { const handleAddComment = () => { console.log(rollingPaperId); - // 추가 가능한지 조건 확인 if (newMessage.trim() === '') { setError('편지를 작성해주세요.'); return; @@ -59,12 +57,12 @@ const WriteCommentButton = ({ rollingPaperId }: WriteCommentButtonProps) => { onComplete={handleAddComment} >

{error}

-

From. {DUMMY_USER_ZIP_CODE}

+

From. {zipCode}

)} + + + + + )} +
+
+ ); +}; + +export default ShareApprovalPage; diff --git a/src/pages/Write/CategorySelect.tsx b/src/pages/Write/CategorySelect.tsx index bdabcbb..7244b0b 100644 --- a/src/pages/Write/CategorySelect.tsx +++ b/src/pages/Write/CategorySelect.tsx @@ -59,9 +59,7 @@ export default function CategorySelect({
작성하신 편지는 - {'00'}시간 - {'00'}분 - {'00'}초 후에 도착합니다. + 1시간 후에 도착합니다.
diff --git a/src/pages/Write/LetterEditor.tsx b/src/pages/Write/LetterEditor.tsx index 4e91d38..21e7d46 100644 --- a/src/pages/Write/LetterEditor.tsx +++ b/src/pages/Write/LetterEditor.tsx @@ -1,34 +1,42 @@ import { useEffect, useState } from 'react'; -import { useLocation } from 'react-router'; +import { useLocation, useNavigate } from 'react-router'; import { twMerge } from 'tailwind-merge'; -import { postFirstReply, postLetter } from '@/apis/write'; +import { postFirstReply, postLetter, postTemporarySave } from '@/apis/write'; import BackButton from '@/components/BackButton'; +import ConfirmModal from '@/components/ConfirmModal'; import WritePageButton from '@/pages/Write/components/WritePageButton'; import { FONT_TYPE_OBJ } from '@/pages/Write/constants'; import OptionSlide from '@/pages/Write/OptionSlide'; import useWrite from '@/stores/writeStore'; import { removeProperty } from '@/utils/removeProperty'; +import useToastStore from '@/stores/toastStore'; +import { useQueryClient } from '@tanstack/react-query'; export default function LetterEditor({ + letterId, setStep, prevLetter, setSend, - searchParams, isReply, }: { - setStep: React.Dispatch>; + letterId: string | null; + isReply: boolean; prevLetter: PrevLetter[]; + setStep: React.Dispatch>; setSend: React.Dispatch>; - searchParams: URLSearchParams; - isReply: boolean; }) { const location = useLocation(); + const queryClient = useQueryClient(); + const navigate = useNavigate(); const [randomMatched, setRandomMatched] = useState(false); + const [isTemporaryConfirmModal, setIsTemporaryConfirmModal] = useState(false); const letterRequest = useWrite((state) => state.letterRequest); const setLetterRequest = useWrite((state) => state.setLetterRequest); + const setToastActive = useToastStore((state) => state.setToastActive); + const handlePostFirstReply = async (firstReplyRequest: Omit) => { const res = await postFirstReply(firstReplyRequest); if (res?.status === 200) { @@ -39,7 +47,6 @@ export default function LetterEditor({ } }; - // MEMO : 답장 전송 matchingId가 undefined로 나오는데 뭐 때문인지 내일 찾아보자 ㅎ const handlePostReply = async (letterRequest: LetterRequest) => { const res = await postLetter(letterRequest); if (res?.status === 200) { @@ -63,35 +70,77 @@ export default function LetterEditor({ console.log('prevLetter', prevLetter); setLetterRequest({ receiverId: prevLetter[0].memberId, - parentLetterId: Number(searchParams.get('letterId')), + parentLetterId: Number(letterId), category: prevLetter[0].category, matchingId: prevLetter[0].matchingId, }); } - }, [prevLetter, searchParams, setLetterRequest, isReply]); + }, [prevLetter, setLetterRequest, isReply]); + + const handlePostTemporarySave = async () => { + if (!letterId) return alert('임시저장중 오류 발생'); + const LETTER_STATE_DUMMY = false; + const requestLetterId = LETTER_STATE_DUMMY || null; + // MEMO : 임시저장 전송 방식 : 최초임시저장은 letterId : null, 임시저장 업데이트는 letterId : location state로 받아오는 임시저장편지의 letterId값 + const temporaryRequest: TemporaryRequest = { ...letterRequest, letterId: requestLetterId }; + const res = await postTemporarySave(temporaryRequest); + if (res?.status === 200) { + console.log(res); + navigate('/'); + } else { + alert('실패'); + } + }; return (
+ {isTemporaryConfirmModal && ( + setIsTemporaryConfirmModal(false)} + onConfirm={() => { + handlePostTemporarySave(); + }} + /> + )}
{isReply ? ( - { - if (letterRequest.title.trim() !== '' && letterRequest.content.trim() !== '') { - if (randomMatched) { - const firstReplyRequest = removeProperty(letterRequest, ['matchingId']); - console.log(firstReplyRequest); - handlePostFirstReply(firstReplyRequest); +
+ {!randomMatched && ( + { + setIsTemporaryConfirmModal(true); + }} + /> + )} + { + if (letterRequest.title.trim() !== '' && letterRequest.content.trim() !== '') { + if (randomMatched) { + const firstReplyRequest = removeProperty(letterRequest, ['matchingId']); + console.log(firstReplyRequest); + handlePostFirstReply(firstReplyRequest); + } else { + handlePostReply(letterRequest); + } + queryClient.invalidateQueries({ queryKey: ['mailBox'] }); + queryClient.invalidateQueries({ queryKey: ['mailBoxDetail'] }); } else { - handlePostReply(letterRequest); + setToastActive({ + toastType: 'Warning', + title: '편지 제목, 내용이 작성되었는지 확인해주세요', + }); } - } else { - alert('편지 제목, 내용이 작성되었는지 확인해주세요'); - } - }} - /> + }} + /> +
) : ( @@ -120,7 +172,7 @@ export default function LetterEditor({