From 7fd6988e4993b4cd5fd9e69f2fc68f6b3addf90a Mon Sep 17 00:00:00 2001 From: ChanLee_KR Date: Wed, 15 May 2024 16:32:31 +0900 Subject: [PATCH 1/2] feat: api mocking (#ATR-11) --- apps/service/__mocks__/data/articles.ts | 205 ++++++++++++++++++ apps/service/__mocks__/data/index.ts | 1 + .../service/__mocks__/handlers/inbox/index.ts | 82 ++++++- 3 files changed, 286 insertions(+), 2 deletions(-) create mode 100644 apps/service/__mocks__/data/articles.ts create mode 100644 apps/service/__mocks__/data/index.ts diff --git a/apps/service/__mocks__/data/articles.ts b/apps/service/__mocks__/data/articles.ts new file mode 100644 index 00000000..06fbaca5 --- /dev/null +++ b/apps/service/__mocks__/data/articles.ts @@ -0,0 +1,205 @@ +const getThumbnailUrl = (keyword: string) => + `https://source.unsplash.com/random/1280x720/?${keyword}` + +export const articles = [ + { + id: 1, + thumbnailUrl: getThumbnailUrl('newsletter'), + title: '트렌드에 대한 최신 업데이트', + category: 'business', + date: '2024-03-05', + readPercentage: 0, + readingTime: 15, + isRead: false, + }, + { + id: 2, + thumbnailUrl: getThumbnailUrl('tech'), + title: '블록체인 기술의 미래 전망', + category: 'technology', + date: '2024-02-20', + readPercentage: 0, + readingTime: 8, + isRead: false, + }, + { + id: 3, + thumbnailUrl: getThumbnailUrl('economy'), + title: '글로벌 경제 상황과 전망', + category: 'economy', + date: '2024-03-10', + readPercentage: 0, + readingTime: 2, + isRead: false, + }, + { + id: 4, + thumbnailUrl: getThumbnailUrl('ai'), + title: 'AI 기술의 현재와 미래', + category: 'technology', + date: '2024-02-29', + readPercentage: 0, + readingTime: 12, + isRead: false, + }, + { + id: 5, + thumbnailUrl: getThumbnailUrl('startup'), + title: '스타트업 생태계의 동향과 분석', + category: 'business', + date: '2024-03-15', + readPercentage: 0, + readingTime: 4, + isRead: false, + }, + { + id: 6, + thumbnailUrl: getThumbnailUrl('environment'), + title: '지속 가능한 환경을 위한 노력들', + category: 'society', + date: '2024-02-18', + readPercentage: 0, + readingTime: 18, + isRead: false, + }, + { + id: 7, + thumbnailUrl: getThumbnailUrl('health'), + title: '건강한 생활 습관에 대한 조언', + category: 'health', + date: '2024-03-01', + readPercentage: 0, + readingTime: 5, + isRead: false, + }, + { + id: 8, + thumbnailUrl: getThumbnailUrl('investment'), + title: '다양한 투자 전략과 팁', + category: 'economy', + date: '2024-02-25', + readPercentage: 0, + readingTime: 7, + isRead: false, + }, + { + id: 9, + thumbnailUrl: getThumbnailUrl('data'), + title: '빅데이터의 활용과 분석 방법', + category: 'technology', + date: '2024-03-05', + readPercentage: 0, + readingTime: 13, + isRead: false, + }, + { + id: 10, + thumbnailUrl: getThumbnailUrl('finance'), + title: '금융 시장의 최신 동향과 전망', + category: 'economy', + date: '2024-02-15', + readPercentage: 0, + readingTime: 20, + isRead: false, + }, + { + id: 11, + thumbnailUrl: getThumbnailUrl('internet'), + title: '인터넷 보안에 대한 중요성과 대응 전략', + category: 'technology', + date: '2024-02-22', + readPercentage: 0, + readingTime: 16, + isRead: false, + }, + { + id: 12, + thumbnailUrl: getThumbnailUrl('marketing'), + title: '디지털 마케팅의 효과적인 전략', + category: 'business', + date: '2024-03-10', + readPercentage: 0, + readingTime: 3, + isRead: false, + }, + { + id: 13, + thumbnailUrl: getThumbnailUrl('education'), + title: '현대 교육 방식의 혁신과 도전', + category: 'society', + date: '2024-02-29', + readPercentage: 0, + readingTime: 14, + isRead: false, + }, + { + id: 14, + thumbnailUrl: getThumbnailUrl('leadership'), + title: '리더십의 핵심 요소와 발전 방향', + category: 'business', + date: '2024-03-20', + readPercentage: 0, + readingTime: 11, + isRead: false, + }, + { + id: 15, + thumbnailUrl: getThumbnailUrl('food'), + title: '건강한 식습관을 위한 다양한 요리 레시피', + category: 'health', + date: '2024-02-17', + readPercentage: 0, + readingTime: 19, + isRead: false, + }, + { + id: 16, + thumbnailUrl: getThumbnailUrl('travel'), + title: '여행지 추천과 여행 팁', + category: 'lifestyle', + date: '2024-03-12', + readPercentage: 0, + readingTime: 10, + isRead: false, + }, + { + id: 17, + thumbnailUrl: 'culture', + title: '세계 각국의 문화와 특징', + category: 'lifestyle', + date: '2024-03-07', + readPercentage: 0, + readingTime: 1, + isRead: false, + }, + { + id: 18, + thumbnailUrl: 'design', + title: '디자인 트렌드와 아이디어', + category: 'design', + date: '2024-02-28', + readPercentage: 0, + readingTime: 6, + isRead: false, + }, + { + id: 19, + thumbnailUrl: 'entertainment', + title: '연예계 소식과 유명인의 인터뷰', + category: 'entertainment', + date: '2024-02-21', + readPercentage: 0, + readingTime: 17, + isRead: false, + }, + { + id: 20, + thumbnailUrl: getThumbnailUrl('society'), + title: '사회 문제와 대응 전략', + category: 'society', + date: '2024-03-18', + readPercentage: 0, + readingTime: 9, + isRead: false, + }, +] diff --git a/apps/service/__mocks__/data/index.ts b/apps/service/__mocks__/data/index.ts new file mode 100644 index 00000000..34c2fa2f --- /dev/null +++ b/apps/service/__mocks__/data/index.ts @@ -0,0 +1 @@ +export { articles } from './articles' diff --git a/apps/service/__mocks__/handlers/inbox/index.ts b/apps/service/__mocks__/handlers/inbox/index.ts index 77f45286..7bfe0683 100644 --- a/apps/service/__mocks__/handlers/inbox/index.ts +++ b/apps/service/__mocks__/handlers/inbox/index.ts @@ -1,5 +1,83 @@ -import { HttpHandler } from 'msw' +import { HttpHandler, HttpResponse } from 'msw' +import { error, get, getParams } from '..' +import { articles } from '@/__mocks__/data' -const inboxHandlers: HttpHandler[] = [] +const defaultPagination = { + size: 0, // 읽어온 데이터 갯수 + number: 0, // size 기준 현재 페이지 (0부터 시작) + sort: { empty: true, sorted: true, unsorted: true }, + numberOfElements: 0, + pageable: { + offset: 0, + sort: { empty: true, sorted: true, unsorted: true }, + pageSize: 0, + paged: true, + pageNumber: 0, + unpaged: true, + }, + first: true, // 첫 번째 페이지 + last: true, // 마지막 페이지 + empty: true, +} + +type DefaultPagination = typeof defaultPagination +type Article = (typeof articles)[number] + +const sorted: { [key: string]: (a: Article, b: Article) => number } = { + desc: (a, b) => new Date(a.date).getTime() - new Date(b.date).getTime(), + asc: (a, b) => new Date(b.date).getTime() - new Date(a.date).getTime(), +} + +const inboxHandlers: HttpHandler[] = [ + get('/v1/member/:memberId/articles', ({ request, params }) => { + const memberId = Number(params.memberId) + const searchParams = getParams(request.url) + const page = Number(searchParams.get('page') ?? 0) + const size = Number(searchParams.get('size') ?? 12) + const sort = searchParams.get('sort') ?? 'asc' + const query = searchParams.get('q') + const isInvalidRequest = [memberId, page, size].some(isNaN) + + if (isInvalidRequest) { + return error('잘못된 요청입니다', 400) + } + + const sortedArticles = [...articles].sort(sorted[sort]) + const searchedArticles = !!query + ? sortedArticles.filter((article) => article.title.includes(query)) + : sortedArticles + const resultOfArticles = Array.from( + { length: size }, + (_, i) => searchedArticles[i], + ).filter((e) => !!e) + + const data: { articles: Article[] } & DefaultPagination = Object.assign( + defaultPagination, + { + articles: resultOfArticles, + size: resultOfArticles.length, + number: page, + first: page === 0, + last: page >= 2 || resultOfArticles.length < size, + }, + ) + + return HttpResponse.json(data) + }), + get('/v1/articles/:articleId', ({ params }) => { + const articleId = Number(params.articleId) + + if (isNaN(articleId)) { + return error('잘못된 요청입니다', 400) + } + if (articleId > articles.length) { + return error('요청하신 아티클을 찾을 수 없습니다', 404) + } + + return HttpResponse.json({ + article: articles.find((article) => article.id === articleId), + }) + }), +] export default inboxHandlers From 198467c8958c783b45508fa50744edcbf850d5ce Mon Sep 17 00:00:00 2001 From: ChanLee_KR Date: Wed, 15 May 2024 22:16:28 +0900 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20msw=20=EC=83=81=ED=98=B8=20=EC=B0=B8?= =?UTF-8?q?=EC=A1=B0=20=EC=97=90=EB=9F=AC=20=EC=88=98=EC=A0=95=20(#ATR-58)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/__mocks__/handlers/inbox/index.ts | 2 +- apps/service/__mocks__/handlers/index.ts | 33 ++----------------- apps/service/__mocks__/handlers/tools.ts | 25 ++++++++++++++ apps/service/app/layout.tsx | 7 ++-- 4 files changed, 30 insertions(+), 37 deletions(-) create mode 100644 apps/service/__mocks__/handlers/tools.ts diff --git a/apps/service/__mocks__/handlers/inbox/index.ts b/apps/service/__mocks__/handlers/inbox/index.ts index 7bfe0683..88d1e7fb 100644 --- a/apps/service/__mocks__/handlers/inbox/index.ts +++ b/apps/service/__mocks__/handlers/inbox/index.ts @@ -1,5 +1,5 @@ import { HttpHandler, HttpResponse } from 'msw' -import { error, get, getParams } from '..' +import { error, get, getParams } from '../tools' import { articles } from '@/__mocks__/data' const defaultPagination = { diff --git a/apps/service/__mocks__/handlers/index.ts b/apps/service/__mocks__/handlers/index.ts index a6542cc5..7823f606 100644 --- a/apps/service/__mocks__/handlers/index.ts +++ b/apps/service/__mocks__/handlers/index.ts @@ -1,38 +1,9 @@ -import { - HttpHandler, - type HttpResponseResolver, - HttpResponse, - http, - delay, -} from 'msw' +import { HttpHandler, HttpResponse } from 'msw' +import { get } from './tools' import inboxHandlers from './inbox' import mainHandlers from './main' import mypageHandlers from './mypage' -const delayMS = 1200 -const baseURL = `${process.env.API_URL}/api` - -export const get = (path: string, resolver: HttpResponseResolver) => - http.get(baseURL + path, async (info) => { - console.log(`[MSW]: ${info.request.method} ${info.request.url} ✅`) - await delay(delayMS) - return resolver(info) - }) - -export const post = (path: string, resolver: HttpResponseResolver) => - http.post(baseURL + path, async (info) => { - const body = await info.request.json() - console.log(`[MSW]: ${info.request.method} ${info.request.url} ✅`) - console.log(` ᄂ ${JSON.stringify(body)}`) - await delay(delayMS) - return resolver(info) - }) - -export const error = (message: string, status: number) => - HttpResponse.json({ message: `${message} (MSW)` }, { status }) - -export const getParams = (url: string) => new URL(url).searchParams - const handlers: HttpHandler[] = [ get('/test', () => { return HttpResponse.json({ message: 'success!!!' }) diff --git a/apps/service/__mocks__/handlers/tools.ts b/apps/service/__mocks__/handlers/tools.ts new file mode 100644 index 00000000..5e1c2064 --- /dev/null +++ b/apps/service/__mocks__/handlers/tools.ts @@ -0,0 +1,25 @@ +import { type HttpResponseResolver, HttpResponse, http, delay } from 'msw' + +const delayMS = 1200 +const baseURL = `${process.env.API_URL}/api` + +export const get = (path: string, resolver: HttpResponseResolver) => + http.get(baseURL + path, async (info) => { + console.log(`[MSW]: ${info.request.method} ${info.request.url} ✅`) + await delay(delayMS) + return resolver(info) + }) + +export const post = (path: string, resolver: HttpResponseResolver) => + http.post(baseURL + path, async (info) => { + const body = await info.request.json() + console.log(`[MSW]: ${info.request.method} ${info.request.url} ✅`) + console.log(` ᄂ ${JSON.stringify(body)}`) + await delay(delayMS) + return resolver(info) + }) + +export const error = (message: string, status: number) => + HttpResponse.json({ message: `${message} (MSW)` }, { status }) + +export const getParams = (url: string) => new URL(url).searchParams diff --git a/apps/service/app/layout.tsx b/apps/service/app/layout.tsx index dc9cbda0..51b259ac 100644 --- a/apps/service/app/layout.tsx +++ b/apps/service/app/layout.tsx @@ -1,15 +1,12 @@ import type { Metadata } from 'next' -import { Inter } from 'next/font/google' -import './globals.css' import '@/public/fonts/fonts.css' +import './globals.css' import initMSW from '@/__mocks__' if (process.env.NODE_ENV !== 'production') { initMSW() } -const inter = Inter({ subsets: ['latin'] }) - export const metadata: Metadata = { title: 'Create Next App', description: 'Generated by create next app', @@ -22,7 +19,7 @@ export default function RootLayout({ }>) { return ( - {children} + {children} ) }