Skip to content

Commit

Permalink
feat: api 구현 및 /projects api route 적용 (#27)
Browse files Browse the repository at this point in the history
- install qs, dayjs dependency 설치
- api 구현 및 /projects api route 적용
  • Loading branch information
saseungmin committed Jun 29, 2024
1 parent 76da268 commit 99daeb0
Show file tree
Hide file tree
Showing 11 changed files with 163 additions and 4 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ yarn-error.log*

# local env files
.env*.local
.env

# vercel
.vercel
Expand Down
6 changes: 6 additions & 0 deletions .pnp.cjs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions @types/environment.d.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand All @@ -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",
Expand Down
61 changes: 61 additions & 0 deletions src/app/api/index.ts
Original file line number Diff line number Diff line change
@@ -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<T, K>({
url, params, config = {}, isBFF, method = 'GET',
}: FetchRequest<K>): Promise<T> {
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<T>;

return data;
}

export default api;
19 changes: 19 additions & 0 deletions src/app/api/model.ts
Original file line number Diff line number Diff line change
@@ -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<T = any> {
url: string;
params?: T;
method?: Method;
isBFF?: boolean;
config?: Omit<RequestInit, 'method'>;
}
26 changes: 26 additions & 0 deletions src/app/api/projects/route.ts
Original file line number Diff line number Diff line change
@@ -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,
});
}
1 change: 1 addition & 0 deletions src/components/pages/HomePage/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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}
/>
Expand Down
24 changes: 24 additions & 0 deletions src/utils/index.test.ts
Original file line number Diff line number Diff line change
@@ -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&param2=banana&param3=orange');
expect(qsSpyOn).toHaveBeenCalledWith(params, {
indices: false,
arrayFormat: 'comma',
});

qsSpyOn.mockRestore();
});
});
7 changes: 7 additions & 0 deletions src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import qs from 'qs';

// eslint-disable-next-line import/prefer-default-export
export const paramsSerializer = <T>(params: T): string => qs.stringify(params, {
arrayFormat: 'comma',
indices: false,
});
9 changes: 6 additions & 3 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -8128,13 +8128,15 @@ __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"
"@typescript-eslint/eslint-plugin": "npm:6.12.0"
"@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"
Expand All @@ -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"
Expand Down Expand Up @@ -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:
Expand Down

0 comments on commit 99daeb0

Please sign in to comment.