diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml new file mode 100644 index 0000000..746ef91 --- /dev/null +++ b/.github/workflows/CI.yml @@ -0,0 +1,47 @@ +name: Frontend CI + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] +jobs: + + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up Node.js + uses: actions/setup-node@v3 + with: + node-version: '20.14.0' + + - name: Install yarn + run: npm install -g yarn + + - name: Cache yarn dependencies + uses: actions/cache@v3 + with: + path: | + ~/.yarn/cache + node_modules + key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-yarn- + + - name: Install dependencies + run: yarn install + + - name: Build project + run: yarn build + + +# - name: Trigger Docker CI/CD +# run: | +# curl -X POST -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \ +# -H "Accept: application/vnd.github.v3+json" \ +# https://api.github.com/repos/your-org/docker-repo/dispatches \ +# -d '{"event_type":"frontend_updated"}' diff --git a/.gitignore b/.gitignore index 0a8e43a..542eba4 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,5 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts + +.idea diff --git a/src/api/auth.ts b/src/api/auth.ts index ffa6a9e..a524637 100644 --- a/src/api/auth.ts +++ b/src/api/auth.ts @@ -1,11 +1,8 @@ -/* eslint-disable import/prefer-default-export */ -/* eslint-disable no-console */ - export function GoogleLogin() { try { window.location.href = `${process.env.NEXT_PUBLIC_BASE_URL}/oauth2/authorization/google`; } catch (error) { - console.error('구글 로그인 에러 : ', error); + throw new Error('구글 로그인 에러 : ', error || ''); } } @@ -13,7 +10,7 @@ export function KakaoLogin() { try { window.location.href = `${process.env.NEXT_PUBLIC_BASE_URL}/oauth2/authorization/kakao`; } catch (error) { - console.error('카카오 로그인 에러 : ', error); + throw new Error('카카오 로그인 에러 : ', error || ''); } } @@ -21,6 +18,6 @@ export function NaverLogin() { try { window.location.href = `${process.env.NEXT_PUBLIC_BASE_URL}/oauth2/authorization/naver`; } catch (error) { - console.error('네이버 로그인 에러 : ', error); + throw new Error('네이버 로그인 에러 : ', error || ''); } } diff --git a/src/api/axios.config.ts b/src/api/axios.config.ts index 5fc1828..a0cb3bc 100644 --- a/src/api/axios.config.ts +++ b/src/api/axios.config.ts @@ -1,5 +1,6 @@ -/* eslint-disable no-underscore-dangle */ +/* eslint-disable no-param-reassign */ /* eslint-disable consistent-return */ +/* eslint-disable no-underscore-dangle */ import axios from 'axios'; import { getCookie, setCookie } from '@/app/cookies.tsx'; @@ -11,12 +12,12 @@ const reIssuedToken = async () => { const response = await axios.post( `${BASE_URL}/reissue`, { - access_token: getCookie('accessToken'), // 액세스 토큰을 사용하고 있으나, 일반적으로는 사용하지 않습니다. - refresh_token: getCookie('refreshToken'), // 리프레시 토큰 + access_token: getCookie('accessToken'), + refresh_token: getCookie('refreshToken'), }, { headers: { - 'Content-Type': 'application/json', // 요청의 본문 타입을 지정 + 'Content-Type': 'application/json', Authorization: `Bearer ${accessToken}`, }, withCredentials: true, // CORS 요청 시 쿠키를 포함 @@ -30,40 +31,48 @@ const reIssuedToken = async () => { } return response.data; } catch (error) { - console.error('Token reissue error:', error); - throw error; // 오류를 상위로 전파하여 호출자가 이를 처리할 수 있도록 합니다. + setCookie('accessToken', ''); + setCookie('refreshToken', ''); } }; const api = axios.create({ withCredentials: true, baseURL: BASE_URL, // 기본 URL 설정 - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${accessToken}`, - }, }); +// 요청 인터셉터를 추가하여 모든 요청에 최신 토큰을 포함시킵니다. +api.interceptors.request.use( + (config) => { + const token = getCookie('accessToken'); // 요청 직전에 액세스 토큰을 쿠키에서 가져옵니다. + config.headers.Authorization = `Bearer ${token}`; + config.headers['Content-Type'] = 'application/json'; + return config; + }, + (error) => Promise.reject(error), +); + api.interceptors.response.use( (response) => response, // 성공 응답은 그대로 반환 async (error) => { - const originalRequest = error.config; - if (error.response.status === 401 && !originalRequest._retry) { - originalRequest._retry = true; // 재요청 플래그를 설정하여 무한 루프 방지 - try { - const data = await reIssuedToken(); // 토큰 재발급 함수 호출 - // 재발급 받은 토큰으로 요청 헤더 설정 - api.defaults.headers.common.Authorization = `Bearer ${data.data.access_token}`; - originalRequest.headers.Authorization = `Bearer ${data.data.access_token}`; + if (accessToken !== undefined) { + const originalRequest = error.config; + if (error.response.status === 401 && !originalRequest._retry) { + originalRequest._retry = true; // 재요청 플래그를 설정하여 무한 루프 방지 + try { + const data = await reIssuedToken(); // 토큰 재발급 함수 호출 + // 재발급 받은 토큰으로 요청 헤더 설정 + api.defaults.headers.common.Authorization = `Bearer ${data.data.access_token}`; + originalRequest.headers.Authorization = `Bearer ${data.data.access_token}`; - return api(originalRequest); // 원래 요청 재시도 - } catch (refreshError) { - console.error('Failed to refresh token:', refreshError); - return Promise.reject(refreshError); + return api(originalRequest); // 원래 요청 재시도 + } catch (refreshError) { + return Promise.reject(refreshError); + } } - } - return Promise.reject(error); + return Promise.reject(error); + } }, ); @@ -72,8 +81,35 @@ export { api }; export const formApi = axios.create({ withCredentials: true, baseURL: BASE_URL, - headers: { - 'Content-Type': 'multipart/form-data', - Authorization: `Bearer ${accessToken}`, - }, }); + +formApi.interceptors.request.use( + (config) => { + const token = getCookie('accessToken'); + config.headers.Authorization = `Bearer ${token}`; + return config; + }, + (error) => Promise.reject(error), +); + +formApi.interceptors.response.use( + (response) => response, + async (error) => { + if (accessToken !== undefined) { + const originalRequest = error.config; + if (error.response.status === 401 && !originalRequest._retry) { + originalRequest._retry = true; + try { + const data = await reIssuedToken(); + formApi.defaults.headers.common.Authorization = `Bearer ${data.data.access_token}`; + originalRequest.headers.Authorization = `Bearer ${data.data.access_token}`; + + return formApi(originalRequest); + } catch (refreshError) { + return Promise.reject(refreshError); + } + } + return Promise.reject(error); + } + }, +); diff --git a/src/api/chat.ts b/src/api/chat.ts new file mode 100644 index 0000000..7803e18 --- /dev/null +++ b/src/api/chat.ts @@ -0,0 +1,19 @@ +import { api } from './axios.config.ts'; + +export const deleteChat = async (chatId: number) => { + try { + const response = await api.delete(`chattings/rooms/${chatId}`); + return response.data; + } catch (error) { + throw new Error('chat delete api request error : ', error || ''); + } +}; + +export const getChats = async (meetingId: number) => { + try { + const response = await api.get(`/chats/${meetingId}`); + return response.data.data; + } catch (error) { + throw new Error('chat get api request error : ', error || ''); + } +}; diff --git a/src/api/check.ts b/src/api/check.ts deleted file mode 100644 index 520c40f..0000000 --- a/src/api/check.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable import/prefer-default-export */ -/* eslint-disable consistent-return */ -/* eslint-disable no-template-curly-in-string */ -/* eslint-disable no-console */ -import { api } from './axios.config.ts'; - -export async function profileCheck() { - try { - const response = await api.get('/users/${userId}'); - return response.data; - } catch (error) { - console.error('sample error : ', error); - } -} - -export async function bannerCheck() { - try { - const response = await api.get('/users/${userId}'); - return response.data; - } catch (error) { - console.error('sample error : ', error); - } -} diff --git a/src/api/kakao.ts b/src/api/kakao.ts index b0b9770..9a4f0ef 100644 --- a/src/api/kakao.ts +++ b/src/api/kakao.ts @@ -1,9 +1,6 @@ -/* eslint-disable import/prefer-default-export */ -/* eslint-disable consistent-return */ -/* eslint-disable no-console */ import axios from 'axios'; -export async function searchAddress( +export default async function searchAddress( keyword: string, lat: number, lng: number, @@ -11,7 +8,7 @@ export async function searchAddress( size: number, ) { if (!keyword) { - return; + return null; } try { const response = await axios.get( @@ -30,9 +27,8 @@ export async function searchAddress( }, }, ); - console.log('searchAddress response : ', response.data); return response.data; } catch (error) { - console.error('searchAddress error : ', error); + throw new Error('kakao search address api request error : ', error || ''); } } diff --git a/src/api/meetings.ts b/src/api/meetings.ts index 8f6b02e..df9f8b6 100644 --- a/src/api/meetings.ts +++ b/src/api/meetings.ts @@ -1,7 +1,3 @@ -/* eslint-disable no-console */ -/* eslint-disable consistent-return */ -/* eslint-disable import/prefer-default-export */ - import { formApi, api } from './axios.config.ts'; export const postMeetings = async ( @@ -47,7 +43,7 @@ export const postMeetings = async ( const response = await formApi.post('/meetings', formData); return response.data; } catch (error) { - console.error('postMeetings error : ', error); + throw new Error('meeting post api request error : ', error || ''); } }; @@ -56,10 +52,9 @@ export const getMeetingList = async (page: number, size: number) => { const response = await api.get( `/meetings/search/latest?page=${page}&size=${size}`, ); - console.log('response.data : ', response.data); return response.data; } catch (error) { - console.error('Meeting List Get API Error : ', error); + throw new Error('meeting list get api request error : ', error || ''); } }; @@ -68,7 +63,7 @@ export const getMeetingsData = async (meetingId: number) => { const response = await api.get(`/meetings/${meetingId}`); return response.data.data; } catch (error) { - console.error('meeting data get api request error : ', error); + throw new Error('meeting data get api request error : ', error || ''); } }; @@ -77,7 +72,7 @@ export const postMeetingJoin = async (meetingId: number) => { const response = await api.post(`/meetings/participant/${meetingId}`); return response.data; } catch (error) { - console.error('meeting join api request error : ', error); + throw new Error('meeting join post api request error : ', error || ''); } }; @@ -86,7 +81,7 @@ export const patchEndMeeting = async (meetingId: number) => { const response = await api.patch(`/meetings/end/${meetingId}`); return response.data; } catch (error) { - console.error('meeting end api request error : ', error); + throw new Error('meeting end patch api request error : ', error || ''); } }; @@ -95,7 +90,7 @@ export const deleteMeeting = async (meetingId: number) => { const response = await api.delete(`/meetings/${meetingId}`); return response.data; } catch (error) { - console.error('meeting delete api request error : ', error); + throw new Error('meeting delete api request error : ', error || ''); } }; @@ -123,7 +118,7 @@ export const editMeeting = async ( }); return response.data; } catch (error) { - console.error('meeting edit api request error : ', error); + throw new Error('meeting edit api request error : ', error || ''); } }; @@ -132,7 +127,7 @@ export const getMeetingApplicants = async (meetingId: number) => { const response = await api.get(`/meetings/${meetingId}/participants`); return response.data.data; } catch (error) { - console.error('meeting applicants get api request error : ', error); + throw new Error('meeting applicants get api request error : ', error || ''); } }; @@ -149,7 +144,7 @@ export const patchMeetingApproval = async ( }); return response.data; } catch (error) { - console.error('meeting approval patch api request error : ', error); + throw new Error('meeting approval patch api request error : ', error || ''); } }; @@ -160,7 +155,6 @@ export const postMeetingReview = async ( rating: number; }[], ) => { - console.log('reviews', reviews); try { const response = await api.post('/meetings/reviews', { meeting_id: meetingId, @@ -168,7 +162,7 @@ export const postMeetingReview = async ( }); return response.data; } catch (error) { - console.error('meeting review post api request error : ', error); + throw new Error('meeting review post api request error : ', error || ''); } }; @@ -178,9 +172,9 @@ export const getReviewParticipation = async (meetingId: number) => { const response = await api.get(`/meetings/reviews/${meetingId}`); return response.data; } catch (error) { - console.error( + throw new Error( 'meeting review participation get api request error : ', - error, + error || '', ); } }; diff --git a/src/api/nearMeeting.ts b/src/api/nearMeeting.ts index 3e3202b..cad71e3 100644 --- a/src/api/nearMeeting.ts +++ b/src/api/nearMeeting.ts @@ -1,15 +1,11 @@ -/* eslint-disable import/extensions */ -/* eslint-disable import/prefer-default-export */ -/* eslint-disable no-console */ -/* eslint-disable consistent-return */ -import { api } from './axios.config'; +import { api } from './axios.config.ts'; -export const searchMeetings = async ( +const searchMeetings = async ( latitude: number, longitude: number, page: number, size: number, - sort: any, + sort: string, ) => { try { const response = await api.get( @@ -17,6 +13,8 @@ export const searchMeetings = async ( ); return response.data; } catch (error) { - console.error('searchMeetings error:', error); + throw new Error('주변 모임 검색 에러 : ', error || ''); } }; + +export default searchMeetings; diff --git a/src/api/search.ts b/src/api/search.ts index e0b74c3..969def9 100644 --- a/src/api/search.ts +++ b/src/api/search.ts @@ -1,9 +1,6 @@ -/* eslint-disable no-console */ -/* eslint-disable import/prefer-default-export */ -/* eslint-disable consistent-return */ import { api } from './axios.config.ts'; -export const getSearchData = async ( +const getSearchData = async ( searchType: string, searchKeyword: string, lat: number, @@ -23,6 +20,8 @@ export const getSearchData = async ( ); return response.data.data; } catch (error) { - console.error(error); + throw new Error('search api request error : ', error || ''); } }; + +export default getSearchData; diff --git a/src/api/user.ts b/src/api/user.ts index 7569394..7b490ca 100644 --- a/src/api/user.ts +++ b/src/api/user.ts @@ -1,7 +1,3 @@ -/* eslint-disable no-undef */ -/* eslint-disable import/prefer-default-export */ -/* eslint-disable consistent-return */ -/* eslint-disable no-console */ import { api, formApi } from './axios.config.ts'; // 회원가입시 정보 입력 @@ -19,8 +15,8 @@ export async function signupUserInfo( location, }); return response.data; - } catch (error: any) { - console.error('users error : ', error.response); + } catch (error) { + throw new Error('users error : ', error || ''); } } @@ -29,8 +25,8 @@ export async function getUserInfo(userId: number) { try { const response = await api.get(`/users/${userId}`); return response.data; - } catch (error: any) { - console.error('users error : ', error.response); + } catch (error) { + throw new Error('users error : ', error || ''); } } @@ -74,10 +70,9 @@ export async function putUserInfo( try { const response = await formApi.put('/users', formData); - console.log(response.data); return response.data; - } catch (error: any) { - console.error('users error : ', error.response); + } catch (error) { + throw new Error('users error : ', error || ''); } } @@ -87,7 +82,7 @@ export async function login() { const response = await api.get('/users/me'); return response.data; } catch (error) { - console.error('Get User ID Error : ', error); + throw new Error('users error : ', error || ''); } } @@ -97,7 +92,7 @@ export async function logout() { const response = await api.post('/logout'); return response.data; } catch (error) { - console.error('Logout Error : ', error); + throw new Error('users error : ', error || ''); } } @@ -121,7 +116,8 @@ export async function getMyMeetingList( ); return response.data; } + throw new Error('users error : tab error'); } catch (error) { - console.error('Get Participant List Error : ', error); + throw new Error('users error : ', error || ''); } } diff --git a/src/app/account/[userIdParam]/Classification.tsx b/src/app/account/[userIdParam]/Classification.tsx index 50406ad..33473a8 100644 --- a/src/app/account/[userIdParam]/Classification.tsx +++ b/src/app/account/[userIdParam]/Classification.tsx @@ -13,6 +13,7 @@ import { useInfiniteQuery } from '@tanstack/react-query'; import { getMyMeetingList } from '@/api/user.ts'; import Post from '@/app/components/Post.tsx'; import category from '@/util/category.json'; +import { meetingListProps } from '@/types/meeting.ts'; const Classification = ({ id }: accountIdProps) => { const [activeTab, setActiveTab] = useState<'joined' | 'created'>('joined'); @@ -20,12 +21,13 @@ const Classification = ({ id }: accountIdProps) => { const loadMoreRef = useRef(null); const { data, fetchNextPage, hasNextPage, refetch } = useInfiniteQuery({ - queryKey: ['myMeetingList'], + queryKey: ['My Meeting List', activeTab], queryFn: ({ pageParam = 0 }: { pageParam: number }) => getMyMeetingList(activeTab, id, pageParam, 24), initialPageParam: 0, getNextPageParam: (lastPage: { data: { + meeting_list: meetingListProps[]; current_page: number; total_page: number; }; @@ -94,20 +96,22 @@ const Classification = ({ id }: accountIdProps) => { {/* 모임 리스트 */}
- {data?.pages.map((pageData: any, pageIndex) => - pageData?.data.meeting_list.map((meeting: any, index: any) => ( - - )), + {data?.pages.map((pageData, pageIndex: number) => + pageData?.data.meeting_list.map( + (meeting: meetingListProps, index: number) => ( + + ), + ), )}
diff --git a/src/app/account/[userIdParam]/Profile.tsx b/src/app/account/[userIdParam]/Profile.tsx index 0ea02fa..adfd5c9 100644 --- a/src/app/account/[userIdParam]/Profile.tsx +++ b/src/app/account/[userIdParam]/Profile.tsx @@ -8,7 +8,7 @@ 'use client'; -import { useEffect, useState, useRef } from 'react'; +import React, { useEffect, useState, useRef } from 'react'; import Image from 'next/image'; import { getUserInfo, putUserInfo } from '@/api/user.ts'; import { useQuery } from '@tanstack/react-query'; @@ -58,7 +58,7 @@ function Profile({ id }: accountIdProps) { }, [data]); const BannerImage = () => { - const handleFileChange = (event: any) => { + const handleFileChange = (event: React.ChangeEvent) => { const file = event.target.files?.[0] || null; if (file) { setPreviewBanner(file); @@ -101,7 +101,7 @@ function Profile({ id }: accountIdProps) { }; const ProfileImage = () => { - const handleFileChange = (event: any) => { + const handleFileChange = (event: React.ChangeEvent) => { const file = event.target.files?.[0] || null; if (file) { setPreviewProfile(file); @@ -110,7 +110,7 @@ function Profile({ id }: accountIdProps) { return (
-
+
{isEdit && ( @@ -247,10 +247,11 @@ function Profile({ id }: accountIdProps) { setStatusMessage(statusMessageRef.current); } }} + placeholder="소개 메세지를 입력해주세요." /> ) : (

- {statusMessage} + {statusMessage || `${name}님의 프로필입니다.`}

)}
@@ -259,7 +260,7 @@ function Profile({ id }: accountIdProps) { }; return ( -
+
diff --git a/src/app/account/[userIdParam]/page.tsx b/src/app/account/[userIdParam]/page.tsx index 888b596..793c668 100644 --- a/src/app/account/[userIdParam]/page.tsx +++ b/src/app/account/[userIdParam]/page.tsx @@ -1,13 +1,14 @@ +/* eslint-disable eol-last */ /* eslint-disable no-shadow */ import Classification from './Classification.tsx'; import Profile from './Profile.tsx'; -function Account(props: any) { +function Account(props: { params: { userIdParam: string } }) { const userId = Number(decodeURIComponent(props.params.userIdParam)); return ( -
+
diff --git a/src/app/auth/[loginType]/page.tsx b/src/app/auth/[loginType]/page.tsx index 6e89e0c..a3dc690 100644 --- a/src/app/auth/[loginType]/page.tsx +++ b/src/app/auth/[loginType]/page.tsx @@ -1,13 +1,16 @@ /* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/rules-of-hooks */ +/* eslint-disable react-hooks/exhaustive-deps */ +/* eslint-disable react-hooks/rules-of-hooks */ + 'use client'; import { useSearchParams, useRouter } from 'next/navigation'; import React, { useEffect, useState } from 'react'; import { Cookies } from 'react-cookie'; -function page(props: any) { +function Page(props: { params: { loginType: string } }) { const loginType = decodeURIComponent(props.params.loginType); const cookies = new Cookies(); const searchParams = useSearchParams(); @@ -30,6 +33,7 @@ function page(props: any) { } } }, [accessToken, refreshToken]); + return (
로그인 중... @@ -37,4 +41,4 @@ function page(props: any) { ); } -export default page; +export default Page; diff --git a/src/app/chat/ChatList.tsx b/src/app/chat/ChatList.tsx index a765082..1a5910e 100644 --- a/src/app/chat/ChatList.tsx +++ b/src/app/chat/ChatList.tsx @@ -2,7 +2,7 @@ import Image from 'next/image'; import { chatListProps } from '@/types/chat.ts'; // 마지막 채팅 시간을 문자열로 변환하는 함수 ( "yyyy-MM-dd HH:mm:ss" 형식의 문자열을 입력받음 ) -function lastTimeStr(lastTime: any) { +function lastTimeStr(lastTime: string) { const lastTimeDate = new Date(lastTime); const now = new Date(); diff --git a/src/app/components/Header.tsx b/src/app/components/Header.tsx index b4a923f..05f29ee 100644 --- a/src/app/components/Header.tsx +++ b/src/app/components/Header.tsx @@ -1,6 +1,9 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +/* eslint-disable operator-linebreak */ + 'use client'; -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import { Montserrat } from 'next/font/google'; import Link from 'next/link'; import { usePathname, useRouter } from 'next/navigation'; @@ -20,17 +23,25 @@ function Header() { const router = useRouter(); const path = usePathname() || ''; - const { data, isLoading, isError } = useQuery({ + const { data, isLoading } = useQuery({ queryKey: ['login'], queryFn: login, }); - // 토큰이 있는데 401 에러가 발생하면 새로고침 - if (isError && getCookie('accessToken')) { - setTimeout(() => { - window.location.reload(); - }, 100); - } + const checkAuthentication = async () => { + if ( + !getCookie('accessToken') && + !getCookie('refreshToken') && + path !== '/auth/login' && + path !== '/auth/signup' + ) { + await router.push('/login'); + } + }; + + useEffect(() => { + checkAuthentication(); + }, []); if (ignorePath().includes(path)) { return null; @@ -145,7 +156,7 @@ function Header() { {category.category_name.map((item: string, index: number) => ( setOpenMenu(false)} > @@ -165,7 +176,7 @@ function Header() { {/* 메뉴 푸터 */}