-
Notifications
You must be signed in to change notification settings - Fork 0
feat: 장바구니 레이아웃 그리기 #4
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
base: feat/migration-web-view
Are you sure you want to change the base?
Conversation
Summary of ChangesHello @99mini, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! 이 PR은 클라이언트 애플리케이션에 장바구니 기능을 도입하는 것을 목표로 합니다. 사용자가 상품을 장바구니에 담고, 수량을 조절하며, 삭제하고, 최종적으로 구매를 진행할 수 있는 전반적인 사용자 경험을 제공합니다. 이를 위해 새로운 UI 컴포넌트와 페이지가 추가되었고, 백엔드 API와의 연동 및 사용자 인터랙션을 개선하기 위한 낙관적 업데이트 로직이 구현되었습니다. 또한, 개발 환경 설정을 최적화하고 코드 베이스의 가독성을 높이는 작업도 함께 진행되었습니다. Highlights
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
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.
Pull Request Overview
This PR implements the cart page layout and refactors product display logic into a reusable ProductCard component. The changes also include code formatting improvements (line breaks and indentation) and configuration updates.
Key Changes:
- Introduced a new cart page with item selection, quantity management, and checkout functionality
- Extracted product display logic into a shared
ProductCardcomponent supporting both detail and cart variants - Added optimistic updates for cart mutations to improve UX responsiveness
Reviewed Changes
Copilot reviewed 38 out of 39 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| apps/client/src/domain/cart/page/cart.page.tsx | New cart page with selection state management and checkout flow |
| apps/client/src/domain/cart/component/cart-item.tsx | Cart item wrapper component integrating ProductCard |
| apps/client/src/component/product/product-card.tsx | Reusable product card component supporting detail and cart variants |
| apps/client/src/domain/detail/component/product-list/product-item.tsx | Refactored to use ProductCard component |
| apps/client/src/api/buckets/query.ts | Added optimistic updates for cart operations |
| apps/client/src/api/buckets/client.ts | Added mock bucket data for development |
| apps/client/src/router/app-router.tsx | Added cart route and updated imports |
| .prettierrc | Updated printWidth from 100 to 120 |
| CLAUDE.md | Added project documentation for AI assistance |
| Multiple files | Formatting changes (line breaks consolidated) |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const [checkedItems, setCheckedItems] = useState<Record<number, boolean>>(() => { | ||
| // 초기값: 모든 아이템 체크 | ||
| const initial: Record<number, boolean> = {}; | ||
| bucketData?.products.forEach((product) => { | ||
| initial[product.id] = true; | ||
| }); | ||
| return initial; | ||
| }); |
Copilot
AI
Oct 27, 2025
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.
The initialization of checkedItems will only run once when the component mounts, but bucketData may be undefined at that time. When bucketData loads asynchronously, the state won't update to check the newly loaded items. Use a useEffect to sync checkedItems with bucketData.products when data loads.
| }; | ||
|
|
||
| export type BucketProductType = Omit<ProductType, "tags" | "productStatus" | "stock"> & { | ||
| export type BucketProductType = Omit<ProductType, "tags" | "productStatus"> & { |
Copilot
AI
Oct 27, 2025
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.
Removing 'stock' from the Omit in BucketProductType means the type now includes the stock field from ProductType. However, the mock data in apps/client/src/api/buckets/client.ts (line 39) and the usage in ProductCard (line 42) suggest that stock should be available. Verify whether stock is intentionally included or if this change introduces a type mismatch with the API response.
| export type BucketProductType = Omit<ProductType, "tags" | "productStatus"> & { | |
| export type BucketProductType = Omit<ProductType, "tags" | "productStatus" | "stock"> & { |
| // onClick={(e) => { | ||
| // e.preventDefault(); | ||
| // postToApp({ | ||
| // type: "NATIVE_NAVIGATION", | ||
| // payload: { | ||
| // screen: "CartRoot", | ||
| // params: { screen: "Cart" }, | ||
| // callbackState: { screen: "Detail", params: { screen: "MarketReview", params: { marketId: marketData.id } }, webUri: window.location.href }, | ||
| // }, | ||
| // }); | ||
| // }} |
Copilot
AI
Oct 27, 2025
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.
The cart navigation logic is commented out, leaving only an href that navigates to '/cart'. This means clicking the cart icon will perform a full page navigation instead of using the native navigation system. Either uncomment and fix the onClick handler or add a comment explaining why native navigation was removed.
| // onClick={(e) => { | |
| // e.preventDefault(); | |
| // postToApp({ | |
| // type: "NATIVE_NAVIGATION", | |
| // payload: { | |
| // screen: "CartRoot", | |
| // params: { screen: "Cart" }, | |
| // callbackState: { screen: "Detail", params: { screen: "MarketReview", params: { marketId: marketData.id } }, webUri: window.location.href }, | |
| // }, | |
| // }); | |
| // }} | |
| onClick={(e) => { | |
| e.preventDefault(); | |
| postToApp({ | |
| type: "NATIVE_NAVIGATION", | |
| payload: { | |
| screen: "CartRoot", | |
| params: { screen: "Cart" }, | |
| callbackState: { screen: "Detail", params: { screen: "MarketReview", params: { marketId: marketData.id } }, webUri: window.location.href }, | |
| }, | |
| }); | |
| }} |
| </div> | ||
| </div> | ||
| {/* 수량 수정 및 삭제 버튼 */} | ||
| <div className="flex flex-col justify-between items-end"> | ||
| {/* Counter */} | ||
| {onCountChange && ( | ||
| <div className="flex items-center gap-2 bg-gray-50 rounded px-2 py-1"> | ||
| <button | ||
| className={cn( | ||
| "w-6 h-6 flex items-center justify-center text-gray-600 transition-colors", | ||
| disabledCountDown ? "opacity-40 cursor-not-allowed" : "hover:text-gray-900", | ||
| )} | ||
| onClick={handleCountDown} | ||
| disabled={disabledCountDown} | ||
| > | ||
| <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | ||
| <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20 12H4" /> | ||
| </svg> | ||
| </button> | ||
| <span className="text-gray-900 text-sm font-medium min-w-[2rem] text-center"> | ||
| {stock > 0 ? count : "품절"} | ||
| </span> | ||
| <button | ||
| className={cn( | ||
| "w-6 h-6 flex items-center justify-center text-gray-600 transition-colors", | ||
| disabledCountUp ? "opacity-40 cursor-not-allowed" : "hover:text-gray-900", | ||
| )} | ||
| onClick={handleCountUp} | ||
| disabled={disabledCountUp} | ||
| > | ||
| <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | ||
| <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" /> | ||
| </svg> | ||
| </button> | ||
| </div> | ||
| )} | ||
| </div> |
Copilot
AI
Oct 27, 2025
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.
The counter controls (lines 103-136) are rendered outside the main product card container (which ends at line 101), but they appear visually disconnected and may not align properly with the product info. The counter should be positioned within the product card layout or use absolute positioning relative to the card container.
| </div> | |
| </div> | |
| {/* 수량 수정 및 삭제 버튼 */} | |
| <div className="flex flex-col justify-between items-end"> | |
| {/* Counter */} | |
| {onCountChange && ( | |
| <div className="flex items-center gap-2 bg-gray-50 rounded px-2 py-1"> | |
| <button | |
| className={cn( | |
| "w-6 h-6 flex items-center justify-center text-gray-600 transition-colors", | |
| disabledCountDown ? "opacity-40 cursor-not-allowed" : "hover:text-gray-900", | |
| )} | |
| onClick={handleCountDown} | |
| disabled={disabledCountDown} | |
| > | |
| <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20 12H4" /> | |
| </svg> | |
| </button> | |
| <span className="text-gray-900 text-sm font-medium min-w-[2rem] text-center"> | |
| {stock > 0 ? count : "품절"} | |
| </span> | |
| <button | |
| className={cn( | |
| "w-6 h-6 flex items-center justify-center text-gray-600 transition-colors", | |
| disabledCountUp ? "opacity-40 cursor-not-allowed" : "hover:text-gray-900", | |
| )} | |
| onClick={handleCountUp} | |
| disabled={disabledCountUp} | |
| > | |
| <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" /> | |
| </svg> | |
| </button> | |
| </div> | |
| )} | |
| </div> | |
| {/* 수량 수정 및 삭제 버튼 */} | |
| <div className="flex flex-col justify-between items-end mt-2"> | |
| {/* Counter */} | |
| {onCountChange && ( | |
| <div className="flex items-center gap-2 bg-gray-50 rounded px-2 py-1"> | |
| <button | |
| className={cn( | |
| "w-6 h-6 flex items-center justify-center text-gray-600 transition-colors", | |
| disabledCountDown ? "opacity-40 cursor-not-allowed" : "hover:text-gray-900", | |
| )} | |
| onClick={handleCountDown} | |
| disabled={disabledCountDown} | |
| > | |
| <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20 12H4" /> | |
| </svg> | |
| </button> | |
| <span className="text-gray-900 text-sm font-medium min-w-[2rem] text-center"> | |
| {stock > 0 ? count : "품절"} | |
| </span> | |
| <button | |
| className={cn( | |
| "w-6 h-6 flex items-center justify-center text-gray-600 transition-colors", | |
| disabledCountUp ? "opacity-40 cursor-not-allowed" : "hover:text-gray-900", | |
| )} | |
| onClick={handleCountUp} | |
| disabled={disabledCountUp} | |
| > | |
| <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" /> | |
| </svg> | |
| </button> | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| </div> |
| <span>{totalPrice.toLocaleString()}원</span> | ||
| </div> | ||
| <div className="flex justify-between mb-2"> | ||
| <span className="text-gray-600">할인 금액</span> |
Copilot
AI
Oct 27, 2025
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.
[nitpick] The discount amount is hardcoded to '0원'. If discounts will be implemented later, consider calculating this value from the data or adding a TODO comment to indicate this is a placeholder.
| <span className="text-gray-600">할인 금액</span> | |
| <span className="text-gray-600">할인 금액</span> | |
| {/* TODO: Replace hardcoded discount amount with dynamic calculation when discount logic is implemented. */} |
| postToApp({ | ||
| type: "NATIVE_NAVIGATION", | ||
| payload: { | ||
| screen: "Checkout", | ||
| params: { totalPrice, totalCount }, |
Copilot
AI
Oct 27, 2025
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.
The handleCheckout function navigates to a 'Checkout' screen but doesn't pass information about which items are selected. The native checkout screen will need the selected product IDs or full product details, not just totalPrice and totalCount. Pass the filtered products array in the params.
| postToApp({ | |
| type: "NATIVE_NAVIGATION", | |
| payload: { | |
| screen: "Checkout", | |
| params: { totalPrice, totalCount }, | |
| const selectedProducts = bucketData?.products | |
| ? bucketData.products.filter((product) => checkedItems[product.id]) | |
| : []; | |
| postToApp({ | |
| type: "NATIVE_NAVIGATION", | |
| payload: { | |
| screen: "Checkout", | |
| params: { totalPrice, totalCount, selectedProducts }, |
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
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.
Code Review
이번 PR은 장바구니 기능 구현과 관련된 레이아웃 및 로직 추가가 주요 내용입니다. 전반적으로 새로운 기능이 잘 구현되었지만, 몇 가지 중요한 수정이 필요해 보입니다. 특히, API 클라이언트의 에러 처리 로직에 도달할 수 없는 코드가 포함되어 있어 즉각적인 수정이 필요합니다. 또한, 새로 추가된 ProductCard 컴포넌트의 레이아웃이 의도와 다르게 동작할 수 있는 문제가 있으며, 장바구니 페이지의 상태 관리 로직에도 버그가 존재합니다. 코드 제안을 포함하여 각 파일에 대한 자세한 리뷰를 남겼으니 확인 부탁드립니다.
| const [checkedItems, setCheckedItems] = useState<Record<number, boolean>>(() => { | ||
| // 초기값: 모든 아이템 체크 | ||
| const initial: Record<number, boolean> = {}; | ||
| bucketData?.products.forEach((product) => { | ||
| initial[product.id] = true; | ||
| }); | ||
| return initial; | ||
| }); |
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.
useState의 초기화 함수는 컴포넌트가 처음 렌더링될 때 한 번만 실행됩니다. useBucketList 훅에서 bucketData가 비동기적으로 로드되기 때문에, 초기 렌더링 시점에는 bucketData가 undefined일 가능성이 높습니다. 이 경우 checkedItems 상태는 빈 객체 {}로 초기화되고, 나중에 bucketData가 로드되어도 상태가 업데이트되지 않아 모든 아이템이 체크되지 않는 문제가 발생합니다.
useEffect를 사용하여 bucketData가 변경될 때 checkedItems 상태를 동기화하는 방식으로 수정하는 것을 권장합니다. 새로운 상품이 추가되면 기본적으로 선택되도록 하고, 삭제된 상품은 상태에서 제거하여 데이터 정합성을 유지할 수 있습니다.
| const [checkedItems, setCheckedItems] = useState<Record<number, boolean>>(() => { | |
| // 초기값: 모든 아이템 체크 | |
| const initial: Record<number, boolean> = {}; | |
| bucketData?.products.forEach((product) => { | |
| initial[product.id] = true; | |
| }); | |
| return initial; | |
| }); | |
| const [checkedItems, setCheckedItems] = useState<Record<number, boolean>>({}); | |
| useEffect(() => { | |
| if (bucketData?.products) { | |
| setCheckedItems((prev) => { | |
| const newCheckedState: Record<number, boolean> = {}; | |
| for (const product of bucketData.products) { | |
| // 기존 상태를 유지하거나, 새로운 아이템이면 true로 초기화 | |
| newCheckedState[product.id] = prev[product.id] ?? true; | |
| } | |
| return newCheckedState; | |
| }); | |
| } | |
| }, [bucketData?.products]); |
| const handleDeleteSelected = async () => { | ||
| if (!bucketData?.products) return; | ||
|
|
||
| const selectedIds = bucketData.products | ||
| .filter((product) => checkedItems[product.id]) | ||
| .map((product) => product.id); | ||
|
|
||
| // 선택된 아이템들 삭제 | ||
| await Promise.all(selectedIds.map((id) => deleteFromBucket.mutateAsync(id))); | ||
| }; |
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.
handleDeleteSelected 함수에서 Promise.all과 mutateAsync를 사용하여 여러 상품을 순차적으로 삭제하고 있습니다. 각 mutateAsync 호출은 개별적인 API 요청과 react-query의 상태 업데이트를 트리거하여, 여러 상품을 삭제할 때 UI가 여러 번 깜빡이거나 불필요한 리렌더링이 발생할 수 있습니다.
성능 향상과 더 나은 사용자 경험을 위해, 백엔드에 여러 상품 ID를 배열로 받아 한 번에 삭제하는 API 엔드포인트를 추가하고, 프론트엔드에서는 해당 API를 호출하는 단일 mutation을 사용하도록 리팩토링하는 것을 고려해보세요. 이것이 당장 어렵다면, 현재의 useDeleteFromBucket 훅을 수정하여 여러 ID를 한 번에 처리하고 낙관적 업데이트도 한 번만 수행하도록 개선할 수 있습니다.
|
/gemini review |
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.
Code Review
This pull request introduces a cart feature to the Ummgoban web application, including layout, component integration, and API interaction. The changes include modifications to Prettier configuration, documentation updates, API client enhancements, and the addition of new components and pages related to the cart functionality. The code incorporates React Query for data fetching and state management, and utilizes a custom message passing system for communication with a React Native mobile app.
| if (process.env.NODE_ENV === "development") { | ||
| return { | ||
| // MOCK: MOCK 데이터 | ||
| market: { | ||
| id: 0, | ||
| name: "맛있는 제육 마켓", | ||
| images: [], | ||
| closeAt: "", | ||
| openAt: "", | ||
| }, | ||
| products: [ | ||
| { | ||
| count: 1, | ||
| id: 1, | ||
| name: "맛있는 제육볶음", | ||
| image: "https://capstone-dev-s3-bucket.s3.ap-northeast-2.amazonaws.com/Market/1/marketProduct/4a40d443", | ||
| originPrice: 1000, | ||
| discountPrice: 900, | ||
| discountRate: 10, | ||
| tags: [ | ||
| { | ||
| id: 1, | ||
| tagName: "매운맛", | ||
| }, | ||
| ], | ||
| productStatus: "IN_STOCK", | ||
| stock: 5, | ||
| }, | ||
| ], | ||
| }; | ||
| } |
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.
high: Mock data should not be included in production code. This block should be removed before merging to ensure that real API data is used in production environments.
// if (process.env.NODE_ENV === "development") {
// return {
// // MOCK: MOCK 데이터
// market: {
// id: 0,
// name: "맛있는 제육 마켓",
// images: [],
// closeAt: "",
// openAt: "",
// },
// products: [
// {
// count: 1,
// id: 1,
// name: "맛있는 제육볶음",
// image: "https://capstone-dev-s3-bucket.s3.ap-northeast-2.amazonaws.com/Market/1/marketProduct/4a40d443",
// originPrice: 1000,
// discountPrice: 900,
// discountRate: 10,
// tags: [
// {
// id: 1,
// tagName: "매운맛",
// },
// ],
// productStatus: "IN_STOCK",
// stock: 5,
// },
// ],
// };
// }| // onClick={(e) => { | ||
| // e.preventDefault(); | ||
| // postToApp({ | ||
| // type: "NATIVE_NAVIGATION", | ||
| // payload: { | ||
| // screen: "CartRoot", | ||
| // params: { screen: "Cart" }, | ||
| // callbackState: { screen: "Detail", params: { screen: "MarketReview", params: { marketId: marketData.id } }, webUri: window.location.href }, | ||
| // }, | ||
| // }); | ||
| // }} |
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.
high: This commented-out code block appears to be related to navigation. If it's no longer needed, it should be removed to avoid confusion and reduce code clutter. If it's intended for future use, consider adding a clear TODO comment explaining its purpose and when it should be re-enabled.
| // onClick={(e) => { | |
| // e.preventDefault(); | |
| // postToApp({ | |
| // type: "NATIVE_NAVIGATION", | |
| // payload: { | |
| // screen: "CartRoot", | |
| // params: { screen: "Cart" }, | |
| // callbackState: { screen: "Detail", params: { screen: "MarketReview", params: { marketId: marketData.id } }, webUri: window.location.href }, | |
| // }, | |
| // }); | |
| // }} | |
| // Remove the commented-out code block if it's no longer needed. | |
| {/* 하단 버튼 */} | ||
| <div | ||
| className="p-4 bg-white border-t border-gray-200 flex justify-center" | ||
| style={{ paddingBottom: `${bottom + 16}px` }} |
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.
#️⃣연관된 이슈
📝작업 내용
스크린샷 (선택)
💬리뷰 요구사항(선택)