From 7346001c3b37738447bb96851c7c1d818dda4b1c Mon Sep 17 00:00:00 2001 From: Ji Hyeong Lee <115636461+Jihyeong00@users.noreply.github.com> Date: Fri, 29 Sep 2023 04:52:04 +0900 Subject: [PATCH] =?UTF-8?q?=F0=9F=8C=88feat=20:=20msw=20=EC=97=B0=EA=B2=B0?= =?UTF-8?q?=EC=84=B1=EA=B3=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/mockServiceWorker.js | 303 ++++++++++++++++++ client/package.json | 5 +- client/src/App.tsx | 6 + client/src/apis/interceptors.ts | 2 +- client/src/apis/post/getPostList.ts | 20 ++ client/src/apis/user/getRefreshToken.ts | 12 +- client/src/constants/api.ts | 7 +- client/src/constants/queryKey.ts | 0 client/src/hooks/api/post/usePostListQuery.ts | 12 + client/src/mocks/api/.gitkeep | 0 client/src/mocks/browser.ts | 4 + client/src/mocks/handlers/index.ts | 6 + client/src/mocks/handlers/post.ts | 15 + client/src/pages/Main/MainBody/MainBody.tsx | 14 +- client/src/pages/Main/index.tsx | 22 +- client/src/pages/MyPage/index.tsx | 13 +- client/src/types/index.d.ts | 3 + client/src/types/main.type.ts | 5 + client/src/utils/KkiriProvider.tsx | 2 +- 19 files changed, 423 insertions(+), 28 deletions(-) create mode 100644 client/mockServiceWorker.js delete mode 100644 client/src/constants/queryKey.ts delete mode 100644 client/src/mocks/api/.gitkeep create mode 100644 client/src/mocks/browser.ts create mode 100644 client/src/mocks/handlers/index.ts create mode 100644 client/src/mocks/handlers/post.ts create mode 100644 client/src/types/index.d.ts diff --git a/client/mockServiceWorker.js b/client/mockServiceWorker.js new file mode 100644 index 00000000..0f24d515 --- /dev/null +++ b/client/mockServiceWorker.js @@ -0,0 +1,303 @@ +/* eslint-disable */ +/* tslint:disable */ + +/** + * Mock Service Worker (1.3.1). + * @see https://github.com/mswjs/msw + * - Please do NOT modify this file. + * - Please do NOT serve this file on production. + */ + +const INTEGRITY_CHECKSUM = '3d6b9f06410d179a7f7404d4bf4c3c70' +const activeClientIds = new Set() + +self.addEventListener('install', function () { + self.skipWaiting() +}) + +self.addEventListener('activate', function (event) { + event.waitUntil(self.clients.claim()) +}) + +self.addEventListener('message', async function (event) { + const clientId = event.source.id + + if (!clientId || !self.clients) { + return + } + + const client = await self.clients.get(clientId) + + if (!client) { + return + } + + const allClients = await self.clients.matchAll({ + type: 'window', + }) + + switch (event.data) { + case 'KEEPALIVE_REQUEST': { + sendToClient(client, { + type: 'KEEPALIVE_RESPONSE', + }) + break + } + + case 'INTEGRITY_CHECK_REQUEST': { + sendToClient(client, { + type: 'INTEGRITY_CHECK_RESPONSE', + payload: INTEGRITY_CHECKSUM, + }) + break + } + + case 'MOCK_ACTIVATE': { + activeClientIds.add(clientId) + + sendToClient(client, { + type: 'MOCKING_ENABLED', + payload: true, + }) + break + } + + case 'MOCK_DEACTIVATE': { + activeClientIds.delete(clientId) + break + } + + case 'CLIENT_CLOSED': { + activeClientIds.delete(clientId) + + const remainingClients = allClients.filter((client) => { + return client.id !== clientId + }) + + // Unregister itself when there are no more clients + if (remainingClients.length === 0) { + self.registration.unregister() + } + + break + } + } +}) + +self.addEventListener('fetch', function (event) { + const { request } = event + const accept = request.headers.get('accept') || '' + + // Bypass server-sent events. + if (accept.includes('text/event-stream')) { + return + } + + // Bypass navigation requests. + if (request.mode === 'navigate') { + return + } + + // Opening the DevTools triggers the "only-if-cached" request + // that cannot be handled by the worker. Bypass such requests. + if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') { + return + } + + // Bypass all requests when there are no active clients. + // Prevents the self-unregistered worked from handling requests + // after it's been deleted (still remains active until the next reload). + if (activeClientIds.size === 0) { + return + } + + // Generate unique request ID. + const requestId = Math.random().toString(16).slice(2) + + event.respondWith( + handleRequest(event, requestId).catch((error) => { + if (error.name === 'NetworkError') { + console.warn( + '[MSW] Successfully emulated a network error for the "%s %s" request.', + request.method, + request.url, + ) + return + } + + // At this point, any exception indicates an issue with the original request/response. + console.error( + `\ +[MSW] Caught an exception from the "%s %s" request (%s). This is probably not a problem with Mock Service Worker. There is likely an additional logging output above.`, + request.method, + request.url, + `${error.name}: ${error.message}`, + ) + }), + ) +}) + +async function handleRequest(event, requestId) { + const client = await resolveMainClient(event) + const response = await getResponse(event, client, requestId) + + // Send back the response clone for the "response:*" life-cycle events. + // Ensure MSW is active and ready to handle the message, otherwise + // this message will pend indefinitely. + if (client && activeClientIds.has(client.id)) { + ;(async function () { + const clonedResponse = response.clone() + sendToClient(client, { + type: 'RESPONSE', + payload: { + requestId, + type: clonedResponse.type, + ok: clonedResponse.ok, + status: clonedResponse.status, + statusText: clonedResponse.statusText, + body: + clonedResponse.body === null ? null : await clonedResponse.text(), + headers: Object.fromEntries(clonedResponse.headers.entries()), + redirected: clonedResponse.redirected, + }, + }) + })() + } + + return response +} + +// Resolve the main client for the given event. +// Client that issues a request doesn't necessarily equal the client +// that registered the worker. It's with the latter the worker should +// communicate with during the response resolving phase. +async function resolveMainClient(event) { + const client = await self.clients.get(event.clientId) + + if (client?.frameType === 'top-level') { + return client + } + + const allClients = await self.clients.matchAll({ + type: 'window', + }) + + return allClients + .filter((client) => { + // Get only those clients that are currently visible. + return client.visibilityState === 'visible' + }) + .find((client) => { + // Find the client ID that's recorded in the + // set of clients that have registered the worker. + return activeClientIds.has(client.id) + }) +} + +async function getResponse(event, client, requestId) { + const { request } = event + const clonedRequest = request.clone() + + function passthrough() { + // Clone the request because it might've been already used + // (i.e. its body has been read and sent to the client). + const headers = Object.fromEntries(clonedRequest.headers.entries()) + + // Remove MSW-specific request headers so the bypassed requests + // comply with the server's CORS preflight check. + // Operate with the headers as an object because request "Headers" + // are immutable. + delete headers['x-msw-bypass'] + + return fetch(clonedRequest, { headers }) + } + + // Bypass mocking when the client is not active. + if (!client) { + return passthrough() + } + + // Bypass initial page load requests (i.e. static assets). + // The absence of the immediate/parent client in the map of the active clients + // means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet + // and is not ready to handle requests. + if (!activeClientIds.has(client.id)) { + return passthrough() + } + + // Bypass requests with the explicit bypass header. + // Such requests can be issued by "ctx.fetch()". + if (request.headers.get('x-msw-bypass') === 'true') { + return passthrough() + } + + // Notify the client that a request has been intercepted. + const clientMessage = await sendToClient(client, { + type: 'REQUEST', + payload: { + id: requestId, + url: request.url, + method: request.method, + headers: Object.fromEntries(request.headers.entries()), + cache: request.cache, + mode: request.mode, + credentials: request.credentials, + destination: request.destination, + integrity: request.integrity, + redirect: request.redirect, + referrer: request.referrer, + referrerPolicy: request.referrerPolicy, + body: await request.text(), + bodyUsed: request.bodyUsed, + keepalive: request.keepalive, + }, + }) + + switch (clientMessage.type) { + case 'MOCK_RESPONSE': { + return respondWithMock(clientMessage.data) + } + + case 'MOCK_NOT_FOUND': { + return passthrough() + } + + case 'NETWORK_ERROR': { + const { name, message } = clientMessage.data + const networkError = new Error(message) + networkError.name = name + + // Rejecting a "respondWith" promise emulates a network error. + throw networkError + } + } + + return passthrough() +} + +function sendToClient(client, message) { + return new Promise((resolve, reject) => { + const channel = new MessageChannel() + + channel.port1.onmessage = (event) => { + if (event.data && event.data.error) { + return reject(event.data.error) + } + + resolve(event.data) + } + + client.postMessage(message, [channel.port2]) + }) +} + +function sleep(timeMs) { + return new Promise((resolve) => { + setTimeout(resolve, timeMs) + }) +} + +async function respondWithMock(response) { + await sleep(response.delay) + return new Response(response.body, response) +} diff --git a/client/package.json b/client/package.json index 0254d02d..d890c44a 100644 --- a/client/package.json +++ b/client/package.json @@ -95,5 +95,8 @@ "vite-plugin-eslint": "^1.8.1", "vite-plugin-svgr": "^3.2.0", "vite-tsconfig-paths": "^4.2.0" + }, + "msw": { + "workerDirectory": "public" } -} +} \ No newline at end of file diff --git a/client/src/App.tsx b/client/src/App.tsx index 0d5b4a97..732b636a 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -2,7 +2,13 @@ import { RouterProvider } from 'react-router-dom'; import router from '@router/Router'; import KkiriProvider from './utils/KkiriProvider'; import '@styles/fonts/font.css'; +import { Dev } from '@constants/api'; +import { worker } from '@mocks/browser'; + function App() { + if (Dev) { + worker.start(); + } return ( diff --git a/client/src/apis/interceptors.ts b/client/src/apis/interceptors.ts index 39395e2d..941fabbc 100644 --- a/client/src/apis/interceptors.ts +++ b/client/src/apis/interceptors.ts @@ -12,7 +12,7 @@ export interface ErrorResponseData { } export const checkAndSetToken = (config: InternalAxiosRequestConfig) => { - if (!config.useAuth || !config.headers || config.headers.Authorization) + if (config.useAuth || !config.headers || config.headers.Authorization) return config; const accessToken = localStorage.getItem(ACCESS_TOKEN_KEY); diff --git a/client/src/apis/post/getPostList.ts b/client/src/apis/post/getPostList.ts index e69de29b..92f1037e 100644 --- a/client/src/apis/post/getPostList.ts +++ b/client/src/apis/post/getPostList.ts @@ -0,0 +1,20 @@ +import { END_POINTS } from '@constants/api'; +import { axiosInstance } from '@apis/AxiosInstance'; +import type { PostType } from '@type/post.type'; + +export type FilterType = { + userId?: string; + tagId?: string; + keyword?: string; + data?: string; + page: string; +}; + +export const getPostList = async (spaceId: string, filter: FilterType) => { + const { data } = await axiosInstance.get( + END_POINTS.POST_LIST(spaceId), + { params: { ...filter } } + ); + + return data; +}; diff --git a/client/src/apis/user/getRefreshToken.ts b/client/src/apis/user/getRefreshToken.ts index 8b519cb9..c4f1e9c8 100644 --- a/client/src/apis/user/getRefreshToken.ts +++ b/client/src/apis/user/getRefreshToken.ts @@ -1,12 +1,12 @@ -import {END_POINTS} from '@constants/api'; -import {axiosInstance} from '@apis/AxiosInstance'; +import { END_POINTS } from '@constants/api'; +import { axiosInstance } from '@apis/AxiosInstance'; type TokenData = { - accessToken: string; -} + accessToken: string; +}; export const getRefreshToken = async () => { - const {data} = await axiosInstance.post(END_POINTS.TOKEN); + const { data } = await axiosInstance.post(END_POINTS.TOKEN); - return data; + return data; }; diff --git a/client/src/constants/api.ts b/client/src/constants/api.ts index 2d822edc..58ee0450 100644 --- a/client/src/constants/api.ts +++ b/client/src/constants/api.ts @@ -1,11 +1,11 @@ -export const isProduct = process.env.NODE_ENV === 'production'; - +export const Product = process.env.NODE_ENV === 'production'; +export const Dev = process.env.NODE_ENV === 'development'; export const NETWORK = { RETRY_COUNT: 2, TIMEOUT: 10000, }; -export const AXIOS_BASE_URL = isProduct +export const AXIOS_BASE_URL = Product ? `${window.location.protocol}//${import.meta.env.BASE_URL}` : '/'; @@ -20,6 +20,7 @@ export const END_POINTS = { USERINFO: '/mypage', BOOKMARK: '/bookmark', INVITATION: '/invitation', + POST_LIST: (spaceId: string) => `/space/${spaceId}`, } as const; export const HTTP_STATUS_CODE = { diff --git a/client/src/constants/queryKey.ts b/client/src/constants/queryKey.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/client/src/hooks/api/post/usePostListQuery.ts b/client/src/hooks/api/post/usePostListQuery.ts index e69de29b..6e9bfde0 100644 --- a/client/src/hooks/api/post/usePostListQuery.ts +++ b/client/src/hooks/api/post/usePostListQuery.ts @@ -0,0 +1,12 @@ +import { useQuery } from '@tanstack/react-query'; +import { FilterType, getPostList } from '@apis/post/getPostList'; +import { PostType } from '@type/post.type'; +import { AxiosError } from 'axios'; + +export const usePostListQuery = (spaceId: string, filter: FilterType) => { + const { data } = useQuery(['postList', spaceId], () => + getPostList(spaceId, filter) + ); + + return { postList: data! }; +}; diff --git a/client/src/mocks/api/.gitkeep b/client/src/mocks/api/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/client/src/mocks/browser.ts b/client/src/mocks/browser.ts new file mode 100644 index 00000000..d9b43e74 --- /dev/null +++ b/client/src/mocks/browser.ts @@ -0,0 +1,4 @@ +import { SetupWorker, setupWorker } from 'msw'; +import { handlers } from '@mocks/handlers'; + +export const worker: SetupWorker = setupWorker(...handlers); diff --git a/client/src/mocks/handlers/index.ts b/client/src/mocks/handlers/index.ts new file mode 100644 index 00000000..8cb78e55 --- /dev/null +++ b/client/src/mocks/handlers/index.ts @@ -0,0 +1,6 @@ +import { postHandlers } from '@mocks/handlers/post'; +import { DefaultBodyType, MockedRequest, RestHandler } from 'msw'; + +export const handlers: RestHandler>[] = [ + ...postHandlers, +]; diff --git a/client/src/mocks/handlers/post.ts b/client/src/mocks/handlers/post.ts new file mode 100644 index 00000000..1054ac18 --- /dev/null +++ b/client/src/mocks/handlers/post.ts @@ -0,0 +1,15 @@ +import { DefaultBodyType, MockedRequest, rest, RestHandler } from 'msw'; + +import { END_POINTS, HTTP_STATUS_CODE } from '@constants/api'; +import detailPageMock from '@mocks/data/DetailPage/detailPage.mock'; +import postListMock from '@mocks/data/PostList/postList.mock'; + +export const postHandlers: RestHandler>[] = [ + rest.get(END_POINTS.SPACE + '/:spaceId', (_, res, ctx) => { + return res(ctx.status(HTTP_STATUS_CODE.SUCCESS), ctx.json(postListMock)); + }), + + rest.get(END_POINTS.SPACE + '/:postId', (_, res, ctx) => { + return res(ctx.status(HTTP_STATUS_CODE.SUCCESS), ctx.json(detailPageMock)); + }), +]; diff --git a/client/src/pages/Main/MainBody/MainBody.tsx b/client/src/pages/Main/MainBody/MainBody.tsx index 954e143a..5ce39eb5 100644 --- a/client/src/pages/Main/MainBody/MainBody.tsx +++ b/client/src/pages/Main/MainBody/MainBody.tsx @@ -12,12 +12,12 @@ import Calender from '@/components/common/Calender/Calender'; const MainBody = (props: MainBodyPropType) => { const [state, setState] = useState<{ - user: selectType[]; - tag: selectType[]; + userId: selectType[]; + tagId: selectType[]; search: string; }>({ - user: [], - tag: [], + userId: [], + tagId: [], search: '', }); @@ -65,7 +65,11 @@ const MainBody = (props: MainBodyPropType) => { ListItem={tagList} /> - + {!selectState ? ( postList.length === 0 ? ( diff --git a/client/src/pages/Main/index.tsx b/client/src/pages/Main/index.tsx index 47c0506c..51e3c793 100644 --- a/client/src/pages/Main/index.tsx +++ b/client/src/pages/Main/index.tsx @@ -3,13 +3,29 @@ import MainHeader from '@pages/Main/MainHeader/MainHeader'; import MainBody from '@pages/Main/MainBody/MainBody'; import { useState } from 'react'; import MainPageMock from '@mocks/data/MainPage/mainPage.mock'; -import * as process from 'process'; +import { usePostListQuery } from '@hooks/api/post/usePostListQuery'; +import { useParams, useSearchParams } from 'react-router-dom'; const MainPage = () => { const { spaceInfo, isAdmin, tagList, postList, total, page } = MainPageMock; - const [selectState, setSelectState] = useState(false); - console.log(process.env.NODE_ENV); + const { spaceId } = useParams(); + const [searchParam] = useSearchParams(); + + const searchPage: string = searchParam.get('page') ?? '1'; + const searchKeyWord: string = searchParam.get('keyword') ?? ''; + const userId: string = searchParam.get('tagId') ?? ''; + const tagId: string = searchParam.get('userId') ?? ''; + + const { postList: test } = usePostListQuery(spaceId!, { + page: searchPage, + userId: userId, + tagId: tagId, + keyword: searchKeyWord, + }); + + console.log(test); + return ( <> diff --git a/client/src/pages/MyPage/index.tsx b/client/src/pages/MyPage/index.tsx index 73bb3f51..6a9d5dcf 100644 --- a/client/src/pages/MyPage/index.tsx +++ b/client/src/pages/MyPage/index.tsx @@ -1,19 +1,16 @@ -import React, { useState } from 'react'; +import { useState } from 'react'; import Navbar from '@pages/MyPage/Navbar'; import MyBody from '@pages/MyPage/MyBody'; import S from '@pages/MyPage/style'; -import HeaderLayout from '@/layout/HeaderLayout/HeaderLayout'; const MyPage = () => { const [selectNumber, setSelectNumber] = useState(0); return ( - - - - - - + + + + ); }; diff --git a/client/src/types/index.d.ts b/client/src/types/index.d.ts new file mode 100644 index 00000000..9fa0f8f3 --- /dev/null +++ b/client/src/types/index.d.ts @@ -0,0 +1,3 @@ +export type Optional = { + [P in keyof T]?: T[P]; +}; diff --git a/client/src/types/main.type.ts b/client/src/types/main.type.ts index bfc7627c..670b542a 100644 --- a/client/src/types/main.type.ts +++ b/client/src/types/main.type.ts @@ -93,6 +93,11 @@ export type UserListProps = { export type SearchProps = { setState: (searchValue: string) => void; placeholder: string; + state: { + userId: selectType[]; + tagId: selectType[]; + search: string; + }; }; export type MapComponentProps = { diff --git a/client/src/utils/KkiriProvider.tsx b/client/src/utils/KkiriProvider.tsx index 36b55e9e..3a10d676 100644 --- a/client/src/utils/KkiriProvider.tsx +++ b/client/src/utils/KkiriProvider.tsx @@ -3,7 +3,7 @@ import { QueryClientProvider } from '@tanstack/react-query'; import GlobalStyles from '@styles/global'; import { theme } from '@/styles/theme/theme'; import { ThemeProvider } from 'styled-components'; -import {queryClient} from "@hooks/api/queryClient"; +import { queryClient } from '@hooks/api/queryClient'; const KkiriProvider = ({ children }: { children: ReactNode }) => { return (