Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

템플릿 목록 조회, 상세 조회, 생성 요청 로직 리펙토링 #110

Merged
merged 18 commits into from
Jul 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
1c7ed27
chore: dotenv 설치 및 webpack 설정 추가
Hain-tain Jul 23, 2024
24689ee
refactor(public): index.html 에서 불필요한 script 제거
Hain-tain Jul 23, 2024
8fbebf6
feat(api): customFetch
Hain-tain Jul 23, 2024
dd5bf59
feat(api): endPoint 및 queryKey 상수화
Hain-tain Jul 23, 2024
a3cd5f8
feat(api): customFetch 활용하여 templates 관련 요청 함수 생성
Hain-tain Jul 23, 2024
e7f8244
refactor(src): 응답 json 객체 키 카멜케이스로 변경
Hain-tain Jul 23, 2024
e14b8f7
refactor(utils): 불필요한 convertToCamelCase 삭제
Hain-tain Jul 23, 2024
010d53d
refactor(hooks): queryKey 상수 적용 및 api 요청 함수 적용
Hain-tain Jul 23, 2024
3fedf90
test(hooks): useTemplateListQuery, useTemplateQuery, useTemplateUploa…
Hain-tain Jul 23, 2024
5130634
refactor(src): UploadsTemplate => TemplateUpload 로 변경
Hain-tain Jul 23, 2024
18e7485
feat(utils): formatRelativeTime
Hain-tain Jul 23, 2024
1fc5aa2
test(utils): formatRelativeTime
Hain-tain Jul 23, 2024
e1aa288
refactor(src): formatRelativeTime 적용 및 SyntaxHighlighter에 fontSize 추가
Hain-tain Jul 23, 2024
22a3e05
chore: msw setupWorker 설정
Hain-tain Jul 23, 2024
b1ec13d
Merge branch 'dev/fe' of https://github.com/woowacourse-teams/2024-co…
Hain-tain Jul 24, 2024
9bc3921
refactor(api): header에 'Content-Type': 'application/json' 추가
Hain-tain Jul 24, 2024
068f44f
test(utils): formatRelativeTime 테스트 명세 수정
Hain-tain Jul 24, 2024
bafe107
refactor(src): TEMPLATE_API_URL templates 파일에서 export 및 config 파일 제거
Hain-tain Jul 24, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions frontend/package-lock.json

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

1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"@types/react-syntax-highlighter": "^15.5.13",
"@typescript-eslint/eslint-plugin": "^7.2.0",
"@typescript-eslint/parser": "^7.2.0",
"dotenv": "^16.4.5",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"eslint-import-resolver-typescript": "^3.6.1",
Expand Down
3 changes: 1 addition & 2 deletions frontend/public/index.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
Expand All @@ -7,6 +7,5 @@
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/index.tsx"></script>
</body>
</html>
40 changes: 40 additions & 0 deletions frontend/src/api/customFetch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
interface Props {
url: string;
method?: 'GET' | 'POST' | 'PATCH' | 'DELETE';
headers?: RequestInit['headers'];
body?: RequestInit['body'];
errorMessage?: string;
}

export const customFetch = async ({
url,
headers,
method = 'GET',
body,
errorMessage = '[Error] response was not ok',
}: Props) => {
try {
const response = await fetch(url, {
method,
headers: {
'Content-Type': 'application/json',
...headers,
},
body,
});

if (!response.ok) {
throw new Error(errorMessage);
}

if (method !== 'GET') {
return response;
}

const data = await response.json();

return data;
vi-wolhwa marked this conversation as resolved.
Show resolved Hide resolved
} catch (error) {
throw new Error(errorMessage);
}
};
4 changes: 4 additions & 0 deletions frontend/src/api/queryKeys.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export const QUERY_KEY = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

키를 쓸 때 Symbol 데이터 타입을 고려해봐도 좋겠네요!

TEMPLATE: 'template',
TEMPLATE_LIST: 'templateList',
};
24 changes: 24 additions & 0 deletions frontend/src/api/templates.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Template, TemplateListResponse, TemplateUploadRequest } from '@/types/template';
import { customFetch } from './customFetch';

const BASE_URL = process.env.REACT_APP_API_URL;

export const TEMPLATE_API_URL = `${BASE_URL}/templates`;

export const getTemplateList = async (): Promise<TemplateListResponse> =>
await customFetch({
url: `${TEMPLATE_API_URL}`,
});

export const getTemplate = async (id: number): Promise<Template> =>
await customFetch({
url: `${TEMPLATE_API_URL}/${id}`,
});

export const postTemplate = async (newTemplate: TemplateUploadRequest): Promise<void> => {
await customFetch({
method: 'POST',
url: `${TEMPLATE_API_URL}`,
body: JSON.stringify(newTemplate),
});
};
16 changes: 9 additions & 7 deletions frontend/src/components/TemplateItem/TemplateItem.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
import { TemplateListItem } from '@/types/template';
import { formatRelativeTime } from '@/utils/formatRelativeTime';
import { Flex } from '../Flex';
import { Text } from '../Text';

Expand All @@ -9,28 +10,29 @@ interface Props {
}

const TemplateItem = ({ item }: Props) => {
const { title, member, modified_at, representative_snippet } = item;
const [year, month, day] = modified_at.split(' ')[0].split('-');
const { title, modifiedAt, thumbnailSnippet } = item;

return (
<Flex direction='column' gap='1.2rem' width='100%'>
<Flex direction='column' justify='flex-start' align='flex-start' width='100%' gap='0.8rem'>
<Text.SubTitle color='white'>{title}</Text.SubTitle>
<Text.Caption color='white'>{member.nickname}</Text.Caption>
</Flex>

<SyntaxHighlighter
language='javascript'
style={vscDarkPlus}
showLineNumbers={true}
customStyle={{ borderRadius: '10px', width: '100%', tabSize: 2 }}
codeTagProps={{
style: {
fontSize: '1.8rem',
},
}}
>
{representative_snippet.content_summary}
{thumbnailSnippet.contentSummary}
</SyntaxHighlighter>

<Text.Caption color='white'>
{year}년 {month}월 {day}일
</Text.Caption>
<Text.Caption color='white'>{formatRelativeTime(modifiedAt)}</Text.Caption>
</Flex>
);
};
Expand Down
5 changes: 3 additions & 2 deletions frontend/src/hooks/useTemplateListQuery.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@ const queryWrapper = ({ children }: PropsWithChildren) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);

describe('useTemplateListQuery test', () => {
it('GET 요청시 templates 데이터를 가져온다', async () => {
describe('useTemplateListQuery', () => {
it('templates 목록을 조회할 수 있다.', async () => {
const { result } = renderHook(() => useTemplateListQuery(), { wrapper: queryWrapper });

await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
expect(result.current.data?.templates[0].title).toBe('title1');
});
});
Expand Down
27 changes: 6 additions & 21 deletions frontend/src/hooks/useTemplateListQuery.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,10 @@
import { useQuery } from '@tanstack/react-query';
import { QUERY_KEY } from '@/api/queryKeys';
import { getTemplateList } from '@/api/templates';
import type { TemplateListResponse } from '@/types/template';

const fetchTemplateList = async (): Promise<TemplateListResponse> => {
// change this url after MSW initial setting
// const apiUrl = process.env.REACT_APP_API_URL;
const response = await fetch('http://localhost:8080/templates', {
method: 'GET',
export const useTemplateListQuery = () =>
vi-wolhwa marked this conversation as resolved.
Show resolved Hide resolved
useQuery<TemplateListResponse, Error>({
queryKey: [QUERY_KEY.TEMPLATE_LIST],
queryFn: getTemplateList,
});

if (!response.ok) {
throw new Error('Network response was not ok');
}

return response.json();
};

export const useTemplateListQuery = () => {
const { data, error, isLoading } = useQuery<TemplateListResponse, Error>({
queryKey: ['templateList'],
queryFn: fetchTemplateList,
});

return { data, error, isLoading };
};
21 changes: 21 additions & 0 deletions frontend/src/hooks/useTemplateQuery.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { renderHook, waitFor } from '@testing-library/react';
import { useTemplateQuery } from './useTemplateQuery';

const queryClient = new QueryClient();

const queryWrapper = ({ children }: { children: React.ReactNode }) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);

describe('useTemplateQuery', () => {
it('한 개의 template을 조회할 수 있다.', async () => {
const { result } = renderHook(() => useTemplateQuery(2024), { wrapper: queryWrapper });

await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
expect(result.current.data?.id).toBe(2024);
expect(result.current.data?.title).toBe('회원가입 유효성 검증');
});
});
});
19 changes: 5 additions & 14 deletions frontend/src/hooks/useTemplateQuery.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,10 @@
import { UseQueryResult, useQuery } from '@tanstack/react-query';
import { QUERY_KEY } from '@/api/queryKeys';
import { getTemplate } from '@/api/templates';
import { Template } from '@/types/template';

const fetchTemplate = async (id: string): Promise<Template> => {
const apiUrl = process.env.REACT_APP_API_URL;
const response = await fetch(`${apiUrl}/templates/${id}`);

if (!response.ok) {
throw new Error('Network response was not ok');
}

return response.json();
};

export const useTemplateQuery = (id: string): UseQueryResult<Template, Error> =>
export const useTemplateQuery = (id: number): UseQueryResult<Template, Error> =>
useQuery<Template, Error>({
queryKey: ['template', id],
queryFn: () => fetchTemplate(id),
queryKey: [QUERY_KEY.TEMPLATE, id],
queryFn: () => getTemplate(id),
});
32 changes: 32 additions & 0 deletions frontend/src/hooks/useTemplateUploadQuery.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { renderHook, waitFor } from '@testing-library/react';
import { useTemplateUploadQuery } from './useTemplateUploadQuery';

const queryClient = new QueryClient();

const queryWrapper = ({ children }: { children: React.ReactNode }) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);

describe('useTemplateUploadQuery', () => {
it('template을 생성할 수 있다.', async () => {
const { result } = renderHook(() => useTemplateUploadQuery(), { wrapper: queryWrapper });

const body = {
title: 'Upload Test',
snippets: [
{
filename: 'filename1.txt',
content: 'content1',
ordinal: 1,
},
],
};

await result.current.mutateAsync(body);

await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});
});
Comment on lines +28 to +31
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

추후에 isSuccess가 true인지 확인 + 실제 데이터가 들어갔는지 확인해도 좋을 것 같아요.

});
34 changes: 7 additions & 27 deletions frontend/src/hooks/useTemplateUploadQuery.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,14 @@
import { UseMutationResult, useMutation, useQueryClient } from '@tanstack/react-query';
import { CreateTemplateRequest, Template } from '@/types/template';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { QUERY_KEY } from '@/api/queryKeys';
import { postTemplate } from '@/api/templates';

const createTemplate = async (newTemplate: CreateTemplateRequest): Promise<Template> => {
const apiUrl = process.env.REACT_APP_API_URL;
const response = await fetch(`${apiUrl}/templates`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(newTemplate),
});

if (!response.ok) {
throw new Error('Failed to create template');
}

if (response.status === 201) {
return newTemplate as Template;
}

return response.json();
};

export const useTemplateUploadQuery = (): UseMutationResult<Template, Error, CreateTemplateRequest> => {
export const useTemplateUploadQuery = () => {
const queryClient = useQueryClient();

return useMutation<Template, Error, CreateTemplateRequest>({
mutationFn: createTemplate,
return useMutation({
mutationFn: postTemplate,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['templateList'] });
queryClient.invalidateQueries({ queryKey: [QUERY_KEY.TEMPLATE_LIST] });
},
});
};
28 changes: 20 additions & 8 deletions frontend/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,23 @@ const queryClient = new QueryClient();

const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);

root.render(
<React.StrictMode>
<QueryClientProvider client={queryClient}>
<GlobalStyles />
<RouterProvider router={router} />
</QueryClientProvider>
</React.StrictMode>,
);
const enableMocking = async () => {
if (process.env.NODE_ENV !== 'development') {
return;
}

const { worker } = await import('./mocks/browser');

await worker.start();
};

enableMocking().then(() => {
root.render(
<React.StrictMode>
<QueryClientProvider client={queryClient}>
<GlobalStyles />
<RouterProvider router={router} />
</QueryClientProvider>
</React.StrictMode>,
);
});
4 changes: 4 additions & 0 deletions frontend/src/mocks/browser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { setupWorker } from 'msw/browser';
import { handlers } from './handlers';

export const worker = setupWorker(...handlers);
19 changes: 12 additions & 7 deletions frontend/src/mocks/handlers.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
import { http, HttpResponse } from 'msw';
import mockTemplates from './templateList.json';

// change this url after MSW initial setting
// const apiUrl = process.env.REACT_APP_API_URL || 'http://localhost:8080';
import { HttpResponse, http } from 'msw';
import { TEMPLATE_API_URL } from '@/api/templates';
import mockTemplate from './template.json';
import mockTemplateList from './templateList.json';

export const handlers = [
http.get('http://localhost:8080/templates', () => {
const response = HttpResponse.json(mockTemplates);
http.get(`${TEMPLATE_API_URL}`, () => {
const response = HttpResponse.json(mockTemplateList);

return response;
}),
http.get(`${TEMPLATE_API_URL}/:id`, () => {
const response = HttpResponse.json(mockTemplate);

return response;
}),
http.post(`${TEMPLATE_API_URL}`, async () => HttpResponse.json({ status: 201 })),
];
Loading