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

[장바구니 협업 Step 2] 윤생(이윤성) 미션 제출합니다. #151

Merged
merged 72 commits into from
Jun 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
72 commits
Select commit Hold shift + click to select a range
31a3818
feat: order page 라우팅 설정
2yunseong May 26, 2023
61da3d3
fix: mock fixture 요구사항에 맞게 변경
2yunseong May 26, 2023
5db4e31
fix: GET cart-items 로직 변경
2yunseong May 26, 2023
62a32a8
refactor: mock 핸들러 분리
2yunseong May 26, 2023
e1365ad
fix: 인증키 변경
2yunseong May 26, 2023
569c91b
feat: 상품 주문 POST mock 구현
2yunseong May 26, 2023
8a45a7a
feat: 주문 목록 조회 Mock api 구현
2yunseong May 26, 2023
f6a524b
feat: 주문 POST 기능 구현
2yunseong May 26, 2023
a40342c
feat: 주문 목록 조회 기능 구현
2yunseong May 26, 2023
edecc95
chore: 배송비 상수 변경
2yunseong May 26, 2023
4bf8f3e
feat: 주문 목록 페이지 링크 추가
2yunseong May 26, 2023
5ca6f99
feat: FetchOrderRes 타입 구체화
2yunseong May 26, 2023
74b3d20
feat: Order 컴포넌트 구현
2yunseong May 26, 2023
6d7ade0
feat: OrderProductItems 구현
2yunseong May 26, 2023
97781dd
feat: OrderList 컴포넌트 구현
2yunseong May 26, 2023
047a533
feat: 비동기 데이터 처리를 위한 OrderPage Suspense 삽입
2yunseong May 26, 2023
57c07c6
refactor: 페이지 제목 로직 메서드 화
2yunseong May 26, 2023
0f82763
feat: 장바구니 상세 조회 mock api 구현
2yunseong May 27, 2023
68ef52c
feat: 주문 상세조회 api 구현
2yunseong May 27, 2023
88fa9a9
refactor: 개발단 local url 삽입
2yunseong May 27, 2023
fb93d6b
feat: 주문 상세 페이지 구현
2yunseong May 27, 2023
f0ddd9b
feat: 주문 상세 페이지 라우팅
2yunseong May 27, 2023
998521b
refactor: 디렉토리 이전
2yunseong May 30, 2023
10f7196
refactor: 주문 상세보기 링크 추가
2yunseong May 30, 2023
72c7e66
refactor: 요구사항 변경에 따른 price 수정
2yunseong May 30, 2023
4672ed3
feat: 쿠폰 조회 mock api 반영
2yunseong May 31, 2023
ced9246
refactor: fetch 로직 축약
2yunseong May 31, 2023
cdec66e
feat: 쿠폰 조회 api 작성
2yunseong May 31, 2023
fd25c85
feat: 쿠폰 recoil 상태 선언
2yunseong May 31, 2023
d93dc9c
fix: api 스키마 변경
2yunseong May 31, 2023
9529090
fix: 할인 정책으로 인한 배송비 제거 후 할인 금액 반영
2yunseong May 31, 2023
87aca32
feat: 개별 쿠폰 선택 컴포넌트 구현
2yunseong May 31, 2023
44194f5
feat: 전체적으로 적용되는 쿠폰 컴포넌트 구현
2yunseong May 31, 2023
088bafb
feat: 쿠폰 컴포넌트 배치
2yunseong May 31, 2023
e1c152a
fix: 잘못된 데이터 변경
2yunseong May 31, 2023
1988141
fix: 쿠폰이 중복으로 적용되는 에러 해결
2yunseong May 31, 2023
22b617d
feat: 할인 가격 측정 로직 구현
2yunseong May 31, 2023
182d262
refactor: 비구조화 할당 사용
2yunseong May 31, 2023
df0ac76
refactor: 중복되는 로직 메서드로 축약
2yunseong May 31, 2023
5c77373
refactor: 타입 분리
2yunseong May 31, 2023
9edc50c
feat: 할인 가격 방어 코드 작성
2yunseong Jun 1, 2023
49a714d
refactor: 이벤트 핸들러 인라인 제거
2yunseong Jun 1, 2023
0085a23
feat: 주문 api에 coupon Id 추가
2yunseong Jun 1, 2023
4b0622d
feat: 선택된 쿠폰의 아이디가 존재하는지 확인하는 로직 구현
2yunseong Jun 1, 2023
8cdc625
refactor: selector 네이밍 변경
2yunseong Jun 1, 2023
b2f3fc2
refactor: 의도에 맞는 셀렉터 사용 및 네이밍 변경
2yunseong Jun 1, 2023
dec5f69
refactor: atom, selector 네이밍 변경
2yunseong Jun 1, 2023
b7a2439
feat: select 해제 시 방어 코드 작성
2yunseong Jun 1, 2023
6a11018
feat: 전체 쿠폰 선택 select 태그 스타일링
2yunseong Jun 1, 2023
2758e1d
feat: 주문 조회 api 변경에 따른 코드 수정
2yunseong Jun 1, 2023
fe2fec5
feat: 쿠폰 조회 api 변경에 따른 데이터 구조 변경
2yunseong Jun 1, 2023
84b0bde
refactor: 컴포넌트 네이밍 변경
2yunseong Jun 1, 2023
ceb3db1
feat: 쿠폰 선택 예외케이스 처리
2yunseong Jun 1, 2023
87f24c8
feat: select 태그 스타일링
2yunseong Jun 1, 2023
8b2999c
refactor: 타입 선언 한 파일로 합침
2yunseong Jun 3, 2023
b4080c9
feat: 에러 페이지 생성
2yunseong Jun 3, 2023
c8fd4ef
refactor: 불필요한 cart Item 파생상태 제거
2yunseong Jun 4, 2023
afdd6d0
refactor: 불필요한 파생상태 제거
2yunseong Jun 4, 2023
c27542d
refactor: 개별 쿠폰 선택 로직 Hook으로 분리
2yunseong Jun 4, 2023
3d70bef
refactor: 전체 적용 쿠폰 선택 로직 Hook으로 분리
2yunseong Jun 4, 2023
bd211d5
refactor: 도메인 로직 recoil 파일에서 분리
2yunseong Jun 4, 2023
9327fc2
refactor: 유틸리티 타입 사용
2yunseong Jun 4, 2023
5202f4c
refactor: 속성 참조 부분 변경
2yunseong Jun 5, 2023
9c4a7bf
feat: 서버 변경하는 로직 추가
2yunseong Jun 5, 2023
6a06eef
refactor: url 고정
2yunseong Jun 5, 2023
1591796
refactor: 불필요한 selector 제거
2yunseong Jun 5, 2023
1bc151c
refactor: 불필요한 주석 제거
2yunseong Jun 8, 2023
78a3ae5
refactor: 불필요한 styled component 선언 제거
2yunseong Jun 8, 2023
767fae3
refactor: suspense 갱신
2yunseong Jun 8, 2023
a13de27
refactor: reducer 사용
2yunseong Jun 8, 2023
650dc99
refactor: hook 사용처에서 필요한 속성만 리턴
2yunseong Jun 8, 2023
20642d0
refactor: 불필요한 파생상태 제거
2yunseong Jun 8, 2023
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
7 changes: 7 additions & 0 deletions src/Router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { PAGE_ROUTES } from './constants/routes';
import CartPage from './pages/CartPage/CartPage';
import EndpointRefresher from './components/EndpointRefresher/EndpointRefresher';
import Loading from './components/common/Loading/Loading';
import OrderPage from './pages/OrderPage/OrderPage';
import OrderDetailPage from './pages/OrderDetailPage/OrderDetailPage';

const Router = () => {
return (
Expand All @@ -17,6 +19,11 @@ const Router = () => {
<Route element={<Content />}>
<Route index element={<ProductPage />} />
<Route path={PAGE_ROUTES.CART} element={<CartPage />} />
<Route
path={`${PAGE_ROUTES.ORDER}/:orderId`}
element={<OrderDetailPage />}
/>
<Route path={PAGE_ROUTES.ORDER} element={<OrderPage />} />
</Route>
</Routes>
</Layout>
Expand Down
10 changes: 4 additions & 6 deletions src/apis/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,7 @@ import {

const base64 = 'YUBhLmNvbToxMjM0';

const BASE =
process.env.NODE_ENV === 'development'
? 'http://localhost:3000/react-shopping-cart/'
: 'https://n0eyes.github.io/react-shopping-cart/';
export const BASE = ENDPOINT['말랑'];

class FetchQuery implements FetchQueryInstance {
private defaultConfig: ExternalConfig = {};
Expand Down Expand Up @@ -106,9 +103,10 @@ class FetchQuery implements FetchQueryInstance {
}
}

export const fetchQuery = new FetchQuery({ baseURL: ENDPOINT['말랑'] });
export const fetchQuery = new FetchQuery({ baseURL: BASE });

export const authFetchQuery = new FetchQuery({
baseURL: ENDPOINT['말랑'],
baseURL: BASE,
headers: {
Authorization: `Basic ${base64}`,
'Content-Type': 'application/json',
Expand Down
36 changes: 35 additions & 1 deletion src/apis/api.type.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { DuplicateKeys } from '../types/common';
import type { CartItem } from '../types/cart';
import type { DuplicateKeys } from '../types/common';
import type { Coupon, SpecificCoupon } from '../types/coupon';
import type { Order } from '../types/orders';
import type { Product } from '../types/products';

export type FetchQueryInstance = {
[m in Lowercase<Method>]: <T>(
Expand Down Expand Up @@ -28,3 +32,33 @@ export type ExternalConfig = Omit<
baseURL?: string;
body?: unknown;
};

export interface AddCartDataReq {
productId: number;
}

export type FetchCartRes = CartItem[];

export interface AddCartDataRes {}

export interface UpdateCartItemRes {}

export interface DeleteCartItemRes {}

export interface FetchCouponsRes {
allCoupons: Coupon[];
specificCoupons: SpecificCoupon[];
}

export interface PostOrderRes {
cartItemIds: number[];
couponIds: number[];
}

export interface FetchOrdersRes {
orders: Order[];
}

export type FetchDetailOrderRes = Order;

export type FetchProductDataRes = Product[];
33 changes: 14 additions & 19 deletions src/apis/cart.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,21 @@
import { CartItem } from '../types/cart';
import type { CartItem } from '../types/cart';
import { waitFor, WaitForOptions } from '../utils/waitFor';
import { authFetchQuery } from './api';
import { FetchQueryRes } from './api.type';

export type FetchCartRes = CartItem[];
import type {
AddCartDataReq,
AddCartDataRes,
DeleteCartItemRes,
FetchCartRes,
FetchQueryRes,
UpdateCartItemRes,
} from './api.type';

export const fetchCart = (options?: WaitForOptions<FetchCartRes>) => {
const promise = authFetchQuery.get<FetchCartRes>(`/cart-items`);

return waitFor(promise, options);
};

export interface AddCartDataReq {
productId: number;
}

interface AddCartDataRes {}

export const addToCart: (
payload: AddCartDataReq
) => FetchQueryRes<AddCartDataRes> = ({ productId }) => {
Expand All @@ -25,13 +24,6 @@ export const addToCart: (
});
};

export interface UpdateCartItemReq {
cartId: number;
quantity: number;
}

interface UpdateCartItemRes {}

export const updateCartItem: (
payload: UpdateCartItemReq
) => FetchQueryRes<UpdateCartItemRes> = ({ cartId, quantity }) => {
Expand All @@ -40,10 +32,13 @@ export const updateCartItem: (
});
};

interface DeleteCartItemRes {}

export const deleteCartItem: (
id: CartItem['id']
) => FetchQueryRes<DeleteCartItemRes> = (id) => {
return authFetchQuery.delete<DeleteCartItemRes>(`/cart-items/${id}`);
};

export interface UpdateCartItemReq {
cartId: number;
quantity: number;
}
6 changes: 6 additions & 0 deletions src/apis/coupons.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { authFetchQuery } from './api';
import type { FetchCouponsRes } from './api.type';

export const fetchCoupons = async () => {
return authFetchQuery.get<FetchCouponsRes>('/coupons');
};
33 changes: 33 additions & 0 deletions src/apis/order.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Order } from '../types/orders';
import { authFetchQuery } from './api';
import type {
FetchDetailOrderRes,
FetchOrdersRes,
FetchQueryRes,
PostOrderRes,
} from './api.type';

export const postOrder: (
payload: PostOrderRes
) => FetchQueryRes<PostOrderRes> = ({
cartItemIds,
couponIds,
}: PostOrderRes) => {
return authFetchQuery.post(`/orders`, {
body: { cartItemIds, couponIds },
});
};

export const fetchOrders = async (): FetchQueryRes<FetchOrdersRes> => {
const data = await authFetchQuery.get<FetchOrdersRes>('/orders');
return data;
};

export const fetchDetailOrder = async (
orderId: number
): FetchQueryRes<Order> => {
const data = await authFetchQuery.get<FetchDetailOrderRes>(
`/orders/${orderId}`
);
return data;
};
9 changes: 2 additions & 7 deletions src/apis/products.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
import { Product } from '../types/products';
import type { FetchProductDataRes, FetchQueryRes } from './api.type';
import { fetchQuery } from './api';
import { FetchQueryRes } from './api.type';

type FetchProductDataRes = Product[];

export const fetchProductData =
async (): FetchQueryRes<FetchProductDataRes> => {
const data = await fetchQuery.get<FetchProductDataRes>('/products');

return data;
return fetchQuery.get<FetchProductDataRes>('/products');
};
106 changes: 58 additions & 48 deletions src/atoms/cart.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,24 @@
import { selector, atom, selectorFamily } from 'recoil';
import { CartItem } from '../types/cart';
import { fetchCart } from '../apis/cart';
import { couponsSelector, selectedCouponsState } from './coupons';
import { getDiscountPrice } from '../domain/discount';

export const cartState = atom({
export const cartSelector = selector({
key: 'cart',
default: selector({
key: 'getMockCart',
get: async () => {
const { data } = await fetchCart();

return data;
},
}),
});
get: async () => {
const { data } = await fetchCart();

export const cartItemsAmountSelector = selector({
key: 'cartItemsAmountSelector',
get: ({ get }) => {
return get(cartState).length;
return data;
},
});

export const selectedItemsState = atom({
key: 'selectedItemsState',
export const selectedCartItemIdsState = atom({
key: 'selectedCartItemIdsState',
default: selector({
key: 'selectedItemsStateSelector',
key: 'selectedCartItemIdsStateSelector',
get: ({ get }) => {
const cart = get(cartState);
const cart = get(cartSelector);

return cart.reduce<Set<CartItem['id']>>(
(selectedItems, item) => selectedItems.add(item.id),
Expand All @@ -36,44 +28,21 @@ export const selectedItemsState = atom({
}),
});

export const selectedItemsSelector = selector({
key: 'selectedItemsSelector',
get: ({ get }) => {
const cart = get(cartState);
const selectedItems = get(selectedItemsState);

return cart.reduce<Set<CartItem['id']>>(
(newSelectedItems, item) =>
selectedItems.has(item.id)
? newSelectedItems.add(item.id)
: newSelectedItems,
new Set()
);
},
set: ({ set }, newValue) => {
set(selectedItemsState, newValue);
},
});

export const selectedItemsAmountSelector = selector({
key: 'selectedItemsAmountSelector',
get: ({ get }) => get(selectedItemsSelector).size,
});

export const getCartItemById = selectorFamily({
key: 'hasItemInCart',
export const isSelectedCartId = selectorFamily({
key: 'isSelectedCartIdSelector',
get:
(id: CartItem['id']) =>
(cartId: number) =>
({ get }) => {
return get(cartState).find((item) => item.id === id);
const selectedCartItems = get(selectedCartItemIdsState);
return selectedCartItems.has(cartId);
},
});

export const totalPriceSelector = selector({
key: 'totalPriceSelector',
get: ({ get }) => {
const cart = get(cartState);
const selectedItems = get(selectedItemsSelector);
const cart = get(cartSelector);
const selectedItems = get(selectedCartItemIdsState);

return cart.reduce(
(totalPrice, { id, quantity, product: { price } }) =>
Expand All @@ -82,3 +51,44 @@ export const totalPriceSelector = selector({
);
},
});

export const discountPrice = selector({
key: 'discountPriceSelector',
2yunseong marked this conversation as resolved.
Show resolved Hide resolved
get: ({ get }) => {
const cart = get(cartSelector);
const { allCoupons, specificCoupons } = get(couponsSelector);
const selectedCoupons = get(selectedCouponsState);
const totalPrice = get(totalPriceSelector);

if (allCoupons.some((coupon) => selectedCoupons.includes(coupon.id))) {
2yunseong marked this conversation as resolved.
Show resolved Hide resolved
const applyCoupon = allCoupons.find(
(coupon) => coupon.id === selectedCoupons[0]

Choose a reason for hiding this comment

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

selectedCoupons의 첫번째 요소만 확인해도 충분한가요?

Copy link
Author

Choose a reason for hiding this comment

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

allCoupon임을 처음에 if문(85번째줄)에서 보장한다고 생각해
도메인 상 선택 로직에서 하나만 선택할 수 있도록 보장하므로 충분하다고 생각합니다.

Choose a reason for hiding this comment

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

allCoupon이 항상 요소 하나만 있는 배열임을 이 모듈 입장에서 항상 보장할 수 있는지에 대한 질문이었습니다. allCoupon의 스펙이 바뀌면 여기는 문제가 될 것 같군요

);
if (!applyCoupon) return 0;

return getDiscountPrice(applyCoupon, totalPrice);
}

return selectedCoupons.reduce((acc, couponId) => {
const applyCoupon = specificCoupons.find(
(coupon) => coupon.id === couponId
);

if (!applyCoupon) return acc;

const applyCartItem = cart.find(
(item) => item.product.id === applyCoupon.targetProductId
Tanney-102 marked this conversation as resolved.
Show resolved Hide resolved
);

if (!applyCartItem) return acc;

return (
acc +
getDiscountPrice(
applyCoupon,
applyCartItem.product.price * applyCartItem.quantity
)
);
}, 0);
},
});
27 changes: 27 additions & 0 deletions src/atoms/coupons.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { atom, selector, selectorFamily } from 'recoil';
import { fetchCoupons } from '../apis/coupons';

export const selectedCouponsState = atom<number[]>({
key: 'selectedCouponsState',
default: [],
});

export const couponsSelector = selector({
key: 'couponsSelector',
get: async () => {
const { data } = await fetchCoupons();
return data;
},
});

export const specificCouponSelector = selectorFamily({
key: 'specificCouponSelector',
get:
(targetCartItemId: number) =>
({ get }) => {
const { specificCoupons } = get(couponsSelector);
return specificCoupons.filter(
(coupon) => coupon.targetProductId === targetCartItemId
);
},
});
19 changes: 19 additions & 0 deletions src/atoms/orders.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { selector, selectorFamily } from 'recoil';
import { fetchDetailOrder, fetchOrders } from '../apis/order';

export const ordersSelector = selector({
key: 'ordersSelector',
get: async () => {
const { data } = await fetchOrders();

return data;
},
});

export const detailOrderSelector = selectorFamily({
key: 'detailOrderSelector',
get: (detailId: number) => async () => {
const { data } = await fetchDetailOrder(detailId);
return data;
},
});
Loading