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_안희정5주차 과제 Step2-3 #65

Open
wants to merge 12 commits into
base: anheejeong
Choose a base branch
from
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module.exports = {
browser: true,
es2021: true,
node: true,
jest: true,
},
extends: [
'plugin:@typescript-eslint/recommended',
Expand Down
43 changes: 43 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,44 @@
# 카카오 테크 캠퍼스 - 프론트엔드 카카오 선물하기 편

### 🚀 1단계

- [ ] Jest와 React Testing Libaray를 사용하여 테스트 기반 환경을 구축해요.
- [ ] MSW를 사용하여 Mock API가 동작하도록 해요. (상세 API / 옵션 API)
- [ ] 단위 테스트로 작성하면 좋을 테스트가 있다면 단위테스트 코드를 작성해요.
- [ ] 상품 상세 페이지와 관련된 통합 테스트 코드를 작성해요.
- [ ] 결제하기 페이지의 Form과 관련된 통합 테스트 코드를 작성해요.
- [ ] 현금영수증 Checkbox가 false인 경우 현금영수증 종류, 현금영수증 번호 field가 비활성화 되어있는지 확인하는 테스트 코드를 작성해요. (만약 - [ ] true인 경우 현금영수증 종류, 번호 field에 값이 입력 되어야 해요)
- [ ] form의 validation 로직이 정상 동작하는지 확인하는 테스트 코드를 작성해요.

### 🚀 2단계

- [ ] 로그인 기능을 구현해요.
- [ ] 회원가입 화면을 만들고, 회원가입 기능이 동작되게 구현해요. (회원가입을 하면 로그인이 되게 해요.)
- [ ] 회원가입 버튼은 로그인 화면 하단에 배치해요. 로그인 화면을 그대로 사용해도 괜찮아요.
- [ ] 상품 상세 페이지에서 관심 등록 버튼을 만들어요.
- [ ] 상품 상세 페이지에서 관심 버튼을 클릭 했을 때 관심 추가 동작되게 해요.
- [ ] 관심 등록 성공 시 Alert로 "관심 등록 완료" 메시지를 노출해요.
- [ ] 나의 계정 페이지에서 관심 목록 리스트를 만들어요.
- [ ] 관심 목록 리스트는 chakra UI를 사용하여 자유롭게 만들어주세요.
- [ ] 관심 목록 API는 카카오테크 선물하기 API 노션의 response 데이터를 사용해요.
- [ ] 관심 목록 리스트에서 관심 삭제가 가능하게 해요.
- [ ] 관심 삭제 시 목록에서 사라져요.

### 🚀 3단계

> 1. Test code를 작성해보면서 좋았던 점과 아쉬웠던 점에 대해 말해주세요.

조건에 따라 코드가 정상작동하는지 하나씩 확인할 수 있었던 것이 좋았습니다. 로그인 유무에 따라, 현금영수증 선택에 따라, 번호 입력시 제대로 되어있는지에 따라 등 직접 해보기에는 번거로운 조건들을 코드로 다 확인할 수 있어 좋았습니다.

그러나 화면 렌더링과 같이 실행했을때 바로바로 확인이 가능한 것들의 테스트 코드는 크게 의미가 있어 보이지는 않았습니다. 제대로 실행이 되는지 바로 확인이 가능한 것들은 테스트 코드를 작성하는 것이 불필요해보였고, 이 점이 조금 아쉬웠습니다.


> 2. 스스로 생각했을 때 좋은 컴포넌트란 무엇인지 본인만의 기준을 세우고 설명해 주세요.

코드의 가독성과 재사용성이 용이한 컴포넌트가 좋은 컴포넌트라고 생각했습니다. 컴포넌트를 사용하면서 재사용하게 되는 경우가 굉장히 많았고, 다시 사용하게 될 때마다 코드를 다시 읽어야 했습니다. 코드의 가독성이 좋지 않을 때는 이 컴포넌트를 다시 사용하는 경우 많은 시간과 어려움이 들었습니다. 따라서 컴포넌트를 생성하게 될 때는 코드의 가독성과 재사용성에 주의하며 구현해야 한다고 생각합니다.


> 3. 스스로 생각했을 때 공통 컴포넌트를 만들 때 가장 중요한 요소 2개를 선택하고 이유와 함께 설명해주세요.

공통 컴포넌트를 만들 때는 위에서의 이유와 같은 이유로 '재사용성'이 가장 중요하다고 생각하고, 또 다른 하나는 단일 책임 원칙이 중요하다고 생각합니다. 공통 컴포넌트를 만드는 궁극적인 이유는 동일하게 사용되는 기능을 여러 곳에서 동일하게 사용하기 위해 만드는 것이므로, 하나의 기능을 정확하게 수행하게 만드는 것이 중요합니다.

7 changes: 7 additions & 0 deletions craco.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,11 @@ module.exports = {
'@': path.resolve(__dirname, 'src'),
},
},
jest: {
configure: {
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1',
},
},
},
};
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"start:mock": "cross-env REACT_APP_RUN_MSW=true npm run start",
"start": "craco start",
"build": "craco build",
"test": "craco test",
"test": "craco test --transformIgnorePatterns \"node_modules/(?!axios)/\"",
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build"
},
Expand Down Expand Up @@ -72,16 +72,18 @@
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-simple-import-sort": "^12.0.0",
"eslint-plugin-storybook": "^0.8.0",
"jest": "^29.7.0",
"msw": "^1.3.3",
"prettier": "^3.2.5",
"prop-types": "^15.8.1",
"react-scripts": "5.0.1",
"storybook": "^7.6.17",
"ts-jest": "^29.2.3",
"tsconfig-paths-webpack-plugin": "^4.1.0",
"typescript": "^4.9.5",
"webpack": "^5.90.3"
},
"overrides": {
"react-refresh": "0.11.0"
}
}
}
29 changes: 29 additions & 0 deletions src/api/hooks/createAccount.mock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { rest } from 'msw';

const BASE_URL = 'http://localhost:3000';

interface RegisterRequestBody {
email: string;
password: string;
}

interface RegisterSuccessResponse {
email: string;
token: string;
}

export const createAccountMockHandler = [
rest.post<RegisterRequestBody>(`${BASE_URL}/api/members/register`, (req, res, ctx) => {
const { email, password } = req.body;

if (!email || !password) {
return res(ctx.status(400), ctx.json({ message: 'Invalid input' }));
}

const response: RegisterSuccessResponse = {
email,
token: 'mocked-registration-token',
};
return res(ctx.status(201), ctx.json(response));
}),
];
29 changes: 29 additions & 0 deletions src/api/hooks/login.mock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { rest } from 'msw';

const BASE_URL = 'http://localhost:3000';

interface RegisterRequestBody {
email: string;
password: string;
}

interface RegisterSuccessResponse {
email: string;
token: string;
}

export const loginMockHandler = [
rest.post<RegisterRequestBody>(`${BASE_URL}/api/members/login`, (req, res, ctx) => {
const { email, password } = req.body;

if (!email || !password) {
return res(ctx.status(400), ctx.json({ message: 'Invalid input' }));
}

const response: RegisterSuccessResponse = {
email,
token: 'mocked-registration-token',
};
return res(ctx.status(201), ctx.json(response));
}),
];
37 changes: 37 additions & 0 deletions src/api/hooks/useCreateAccount.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import axios from 'axios';
import { useState } from 'react';

type RegisterRequestBody = {
email: string;
password: string;
};

type RegisterResponseBody = {
email: string;
token: string;
};

export const useCreateAccount = () => {
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);

const createAccount = async (data: RegisterRequestBody): Promise<RegisterResponseBody | null> => {
setLoading(true);
setError(null);
try {
const response = await axios.post<RegisterResponseBody>('/api/members/register', data);
setLoading(false);
return response.data;
} catch (err) {
setLoading(false);
if (axios.isAxiosError(err) && err.response) {
setError(err.response.data.message || 'Registration failed');
} else {
setError('An unexpected error occurred');
}
return null;
}
};

return { createAccount, loading, error };
};
37 changes: 37 additions & 0 deletions src/api/hooks/useGetLogin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import axios from 'axios';
import { useState } from 'react';

type RegisterRequestBody = {
email: string;
password: string;
};

type RegisterResponseBody = {
email: string;
token: string;
};

export const useLogin = () => {
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);

const login = async (data: RegisterRequestBody): Promise<RegisterResponseBody | null> => {
setLoading(true);
setError(null);
try {
const response = await axios.post<RegisterResponseBody>('/api/members/login', data);
setLoading(false);
return response.data;
} catch (err) {
setLoading(false);
if (axios.isAxiosError(err) && err.response) {
setError(err.response.data.message || 'login failed');
} else {
setError('An unexpected error occurred');
}
return null;
}
};

return { login, loading, error };
};
143 changes: 143 additions & 0 deletions src/api/hooks/useGetWishList.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import axios from 'axios';
import { useState } from 'react';

type AddToWishlistRequestBody = {
productId: number;
};

type AddToWishlistResponseBody = {
id: number;
productId: number;
};

type Product = {
id: number;
name: string;
price: number;
imageUrl: string;
};

type Wish = {
id: number;
product: Product;
};

type PaginationResponse<T> = {
content: T[];
pageable: {
sort: {
sorted: boolean;
unsorted: boolean;
empty: boolean;
};
pageNumber: number;
pageSize: number;
offset: number;
unpaged: boolean;
paged: boolean;
};
totalPages: number;
totalElements: number;
last: boolean;
number: number;
size: number;
numberOfElements: number;
first: boolean;
empty: boolean;
};

export const useAddToWishlist = () => {
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);

const addToWishlist = async (data: AddToWishlistRequestBody, token: string): Promise<AddToWishlistResponseBody | null> => {
setLoading(true);
setError(null);
try {
const response = await axios.post<AddToWishlistResponseBody>(
'/api/wishes',
data,
{
headers: {
Authorization: `Bearer ${token}`,
},
}
);
setLoading(false);
return response.data;
} catch (err) {
setLoading(false);
if (axios.isAxiosError(err) && err.response) {
setError(err.response.data.message || 'Failed to add to wishlist');
} else {
setError('An unexpected error occurred');
}
return null;
}
};

return { addToWishlist, loading, error };
};

export const useRemoveFromWishlist = () => {
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);

const removeFromWishlist = async (wishId: number, token: string): Promise<boolean> => {
setLoading(true);
setError(null);
try {
await axios.delete(`/api/wishes/${wishId}`, {
headers: {
Authorization: `Bearer ${token}`,
},
});
setLoading(false);
return true;
} catch (err) {
setLoading(false);
if (axios.isAxiosError(err) && err.response) {
setError(err.response.data.message || 'Failed to remove from wishlist');
} else {
setError('An unexpected error occurred');
}
return false;
}
};

return { removeFromWishlist, loading, error };
};

export const useFetchWishlist = () => {
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [wishlist, setWishlist] = useState<PaginationResponse<Wish> | null>(null);

const fetchWishlist = async (token: string, page: number, size: number, sort: string): Promise<void> => {
setLoading(true);
setError(null);
try {
const response = await axios.get<PaginationResponse<Wish>>('/api/wishes', {
headers: {
Authorization: `Bearer ${token}`,
},
params: {
page,
size,
sort,
},
});
setWishlist(response.data);
setLoading(false);
} catch (err) {
setLoading(false);
if (axios.isAxiosError(err) && err.response) {
setError(err.response.data.message || 'Failed to fetch wishlist');
} else {
setError('An unexpected error occurred');
}
}
};

return { fetchWishlist, loading, error, wishlist };
};
Loading