Skip to content

[1단계 - 장바구니] 리바이(성은우) 미션 제출합니다. #351

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

Merged
merged 67 commits into from
Jun 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
67 commits
Select commit Hold shift + click to select a range
5484192
chore: 의존성 제거
woowa-euijinkk May 27, 2025
627d584
chore: Emotion 라이브러리 설치
eunwoo-levi May 27, 2025
03a62f8
docs: README 기능 요구 명세서 작성
eunwoo-levi May 27, 2025
dc5d83a
feat: Navbar 컴포넌트 구현
eunwoo-levi May 27, 2025
ac026b9
styles: 전역 css 초기화
eunwoo-levi May 27, 2025
ec2792f
feat: Cart 메인 페이지에서 큰 레이아웃 구현
eunwoo-levi May 27, 2025
c317e07
chore: emotion tsconfig 전역 설정
eunwoo-levi May 27, 2025
3790903
feat: Custom Button 컴포넌트 구현
eunwoo-levi May 27, 2025
fefdc4f
feat: Footer 컴포넌트 구현 및 Button컴포넌트 적용
eunwoo-levi May 27, 2025
e4644b1
feat: App 컴포넌트에 Footer 및 AppContent 적용
eunwoo-levi May 27, 2025
45276a5
feat: CartHeader 컴포넌트 구현 및 적용
eunwoo-levi May 27, 2025
600b695
feat: Cart feature endpoint 추가
eunwoo-levi May 27, 2025
eaa9d32
chore: 파일명 변경
eunwoo-levi May 27, 2025
aeb9f0e
feat: CartItemCard 컴포넌트 구현
eunwoo-levi May 27, 2025
3881eea
feat: CartItemQuantitySelector 컴포넌트 구현
eunwoo-levi May 27, 2025
d40f32d
feat: CartList 컴포넌트 구현
eunwoo-levi May 27, 2025
727f487
styles: Button 컴포넌트 style 구현
eunwoo-levi May 27, 2025
0e324e3
feat: SelectInput 컴포넌트 구현
eunwoo-levi May 27, 2025
0dd8cb8
feat: CartList컴포넌트를 App에 적용 및 endpoint 추가
eunwoo-levi May 27, 2025
d22f6ec
feat: 가격 정보 OrderPriceSummary 컴포넌트 구현
eunwoo-levi May 28, 2025
12c5e91
feat: Shared 디렉토리에 Cart타입 정의
eunwoo-levi May 28, 2025
3d4049f
feat: SelectedCart 정보를 가진 context 구현
eunwoo-levi May 28, 2025
e4a9310
feat: httpClient 유틸 함수 구현
eunwoo-levi May 28, 2025
9e20a79
feat: 백엔드로부터 CartItem 들고오는 GET API 로직 구현
eunwoo-levi May 28, 2025
73613f1
feat: context를 통하여 CartItemCard의 input box를 통한 가격 계산 로직 구현
eunwoo-levi May 28, 2025
9e0e819
feat: 삭제 버튼 눌렀을 때 cartItem에 대해 DELETE API 호출 및 context에서 제거 로직 구현
eunwoo-levi May 28, 2025
bff7e83
feat: - 또는 +버튼을 눌렀을 때 CartItem 수량 업데이트 로직 구현
eunwoo-levi May 28, 2025
82437b5
docs: 기능 요구 명세서 업데이트
eunwoo-levi May 28, 2025
3ed91e8
feat: 장바구니 상품 없을 시 UI 보여주는 컴포넌트 및 로직 구현
eunwoo-levi May 28, 2025
efecc48
refactor: 상품 없을 시 헤더 수정
eunwoo-levi May 28, 2025
bd2e97e
feat: createBrowserRouter 사용하여 routes 구현
eunwoo-levi May 28, 2025
e7a0c92
feat: 모든 페이지를 위한 Layout 컴포넌트 구현
eunwoo-levi May 28, 2025
bf2c469
feat: Error Page 구현
eunwoo-levi May 28, 2025
1d2d200
chore: CartPageFooter 파일 경로 이동
eunwoo-levi May 28, 2025
53330f1
refactor: Navbar 컴포넌트에 handleClick 매개변수 전달
eunwoo-levi May 28, 2025
51b6039
feat: main.tsx에 RouterProvider 적용
eunwoo-levi May 28, 2025
b70b634
feat: ConfirmationPage 컴포넌트 적용
eunwoo-levi May 28, 2025
19a869b
feat: 경로 URL 상수화 정의
eunwoo-levi May 28, 2025
aaaebdd
chore: vitest 설치 및 세팅
eunwoo-levi May 29, 2025
34403fd
fix: vitest setup 에러 해결
eunwoo-levi May 29, 2025
7996e35
chore: msw 설치 및 환경 설정
eunwoo-levi May 29, 2025
402743d
docs: PR template
woowa-euijinkk May 29, 2025
2a70c2c
chore: msw 설정 수정 및 mock data 추가
eunwoo-levi May 29, 2025
f365138
refactor: data-testid 추가
eunwoo-levi May 29, 2025
8049a7f
test: 장바구니 테스트코드 추가
eunwoo-levi May 29, 2025
8bdfedd
docs: README 기능 요구 명세서 업데이트
eunwoo-levi May 29, 2025
2519c0e
fix: router에 의해 base URL 설정 수정
eunwoo-levi May 29, 2025
1f4d75e
feat: Lazy import 및 suspense 적용하여 Skeleton UI 적용
eunwoo-levi May 29, 2025
285e669
refactor: Navbar 올바른 로직으로 리팩토링
eunwoo-levi May 29, 2025
6815a04
chore: types 디렉토리명 변경
eunwoo-levi May 29, 2025
9f8e564
chore: 함수명이랑 파일명이 동일하도록 파일명 변경
eunwoo-levi May 31, 2025
b10222f
refactor: 개별 input box가 모두 선택 시 전체전첵 input box 가 checked 되도록 하여 UX 개선
eunwoo-levi May 31, 2025
9cca2c8
refactor: some 메소드를 사용함으로써 boolean값을 return으로 받아 코드 가독성 측면에서 개선
eunwoo-levi May 31, 2025
7230cca
refactor: SelectInput 컴포넌트에 Type 명시
eunwoo-levi May 31, 2025
4805823
feat: OrderPriceSummary 매직넘버 상수화
eunwoo-levi May 31, 2025
5e04047
test: 빈 배열을 반환하는 새로운 App 컴포넌트 구현 및 빈 장바구니 테스트에 적용
eunwoo-levi May 31, 2025
aa138c2
chore: router에서 basename 제거
eunwoo-levi May 31, 2025
618a98b
chore: App 네이밍을 CartPage로 바꿈으로써 명료하게 개선
eunwoo-levi May 31, 2025
4c18d5a
refactor: 카드 페이지 처음 진입때 전체선택 자동화 로죅 추가
eunwoo-levi Jun 1, 2025
3ed2fec
test: 매직넘버 상수화
eunwoo-levi Jun 1, 2025
fdc0e79
refactor: cartItems도 context 통하여 전역 상태 관리로 리팩토링
eunwoo-levi Jun 2, 2025
e787b52
chore: 불필요한 라이브러리 제거
eunwoo-levi Jun 3, 2025
ca2e72a
refactor: CartItemQuantitySelector 컴포넌트에서 불필요한 state제거 후 효율적으로 리팩토링
eunwoo-levi Jun 3, 2025
dc85038
test: UI 테스트 로직과 비즈니스 로직 분리
eunwoo-levi Jun 3, 2025
bfad5e6
test: UI 테스트 로직과 비즈니스 로직 분리
eunwoo-levi Jun 3, 2025
174dabf
test: MSW 핸들러 override로 빈 장바구니 상태 테스트 구현
eunwoo-levi Jun 3, 2025
250e53c
chore: cartItem type 파일 경로 이동
eunwoo-levi Jun 4, 2025
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
58 changes: 58 additions & 0 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
## 📦 장바구니 미션

이번 미션을 통해 다음과 같은 학습 경험들을 쌓는 것을 목표로 합니다.

### 1단계

- 클라이언트 상태를 효과적으로 모델링하고 관리할 수 있다.
- Jest, React Testing Library(RTL)를 활용하여 주요 기능에 대한 테스트를 작성할 수 있다.

## 🕵️ 셀프 리뷰(Self-Review)

### 제출 전 체크 리스트

- [ ] 기능 요구 사항을 모두 구현했고, 정상적으로 동작하는지 확인했나요?
- [ ] RTL 테스트 케이스를 모두 작성했나요?
- [ ] 배포한 데모 페이지에 정상적으로 접근할 수 있나요?

- 배포 링크 기입: **\_\_**

- [ ] 리뷰어가 장바구니 추가를 쉽게 할 수 있도록 Curl 명령어를 DM 으로 전달해주세요.

```
curl -X 'POST' \
'<url>' \
-H 'accept: */*' \
-H 'Authorization: Basic <토큰>' \
-H 'Content-Type: application/json' \
-d '{
"productId": 1,
"quantity": 1
}'
```

### 리뷰 요청 & 논의하고 싶은 내용

### 1) 상태 설계 의도

### 2) 이번 단계에서 가장 많이 고민했던 문제와 해결 과정에서 배운 점

### 3) 이번 리뷰를 통해 논의하고 싶은 부분

---

## ✅ 리뷰어 체크 포인트

<!-- 리뷰어가 이 PR을 검토할 때 중점적으로 확인할 사항입니다.
코드의 완성도뿐만 아니라, 리뷰이가 구현 과정에서 어떤 고민과 결정을 하며 학습했는지도 함께 고려해 주세요. -->

### 1. 클라이언트 상태관리

- 원본상태/파생상태를 적절히 구분하여 선언하였나요?
- React state 를 불필요하게 선언한 부분은 없나요?
- 상태 관리 로직의 책임이 적절히 응집/분리되었나요? (ex. reducer, hook, Context)

### 2. MSW/Test

- 주요 기능을 적절히 정의하였나요?
- 주요 기능/예외에 대한 테스트가 충분히 이루어졌나요?
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,5 @@ dist-ssr
*.njsproj
*.sln
*.sw?

.env*
19 changes: 18 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,18 @@
# react-shopping-cart
# 1단계.

react-shopping-cart

# 기능 요구 명세서

- [x] /cart-items API를 호출하여 장바구니 상품 데이터를 불러온다.
- [x]불러온 데이터를 기반으로 클라이언트 상태를 구성하고 관리한다.
- 개별 상품의 선택 여부, 결제 금액, 배송비 등의 상태를 관리한다.
- [x]상품 선택에 따른 결제 금액, 배송비 등의 동적인 변경 사항을 처리한다.
- 진입 시, 전체 선택 되어 있는 것이 디폴트이다.
- 상품 선택/해제 시 결제 금액을 동적으로 변경한다.
- 결제 금액이 10만원 이상일 경우 배송비는 무료이다.
- [x] 장바구니 상품의 수량을 변경할 수 있다.
- [x] 장바구니에 담긴 상품을 제거할 수 있다.

- [x] 장바구니에 상품이 없을 시 UI 보여줌
- [x] 주문을 완료하면 주문 확인 페이지로 라우팅
121 changes: 121 additions & 0 deletions __test__/cart.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import { MemoryRouter } from 'react-router';
import CartPage from '../src/pages/CartPage/CartPage';
import { CartProvider } from '../src/shared/context/CartProvider';
import { screen, render, within, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { describe, it } from 'vitest';
import { DELIVERY_FEE, DELIVERY_FEE_THRESHOLD } from '../src/features/cart/constants/orderPriceSummary';
import { server } from '../src/mocks/server';
import { http, HttpResponse } from 'msw';

const BASE_URL = import.meta.env.VITE_API_BASE_URL;

function renderCartPage() {
return render(
<MemoryRouter initialEntries={['/']}>
<CartProvider>
<CartPage />
</CartProvider>
</MemoryRouter>
);
}

describe('빈 장바구니 테스트', () => {
it('장바구니에 상품이 없으면 EmptyCartItemUI가 보인다', async () => {
server.use(
http.get(`${BASE_URL}/cart-items*`, () => {
return HttpResponse.json({ content: [] });
})
);

renderCartPage();

const emptyMessage = await screen.findByText('장바구니에 담은 상품이 없습니다.');
expect(emptyMessage).toBeInTheDocument();
});
});

describe('장바구니에 상품이 1개 이상일 때의 Vitest + RTL 테스트', () => {
beforeEach(() => {
renderCartPage();
});

it('장바구니가 1개 이상일 때 장바구니 페이지에 장바구니 카드들이 잘 렌더링된다.', async () => {
const cartItemCards = await screen.findAllByTestId('cart-item-card');
expect(cartItemCards.length).toBeGreaterThan(0);
});

it('장바구니에서 체크박스를 누르면 배송비를 고려하여 해당 금액이 반영된다.', async () => {

Choose a reason for hiding this comment

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

현재 해당테스트를 보니, 한 번의 테스트에서 너무 많은 것들을 검증하려고 하는 것 같아요. DOM 요소 존재 확인부터 시작해서 텍스트 파싱, 숫자 변환, 배송비 계산 로직, UI 상호작용, 그리고 최종 결과 검증까지 모든 게 하나의 테스트에 들어가 있더라고요.

이렇게 되면 테스트가 실패했을 때 정확히 어느 부분에서 문제가 발생했는지 파악하기 어려워질 수 있고, 나중에 비즈니스 로직이 바뀌거나 UI가 변경될 때 테스트 유지보수도 힘들어질 것 같습니다. 예를 들어 배송비 정책만 바뀌었는데 DOM 구조 파싱하는 부분까지 영향받을 수 있잖아요.

개인적으로는 이런 복합적인 기능은 단위별로 쪼개서 테스트하는 게 어떨까 싶어요. 체크박스 클릭 동작 자체는 따로 테스트하고, 배송비 계산 로직도 별도로 테스트하고, 그다음에 통합 플로우에서는 "사용자가 이런 행동을 했을 때 최종 결과가 이렇게 나온다"는 정도만 검증하는 식으로요. 그러면 각 테스트의 목적도 명확해지고, 실패했을 때 디버깅도 훨씬 수월할 것 같습니다.

Copy link
Author

Choose a reason for hiding this comment

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

테스트에 대한 피드백 감사합니다! 고려해서 전반적으로 테스트코드를 더 명료하게 리팩토링해보겠습니다!

Choose a reason for hiding this comment

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

요거는 아직 반영이 안된 것이죠?

Copy link
Author

@eunwoo-levi eunwoo-levi Jun 3, 2025

Choose a reason for hiding this comment

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

앗.. 제가 이전에 의도 파악을 잘못했었던 것 같습니다! 혹시 이런 의도가 맞을까요?
commit: dc85038 , bfad5e6

Choose a reason for hiding this comment

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

오 넵 expect문이 여러개가 되는 것은 문제가 되지 않습니다만 지금 상황에서는 너무많은 expect가 반복되어서 분리되면 좋을 것 같았습니다!

const user = userEvent.setup();

const cartItemCards = await screen.findAllByTestId('cart-item-card');
const allSelectCheckbox = screen.getByLabelText('전체 선택');

await user.click(allSelectCheckbox);

const firstCard = cartItemCards[0];
const checkbox = within(firstCard).getByRole('checkbox');

const priceElement = within(firstCard).getByTestId('card-item-price');
const expectedPriceText = priceElement.textContent?.trim() || '';

// 쉼표 제거 후 숫자 변환
const itemPrice = parseInt(expectedPriceText.replace(/,/g, ''), 10);

const deliveryFeeElement = screen.getByTestId('delivery-fee');
const expectedDeliveryFeeText = deliveryFeeElement.textContent?.trim() || '';

let expectedTotal = itemPrice;
if (itemPrice > DELIVERY_FEE_THRESHOLD) {
expect(expectedDeliveryFeeText).toBe('0원');
} else {
expect(expectedDeliveryFeeText).toContain('3,000원');
expectedTotal += DELIVERY_FEE;
}

const totalPurchasePrice = screen.getByTestId('total-purchase-price');

await user.click(checkbox);

await waitFor(() => {
expect(totalPurchasePrice.textContent).toContain(expectedTotal.toLocaleString() + '원');
});
});

it('장바구니에서 상품을 삭제하면 해당 상품이 화면에서 사라진다.', async () => {
const user = userEvent.setup();

const cartItemCards = await screen.findAllByTestId('cart-item-card');
const firstCard = cartItemCards[0];

const deleteButton = within(firstCard).getByRole('button', { name: '삭제' });

await user.click(deleteButton);

const updatedCartItemCards = await screen.queryAllByTestId('cart-item-card');

await waitFor(() => {
expect(updatedCartItemCards.length).toBe(cartItemCards.length - 1);
});
});

it('장바구니에서 + 버튼을 누르면 현재 수량보다 1 증가한다.', async () => {
const user = userEvent.setup();

const cartItemCards = await screen.findAllByTestId('cart-item-card');

const firstCard = cartItemCards[0];

const quantityPlusButton = within(firstCard).getByRole('button', { name: '+' });
const quantityElement = within(firstCard).getByTestId('card-item-quantity');

const initialQuantity = parseInt(quantityElement.textContent ?? '1', 10);

await user.click(quantityPlusButton);

await waitFor(() => {
const updatedQuantity = parseInt(quantityElement.textContent ?? '0', 10);
expect(updatedQuantity).toBe(initialQuantity + 1);
});
});
});
11 changes: 0 additions & 11 deletions jest.config.ts

This file was deleted.

1 change: 0 additions & 1 deletion jest.setup.ts

This file was deleted.

Loading