ewhk9887/korean_code_reviews_from_github huggingface dataset:
@@ -1,7 +1,27 @@
-export default InputView = {
-    async readDate() {
-        const input = await Console.readLineAsync("12월 중 식당 예상 방문 날짜는 언제인가요? (숫자만 입력해 주세요!)");
-        // ...
-    }
-    // ...
-}
+import { MissionUtils } from "@woowacourse/mission-utils";
+import ChristmasDomain from "./ChristmasPromotion.js";
+import Order from "./Order.js";
+
+export const christmasInstance = new ChristmasDomain();
+
+const InputView = {
+  async readDate() {
+    const input = await MissionUtils.Console.readLineAsync(
+      "12월 중 식당 예상 방문 날짜는 언제인가요? (숫자만 입력해 주세요!)\n"
+    );
+    const DATE = parseInt(input, 10);
+    christmasInstance.startDateValidate(DATE);
+  },
+
+  async readOrderMenu() {
+    const input = await MissionUtils.Console.readLineAsync(
+      "주문하실 메뉴를 메뉴와 개수를 알려 주세요. (e.g. 해산물파스타-2,레드와인-1,초코케이크-1)\n"
+    );
+    new Order(input);
+    const MENUES = input.split(",");
+    christmasInstance.startMenuValidate(MENUES);
+  },
+  // ...
+};
+
+export default InputView;

리뷰: 네 변하지 않는 것은 상수로 바꿔야 좋네요. 감사합니다!
@@ -0,0 +1,24 @@
+import menuAndQuantity from "./utils/menuAndQuantity.js";
+
+class Order {
+  #order;
+
+  constructor(order) {
+    this.#order = Array.isArray(order) ? order : [order];
+    this.#orderQuantityValidate();
+  }
+
+  #orderQuantityValidate() {
+    this.#order.forEach((menu) => {
+      const { QUANTITY } = menuAndQuantity(menu);
+
+      if (QUANTITY > 20) {
+        throw new Error(
+          `[ERROR] 메뉴는 한 번에 최대 20개까지만 주문할 수 있습니다.`
+        );
+      }
+    });
+  }
+}
+
+export default Order;

리뷰: ```js
export default function menuAndQuantity(menu) {
  const [MENU_NAME, NUMBER] = menu.split("-");
  const QUANTITY = parseInt(NUMBER, 10);
  return { MENU_NAME, QUANTITY };
}
```
**이 `menuAndQuantity(menu)` 함수를 사용해서 수량 파악**했습니다.
그러기 위해서는 menu (menu-quantity)가 하나씩 전달해야 해서 `forEach()`를 사용했습니다.
@@ -1,7 +1,26 @@
 package store;
 
+
+import store.controller.FrontController;
+import store.controller.ProductController;
+import store.controller.PromotionController;
+import store.view.InputView;
+import store.view.OutputView;
+
+
 public class Application {
+
+
     public static void main(String[] args) {
-        // TODO: 프로그램 구현
+        InputView inputView = new InputView();
+        OutputView outputView = new OutputView();
+        ProductController productController = new ProductController();
+        PromotionController promotionController = new PromotionController(inputView, productController);
+
+        FrontController frontController = new FrontController(inputView, outputView, productController,
+                promotionController);
+        frontController.runFrontController();
+
     }
+
 }

리뷰: 클래스명으로 의미를 알 수 있으므로 메소드명에 표현안해도 될 거 같습니다
@@ -0,0 +1,10 @@
+일관성을 유지하기 위한 규칙
+
+### recoil
+
+1. 리렌더를 촉발하는 데이터에 해당하는 경우 이름에 State를 붙인다.
+   1. atom은 무조건 State를 붙인다.
+   2. selector는 내부적으로 하나 이상의 atom을 이용하는 경우 State를 붙인다.
+2. selector의 용도는 세 가지이다.
+   1. atom 파생 상태
+   2. 비동기 데이터 패칭

리뷰: 규칙까지 디테일.. 👏
@@ -1,10 +1,24 @@
 import "./App.css";
+import { RecoilRoot } from "recoil";
+import { BrowserRouter, Route, Routes } from "react-router-dom";
+
+import CartPage from "./pages/CartPage";
+import { PATH } from "./constants/path";
+import OrderSummaryPage from "./pages/OrderSummaryPage";
+import { ErrorBoundary } from "react-error-boundary";
 
 function App() {
   return (
-    <>
-      <h1>react-shopping-cart</h1>
-    </>
+    <BrowserRouter>
+      <RecoilRoot>
+        <ErrorBoundary fallbackRender={({ error }) => error.message}>
+          <Routes>
+            <Route path={PATH.cart} element={<CartPage />} />
+            <Route path={PATH.orderSummary} element={<OrderSummaryPage />} />
+          </Routes>
+        </ErrorBoundary>
+      </RecoilRoot>
+    </BrowserRouter>
   );
 }
 

리뷰: [crerateBrowserRouter](https://reactrouter.com/en/main/routers/create-browser-router)도 추후에 한 번 써보시는거 추천드립니다 :) 🐫
@@ -0,0 +1,31 @@
+import styled from "styled-components";
+
+export interface IButtonProps extends React.HTMLAttributes<HTMLButtonElement> {
+  disabled?: boolean;
+}
+
+export default function Button({ children, ...attributes }: IButtonProps) {
+  return <S.Button {...attributes}>{children}</S.Button>;
+}
+
+const S = {
+  Button: styled.button`
+    position: fixed;
+    bottom: 0;
+    max-width: 429px;
+
+    height: 64px;
+    font-size: 16px;
+    font-weight: 700;
+    text-align: center;
+    width: 100%;
+
+    background-color: black;
+    color: white;
+    border: none;
+    cursor: pointer;
+    &:disabled {
+      background-color: rgba(190, 190, 190, 1);
+    }
+  `,
+};

리뷰: 스타일을 위한 버튼 컴포넌트로 확인되는데, 이를 폴더로 구조화시키면 버튼의 역할을 파악하는데 더욱 좋을 것 같아요! 🐫
@@ -0,0 +1,91 @@
+import { useRecoilValue } from "recoil";
+import styled from "styled-components";
+import { deliveryCostState, orderAmountState, totalOrderAmountState } from "../recoil/cartAmount";
+import { ReactComponent as InfoIcon } from "../assets/info-icon.svg";
+
+export interface ICartAmountProps {}
+
+export default function CartAmount() {
+  const orderAmount = useRecoilValue(orderAmountState);
+  const deliveryCost = useRecoilValue(deliveryCostState);
+  const totalOrderAmount = useRecoilValue(totalOrderAmountState);
+
+  return (
+    <S.CartAmountContainer>
+      <S.CartAmountNoti>
+        <S.InfoIcon />
+        <S.CartAmountNotiText>
+          총 주문 금액이 100,000원 이상일 경우 무료 배송됩니다.
+        </S.CartAmountNotiText>
+      </S.CartAmountNoti>
+
+      <S.UpperCartAmountInfoWrapper>
+        <S.CartAmountInfo>
+          <S.AmountText>주문 금액</S.AmountText>{" "}
+          <S.Amount>{orderAmount.toLocaleString()}원</S.Amount>
+        </S.CartAmountInfo>
+        <S.CartAmountInfo>
+          <S.AmountText>배송비</S.AmountText> <S.Amount>{deliveryCost.toLocaleString()}원</S.Amount>
+        </S.CartAmountInfo>
+      </S.UpperCartAmountInfoWrapper>
+      <S.LowerCartAmountInfoWrapper>
+        <S.CartAmountInfo>
+          <S.AmountText>총 주문 금액</S.AmountText>{" "}
+          <S.Amount>{totalOrderAmount.toLocaleString()}원</S.Amount>
+        </S.CartAmountInfo>
+      </S.LowerCartAmountInfoWrapper>
+    </S.CartAmountContainer>
+  );
+}
+
+const S = {
+  CartAmountContainer: styled.div`
+    display: flex;
+    flex-direction: column;
+    gap: 12px;
+  `,
+
+  CartAmountNoti: styled.p`
+    font-size: 14px;
+    color: #888;
+    margin-bottom: 20px;
+    display: flex;
+    align-items: center;
+    gap: 4px;
+  `,
+
+  UpperCartAmountInfoWrapper: styled.div`
+    border-top: 1px solid rgba(0, 0, 0, 0.1);
+  `,
+
+  LowerCartAmountInfoWrapper: styled.div`
+    border-top: 1px solid rgba(0, 0, 0, 0.1);
+  `,
+
+  CartAmountInfo: styled.div`
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    padding-top: 12px;
+  `,
+
+  AmountText: styled.span`
+    font-size: 16px;
+    font-weight: 700;
+  `,
+
+  Amount: styled.span`
+    font-size: 24px;
+    font-weight: 700;
+  `,
+  InfoIcon: styled(InfoIcon)`
+    width: 16px;
+    height: 16px;
+  `,
+  CartAmountNotiText: styled.span`
+    font-size: 12px;
+    font-weight: 500;
+    line-height: 15px;
+    color: rgba(10, 13, 19, 1);
+  `,
+};

리뷰: 사용되지 않는 Props인가요?! 👀
@@ -0,0 +1,82 @@
+import { useSetRecoilState } from "recoil";
+import CartItemView from "./CartItemView";
+import styled from "styled-components";
+import { selectedCartItemIdsState } from "../recoil/selectedCartItemIds";
+import { CartItem } from "../types/cartItems";
+import { useCartItemControl } from "../hooks/useCartItemControl";
+
+export interface ICartItemList {
+  cartItems: CartItem[];
+}
+
+export default function CartItemList({ cartItems }: ICartItemList) {
+  const cartItemControl = useCartItemControl();
+  const setSelectedCartItemIds = useSetRecoilState(selectedCartItemIdsState);
+  const isAllSelected = cartItems.every(({ isSelected }) => isSelected);
+
+  const handleSelectAllChange = () => {
+    if (isAllSelected) {
+      setSelectedCartItemIds([]);
+    } else {
+      setSelectedCartItemIds(cartItems.map(({ id }) => id));
+    }
+  };
+
+  return (
+    <S.CartItemListContainer>
+      <S.SelectAll>
+        <S.Checkbox
+          id="select-all-checkbox"
+          type="checkbox"
+          checked={isAllSelected}
+          onChange={handleSelectAllChange}
+        />
+        <S.SelectAllLabel htmlFor="select-all-checkbox">전체선택</S.SelectAllLabel>
+      </S.SelectAll>
+      <S.CartItemList>
+        {cartItems.map((cartItem) => (
+          <CartItemView
+            key={cartItem.product.id}
+            cartItem={cartItem}
+            cartItemControl={cartItemControl}
+          />
+        ))}
+      </S.CartItemList>
+    </S.CartItemListContainer>
+  );
+}
+
+const S = {
+  CartItemListContainer: styled.div`
+    display: flex;
+    flex-direction: column;
+    gap: 20px;
+
+    margin: 36px 0 52px 0;
+  `,
+
+  CartItemList: styled.div`
+    display: flex;
+    flex-direction: column;
+    gap: 20px;
+  `,
+
+  SelectAll: styled.div`
+    display: flex;
+    align-items: center;
+    gap: 8px;
+  `,
+
+  Checkbox: styled.input`
+    accent-color: black;
+    margin: 0;
+    width: 24px;
+    height: 24px;
+  `,
+
+  SelectAllLabel: styled.label`
+    font-size: 12px;
+    font-weight: 500;
+    line-height: 15px;
+  `,
+};

리뷰: (단순 궁금) cartItem.id가 아니라 product.id로 설정해주신 이유가 있을까요?
@@ -0,0 +1,91 @@
+import { RecoilRoot, useRecoilState } from "recoil";
+
+import { act } from "react";
+import { renderHook } from "@testing-library/react";
+import { updateCartItemQuantity } from "../api/cartItems";
+import { useCartItemControl } from "./useCartItemControl";
+
+jest.mock("recoil", () => ({
+  ...jest.requireActual("recoil"),
+  useRecoilState: jest.fn(),
+}));
+jest.mock("../api/cartItems");
+jest.mock("../utils/sessionStorage");
+
+describe("useCartItemControl", () => {
+  // describe("remove", () => {
+  //   const mockCartItems = [{ id: 1 }, { id: 2 }, { id: 3 }];
+  //   const mockRemovingCartItemId = 1;
+  //   const mockRemovedCartItems = [{ id: 2 }, { id: 3 }];
+
+  //   it("1. delete api 요청 2. 상태 변경", async () => {
+  //     const mockDeleteCartItemRequest = removeCartItem as jest.Mock;
+  //     const setRawCartItems = jest.fn();
+
+  //     (useRecoilState as jest.Mock).mockImplementation(() => [mockCartItems, setRawCartItems]);
+
+  //     const { result } = renderHook(() => useCartItemControl(), {
+  //       wrapper: RecoilRoot,
+  //     });
+
+  //     await act(async () => result.current.remove(mockRemovingCartItemId));
+
+  //     expect(mockDeleteCartItemRequest).toHaveBeenCalledWith(mockRemovingCartItemId);
+  //     expect(setRawCartItems).toHaveBeenCalledWith(mockRemovedCartItems);
+  //   });
+  // });
+
+  describe("updateQuantity", () => {
+    const mockUpdatingCartItemId = 1;
+    const updatingQuantity = 2;
+    const mockCartItems = [{ id: 1, quantity: 1 }];
+    it("1. patch api 요청 2. 상태 변경", async () => {
+      const mockUpdateCartItemQuantity = updateCartItemQuantity as jest.Mock;
+      const setRawCartItems = jest.fn();
+
+      (useRecoilState as jest.Mock).mockImplementation(() => [
+        mockCartItems,
+        setRawCartItems,
+      ]);
+
+      const { result } = renderHook(() => useCartItemControl(), {
+        wrapper: RecoilRoot,
+      });
+
+      await act(async () =>
+        result.current.updateQuantity(mockUpdatingCartItemId, updatingQuantity)
+      );
+
+      expect(mockUpdateCartItemQuantity).toHaveBeenCalledWith(
+        mockUpdatingCartItemId,
+        updatingQuantity
+      );
+      expect(setRawCartItems).toHaveBeenCalledWith([{ id: 1, quantity: 2 }]);
+    });
+  });
+
+  /**
+   * TODO: 상태 변경에 대한 테스트 케이스 작성 요망 (모킹한 setSelectedCartItemIds의 내부 callback에서 문제 발생)
+  describe("toggleSelection", () => {
+    const mockCartItemId = 1;
+    const mockSelectedCartItemIds = [1, 2];
+    const mockPutInSelectedCartItemIds = putInSelectedCartItemIds as jest.Mock;
+    const mockSetSelectedCartItemIds = jest.fn();
+
+    it("1. 스토리지 동기화 2. 상태 변경", () => {
+      (useRecoilState as jest.Mock).mockImplementation(() => [
+        mockSelectedCartItemIds,
+        mockSetSelectedCartItemIds,
+      ]);
+
+      const { result } = renderHook(() => useCartItemControl(), {
+        wrapper: RecoilRoot,
+      });
+
+      act(() => result.current.toggleSelection(mockCartItemId));
+
+      expect(mockPutInSelectedCartItemIds).toHaveBeenCalledWith([2]);
+    });
+  });
+   */
+});

리뷰: 여기 주석은 일부러 남겨두신걸까요?! 🐫
@@ -0,0 +1,91 @@
+import { RecoilRoot, useRecoilState } from "recoil";
+
+import { act } from "react";
+import { renderHook } from "@testing-library/react";
+import { updateCartItemQuantity } from "../api/cartItems";
+import { useCartItemControl } from "./useCartItemControl";
+
+jest.mock("recoil", () => ({
+  ...jest.requireActual("recoil"),
+  useRecoilState: jest.fn(),
+}));
+jest.mock("../api/cartItems");
+jest.mock("../utils/sessionStorage");
+
+describe("useCartItemControl", () => {
+  // describe("remove", () => {
+  //   const mockCartItems = [{ id: 1 }, { id: 2 }, { id: 3 }];
+  //   const mockRemovingCartItemId = 1;
+  //   const mockRemovedCartItems = [{ id: 2 }, { id: 3 }];
+
+  //   it("1. delete api 요청 2. 상태 변경", async () => {
+  //     const mockDeleteCartItemRequest = removeCartItem as jest.Mock;
+  //     const setRawCartItems = jest.fn();
+
+  //     (useRecoilState as jest.Mock).mockImplementation(() => [mockCartItems, setRawCartItems]);
+
+  //     const { result } = renderHook(() => useCartItemControl(), {
+  //       wrapper: RecoilRoot,
+  //     });
+
+  //     await act(async () => result.current.remove(mockRemovingCartItemId));
+
+  //     expect(mockDeleteCartItemRequest).toHaveBeenCalledWith(mockRemovingCartItemId);
+  //     expect(setRawCartItems).toHaveBeenCalledWith(mockRemovedCartItems);
+  //   });
+  // });
+
+  describe("updateQuantity", () => {
+    const mockUpdatingCartItemId = 1;
+    const updatingQuantity = 2;
+    const mockCartItems = [{ id: 1, quantity: 1 }];
+    it("1. patch api 요청 2. 상태 변경", async () => {
+      const mockUpdateCartItemQuantity = updateCartItemQuantity as jest.Mock;
+      const setRawCartItems = jest.fn();
+
+      (useRecoilState as jest.Mock).mockImplementation(() => [
+        mockCartItems,
+        setRawCartItems,
+      ]);
+
+      const { result } = renderHook(() => useCartItemControl(), {
+        wrapper: RecoilRoot,
+      });
+
+      await act(async () =>
+        result.current.updateQuantity(mockUpdatingCartItemId, updatingQuantity)
+      );
+
+      expect(mockUpdateCartItemQuantity).toHaveBeenCalledWith(
+        mockUpdatingCartItemId,
+        updatingQuantity
+      );
+      expect(setRawCartItems).toHaveBeenCalledWith([{ id: 1, quantity: 2 }]);
+    });
+  });
+
+  /**
+   * TODO: 상태 변경에 대한 테스트 케이스 작성 요망 (모킹한 setSelectedCartItemIds의 내부 callback에서 문제 발생)
+  describe("toggleSelection", () => {
+    const mockCartItemId = 1;
+    const mockSelectedCartItemIds = [1, 2];
+    const mockPutInSelectedCartItemIds = putInSelectedCartItemIds as jest.Mock;
+    const mockSetSelectedCartItemIds = jest.fn();
+
+    it("1. 스토리지 동기화 2. 상태 변경", () => {
+      (useRecoilState as jest.Mock).mockImplementation(() => [
+        mockSelectedCartItemIds,
+        mockSetSelectedCartItemIds,
+      ]);
+
+      const { result } = renderHook(() => useCartItemControl(), {
+        wrapper: RecoilRoot,
+      });
+
+      act(() => result.current.toggleSelection(mockCartItemId));
+
+      expect(mockPutInSelectedCartItemIds).toHaveBeenCalledWith([2]);
+    });
+  });
+   */
+});

리뷰: 명세가 조금 더 명확했으면 좋겠어요!

어떤 patch api를 요청했고 어떻게 상태가 변경되었는지 구체적으로 명시하면 더 좋은 명세가 될 듯 합니다 :)
@@ -0,0 +1,22 @@
+import Header from "../components/Header";
+import styled from "styled-components";
+import CartContent from "../components/CartContent";
+import { Suspense } from "react";
+
+export default function CartPage() {
+  return (
+    <S.Container>
+      <Header hasBackButton={false} title="SHOP" />
+      <Suspense fallback={<div>Loading...</div>}>
+        <CartContent />
+      </Suspense>
+    </S.Container>
+  );
+}
+
+const S = {
+  Container: styled.div`
+    width: 429px;
+    border: 1px solid #808080;
+  `,
+};

리뷰: Suspense 구현하셨네요..! 👍

추가적으로 이미지가 로딩상태일 때, 화면이 접히네요..! 스타일도 추가적으로 적용하면 좋을 것 같습니다!
![image](https://github.com/cys4585/react-shopping-cart/assets/106071687/5c0b6fa9-7f24-4c18-8f66-f800db48bcaa)
@@ -1,10 +1,24 @@
 import "./App.css";
+import { RecoilRoot } from "recoil";
+import { BrowserRouter, Route, Routes } from "react-router-dom";
+
+import CartPage from "./pages/CartPage";
+import { PATH } from "./constants/path";
+import OrderSummaryPage from "./pages/OrderSummaryPage";
+import { ErrorBoundary } from "react-error-boundary";
 
 function App() {
   return (
-    <>
-      <h1>react-shopping-cart</h1>
-    </>
+    <BrowserRouter>
+      <RecoilRoot>
+        <ErrorBoundary fallbackRender={({ error }) => error.message}>
+          <Routes>
+            <Route path={PATH.cart} element={<CartPage />} />
+            <Route path={PATH.orderSummary} element={<OrderSummaryPage />} />
+          </Routes>
+        </ErrorBoundary>
+      </RecoilRoot>
+    </BrowserRouter>
   );
 }
 

리뷰: BrowserRouter와 비교해서 대표적인 차이가 어떤 게 있을까요??? 
다음에 한 번 시도해 보도록 해보겠습니다!
@@ -0,0 +1,31 @@
+import styled from "styled-components";
+
+export interface IButtonProps extends React.HTMLAttributes<HTMLButtonElement> {
+  disabled?: boolean;
+}
+
+export default function Button({ children, ...attributes }: IButtonProps) {
+  return <S.Button {...attributes}>{children}</S.Button>;
+}
+
+const S = {
+  Button: styled.button`
+    position: fixed;
+    bottom: 0;
+    max-width: 429px;
+
+    height: 64px;
+    font-size: 16px;
+    font-weight: 700;
+    text-align: center;
+    width: 100%;
+
+    background-color: black;
+    color: white;
+    border: none;
+    cursor: pointer;
+    &:disabled {
+      background-color: rgba(190, 190, 190, 1);
+    }
+  `,
+};

리뷰: 오... 너무 좋을 것 같네요!!
혹시 폴더네이밍에 대한 의견도 있으실까요~? `components/styled` ..?
@@ -0,0 +1,91 @@
+import { useRecoilValue } from "recoil";
+import styled from "styled-components";
+import { deliveryCostState, orderAmountState, totalOrderAmountState } from "../recoil/cartAmount";
+import { ReactComponent as InfoIcon } from "../assets/info-icon.svg";
+
+export interface ICartAmountProps {}
+
+export default function CartAmount() {
+  const orderAmount = useRecoilValue(orderAmountState);
+  const deliveryCost = useRecoilValue(deliveryCostState);
+  const totalOrderAmount = useRecoilValue(totalOrderAmountState);
+
+  return (
+    <S.CartAmountContainer>
+      <S.CartAmountNoti>
+        <S.InfoIcon />
+        <S.CartAmountNotiText>
+          총 주문 금액이 100,000원 이상일 경우 무료 배송됩니다.
+        </S.CartAmountNotiText>
+      </S.CartAmountNoti>
+
+      <S.UpperCartAmountInfoWrapper>
+        <S.CartAmountInfo>
+          <S.AmountText>주문 금액</S.AmountText>{" "}
+          <S.Amount>{orderAmount.toLocaleString()}원</S.Amount>
+        </S.CartAmountInfo>
+        <S.CartAmountInfo>
+          <S.AmountText>배송비</S.AmountText> <S.Amount>{deliveryCost.toLocaleString()}원</S.Amount>
+        </S.CartAmountInfo>
+      </S.UpperCartAmountInfoWrapper>
+      <S.LowerCartAmountInfoWrapper>
+        <S.CartAmountInfo>
+          <S.AmountText>총 주문 금액</S.AmountText>{" "}
+          <S.Amount>{totalOrderAmount.toLocaleString()}원</S.Amount>
+        </S.CartAmountInfo>
+      </S.LowerCartAmountInfoWrapper>
+    </S.CartAmountContainer>
+  );
+}
+
+const S = {
+  CartAmountContainer: styled.div`
+    display: flex;
+    flex-direction: column;
+    gap: 12px;
+  `,
+
+  CartAmountNoti: styled.p`
+    font-size: 14px;
+    color: #888;
+    margin-bottom: 20px;
+    display: flex;
+    align-items: center;
+    gap: 4px;
+  `,
+
+  UpperCartAmountInfoWrapper: styled.div`
+    border-top: 1px solid rgba(0, 0, 0, 0.1);
+  `,
+
+  LowerCartAmountInfoWrapper: styled.div`
+    border-top: 1px solid rgba(0, 0, 0, 0.1);
+  `,
+
+  CartAmountInfo: styled.div`
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    padding-top: 12px;
+  `,
+
+  AmountText: styled.span`
+    font-size: 16px;
+    font-weight: 700;
+  `,
+
+  Amount: styled.span`
+    font-size: 24px;
+    font-weight: 700;
+  `,
+  InfoIcon: styled(InfoIcon)`
+    width: 16px;
+    height: 16px;
+  `,
+  CartAmountNotiText: styled.span`
+    font-size: 12px;
+    font-weight: 500;
+    line-height: 15px;
+    color: rgba(10, 13, 19, 1);
+  `,
+};

리뷰: 처음에 구조를 자동 완성해주는 무슨 단축 명령어가 있었는데, 걔가 만들어줬고 그걸 지우지 않았습니다... 다른 많은 컴포넌트가 그렇네요...ㅎ
@@ -0,0 +1,82 @@
+import { useSetRecoilState } from "recoil";
+import CartItemView from "./CartItemView";
+import styled from "styled-components";
+import { selectedCartItemIdsState } from "../recoil/selectedCartItemIds";
+import { CartItem } from "../types/cartItems";
+import { useCartItemControl } from "../hooks/useCartItemControl";
+
+export interface ICartItemList {
+  cartItems: CartItem[];
+}
+
+export default function CartItemList({ cartItems }: ICartItemList) {
+  const cartItemControl = useCartItemControl();
+  const setSelectedCartItemIds = useSetRecoilState(selectedCartItemIdsState);
+  const isAllSelected = cartItems.every(({ isSelected }) => isSelected);
+
+  const handleSelectAllChange = () => {
+    if (isAllSelected) {
+      setSelectedCartItemIds([]);
+    } else {
+      setSelectedCartItemIds(cartItems.map(({ id }) => id));
+    }
+  };
+
+  return (
+    <S.CartItemListContainer>
+      <S.SelectAll>
+        <S.Checkbox
+          id="select-all-checkbox"
+          type="checkbox"
+          checked={isAllSelected}
+          onChange={handleSelectAllChange}
+        />
+        <S.SelectAllLabel htmlFor="select-all-checkbox">전체선택</S.SelectAllLabel>
+      </S.SelectAll>
+      <S.CartItemList>
+        {cartItems.map((cartItem) => (
+          <CartItemView
+            key={cartItem.product.id}
+            cartItem={cartItem}
+            cartItemControl={cartItemControl}
+          />
+        ))}
+      </S.CartItemList>
+    </S.CartItemListContainer>
+  );
+}
+
+const S = {
+  CartItemListContainer: styled.div`
+    display: flex;
+    flex-direction: column;
+    gap: 20px;
+
+    margin: 36px 0 52px 0;
+  `,
+
+  CartItemList: styled.div`
+    display: flex;
+    flex-direction: column;
+    gap: 20px;
+  `,
+
+  SelectAll: styled.div`
+    display: flex;
+    align-items: center;
+    gap: 8px;
+  `,
+
+  Checkbox: styled.input`
+    accent-color: black;
+    margin: 0;
+    width: 24px;
+    height: 24px;
+  `,
+
+  SelectAllLabel: styled.label`
+    font-size: 12px;
+    font-weight: 500;
+    line-height: 15px;
+  `,
+};

리뷰: 엇..ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ 없습니다.
순간 에러 뜰 거라 생각하고 아찔했네요. 하지만, `cartItem.id`가 맞는 표현이고, 실수로 `product.id`로 한 것 같습니다.
@@ -0,0 +1,22 @@
+import Header from "../components/Header";
+import styled from "styled-components";
+import CartContent from "../components/CartContent";
+import { Suspense } from "react";
+
+export default function CartPage() {
+  return (
+    <S.Container>
+      <Header hasBackButton={false} title="SHOP" />
+      <Suspense fallback={<div>Loading...</div>}>
+        <CartContent />
+      </Suspense>
+    </S.Container>
+  );
+}
+
+const S = {
+  Container: styled.div`
+    width: 429px;
+    border: 1px solid #808080;
+  `,
+};

리뷰: 정확히 같은 부분에 피터에게 코멘트를 받았어요! 낙타 날카로움이 거의 뭐 현직잔데요? 👍 
"전체 레이아웃이 유지되도록 스타일링을 해주면, CLS(Cumulative Layout Shift)를 방지할 수 있을 것"이라는 피터의 코멘트를 공유해드립니다!
@@ -0,0 +1,31 @@
+import styled from "styled-components";
+
+export interface IButtonProps extends React.HTMLAttributes<HTMLButtonElement> {
+  disabled?: boolean;
+}
+
+export default function Button({ children, ...attributes }: IButtonProps) {
+  return <S.Button {...attributes}>{children}</S.Button>;
+}
+
+const S = {
+  Button: styled.button`
+    position: fixed;
+    bottom: 0;
+    max-width: 429px;
+
+    height: 64px;
+    font-size: 16px;
+    font-weight: 700;
+    text-align: center;
+    width: 100%;
+
+    background-color: black;
+    color: white;
+    border: none;
+    cursor: pointer;
+    &:disabled {
+      background-color: rgba(190, 190, 190, 1);
+    }
+  `,
+};

리뷰: ps. 저는 개인적으로 `common` 이라는 폴더를 사용합니다...!