diff --git a/frontend/package-lock.json b/frontend/package-lock.json index f8e1f60a1..a8eefaf1b 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -39,6 +39,7 @@ "@types/styled-components": "^5.1.26", "@typescript-eslint/eslint-plugin": "^6.0.0", "@typescript-eslint/parser": "^6.0.0", + "axios": "^1.6.1", "babel-loader": "^9.1.3", "copy-webpack-plugin": "^11.0.0", "core-js": "^3.31.1", @@ -7760,6 +7761,31 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/axios": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.1.tgz", + "integrity": "sha512-vfBmhDpKafglh0EldBEbVuoe7DyAavGSLWhuSm5ZSEKQnHhBf0xAAwybbNH1IkrJNGnS/VG4I5yxig1pCEXE4g==", + "dev": true, + "dependencies": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/axios/node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dev": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/babel-core": { "version": "7.0.0-bridge.0", "resolved": "https://registry.npmjs.org/babel-core/-/babel-core-7.0.0-bridge.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 45c878afd..dbbc95a82 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -43,6 +43,7 @@ "@types/styled-components": "^5.1.26", "@typescript-eslint/eslint-plugin": "^6.0.0", "@typescript-eslint/parser": "^6.0.0", + "axios": "^1.6.1", "babel-loader": "^9.1.3", "copy-webpack-plugin": "^11.0.0", "core-js": "^3.31.1", diff --git a/frontend/src/features/auth/remotes/auth.ts b/frontend/src/features/auth/remotes/auth.ts new file mode 100644 index 000000000..fd77a7a63 --- /dev/null +++ b/frontend/src/features/auth/remotes/auth.ts @@ -0,0 +1,16 @@ +import { client, clientBasic } from '@/shared/remotes/axios'; +import type { AccessTokenRes } from '../types/auth.type'; + +export const getAccessToken = async (platform: string, code: string) => { + const { data } = await client.get(`/login/${platform}`, { + params: { code }, + }); + + return data; +}; + +export const postRefreshAccessToken = async () => { + const { data } = await clientBasic.post('/reissue'); + + return data; +}; diff --git a/frontend/src/features/auth/types/auth.type.ts b/frontend/src/features/auth/types/auth.type.ts new file mode 100644 index 000000000..fef720746 --- /dev/null +++ b/frontend/src/features/auth/types/auth.type.ts @@ -0,0 +1,3 @@ +export interface AccessTokenRes { + accessToken: string; +} diff --git a/frontend/src/features/comments/components/CommentForm.tsx b/frontend/src/features/comments/components/CommentForm.tsx index 414f47a6d..97d3c60b5 100644 --- a/frontend/src/features/comments/components/CommentForm.tsx +++ b/frontend/src/features/comments/components/CommentForm.tsx @@ -8,23 +8,23 @@ import Avatar from '@/shared/components/Avatar'; import useModal from '@/shared/components/Modal/hooks/useModal'; import useToastContext from '@/shared/components/Toast/hooks/useToastContext'; import { useMutation } from '@/shared/hooks/useMutation'; -import fetcher from '@/shared/remotes'; +import { postComment } from '../remotes/comments'; interface CommentFormProps { - getComment: () => Promise; + getComments: () => Promise; songId: number; partId: number; } -const CommentForm = ({ getComment, songId, partId }: CommentFormProps) => { +const CommentForm = ({ getComments, songId, partId }: CommentFormProps) => { const [newComment, setNewComment] = useState(''); const { isOpen, closeModal: closeLoginModal, openModal: openLoginModal } = useModal(); const { user } = useAuthContext(); const isLoggedIn = !!user; - const { mutateData } = useMutation(() => - fetcher(`/songs/${songId}/parts/${partId}/comments`, 'POST', { content: newComment.trim() }) + const { mutateData: postNewComment } = useMutation(() => + postComment(songId, partId, newComment.trim()) ); const { showToast } = useToastContext(); @@ -38,11 +38,11 @@ const CommentForm = ({ getComment, songId, partId }: CommentFormProps) => { const submitNewComment: React.FormEventHandler = async (event) => { event.preventDefault(); - await mutateData(); + await postNewComment(); showToast('댓글이 등록되었습니다.'); resetNewComment(); - getComment(); + await getComments(); }; return ( diff --git a/frontend/src/features/comments/components/CommentList.tsx b/frontend/src/features/comments/components/CommentList.tsx index eca833919..8ddb99001 100644 --- a/frontend/src/features/comments/components/CommentList.tsx +++ b/frontend/src/features/comments/components/CommentList.tsx @@ -6,17 +6,10 @@ import useModal from '@/shared/components/Modal/hooks/useModal'; import Spacing from '@/shared/components/Spacing'; import SRHeading from '@/shared/components/SRHeading'; import useFetch from '@/shared/hooks/useFetch'; -import fetcher from '@/shared/remotes'; +import { getComments } from '../remotes/comments'; import Comment from './Comment'; import CommentForm from './CommentForm'; -interface Comment { - id: number; - content: string; - createdAt: string; - writerNickname: string; -} - interface CommentListProps { songId: number; partId: number; @@ -24,12 +17,12 @@ interface CommentListProps { const CommentList = ({ songId, partId }: CommentListProps) => { const { isOpen, openModal, closeModal } = useModal(false); - const { data: comments, fetchData: getComment } = useFetch(() => - fetcher(`/songs/${songId}/parts/${partId}/comments`, 'GET') + const { data: comments, fetchData: refetchComments } = useFetch(() => + getComments(songId, partId) ); useEffect(() => { - getComment(); + refetchComments(); }, [partId]); if (!comments) { @@ -73,7 +66,7 @@ const CommentList = ({ songId, partId }: CommentListProps) => { ))} - + ); diff --git a/frontend/src/features/comments/remotes/comments.ts b/frontend/src/features/comments/remotes/comments.ts new file mode 100644 index 000000000..ed4f68e85 --- /dev/null +++ b/frontend/src/features/comments/remotes/comments.ts @@ -0,0 +1,12 @@ +import { client } from '@/shared/remotes/axios'; +import type { Comment } from '../types/comment.type'; + +export const postComment = async (songId: number, partId: number, content: string) => { + await client.post(`/songs/${songId}/parts/${partId}/comments`, { content }); +}; + +export const getComments = async (songId: number, partId: number) => { + const { data } = await client.get(`/songs/${songId}/parts/${partId}/comments`); + + return data; +}; diff --git a/frontend/src/features/comments/types/comment.type.ts b/frontend/src/features/comments/types/comment.type.ts new file mode 100644 index 000000000..914d85798 --- /dev/null +++ b/frontend/src/features/comments/types/comment.type.ts @@ -0,0 +1,6 @@ +export interface Comment { + id: number; + content: string; + createdAt: string; + writerNickname: string; +} diff --git a/frontend/src/features/killingParts/components/RegisterPart.tsx b/frontend/src/features/killingParts/components/RegisterPart.tsx index e5a388415..bda701750 100644 --- a/frontend/src/features/killingParts/components/RegisterPart.tsx +++ b/frontend/src/features/killingParts/components/RegisterPart.tsx @@ -2,19 +2,20 @@ import { useNavigate } from 'react-router-dom'; import styled from 'styled-components'; import { useAuthContext } from '@/features/auth/components/AuthProvider'; import useCollectingPartContext from '@/features/killingParts/hooks/useCollectingPartContext'; -import { usePostKillingPart } from '@/features/killingParts/remotes/usePostKillingPart'; import useVideoPlayerContext from '@/features/youtube/hooks/useVideoPlayerContext'; import useModal from '@/shared/components/Modal/hooks/useModal'; import Modal from '@/shared/components/Modal/Modal'; import Spacing from '@/shared/components/Spacing'; +import { useMutation } from '@/shared/hooks/useMutation'; import { toPlayingTimeText } from '@/shared/utils/convertTime'; +import { postKillingPart } from '../remotes/killingPart'; const RegisterPart = () => { const { isOpen, openModal, closeModal } = useModal(); const { user } = useAuthContext(); const { interval, partStartTime, songId } = useCollectingPartContext(); const video = useVideoPlayerContext(); - const { createKillingPart } = usePostKillingPart(); + const { mutateData: createKillingPart } = useMutation(postKillingPart); const navigate = useNavigate(); // 현재 useMutation 훅이 response 객체를 리턴하지 않고 내부적으로 처리합니다. diff --git a/frontend/src/features/killingParts/hooks/killingPart.ts b/frontend/src/features/killingParts/hooks/killingPart.ts deleted file mode 100644 index d711edb72..000000000 --- a/frontend/src/features/killingParts/hooks/killingPart.ts +++ /dev/null @@ -1,6 +0,0 @@ -import fetcher from '@/shared/remotes'; -import type { KillingPartPostRequest } from '@/shared/types/killingPart'; - -export const postKillingPart = async (songId: number, body: KillingPartPostRequest) => { - return await fetcher(`/songs/${songId}/member-parts`, 'POST', body); -}; diff --git a/frontend/src/features/killingParts/remotes/killingPart.ts b/frontend/src/features/killingParts/remotes/killingPart.ts new file mode 100644 index 000000000..c5257d069 --- /dev/null +++ b/frontend/src/features/killingParts/remotes/killingPart.ts @@ -0,0 +1,18 @@ +import { client } from '@/shared/remotes/axios'; +import type { KillingPartPostRequest } from '@/shared/types/killingPart'; +import type { SongInfo } from '@/shared/types/song'; + +// PartCollectingPage에 존재하던 remote 함수입니다. +// useFetch(() => fetcher(`/songs/${songId}`, 'GET')) 로직에서 분리하였습니다. +// SongInfo type에는 killingPart[] 필드가 있는데, 마이파트 수집용 `노래 1개` 조회에서 해당 타입이 사용되고 있습니다. +// 추후 수정되어야 합니다. + +export const getSong = async (songId: number) => { + const { data } = await client.get(`/songs/${songId}`); + + return data; +}; + +export const postKillingPart = async (songId: number, body: KillingPartPostRequest) => { + await client.post(`/songs/${songId}/member-parts`, body); +}; diff --git a/frontend/src/features/killingParts/remotes/usePostKillingPart.ts b/frontend/src/features/killingParts/remotes/usePostKillingPart.ts deleted file mode 100644 index c0620378e..000000000 --- a/frontend/src/features/killingParts/remotes/usePostKillingPart.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { postKillingPart } from '@/features/killingParts/hooks/killingPart'; -import { useMutation } from '@/shared/hooks/useMutation'; - -export const usePostKillingPart = () => { - const { isLoading, error, mutateData: createKillingPart } = useMutation(postKillingPart); - - return { isLoading, error, createKillingPart }; -}; diff --git a/frontend/src/features/member/components/MyPartList.tsx b/frontend/src/features/member/components/MyPartList.tsx index fb95df958..f7106d2a4 100644 --- a/frontend/src/features/member/components/MyPartList.tsx +++ b/frontend/src/features/member/components/MyPartList.tsx @@ -19,8 +19,8 @@ export type LikeKillingPart = Pick< const MyPartList = () => { const [tab, setTab] = useState('Like'); - const { data: likes } = useFetch(getLikeParts); - const { data: myParts } = useFetch(getMyParts); + const { data: likes } = useFetch(getLikeParts); + const { data: myParts } = useFetch(getMyParts); if (!likes || !myParts) { return null; diff --git a/frontend/src/features/member/remotes/member.ts b/frontend/src/features/member/remotes/member.ts index b9df67e00..79a0c5abe 100644 --- a/frontend/src/features/member/remotes/member.ts +++ b/frontend/src/features/member/remotes/member.ts @@ -1,5 +1,5 @@ -import fetcher from '@/shared/remotes'; +import { client } from '@/shared/remotes/axios'; -export const deleteMember = (memberId: number | undefined) => () => { - return fetcher(`/members/${memberId}`, 'DELETE'); +export const deleteMember = async (memberId: number) => { + await client.delete(`/members/${memberId}`); }; diff --git a/frontend/src/features/member/remotes/memberParts.ts b/frontend/src/features/member/remotes/memberParts.ts index 28ef36132..ba11152ed 100644 --- a/frontend/src/features/member/remotes/memberParts.ts +++ b/frontend/src/features/member/remotes/memberParts.ts @@ -1,3 +1,5 @@ -import fetcher from '@/shared/remotes'; +import { client } from '@/shared/remotes/axios'; -export const deleteMemberParts = (partId: number) => fetcher(`/member-parts/${partId}`, 'DELETE'); +export const deleteMemberParts = async (partId: number) => { + await client.delete(`/member-parts/${partId}`); +}; diff --git a/frontend/src/features/member/remotes/myPage.ts b/frontend/src/features/member/remotes/myPage.ts index b0ab55df8..3569a3e58 100644 --- a/frontend/src/features/member/remotes/myPage.ts +++ b/frontend/src/features/member/remotes/myPage.ts @@ -1,5 +1,14 @@ -import fetcher from '@/shared/remotes'; +import { client } from '@/shared/remotes/axios'; +import type { LikeKillingPart } from '../components/MyPartList'; -export const getLikeParts = () => fetcher('/my-page/like-parts', 'GET'); +export const getLikeParts = async () => { + const { data } = await client.get('/my-page/like-parts'); -export const getMyParts = () => fetcher('/my-page/my-parts', 'GET'); + return data; +}; + +export const getMyParts = async () => { + const { data } = await client.get('/my-page/my-parts'); + + return data; +}; diff --git a/frontend/src/features/search/remotes/search.ts b/frontend/src/features/search/remotes/search.ts index 86208433c..f4e19c4a9 100644 --- a/frontend/src/features/search/remotes/search.ts +++ b/frontend/src/features/search/remotes/search.ts @@ -1,13 +1,25 @@ -import fetcher from '@/shared/remotes'; +import { client } from '@/shared/remotes/axios'; import type { SingerDetail } from '../../singer/types/singer.type'; import type { SingerSearchPreview } from '../types/search.type'; -export const getSingerSearchPreview = async (query: string): Promise => { - const encodedQuery = encodeURIComponent(query); - return await fetcher(`/search?keyword=${encodedQuery}&type=singer`, 'GET'); +export const getSingerSearchPreview = async (query: string) => { + const { data } = await client.get(`/search`, { + params: { + keyword: query, + type: 'singer', + }, + }); + + return data; }; -export const getSingerSearch = async (query: string): Promise => { - const encodedQuery = encodeURIComponent(query); - return await fetcher(`/search?keyword=${encodedQuery}&type=singer&type=song`, 'GET'); +export const getSingerSearch = async (query: string) => { + const params = new URLSearchParams(); + params.append('keyword', query); + params.append('type', 'singer'); + params.append('type', 'song'); + + const { data } = await client.get(`/search`, { params }); + + return data; }; diff --git a/frontend/src/features/singer/remotes/singer.ts b/frontend/src/features/singer/remotes/singer.ts index bea1d670b..0e538633f 100644 --- a/frontend/src/features/singer/remotes/singer.ts +++ b/frontend/src/features/singer/remotes/singer.ts @@ -1,6 +1,8 @@ -import fetcher from '@/shared/remotes'; +import { client } from '@/shared/remotes/axios'; import type { SingerDetail } from '../types/singer.type'; -export const getSingerDetail = async (singerId: number): Promise => { - return await fetcher(`/singers/${singerId}`, 'GET'); +export const getSingerDetail = async (singerId: number) => { + const { data } = await client.get(`/singers/${singerId}`); + + return data; }; diff --git a/frontend/src/features/songs/components/SongItem.stories.tsx b/frontend/src/features/songs/components/SongItem.stories.tsx index eb59ac905..a86d0cdcb 100644 --- a/frontend/src/features/songs/components/SongItem.stories.tsx +++ b/frontend/src/features/songs/components/SongItem.stories.tsx @@ -1,4 +1,4 @@ -import popularSongs from '@/mocks/fixtures/popularSongs.json'; +import highLikedSongs from '@/mocks/fixtures/highLikedSongs.json'; import SongItem from './SongItem'; import type { Meta, StoryObj } from '@storybook/react'; @@ -11,7 +11,7 @@ export default meta; type Story = StoryObj; -const { title, singer, albumCoverUrl, totalLikeCount } = popularSongs[0]; +const { title, singer, albumCoverUrl, totalLikeCount } = highLikedSongs[0]; export const Default: Story = { args: { diff --git a/frontend/src/features/songs/remotes/likes.ts b/frontend/src/features/songs/remotes/likes.ts index 7a766f0b8..d21a31116 100644 --- a/frontend/src/features/songs/remotes/likes.ts +++ b/frontend/src/features/songs/remotes/likes.ts @@ -1,5 +1,5 @@ -import fetcher from '@/shared/remotes'; +import { client } from '@/shared/remotes/axios'; export const putKillingPartLikes = async (songId: number, partId: number, likeStatus: boolean) => { - return await fetcher(`/songs/${songId}/parts/${partId}/likes`, 'PUT', { likeStatus }); + await client.put(`/songs/${songId}/parts/${partId}/likes`, { likeStatus }); }; diff --git a/frontend/src/features/songs/remotes/song.ts b/frontend/src/features/songs/remotes/song.ts index 9039cc549..c48a110b3 100644 --- a/frontend/src/features/songs/remotes/song.ts +++ b/frontend/src/features/songs/remotes/song.ts @@ -1,16 +1,20 @@ -import fetcher from '@/shared/remotes'; +import { client } from '@/shared/remotes/axios'; import type { Genre, Song } from '../types/Song.type'; import type { RecentSong } from '@/shared/types/song'; // 메인 케러셀 최신순 노래 n개 조회 api - 쿼리파람 없는경우, 응답 기본값은 5개입니다. -export const getRecentSongs = async (songCount?: number): Promise => { - const query = songCount ? `?size=${songCount}` : ''; +export const getRecentSongs = async (songCount?: number) => { + const { data } = await client.get(`/songs/recent`, { + params: { size: songCount }, + }); - return await fetcher(`/songs/recent${query}`, 'GET'); + return data; }; -export const getHighLikedSongs = async (genre: Genre): Promise => { - const query = genre === 'ALL' ? '' : `?genre=${genre}`; +export const getHighLikedSongs = async (genre: Genre) => { + const { data } = await client.get(`/songs/high-liked`, { + params: { genre: genre === 'ALL' ? null : genre }, + }); - return await fetcher(`/songs/high-liked${query}`, 'GET'); + return data; }; diff --git a/frontend/src/features/songs/remotes/songs.ts b/frontend/src/features/songs/remotes/songs.ts index 004ff60eb..21ec45c33 100644 --- a/frontend/src/features/songs/remotes/songs.ts +++ b/frontend/src/features/songs/remotes/songs.ts @@ -1,27 +1,27 @@ -import fetcher from '@/shared/remotes'; +import { client } from '@/shared/remotes/axios'; import type { Genre } from '../types/Song.type'; import type { SongDetail, SongDetailEntries } from '@/shared/types/song'; -export const getSongDetailEntries = async ( - songId: number, - genre: Genre -): Promise => { - const query = genre === 'ALL' ? '' : `?genre=${genre}`; - return await fetcher(`/songs/high-liked/${songId}${query}`, 'GET'); +export const getSongDetailEntries = async (songId: number, genre: Genre) => { + const { data } = await client.get(`/songs/high-liked/${songId}`, { + params: { genre: genre === 'ALL' ? null : genre }, + }); + + return data; }; -export const getExtraPrevSongDetails = async ( - songId: number, - genre: Genre -): Promise => { - const query = genre === 'ALL' ? '' : `?genre=${genre}`; - return await fetcher(`/songs/high-liked/${songId}/prev${query}`, 'GET'); +export const getExtraPrevSongDetails = async (songId: number, genre: Genre) => { + const { data } = await client.get(`/songs/high-liked/${songId}/prev`, { + params: { genre: genre === 'ALL' ? null : genre }, + }); + + return data; }; -export const getExtraNextSongDetails = async ( - songId: number, - genre: Genre -): Promise => { - const query = genre === 'ALL' ? '' : `?genre=${genre}`; - return await fetcher(`/songs/high-liked/${songId}/next${query}`, 'GET'); +export const getExtraNextSongDetails = async (songId: number, genre: Genre) => { + const { data } = await client.get(`/songs/high-liked/${songId}/next`, { + params: { genre: genre === 'ALL' ? null : genre }, + }); + + return data; }; diff --git a/frontend/src/mocks/fixtures/popularSongs.json b/frontend/src/mocks/fixtures/highLikedSongs.json similarity index 83% rename from frontend/src/mocks/fixtures/popularSongs.json rename to frontend/src/mocks/fixtures/highLikedSongs.json index d0667b641..477593cec 100644 --- a/frontend/src/mocks/fixtures/popularSongs.json +++ b/frontend/src/mocks/fixtures/highLikedSongs.json @@ -4,279 +4,319 @@ "title": "Super Shy", "singer": "New Jeans", "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/260/quality/80/optimize", - "totalLikeCount": 258200 + "totalLikeCount": 258200, + "genre": "DANCE" }, { "id": 2, "title": "Super Shy", "singer": "New Jeans", "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/260/quality/80/optimize", - "totalLikeCount": 121312 + "totalLikeCount": 1, + "genre": "DANCE" }, { "id": 3, "title": "노래 길이가 좀 많이 길어요. 노래 길이가 좀 많이 길어요. 노래 길이가 좀 많이 길어요. 노래 길이가 좀 많이 길어요.", "singer": "가수 이름이 좀 길어요. 가수 이름이 좀 길어요. 가수 이름이 좀 길어요. 가수 이름이 좀 길어요. 가수 이름이 좀 길어요.", "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/260/quality/80/optimize", - "totalLikeCount": 9000 + "totalLikeCount": 9000, + "genre": "DANCE" }, { "id": 4, "title": "Super Shy", "singer": "New Jeans", "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/260/quality/80/optimize", - "totalLikeCount": 8000 + "totalLikeCount": 8000, + "genre": "HIPHOP" }, { "id": 5, "title": "Super Shy", "singer": "New Jeans", "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/260/quality/80/optimize", - "totalLikeCount": 1 + "totalLikeCount": 1, + "genre": "HIPHOP" }, { "id": 6, "title": "Seven (feat. Latto) - Clean Ver.", "singer": "정국", "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/86/070/11286070_20230713181059_500.jpg/melon/resize/260/quality/80/optimize", - "totalLikeCount": 1 + "totalLikeCount": 1, + "genre": "HIPHOP" }, { "id": 7, "title": "노래 길이가 좀 많이 길어요. 노래 길이가 좀 많이 길어요. 노래 길이가 좀 많이 길어요. 노래 길이가 좀 많이 길어요.", "singer": "가수 이름이 좀 길어요. 가수 이름이 좀 길어요. 가수 이름이 좀 길어요. 가수 이름이 좀 길어요. 가수 이름이 좀 길어요.", "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/86/070/11286070_20230713181059_500.jpg/melon/resize/260/quality/80/optimize", - "totalLikeCount": 1 + "totalLikeCount": 1, + "genre": "HIPHOP" }, { "id": 8, "title": "Seven (feat. Latto) - Clean Ver.", "singer": "정국", "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/86/070/11286070_20230713181059_500.jpg/melon/resize/260/quality/80/optimize", - "totalLikeCount": 1 + "totalLikeCount": 1, + "genre": "BALLAD" }, { "id": 9, "title": "Seven (feat. Latto) - Clean Ver.", "singer": "정국", "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/86/070/11286070_20230713181059_500.jpg/melon/resize/260/quality/80/optimize", - "totalLikeCount": 1 + "totalLikeCount": 1, + "genre": "BALLAD" }, { "id": 10, "title": "Seven (feat. Latto) - Clean Ver.", "singer": "정국", "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/86/070/11286070_20230713181059_500.jpg/melon/resize/260/quality/80/optimize", - "totalLikeCount": 1 + "totalLikeCount": 1, + "genre": "BALLAD" }, { "id": 11, "title": "I AM", "singer": "IVE (아이브)", "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/11/297/11211297_20230410151046_500.jpg/melon/resize/260/quality/80/optimize", - "totalLikeCount": 1 + "totalLikeCount": 1, + "genre": "POP" }, { "id": 12, "title": "I AM", "singer": "IVE (아이브)", "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/11/297/11211297_20230410151046_500.jpg/melon/resize/260/quality/80/optimize", - "totalLikeCount": 1 + "totalLikeCount": 1, + "genre": "POP" }, { "id": 13, "title": "I AM", "singer": "IVE (아이브)", "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/11/297/11211297_20230410151046_500.jpg/melon/resize/260/quality/80/optimize", - "totalLikeCount": 1 + "totalLikeCount": 1, + "genre": "POP" }, { "id": 14, "title": "I AM", "singer": "IVE (아이브)", "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/11/297/11211297_20230410151046_500.jpg/melon/resize/260/quality/80/optimize", - "totalLikeCount": 1 + "totalLikeCount": 1, + "genre": "INDIE" }, { "id": 15, "title": "I AM", "singer": "IVE (아이브)", "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/11/297/11211297_20230410151046_500.jpg/melon/resize/260/quality/80/optimize", - "totalLikeCount": 1 + "totalLikeCount": 1, + "genre": "INDIE" }, { "id": 16, "title": "Super Shy", "singer": "New Jeans", "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/260/quality/80/optimize", - "totalLikeCount": 1 + "totalLikeCount": 1, + "genre": "INDIE" }, { "id": 17, "title": "Super Shy", "singer": "New Jeans", "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/260/quality/80/optimize", - "totalLikeCount": 1 + "totalLikeCount": 1, + "genre": "TROT" }, { "id": 18, "title": "Super Shy", "singer": "New Jeans", "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/260/quality/80/optimize", - "totalLikeCount": 1 + "totalLikeCount": 1, + "genre": "TROT" }, { "id": 19, "title": "Super Shy", "singer": "New Jeans", "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/260/quality/80/optimize", - "totalLikeCount": 1 + "totalLikeCount": 1, + "genre": "TROT" }, { "id": 20, "title": "Super Shy", "singer": "New Jeans", "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/260/quality/80/optimize", - "totalLikeCount": 1 + "totalLikeCount": 1, + "genre": "JAZZ" }, { "id": 21, "title": "Super Shy", "singer": "New Jeans", "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/260/quality/80/optimize", - "totalLikeCount": 1 + "totalLikeCount": 1, + "genre": "JAZZ" }, { "id": 22, "title": "Super Shy", "singer": "New Jeans", "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/260/quality/80/optimize", - "totalLikeCount": 1 + "totalLikeCount": 1, + "genre": "JAZZ" }, { "id": 23, "title": "Super Shy", "singer": "New Jeans", "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/260/quality/80/optimize", - "totalLikeCount": 1 + "totalLikeCount": 1, + "genre": "EDM" }, { "id": 24, "title": "Super Shy", "singer": "New Jeans", "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/260/quality/80/optimize", - "totalLikeCount": 1 + "totalLikeCount": 1, + "genre": "EDM" }, { "id": 25, "title": "Super Shy", "singer": "New Jeans", "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/260/quality/80/optimize", - "totalLikeCount": 1 + "totalLikeCount": 1, + "genre": "EDM" }, { "id": 26, "title": "Super Shy", "singer": "New Jeans", "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/260/quality/80/optimize", - "totalLikeCount": 1 + "totalLikeCount": 1, + "genre": "ETC" }, { "id": 27, "title": "Super Shy", "singer": "New Jeans", "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/260/quality/80/optimize", - "totalLikeCount": 1 + "totalLikeCount": 1, + "genre": "ETC" }, { "id": 28, "title": "Super Shy", "singer": "New Jeans", "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/260/quality/80/optimize", - "totalLikeCount": 1 + "totalLikeCount": 1, + "genre": "ETC" }, { "id": 29, "title": "Super Shy", "singer": "New Jeans", "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/260/quality/80/optimize", - "totalLikeCount": 1 + "totalLikeCount": 1, + "genre": "ETC" }, { "id": 30, "title": "Super Shy", "singer": "New Jeans", "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/260/quality/80/optimize", - "totalLikeCount": 1 + "totalLikeCount": 1, + "genre": "ROCK_METAL" }, { "id": 31, "title": "Super Shy", "singer": "New Jeans", "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/260/quality/80/optimize", - "totalLikeCount": 1 + "totalLikeCount": 1, + "genre": "ROCK_METAL" }, { "id": 32, "title": "Super Shy", "singer": "New Jeans", "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/260/quality/80/optimize", - "totalLikeCount": 1 + "totalLikeCount": 1, + "genre": "ROCK_METAL" }, { "id": 33, "title": "Super Shy", "singer": "New Jeans", "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/260/quality/80/optimize", - "totalLikeCount": 1 + "totalLikeCount": 1, + "genre": "CLASSIC" }, { "id": 34, "title": "Super Shy", "singer": "New Jeans", "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/260/quality/80/optimize", - "totalLikeCount": 1 + "totalLikeCount": 1, + "genre": "CLASSIC" }, { "id": 35, "title": "Super Shy", "singer": "New Jeans", "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/260/quality/80/optimize", - "totalLikeCount": 1 + "totalLikeCount": 1, + "genre": "CLASSIC" }, { "id": 36, "title": "Super Shy", "singer": "New Jeans", "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/260/quality/80/optimize", - "totalLikeCount": 1 + "totalLikeCount": 1, + "genre": "RHYTHM_AND_BLUES" }, { "id": 37, "title": "Super Shy", "singer": "New Jeans", "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/260/quality/80/optimize", - "totalLikeCount": 1 + "totalLikeCount": 1, + "genre": "RHYTHM_AND_BLUES" }, { "id": 38, "title": "Super Shy", "singer": "New Jeans", "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/260/quality/80/optimize", - "totalLikeCount": 1 + "totalLikeCount": 1, + "genre": "RHYTHM_AND_BLUES" }, { "id": 39, "title": "Super Shy", "singer": "New Jeans", "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/260/quality/80/optimize", - "totalLikeCount": 1 + "totalLikeCount": 1, + "genre": "FOLK_BLUES" }, { "id": 40, "title": "Super Shy", "singer": "New Jeans", "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/260/quality/80/optimize", - "totalLikeCount": 1 + "totalLikeCount": 1, + "genre": "FOLK_BLUES" } ] diff --git a/frontend/src/mocks/handlers/songsHandlers.ts b/frontend/src/mocks/handlers/songsHandlers.ts index fd56a3b2a..ca3d78e18 100644 --- a/frontend/src/mocks/handlers/songsHandlers.ts +++ b/frontend/src/mocks/handlers/songsHandlers.ts @@ -2,6 +2,7 @@ import { rest } from 'msw'; import comments from '../fixtures/comments.json'; import extraNextSongDetails from '../fixtures/extraNextSongDetails.json'; import extraPrevSongDetails from '../fixtures/extraPrevSongDetails.json'; +import highLikedSongs from '../fixtures/highLikedSongs.json'; import recentSongs from '../fixtures/recentSongs.json'; import songEntries from '../fixtures/songEntries.json'; import type { KillingPartPostRequest } from '@/shared/types/killingPart'; @@ -34,10 +35,23 @@ const songsHandlers = [ return res(ctx.status(201)); }), + rest.get(`${BASE_URL}/songs/high-liked`, (req, res, ctx) => { + const genre = req.url.searchParams.get('genre'); + + if (genre !== null) { + const targetGenreSongs = highLikedSongs.filter((song) => song.genre === genre); + + return res(ctx.status(200), ctx.json(targetGenreSongs)); + } + + return res(ctx.status(200), ctx.json(highLikedSongs)); + }), + rest.get(`${BASE_URL}/songs/high-liked/:songId`, (req, res, ctx) => { // const genre = req.url.searchParams.get('genre') return res(ctx.status(200), ctx.json(songEntries)); }), + rest.get(`${BASE_URL}/songs/high-liked/:songId/prev`, (req, res, ctx) => { // const genre = req.url.searchParams.get('genre'); const { songId } = req.params; diff --git a/frontend/src/pages/AuthPage.tsx b/frontend/src/pages/AuthPage.tsx index 700c451a9..1e6e47c81 100644 --- a/frontend/src/pages/AuthPage.tsx +++ b/frontend/src/pages/AuthPage.tsx @@ -1,21 +1,19 @@ import { useEffect } from 'react'; -import { Navigate, useNavigate, useParams, useSearchParams } from 'react-router-dom'; +import { Navigate, useNavigate, useSearchParams } from 'react-router-dom'; import { useAuthContext } from '@/features/auth/components/AuthProvider'; +import { getAccessToken } from '@/features/auth/remotes/auth'; import path from '@/shared/constants/path'; +import useValidParams from '@/shared/hooks/useValidParams'; import accessTokenStorage from '@/shared/utils/accessTokenStorage'; -interface AccessTokenResponse { - accessToken: string; -} - const AuthPage = () => { const [searchParams] = useSearchParams(); - const { platform } = useParams(); + const { platform } = useValidParams(); const { login } = useAuthContext(); const navigate = useNavigate(); - const getAccessToken = async () => { + const authLogin = async () => { const code = searchParams.get('code'); if (!code) { @@ -25,13 +23,7 @@ const AuthPage = () => { return; } - const response = await fetch(`${process.env.BASE_URL}/login/${platform}?code=${code}`, { - method: 'get', - credentials: 'include', - }); - - const data = (await response.json()) as AccessTokenResponse; - const { accessToken } = data; + const { accessToken } = await getAccessToken(platform, code); if (accessToken) { login(accessToken); @@ -39,7 +31,7 @@ const AuthPage = () => { }; useEffect(() => { - getAccessToken(); + authLogin(); }, []); return ; diff --git a/frontend/src/pages/EditProfilePage.tsx b/frontend/src/pages/EditProfilePage.tsx index 4191e8e0c..3f5500d82 100644 --- a/frontend/src/pages/EditProfilePage.tsx +++ b/frontend/src/pages/EditProfilePage.tsx @@ -12,16 +12,16 @@ import { useMutation } from '@/shared/hooks/useMutation'; const EditProfilePage = () => { const { user, logout } = useAuthContext(); const { isOpen, openModal, closeModal } = useModal(); - const { mutateData } = useMutation(deleteMember(user?.memberId)); + const { mutateData: withdrawal } = useMutation(deleteMember); const navigate = useNavigate(); if (!user) { navigate(ROUTE_PATH.LOGIN); - return; + return null; } const handleWithdrawal = async () => { - await mutateData(); + await withdrawal(user.memberId); logout(); navigate(ROUTE_PATH.ROOT); }; diff --git a/frontend/src/pages/PartCollectingPage.tsx b/frontend/src/pages/PartCollectingPage.tsx index 822f190b0..cf858359c 100644 --- a/frontend/src/pages/PartCollectingPage.tsx +++ b/frontend/src/pages/PartCollectingPage.tsx @@ -1,8 +1,8 @@ -import { useParams } from 'react-router-dom'; import { Flex } from 'shook-layout'; import { styled } from 'styled-components'; import RegisterPart from '@/features/killingParts/components/RegisterPart'; import VideoController from '@/features/killingParts/components/VideoController'; +import { getSong } from '@/features/killingParts/remotes/killingPart'; import { CollectingPartProvider } from '@/features/songs/components/CollectingPartProvider'; import SongInformation from '@/features/songs/components/SongInformation'; import { VideoPlayerProvider } from '@/features/youtube/components/VideoPlayerProvider'; @@ -10,13 +10,12 @@ import Youtube from '@/features/youtube/components/Youtube'; import Spacing from '@/shared/components/Spacing'; import SRHeading from '@/shared/components/SRHeading'; import useFetch from '@/shared/hooks/useFetch'; -import fetcher from '@/shared/remotes'; -import type { SongInfo } from '@/shared/types/song'; +import useValidParams from '@/shared/hooks/useValidParams'; const PartCollectingPage = () => { - const { id: songId } = useParams(); + const { id: songId } = useValidParams(); // TODO: 조회 API 만들어야함. - const { data: songInfo } = useFetch(() => fetcher(`/songs/${songId}`, 'GET')); + const { data: songInfo } = useFetch(() => getSong(Number(songId))); if (!songInfo) return; const { id, title, singer, videoLength, songVideoId, albumCoverUrl } = songInfo; diff --git a/frontend/src/shared/hooks/useExtraFetch.ts b/frontend/src/shared/hooks/useExtraFetch.ts index 758cc5783..d44bc3665 100644 --- a/frontend/src/shared/hooks/useExtraFetch.ts +++ b/frontend/src/shared/hooks/useExtraFetch.ts @@ -1,5 +1,5 @@ import { useCallback, useState } from 'react'; -import type { ErrorResponse } from '@/shared/remotes'; +import type { ErrorResponse } from '../types/errorResponse'; type FetchDirection = 'prev' | 'next'; diff --git a/frontend/src/shared/hooks/useFetch.ts b/frontend/src/shared/hooks/useFetch.ts index 410d22835..b4a96dfc7 100644 --- a/frontend/src/shared/hooks/useFetch.ts +++ b/frontend/src/shared/hooks/useFetch.ts @@ -2,11 +2,12 @@ import { useCallback, useEffect, useState } from 'react'; import { useAuthContext } from '@/features/auth/components/AuthProvider'; import { useLoginPopup } from '@/features/auth/hooks/LoginPopUpContext'; import AuthError from '@/shared/remotes/AuthError'; +import type { ErrorResponse } from '../types/errorResponse'; const useFetch = (fetcher: () => Promise, defaultFetch: boolean = true) => { const [data, setData] = useState(null); const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(null); + const [error, setError] = useState(null); const { popupLoginModal } = useLoginPopup(); const { logout } = useAuthContext(); @@ -28,7 +29,7 @@ const useFetch = (fetcher: () => Promise, defaultFetch: boolean = true) => popupLoginModal(error.code); return; } - setError(error as Error); + setError(error as ErrorResponse); } finally { setIsLoading(false); } diff --git a/frontend/src/shared/hooks/useMutation.ts b/frontend/src/shared/hooks/useMutation.ts index a7289a371..7d60fec65 100644 --- a/frontend/src/shared/hooks/useMutation.ts +++ b/frontend/src/shared/hooks/useMutation.ts @@ -2,12 +2,13 @@ import { useCallback, useState } from 'react'; import { useAuthContext } from '@/features/auth/components/AuthProvider'; import { useLoginPopup } from '@/features/auth/hooks/LoginPopUpContext'; import AuthError from '@/shared/remotes/AuthError'; +import type { ErrorResponse } from '../types/errorResponse'; // eslint-disable-next-line export const useMutation = (mutateFn: (...params: P) => Promise) => { const [data, setData] = useState(null); const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(null); + const [error, setError] = useState(null); const { popupLoginModal } = useLoginPopup(); const { logout } = useAuthContext(); @@ -30,7 +31,7 @@ export const useMutation = (mutateFn: (...params: P) => Prom popupLoginModal(error.code); return; } - setError(error as Error); + setError(error as ErrorResponse); } finally { setIsLoading(false); } diff --git a/frontend/src/shared/remotes/axios.ts b/frontend/src/shared/remotes/axios.ts new file mode 100644 index 000000000..dd54b8939 --- /dev/null +++ b/frontend/src/shared/remotes/axios.ts @@ -0,0 +1,75 @@ +import axios from 'axios'; +import { postRefreshAccessToken } from '@/features/auth/remotes/auth'; +import type { AccessTokenRes } from '@/features/auth/types/auth.type'; +import type { AxiosError, CreateAxiosDefaults, InternalAxiosRequestConfig } from 'axios'; + +const { BASE_URL } = process.env; + +const defaultConfig: CreateAxiosDefaults = { + baseURL: BASE_URL, + headers: { + 'Content-Type': 'application/json', + }, + withCredentials: true, +}; + +const clientBasic = axios.create(defaultConfig); +const client = axios.create(defaultConfig); + +// 요청 인터셉터 +const setToken = (config: InternalAxiosRequestConfig) => { + const accessToken = localStorage.getItem('userToken'); + + if (accessToken) { + config.headers.Authorization = `Bearer ${accessToken}`; + } + + return config; +}; + +// 응답 에러 인터셉터 +let reissuePromise: Promise | null = null; + +const reissueOnExpiredTokenError = async (error: AxiosError) => { + const originalRequest = error.config; + const isAuthError = error.response?.status === 401; + const hasAuthorization = !!originalRequest?.headers.Authorization; + + if (isAuthError && hasAuthorization) { + try { + const { accessToken } = await (reissuePromise ??= reissue()); + + originalRequest.headers.Authorization = `Bearer ${accessToken}`; + + return client(originalRequest); + } catch (error) { + return Promise.reject(error); + } finally { + reissuePromise = null; + } + } + + return Promise.reject(error); +}; + +const reissue = async () => { + try { + const response = await postRefreshAccessToken(); + const { accessToken } = response; + + localStorage.setItem('userToken', accessToken); + return response; + } catch (error) { + window.alert('세션이 만료되었습니다. 다시 로그인 해주세요'); + localStorage.removeItem('userToken'); + window.location.href = '/login'; + + throw error; + } +}; + +clientBasic.interceptors.request.use(setToken); +client.interceptors.request.use(setToken); +client.interceptors.response.use((response) => response, reissueOnExpiredTokenError); + +export { clientBasic, client }; diff --git a/frontend/src/shared/remotes/index.ts b/frontend/src/shared/remotes/index.ts deleted file mode 100644 index 45f26d4d9..000000000 --- a/frontend/src/shared/remotes/index.ts +++ /dev/null @@ -1,53 +0,0 @@ -import AuthError from '@/shared/remotes/AuthError'; -import preCheckAccessToken from '@/shared/remotes/preCheckAccessToken'; - -export interface ErrorResponse { - code: number; - message: string; -} - -const { BASE_URL } = process.env; - -const fetcher = async (url: string, method: string, body?: unknown) => { - const headers: Record = { - 'Content-type': 'application/json', - }; - - const accessToken = await preCheckAccessToken(); - - if (accessToken) { - headers['Authorization'] = `Bearer ${accessToken}`; - } - - const options: RequestInit = { - method, - headers, - }; - - if (body) { - options.body = JSON.stringify(body); - } - - const response = await fetch(`${BASE_URL}${url}`, options); - - if (!response.ok) { - const errorResponse: ErrorResponse = await response.json(); - - if (response.status >= 500) { - throw new Error(errorResponse.message); - } - - if (response.status === 401) { - throw new AuthError(errorResponse); - } - - throw new Error(errorResponse.message); - } - - const contentType = response.headers.get('content-type'); - if (!contentType || !contentType.includes('application/json')) return response; - - return response.json(); -}; - -export default fetcher; diff --git a/frontend/src/shared/remotes/preCheckAccessToken.ts b/frontend/src/shared/remotes/preCheckAccessToken.ts deleted file mode 100644 index 718e10bed..000000000 --- a/frontend/src/shared/remotes/preCheckAccessToken.ts +++ /dev/null @@ -1,38 +0,0 @@ -import accessTokenStorage from '@/shared/utils/accessTokenStorage'; - -const isTokenExpiredAfter60seconds = (tokenExp: number) => { - return tokenExp * 1000 - 30 * 1000 < Date.now(); -}; - -const preCheckAccessToken = async () => { - const accessTokenWithPayload = accessTokenStorage.getTokenWithPayload(); - - if (accessTokenWithPayload) { - const { - accessToken, - payload: { exp }, - } = accessTokenWithPayload; - - if (!isTokenExpiredAfter60seconds(exp)) { - return accessToken; - } - - const response = await fetch(`${process.env.BASE_URL}/reissue`, { - method: 'POST', - headers: { - Authorization: `Bearer ${accessToken}`, - }, - }); - - if (response.ok) { - const { accessToken } = await response.json(); - accessTokenStorage.setToken(accessToken); - return accessToken; - } - accessTokenStorage.removeToken(); - } - - return null; -}; - -export default preCheckAccessToken; diff --git a/frontend/src/shared/types/errorResponse.ts b/frontend/src/shared/types/errorResponse.ts new file mode 100644 index 000000000..c85a9e871 --- /dev/null +++ b/frontend/src/shared/types/errorResponse.ts @@ -0,0 +1,4 @@ +export interface ErrorResponse { + code: number; + message: string; +}