diff --git a/README.md b/README.md index 628dbf3..4b4ebd7 100644 --- a/README.md +++ b/README.md @@ -1 +1,62 @@ # Emotional-Core + +## 프로젝트 + +감성코어는 사용자가 시, 소설, 웹툰과 같은 창작물을 업로드하고, 댓글과 북마크 등으로 소통할 수 있는 창작물 커뮤니티 웹 서비스입니다. + +## Team Member(FE) + +| 박지원(Leader) | 김선진 | 이현지 | +| ------------------------------------------ | -------------------------------------- | ---------------------------------------- | +| [@PJW980921](https://github.com/PJW980921) | [@SJ-1220](https://github.com/SJ-1220) | [@Hyunji0012](https://github.com/za0012) | + +백엔드 3명, 프론트엔드 3명, 디자이너 2명으로 구성된 협업 프로젝트입니다. + +## 프로젝트 개요 + +게시판 형식을 활용한 창작물 공유 커뮤니티 사이트 개발 + +- **이름**: 감성코어(Emotional-Core) +- **형식**: 게시판 기반 창작물 공유 커뮤니티 +- **기획/디자인**: Figma +- **배포**: Vercel(총 3차 중 2차 배포 완료 기준 작성) + +## 기술 스택 + +- **Frontend**: React, TypeScript, Tailwind CSS +- **Backend**: Spring Boot, JWT 기반 인증, Swagger +- **Deployment**: Vercel (FE), ?? (BE) +- **협업 툴**: GitHub, Notion, Figma + +## 주요 기능 + +1. **회원 기능** + +- 일반 로그인 / 회원가입 (React Hook Form) +- 소셜 로그인: Google, Kakao, Naver +- JWT 기반 인증 (AccessToken: Body / RefreshToken: Cookie) +- 마이페이지: 회원 정보 수정, 탈퇴 기능 + +2. **게시판 기능** + +- 카테고리별 게시판 제공: 시 / 소설 / 웹툰 +- 작품 등록: 제목, 이미지, 카테고리, 장르, 내용 +- 장르 분류: 전체, 일상, 에세이, 판타지, 로맨스, 공포, SF, 스포츠, 기타 +- 댓글, 좋아요, 북마크 기능 + +3. **검색 & 추천** + +- 검색: 작가명 / 작품명 기준 검색 +- 추천 콘텐츠: ?? + +4. **메인 페이지 구성** + +- 게시판 이동 탭 +- 이달의 인기 작품 (글/시/웹툰 각 3개) +- 이달의 우수 작가 (총 3명, 한줄 소개 및 대표작) +- 인기 Best 작품 TOP 5 +- 추천 콘텐츠: 시(3), 소설(3), 웹툰(6) + +## 프로젝트 구조 + +## 프로젝트 수행 결과(영상) diff --git a/src/app/(sub pages)/(social login)/auth/oauth2/kakao/page.tsx b/src/app/(sub pages)/(social login)/auth/oauth2/kakao/page.tsx index 5a1c8fb..0aeefd8 100644 --- a/src/app/(sub pages)/(social login)/auth/oauth2/kakao/page.tsx +++ b/src/app/(sub pages)/(social login)/auth/oauth2/kakao/page.tsx @@ -26,8 +26,8 @@ const KakaoPage = () => { if (res.ok) { // accessToken 받기 - const { accessToken } = await res.json(); - localStorage.setItem('accessToken', accessToken); + const { access_token } = await res.json(); + localStorage.setItem('accessToken', access_token); router.push('/'); } else { console.error('토큰 요청 실패'); diff --git a/src/app/(sub pages)/(social login)/login/oauth2/code/google/page.tsx b/src/app/(sub pages)/(social login)/login/oauth2/code/google/page.tsx index 12534fb..d412903 100644 --- a/src/app/(sub pages)/(social login)/login/oauth2/code/google/page.tsx +++ b/src/app/(sub pages)/(social login)/login/oauth2/code/google/page.tsx @@ -26,8 +26,8 @@ const GooglePage = () => { if (res.ok) { // accessToken 받기 - const { accessToken } = await res.json(); - localStorage.setItem('accessToken', accessToken); + const { access_token } = await res.json(); + localStorage.setItem('accessToken', access_token); router.push('/'); } else { console.error('토큰 요청 실패'); diff --git a/src/app/(sub pages)/(social login)/signin/naver/page.tsx b/src/app/(sub pages)/(social login)/signin/naver/page.tsx index df4233d..2fb45c5 100644 --- a/src/app/(sub pages)/(social login)/signin/naver/page.tsx +++ b/src/app/(sub pages)/(social login)/signin/naver/page.tsx @@ -28,8 +28,8 @@ const NaverPage = () => { if (res.ok) { // accessToken 받기 - const { accessToken } = await res.json(); - localStorage.setItem('accessToken', accessToken); + const { access_token } = await res.json(); + localStorage.setItem('accessToken', access_token); router.push('/'); } else { console.error('토큰 요청 실패'); diff --git a/src/app/_lib/axios/instance/instance.ts b/src/app/_lib/axios/instance/instance.ts index 4a0b875..9f8ed8d 100644 --- a/src/app/_lib/axios/instance/instance.ts +++ b/src/app/_lib/axios/instance/instance.ts @@ -47,7 +47,9 @@ const applyRequestInterceptor = (instance: typeof apiInstance) => { const applyResponseInterceptor = (instance: typeof apiInstance) => { instance.interceptors.response.use( (response) => response, - (error) => { + async (error) => { + const originalRequest = error.config; + if (isAxiosError(error) && error.response) { const status = error.response.status; switch (status) { @@ -55,7 +57,22 @@ const applyResponseInterceptor = (instance: typeof apiInstance) => { console.error('400: Bad Request'); break; case HttpStatusCode.Unauthorized: - console.error('401: Unauthorized - Token may have expired'); + if (!originalRequest._retry) { + originalRequest._retry = true; + console.error('401: Unauthorized - Token may have expired'); + + try { + const refreshToken = await apiInstance.post('/api/member/token/refresh', {}, { withCredentials: true }); + console.log('refreshToken 전체 response:', refreshToken); + const newAccsessToken = refreshToken.data.accessToken; + console.log('New access token received:', newAccsessToken); + localStorage.setItem('accessToken', newAccsessToken); + originalRequest.headers.Authorization = `Bearer ${newAccsessToken}`; + return instance(originalRequest); + } catch (error) { + console.error('Token refresh failed:', error); + } + } break; case HttpStatusCode.Forbidden: console.error('403: Forbidden');