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

경북대 FE_이정민 6주차 과제 Step4 #106

Open
wants to merge 24 commits into
base: userjmmm
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
a3f7313
chore: 로그인 및 관심목록 코드 준비
Jul 31, 2024
966fa17
refactor: useGetCategories 파일명 오타 수정
Jul 31, 2024
a2e0617
refactor: CategoryResponseData 타입을 Pageable에 맞추어 수정
Jul 31, 2024
0e7e5bb
refactor: 옵션 선택 시 총 결제 금액이 계산되도록 수정
Jul 31, 2024
6ec10aa
refactor: 옵션 핸들링 로직 수정
Jul 31, 2024
183d422
feat: 상단 네비게이션 바에서 백엔드 API 선택하는 기능 추가
Jul 31, 2024
fd3109d
refactor: baseUrl이 변경될 때 실시간으로 반영하도록 수정
Aug 1, 2024
f98715a
feat: 모든 카테고리를 받아올 수 있도록 하는 로직 추가
Aug 1, 2024
300f340
refactor: useGetProducts.ts에서 getProductsPath의 쿼리 매개변수 순서 변경
Aug 1, 2024
6895a87
feat: 서버에서 받아온 token을 저장해서 로그인 상태 유지되는 로직 추가
Aug 1, 2024
c8f8353
feat: 위시 조회시 Page<WishList>로 return 받을 수 있도록 Data 타입 추가
Aug 1, 2024
048cc4d
refactor: localStorage로 로그아웃 기능 가능하도록 수정
Aug 1, 2024
71dab77
feat: 변경된 API에 맞춰 위시 삭제 기능 추가
Aug 2, 2024
bd3b2a7
feat: 토큰 디코딩해서 사용자 이름 받아오는 기능 추가
Aug 2, 2024
e03b327
feat: 주문 페이지에서 상품 옵션까지 받아오는 기능 추가
Aug 2, 2024
33e7769
chore(ui): 카카오로 로그인 하는 로그인 페이지 UI 수정
Aug 2, 2024
c10a150
refactor: 옵션마다 post 요청 각각 진행하도록 수정
Aug 2, 2024
9edd99b
refactor: 재용님 BASE_URL 변경
Aug 2, 2024
a876457
refactor(ui): 주문하기 페이지에서 선택한 옵션을 모두 확인할 수 있게 UI 변경
Aug 2, 2024
4a41411
feat: 포인트 기능 - 포인트로 결제 시 10% 할인되는 기능 추가
Aug 2, 2024
b16ff7b
refactor: 결제하기 누를 때, API 요청을 직렬로 보내 포인트가 차감되도록 로직 수정
Aug 2, 2024
bba4c08
feat: 모바일에서도 옵션이 잘 뜨도록 반응형 코드 추가
Aug 4, 2024
de019d1
docs: 4단계 답변 READMD.md에 작성
Aug 4, 2024
6bedd4b
feat: 보유 포인트 조회 기능 추가
Aug 5, 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
41 changes: 40 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,40 @@
# react-deploy
# react-deploy
## 4단계 - 질문의 답변을 README에 작성

### 6주차 질문
- 질문 1. SPA 페이지를 정적 배포를 하려고 할 때 Vercel을 사용하지 않고 한다면 어떻게 할 수 있을까요?
- Vercel로 정적 배포를 하는 경우엔 웹 서버의 기능을 추상화하여 제공해서 서버리스 함수와 같은 기능을 통해 사용자가 직접 웹 서버를 설정할 필요가 없습니다.
- 하지만 저는 네이버 클라우드 서버를 이용해서 정적 배포를 진행했습니다. 이 서비스는 가상 머신과 유사하게 작동하기 때문에 웹 서버 설치, 프론트엔드 코드 배포, 웹 서버 설정 등의 추가적인 작업이 필요했습니다.
- 해당 배포 과정에 대한 자세한 설명은 [여기](https://garbagetime.notion.site/0000b1da7eb34bba924d109b68ed972b)에서 확인하실 수 있습니다!

- 질문 2. CSRF나 XSS 공격을 막는 방법은 무엇일까요?
- CSRF를 막기 위해선 서버에서 고유한 토큰을 생성해서 요청 시 서버에서 토큰의 유효성을 검증하는 로직을 추가하는 방법이 있을 것 같습니다.
- XSS 공격을 막기 위해선 사용자 입력을 받을 때 유효성을 검사하고 특수 문자를 escaping 처리하거나, textContent를 사용하여 텍스트 삽입 시 XSS를 방지할 수 있다고 생각합니다.

- 질문 3. 브라우저 렌더링 원리에대해 설명해주세요.
- 브라우저 렌더링은 다음과 같은 4가지 과정을 거쳐서 진행됩니다.

1. DOM Tree와 CSSOM Tree의 생성

<HTML을 DOM Tree로 만드는 과정>

: 토큰화 → 브라우저의 렉싱 과정(토큰이 해당 속성과 규칙을 정의하는 노드 객체로 변환됨) → DOM 트리 생성(각 노드가 서로 연관성을 가질 수 있도록)

<CSS를 CSSOM Tree로 만드는 과정>

- DOM이 화면에 어떻게 표시될지 알려주는 역할을 함
- CSS도 위→아래로 스타일 규칙이 정해짐 : tree 구조 가짐
2. Render Tree 생성

: 화면에 표시되어야 할 모든 노드의 컨텐츠, 스타일 정보를 포함하는 트리

- document 객체부터 각 노드 순회하며 그에 맞는 CSSOM 찾아 규칙 적용
- meta 태그, `display: none;` 속성 - 렌더와 관련 없어 포함X
3. Layout (= Reflow)

: 뷰포트 내에서 요소들의 정확한 위치와 크기를 계산하는 과정

- css에서 상대적 단위(%, em 등) 사용 시, 뷰포트에 맞춰 픽셀 단위로 변환됨

4. 페인트 과정
: 화면에 실제 픽셀로 그려지도록 변환하는 과정
15 changes: 15 additions & 0 deletions package-lock.json

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

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"@emotion/styled": "^11.11.0",
"@tanstack/react-query": "^5.24.1",
"axios": "^1.6.7",
"base-64": "^1.0.0",
"framer-motion": "^11.0.6",
"glob": "^11.0.0",
"react": "^18.2.0",
Expand All @@ -54,6 +55,7 @@
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"@types/base-64": "^1.0.2",
"@types/jest": "^27.5.2",
"@types/node": "^16.18.82",
"@types/react": "^18.2.57",
Expand Down Expand Up @@ -87,4 +89,4 @@
"overrides": {
"react-refresh": "0.11.0"
}
}
}
33 changes: 33 additions & 0 deletions public/token.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>OAuth Callback</title>
<style>
body {
font-family: Arial, sans-serif;
text-align: center;
margin-top: 50px;
}
</style>
</head>
<body>
<h1>OAuth Callback</h1>
<p id="tokenDisplay">토큰을 기다리고 있습니다...</p>
<script>
const params = new URLSearchParams(window.location.search);
const token = params.get('tokenValue');

if (token) {
// 토큰이 존재하면 로컬 스토리지에 저장
console.log('Received token:', token);
localStorage.setItem('authToken', token);
window.location.href = '/';
} else {
console.error('Token not found in query parameters.');
tokenDisplay.textContent = 'Token not found in query parameters.';
}
</script>
</body>
</html>
59 changes: 43 additions & 16 deletions src/api/hooks/categories.mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,48 @@ export const categoriesMockHandler = [
}),
];

const CATEGORIES_RESPONSE_DATA = [
{
id: 2920,
name: '생일',
description: '감동을 높여줄 생일 선물 리스트',
color: '#5949a3',
imageUrl:
'https://img1.daumcdn.net/thumb/S104x104/?fname=https%3A%2F%2Ft1.daumcdn.net%2Fgift%2Fhome%2Ftheme%2F292020231106_MXMUB.png',
const CATEGORIES_RESPONSE_DATA = {
totalElements: 2,
totalPages: 1,
first: true,
last: true,
size: 10,
content: [
{
id: 2920,
name: '생일',
description: '감동을 높여줄 생일 선물 리스트',
color: '#5949a3',
imageUrl:
'https://img1.daumcdn.net/thumb/S104x104/?fname=https%3A%2F%2Ft1.daumcdn.net%2Fgift%2Fhome%2Ftheme%2F292020231106_MXMUB.png',
},
{
id: 2930,
name: '교환권',
description: '놓치면 후회할 교환권 특가',
color: '#9290C3',
imageUrl:
'https://img1.daumcdn.net/thumb/S104x104/?fname=https%3A%2F%2Fst.kakaocdn.net%2Fproduct%2Fgift%2Fproduct%2F20240131153049_5a22b137a8d346e9beb020a7a7f4254a.jpg',
},
],
number: 0,
sort: {
empty: true,
sorted: false,
unsorted: true,
},
{
id: 2930,
name: '교환권',
description: '놓치면 후회할 교환권 특가',
color: '#9290C3',
imageUrl:
'https://img1.daumcdn.net/thumb/S104x104/?fname=https%3A%2F%2Fst.kakaocdn.net%2Fproduct%2Fgift%2Fproduct%2F20240131153049_5a22b137a8d346e9beb020a7a7f4254a.jpg',
numberOfElements: 2,
pageable: {
pageNumber: 0,
pageSize: 10,
sort: {
empty: true,
sorted: false,
unsorted: true,
},
offset: 0,
paged: true,
unpaged: false,
},
];
empty: false,
};
54 changes: 46 additions & 8 deletions src/api/hooks/useGetCategories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,58 @@ import { useQuery } from '@tanstack/react-query';

import type { CategoryData } from '@/types';

import { BASE_URL, fetchInstance } from '../instance';
import { getBaseUrl, fetchInstance } from '../instance';

export type CategoryResponseData = CategoryData[];
export type CategoryResponseData = {
totalElements: number;
totalPages: number;
first: boolean;
last: boolean;
size: number;
content: CategoryData[];
number: number;
sort: {
empty: boolean;
sorted: boolean;
unsorted: boolean;
};
numberOfElements: number;
pageable: {
pageNumber: number;
pageSize: number;
sort: {
empty: boolean;
sorted: boolean;
unsorted: boolean;
};
offset: number;
paged: boolean;
unpaged: boolean;
};
empty: boolean;
};

export const getCategoriesPath = () => `${getBaseUrl()}/api/categories`;

export const getCategories = async (): Promise<CategoryData[]> => {
let allCategories: CategoryData[] = [];
let currentPage = 0;
let hasNextPage = true;

export const getCategoriesPath = () => `${BASE_URL}/api/categories`;
const categoriesQueryKey = [getCategoriesPath()];
while (hasNextPage) {
const response = await fetchInstance.get<CategoryResponseData>(
`${getCategoriesPath()}?page=${currentPage}&size=100`
);
allCategories = [...allCategories, ...response.data.content];
hasNextPage = !response.data.last;
currentPage++;
}

export const getCategories = async () => {
const response = await fetchInstance.get<CategoryResponseData>(getCategoriesPath());
return response.data;
return allCategories;
};

export const useGetCategories = () =>
useQuery({
queryKey: categoriesQueryKey,
queryKey: ['categories'],
queryFn: getCategories,
});
32 changes: 32 additions & 0 deletions src/api/hooks/useGetMemberPoints.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { useQuery } from '@tanstack/react-query';
import { getBaseUrl } from '@/api/instance';
import { useAuth } from '@/provider/Auth';

interface PointsResponse {
point: number;
}

const fetchMemberPoints = async (token: string): Promise<number> => {
const response = await fetch(`${getBaseUrl()}/api/points`, {
headers: {
Authorization: `Bearer ${token}`,
},
});

if (!response.ok) {
throw new Error('Failed to fetch member points');
}

const data: PointsResponse = await response.json();
return data.point;
};

export const useGetMemberPoints = () => {
const authInfo = useAuth();

return useQuery({
queryKey: ['memberPoints'],
queryFn: () => fetchMemberPoints(authInfo?.token ?? ''),
enabled: !!authInfo?.token,
});
};
4 changes: 2 additions & 2 deletions src/api/hooks/useGetProductDetail.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { useSuspenseQuery } from '@tanstack/react-query';

import type { ProductData } from '@/types';

import { BASE_URL, fetchInstance } from '../instance';
import { getBaseUrl, fetchInstance } from '../instance';

export type ProductDetailRequestParams = {
productId: string;
Expand All @@ -12,7 +12,7 @@ type Props = ProductDetailRequestParams;

export type GoodsDetailResponseData = ProductData;

export const getProductDetailPath = (productId: string) => `${BASE_URL}/api/products/${productId}`;
export const getProductDetailPath = (productId: string) => `${getBaseUrl()}/api/products/${productId}`;

export const getProductDetail = async (params: ProductDetailRequestParams) => {
const response = await fetchInstance.get<GoodsDetailResponseData>(
Expand Down
4 changes: 2 additions & 2 deletions src/api/hooks/useGetProductOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@ import { useSuspenseQuery } from '@tanstack/react-query';

import type { ProductOptionsData } from '@/types';

import { BASE_URL, fetchInstance } from '../instance';
import {getBaseUrl, fetchInstance } from '../instance';
import type { ProductDetailRequestParams } from './useGetProductDetail';

type Props = ProductDetailRequestParams;

export type ProductOptionsResponseData = ProductOptionsData[];

export const getProductOptionsPath = (productId: string) =>
`${BASE_URL}/api/products/${productId}/options`;
`${getBaseUrl()}/api/products/${productId}/options`;

export const getProductOptions = async (params: ProductDetailRequestParams) => {
const response = await fetchInstance.get<ProductOptionsResponseData>(
Expand Down
15 changes: 9 additions & 6 deletions src/api/hooks/useGetProducts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {

import type { ProductData } from '@/types';

import { BASE_URL } from '../instance';
import { getBaseUrl } from '../instance';
import { fetchInstance } from './../instance/index';

type RequestParams = {
Expand Down Expand Up @@ -35,12 +35,14 @@ type ProductsResponseRawData = {
export const getProductsPath = ({ categoryId, pageToken, maxResults }: RequestParams) => {
const params = new URLSearchParams();

params.append('categoryId', categoryId);
if (pageToken)
params.append('page', pageToken);
if (maxResults)
params.append('size', maxResults.toString());
params.append('sort', 'name,asc');
if (pageToken) params.append('page', pageToken);
if (maxResults) params.append('size', maxResults.toString());
params.append('categoryId', categoryId);

return `${BASE_URL}/api/products?${params.toString()}`;
return `${getBaseUrl()}/api/products?${params.toString()}`;
};

export const getProducts = async (params: RequestParams): Promise<ProductsResponseData> => {
Expand All @@ -58,6 +60,7 @@ export const getProducts = async (params: RequestParams): Promise<ProductsRespon
};

type Params = Pick<RequestParams, 'maxResults' | 'categoryId'> & { initPageToken?: string };

export const useGetProducts = ({
categoryId,
maxResults = 20,
Expand All @@ -71,4 +74,4 @@ export const useGetProducts = ({
initialPageParam: initPageToken,
getNextPageParam: (lastPage) => lastPage.nextPageToken,
});
};
};
Loading