Skip to content

Conversation

@99mini
Copy link
Member

@99mini 99mini commented Oct 27, 2025

#️⃣연관된 이슈

📝작업 내용

스크린샷 (선택)

💬리뷰 요구사항(선택)

@99mini 99mini requested a review from Copilot October 27, 2025 13:33
@99mini 99mini self-assigned this Oct 27, 2025
@gemini-code-assist
Copy link

Summary of Changes

Hello @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

  • 장바구니 페이지 추가: 사용자가 선택한 상품을 확인하고 관리할 수 있는 새로운 장바구니 페이지와 관련 라우팅이 클라이언트 애플리케이션에 추가되었습니다.
  • 재사용 가능한 상품 카드 컴포넌트 도입: 상품 상세 페이지와 장바구니 페이지 모두에서 활용할 수 있는 ProductCard 컴포넌트가 새로 개발되어 UI 일관성과 코드 재사용성을 높였습니다.
  • 장바구니 API 연동 및 낙관적 업데이트: 장바구니에 상품을 추가, 업데이트, 삭제하는 API 연동 로직이 구현되었으며, 사용자 경험 향상을 위해 낙관적 업데이트(Optimistic Updates)가 적용되었습니다.
  • 개발 환경 설정 업데이트: 코드 포맷팅을 위한 Prettier의 printWidth 설정이 100에서 120으로 확장되었고, Claude Code를 위한 상세한 프로젝트 가이드 문서인 CLAUDE.md 파일이 추가되었습니다.
  • 코드 가독성 및 간결성 개선: 여러 파일에서 타입 정의, 함수 호출, 조건문 등의 코드 스타일이 더욱 간결하고 가독성 있게 리팩토링되었습니다.
Using Gemini Code Assist

The 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 /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

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 .gemini/ folder in the base of the repository. Detailed instructions can be found here.

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

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@99mini 99mini linked an issue Oct 27, 2025 that may be closed by this pull request
1 task
Copy link

Copilot AI left a 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 ProductCard component 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.

Comment on lines +18 to +25
const [checkedItems, setCheckedItems] = useState<Record<number, boolean>>(() => {
// 초기값: 모든 아이템 체크
const initial: Record<number, boolean> = {};
bucketData?.products.forEach((product) => {
initial[product.id] = true;
});
return initial;
});
Copy link

Copilot AI Oct 27, 2025

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.

Copilot uses AI. Check for mistakes.
};

export type BucketProductType = Omit<ProductType, "tags" | "productStatus" | "stock"> & {
export type BucketProductType = Omit<ProductType, "tags" | "productStatus"> & {
Copy link

Copilot AI Oct 27, 2025

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.

Suggested change
export type BucketProductType = Omit<ProductType, "tags" | "productStatus"> & {
export type BucketProductType = Omit<ProductType, "tags" | "productStatus" | "stock"> & {

Copilot uses AI. Check for mistakes.
Comment on lines +120 to +130
// 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 },
// },
// });
// }}
Copy link

Copilot AI Oct 27, 2025

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.

Suggested change
// 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 },
},
});
}}

Copilot uses AI. Check for mistakes.
Comment on lines +100 to +136
</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>
Copy link

Copilot AI Oct 27, 2025

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.

Suggested change
</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>

Copilot uses AI. Check for mistakes.
<span>{totalPrice.toLocaleString()}</span>
</div>
<div className="flex justify-between mb-2">
<span className="text-gray-600">할인 금액</span>
Copy link

Copilot AI Oct 27, 2025

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.

Suggested change
<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. */}

Copilot uses AI. Check for mistakes.
Comment on lines +85 to +89
postToApp({
type: "NATIVE_NAVIGATION",
payload: {
screen: "Checkout",
params: { totalPrice, totalCount },
Copy link

Copilot AI Oct 27, 2025

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.

Suggested change
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 },

Copilot uses AI. Check for mistakes.
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Copy link

@gemini-code-assist gemini-code-assist bot left a 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 컴포넌트의 레이아웃이 의도와 다르게 동작할 수 있는 문제가 있으며, 장바구니 페이지의 상태 관리 로직에도 버그가 존재합니다. 코드 제안을 포함하여 각 파일에 대한 자세한 리뷰를 남겼으니 확인 부탁드립니다.

Comment on lines +18 to +25
const [checkedItems, setCheckedItems] = useState<Record<number, boolean>>(() => {
// 초기값: 모든 아이템 체크
const initial: Record<number, boolean> = {};
bucketData?.products.forEach((product) => {
initial[product.id] = true;
});
return initial;
});

Choose a reason for hiding this comment

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

high

useState의 초기화 함수는 컴포넌트가 처음 렌더링될 때 한 번만 실행됩니다. useBucketList 훅에서 bucketData가 비동기적으로 로드되기 때문에, 초기 렌더링 시점에는 bucketDataundefined일 가능성이 높습니다. 이 경우 checkedItems 상태는 빈 객체 {}로 초기화되고, 나중에 bucketData가 로드되어도 상태가 업데이트되지 않아 모든 아이템이 체크되지 않는 문제가 발생합니다.

useEffect를 사용하여 bucketData가 변경될 때 checkedItems 상태를 동기화하는 방식으로 수정하는 것을 권장합니다. 새로운 상품이 추가되면 기본적으로 선택되도록 하고, 삭제된 상품은 상태에서 제거하여 데이터 정합성을 유지할 수 있습니다.

Suggested change
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]);

Comment on lines +72 to +81
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)));
};

Choose a reason for hiding this comment

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

medium

handleDeleteSelected 함수에서 Promise.allmutateAsync를 사용하여 여러 상품을 순차적으로 삭제하고 있습니다. 각 mutateAsync 호출은 개별적인 API 요청과 react-query의 상태 업데이트를 트리거하여, 여러 상품을 삭제할 때 UI가 여러 번 깜빡이거나 불필요한 리렌더링이 발생할 수 있습니다.

성능 향상과 더 나은 사용자 경험을 위해, 백엔드에 여러 상품 ID를 배열로 받아 한 번에 삭제하는 API 엔드포인트를 추가하고, 프론트엔드에서는 해당 API를 호출하는 단일 mutation을 사용하도록 리팩토링하는 것을 고려해보세요. 이것이 당장 어렵다면, 현재의 useDeleteFromBucket 훅을 수정하여 여러 ID를 한 번에 처리하고 낙관적 업데이트도 한 번만 수행하도록 개선할 수 있습니다.

@99mini
Copy link
Member Author

99mini commented Oct 27, 2025

/gemini review

Copy link

@gemini-code-assist gemini-code-assist bot left a 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.

Comment on lines +14 to +44
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,
},
],
};
}

Choose a reason for hiding this comment

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

high

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,
    //       },
    //     ],
    //   };
    // }

Comment on lines +120 to +130
// 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 },
// },
// });
// }}

Choose a reason for hiding this comment

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

high

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.

Suggested change
// 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` }}

Choose a reason for hiding this comment

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

medium

medium: Consider using template literals or a utility function to construct the style string for better readability and maintainability.

          style={{ paddingBottom: `${bottom + 16}px` }}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(web-view/client): 장바구니 페이지 웹뷰 전환

2 participants