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}
+
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 && (
+ <>
+
+
+
+ | ID |
+ 제목 |
+ 상태 |
+ |
+
+
+
+ {data.content.map((rollingPaper) => (
+
+ ))}
+
+
+ {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)}
+ />
+
알림
-
3. 고민 편지에 대한 답장은 검수 후에 전달됩니다.
- From.9황작물
+ From.9황작물
-
-
+
+
+ 기다림의 미학을 느껴보시는건 어떨까요?
+
{
const navigate = useNavigate();
- const [reportModalOpen, setReportModalOpen] = useState(false);
-
useEffect(() => {}, [matchedLetter]);
return (
<>
- {reportModalOpen && setReportModalOpen(false)} reportType={'LETTER'} letterId={null} />}
>;
setSelectedLetter: React.Dispatch
>;
}) {
- const [selectedCategory, setSelectedCategory] = useState('CONSOLATION');
+ const navigate = useNavigate();
+
+ const [selectedCategory, setSelectedCategory] = useState('ALL');
const [randomLetters, setRandomLetters] = useState([]);
- const handleGetRandomLetters = async (selectedCategory: Category) => {
+ const handleGetRandomLetters = async (selectedCategory: Category | 'ALL') => {
const res = await getRandomLetters(selectedCategory);
if (res?.status === 200) {
setRandomLetters(res.data.data);
@@ -49,27 +53,42 @@ export default function MatchingSelect({
-
- {randomLetters.map((list, idx) => {
- return (
-
- {
- setOpenModal(true);
- setSelectedLetter(list);
- }}
- >
-
-
-
- );
- })}
-
+ {randomLetters.length === 0 ? (
+
+
+
+ 편지가 없습니다.
+ 따숨님의 편지를 작성해보시겠어요?
+
+
navigate('/letter/write')}
+ >{`작성하러 가기 >`}
+
+
+ ) : (
+
+ {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}
)}
setActiveMessageModal(true)}
>
diff --git a/src/pages/RollingPaper/index.tsx b/src/pages/RollingPaper/index.tsx
index 6bcc9e6..37ded0f 100644
--- a/src/pages/RollingPaper/index.tsx
+++ b/src/pages/RollingPaper/index.tsx
@@ -1,6 +1,6 @@
import { MasonryInfiniteGrid } from '@egjs/react-infinitegrid';
-import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
-import { useState } from 'react';
+import { useMutation, useInfiniteQuery, useQueryClient } from '@tanstack/react-query';
+import { useState, useCallback } from 'react';
import { useParams } from 'react-router';
import { deleteRollingPaperComment, getRollingPaperDetail } from '@/apis/rolling';
@@ -12,51 +12,54 @@ import Header from '@/layouts/Header';
import Comment from './components/Comment';
import CommentDetailModal from './components/CommentDetailModal';
import WriteCommentButton from './components/WriteCommentButton';
+import useAuthStore from '@/stores/authStore';
-// TODO: 로그인 구현 완료 시, 더미 완전히 제거
-const DUMMY_USER_ZIP_CODE = '1DR41';
-const DUMMY_MESSAGE_COUNT = 20;
+const MESSAGE_SIZE = 10;
const RollingPaperPage = () => {
const id = useParams().id ?? '';
const [activeComment, setActiveComment] = useState(null);
const [activeDetailModal, setActiveDetailModal] = useState(false);
const [activeDeleteModal, setActiveDeleteModal] = useState(false);
+ const zipCode = useAuthStore((props) => props.zipCode);
const queryClient = useQueryClient();
- const { data } = useQuery({
+ const { data, isSuccess, fetchNextPage, hasNextPage, isFetchingNextPage } = useInfiniteQuery({
queryKey: ['rolling-paper', id],
- queryFn: () => getRollingPaperDetail(id),
+ queryFn: ({ pageParam = 1 }) => getRollingPaperDetail(id, pageParam, MESSAGE_SIZE),
+ getNextPageParam: (lastPage) => {
+ const { currentPage, totalPages } = lastPage.eventPostComments;
+ return currentPage < totalPages ? currentPage + 1 : undefined;
+ },
+ initialPageParam: 1,
});
const { mutate: deleteComment } = useMutation({
mutationFn: (rollingPaperId: number | string) => deleteRollingPaperComment(rollingPaperId),
- onSuccess: (data) => {
- queryClient.setQueryData(['rolling-paper', id], (oldData: RollingPaper) => {
- if (!oldData) return oldData;
-
- return {
- ...oldData,
- eventPostComments: oldData.eventPostComments.filter(
- (comment: RollingPaperComment) => comment.commentId !== data.commentId,
- ),
- };
- });
-
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['rolling-paper', id] });
setActiveDeleteModal(false);
setActiveComment(null);
},
- onError: (err) => {
- console.error(err);
+ onError: () => {
+ alert('편지 삭제에 실패했어요. 다시 시도해주세요');
},
});
+ const handleLoadMore = useCallback(() => {
+ if (!isFetchingNextPage && hasNextPage) fetchNextPage();
+ }, [fetchNextPage, hasNextPage, isFetchingNextPage]);
+
+ const allComments = data?.pages.flatMap((page) => page.eventPostComments.content) || [];
+ const totalComments = data?.pages[0]?.eventPostComments.totalElements || 0;
+ const title = data?.pages[0]?.title || '';
+
return (
<>
{activeDetailModal && activeComment && (
{
setActiveDetailModal(false);
setActiveComment(null);
@@ -83,13 +86,13 @@ const RollingPaperPage = () => {
/>
)}
-
- {data?.title}
- 등록된 편지 {DUMMY_MESSAGE_COUNT}
+
+ {title}
+ 등록된 편지 {totalComments}
-
- {data &&
- data.eventPostComments.map((comment) => (
+
+ {isSuccess &&
+ allComments.map((comment) => (
{
/>
))}
+ {isSuccess && allComments.length === 0 && (
+
+ 아직 등록된 편지가 없어요.
+
+ 첫번째로 편지를 남겨볼까요?
+
+ )}
+ {isFetchingNextPage && (
+ Loading...
+ )}
diff --git a/src/pages/Write/CategorySelect.tsx b/src/pages/Write/CategorySelect.tsx
index bdabcbb..7244b0b 100644
--- a/src/pages/Write/CategorySelect.tsx
+++ b/src/pages/Write/CategorySelect.tsx
@@ -59,9 +59,7 @@ export default function CategorySelect({
작성하신 편지는
- {'00'}시간
- {'00'}분
- {'00'}초 후에 도착합니다.
+ 1시간 후에 도착합니다.
diff --git a/src/pages/Write/LetterEditor.tsx b/src/pages/Write/LetterEditor.tsx
index 4e91d38..21e7d46 100644
--- a/src/pages/Write/LetterEditor.tsx
+++ b/src/pages/Write/LetterEditor.tsx
@@ -1,34 +1,42 @@
import { useEffect, useState } from 'react';
-import { useLocation } from 'react-router';
+import { useLocation, useNavigate } from 'react-router';
import { twMerge } from 'tailwind-merge';
-import { postFirstReply, postLetter } from '@/apis/write';
+import { postFirstReply, postLetter, postTemporarySave } from '@/apis/write';
import BackButton from '@/components/BackButton';
+import ConfirmModal from '@/components/ConfirmModal';
import WritePageButton from '@/pages/Write/components/WritePageButton';
import { FONT_TYPE_OBJ } from '@/pages/Write/constants';
import OptionSlide from '@/pages/Write/OptionSlide';
import useWrite from '@/stores/writeStore';
import { removeProperty } from '@/utils/removeProperty';
+import useToastStore from '@/stores/toastStore';
+import { useQueryClient } from '@tanstack/react-query';
export default function LetterEditor({
+ letterId,
setStep,
prevLetter,
setSend,
- searchParams,
isReply,
}: {
- setStep: React.Dispatch>;
+ letterId: string | null;
+ isReply: boolean;
prevLetter: PrevLetter[];
+ setStep: React.Dispatch>;
setSend: React.Dispatch>;
- searchParams: URLSearchParams;
- isReply: boolean;
}) {
const location = useLocation();
+ const queryClient = useQueryClient();
+ const navigate = useNavigate();
const [randomMatched, setRandomMatched] = useState(false);
+ const [isTemporaryConfirmModal, setIsTemporaryConfirmModal] = useState(false);
const letterRequest = useWrite((state) => state.letterRequest);
const setLetterRequest = useWrite((state) => state.setLetterRequest);
+ const setToastActive = useToastStore((state) => state.setToastActive);
+
const handlePostFirstReply = async (firstReplyRequest: Omit) => {
const res = await postFirstReply(firstReplyRequest);
if (res?.status === 200) {
@@ -39,7 +47,6 @@ export default function LetterEditor({
}
};
- // MEMO : 답장 전송 matchingId가 undefined로 나오는데 뭐 때문인지 내일 찾아보자 ㅎ
const handlePostReply = async (letterRequest: LetterRequest) => {
const res = await postLetter(letterRequest);
if (res?.status === 200) {
@@ -63,35 +70,77 @@ export default function LetterEditor({
console.log('prevLetter', prevLetter);
setLetterRequest({
receiverId: prevLetter[0].memberId,
- parentLetterId: Number(searchParams.get('letterId')),
+ parentLetterId: Number(letterId),
category: prevLetter[0].category,
matchingId: prevLetter[0].matchingId,
});
}
- }, [prevLetter, searchParams, setLetterRequest, isReply]);
+ }, [prevLetter, setLetterRequest, isReply]);
+
+ const handlePostTemporarySave = async () => {
+ if (!letterId) return alert('임시저장중 오류 발생');
+ const LETTER_STATE_DUMMY = false;
+ const requestLetterId = LETTER_STATE_DUMMY || null;
+ // MEMO : 임시저장 전송 방식 : 최초임시저장은 letterId : null, 임시저장 업데이트는 letterId : location state로 받아오는 임시저장편지의 letterId값
+ const temporaryRequest: TemporaryRequest = { ...letterRequest, letterId: requestLetterId };
+ const res = await postTemporarySave(temporaryRequest);
+ if (res?.status === 200) {
+ console.log(res);
+ navigate('/');
+ } else {
+ alert('실패');
+ }
+ };
return (
+ {isTemporaryConfirmModal && (
+
setIsTemporaryConfirmModal(false)}
+ onConfirm={() => {
+ handlePostTemporarySave();
+ }}
+ />
+ )}
{isReply ? (
-
{
- if (letterRequest.title.trim() !== '' && letterRequest.content.trim() !== '') {
- if (randomMatched) {
- const firstReplyRequest = removeProperty(letterRequest, ['matchingId']);
- console.log(firstReplyRequest);
- handlePostFirstReply(firstReplyRequest);
+
+ {!randomMatched && (
+ {
+ setIsTemporaryConfirmModal(true);
+ }}
+ />
+ )}
+ {
+ if (letterRequest.title.trim() !== '' && letterRequest.content.trim() !== '') {
+ if (randomMatched) {
+ const firstReplyRequest = removeProperty(letterRequest, ['matchingId']);
+ console.log(firstReplyRequest);
+ handlePostFirstReply(firstReplyRequest);
+ } else {
+ handlePostReply(letterRequest);
+ }
+ queryClient.invalidateQueries({ queryKey: ['mailBox'] });
+ queryClient.invalidateQueries({ queryKey: ['mailBoxDetail'] });
} else {
- handlePostReply(letterRequest);
+ setToastActive({
+ toastType: 'Warning',
+ title: '편지 제목, 내용이 작성되었는지 확인해주세요',
+ });
}
- } else {
- alert('편지 제목, 내용이 작성되었는지 확인해주세요');
- }
- }}
- />
+ }}
+ />
+
) : (
@@ -120,7 +172,7 @@ export default function LetterEditor({