From 99daeb0f27bf15e3db44addb0f43d4fe52170e1a Mon Sep 17 00:00:00 2001 From: Harang Date: Sun, 21 Jan 2024 23:14:56 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20api=20=EA=B5=AC=ED=98=84=20=EB=B0=8F=20?= =?UTF-8?q?/projects=20api=20route=20=EC=A0=81=EC=9A=A9=20(#27)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - install qs, dayjs dependency 설치 - api 구현 및 /projects api route 적용 --- .gitignore | 1 + .pnp.cjs | 6 +++ @types/environment.d.ts | 7 +++ package.json | 6 ++- src/app/api/index.ts | 61 +++++++++++++++++++++++++ src/app/api/model.ts | 19 ++++++++ src/app/api/projects/route.ts | 26 +++++++++++ src/components/pages/HomePage/index.tsx | 1 + src/utils/index.test.ts | 24 ++++++++++ src/utils/index.ts | 7 +++ yarn.lock | 9 ++-- 11 files changed, 163 insertions(+), 4 deletions(-) create mode 100644 @types/environment.d.ts create mode 100644 src/app/api/index.ts create mode 100644 src/app/api/model.ts create mode 100644 src/app/api/projects/route.ts create mode 100644 src/utils/index.test.ts create mode 100644 src/utils/index.ts diff --git a/.gitignore b/.gitignore index f938245..e917a53 100644 --- a/.gitignore +++ b/.gitignore @@ -27,6 +27,7 @@ yarn-error.log* # local env files .env*.local +.env # vercel .vercel diff --git a/.pnp.cjs b/.pnp.cjs index d0fdb33..219aea1 100755 --- a/.pnp.cjs +++ b/.pnp.cjs @@ -42,6 +42,7 @@ const RAW_RUNTIME_STATE = ["@testing-library/react", "virtual:fb849477c4f6827542d449f15c7c722160da93f176c17f812a0313ca46807e09cc98138e0137a27649585f2cd5cc0e349cf48f47e4f114851c36835d574e8126#npm:14.1.2"],\ ["@types/jest", "npm:29.5.9"],\ ["@types/node", "npm:20.11.0"],\ + ["@types/qs", "npm:6.9.11"],\ ["@types/react", "npm:18.2.47"],\ ["@types/react-dom", "npm:18.2.18"],\ ["@types/react-test-renderer", "npm:18.0.0"],\ @@ -49,6 +50,7 @@ const RAW_RUNTIME_STATE = ["@typescript-eslint/parser", "virtual:fb849477c4f6827542d449f15c7c722160da93f176c17f812a0313ca46807e09cc98138e0137a27649585f2cd5cc0e349cf48f47e4f114851c36835d574e8126#npm:6.12.0"],\ ["clsx", "npm:2.1.0"],\ ["cypress", "npm:12.1.0"],\ + ["dayjs", "npm:1.11.10"],\ ["eslint", "npm:8.56.0"],\ ["eslint-config-airbnb", "virtual:fb849477c4f6827542d449f15c7c722160da93f176c17f812a0313ca46807e09cc98138e0137a27649585f2cd5cc0e349cf48f47e4f114851c36835d574e8126#npm:19.0.4"],\ ["eslint-config-airbnb-typescript", "virtual:fb849477c4f6827542d449f15c7c722160da93f176c17f812a0313ca46807e09cc98138e0137a27649585f2cd5cc0e349cf48f47e4f114851c36835d574e8126#npm:17.1.0"],\ @@ -71,6 +73,7 @@ const RAW_RUNTIME_STATE = ["next", "virtual:fb849477c4f6827542d449f15c7c722160da93f176c17f812a0313ca46807e09cc98138e0137a27649585f2cd5cc0e349cf48f47e4f114851c36835d574e8126#npm:14.0.4"],\ ["postcss", "npm:8.4.32"],\ ["postcss-scss", "virtual:fb849477c4f6827542d449f15c7c722160da93f176c17f812a0313ca46807e09cc98138e0137a27649585f2cd5cc0e349cf48f47e4f114851c36835d574e8126#npm:4.0.9"],\ + ["qs", "npm:6.11.2"],\ ["react", "npm:18.2.0"],\ ["react-dom", "virtual:fb849477c4f6827542d449f15c7c722160da93f176c17f812a0313ca46807e09cc98138e0137a27649585f2cd5cc0e349cf48f47e4f114851c36835d574e8126#npm:18.2.0"],\ ["react-fast-marquee", "virtual:fb849477c4f6827542d449f15c7c722160da93f176c17f812a0313ca46807e09cc98138e0137a27649585f2cd5cc0e349cf48f47e4f114851c36835d574e8126#npm:1.6.3"],\ @@ -11946,6 +11949,7 @@ const RAW_RUNTIME_STATE = ["@testing-library/react", "virtual:fb849477c4f6827542d449f15c7c722160da93f176c17f812a0313ca46807e09cc98138e0137a27649585f2cd5cc0e349cf48f47e4f114851c36835d574e8126#npm:14.1.2"],\ ["@types/jest", "npm:29.5.9"],\ ["@types/node", "npm:20.11.0"],\ + ["@types/qs", "npm:6.9.11"],\ ["@types/react", "npm:18.2.47"],\ ["@types/react-dom", "npm:18.2.18"],\ ["@types/react-test-renderer", "npm:18.0.0"],\ @@ -11953,6 +11957,7 @@ const RAW_RUNTIME_STATE = ["@typescript-eslint/parser", "virtual:fb849477c4f6827542d449f15c7c722160da93f176c17f812a0313ca46807e09cc98138e0137a27649585f2cd5cc0e349cf48f47e4f114851c36835d574e8126#npm:6.12.0"],\ ["clsx", "npm:2.1.0"],\ ["cypress", "npm:12.1.0"],\ + ["dayjs", "npm:1.11.10"],\ ["eslint", "npm:8.56.0"],\ ["eslint-config-airbnb", "virtual:fb849477c4f6827542d449f15c7c722160da93f176c17f812a0313ca46807e09cc98138e0137a27649585f2cd5cc0e349cf48f47e4f114851c36835d574e8126#npm:19.0.4"],\ ["eslint-config-airbnb-typescript", "virtual:fb849477c4f6827542d449f15c7c722160da93f176c17f812a0313ca46807e09cc98138e0137a27649585f2cd5cc0e349cf48f47e4f114851c36835d574e8126#npm:17.1.0"],\ @@ -11975,6 +11980,7 @@ const RAW_RUNTIME_STATE = ["next", "virtual:fb849477c4f6827542d449f15c7c722160da93f176c17f812a0313ca46807e09cc98138e0137a27649585f2cd5cc0e349cf48f47e4f114851c36835d574e8126#npm:14.0.4"],\ ["postcss", "npm:8.4.32"],\ ["postcss-scss", "virtual:fb849477c4f6827542d449f15c7c722160da93f176c17f812a0313ca46807e09cc98138e0137a27649585f2cd5cc0e349cf48f47e4f114851c36835d574e8126#npm:4.0.9"],\ + ["qs", "npm:6.11.2"],\ ["react", "npm:18.2.0"],\ ["react-dom", "virtual:fb849477c4f6827542d449f15c7c722160da93f176c17f812a0313ca46807e09cc98138e0137a27649585f2cd5cc0e349cf48f47e4f114851c36835d574e8126#npm:18.2.0"],\ ["react-fast-marquee", "virtual:fb849477c4f6827542d449f15c7c722160da93f176c17f812a0313ca46807e09cc98138e0137a27649585f2cd5cc0e349cf48f47e4f114851c36835d574e8126#npm:1.6.3"],\ diff --git a/@types/environment.d.ts b/@types/environment.d.ts new file mode 100644 index 0000000..31c8a6c --- /dev/null +++ b/@types/environment.d.ts @@ -0,0 +1,7 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +namespace NodeJS { + interface ProcessEnv extends NodeJS.ProcessEnv { + NEXT_PUBLIC_API_HOST: string; + NEXT_PUBLIC_ORIGIN: string; + } +} diff --git a/package.json b/package.json index 5ac0d28..bf12f2b 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,8 @@ "license": "MIT", "author": "dnd-academy", "scripts": { - "dev": "next dev", + "open-browser": "open http://dnd-academy.localhost:3000", + "dev": "next dev -H dnd-academy.localhost -p 3000 & yarn open-browser", "build": "next build", "start": "next start", "lint": "eslint '**/*.{js,jsx,ts,tsx}' --fix", @@ -31,7 +32,9 @@ "homepage": "https://github.com/DNDACADEMY/dnd-academy-v2#readme", "dependencies": { "clsx": "2.1.0", + "dayjs": "1.11.10", "next": "14.0.4", + "qs": "6.11.2", "react": "18.2.0", "react-dom": "18.2.0", "react-fast-marquee": "1.6.3", @@ -54,6 +57,7 @@ "@testing-library/react": "14.1.2", "@types/jest": "29.5.9", "@types/node": "20", + "@types/qs": "^6", "@types/react": "18.2.47", "@types/react-dom": "18.2.18", "@types/react-test-renderer": "18.0.0", diff --git a/src/app/api/index.ts b/src/app/api/index.ts new file mode 100644 index 0000000..2b6bc67 --- /dev/null +++ b/src/app/api/index.ts @@ -0,0 +1,61 @@ +import dayjs from 'dayjs'; + +import { paramsSerializer } from '@/utils'; + +import { FetchRequest } from './model'; + +const CACHE_MINUTE = 5; + +// TODO - fetch error 수정 필요 +export class FetchError extends Error { + constructor( + response: Response, + ) { + super(); + this.response = response; + } + + response?: Response; +} + +export const getCacheDate = (cacheTime = CACHE_MINUTE) => { + const date = dayjs().format('YYYY-MM-DD-HH'); + const currentMin = dayjs().get('minute'); + const modMin = dayjs().get('minute') % cacheTime; + const minute = modMin === 0 ? currentMin : currentMin - modMin; + + return `?date=${date}-${minute}`; +}; + +const getUrl = (url: string, isBFF = false) => { + if (isBFF) { + return `${process.env.NEXT_PUBLIC_ORIGIN}/api${url}`; + } + + return `${process.env.NEXT_PUBLIC_API_HOST}${url}`; +}; + +async function api({ + url, params, config = {}, isBFF, method = 'GET', +}: FetchRequest): Promise { + const response = await fetch(`${getUrl(url, isBFF)}?${paramsSerializer({ + ...params, + })}`, { + ...config, + method, + headers: { + ...config.headers, + 'Content-Type': 'application/json', + }, + }); + + if (!response.ok) { + throw new FetchError(response); + } + + const data = await response.json() as Promise; + + return data; +} + +export default api; diff --git a/src/app/api/model.ts b/src/app/api/model.ts new file mode 100644 index 0000000..3bf55ae --- /dev/null +++ b/src/app/api/model.ts @@ -0,0 +1,19 @@ +type Method = + | 'get' | 'GET' + | 'delete' | 'DELETE' + | 'head' | 'HEAD' + | 'options' | 'OPTIONS' + | 'post' | 'POST' + | 'put' | 'PUT' + | 'patch' | 'PATCH' + | 'purge' | 'PURGE' + | 'link' | 'LINK' + | 'unlink' | 'UNLINK'; + +export interface FetchRequest { + url: string; + params?: T; + method?: Method; + isBFF?: boolean; + config?: Omit; +} diff --git a/src/app/api/projects/route.ts b/src/app/api/projects/route.ts new file mode 100644 index 0000000..ead28c1 --- /dev/null +++ b/src/app/api/projects/route.ts @@ -0,0 +1,26 @@ +import { NextRequest, NextResponse } from 'next/server'; + +import { getCacheDate } from '..'; + +export const runtime = 'edge'; + +export async function GET(request: NextRequest) { + const requestHeaders = new Headers(request.headers); + + const response = await fetch(`${process.env.NEXT_PUBLIC_API_HOST}/data/project.json${getCacheDate()}`); + + if (!response.ok) { + return NextResponse.json(null, { + status: response.status, + statusText: response.statusText, + }); + } + + const data = await response.json(); + + return NextResponse.json(data, { + status: response.status, + statusText: response.statusText, + headers: requestHeaders, + }); +} diff --git a/src/components/pages/HomePage/index.tsx b/src/components/pages/HomePage/index.tsx index cf41e48..cda7603 100644 --- a/src/components/pages/HomePage/index.tsx +++ b/src/components/pages/HomePage/index.tsx @@ -41,6 +41,7 @@ function HomePage() { src="https://dnd-academy-v3.s3.ap-northeast-2.amazonaws.com/images/banner/about.png" alt="main-banner" fill + priority sizes="(max-width: 1204px) 50vw, 33vw" className={styles.banner} /> diff --git a/src/utils/index.test.ts b/src/utils/index.test.ts new file mode 100644 index 0000000..e0c99e5 --- /dev/null +++ b/src/utils/index.test.ts @@ -0,0 +1,24 @@ +import qs from 'qs'; + +import { paramsSerializer } from '.'; + +describe('paramsSerializer', () => { + it('"qs.stringify"를 호출해야만 한다', () => { + const qsSpyOn = jest.spyOn(qs, 'stringify'); + const params = { + param1: 'apple', + param2: 'banana', + param3: 'orange', + }; + + const result = paramsSerializer(params); + + expect(result).toBe('param1=apple¶m2=banana¶m3=orange'); + expect(qsSpyOn).toHaveBeenCalledWith(params, { + indices: false, + arrayFormat: 'comma', + }); + + qsSpyOn.mockRestore(); + }); +}); diff --git a/src/utils/index.ts b/src/utils/index.ts new file mode 100644 index 0000000..b2fcc17 --- /dev/null +++ b/src/utils/index.ts @@ -0,0 +1,7 @@ +import qs from 'qs'; + +// eslint-disable-next-line import/prefer-default-export +export const paramsSerializer = (params: T): string => qs.stringify(params, { + arrayFormat: 'comma', + indices: false, +}); diff --git a/yarn.lock b/yarn.lock index f282c26..f0376eb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4983,7 +4983,7 @@ __metadata: languageName: node linkType: hard -"@types/qs@npm:*, @types/qs@npm:^6.9.5": +"@types/qs@npm:*, @types/qs@npm:^6, @types/qs@npm:^6.9.5": version: 6.9.11 resolution: "@types/qs@npm:6.9.11" checksum: 620ca1628bf3da65662c54ed6ebb120b18a3da477d0bfcc872b696685a9bb1893c3c92b53a1190a8f54d52eaddb6af8b2157755699ac83164604329935e8a7f2 @@ -7758,7 +7758,7 @@ __metadata: languageName: node linkType: hard -"dayjs@npm:^1.10.4": +"dayjs@npm:1.11.10, dayjs@npm:^1.10.4": version: 1.11.10 resolution: "dayjs@npm:1.11.10" checksum: 27e8f5bc01c0a76f36c656e62ab7f08c2e7b040b09e613cd4844abf03fb258e0350f0a83b02c887b84d771c1f11e092deda0beef8c6df2a1afbc3f6c1fade279 @@ -8128,6 +8128,7 @@ __metadata: "@testing-library/react": "npm:14.1.2" "@types/jest": "npm:29.5.9" "@types/node": "npm:20" + "@types/qs": "npm:^6" "@types/react": "npm:18.2.47" "@types/react-dom": "npm:18.2.18" "@types/react-test-renderer": "npm:18.0.0" @@ -8135,6 +8136,7 @@ __metadata: "@typescript-eslint/parser": "npm:6.12.0" clsx: "npm:2.1.0" cypress: "npm:12.1.0" + dayjs: "npm:1.11.10" eslint: "npm:8.56.0" eslint-config-airbnb: "npm:19.0.4" eslint-config-airbnb-typescript: "npm:17.1.0" @@ -8157,6 +8159,7 @@ __metadata: next: "npm:14.0.4" postcss: "npm:8.4.32" postcss-scss: "npm:4.0.9" + qs: "npm:6.11.2" react: "npm:18.2.0" react-dom: "npm:18.2.0" react-fast-marquee: "npm:1.6.3" @@ -14428,7 +14431,7 @@ __metadata: languageName: node linkType: hard -"qs@npm:^6.10.0, qs@npm:^6.11.2": +"qs@npm:6.11.2, qs@npm:^6.10.0, qs@npm:^6.11.2": version: 6.11.2 resolution: "qs@npm:6.11.2" dependencies: