diff --git a/.gitignore b/.gitignore index bc120b1..910ab23 100644 --- a/.gitignore +++ b/.gitignore @@ -37,4 +37,7 @@ next-env.d.ts *storybook.log /storybook-static -*.log \ No newline at end of file +*.log + + +pull_request.md \ No newline at end of file diff --git a/app/page.tsx b/app/page.tsx index edf39c3..0ae5269 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,23 +1,8 @@ -import axios from 'axios'; - import IconArrowFront from '/public/assets/icon/arrow-front.svg'; import { PageContainer } from '@/widgets/pageContainer/ui/PageContainer'; -type User = { - firstName: string; - lastName: string; -}; - -async function getUser() { - const result = await axios.get('https://api.example.com/user'); - const user = result.data as User; - return user; -} - export default async function Home() { - const user = await getUser(); - return (
-

Hello, {user.firstName}!

White

Gray 01

diff --git a/package.json b/package.json index 83f15a0..80402b2 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,7 @@ "eslint-config-prettier": "^9.1.0", "eslint-plugin-import": "^2.31.0", "eslint-plugin-storybook": "^0.9.0", + "fuse.js": "^7.0.0", "globals": "^15.10.0", "husky": "^9.1.6", "jsdom": "^25.0.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8d274ef..fffbf45 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -114,6 +114,9 @@ importers: eslint-plugin-storybook: specifier: ^0.9.0 version: 0.9.0(eslint@8.57.0)(typescript@5.6.3) + fuse.js: + specifier: ^7.0.0 + version: 7.0.0 globals: specifier: ^15.10.0 version: 15.11.0 @@ -3337,6 +3340,10 @@ packages: functions-have-names@1.2.3: resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + fuse.js@7.0.0: + resolution: {integrity: sha512-14F4hBIxqKvD4Zz/XjDc3y94mNZN6pRv3U13Udo0lNLCWRBUsrMv2xwcF/y/Z5sV6+FQW+/ow68cHpm4sunt8Q==} + engines: {node: '>=10'} + gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} @@ -9352,6 +9359,8 @@ snapshots: functions-have-names@1.2.3: {} + fuse.js@7.0.0: {} + gensync@1.0.0-beta.2: {} get-caller-file@2.0.5: {} diff --git a/src/app/mocks/browser.ts b/src/app/mocks/browser.ts index 25ab2f1..d3b61fa 100644 --- a/src/app/mocks/browser.ts +++ b/src/app/mocks/browser.ts @@ -1,5 +1,5 @@ import { setupWorker } from 'msw/browser'; -import { handlers } from './exam/handlers'; +import { searchHandlers } from './handlers'; -export const worker = setupWorker(...handlers); +export const worker = setupWorker(...searchHandlers); diff --git a/src/app/mocks/data/feed.ts b/src/app/mocks/data/feed.ts new file mode 100644 index 0000000..2d49168 --- /dev/null +++ b/src/app/mocks/data/feed.ts @@ -0,0 +1,80 @@ +interface IImage { + id: number; + imageUrl: string; +} + +interface IFeed { + id: number; + + user: { + id: number; + profileImage: string; + username: string; + }; + + content: string; + images: IImage[]; + + likeCount: number; + commentCount: number; + + isLiked: boolean; + isBookmarked: boolean; + + createdAt: string; + updatedAt: string; +} + +function generateRandomContent(length: number) { + const words = 'Lorem ipsum dolor sit amet consectetur adipiscing elit'.split(' '); + let content = ''; + + for (let i = 0; i < length; i++) { + content += words[Math.floor(Math.random() * words.length)] + ' '; + } + + return content.trim(); +} + +function generateRandomImages() { + const imageCount = Math.floor(Math.random() * 4); + const images: IImage[] = []; + + for (let id = 1; id <= imageCount; id++) { + images.push({ + id, + imageUrl: `https://picsum.photos/id/${id}/200/300`, + }); + } + + return images; +} + +function generateFeedMockData(count: number) { + const mockFeeds: IFeed[] = []; + + for (let id = 1; id <= count; id++) { + const feed = { + id, + user: { + id: id, + profileImage: `https://randomuser.me/api/portraits/${Math.random() > 0.5 ? 'men' : 'women'}/${id}.jpg`, + username: `user_${id}`, + }, + content: generateRandomContent(Math.floor(Math.random() * 300)), + images: generateRandomImages(), + likeCount: Math.floor(Math.random() * 1000), + commentCount: Math.floor(Math.random() * 500), + isLiked: Math.random() < 0.5, + isBookmarked: Math.random() < 0.5, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }; + + mockFeeds.push(feed); + } + + return mockFeeds; +} + +export const feedMockData = generateFeedMockData(100); diff --git a/src/app/mocks/data/index.ts b/src/app/mocks/data/index.ts new file mode 100644 index 0000000..0b2a3ad --- /dev/null +++ b/src/app/mocks/data/index.ts @@ -0,0 +1 @@ +export * from './feed'; diff --git a/src/app/mocks/exam/handlers.ts b/src/app/mocks/exam/handlers.ts deleted file mode 100644 index ec1a90f..0000000 --- a/src/app/mocks/exam/handlers.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { http, HttpResponse } from 'msw'; - -export type User = { - firstName: string; - lastName: string; -}; - -export const handlers = [ - http.get('https://api.example.com/user', () => { - return HttpResponse.json({ - firstName: 'Lee', - lastName: 'Maverick', - }); - }), -]; diff --git a/src/app/mocks/handlers/index.ts b/src/app/mocks/handlers/index.ts new file mode 100644 index 0000000..5a2bdeb --- /dev/null +++ b/src/app/mocks/handlers/index.ts @@ -0,0 +1 @@ +export * from './search'; diff --git a/src/app/mocks/handlers/search.ts b/src/app/mocks/handlers/search.ts new file mode 100644 index 0000000..24689b5 --- /dev/null +++ b/src/app/mocks/handlers/search.ts @@ -0,0 +1,40 @@ +import Fuse from 'fuse.js'; +import { http } from 'msw'; + +import { feedMockData } from '../data'; +import { createHttpErrorResponse, createHttpSuccessResponse } from '../lib'; + +export const searchHandlers = [ + http.get('http://api.example.com/v2/feeds', ({ request }) => { + const url = new URL(request.url); + + const query = url.searchParams.get('query'); + const page = Number(url.searchParams.get('page') || 1); + const size = Number(url.searchParams.get('size') || 15); + + if (!query || query.length < 2) { + return createHttpErrorResponse('검색어는 최소 2글자 이상이어야 합니다'); + } + + const fuse = new Fuse(feedMockData, { + keys: ['content', 'user.username'], + }); + + const contents = fuse + .search(query) + .map((result) => result.item) + .slice((page - 1) * size, page * size); + const totalFeeds = contents.length; + const hasNextPage = totalFeeds > page * size; + + return createHttpSuccessResponse({ + feed: { + contents, + currentPageNumber: page, + pageSize: size, + numberOfElements: contents.length, + hasNextPage, + }, + }); + }), +]; diff --git a/src/app/mocks/index.ts b/src/app/mocks/index.ts index 2f94b16..a375173 100644 --- a/src/app/mocks/index.ts +++ b/src/app/mocks/index.ts @@ -1,3 +1,2 @@ export { MSWProvider } from './MswProvider'; -export type { User } from './exam/handlers'; export { server } from './node'; diff --git a/src/app/mocks/lib/index.ts b/src/app/mocks/lib/index.ts new file mode 100644 index 0000000..dbc1ea0 --- /dev/null +++ b/src/app/mocks/lib/index.ts @@ -0,0 +1 @@ +export * from './response'; diff --git a/src/app/mocks/lib/response.ts b/src/app/mocks/lib/response.ts new file mode 100644 index 0000000..a09b4bf --- /dev/null +++ b/src/app/mocks/lib/response.ts @@ -0,0 +1,31 @@ +import { HttpResponse, delay } from 'msw'; + +/** + * @description Mocking API의 Success 응답을 생성하는 메서드 + */ +export async function createHttpSuccessResponse(data: T) { + await delay(); + + return HttpResponse.json( + { + code: '2000', + data, + }, + { status: 200 }, + ); +} + +/** + * @description Mocking API의 Error 응답을 생성하는 메서드 + */ +export async function createHttpErrorResponse(errorMessage: string) { + await delay(); + + return HttpResponse.json( + { + code: '5200', + message: errorMessage, + }, + { status: 200 }, + ); +} diff --git a/src/app/mocks/node.ts b/src/app/mocks/node.ts index 087f3cd..618e02b 100644 --- a/src/app/mocks/node.ts +++ b/src/app/mocks/node.ts @@ -1,5 +1,3 @@ import { setupServer } from 'msw/node'; -import { handlers } from './exam/handlers'; - -export const server = setupServer(...handlers); +export const server = setupServer();