ewhk9887/korean_code_reviews_from_github huggingface dataset:
@@ -0,0 +1,66 @@
+@use '../../../styles/mixin.scss' as mixin;
+@use '../../../styles/color.scss' as clr;
+
+body {
+  background: clr.$bg-login;
+
+  .login {
+    position: relative;
+    top: -80px;
+
+    #wrapper {
+      @include mixin.flex($align: center);
+      position: relative;
+      flex-direction: column;
+      width: 700px;
+      height: 761px;
+      border: 1px solid clr.$clr-border;
+
+      h1 {
+        margin-top: 90px;
+        margin-bottom: 130px;
+        font-size: 81px;
+        font-family: 'Lobster', cursive;
+      }
+
+      .login_form {
+        input {
+          display: block;
+          width: 536px;
+          height: 76px;
+          margin-bottom: 12px;
+          padding-left: 10px;
+          background: #fafafa;
+          border: 1px solid clr.$clr-border;
+          border-radius: 3px;
+          font-size: 16px;
+        }
+
+        input[type='button'] {
+          display: flex;
+          align-items: center;
+          justify-content: center;
+          width: 536px;
+          height: 60px;
+          margin-top: 16px;
+          background-color: clr.$clr-primary;
+          opacity: 0.5;
+          border: none;
+          border-radius: 4px;
+          font-size: 16px;
+          color: #fff;
+
+          &.activated {
+            opacity: 1;
+          }
+        }
+      }
+
+      p {
+        position: absolute;
+        bottom: 20px;
+        color: #043667;
+      }
+    }
+  }
+}

리뷰: 위 코드는 어디에 작성되면 좋을까요?
@@ -0,0 +1,43 @@
+span.comment {
+  padding-left: 10px;
+
+  small {
+    color: #a3a3a3;
+  }
+
+  &:first-child {
+    padding: 10px;
+    font-weight: bold;
+  }
+
+  svg {
+    &.delete {
+      display: none;
+      cursor: pointer;
+    }
+
+    &.hearts {
+      color: #a3a3a3;
+      position: absolute;
+      right: 20px;
+      transition: color 1s;
+      cursor: pointer;
+
+      path {
+        color: #a3a3a3;
+      }
+    }
+
+    &.liked {
+      path {
+        color: #f00;
+      }
+    }
+  }
+
+  &:hover svg {
+    &.delete {
+      display: inline-block;
+    }
+  }
+}

리뷰: 자주 사용이 되는 컬러값이네요! 이런 경우 sass variables 기능을 이용하는것은 어떨까요?
@@ -0,0 +1,87 @@
+import React, { useState } from 'react';
+import { AiFillHeart, AiOutlineComment, AiOutlineUpload } from 'react-icons/ai';
+import { BiDotsHorizontalRounded } from 'react-icons/bi';
+import { GrBookmark } from 'react-icons/gr';
+import './Feeds.scss';
+import Comments from './Comment/Comments';
+
+export default function Feeds({ userId, profileImg, img, comments }) {
+  const [commentVal, setCommentVal] = useState(comments);
+  let addedCommentVal = [...commentVal];
+  const [currInputVal, setCurrInputVal] = useState('');
+
+  const handleInput = e => {
+    const { value } = e.target;
+    if (e.keyCode === 13) uploadComment();
+    setCurrInputVal(value);
+  };
+
+  const uploadComment = e => {
+    e.preventDefault();
+    if (!currInputVal) return;
+    addedCommentVal.push({
+      id: addedCommentVal.length + 1,
+      userId: 'userid',
+      value: currInputVal,
+      time: '방금전',
+    });
+    setCommentVal(addedCommentVal);
+    setCurrInputVal('');
+  };
+
+  const handleDelete = e => {
+    const { id } = e.target.parentNode.parentNode;
+    addedCommentVal = addedCommentVal.filter(el => el.id !== Number(id));
+    setCommentVal(addedCommentVal);
+  };
+
+  return (
+    <div className="feeds">
+      <article>
+        <div className="top_menu">
+          <span>
+            <img src={profileImg} alt="프로필" />
+            {userId}
+          </span>
+          <span>
+            <BiDotsHorizontalRounded />
+          </span>
+        </div>
+        <img src={img} alt="무화과" />
+        <div className="bottom_menu">
+          <span>
+            <AiFillHeart className="" />
+          </span>
+          <span>
+            <AiOutlineComment className="" />
+          </span>
+          <span>
+            <AiOutlineUpload className="" />
+          </span>
+          <span>
+            <GrBookmark className="" />
+          </span>
+        </div>
+        <div className="comments">
+          <span>4 Likes</span>
+          <Comments commentArr={commentVal} handleDelete={handleDelete} />
+        </div>
+        <form className="add_comments">
+          <input
+            type="text"
+            placeholder="댓글 달기..."
+            className="input_upload"
+            onChange={handleInput}
+            value={currInputVal}
+          />
+          <button
+            className={currInputVal ? 'activated' : ''}
+            onClick={uploadComment}
+          >
+            게시
+          </button>
+        </form>
+      </article>
+    </div>
+  );
+}

리뷰: .해당 부분도 좀 더 반복되는 단어를 줄일 수 없을지 고민해봅시다. 
더불어 불필요한 state값도 보이네요! 위 리뷰를 참고해 수정해주세요!
@@ -0,0 +1,101 @@
+@use '../../../../styles/mixin.scss' as mixin;
+@use '../../../../styles/color.scss' as clr;
+
+.feeds {
+  position: relative;
+  top: 77px;
+  width: 60%;
+  margin-bottom: 80px;
+  background: #fff;
+
+  .top_menu {
+    @include mixin.flex($justify: space-between, $align: center);
+    height: 82px;
+
+    span {
+      @include mixin.flex($align: center);
+      margin-left: 20px;
+      font-weight: bold;
+
+      &:last-child {
+        margin-right: 22px;
+        color: #2a2a2a;
+        font-size: 35px;
+      }
+
+      img {
+        width: 42px;
+        height: 42px;
+        border-radius: 50%;
+        margin-right: 20px;
+      }
+    }
+  }
+
+  article {
+    img {
+      width: 100%;
+      height: 818px;
+    }
+
+    .bottom_menu {
+      @include mixin.flex($align: center);
+      position: relative;
+      height: 62px;
+
+      span > svg {
+        margin: 0 10px;
+        font-size: 32px;
+        color: #414141;
+      }
+
+      span:last-child > svg {
+        position: absolute;
+        top: 15px;
+        right: 0;
+      }
+
+      span:first-child > svg > path {
+        color: #eb4b59;
+      }
+    }
+  }
+
+  .comments {
+    display: flex;
+    flex-direction: column;
+
+    span {
+      padding-left: 10px;
+    }
+  }
+
+  .add_comments {
+    position: absolute;
+    @include mixin.flex($justify: space-between, $align: center);
+    bottom: -62px;
+    width: 100%;
+    height: 62px;
+    border-top: 1px solid clr.$clr-border;
+    font-size: 14px;
+
+    input {
+      width: 90%;
+      height: 50%;
+      margin-left: 15px;
+      border: none;
+    }
+
+    button {
+      margin-right: 15px;
+      border: none;
+      color: clr.$clr-primary;
+      opacity: 0.5;
+      background: #fff;
+
+      &.activated {
+        opacity: 1;
+      }
+    }
+  }
+}

리뷰: class명은 어떤 형태가 좋을까요?! 
해당 class명은 '동작'을 나타내기에 클래스 네임보다는 함수명처럼 느껴집니다!
@@ -1,5 +1,98 @@
 import React from 'react';
+import { useState, useEffect } from 'react';
+import { useNavigate } from 'react-router-dom';
+import './Login.scss';
 
-export default function Login() {
-  return <div>Hello, Hyeonze!</div>;
-}
+const Login = () => {
+  const [idInput, setIdInput] = useState('');
+  const [pwInput, setPwInput] = useState('');
+  const [isValidatedUser, setIsValidatedUser] = useState(false);
+
+  const navigate = useNavigate();
+  const changeIdInput = e => {
+    const { value } = e.target;
+    setIdInput(value);
+  };
+
+  const changePwInput = e => {
+    const { value } = e.target;
+    setPwInput(value);
+  };
+
+  useEffect(() => {
+    setIsValidatedUser(idInput.indexOf('@') !== -1 && pwInput.length > 4);
+  }, [idInput, pwInput]);
+
+  // 메인으로 이동할 때 로직
+  const goToMain = () => {
+    navigate('/main-hyeonze');
+  };
+
+  // 회원가입시 사용할 로직
+  // const signup = () => {
+  //   fetch('http://10.58.4.22:8000/users/signup', {
+  //     method: 'POST',
+  //     body: JSON.stringify({
+  //       email: idInput,
+  //       password: pwInput,
+  //     }),
+  //   })
+  //     .then(response => response.json())
+  //     .then(result => {
+  //       //console.log('결과: ', result.message)
+  //       if (result.message === 'EMAIL_ALREADY_EXISTS') {
+  //         console.log('Error : Duplicated');
+  //       }
+  //     });
+  // };
+
+  // 로그인시 사용할 로직
+  // const login = () => {
+  //   fetch('http://10.58.4.22:8000/users/login', {
+  //     method: 'POST',
+  //     body: JSON.stringify({
+  //       email: idInput,
+  //       password: pwInput,
+  //     }),
+  //   })
+  //     .then(response => response.json())
+  //     .then(result => {
+  //       //console.log('결과: ', result.message)
+  //       if (result.message === 'SUCCESS') {
+  //         console.log('login SUCCESS');
+  //       }
+  //     });
+  // };
+
+  return (
+    <div className="login">
+      <div id="wrapper">
+        <h1>westagram</h1>
+        <form className="login_form">
+          <input
+            type="text"
+            placeholder="전화번호, 사용자 이름 또는 이메일"
+            className="username"
+            onChange={changeIdInput}
+          />
+          <input
+            type="password"
+            placeholder="비밀번호"
+            className="userpassword"
+            onChange={changePwInput}
+          />
+          <input
+            className={isValidatedUser ? 'activated' : ''}
+            disabled={!isValidatedUser}
+            type="button"
+            value="로그인"
+            onClick={goToMain}
+          />
+        </form>
+        <p>비밀번호를 잊으셨나요?</p>
+      </div>
+    </div>
+  );
+};
+
+export default Login;

리뷰: - 불필요한 주석처리는 삭제해주세요!
- 더불어서 className 자체를 '색'으로 지정하는 것은 좋은 네이밍은 아닌 것 같습니다. 어떤 네이밍이 적합할지 고민하셔서 , 해당 요소가 무엇인지를 잘 나타내는 네이밍으로 수정해주세요. 
- 플러스! 여기에도 사용하지 않아도 되는 삼항연사자가 숨어있네요! 확인해주세요
@@ -0,0 +1,101 @@
+@use '../../../../styles/mixin.scss' as mixin;
+@use '../../../../styles/color.scss' as clr;
+
+.feeds {
+  position: relative;
+  top: 77px;
+  width: 60%;
+  margin-bottom: 80px;
+  background: #fff;
+
+  .top_menu {
+    @include mixin.flex($justify: space-between, $align: center);
+    height: 82px;
+
+    span {
+      @include mixin.flex($align: center);
+      margin-left: 20px;
+      font-weight: bold;
+
+      &:last-child {
+        margin-right: 22px;
+        color: #2a2a2a;
+        font-size: 35px;
+      }
+
+      img {
+        width: 42px;
+        height: 42px;
+        border-radius: 50%;
+        margin-right: 20px;
+      }
+    }
+  }
+
+  article {
+    img {
+      width: 100%;
+      height: 818px;
+    }
+
+    .bottom_menu {
+      @include mixin.flex($align: center);
+      position: relative;
+      height: 62px;
+
+      span > svg {
+        margin: 0 10px;
+        font-size: 32px;
+        color: #414141;
+      }
+
+      span:last-child > svg {
+        position: absolute;
+        top: 15px;
+        right: 0;
+      }
+
+      span:first-child > svg > path {
+        color: #eb4b59;
+      }
+    }
+  }
+
+  .comments {
+    display: flex;
+    flex-direction: column;
+
+    span {
+      padding-left: 10px;
+    }
+  }
+
+  .add_comments {
+    position: absolute;
+    @include mixin.flex($justify: space-between, $align: center);
+    bottom: -62px;
+    width: 100%;
+    height: 62px;
+    border-top: 1px solid clr.$clr-border;
+    font-size: 14px;
+
+    input {
+      width: 90%;
+      height: 50%;
+      margin-left: 15px;
+      border: none;
+    }
+
+    button {
+      margin-right: 15px;
+      border: none;
+      color: clr.$clr-primary;
+      opacity: 0.5;
+      background: #fff;
+
+      &.activated {
+        opacity: 1;
+      }
+    }
+  }
+}

리뷰: 이렇게 각 요소에 background color를 각각 다 넣어주신 이유가 있으실까요?
@@ -0,0 +1,92 @@
+{
+  "stories": [
+    { "imgUrl": "images/joonyoung/stories/story1.jpg", "username": "Enna" },
+    { "imgUrl": "images/joonyoung/stories/story2.jpg", "username": "Jesy" },
+    { "imgUrl": "images/joonyoung/stories/story3.jpg", "username": "Denial" },
+    { "imgUrl": "images/joonyoung/stories/story4.jpg", "username": "Meow" },
+    {
+      "imgUrl": "images/joonyoung/stories/story5.jpg",
+      "username": "Christina"
+    },
+    { "imgUrl": "images/joonyoung/stories/story6.jpg", "username": "Mally" },
+    { "imgUrl": "images/joonyoung/stories/story7.jpg", "username": "Karel" },
+    { "imgUrl": "images/joonyoung/stories/story8.jpg", "username": "Bred" },
+    { "imgUrl": "images/joonyoung/stories/story9.jpg", "username": "Andrew" },
+    { "imgUrl": "images/joonyoung/stories/story10.jpg", "username": "Emily" }
+  ],
+  "otherProfileInfos": [
+    {
+      "id": 1,
+      "username": "wecode28기",
+      "description": "wecode님외 36명이 follow"
+    },
+    {
+      "id": 2,
+      "username": "Younha",
+      "description": "Enna Kim외 10명이 follow"
+    },
+    {
+      "id": 3,
+      "username": "wework",
+      "description": "wecode28님외 236명이 follow"
+    }
+  ],
+  "feed": [
+    {
+      "id": 1,
+      "feedUpLoader": "Enna",
+      "uploaderProfileImg": "./images/joonyoung/stories/story1.jpg",
+      "uploaderInfo": "Enna's Feed icon",
+      "carouselImages": [
+        "./images/joonyoung/feed/feed1/pexels-fauxels-3184291.jpg",
+        "./images/joonyoung/feed/feed1/pexels-fauxels-3184302.jpg",
+        "./images/joonyoung/feed/feed1/pexels-fauxels-3184433.jpg",
+        "./images/joonyoung/feed/feed1/pexels-min-an-853168.jpg",
+        "./images/joonyoung/feed/feed1/pexels-this-is-zun-1116302.jpg"
+      ],
+      "likes": 1129,
+      "comments": [
+        {
+          "id": 1,
+          "user": "28기 양대영님",
+          "comment": "여러분 치킨계 쓰셔야 합니다..!! 👀 👀"
+        },
+        {
+          "id": 2,
+          "user": "28기 이아영님",
+          "comment": "대영님도 안 쓰셨잖아요?! 🤭"
+        },
+        {
+          "id": 3,
+          "user": "28기 박윤국님",
+          "comment": "🎸 🎸 🎸 (신곡 홍보중)"
+        }
+      ]
+    },
+    {
+      "id": 2,
+      "feedUpLoader": "James",
+      "uploaderProfileImg": "./images/joonyoung/stories/story2.jpg",
+      "uploaderInfo": "It's jesy!",
+      "carouselImages": [
+        "./images/joonyoung/feed/feed2/handtohand.jpg",
+        "./images/joonyoung/feed/feed2/branch.jpg",
+        "./images/joonyoung/feed/feed2/pexels-te-lensfix-1371360.jpg",
+        "./images/joonyoung/feed/feed2/pexels-victor-freitas-803105.jpg"
+      ],
+      "likes": 1129,
+      "comments": [
+        {
+          "id": 1,
+          "user": "멘토 김INTP님",
+          "comment": "(안경을 치켜 올리며) git branch merge"
+        },
+        {
+          "id": 2,
+          "user": "28기 이ESTJ님",
+          "comment": "아니 계획을 안짜고 여행을 간다고?"
+        }
+      ]
+    }
+  ]
+}

리뷰: mockData king 준영님👍 정말 감사합니다. 세부 내용은 임의로 변경한 후, 데이터의 형식좀 빌려다 쓰겠습니다 감사합니다!
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>react-shopping-products</title>
+    <script type="module" crossorigin src="/react-shopping-products/dist/assets/index-B5NHo3G9.js"></script>
+    <link rel="stylesheet" crossorigin href="/react-shopping-products/dist/assets/index-BlzNY9oW.css">
+  </head>
+  <body>
+    <div id="root"></div>
+  </body>
+</html>

리뷰: dist 폴더가 git ignore되지 않았군용
@@ -0,0 +1,10 @@
+/**
+ * generateBasicToken - Basic auth를 위한 토큰을 만드는 함수입니다.
+ * @param {string} userId - USERNAME입니다.
+ * @param {string} userPassword - PASSWORD입니다.
+ * @returns {string}
+ */
+export function generateBasicToken(userId: string, userPassword: string): string {
+  const token = btoa(`${userId}:${userPassword}`);
+  return `Basic ${token}`;
+}

리뷰: 섬세한 jsdocs 👍👍👍👍
@@ -0,0 +1,95 @@
+import { CartItemType, ProductType } from '../types';
+import { generateBasicToken } from './auth';
+
+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;
+
+/**
+ * 공통 요청을 처리하는 함수
+ * @param {string} endpoint - API endpoint
+ * @param {RequestInit} options - fetch options
+ * @returns {Response} - fetch response
+ */
+async function makeRequest(endpoint: string, options: RequestInit): Promise<Response> {
+  const token = generateBasicToken(USER_ID, USER_PASSWORD);
+
+  const response = await fetch(`${API_URL}${endpoint}`, {
+    ...options,
+    headers: {
+      ...options.headers,
+      Authorization: token,
+      'Content-Type': 'application/json',
+    },
+  });
+
+  if (!response.ok) {
+    throw new Error(`Failed to ${options.method?.toLowerCase()} ${endpoint}`);
+  }
+
+  return response;
+}
+
+interface FetchProductsParams {
+  category?: string;
+  page?: number;
+  size?: number;
+  sort?: string;
+}
+
+/**
+ * fetchCartItems - API에서 상품 목록을 fetch합니다.
+ * @returns {Promise<ProductType[]>}
+ */
+export async function fetchProducts(params: FetchProductsParams = {}): Promise<ProductType[]> {
+  const queryString = Object.entries(params)
+    .map(([key, value]) => {
+      if (key === 'category' && value === 'all') {
+        return;
+      } else {
+        return `${key}=${value}`;
+      }
+    })
+    .join('&');
+  const response = await makeRequest(`/products?${queryString}`, {
+    method: 'GET',
+  });
+  const data = await response.json();
+
+  return data.content;
+}
+
+/**
+ * addCartItem - 선택한 상품을 장바구니에 추가합니다.
+ * @returns {Promise<void>}
+ */
+export async function addCartItem(productId: number): Promise<void> {
+  await makeRequest('/cart-items', {
+    method: 'POST',
+    body: JSON.stringify({ productId }),
+  });
+}
+
+/**
+ * fetchCartItem - API에서 장바구니 목록을 fetch합니다.
+ * @returns {Promise<CartItemType[]>}
+ */
+export async function fetchCartItem(): Promise<CartItemType[]> {
+  const response = await makeRequest('/cart-items?page=0&size=100', {
+    method: 'GET',
+  });
+
+  const data = await response.json();
+
+  return data.content;
+}
+
+/**
+ * deleteCartItem - 선택한 상품을 장바구니에서 삭제합니다.
+ * @returns {Promise<void>}
+ */
+export async function deleteCartItem(cartItemId: number): Promise<void> {
+  await makeRequest(`/cart-items/${cartItemId}`, {
+    method: 'DELETE',
+  });
+}

리뷰: 공통 로직을 묶어주는 것 너무 좋습니다~!
@@ -0,0 +1,95 @@
+import { CartItemType, ProductType } from '../types';
+import { generateBasicToken } from './auth';
+
+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;
+
+/**
+ * 공통 요청을 처리하는 함수
+ * @param {string} endpoint - API endpoint
+ * @param {RequestInit} options - fetch options
+ * @returns {Response} - fetch response
+ */
+async function makeRequest(endpoint: string, options: RequestInit): Promise<Response> {
+  const token = generateBasicToken(USER_ID, USER_PASSWORD);
+
+  const response = await fetch(`${API_URL}${endpoint}`, {
+    ...options,
+    headers: {
+      ...options.headers,
+      Authorization: token,
+      'Content-Type': 'application/json',
+    },
+  });
+
+  if (!response.ok) {
+    throw new Error(`Failed to ${options.method?.toLowerCase()} ${endpoint}`);
+  }
+
+  return response;
+}
+
+interface FetchProductsParams {
+  category?: string;
+  page?: number;
+  size?: number;
+  sort?: string;
+}
+
+/**
+ * fetchCartItems - API에서 상품 목록을 fetch합니다.
+ * @returns {Promise<ProductType[]>}
+ */
+export async function fetchProducts(params: FetchProductsParams = {}): Promise<ProductType[]> {
+  const queryString = Object.entries(params)
+    .map(([key, value]) => {
+      if (key === 'category' && value === 'all') {
+        return;
+      } else {
+        return `${key}=${value}`;
+      }
+    })
+    .join('&');
+  const response = await makeRequest(`/products?${queryString}`, {
+    method: 'GET',
+  });
+  const data = await response.json();
+
+  return data.content;
+}
+
+/**
+ * addCartItem - 선택한 상품을 장바구니에 추가합니다.
+ * @returns {Promise<void>}
+ */
+export async function addCartItem(productId: number): Promise<void> {
+  await makeRequest('/cart-items', {
+    method: 'POST',
+    body: JSON.stringify({ productId }),
+  });
+}
+
+/**
+ * fetchCartItem - API에서 장바구니 목록을 fetch합니다.
+ * @returns {Promise<CartItemType[]>}
+ */
+export async function fetchCartItem(): Promise<CartItemType[]> {
+  const response = await makeRequest('/cart-items?page=0&size=100', {
+    method: 'GET',
+  });
+
+  const data = await response.json();
+
+  return data.content;
+}
+
+/**
+ * deleteCartItem - 선택한 상품을 장바구니에서 삭제합니다.
+ * @returns {Promise<void>}
+ */
+export async function deleteCartItem(cartItemId: number): Promise<void> {
+  await makeRequest(`/cart-items/${cartItemId}`, {
+    method: 'DELETE',
+  });
+}

리뷰: 장바구니에 100개 넘게 담을 수 있는 case가 존재할 것 같아요~!
@@ -0,0 +1,50 @@
+.loaderContainer {
+  height: 60px;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+}
+
+.loader {
+  width: 60px;
+  display: flex;
+  justify-content: space-evenly;
+}
+
+.ball {
+  list-style: none;
+  width: 12px;
+  height: 12px;
+  border-radius: 50%;
+  background-color: black;
+}
+
+.ball:nth-child(1) {
+  animation: bounce1 0.4s ease-in-out infinite;
+}
+
+@keyframes bounce1 {
+  50% {
+    transform: translateY(-18px);
+  }
+}
+
+.ball:nth-child(2) {
+  animation: bounce2 0.4s ease-in-out 0.2s infinite;
+}
+
+@keyframes bounce2 {
+  50% {
+    transform: translateY(-18px);
+  }
+}
+
+.ball:nth-child(3) {
+  animation: bounce3 0.4s ease-in-out 0.4s infinite;
+}
+
+@keyframes bounce3 {
+  50% {
+    transform: translateY(-18px);
+  }
+}

리뷰: 귀여운 애니메이션을 만들었군용 🚀🚀🚀
@@ -0,0 +1,15 @@
+export const productCategories = {
+  all: '전체',
+  fashion: '패션',
+  beverage: '음료',
+  electronics: '전자제품',
+  kitchen: '주방용품',
+  fitness: '피트니스',
+  books: '도서',
+} as const;
+
+export const sortOptions = { priceAsc: '낮은 가격순', priceDesc: '높은 가격순' } as const;
+
+export const FIRST_FETCH_PAGE = 0;
+export const FIRST_FETCH_SIZE = 20;
+export const AFTER_FETCH_SIZE = 4;

리뷰: 상수화 👍👍👍
@@ -0,0 +1,32 @@
+import React, { createContext, useState, useCallback, ReactNode } from 'react';
+import ErrorToast from '../components/ErrorToast/ErrorToast';
+
+export interface ToastContextType {
+  showToast: (message: string) => void;
+}
+
+export const ToastContext = createContext<ToastContextType | undefined>(undefined);
+
+interface ToastProviderProps {
+  children: ReactNode;
+}
+
+export const ToastProvider: React.FC<ToastProviderProps> = ({ children }) => {
+  const [isOpen, setIsOpen] = useState(false);
+  const [message, setMessage] = useState('');
+
+  const showToast = useCallback((message: string) => {
+    setMessage(message);
+    setIsOpen(true);
+    setTimeout(() => {
+      setIsOpen(false);
+    }, 3000);
+  }, []);
+
+  return (
+    <ToastContext.Provider value={{ showToast }}>
+      {children}
+      {isOpen && <ErrorToast message={message} />}
+    </ToastContext.Provider>
+  );
+};

리뷰: 별도의 Provider로 만들어 준 것 너무 좋네요