ewhk9887/korean_code_reviews_from_github huggingface dataset:
@@ -0,0 +1,118 @@
+import { RULE } from "../constants/rules";
+import {
+  CART_ITEMS_COUNTS_ENDPOINT,
+  CART_ITEMS_ENDPOINT,
+  PRODUCTS_ENDPOINT,
+} from "./endpoints";
+import { fetchWithAuth } from "./fetchWithAuth";
+
+interface QueryParams {
+  [key: string]:
+    | undefined
+    | string
+    | number
+    | boolean
+    | (string | number | boolean)[];
+}
+
+export interface GetProductsParams {
+  category?: Category;
+  page?: number;
+  size?: number;
+  sort?: Sort;
+}
+
+const createQueryString = (params: QueryParams): string => {
+  return Object.entries(params)
+    .map(([key, value]) => {
+      if (value === undefined) {
+        return;
+      }
+      if (Array.isArray(value)) {
+        return `${encodeURIComponent(key)}=${encodeURIComponent(
+          value.join(",")
+        )}`;
+      }
+      return `${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`;
+    })
+    .join("&");
+};
+
+export const getProducts = async ({
+  category,
+  page = 0,
+  size = 20,
+  sort = "asc",
+}: GetProductsParams = {}) => {
+  const params = {
+    category,
+    page,
+    size,
+    sort: ["price", sort],
+  };
+  const queryString = createQueryString(params) + RULE.sortQueryByIdAsc;
+
+  const response = await fetchWithAuth(`${PRODUCTS_ENDPOINT}?${queryString}`, {
+    method: "GET",
+  });
+  const data = await response.json();
+
+  if (!response.ok) {
+    throw new Error("Failed to get products item");
+  }
+
+  return data;
+};
+
+export const postProductInCart = async (
+  productId: number,
+  quantity: number = 1
+) => {
+  const response = await fetchWithAuth(CART_ITEMS_ENDPOINT, {
+    method: "POST",
+    body: {
+      productId,
+      quantity,
+    },
+  });
+
+  if (!response.ok) {
+    throw new Error("Failed to post product item in cart");
+  }
+};
+
+export const deleteProductInCart = async (cartId: number) => {
+  const response = await fetchWithAuth(`${CART_ITEMS_ENDPOINT}/${cartId}`, {
+    method: "DELETE",
+  });
+
+  if (!response.ok) {
+    throw new Error("Failed to delete product item in cart");
+  }
+};
+
+export const getCartItemsCount = async () => {
+  const response = await fetchWithAuth(CART_ITEMS_COUNTS_ENDPOINT, {
+    method: "GET",
+  });
+  const data = await response.json();
+
+  if (!response.ok) {
+    throw new Error("Failed to get cart items count");
+  }
+
+  return data;
+};
+
+export const getCartItems = async (): Promise<CartItem[]> => {
+  const response = await fetchWithAuth(CART_ITEMS_ENDPOINT, {
+    method: "GET",
+  });
+  const data = await response.json();
+
+  if (!response.ok) {
+    throw new Error("Failed to get cart items count");
+  }
+
+  return data.content;
+};

리뷰: 아마 key로 접근해서 값을 가져오기 위해서 index signature를 사용하려고 하셨던 걸까요?!
하지만 이렇게 되면, 예상치 못하게 `page = "page1"` 과 같은 prop이나
의도하지 않은 key값이 들어올 경우,  queryParams interface에서 걸러지지 않을 것 같아요.
여유있으실 때 한번 생각해 보시면 좋을 것 같아요 :)
@@ -0,0 +1,34 @@
+import styled from "styled-components";
+
+export const ButtonStyle = styled.button`
+  cursor: pointer;
+  border: none;
+  outline: none;
+`;
+
+export const CartControlButtonStyle = styled(ButtonStyle)`
+  display: flex;
+  position: absolute;
+  right: 8px;
+  align-items: center;
+  border-radius: 4px;
+  padding: 4px 8px;
+  gap: 1px;
+  bottom: 15px;
+  font-weight: 600;
+  font-size: 12px;
+
+  img {
+    width: 14px;
+    height: 14px;
+  }
+`;
+
+export const AddCartStyle = styled(CartControlButtonStyle)`
+  background-color: #000000;
+  color: #ffffff;
+`;
+export const RemoveCartStyle = styled(CartControlButtonStyle)`
+  background-color: #eaeaea;
+  color: #000000;
+`;

리뷰: 저는 Button의 Prop을 받아서 색상을 바꿔주는 식으로 작성했어요!
```suggestion
export const ButtonStyle = styled.button<ButtonProps>`
  cursor: pointer;
  border: none;
  outline: none;
    ${({ color }) => {
    if (color === "primary") {
      return `
          background-color: black;
          color: white;
        `;
    }
    if (color === "secondary") {
      return `
          background-color: lightGrey;
          color: black;
        `;
    }
    return `
          background-color: white;
          color: black;
          border: 1px solid lightGray;
        `;
  }}
`;

```

style의 코드가 보기 좋지 않지만, 사용하는 곳에서 별도의 태그를 선언하지 않아도 쓸 수 있는 장점이 있더라구요~
아마 수야의 코드에서는 이렇게 되겠네요
```tsx
const CartControlButton = ({ isInCart, onClick }: CartControlButtonProps) => {
  return (
        <CartControlButtonStyle onClick={onClick} $color={isInCart?'secondary':'primary'}>
          <img src={isInCart?RemoveCart:AddCart} alt={isInCart?'장바구니 빼기':'장바구니 더하기'} />
          <span>{isInCart?'빼기':'더하기'}</span>
        </CartControlButtonStyle>
  );
};
```

막상 다 적고 보니까, 수야의 코드가 더 깔끔해 보이네요 ㅋㅋㅋㅋㅋ
참고 하실 수 있을 것 같아서 적어봤고, 저도 저와 다른 코드 방식을 보니 재밌었습니다 :)
@@ -0,0 +1,39 @@
+import styled from "styled-components";
+
+export const HeaderStyle = styled.header`
+  display: flex;
+  background-color: #000000;
+  width: inherit;
+  height: 64px;
+  position: fixed;
+  z-index: 1;
+  padding: 16px 24px 16px;
+  box-sizing: border-box;
+`;
+
+export const LogoImg = styled.img`
+  width: 56px;
+`;
+
+export const CartImg = styled.img`
+  width: 32px;
+  position: absolute;
+  right: 24px;
+  bottom: 16px;
+`;
+
+export const CartCount = styled.div`
+  background-color: #ffffff;
+  border-radius: 999px;
+  width: 19px;
+  height: 19px;
+  font-size: 10px;
+  font-weight: 700;
+  position: absolute;
+  right: 24px;
+  bottom: 16px;
+
+  display: flex;
+  align-items: center;
+  justify-content: center;
+`;

리뷰: 프로젝트의 컴포넌트 규모가 복잡해지면, zindex 관리가 힘들더라구요
헤더를 1로 해놨는데, 헤더보다 낮은 어떠한 component가 생길수도 있고...!
그래서 저도 추천받은 방법이 zIndex들을 상수로 관리해서 한곳에서 편하게 볼 수 있도록 하는거였어요
저도 자주 사용하진 않지만, 꿀팁 공유해봅니다 ㅎㅎ
@@ -0,0 +1,57 @@
+import {
+  createContext,
+  useState,
+  ReactNode,
+  useEffect,
+  useCallback,
+} from "react";
+import { getCartItems } from "../api";
+import { useErrorContext } from "../hooks/useErrorContext";
+
+export interface CartItemsContextType {
+  cartItems: CartItem[];
+  refreshCartItems: () => void;
+}
+
+export const CartItemsContext = createContext<CartItemsContextType | undefined>(
+  undefined
+);
+
+interface CartItemsProviderProps {
+  children: ReactNode;
+}
+
+export const CartItemsProvider: React.FC<CartItemsProviderProps> = ({
+  children,
+}) => {
+  const { showError } = useErrorContext();
+
+  const [toggle, setToggle] = useState(false);
+
+  const [cartItems, setCartItems] = useState<CartItem[]>([]);
+
+  const refreshCartItems = useCallback(() => {
+    setToggle((prev) => !prev);
+  }, []);
+
+  useEffect(() => {
+    const fetchCartItems = async () => {
+      try {
+        const cartItems = await getCartItems();
+        setCartItems(cartItems);
+      } catch (error) {
+        if (error instanceof Error) {
+          showError(error.message);
+        }
+      }
+    };
+
+    fetchCartItems();
+  }, [toggle, showError]);
+
+  return (
+    <CartItemsContext.Provider value={{ cartItems, refreshCartItems }}>
+      {children}
+    </CartItemsContext.Provider>
+  );
+};

리뷰: 저는 Quantity만을 context로 관리했는데, 전체 List를 context 로 관리하셨군요!!
Provider를 custom 해주는 것도 매우 좋은 것 같습니다 :)

좋은 정보 배우고 갑니다!!
@@ -0,0 +1,34 @@
+import styled from "styled-components";
+
+export const ButtonStyle = styled.button`
+  cursor: pointer;
+  border: none;
+  outline: none;
+`;
+
+export const CartControlButtonStyle = styled(ButtonStyle)`
+  display: flex;
+  position: absolute;
+  right: 8px;
+  align-items: center;
+  border-radius: 4px;
+  padding: 4px 8px;
+  gap: 1px;
+  bottom: 15px;
+  font-weight: 600;
+  font-size: 12px;
+
+  img {
+    width: 14px;
+    height: 14px;
+  }
+`;
+
+export const AddCartStyle = styled(CartControlButtonStyle)`
+  background-color: #000000;
+  color: #ffffff;
+`;
+export const RemoveCartStyle = styled(CartControlButtonStyle)`
+  background-color: #eaeaea;
+  color: #000000;
+`;

리뷰: Button에 `hover` 속성도 있으면 어떨까 싶네요!
@@ -0,0 +1,37 @@
+import { forwardRef } from "react";
+import * as PI from "./ProductItem.style";
+import CartControlButton from "../../Button/CartControlButton";
+import useProductInCart from "../../../hooks/useProductInCart";
+
+interface ProductProps {
+  product: Product;
+}
+
+const ProductItem = forwardRef<HTMLDivElement, ProductProps>(
+  ({ product }, ref) => {
+    const { isProductInCart, handleProductInCart } = useProductInCart(
+      product.id
+    );
+
+    return (
+      <PI.ProductItemStyle ref={ref}>
+        <PI.ProductImg
+          src={`${product.imageUrl}`}
+          alt={`${product.name} 상품 이미지`}
+        />
+        <PI.ProductGroup>
+          <PI.ProductContent>
+            <PI.ProductName>{product.name}</PI.ProductName>
+            <span>{product.price.toLocaleString("ko-kr")}원</span>
+          </PI.ProductContent>
+          <CartControlButton
+            onClick={handleProductInCart}
+            isInCart={isProductInCart}
+          />
+        </PI.ProductGroup>
+      </PI.ProductItemStyle>
+    );
+  }
+);
+
+export default ProductItem;

리뷰: 혹시 try-catch 에서 catch되는 error가 `Error` 타입이 아닌 경우도 있나요?!
단순히 궁금해서 여쭤봅니다...!
catch되는 것은 Error만  catch되는줄 알아서, 이 로직이 필요한가 하는 생각이 들었어요 ㅋㅋ
@@ -0,0 +1,51 @@
+import useProducts from "../../hooks/useProducts";
+import ProductListHeader from "../ProductListHeader/ProductListHeader";
+import ProductItem from "./ProductItem/ProductItem";
+import * as PL from "./ProductList.style";
+import useInfiniteScroll from "../../hooks/useInfiniteScroll";
+import usePagination from "../../hooks/usePagination";
+
+const ProductList = () => {
+  const { page, nextPage, resetPage } = usePagination();
+
+  const { products, loading, hasMore, handleCategory, handleSort } =
+    useProducts({
+      page,
+      resetPage,
+    });
+
+  const { lastElementRef: lastProductElementRef } = useInfiniteScroll({
+    hasMore,
+    loading,
+    nextPage,
+  });
+
+  return (
+    <>
+      <ProductListHeader
+        handleCategory={handleCategory}
+        handleSort={handleSort}
+      />
+      {!loading && products.length === 0 ? (
+        <PL.Empty>상품이 존재하지 않습니다! 🥲</PL.Empty>
+      ) : (
+        <PL.ProductListStyle>
+          {products.map((item, index) => {
+            return (
+              <ProductItem
+                product={item}
+                key={item.id}
+                ref={
+                  index === products.length - 1 ? lastProductElementRef : null
+                }
+              />
+            );
+          })}
+        </PL.ProductListStyle>
+      )}
+      {loading && <PL.Loading>로딩중! 💪</PL.Loading>}
+    </>
+  );
+};
+
+export default ProductList;

리뷰: 오... 해결법 제시까지 대단하네요 🚀 🚀🚀🚀🚀🚀🚀
@@ -0,0 +1,49 @@
+const generateBasicToken = (userId: string, userPassword: string): string => {
+  const token = btoa(`${userId}:${userPassword}`);
+  return `Basic ${token}`;
+};
+
+const API_URL = `${import.meta.env.VITE_API_URL}`;
+const USER_ID = `${import.meta.env.VITE_USER_ID}`;
+const USER_PASSWORD = `${import.meta.env.VITE_USER_PASSWORD}`;
+
+if (!API_URL || !USER_ID || !USER_PASSWORD) {
+  throw new Error(
+    "API_URL, USER_ID, PASSWORD environment variables are not set"
+  );
+}
+
+type HttpMethod = "GET" | "POST" | "PATCH" | "DELETE";
+
+interface RequestOptions {
+  method: HttpMethod;
+  body?: Record<string, unknown>;
+}
+
+export const fetchWithAuth = async (path: string, options: RequestOptions) => {
+  const requestInit = requestBuilder(options);
+  const response = await fetch(path, requestInit);
+
+  if (!response.ok) {
+    throw new Error(`Failed to ${options.method} ${path}`);
+  }
+
+  return response;
+};
+
+const requestBuilder = (options: RequestOptions): RequestInit => {
+  const token = generateBasicToken(USER_ID, USER_PASSWORD);
+
+  const { method, body } = options;
+
+  const headers: HeadersInit = {
+    "Content-Type": "application/json",
+    Authorization: token,
+  };
+
+  return {
+    method,
+    headers,
+    body: body ? JSON.stringify(body) : undefined,
+  };
+};

리뷰: `requestBuilder` 라는 이름으로 요청을 만드는 책임을 가진 함수를 별도로 분리해주신 부분이 인상깊네요!👍👍
@@ -0,0 +1,57 @@
+import {
+  createContext,
+  useState,
+  ReactNode,
+  useEffect,
+  useCallback,
+} from "react";
+import { getCartItems } from "../api";
+import { useErrorContext } from "../hooks/useErrorContext";
+
+export interface CartItemsContextType {
+  cartItems: CartItem[];
+  refreshCartItems: () => void;
+}
+
+export const CartItemsContext = createContext<CartItemsContextType | undefined>(
+  undefined
+);
+
+interface CartItemsProviderProps {
+  children: ReactNode;
+}
+
+export const CartItemsProvider: React.FC<CartItemsProviderProps> = ({
+  children,
+}) => {
+  const { showError } = useErrorContext();
+
+  const [toggle, setToggle] = useState(false);
+
+  const [cartItems, setCartItems] = useState<CartItem[]>([]);
+
+  const refreshCartItems = useCallback(() => {
+    setToggle((prev) => !prev);
+  }, []);
+
+  useEffect(() => {
+    const fetchCartItems = async () => {
+      try {
+        const cartItems = await getCartItems();
+        setCartItems(cartItems);
+      } catch (error) {
+        if (error instanceof Error) {
+          showError(error.message);
+        }
+      }
+    };
+
+    fetchCartItems();
+  }, [toggle, showError]);
+
+  return (
+    <CartItemsContext.Provider value={{ cartItems, refreshCartItems }}>
+      {children}
+    </CartItemsContext.Provider>
+  );
+};

리뷰: `useCallback`과`toggle`이라는 상태를 통해 `fetchCartItems`을 재실행시킨다는 발상이 몹시 새롭게 느껴지네요! 한 번도 생각해보지 못했던 방식인데... 상태는 최소화되어야 한다고 생각하는 편이지만, 특정 트리거를 위해 상태를 활용하는 것이 좋은지 저도 한 번 고민해볼 수 있었던 것 같아요!

한편으로는 상태를 만들지않고, `fetchCartItems`로직을 `useEffect` 밖으로 뺀 뒤 refreshCartItems 을 잘 작성해주면 상태가 필요하지 않을 것 같다는 생각이 들기도 합니다! (이 부분은 개발자의 취향이나 선호에 가까울 수 있을 것 같아요..!)
@@ -0,0 +1,15 @@
+import { useContext } from "react";
+import {
+  CartItemsContext,
+  CartItemsContextType,
+} from "../context/CartItemsContext";
+
+export const useCartItemsContext = (): CartItemsContextType => {
+  const context = useContext(CartItemsContext);
+  if (!context) {
+    throw new Error(
+      "useCartItemsContext must be used within an CartItemsProvider"
+    );
+  }
+  return context;
+};

리뷰: 현재 `useErrorContext`와 `useCartItemsContext`는 context가 `undefined`인 경우 에러를 던져 타입을 좁히기 위한 커스텀 훅인 것 같습니다. 제가 느끼기엔 두 훅 모두 같은 역할을 해주고 있는 것 같아서 context를 인자로 받는다면 하나의 훅으로 합쳐서 재사용해주어도 좋을 것 같습니다!ㅎㅎ
@@ -0,0 +1,118 @@
+import { RULE } from "../constants/rules";
+import {
+  CART_ITEMS_COUNTS_ENDPOINT,
+  CART_ITEMS_ENDPOINT,
+  PRODUCTS_ENDPOINT,
+} from "./endpoints";
+import { fetchWithAuth } from "./fetchWithAuth";
+
+interface QueryParams {
+  [key: string]:
+    | undefined
+    | string
+    | number
+    | boolean
+    | (string | number | boolean)[];
+}
+
+export interface GetProductsParams {
+  category?: Category;
+  page?: number;
+  size?: number;
+  sort?: Sort;
+}
+
+const createQueryString = (params: QueryParams): string => {
+  return Object.entries(params)
+    .map(([key, value]) => {
+      if (value === undefined) {
+        return;
+      }
+      if (Array.isArray(value)) {
+        return `${encodeURIComponent(key)}=${encodeURIComponent(
+          value.join(",")
+        )}`;
+      }
+      return `${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`;
+    })
+    .join("&");
+};
+
+export const getProducts = async ({
+  category,
+  page = 0,
+  size = 20,
+  sort = "asc",
+}: GetProductsParams = {}) => {
+  const params = {
+    category,
+    page,
+    size,
+    sort: ["price", sort],
+  };
+  const queryString = createQueryString(params) + RULE.sortQueryByIdAsc;
+
+  const response = await fetchWithAuth(`${PRODUCTS_ENDPOINT}?${queryString}`, {
+    method: "GET",
+  });
+  const data = await response.json();
+
+  if (!response.ok) {
+    throw new Error("Failed to get products item");
+  }
+
+  return data;
+};
+
+export const postProductInCart = async (
+  productId: number,
+  quantity: number = 1
+) => {
+  const response = await fetchWithAuth(CART_ITEMS_ENDPOINT, {
+    method: "POST",
+    body: {
+      productId,
+      quantity,
+    },
+  });
+
+  if (!response.ok) {
+    throw new Error("Failed to post product item in cart");
+  }
+};
+
+export const deleteProductInCart = async (cartId: number) => {
+  const response = await fetchWithAuth(`${CART_ITEMS_ENDPOINT}/${cartId}`, {
+    method: "DELETE",
+  });
+
+  if (!response.ok) {
+    throw new Error("Failed to delete product item in cart");
+  }
+};
+
+export const getCartItemsCount = async () => {
+  const response = await fetchWithAuth(CART_ITEMS_COUNTS_ENDPOINT, {
+    method: "GET",
+  });
+  const data = await response.json();
+
+  if (!response.ok) {
+    throw new Error("Failed to get cart items count");
+  }
+
+  return data;
+};
+
+export const getCartItems = async (): Promise<CartItem[]> => {
+  const response = await fetchWithAuth(CART_ITEMS_ENDPOINT, {
+    method: "GET",
+  });
+  const data = await response.json();
+
+  if (!response.ok) {
+    throw new Error("Failed to get cart items count");
+  }
+
+  return data.content;
+};

리뷰: 몹시 흥미로운 함수네요!👍👍  별거 아니지만 이러한 부분은 api 내부 util로 파일을 분리해주셔도 좋을 것 같아요!
@@ -0,0 +1,118 @@
+import { RULE } from "../constants/rules";
+import {
+  CART_ITEMS_COUNTS_ENDPOINT,
+  CART_ITEMS_ENDPOINT,
+  PRODUCTS_ENDPOINT,
+} from "./endpoints";
+import { fetchWithAuth } from "./fetchWithAuth";
+
+interface QueryParams {
+  [key: string]:
+    | undefined
+    | string
+    | number
+    | boolean
+    | (string | number | boolean)[];
+}
+
+export interface GetProductsParams {
+  category?: Category;
+  page?: number;
+  size?: number;
+  sort?: Sort;
+}
+
+const createQueryString = (params: QueryParams): string => {
+  return Object.entries(params)
+    .map(([key, value]) => {
+      if (value === undefined) {
+        return;
+      }
+      if (Array.isArray(value)) {
+        return `${encodeURIComponent(key)}=${encodeURIComponent(
+          value.join(",")
+        )}`;
+      }
+      return `${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`;
+    })
+    .join("&");
+};
+
+export const getProducts = async ({
+  category,
+  page = 0,
+  size = 20,
+  sort = "asc",
+}: GetProductsParams = {}) => {
+  const params = {
+    category,
+    page,
+    size,
+    sort: ["price", sort],
+  };
+  const queryString = createQueryString(params) + RULE.sortQueryByIdAsc;
+
+  const response = await fetchWithAuth(`${PRODUCTS_ENDPOINT}?${queryString}`, {
+    method: "GET",
+  });
+  const data = await response.json();
+
+  if (!response.ok) {
+    throw new Error("Failed to get products item");
+  }
+
+  return data;
+};
+
+export const postProductInCart = async (
+  productId: number,
+  quantity: number = 1
+) => {
+  const response = await fetchWithAuth(CART_ITEMS_ENDPOINT, {
+    method: "POST",
+    body: {
+      productId,
+      quantity,
+    },
+  });
+
+  if (!response.ok) {
+    throw new Error("Failed to post product item in cart");
+  }
+};
+
+export const deleteProductInCart = async (cartId: number) => {
+  const response = await fetchWithAuth(`${CART_ITEMS_ENDPOINT}/${cartId}`, {
+    method: "DELETE",
+  });
+
+  if (!response.ok) {
+    throw new Error("Failed to delete product item in cart");
+  }
+};
+
+export const getCartItemsCount = async () => {
+  const response = await fetchWithAuth(CART_ITEMS_COUNTS_ENDPOINT, {
+    method: "GET",
+  });
+  const data = await response.json();
+
+  if (!response.ok) {
+    throw new Error("Failed to get cart items count");
+  }
+
+  return data;
+};
+
+export const getCartItems = async (): Promise<CartItem[]> => {
+  const response = await fetchWithAuth(CART_ITEMS_ENDPOINT, {
+    method: "GET",
+  });
+  const data = await response.json();
+
+  if (!response.ok) {
+    throw new Error("Failed to get cart items count");
+  }
+
+  return data.content;
+};

리뷰: 추상화된 `fetchWithAuth` 내부에서 이미 `response.ok`가 아닌 경우 에러를 던지도록 처리해두었는데 여기를 포함하여 모든 fetch 함수에서 여전히 에러를 던지시는 이유가 있으실까요..??
@@ -0,0 +1,5 @@
+package store.constants;
+
+public class MembershipConstants {
+    public static final double DISCOUNT_RATE = 0.30;
+}

리뷰: 이 부분은 클래스를 생성할 수 없게끔 생성자를 private으로 만들면 좋을 것 같아요.
@@ -0,0 +1,18 @@
+package store.constants;
+
+public class ReceiptConstants {
+    public static final String ORDER_DETAIL_FORMAT = "%-10s %6s %12s";
+    public static final String PROMOTION_DETAIL_FORMAT = "%-10s %6s";
+    public static final String TOTAL_DETAIL_FORMAT = "%-10s  %18s";
+    public static final String LINE_SEPARATOR = "====================================";
+    public static final String PROMOTION_HEADER = "=============증\t\t정===============";
+    public static final String RECEIPT_HEADER = "==============W 편의점================";
+    public static final String PRODUCT_HEADER = "%-10s %6s %12s";
+    public static final String PRODUCT_NAME = "상품명";
+    public static final String QUANTITY = "수량";
+    public static final String PRICE = "금액";
+    public static final String TOTAL_PURCHASE_AMOUNT = "총구매액";
+    public static final String EVENT_DISCOUNT = "행사할인";
+    public static final String MEMBERSHIP_DISCOUNT = "멤버십할인";
+    public static final String FINAL_AMOUNT = "내실돈";
+}

리뷰: Enum을 사용하지 않으신 이유가 있을까요?
String.format을 사용하실 때 Enum 내에 변환해서 반환하면 OuptView에서도 더 깔끔하게 로직을 처리할 수 있을 것 같아요!
@@ -0,0 +1,38 @@
+package store.controller;
+
+import java.util.List;
+import java.util.Map;
+import store.domain.Product;
+import store.domain.Promotion;
+import store.dto.StoreDto;
+import store.dto.StoreInitializationDto;
+import store.parser.ProductParser;
+import store.parser.PromotionParser;
+import store.service.FileReaderService;
+
+public class FileReaderController {
+    private final FileReaderService fileReaderService;
+    private final PromotionParser promotionParser;
+    private final ProductParser productParser;
+
+    public FileReaderController() {
+        this.fileReaderService = new FileReaderService();
+        this.productParser = new ProductParser();
+        this.promotionParser = new PromotionParser();
+    }
+
+    public StoreDto runFileData() {
+        return initialize();
+    }
+
+    public StoreDto initialize() {
+        StoreInitializationDto storeInitializationDto = fileReaderService.initializeStoreData();
+        return parseStoreData(storeInitializationDto);
+    }
+
+    public StoreDto parseStoreData(StoreInitializationDto storeInitializationDto) {
+        List<Promotion> promotions = promotionParser.parse(storeInitializationDto.promotionDtos());
+        Map<String, Product> products = productParser.parse(storeInitializationDto.productDtos(), promotions);
+        return new StoreDto(products, promotions);
+    }
+}

리뷰: 이런 부분은 private를 통해 캡슐화하는 것이 어떨까요?
@@ -0,0 +1,23 @@
+package store.controller;
+
+import store.domain.Store;
+import store.dto.StoreDto;
+import store.service.StoreService;
+
+public class StoreController {
+
+    private final StoreService storeService;
+
+    public StoreController() {
+        this.storeService = new StoreService();
+    }
+
+    public void run(StoreDto storeDto) {
+        Store store = createStore(storeDto);
+        storeService.processOrder(store);
+    }
+
+    private Store createStore(StoreDto storeDto) {
+        return new Store(storeDto);
+    }
+}

리뷰: 외부에서 의존성 주입하는 방식을 사용하지 않으신 이유가 있을까요?
@@ -0,0 +1,35 @@
+package store.domain;
+
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+public class Cart {
+    private final Map<String, CartItem> cartItems;
+
+    public Cart() {
+        this.cartItems = new LinkedHashMap<>();
+    }
+
+    public void addItem(List<CartItem> cartItems) {
+        for (CartItem cartItem : cartItems) {
+            this.cartItems.put(cartItem.getProduct().getName(), cartItem);
+        }
+    }
+
+    public List<CartItem> getAllItemsInCart() {
+        return cartItems.values().stream().toList();
+    }
+
+    public int getTotalFreeItemQuantity() {
+        return cartItems.values().stream().mapToInt(CartItem::getFreeQuantity).sum();
+    }
+
+    public int getTotalFreeItemPrice() {
+        return cartItems.values().stream().mapToInt(CartItem::getTotalFreePrice).sum();
+    }
+
+    public int getTotalItemPrice() {
+        return cartItems.values().stream().mapToInt(CartItem::totalPrice).sum();
+    }
+}

리뷰: Stream API는 가독성을 위해 엔터를 사용하면 더 좋을 것 같아요!