From c49b97b558b6032c1a2d69f194eae8a49115bfab Mon Sep 17 00:00:00 2001 From: nirii00 Date: Thu, 6 Mar 2025 22:18:19 +0900 Subject: [PATCH 1/8] =?UTF-8?q?chore:=20deploy.yml=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20-=20=EB=B0=B0=ED=8F=AC=20=EC=84=9C?= =?UTF-8?q?=EB=B2=84=20=EB=B3=80=EC=88=98=20=EB=B0=94=EA=BF=88=20(?= =?UTF-8?q?=EC=9E=84=EC=8B=9C=20=EC=A3=BC=EC=86=8C=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EB=B0=B0=ED=8F=AC=20=EC=84=9C=EB=B2=84=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 58 ++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 .github/workflows/deploy.yml diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..3a421e6 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,58 @@ +name: MAIN CI + +on: + push: + branches: + - main + +jobs: + Deploy: + runs-on: ubuntu-latest + + steps: + - name: Checkout source code + uses: actions/checkout@v3 + + - name: Install pnpm + run: | + npm install -g pnpm + + - name: Get pnpm store path + id: pnpm-cache-path + run: echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_ENV + + - name: Cache pnpm modules + id: cache + uses: actions/cache@v3 + with: + path: ${{ env.STORE_PATH }} + key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm- + ${{ runner.os }}- + + - name: Install Dependencies + run: | + pnpm install --frozen-lockfile + pnpm store prune + + - name: Set up .env file + run: | + echo "VITE_API_URL=${{ secrets.VITE_API_URL }}" > .env.production + echo "HTTPS=true" >> .env.production + + - name: Build + run: pnpm run build + + - name: Configure AWS Credentials + uses: aws-actions/configure-aws-credentials@v2 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: ${{ secrets.AWS_REGION }} + + - name: Deploy to S3 + run: aws s3 sync ./dist s3://${{ secrets.AWS_BUCKET_NAME }} --delete + + - name: Invalidate CloudFront Cache + run: aws cloudfront create-invalidation --distribution-id ${{ secrets.AWS_DISTRIBUTION_ID }} --paths "/*" From 724b7c1e717bb43d28bbc7a174497cee0ccf30d2 Mon Sep 17 00:00:00 2001 From: nirii00 Date: Thu, 6 Mar 2025 22:22:40 +0900 Subject: [PATCH 2/8] =?UTF-8?q?fix:=20strictMode=20=EC=A3=BC=EC=84=9D=20?= =?UTF-8?q?=ED=95=B4=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main.tsx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/main.tsx b/src/main.tsx index 8b87556..4df6fa6 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -15,11 +15,11 @@ queryClient.setDefaultOptions({ }); createRoot(document.getElementById('root')!).render( - // - - - - - , - // + + + + + + + , ); From 3914a0052cf70167586d829cad38cd7683772a73 Mon Sep 17 00:00:00 2001 From: nirii00 Date: Thu, 6 Mar 2025 22:43:52 +0900 Subject: [PATCH 3/8] =?UTF-8?q?fix:=20build=20error=20=EC=9B=90=EC=9D=B8?= =?UTF-8?q?=20=EC=88=98=EC=A0=95=20(type=20=EC=98=A4=EB=A5=98=20=EC=88=98?= =?UTF-8?q?=EC=A0=95)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Admin/components/ReportHandlingModal.tsx | 2 +- src/pages/LetterBoardDetail/index.tsx | 9 ++++++-- src/pages/MyPage/components/MyBoardPage.tsx | 22 ++++++++++--------- .../components/MatchedLetter.tsx | 2 +- src/stores/myPageStore.ts | 2 +- src/types/admin.d.ts | 2 +- 6 files changed, 23 insertions(+), 16 deletions(-) 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/LetterBoardDetail/index.tsx b/src/pages/LetterBoardDetail/index.tsx index 3f8292d..102a37d 100644 --- a/src/pages/LetterBoardDetail/index.tsx +++ b/src/pages/LetterBoardDetail/index.tsx @@ -6,7 +6,6 @@ import { getSharePostDetail, postShareProposalApproval, SharePost, - postSharePostLike, getSharePostLikeCount, } from '@/apis/share'; import BlurImg from '@/assets/images/landing-blur.png'; @@ -87,7 +86,13 @@ const LetterBoardDetailPage = ({ confirmDisabled }: ShareLetterPreviewProps) => return ( <> - {activeReportModal && setActiveReportModal(false)} />} + {activeReportModal && ( + setActiveReportModal(false)} + reportType={'SHARE_POST'} + letterId={null} + /> + )}
{

loading

) : (
- {postLists.map((item, index) => ( - - ))} + /> + ), + )}
)} diff --git a/src/pages/RandomLetters/components/MatchedLetter.tsx b/src/pages/RandomLetters/components/MatchedLetter.tsx index 1e05d46..aa8e634 100644 --- a/src/pages/RandomLetters/components/MatchedLetter.tsx +++ b/src/pages/RandomLetters/components/MatchedLetter.tsx @@ -15,7 +15,7 @@ const MatchedLetter = ({ matchedLetter }: { matchedLetter: MatchedLetter }) => { return ( <> - {reportModalOpen && setReportModalOpen(false)} />} + {reportModalOpen && setReportModalOpen(false)} reportType={'LETTER'} letterId={null} />}
Date: Sun, 9 Mar 2025 23:21:45 +0900 Subject: [PATCH 4/8] =?UTF-8?q?deploy=20:=20=EB=B0=B0=ED=8F=AC=EC=A4=80?= =?UTF-8?q?=EB=B9=84=20(#104)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 임시저장된 편지 삭제 기능 구현 (#84) * fix: 공유 게시글 목록 페이지네이션에서 무한 스크롤로 전환 * refactor: 필요없는 코드 삭제 * feat: 임시저장된 편지 삭제 기능 구현 * design: 임시저장 편지 삭제 아이콘 클릭 시 색상 변경 * feat: 롤링페이퍼 배포된 api로 연결 수정 (#85) * style:롤링페이퍼 공지 애니메이션 수정 * fix: 롤링페이퍼가 진행되지 않을 때 화면에 보여지지 않도록 처리 * feat: 새로운 롤링페이퍼 생성 api 연동 * feat: 롤링페이퍼 목록 조회 api 연동 * feat: 롤링페이퍼 삭제 api 연동 * feat: 롤링페이퍼 사용여부 변경 api 연동 * feat: 롤링페이퍼 코멘트 목록 조회 api 연동 * feat: 롤링페이퍼 등록 및 삭제 api 연동 - api 응답 구조 수정 * feat : 알림 2차 기능 구현 (#81) * feat:알림 컨텐츠 상호작용, 라우팅 기능 구현 * feat : SSE구현시도 * feat : SSE전역변수로 관리할지 store 하나 만들어서 만들면서 고민중 * feat : 알림구독 테스트용 App.tsx에 훅 호출한 코드 * fix: 자잘한 이슈 수정 (#86) * fix: 401 에러가 아닌 경우 바로 로그아웃 되는 문제 해결 * fix: 토큰 만료로 reissue 실패시 로그아웃 안되는 문제 해결 * fix: reissue 에러 시 에러 처리 안되는 문제 해결 - client.ts의 interceptors.response.use 구문 내에서 api 호출및 데이터 가공을 함수 밖으로 꺼냄 * fix: mailbox에서 isClosed 옵션 반대로 보여주는 문제 해결 - isClosed 상태를 반대로 받아와서 상태를 잘못 보여주는 문제 해결 * fix: mailBox 배포 api에 따른 수정 작업 - sharePost 요청 요청자 id 삭제 - 상세 페이지 api 경로 수정 - 우편함 상세체이지 날짜 잘못표기하는 에러 수정 * fix: reissue시 access token을 사용하지 않도록 수정 * feat: 신고 모달 파라미터 변경으로 인한, 파라미터 추가(게시판 상세) * fix: 마이페이지 api 수정 * feat: 임시저장된 편지 삭제 기능 구현 (#84) * fix: 공유 게시글 목록 페이지네이션에서 무한 스크롤로 전환 * refactor: 필요없는 코드 삭제 * feat: 임시저장된 편지 삭제 기능 구현 * design: 임시저장 편지 삭제 아이콘 클릭 시 색상 변경 * feat: 롤링페이퍼 배포된 api로 연결 수정 (#85) * style:롤링페이퍼 공지 애니메이션 수정 * fix: 롤링페이퍼가 진행되지 않을 때 화면에 보여지지 않도록 처리 * feat: 새로운 롤링페이퍼 생성 api 연동 * feat: 롤링페이퍼 목록 조회 api 연동 * feat: 롤링페이퍼 삭제 api 연동 * feat: 롤링페이퍼 사용여부 변경 api 연동 * feat: 롤링페이퍼 코멘트 목록 조회 api 연동 * feat: 롤링페이퍼 등록 및 삭제 api 연동 - api 응답 구조 수정 * fix: type 에러 수정 * fix: 데이터가 없는 경우 컴포넌트 에러가 나는 부분 수정 - 배열이 없거나, 길이가 0이면 placeholder를 보여줌 * Update index.tsx 이상한 import 지움 --------- Co-authored-by: nirii00 Co-authored-by: Seungyeon Han (Tiffany) <125551867+tiffanyhansy@users.noreply.github.com> Co-authored-by: Minha Ahn * feat : 재사용 가능한 페이지네이션 구현 (#92) * feat: 편지 공유 요청 수신 조회 기능 구현 (#90) * feat: 편지 공유 요청 수신 조회 기능 구현 * refactor: incomingLetters.ts 코드 리팩토링 * refactor: incomingLettersStore.ts에서 필요 없는 필드 정리 * feat: 오고 있는 편지 도착까지 걸리는 시간 카운트다운 기능 구현 * design: 오고 있는 편지 모달에서 데이터가 없을 때 대체 텍스트 추가 * design: 임시저장된 편지 모달에서 데이터가 없을 때 대체 텍스트 추가 * design: 편지 공유 요청 수신 조회 모달에서 데이터가 없을 때 대체 텍스트 추가 * feat : 편지작성, 랜덤편지, 상세페이지 3차 기능구현 (#94) * feat : 게시글 신고기능 구현 * feat : 카테고리 전체 선택 안되는 오류 수정 + 답장 전송시 도착시간 1시간으로 텍스트 고정 * feat : getPrevLetter api 엔드포인트 변경 * feat : 디테일 페이지 답장버튼 분기처리 * feat : 편지상세페이지 zipCode바인딩 * refactor : 편지상세 페이지 컴포넌트 분리 * feat : 편지 상세 컴포넌트 추가 분리 + 편지 평가 기능 구현 완료 * refactor : 신고모달 타입에서 null 제거 + 이전편지 가져오기, 타입 조금 수정 * feat : 랜덤편지 편지 없을 경우 예외처리 UI 추가 * design : 편지작성, 편지상세 resize속성 제거 * feat : 랜덤편지 데이터가 없을시 예외처리 UI 추가 + 쿨타임 상태일때 예외처리 UI 수정 * chore : 랜덤편지 api console 제거 * feat : 임시저장 api 생성(연결 테스트 아직 안함) * feat : 편지 작성 페이지 임시저장 버튼 구현 * feat : 편지 임시저장 80% 구현(승연님 작업 이후 임시저장 업데이트 분기 나눠야함) * feat : 임시저장 최초답장 예외처리 * feat: 롤링페이퍼 추가 기능 구현 (#95) * fix: 더미데이터 제거 * feat: 롤링페이퍼 무한 스크롤 적용 * feat: 관리자 페이지 롤링 페이퍼 설정 화면에 페이지네이션 적용 * design: 롤링페이퍼 작성 버튼 위치 수정 * fix: 데이터 변경 후 캐싱 초기화 하는 조건 수정 * design: 하단 언덕 이미지 깨지는 문제 해결 --------- Co-authored-by: wldnjs990 <139528356+wldnjs990@users.noreply.github.com> * feat: 임시저장 편지 조회 기능 완성 (#97) * design: 편지 공유 요청 모달 텍스트 수정 * fix: 임시저장 편지 조회 데이터 구조 수정 * feat: 임시저장 편지 작성 페이지로 데이터 넘겨주기 * design: 홈 페이지에서 새 이미지를 눌러도 랜덤응원메시지가 바뀌도록 --------- Co-authored-by: wldnjs990 <139528356+wldnjs990@users.noreply.github.com> * feat : 토스트 UI 구현 + 알림 페이지 코드 작업 90% 완료 + 실시간 알림, 편지 작성 예외처리에 토스트UI 연결 (#98) * feat : 알림 타입에 SENDING 추가 * feat : 알림 페이지 타임라인 데이터바인딩 + 알림타입에 따른 UI작업 완료(구독 UI작업만 남음) * feat : 토스트UI 작업중 * feat : SSE훅 호출 위치 App에서 PrivateRouter로 이동 + Home 라우트 PrivateRouter 안으로 이동 * feat : 토스트 기능 1차 구현(알림기능 알림도착, 편지작성 내용 미입력시 토스트 넣어둠) * feat : 토스트알림 최대넓이 지정 * feat : 토스트 컨텐츠 타입별 이모지 추가 * refactor : 토스트UI 알림 1개만 표시 => 알림 1개 이상 표시 되도록 업그레이드(단일 객체 -> 객체 배열로 데이터값 수정) * refactor : 토스트UI position 타입 수정 * fix: reissue 문제, 내 편지함 data 최신화 문제 해결 (#100) * fix: 에러나는 경우 무조건 로그아웃되는 현상 해결 * fix: client.ts 주석 제거 * fix: 탈퇴시 로그아웃 자동적으로 되도록 수정 * feat: 탈퇴 에러시 alert 추가 * fix: 마이페이지, 내 공유 게시물 데이터 없을때 자잘한 에러 수정 * fix: 편치 쿼리 갱신 로직 추가 - queryClient.invalidateQueries로 쿼리를 갱신 * fix: 에러시 alert 추가 * fix: 탈퇴한 회원 로직 추가, 온보딩 from 왼쪽 정렬 * fix: 편지 전송/삭제 시 편지함에서 바로 수정사항 반영되도록 함 - 편지 삭제/전송시 queryClient.invalidateQueries 적용 * fix: 게시판에서 response 없을 경우 페이지 로딩이 안되는 문제 해결 - 추후 지속적으로 확인 예정 * fix: 파비콘, 사이트 이름 수정 * feat: ToastUI 적용 -내 편지함의 alert를 toastUI로 변경했습니다. * fix: reissue token 담기지 않는 문제 해결 * fix: letterBoard length 없음 문제 해결 - 계속 확인 필 --------- Co-authored-by: nirii00 * feat : 알림 페이지 알림 확인 처리 안되던 현상 수정 + 신고페이지 4차 구현 (#101) * refactor : API 일부 리팩토링 * feat : 알림페이지 알림 단일확인시 읽음알림으로 처리되지않던 현상 수정 * feat : 신고 페이지 status 필터링 임시 구현(수정해야함) * deploy : 오류 수정 --------- Co-authored-by: Seungyeon Han (Tiffany) <125551867+tiffanyhansy@users.noreply.github.com> Co-authored-by: Minha Ahn Co-authored-by: Sebin Kim <108220388+nirii00@users.noreply.github.com> Co-authored-by: nirii00 --- eslint.config.js | 1 + index.html | 6 +- package.json | 2 + pnpm-lock.yaml | 16 ++ public/favicon.ico | Bin 0 -> 15406 bytes public/vite.svg | 1 - src/App.tsx | 4 +- src/apis/admin.ts | 20 +-- src/apis/auth.ts | 1 + src/apis/client.ts | 45 +----- src/apis/draftLetters.ts | 34 ++-- src/apis/incomingLetters.ts | 10 +- src/apis/letterDetail.ts | 15 +- src/apis/mailBox.ts | 2 +- src/apis/myPage.ts | 2 +- src/apis/notification.ts | 34 ++++ src/apis/randomLetter.ts | 6 - src/apis/rolling.ts | 65 +++++++- src/apis/share.ts | 30 +++- src/apis/write.ts | 24 ++- src/components/BackgroundBottom.tsx | 2 +- src/components/ConfirmModal.tsx | 1 - src/components/NoticeRollingPaper.tsx | 40 ++++- src/components/ReportModal.tsx | 12 +- src/components/Toast.tsx | 16 ++ src/components/ToastItem.tsx | 54 +++++++ src/hooks/useServerSentEvents.tsx | 72 +++++++++ src/layouts/PrivateRoute.tsx | 10 +- src/pages/Admin/Report.tsx | 76 ++++----- src/pages/Admin/RollingPaper.tsx | 105 ++++++------ src/pages/Admin/components/AddInputButton.tsx | 7 +- .../Admin/components/AddRollingPaperModal.tsx | 21 ++- .../Admin/components/PagenationNavigation.tsx | 94 +++++++++++ .../Admin/components/RollingPaperItem.tsx | 90 +++++++++++ src/pages/Auth/index.tsx | 6 + src/pages/Home/components/RandomCheer.tsx | 1 + src/pages/Home/components/ShowDraftModal.tsx | 67 +++++--- .../components/ShowIncomingLettersModal.tsx | 22 +-- .../Home/components/ShowShareAccessModal.tsx | 71 +++++---- src/pages/LetterBoard/index.tsx | 50 +++--- src/pages/LetterBoardDetail/index.tsx | 79 +++++---- src/pages/LetterBox/index.tsx | 2 +- src/pages/LetterBoxDetail/index.tsx | 30 +++- .../components/DegreeSelector.tsx | 61 +++++++ .../components/LetterDetailContent.tsx | 26 +++ .../components/LetterDetailDegreeButton.tsx | 50 ++++++ .../components/LetterDetailHeader.tsx | 60 +++++++ .../components/LetterDetailReplyButton.tsx | 19 +++ src/pages/LetterDetail/index.tsx | 150 +++++------------- src/pages/MyPage/components/MyBoardPage.tsx | 38 ++--- src/pages/MyPage/index.tsx | 17 +- .../components/NotificationItem.tsx | 12 +- .../Notifications/components/SendingModal.tsx | 36 +++++ .../Notifications/components/WarningModal.tsx | 12 +- src/pages/Notifications/constants/index.ts | 8 +- src/pages/Notifications/index.tsx | 122 +++++++++++--- src/pages/Onboarding/WelcomeLetter.tsx | 4 +- .../RandomLetters/components/CoolTime.tsx | 10 +- .../components/MatchedLetter.tsx | 6 +- .../components/MatchingSelect.tsx | 65 +++++--- src/pages/RandomLetters/constants/index.ts | 4 +- .../components/CommentDetailModal.tsx | 2 +- .../components/WriteCommentButton.tsx | 12 +- src/pages/RollingPaper/index.tsx | 69 ++++---- src/pages/Write/CategorySelect.tsx | 4 +- src/pages/Write/LetterEditor.tsx | 102 +++++++++--- src/pages/Write/index.tsx | 45 +----- src/stores/incomingLettersStore.ts | 32 ++-- src/stores/toastStore.ts | 44 +++++ src/styles/animations.css | 21 ++- src/styles/components.css | 7 + src/styles/preflight.css | 1 + src/types/letterDetail.d.ts | 5 + src/types/notifications.d.ts | 7 + src/types/rolling.d.ts | 16 +- src/types/write.d.ts | 11 +- 76 files changed, 1653 insertions(+), 671 deletions(-) create mode 100644 public/favicon.ico delete mode 100644 public/vite.svg create mode 100644 src/apis/notification.ts create mode 100644 src/components/Toast.tsx create mode 100644 src/components/ToastItem.tsx create mode 100644 src/hooks/useServerSentEvents.tsx create mode 100644 src/pages/Admin/components/PagenationNavigation.tsx create mode 100644 src/pages/Admin/components/RollingPaperItem.tsx create mode 100644 src/pages/LetterDetail/components/DegreeSelector.tsx create mode 100644 src/pages/LetterDetail/components/LetterDetailContent.tsx create mode 100644 src/pages/LetterDetail/components/LetterDetailDegreeButton.tsx create mode 100644 src/pages/LetterDetail/components/LetterDetailHeader.tsx create mode 100644 src/pages/LetterDetail/components/LetterDetailReplyButton.tsx create mode 100644 src/pages/Notifications/components/SendingModal.tsx create mode 100644 src/stores/toastStore.ts create mode 100644 src/types/notifications.d.ts 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 0000000000000000000000000000000000000000..2eaefa2276aac3db40cfd707438794cea248689b GIT binary patch literal 15406 zcmeHNSyNO=7Osw&AJFqK?>@=ljg}2jAzP z`=HnPw*1lO`>W6ATer^izxb2S*9_a2U%KOe@%irj+2{KLKA3_T#t!v={U;)yeT>8= zBeh`c`GLlTP-EwEsIdzIeGJ-)?MKHs*1i1PuCXy~WN2gRuoPBIN`2ds9PeHh%oX^k z);`-1wy_88Lw5eQtkI#Q>`|d4=Z#6lp||SS)Dx295Ng|IU!^+~ou67bKD4uFT+$%a z=_TWmvu9EcH7!bW?>m*2KBKjdc2iF{IzMmEw(sp7=uH2NU44{4b(u%td*9uE}|4MZK zio>%*dunGfZ&9w@iOB6AMQ%Tg7ymtxvzH^%*3Y$8=eP6)pZo8vgdYDaDo=hfntj{0 znzp--By{>>B>MdL96sjVp)conf9C(pIwaz65dMGi{=5u$8TjAMz{mct`@R1EE(<$y zM+d6w<`$i<9Y|}T^*67Z+HnUc6;6n^w zZF}MWCS^!6@f~@nc~QS-F-Iu;UUoWz+HUAr{jTO1zt8#IO|gC2a3(MkGneZe@1Z^D zkK2Fyuzy3!3)ztRQr7QyDZBPfi_;M_=LI_t+V01-cy6%To*&q6!XDg*SlOpo+??Og z5x@V&v|<02j1k$KJ|aA`Y|9*xy>QHd?jcD$1B14NBil;Z*XQ=zw2{?4x`2hSW|> zjliH7F=pC0AGvWH>j}l>pZh1}jLMGOG1;E`8hR9ZOm^Vh#Wv4hCDqe>cEDls!9BSq z`_^XrP4sd7sP()?4S&pw+aK%ux97c<I&u&dP z%N_5=*)8Tw$8#C?G}zwhInA68X@3{De_GKSe;V?fT0DWzz#I3x2n|~XJ`-8Hpi50Z z6FR$WN{+THy6c+td4?n=dnUAJO0^RY#$AkY`)BQ*@aN!+$@6AL>4czoH)ON_;HRtX>h!U3D}H1K7O3*{iXY6 zG_Sj$OZNpJreAw(PR#S@qRs=8S}zJH|R@SKz%nv=dWLHyo`2!0dDy~jq_j_u#U?~ywn z*zUsD`f-dtKMY26|F)AW(sc@WPKD$4udbi>*EGxn;}YgXx@@66j{V)CGPu|--o~VJn^yfKK$e400n}gvsIkFj>`rG$Z5%`0QhiV>4LI;e-4edb0n`;?4+T(H)Oos&uEn`n zhjU#qzIGpD=iXcg^|b(Ml7V{kn|gw^iDLMA`>?Y<`GsVaj?3Pov#iIO+Hp5^Q0uj< zU1CqyZnUlE(ucKAtvADuc2j#cbM3r__8{tlrdCTE{fw;c!F@QF<65&;jkZ>SNPxty=~ z>^S=r%s26JJ(D}G!}(exR=>pf|DH4g{4Y`GekmJLAkdpo^R5KH-q^V$jo6R-a1XA@ zdNcdSbzS5XecJ2WO&rm@VVpQx`;2B==ehoz$2DVf#CCKa?gim`4IPQ`f1Nb!e3LvZ zThbw@*>6Z0R?cA4&JoE*owmAe*5noIFqft_y_#dqA@*TzDK;LhQ8QN5sEM8PIA(Hd z>g}wh<1-P>3-J=a@i+M}F($^pDQ(o*ymJ_}{b9UA!@^iL1N-)z5lPA#hP=XOcrJ!RFgRO~n^u>zAZD(Q zm?QLG>;2!F_1f8zL2hD1wt}M|Z?P#IeLM?3gRS5%wq(4*y^S%gXV$x)L**-!k1#o2 zjm_kmIn)@GKQZ}cEVR+rJQtG(_U&__j@|VAG4RiY~C$kVa~8Gc^TT+rXSl@h?#BTqvp6iJJ&|u zh-3P!yTS?aCn5hyIfj=ZXOmh8?AhbsXb=Z@nk4WydS@mdZP$-HN;bHJgN+Ny4_J;s zc@y9uFF`!5Ck!sW7tQEvE;-INgt&>1^K4v=UA+Hd#??<_PmCY;a-40N=P|(@BO&jT z2EKiJK}go z31WdxgYYg=F8Gv+BlG(CYy)3oV*xjXcW6~?Y*sTyJ_q&Jc;122F#hHlD!;WR{;aYI zN3nrdN-ms0el6!^I4;X`W$)shDPzk*d^zBxGSH`u99SB-GV(=-d7lbch^Yzqc^8T} zm4gB{zH5q?oD|3C6YKA`8}Xd%8sKRXzW?CDoE&^6Gr@ypluo+(jdEt>#`4O+l>sZ; z*)ESpKFx4wjE6CE{uH>DRUVDpP?H-2u@g7n8^#mMrO|G4Zt>c9HD(=O3;#6mPwHK? z!bFfaBsng^R!+KYd_@j7Z^3FJ|y>*TMpL=QpPT4_X3*eqdK#HrN_mgjRwZ;Yx^zd8h zIlZj7E!Rr^m2!hTEO^%_CmX}ZUUlu9qw}C?JAXyFN4!HWAF6l_Cu(?N*fGX?op??O z-~X}$vrZW}T4+7rFb49r63d z%R$p`>RCXZLPAcN+$}MyX4@4zMt+xV3diVY=WrhRYT8w^PhZ*~S8ql1&H!T|H?8=g z*=HQk3Gr9e%{evrEY{S|;Yd*?)cp05Y`4~O}-k~?msbH^nI7VwmdfN9DhJ< z$I#b#=#w|5+$4x!pkATm1-y7iOb8(+dLi{xi3r=<2JbtIl>%E@t zQ$e|py)3tF_a}F4Ic?gM69=~)J8pfV+&Fk|&f!|>Z~K_9oH$};{ES`aVw^cRhtD|g z3b?snGynUYCFe-#v%Yc`GPmzIt0R`@TvH$=8 literal 0 HcmV?d00001 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..c8af9ff 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'; @@ -36,10 +37,10 @@ const App = () => { } /> } /> } /> - } /> } /> }> + } /> }> } /> @@ -69,6 +70,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..15c97d2 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,6 +30,15 @@ export interface SharePostResponse { totalPages: number; } +// 편지 공유 요청 수신 조회 +export interface ShareProposal { + shareProposalId: number; + requesterZipCode: string; + recipientZipCode: string; + message: string; + status: 'REJECTED' | 'APPROVED' | 'PENDING'; +} + // 편지 공유 수락 / 거절 export interface SharePostApproval { shareProposalId: number; @@ -53,7 +62,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,6 +93,19 @@ 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 postShareProposalApproval = async ( shareProposalId: number, @@ -102,7 +124,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 +136,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/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..5cbb5bc 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(`/board/letter/${draft.letterId}?isDraft=true`, { + state: { draft: 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..f74cd6a 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 { useNavigate } from 'react-router'; + +// import { getSharePostDetail } from '@/apis/share'; +import { getShareProposalList } from '@/apis/share'; +import { ShareProposal } from '@/apis/share'; -import { getSharePostDetail, getSharePostList } from '@/apis/share'; -import { SharePostResponse } from '@/apis/share'; import ModalBackgroundWrapper from '@/components/ModalBackgroundWrapper'; import ModalOverlay from '@/components/ModalOverlay'; @@ -12,33 +14,30 @@ interface ShowShareAccessModalProps { } const ShowShareAccessModal = ({ onClose }: ShowShareAccessModalProps) => { - const navigate = useNavigate(); + // 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) => { - try { - const postDetail = await getSharePostDetail(sharePostId); - navigate(`/board/letter/${sharePostId}`, { - state: { postDetail, isShareLetterPreview: true }, - }); - } catch (error) { - console.error('❌ 게시글 상세 페이지로 이동하는 데에 실패했습니다.', error); - } - }; + // const handleNavigation = async (shareProposalId: number) => { + // try { + // const postDetail = await getSharePostDetail(shareProposalId); + // navigate(`/board/letter/${shareProposalId}`, { + // state: { postDetail, isShareLetterPreview: true }, + // }); + // } catch (error) { + // console.error('❌ 게시글 상세 페이지로 이동하는 데에 실패했습니다.', error); + // } + // }; return ( @@ -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 102a37d..645218e 100644 --- a/src/pages/LetterBoardDetail/index.tsx +++ b/src/pages/LetterBoardDetail/index.tsx @@ -7,6 +7,7 @@ import { postShareProposalApproval, SharePost, getSharePostLikeCount, + postSharePostLike, } from '@/apis/share'; import BlurImg from '@/assets/images/landing-blur.png'; import ReportModal from '@/components/ReportModal'; @@ -24,65 +25,75 @@ const LetterBoardDetailPage = ({ confirmDisabled }: ShareLetterPreviewProps) => const [isLike, setIsLike] = useState(false); const isWriter = false; const [activeReportModal, setActiveReportModal] = useState(false); + const location = useLocation(); + const sharePostId: string = location.pathname.split('/')[3]; + 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 ( <> @@ -90,7 +101,7 @@ const LetterBoardDetailPage = ({ confirmDisabled }: ShareLetterPreviewProps) => setActiveReportModal(false)} reportType={'SHARE_POST'} - letterId={null} + letterId={parseInt(sharePostId)} /> )}
diff --git a/src/pages/LetterBox/index.tsx b/src/pages/LetterBox/index.tsx index c8010c9..5f173f1 100644 --- a/src/pages/LetterBox/index.tsx +++ b/src/pages/LetterBox/index.tsx @@ -32,7 +32,7 @@ const LetterBoxPage = () => { 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 d44d5ed..a4c3759 100644 --- a/src/pages/MyPage/components/MyBoardPage.tsx +++ b/src/pages/MyPage/components/MyBoardPage.tsx @@ -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,25 +42,24 @@ const MyBoardPage = () => { } return ( <> -
+
내가 올린 게시물 {isLoading ? ( -

loading

- ) : ( +

로딩 중 입니다.

+ ) : postLists && postLists?.length > 0 ? (
- {postLists.map( - (item: { sharePostId: number; writerZipCode: string }, index: number) => ( - - ), - )} + {postLists?.map((item, index) => ( + + ))}
+ ) : ( +

게시글이 없습니다.

)}
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}

)}
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({