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-login #39

Open
wants to merge 45 commits into
base: sugoring
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
7142c9b
docs: 전체 기능요구사항 정리
Jul 22, 2024
4f5ac5d
docs: step0 기능요구사항 구현
Jul 22, 2024
98f0be9
docs: step1 구현 요구사항 정리
Jul 25, 2024
e13b0c0
feat: MSW를 사용한 상세 및 옵션 API 모킹 핸들러 추가
Jul 25, 2024
c1bc343
feat: 제품 옵션 API 모킹 핸들러 추가
Jul 25, 2024
799ed8a
feat: 제품 상세 정보 API 모킹 핸들러 추가
Jul 25, 2024
cb54939
chore: ESLint 룰 적용 및 설정 추가
Jul 25, 2024
a75ce66
refactor: useGetCategorys 대신 categories.mock.ts를 사용하여 카테고리 데이터 모킹
Jul 25, 2024
8d4c1e4
refactor: useGetProducts에서 pageParam 사용 제거
Jul 25, 2024
0c390c8
fix: 제품 상세 정보 API 호출에서 핸들러 경로와 요청 경로 일치 문제 해결
Jul 25, 2024
394ca8e
feat: 상품 상세 정보 및 옵션 조회 API 모킹 데이터 개선
Jul 25, 2024
8468844
fix: 상품 상세 헤더 컴포넌트 null 체크 추가
Jul 25, 2024
68c6c14
fix: handle undefined price and add error handling in OptionSection c…
Jul 25, 2024
f8c5bdf
fix: handle null detail and remove unused error variable in GoodsInfo…
Jul 25, 2024
ea21d7a
fix: handle 'detail' possibly being null
Jul 25, 2024
b73df39
feat: Implement product options data fetching and mocking
Jul 25, 2024
d879506
ix: 상품 옵션 계산 로직 수정 및 타입 오류 해결
Jul 25, 2024
2b1263b
feat: 제품 상세 정보 조회 기능 구현 및 MSW 핸들러 추가
Jul 25, 2024
40e33f7
feat: 제품 상세 정보 API 모킹 및 useSuspenseQuery 훅 추가
Jul 25, 2024
d4845d6
docs: step1 msw에 대한 브랜치 전환
Jul 25, 2024
c71e5d9
Merge pull request #4 from sugoring/step0
sugoring Jul 25, 2024
2ac29c5
test: useGetProducts 테스트 추가
Jul 25, 2024
a02b848
test: getProductDetail 함수에 대한 단위 테스트 추가
Jul 25, 2024
b5c2014
feat: 현금영수증 입력 컴포넌트(CashReceiptFields) 단위 테스트 추가
Jul 25, 2024
3960572
feat(components/order): 메시지 카드 입력 컴포넌트(MessageCardFields) 단위 테스트 추가
Jul 25, 2024
b278fd5
docs: 단위 테스트 구현 목록
Jul 25, 2024
33d8b9a
test: useGetCategories 훅에 대한 통합 테스트 추가
Jul 25, 2024
4842d82
test: useGetProducts 훅에 대한 통합 테스트 추가
Jul 25, 2024
91ed3a4
test: useGetProductDetail 훅 통합 테스트 구현
Jul 25, 2024
8c30ec4
test: useGetProductOptions 훅 통합 테스트 구현
Jul 25, 2024
c288458
docs: mock 통합 테스트 구현
Jul 25, 2024
1ae0bd3
feat: 현금영수증 입력 컴포넌트 통합 테스트 추가
Jul 25, 2024
9826bdb
test: 메시지 카드 입력 컴포넌트 통합 테스트 추가
Jul 25, 2024
ec2703b
docs: step2 구현 목록 정리
Jul 25, 2024
da347d0
fix: Remove req
Jul 26, 2024
18366d0
feat: 로그인 기능 추가
Jul 26, 2024
689ef92
feat(login): 회원가입 버튼 UI 구현
Jul 26, 2024
526e094
feat(router): 회원가입 페이지 라우팅 설정 및 컴포넌트 추가
Jul 26, 2024
22925be
feat(signup): 회원가입 UI 구현
Jul 26, 2024
90cbb38
feat(api): 회원가입 요청을 처리하는 useRegister 훅 생성
Jul 26, 2024
786b62d
fest: 타입스크립트 환경에서 MSW 초기화 코드 리팩터링
Jul 26, 2024
6764475
feat(auth): implement login functionality and always return success r…
Jul 26, 2024
eec996c
feat(mock): 상품 옵션 API 모의 응답 개선
Jul 26, 2024
e11b867
fix: lint 룰 적용
Jul 26, 2024
7fd36c6
Merge branch 'sugoring' into step2-login
sugoring Jul 29, 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
54 changes: 16 additions & 38 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@

### MSW를 사용하여 Mock API 설정

- [ ] 상세 API 엔드포인트 추가
- [ ] 옵션 API 엔드포인트 추가
- [x] 상세 API 엔드포인트 추가
- [x] 옵션 API 엔드포인트 추가

### 단위 테스트 작성

Expand All @@ -49,45 +49,23 @@
- [x] 필수 입력 필드 검사
- [x] 입력 값 형식 검사

- [x] 상세 API 엔드포인트 추가
- [x] 옵션 API 엔드포인트 추가

### 단위 테스트 작성

- [ ] 컴포넌트 별 단위 테스트 작성
- [ ] 훅(hooks) 단위 테스트 작성

### 통합 테스트 작성
## 2단계 - 로그인, 관심 상품 등록 / 삭제, 관심 목록 구현

- 계정 관리
- [x] 로그인 기능 구현
- [x] 회원가입 버튼 UI 구현: 로그인 화면 하단에 회원가입 버튼 배치
- [x] 회원가입 버튼 로직 구현: 버튼 클릭 시 회원가입 페이지로 이동
- [x] 회원가입 UI 구현: 로그인 UI 참고 및 사용
- [x] 회원가입 로직 구현: 회원가입 성공 시 로그인 페이지로 이동 및 성공 메시지 표시

- 상품 상세 페이지
- [ ] 상품 상세 정보 로딩 테스트
- [ ] 옵션 선택 테스트

- 결제하기 페이지
- [ ] 입력 필드 테스트
- [ ] 버튼 클릭 테스트
- 현금영수증 Checkbox가 `false`인 경우 현금영수증 종류, 현금영수증 번호 필드가 비활성화 되어있는지 확인하는 테스트 코드 작성
- [ ] Checkbox 상태에 따른 필드 활성화/비활성화 테스트
- [ ] Checkbox가 `true`인 경우 필드 값 입력 테스트
- Form의 validation 로직이 정상 동작하는지 확인하는 테스트 코드 작성
- [ ] 필수 입력 필드 검사
- [ ] 입력 값 형식 검사

## 2단계 - 로그인, 관심 상품 등록 / 삭제, 관심 목록 구현
- [ ] 관심 등록 버튼 UI 구현
- [ ] 관심 등록 버튼 로직 구현: 관심 등록 성공 시 "관심 등록 완료" Alert 메시지 표시

- 로그인 기능을 구현합니다.
- 회원가입 화면을 만들고, 회원가입 기능이 동작하도록 구현합니다. (회원가입을 하면 로그인이 되도록 합니다.)
- 회원가입 버튼은 로그인 화면 하단에 배치합니다. 로그인 화면을 그대로 사용해도 괜찮습니다.
- 상품 상세 페이지에서 관심 등록 버튼을 만듭니다.
- 상품 상세 페이지에서 관심 버튼을 클릭했을 때 관심 추가가 동작하도록 합니다.
- 관심 등록 성공 시 Alert로 "관심 등록 완료" 메시지를 노출합니다.
- 마이 페이지에서 관심 목록 리스트를 만듭니다.
- 관심 목록 리스트는 Chakra UI를 사용하여 자유롭게 만듭니다.
- 관심 목록 API는 카카오테크 선물하기 API 노션의 response 데이터를 사용합니다.
- 관심 목록 리스트에서 관심 삭제가 가능하도록 합니다.
- 관심 삭제 시 목록에서 사라집니다.
- 본인만의 기준으로 일관된 코드를 작성합니다.
- 기능 단위로 나누어 커밋을 합니다.
- 마이 페이지
- [ ] 관심 목록 리스트 UI 구현: Chakra UI 컴포넌트 활용
- [ ] 관심 목록 API 활용: 선물하기 API 노션의 response 데이터 활용
- [ ] 관심 목록 리스트 로직 구현: 관심 삭제 성공 시 해당 항목 리스트에서 제거

---

Expand Down
15 changes: 15 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,26 @@ import { queryClient } from './api/instance';
import { AuthProvider } from './provider/Auth';
import { Routes } from './routes';

const initializeMocks = async () => {
if (process.env.NODE_ENV === 'development') {
const { worker } = await import('./mocks/browser');
worker.start().then(() => {
console.log('Mock Service Worker is running');
});
}
};

initializeMocks();

const App = () => {
return (
// ChakraProvider wraps the app to provide Chakra UI components and theme
<ChakraProvider>
{/* QueryClientProvider provides React Query context for managing server state */}
<QueryClientProvider client={queryClient}>
{/* AuthProvider provides authentication context to the app */}
<AuthProvider>
{/* Routes component handles the routing of the application */}
sugoring marked this conversation as resolved.
Show resolved Hide resolved
<Routes />
</AuthProvider>
</QueryClientProvider>
Expand Down
26 changes: 26 additions & 0 deletions src/api/hooks/login.mock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { rest } from 'msw';

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

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

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

export const loginMockHandler = [
rest.post<LoginRequestBody>(`${BASE_URL}/api/members/login`, async (req, res, ctx) => {
const { email } = await req.json();

// 항상 성공 응답 반환
const response: LoginSuccessResponse = {
email,
token: 'mocked-jwt-token',
};
return res(ctx.status(200), ctx.json(response));
}),
];
44 changes: 20 additions & 24 deletions src/api/hooks/productDetail.mock.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,34 @@
import { rest } from 'msw';
import { z } from 'zod';

import { getProductDetailPath } from './productDetailPath'; // getProductDetailPath 함수 import
const BASE_URL = 'http://localhost:3000';

// 제품 상세 정보 데이터 스키마 (zod)
const productDetailResponseDataSchema = z.object({
id: z.number(),
name: z.string(),
price: z.number(),
imageUrl: z.string(),
categoryId: z.number(),
});
interface ProductDetail {
id: number;
name: string;
price: number;
imageUrl: string;
categoryId: number;
}

export type ProductDetailResponseData = z.infer<typeof productDetailResponseDataSchema>;

// 샘플 제품 상세 정보 데이터
const sampleProductDetail: ProductDetailResponseData = {
const mockProductDetail: ProductDetail = {
id: 1,
name: '[단독각인] 피렌체 1221 에디션 오드코롱 50ml (13종 택1)',
price: 145000,
name: 'Sample Product',
price: 100,
imageUrl:
'https://st.kakaocdn.net/product/gift/product/20240215083306_8e1db057580145829542463a84971ae3.png',
categoryId: 2920,
'https://i.namu.wiki/i/lTIwu3NCJk-m5VOdugukoiVGzyZAVauahUc2qnrOX-j8XFCA7PXv95cioeTRqrixnTUYDdfZnapP2Fo-jz3OBl5VYyd5SJpft-ZcMedgg4QmJGEkeol2W-do5U3mL6_vqQYTPAr7QBwp7VTts7kmfiYUgQ_Hosv7gwcBxnFagmo.webp',
categoryId: 1,
};

// MSW 핸들러 (API 모킹)
export const productDetailMockHandler = [
rest.get(getProductDetailPath(':productId'), (req, res, ctx) => {
rest.get(`${BASE_URL}/api/products/:productId`, (req, res, ctx) => {
const { productId } = req.params;

if (productId === sampleProductDetail.id.toString()) {
return res(ctx.json(sampleProductDetail));
// 실제 환경에서는 여기서 productId를 사용하여 다양한 상품을 반환할 수 있습니다.
// 이 예제에서는 항상 같은 mockProductDetail을 반환합니다.
if (productId) {
return res(ctx.status(200), ctx.json(mockProductDetail));
} else {
return res(ctx.status(404), ctx.json({ message: 'Product not found' }));
}

return res(ctx.status(404), ctx.json({ error: 'Product not found' }));
}),
];
45 changes: 20 additions & 25 deletions src/api/hooks/productOptions.mock.ts
Original file line number Diff line number Diff line change
@@ -1,35 +1,30 @@
import { rest } from 'msw';
import { z } from 'zod';

import { getProductOptionsPath } from './productOptionsPath'; // getProductOptionsPath 함수 import
const BASE_URL = 'http://localhost:3000';

// 제품 옵션 API 응답 스키마 (zod)
const productOptionsResponseDataSchema = z.array(
z.object({
id: z.number(),
name: z.string(),
quantity: z.number(),
productId: z.number(),
})
);
interface ProductOption {
id: number;
name: string;
quantity: number;
productId: number;
}

export type ProductOptionsResponseData = z.infer<typeof productOptionsResponseDataSchema>;
// 동적으로 옵션을 생성하는 함수
const generateOptions = (productId: number): ProductOption[] => {
const optionCount = Math.floor(Math.random() * 3) + 1; // 1에서 3개의 옵션 생성
return Array.from({ length: optionCount }, (_, index) => ({
id: index + 1,
name: `Option ${String.fromCharCode(65 + index)}`, // A, B, C...
quantity: Math.floor(Math.random() * 50) + 10, // 10에서 59 사이의 수량
productId: productId,
}));
};

// 샘플 제품 옵션 데이터 (productId: 1)
const sampleProductOptions: ProductOptionsResponseData = [
{ id: 1, name: 'Option A', quantity: 10, productId: 1 },
{ id: 2, name: 'Option B', quantity: 20, productId: 1 },
];

// 제품 옵션 모킹 핸들러
export const productOptionsMockHandler = [
rest.get(getProductOptionsPath(':productId'), (req, res, ctx) => {
rest.get(`${BASE_URL}/api/products/:productId/options`, (req, res, ctx) => {
const { productId } = req.params;
const options = generateOptions(Number(productId));

if (productId === '1') {
return res(ctx.status(200), ctx.json(sampleProductOptions));
}

return res(ctx.status(404), ctx.json({ message: 'Product not found' }));
return res(ctx.status(200), ctx.json(options));
}),
];
31 changes: 31 additions & 0 deletions src/api/hooks/register.mock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { rest } from 'msw';

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

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

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

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

// Basic validation (add more robust validation as needed)
if (!email || !password) {
return res(ctx.status(400), ctx.json({ message: 'Invalid input' }));
}

// Simulate successful registration
const response: RegisterSuccessResponse = {
email,
token: 'mocked-registration-token',
};
return res(ctx.status(201), ctx.json(response));
}),
];
51 changes: 36 additions & 15 deletions src/api/hooks/useGetProductDetail.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,43 @@
import { useSuspenseQuery } from '@tanstack/react-query';
import type { UseQueryOptions } from '@tanstack/react-query';
import { useQuery } from '@tanstack/react-query';

import { fetchInstance } from '../instance';
import type { ProductDetailResponseData } from './productDetail.mock';
import { getProductDetailPath } from './productDetailPath';
const BASE_URL = 'http://localhost:3000';

export type ProductDetailRequestParams = {
productId: string;
};
export interface ProductDetail {
id: number;
name: string;
price: number;
imageUrl: string;
categoryId: number;
}

export interface ProductDetailRequestParams {
productId: number;
}

const fetchProductDetail = async ({
productId,
}: ProductDetailRequestParams): Promise<ProductDetail> => {
const response = await fetch(`${BASE_URL}/api/products/${productId}`);

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

// 실제 API 호출 함수
export const getProductDetail = async ({ productId }: ProductDetailRequestParams): Promise<ProductDetailResponseData> => {
const response = await fetchInstance.get<ProductDetailResponseData>(getProductDetailPath(productId));
return response.data;
return response.json();
};

export const useGetProductDetail = ({ productId }: ProductDetailRequestParams) => {
return useSuspenseQuery({
queryKey: [getProductDetailPath(productId)],
queryFn: () => getProductDetail({ productId }),
export const useGetProductDetail = (
{ productId }: ProductDetailRequestParams,
options?: Omit<
UseQueryOptions<ProductDetail, Error, ProductDetail, [string, number]>,
'queryKey' | 'queryFn'
>,
) => {
return useQuery({
queryKey: ['productDetail', productId],
queryFn: () => fetchProductDetail({ productId }),
staleTime: 5 * 60 * 1000, // 5 minutes
...options,
});
};
55 changes: 34 additions & 21 deletions src/api/hooks/useGetProductOptions.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,45 @@
import type { UseQueryOptions, UseQueryResult } from '@tanstack/react-query';

import type { UseQueryOptions } from '@tanstack/react-query';
import { useQuery } from '@tanstack/react-query';

import { fetchInstance } from '../instance'; // 실제 API 통신을 위한 fetchInstance
import type { ProductOptionsResponseData } from './productOptions.mock'; // 타입 공유
import { getProductOptionsPath } from './productOptionsPath'; // path 생성 함수 공유
const BASE_URL = 'http://localhost:3000';

type Props = {
productId: string;
};
export interface ProductOption {
id: number;
name: string;
quantity: number;
productId: number;
}

export interface ProductOptionsRequestParams {
productId: number;
}

const fetchProductOptions = async ({
productId,
}: ProductOptionsRequestParams): Promise<ProductOption[]> => {
const response = await fetch(`${BASE_URL}/api/products/${productId}/options`);

if (!response.ok) {
if (response.status === 404) {
throw new Error('Product not found');
}
throw new Error('Failed to fetch product options');
}

// 데이터 가져오는 함수
export const getProductOptions = async ({ productId }: Props): Promise<ProductOptionsResponseData> => {
const response = await fetchInstance.get<ProductOptionsResponseData>(
getProductOptionsPath(productId)
);
return response.data;
return response.json();
};

// React Query 훅
export const useGetProductOptions = (
params: Props,
options?: UseQueryOptions<ProductOptionsResponseData, Error>
): UseQueryResult<ProductOptionsResponseData, Error> => {
{ productId }: ProductOptionsRequestParams,
options?: Omit<
UseQueryOptions<ProductOption[], Error, ProductOption[], [string, number]>,
'queryKey' | 'queryFn'
>,
) => {
return useQuery({
queryKey: ['productOptions', productId],
queryFn: () => fetchProductOptions({ productId }),
...options,
queryKey: [getProductOptionsPath(params.productId)], // 쿼리 키에 동적 productId 포함
queryFn: () => getProductOptions(params),
enabled: !!params.productId, // productId가 있을 때만 쿼리 활성화
retry: false,
});
};
Loading