-
Notifications
You must be signed in to change notification settings - Fork 204
[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
Changes from all commits
Commits
Show all changes
67 commits
Select commit
Hold shift + click to select a range
5484192
chore: 의존성 제거
woowa-euijinkk 627d584
chore: Emotion 라이브러리 설치
eunwoo-levi 03a62f8
docs: README 기능 요구 명세서 작성
eunwoo-levi dc5d83a
feat: Navbar 컴포넌트 구현
eunwoo-levi ac026b9
styles: 전역 css 초기화
eunwoo-levi ec2792f
feat: Cart 메인 페이지에서 큰 레이아웃 구현
eunwoo-levi c317e07
chore: emotion tsconfig 전역 설정
eunwoo-levi 3790903
feat: Custom Button 컴포넌트 구현
eunwoo-levi fefdc4f
feat: Footer 컴포넌트 구현 및 Button컴포넌트 적용
eunwoo-levi e4644b1
feat: App 컴포넌트에 Footer 및 AppContent 적용
eunwoo-levi 45276a5
feat: CartHeader 컴포넌트 구현 및 적용
eunwoo-levi 600b695
feat: Cart feature endpoint 추가
eunwoo-levi eaa9d32
chore: 파일명 변경
eunwoo-levi aeb9f0e
feat: CartItemCard 컴포넌트 구현
eunwoo-levi 3881eea
feat: CartItemQuantitySelector 컴포넌트 구현
eunwoo-levi d40f32d
feat: CartList 컴포넌트 구현
eunwoo-levi 727f487
styles: Button 컴포넌트 style 구현
eunwoo-levi 0e324e3
feat: SelectInput 컴포넌트 구현
eunwoo-levi 0dd8cb8
feat: CartList컴포넌트를 App에 적용 및 endpoint 추가
eunwoo-levi d22f6ec
feat: 가격 정보 OrderPriceSummary 컴포넌트 구현
eunwoo-levi 12c5e91
feat: Shared 디렉토리에 Cart타입 정의
eunwoo-levi 3d4049f
feat: SelectedCart 정보를 가진 context 구현
eunwoo-levi e4a9310
feat: httpClient 유틸 함수 구현
eunwoo-levi 9e20a79
feat: 백엔드로부터 CartItem 들고오는 GET API 로직 구현
eunwoo-levi 73613f1
feat: context를 통하여 CartItemCard의 input box를 통한 가격 계산 로직 구현
eunwoo-levi 9e0e819
feat: 삭제 버튼 눌렀을 때 cartItem에 대해 DELETE API 호출 및 context에서 제거 로직 구현
eunwoo-levi bff7e83
feat: - 또는 +버튼을 눌렀을 때 CartItem 수량 업데이트 로직 구현
eunwoo-levi 82437b5
docs: 기능 요구 명세서 업데이트
eunwoo-levi 3ed91e8
feat: 장바구니 상품 없을 시 UI 보여주는 컴포넌트 및 로직 구현
eunwoo-levi efecc48
refactor: 상품 없을 시 헤더 수정
eunwoo-levi bd2e97e
feat: createBrowserRouter 사용하여 routes 구현
eunwoo-levi e7a0c92
feat: 모든 페이지를 위한 Layout 컴포넌트 구현
eunwoo-levi bf2c469
feat: Error Page 구현
eunwoo-levi 1d2d200
chore: CartPageFooter 파일 경로 이동
eunwoo-levi 53330f1
refactor: Navbar 컴포넌트에 handleClick 매개변수 전달
eunwoo-levi 51b6039
feat: main.tsx에 RouterProvider 적용
eunwoo-levi b70b634
feat: ConfirmationPage 컴포넌트 적용
eunwoo-levi 19a869b
feat: 경로 URL 상수화 정의
eunwoo-levi aaaebdd
chore: vitest 설치 및 세팅
eunwoo-levi 34403fd
fix: vitest setup 에러 해결
eunwoo-levi 7996e35
chore: msw 설치 및 환경 설정
eunwoo-levi 402743d
docs: PR template
woowa-euijinkk 2a70c2c
chore: msw 설정 수정 및 mock data 추가
eunwoo-levi f365138
refactor: data-testid 추가
eunwoo-levi 8049a7f
test: 장바구니 테스트코드 추가
eunwoo-levi 8bdfedd
docs: README 기능 요구 명세서 업데이트
eunwoo-levi 2519c0e
fix: router에 의해 base URL 설정 수정
eunwoo-levi 1f4d75e
feat: Lazy import 및 suspense 적용하여 Skeleton UI 적용
eunwoo-levi 285e669
refactor: Navbar 올바른 로직으로 리팩토링
eunwoo-levi 6815a04
chore: types 디렉토리명 변경
eunwoo-levi 9f8e564
chore: 함수명이랑 파일명이 동일하도록 파일명 변경
eunwoo-levi b10222f
refactor: 개별 input box가 모두 선택 시 전체전첵 input box 가 checked 되도록 하여 UX 개선
eunwoo-levi 9cca2c8
refactor: some 메소드를 사용함으로써 boolean값을 return으로 받아 코드 가독성 측면에서 개선
eunwoo-levi 7230cca
refactor: SelectInput 컴포넌트에 Type 명시
eunwoo-levi 4805823
feat: OrderPriceSummary 매직넘버 상수화
eunwoo-levi 5e04047
test: 빈 배열을 반환하는 새로운 App 컴포넌트 구현 및 빈 장바구니 테스트에 적용
eunwoo-levi aa138c2
chore: router에서 basename 제거
eunwoo-levi 618a98b
chore: App 네이밍을 CartPage로 바꿈으로써 명료하게 개선
eunwoo-levi 4c18d5a
refactor: 카드 페이지 처음 진입때 전체선택 자동화 로죅 추가
eunwoo-levi 3ed2fec
test: 매직넘버 상수화
eunwoo-levi fdc0e79
refactor: cartItems도 context 통하여 전역 상태 관리로 리팩토링
eunwoo-levi e787b52
chore: 불필요한 라이브러리 제거
eunwoo-levi ca2e72a
refactor: CartItemQuantitySelector 컴포넌트에서 불필요한 state제거 후 효율적으로 리팩토링
eunwoo-levi dc85038
test: UI 테스트 로직과 비즈니스 로직 분리
eunwoo-levi bfad5e6
test: UI 테스트 로직과 비즈니스 로직 분리
eunwoo-levi 174dabf
test: MSW 핸들러 override로 빈 장바구니 상태 테스트 구현
eunwoo-levi 250e53c
chore: cartItem type 파일 경로 이동
eunwoo-levi File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
|
||
- 주요 기능을 적절히 정의하였나요? | ||
- 주요 기능/예외에 대한 테스트가 충분히 이루어졌나요? |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -22,3 +22,5 @@ dist-ssr | |
*.njsproj | ||
*.sln | ||
*.sw? | ||
|
||
.env* |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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] 주문을 완료하면 주문 확인 페이지로 라우팅 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 () => { | ||
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); | ||
}); | ||
}); | ||
}); |
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
현재 해당테스트를 보니, 한 번의 테스트에서 너무 많은 것들을 검증하려고 하는 것 같아요. DOM 요소 존재 확인부터 시작해서 텍스트 파싱, 숫자 변환, 배송비 계산 로직, UI 상호작용, 그리고 최종 결과 검증까지 모든 게 하나의 테스트에 들어가 있더라고요.
이렇게 되면 테스트가 실패했을 때 정확히 어느 부분에서 문제가 발생했는지 파악하기 어려워질 수 있고, 나중에 비즈니스 로직이 바뀌거나 UI가 변경될 때 테스트 유지보수도 힘들어질 것 같습니다. 예를 들어 배송비 정책만 바뀌었는데 DOM 구조 파싱하는 부분까지 영향받을 수 있잖아요.
개인적으로는 이런 복합적인 기능은 단위별로 쪼개서 테스트하는 게 어떨까 싶어요. 체크박스 클릭 동작 자체는 따로 테스트하고, 배송비 계산 로직도 별도로 테스트하고, 그다음에 통합 플로우에서는 "사용자가 이런 행동을 했을 때 최종 결과가 이렇게 나온다"는 정도만 검증하는 식으로요. 그러면 각 테스트의 목적도 명확해지고, 실패했을 때 디버깅도 훨씬 수월할 것 같습니다.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
테스트에 대한 피드백 감사합니다! 고려해서 전반적으로 테스트코드를 더 명료하게 리팩토링해보겠습니다!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
요거는 아직 반영이 안된 것이죠?
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
앗.. 제가 이전에 의도 파악을 잘못했었던 것 같습니다! 혹시 이런 의도가 맞을까요?
commit: dc85038 , bfad5e6
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
오 넵 expect문이 여러개가 되는 것은 문제가 되지 않습니다만 지금 상황에서는 너무많은 expect가 반복되어서 분리되면 좋을 것 같았습니다!