ewhk9887/korean_code_reviews_from_github huggingface dataset:
@@ -0,0 +1,76 @@
+package christmas.domain.entity;
+
+import static christmas.domain.verifier.VisitDateVerifier.VISIT_DATE_VERIFIER;
+import static christmas.system.Constant.CHRISTMAS;
+import static christmas.system.Constant.FIRST_DAY_OF_MONTH;
+
+public class Day {
+
+    private static final int WEEK_LENGTH = 7;
+    private final int day;
+
+    public Day(int day) {
+        VISIT_DATE_VERIFIER.validateInputInDomain(day);
+        this.day = day;
+    }
+
+    public int getDay() {
+        return day;
+    }
+
+    public boolean isChristmasSeason() {
+        if (day >= FIRST_DAY_OF_MONTH && day <= CHRISTMAS) {
+            return true;
+        }
+        return false;
+    }
+
+    public boolean isWeekday() {
+        if (!(isFriday() || isSaturday())) {
+            return true;
+        }
+        return false;
+    }
+
+    public boolean isWeekend() {
+        if (isFriday() || isSaturday()) {
+            return true;
+        }
+        return false;
+    }
+
+    public boolean isSpecialDay() {
+        if (isSunday() || isChristmas()) {
+            return true;
+        }
+        return false;
+    }
+
+    private boolean isFriday() {
+        if (day % WEEK_LENGTH == 1) {
+            return true;
+        }
+        return false;
+    }
+
+    private boolean isSaturday() {
+        if (day % WEEK_LENGTH == 2) {
+            return true;
+        }
+        return false;
+    }
+
+    private boolean isSunday() {
+        if (day % WEEK_LENGTH == 3) {
+            return true;
+        }
+        return false;
+    }
+
+    private boolean isChristmas() {
+        if (day == CHRISTMAS) {
+            return true;
+        }
+        return false;
+    }
+}

리뷰: 오... 감사합니다.! 리펙토링 해볼게요
@@ -0,0 +1,12 @@
+package christmas.domain.util;
+
+
+import java.text.DecimalFormat;
+
+public final class Util {
+    public static String createFormattedAmount(int target) {
+        DecimalFormat formatter = new DecimalFormat("###,##0");
+        return formatter.format(target);
+    }
+
+}

리뷰: 예를들어, 
Input: 샴페인-
이렇게 입력된 경우, orderMenuVerifier의 checkMenuFormat 메소드에 의해 올바른 메뉴 형식이 아닌 예외처리가 되어져버려
위의 Util메소드를 호출하는 경우가 없도록 구현했습니다.
<img width="474" alt="image" src="https://github.com/Hoo-sung/java-christmas-6-Hoo-sung/assets/121723421/e2a0a147-e665-4a6a-94ce-3876632e0d0b">
이렇게 orderMenuVerifier로 검증을 마치고 Util class를 이용해서 order객체를 반환하도록 했습니다.
@@ -1,36 +1,40 @@
 package com.example.project.compile.controller.rest;
 
+import com.example.project.auth.service.AuthTokenService;
 import com.example.project.common.dto.ResponseDto;
 import com.example.project.common.dto.ResponseMessage;
-import com.example.project.solution.dto.request.user.SolutionCompileRequest;
+import com.example.project.common.util.HeaderUtil;
+import com.example.project.compile.dto.CompileRequest;
 import com.example.project.compile.service.CompileService;
+import com.example.project.error.dto.ErrorMessage;
+import com.example.project.error.dto.ErrorResponseDto;
+import jakarta.servlet.http.HttpServletRequest;
 import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
 import org.springframework.http.ResponseEntity;
-import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.*;
 
 import java.io.IOException;
 
+@Slf4j
 @RestController
 @RequiredArgsConstructor
 @RequestMapping("/api/compile")
 public class CompileApiController {
+
     private final CompileService compileService;
+    private final AuthTokenService authTokenService;
 
     @PostMapping
-    @PreAuthorize("hasAnyRole('USER', 'ADMIN')")
-    public ResponseEntity<?> compileCode(@RequestBody SolutionCompileRequest request) {
-        try {
-            String result = compileService.compileAndRun(request.getLanguage(), request.getCode());
+    public ResponseEntity<?> compileCode(@RequestBody CompileRequest request, HttpServletRequest httpServletRequest, @Value("${compile.url}") String codeDir) throws IOException {
+        if (authTokenService.isValidateToken(HeaderUtil.resolveToken(httpServletRequest))) {
+            String result = compileService.compileAndRun(
+                    request.getLanguage(),
+                    request.getCode(),
+                    codeDir
+            );
             return ResponseDto.toResponseEntity(ResponseMessage.COMPILE_SUCCESS, result);
-        } catch (IllegalArgumentException e) {
-            return ResponseDto.toResponseEntity(ResponseMessage.INVALID_LANGUAGE, e.getMessage());
-        } catch (IOException e) {
-            return ResponseDto.toResponseEntity(ResponseMessage.IO_ERROR, e.getMessage());
-        } catch (InterruptedException e) {
-            return ResponseDto.toResponseEntity(ResponseMessage.EXECUTION_INTERRUPTED, e.getMessage());
-        } catch (Exception e) {
-            return ResponseDto.toResponseEntity(ResponseMessage.GENERAL_COMPILE_ERROR, e.getMessage());
-        }
+        } else return ErrorResponseDto.of(ErrorMessage.NOT_FOUND_CLIENT_ID_HEADER);
     }
 }
\ No newline at end of file

리뷰: HttpServletRequest만 받아오면 토큰 값을 따로 받아오지 않아도 됩니다
@@ -0,0 +1,37 @@
+package com.example.project.compile.service;
+
+import com.example.project.compile.domain.CompileLanguage;
+import com.example.project.error.dto.ErrorMessage;
+import org.springframework.stereotype.Service;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.InvalidPathException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.UUID;
+
+@Service
+public class FileService {
+
+    public Path createCodeFile(String code, CompileLanguage compileLanguage, String codeDir) throws IOException {
+        try {
+            Path codePath = Paths.get(codeDir);
+            if (!Files.exists(codePath)) {
+                Files.createDirectories(codePath);
+            }
+
+            //UUID를 사용하는 이유는  고유한 파일 이름을 생성하여 파일의 충돌을 방지하기 위함입니다.
+            String filename = "Main_" + UUID.randomUUID() + compileLanguage.getExtension();
+
+            Path filePath = codePath.resolve(filename);
+            Files.writeString(filePath, code);
+            return filePath;
+
+        } catch (InvalidPathException e) {
+            throw new IOException(ErrorMessage.INVALID_PATH_EXCEPTION.getMessage(), e);
+        } catch (SecurityException e) {
+            throw new IOException(ErrorMessage.ACCESS_PERMISSION_EXCEPTION.getMessage(), e);
+        }
+    }
+}
\ No newline at end of file

리뷰: yml 에 설정한 값을 사용해주세요.
그리고 static을 사용할 필요는 없어보입니다.
@@ -1,36 +1,40 @@
 package com.example.project.compile.controller.rest;
 
+import com.example.project.auth.service.AuthTokenService;
 import com.example.project.common.dto.ResponseDto;
 import com.example.project.common.dto.ResponseMessage;
-import com.example.project.solution.dto.request.user.SolutionCompileRequest;
+import com.example.project.common.util.HeaderUtil;
+import com.example.project.compile.dto.CompileRequest;
 import com.example.project.compile.service.CompileService;
+import com.example.project.error.dto.ErrorMessage;
+import com.example.project.error.dto.ErrorResponseDto;
+import jakarta.servlet.http.HttpServletRequest;
 import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
 import org.springframework.http.ResponseEntity;
-import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.*;
 
 import java.io.IOException;
 
+@Slf4j
 @RestController
 @RequiredArgsConstructor
 @RequestMapping("/api/compile")
 public class CompileApiController {
+
     private final CompileService compileService;
+    private final AuthTokenService authTokenService;
 
     @PostMapping
-    @PreAuthorize("hasAnyRole('USER', 'ADMIN')")
-    public ResponseEntity<?> compileCode(@RequestBody SolutionCompileRequest request) {
-        try {
-            String result = compileService.compileAndRun(request.getLanguage(), request.getCode());
+    public ResponseEntity<?> compileCode(@RequestBody CompileRequest request, HttpServletRequest httpServletRequest, @Value("${compile.url}") String codeDir) throws IOException {
+        if (authTokenService.isValidateToken(HeaderUtil.resolveToken(httpServletRequest))) {
+            String result = compileService.compileAndRun(
+                    request.getLanguage(),
+                    request.getCode(),
+                    codeDir
+            );
             return ResponseDto.toResponseEntity(ResponseMessage.COMPILE_SUCCESS, result);
-        } catch (IllegalArgumentException e) {
-            return ResponseDto.toResponseEntity(ResponseMessage.INVALID_LANGUAGE, e.getMessage());
-        } catch (IOException e) {
-            return ResponseDto.toResponseEntity(ResponseMessage.IO_ERROR, e.getMessage());
-        } catch (InterruptedException e) {
-            return ResponseDto.toResponseEntity(ResponseMessage.EXECUTION_INTERRUPTED, e.getMessage());
-        } catch (Exception e) {
-            return ResponseDto.toResponseEntity(ResponseMessage.GENERAL_COMPILE_ERROR, e.getMessage());
-        }
+        } else return ErrorResponseDto.of(ErrorMessage.NOT_FOUND_CLIENT_ID_HEADER);
     }
 }
\ No newline at end of file

리뷰: 컴파일하는 서버에서도 JWT 토큰을 가지고 유효한 접근인지 아닌지 판단해서 컴파일할 수 있도록 해주세요
@@ -0,0 +1,37 @@
+package com.example.project.compile.service;
+
+import com.example.project.compile.domain.CompileLanguage;
+import com.example.project.error.dto.ErrorMessage;
+import org.springframework.stereotype.Service;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.InvalidPathException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.UUID;
+
+@Service
+public class FileService {
+
+    public Path createCodeFile(String code, CompileLanguage compileLanguage, String codeDir) throws IOException {
+        try {
+            Path codePath = Paths.get(codeDir);
+            if (!Files.exists(codePath)) {
+                Files.createDirectories(codePath);
+            }
+
+            //UUID를 사용하는 이유는  고유한 파일 이름을 생성하여 파일의 충돌을 방지하기 위함입니다.
+            String filename = "Main_" + UUID.randomUUID() + compileLanguage.getExtension();
+
+            Path filePath = codePath.resolve(filename);
+            Files.writeString(filePath, code);
+            return filePath;
+
+        } catch (InvalidPathException e) {
+            throw new IOException(ErrorMessage.INVALID_PATH_EXCEPTION.getMessage(), e);
+        } catch (SecurityException e) {
+            throw new IOException(ErrorMessage.ACCESS_PERMISSION_EXCEPTION.getMessage(), e);
+        }
+    }
+}
\ No newline at end of file

리뷰: 변수명은 카멜케이스를 지켜서 작성해주세요
@@ -0,0 +1,54 @@
+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}`;
+
+try {
+  if (!API_URL || !USER_ID || !USER_PASSWORD) {
+    throw new Error(
+      "API_URL, USER_ID, PASSWORD environment variables are not set",
+    );
+  }
+} catch (error) {
+  console.log(error);
+}
+
+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);
+
+  try {
+    if (!response.ok) {
+      throw new Error(`Failed to ${options.method} ${path}`);
+    }
+  } catch (error) {
+    console.log(error);
+  }
+
+  return response;
+};
+
+const requestBuilder = ({ method, body }: RequestOptions): RequestInit => {
+  const token = generateBasicToken(USER_ID, USER_PASSWORD);
+  const headers: HeadersInit = {
+    "Content-Type": "application/json",
+    Authorization: token,
+  };
+
+  return {
+    method,
+    headers,
+    body: body ? JSON.stringify(body) : undefined,
+  };
+};

리뷰: ```
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}`;
```
이 부분은 상수 파일에서 따로 관리해보는 것은 어떨까요?

`API_URL `이 아이는 endPoint, fetchWithAuth에서 두 번 선언되네요
@@ -0,0 +1,123 @@
+import { RULE } from "@constants/rules";
+import {
+  CART_ITEMS_COUNTS_ENDPOINT,
+  CART_ITEMS_ENDPOINT,
+  PRODUCTS_ENDPOINT,
+} from "./endpoints";
+import { fetchWithAuth } from "./fetchWithAuth";
+
+/**
+ * @example
+ * const params: QueryParams = {
+ *  category: 'fashion',
+ *  page: 1,
+ *  size: 20,
+ *  sort: ['price', 'asc']
+ * }
+ */
+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) => {
+  return Object.entries(params)
+    .filter(([, value]) => value !== undefined)
+    .map(([key, value]) => {
+      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 = {}): Promise<Product[]> => {
+  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.content;
+};
+
+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 (): Promise<number> => {
+  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 가 많아진다면, api/index.ts 코드가 많아지겠네요.

관련있는 api 요청 끼리 묶어서 파일로 관리하고, index.ts에서 해당 파일들의 api 요청 메서드를 export 하는 것을 어떨까요?

```dash
api
 ㄴ  cart 
 ㄴ products
 ㄴ index
```
@@ -0,0 +1,61 @@
+import { forwardRef, useContext } from "react";
+import * as PI from "./ProductItem.style";
+import CartControlButton from "../../Button/CartControlButton";
+import { deleteProductInCart, postProductInCart } from "@api/index";
+import { useError } from "@hooks/index";
+import { CartItemsContext } from "@context/CartItemsContext";
+
+interface ProductProps {
+  product: Product;
+}
+
+const ProductItem = forwardRef<HTMLDivElement, ProductProps>(
+  ({ product }, ref) => {
+    const { cartItems, refreshCartItems } = useContext(CartItemsContext);
+
+    const cartItemIds = cartItems.map((item) => item.product.id);
+    const isInCart = cartItemIds.includes(product.id);
+
+    const { showError } = useError();
+
+    const handleIsInCart = async () => {
+      try {
+        if (!isInCart) {
+          await postProductInCart(product.id);
+          refreshCartItems();
+          return;
+        }
+
+        const filteredItem = cartItems.find(
+          (item) => item.product.id === product.id,
+        );
+        if (filteredItem) {
+          await deleteProductInCart(filteredItem.id);
+          refreshCartItems();
+        }
+      } catch (error) {
+        if (error instanceof Error) {
+          showError(error.message);
+        }
+      }
+    };
+
+    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={handleIsInCart} isInCart={isInCart} />
+        </PI.ProductGroup>
+      </PI.ProductItemStyle>
+    );
+  },
+);
+
+export default ProductItem;

리뷰: 장바구니에 담는 로직과 빼는 로직을 함수로 분리하고,
if  else대신 early  return 을 사용해보는 것을 어떨까요?
@@ -0,0 +1,61 @@
+import { forwardRef, useContext } from "react";
+import * as PI from "./ProductItem.style";
+import CartControlButton from "../../Button/CartControlButton";
+import { deleteProductInCart, postProductInCart } from "@api/index";
+import { useError } from "@hooks/index";
+import { CartItemsContext } from "@context/CartItemsContext";
+
+interface ProductProps {
+  product: Product;
+}
+
+const ProductItem = forwardRef<HTMLDivElement, ProductProps>(
+  ({ product }, ref) => {
+    const { cartItems, refreshCartItems } = useContext(CartItemsContext);
+
+    const cartItemIds = cartItems.map((item) => item.product.id);
+    const isInCart = cartItemIds.includes(product.id);
+
+    const { showError } = useError();
+
+    const handleIsInCart = async () => {
+      try {
+        if (!isInCart) {
+          await postProductInCart(product.id);
+          refreshCartItems();
+          return;
+        }
+
+        const filteredItem = cartItems.find(
+          (item) => item.product.id === product.id,
+        );
+        if (filteredItem) {
+          await deleteProductInCart(filteredItem.id);
+          refreshCartItems();
+        }
+      } catch (error) {
+        if (error instanceof Error) {
+          showError(error.message);
+        }
+      }
+    };
+
+    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={handleIsInCart} isInCart={isInCart} />
+        </PI.ProductGroup>
+      </PI.ProductItemStyle>
+    );
+  },
+);
+
+export default ProductItem;

리뷰: alt에 대해서 저번 미션에서 하루에게 받은 피드백을 공유할 게요.

image의 alt는 '흰 운동화에 검은색 줄이 하나 들어간 아디다스 운동화'처럼 해당 이미지에 대한 구체적인 설명이 들어가야해요.
하지만 현재 서버에서 주는 자세한 상품 설명이 없는 상황에서는`alt=''"`를 사용하는 것이 좋다고 하네요.

alt를 사용하지 않는 것과 alt을 빈문자열로 두는 것의 차이를 공부해본다면, 소하에게 도움이 될거에요. 😆
@@ -0,0 +1,44 @@
+import { FILTER_CATEGORIES, SORT_PRICE } from "@constants/rules";
+import * as PLH from "./ProductListHeader.style";
+
+interface ProductListHeaderProps {
+  handleCategory: (category: Category) => void;
+  handleSort: (sort: Sort) => void;
+}
+
+const ProductListHeader = ({
+  handleCategory,
+  handleSort,
+}: ProductListHeaderProps) => {
+  return (
+    <PLH.Header>
+      <PLH.Title>bpple 상품 목록</PLH.Title>
+      <PLH.SelectBoxGroup>
+        <PLH.SelectBox
+          name=""
+          id=""
+          onChange={(e) => handleCategory(e.target.value as Category)}
+        >
+          {Object.entries(FILTER_CATEGORIES).map(([key, value]) => (
+            <option value={key} key={key}>
+              {value}
+            </option>
+          ))}
+        </PLH.SelectBox>
+        <PLH.SelectBox
+          name=""
+          id=""
+          onChange={(e) => handleSort(e.target.value as Sort)}
+        >
+          {Object.entries(SORT_PRICE).map(([key, value]) => (
+            <option value={key} key={key}>
+              {value}
+            </option>
+          ))}
+        </PLH.SelectBox>
+      </PLH.SelectBoxGroup>
+    </PLH.Header>
+  );
+};
+
+export default ProductListHeader;

리뷰: selectbox 둘 다 name,id를  빈문자열로 둔 이유가 있을까요?
@@ -0,0 +1,44 @@
+import { FILTER_CATEGORIES, SORT_PRICE } from "@constants/rules";
+import * as PLH from "./ProductListHeader.style";
+
+interface ProductListHeaderProps {
+  handleCategory: (category: Category) => void;
+  handleSort: (sort: Sort) => void;
+}
+
+const ProductListHeader = ({
+  handleCategory,
+  handleSort,
+}: ProductListHeaderProps) => {
+  return (
+    <PLH.Header>
+      <PLH.Title>bpple 상품 목록</PLH.Title>
+      <PLH.SelectBoxGroup>
+        <PLH.SelectBox
+          name=""
+          id=""
+          onChange={(e) => handleCategory(e.target.value as Category)}
+        >
+          {Object.entries(FILTER_CATEGORIES).map(([key, value]) => (
+            <option value={key} key={key}>
+              {value}
+            </option>
+          ))}
+        </PLH.SelectBox>
+        <PLH.SelectBox
+          name=""
+          id=""
+          onChange={(e) => handleSort(e.target.value as Sort)}
+        >
+          {Object.entries(SORT_PRICE).map(([key, value]) => (
+            <option value={key} key={key}>
+              {value}
+            </option>
+          ))}
+        </PLH.SelectBox>
+      </PLH.SelectBoxGroup>
+    </PLH.Header>
+  );
+};
+
+export default ProductListHeader;

리뷰: selectbox를 하나의 컴포넌트로 분리해도 좋을 것 같아요.
@@ -0,0 +1,54 @@
+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}`;
+
+try {
+  if (!API_URL || !USER_ID || !USER_PASSWORD) {
+    throw new Error(
+      "API_URL, USER_ID, PASSWORD environment variables are not set",
+    );
+  }
+} catch (error) {
+  console.log(error);
+}
+
+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);
+
+  try {
+    if (!response.ok) {
+      throw new Error(`Failed to ${options.method} ${path}`);
+    }
+  } catch (error) {
+    console.log(error);
+  }
+
+  return response;
+};
+
+const requestBuilder = ({ method, body }: RequestOptions): RequestInit => {
+  const token = generateBasicToken(USER_ID, USER_PASSWORD);
+  const headers: HeadersInit = {
+    "Content-Type": "application/json",
+    Authorization: token,
+  };
+
+  return {
+    method,
+    headers,
+    body: body ? JSON.stringify(body) : undefined,
+  };
+};

리뷰: 오, 바로 적극 반영 하겠습니다!