ewhk9887/korean_code_reviews_from_github huggingface dataset:
@@ -1,38 +1,53 @@
 package nextstep.security.authorization.web;
 
 import java.util.Collection;
+import java.util.Set;
+import nextstep.security.access.RoleHierarchy;
 import nextstep.security.authentication.Authentication;
 import nextstep.security.authentication.AuthenticationException;
 import nextstep.security.authorization.AuthorizationDecision;
 import nextstep.security.authorization.AuthorizationManager;
+import nextstep.security.core.GrantedAuthority;
 
 public class AuthorityAuthorizationManager<T> implements AuthorizationManager<T> {
+    private final RoleHierarchy roleHierarchy;
+    private Collection<String> authorities;
 
-    private final Collection<String> authorities;
+    public AuthorityAuthorizationManager(RoleHierarchy roleHierarchy) {
+        this.roleHierarchy = roleHierarchy;
+        this.authorities = Set.of();
+    }
 
-    public AuthorityAuthorizationManager(Collection<String> authorities) {
+    public AuthorityAuthorizationManager(RoleHierarchy roleHierarchy, Collection<String> authorities) {
+        this.roleHierarchy = roleHierarchy;
         this.authorities = authorities;
     }
 
-
     @Override
     public AuthorizationDecision check(Authentication authentication, T object) {
         if (authentication == null) {
             throw new AuthenticationException();
         }
 
+        if(object instanceof Collection<?>) {
+            this.authorities = (Collection<String>) object;
+        }
+
         boolean hasAuthority = isAuthorized(authentication, authorities);
 
         return AuthorizationDecision.of(hasAuthority);
     }
 
-
     private boolean isAuthorized(Authentication authentication, Collection<String> authorities) {
-        for (String authority : authentication.getAuthorities()) {
-            if (authorities.contains(authority)) {
+        for (GrantedAuthority grantedAuthority : getGrantedAuthorities(authentication)) {
+            if (authorities.contains(grantedAuthority.getAuthority())) {
                 return true;
             }
         }
         return false;
     }
+
+    private Collection<GrantedAuthority> getGrantedAuthorities(Authentication authentication) {
+        return this.roleHierarchy.getReachableGrantedAuthorities(authentication.getAuthorities());
+    }
 }

리뷰: 기능으로 사용하지 않는 출력문인 것 같아요.
@@ -0,0 +1,13 @@
+package nextstep.security.access;
+
+import java.util.Collection;
+import nextstep.security.core.GrantedAuthority;
+
+public final class NullRoleHierarchy implements RoleHierarchy {
+
+    @Override
+    public Collection<GrantedAuthority> getReachableGrantedAuthorities(
+            Collection<GrantedAuthority> authorities) {
+        return authorities;
+    }
+}

리뷰: 기존 스프링시큐리티 코드에서 가져온 부분인데,
생각해보니 저희 구조에선 불필요하네요,, 수정해두겠습니다
@@ -1,16 +1,36 @@
-import MyPlanNav from "./MyPlanNav";
-import Layout from "../Common/Layout";
-import PlanCard from "./Cards/PlanCard";
-import QuotationCardList from "./QuotationCardList";
-import { Plan } from "@/services/planService";
+import { useRouter } from "next/router";
+import MyPlanNav from "@/components/MyPlans/MyPlanNav";
+import Layout from "@/components/Common/Layout";
+import PlanCard from "@/components/MyPlans/Cards/PlanCard";
+import QuotationCardList from "@/components/MyPlans/QuotationCardList";
+import planService, { Plan } from "@/services/planService";
+import Image from "next/image";
+import loading from "@public/assets/icon_loading.gif";
 
 interface PlanData {
   planDetail: Plan;
 }
 
 export default function MyPlanDetail({ planDetail }: PlanData) {
+  const router = useRouter();
+
+  async function handleDeletePlan() {
+    try {
+      await planService.deletePlan(planDetail.id);
+      alert("플랜을 취소하였습니다.");
+    } catch (error) {
+      alert(`플랜 취소를 실패했습니다. 다시 시도해주세요. ${error}`);
+    } finally {
+      router.push("/mytrip-manage/ongoing-plan");
+    }
+  }
+
   if (!planDetail) {
-    return <div>Loading...</div>;
+    return (
+      <div className="flex h-screen items-center justify-center">
+        <Image src={loading} alt="로딩 중" />
+      </div>
+    );
   }
 
   return (
@@ -19,11 +39,23 @@ export default function MyPlanDetail({ planDetail }: PlanData) {
       <Layout bodyClass="bg-gray">
         <div className="my-10 flex-col">
           <div className="my-10 rounded-2xl border-gray-300 bg-color-gray-50 p-5 shadow">
-            <div className="semibold mb-3 text-2xl text-color-black-500">{planDetail.title}</div>
+            <div className="flex justify-between">
+              <div className="semibold mb-3 content-center text-2xl text-color-black-500 mobile-tablet:text-xl">
+                {planDetail.title}
+              </div>
+              {planDetail.status === "PENDING" && (
+                <button
+                  className="semibold mb-3 text-nowrap rounded-lg border-[1px] border-solid border-color-blue-300 bg-color-blue-100 px-[16px] py-[11px] text-lg text-color-blue-300 mobile:text-md tablet:text-lg"
+                  onClick={handleDeletePlan}
+                >
+                  플랜 취소
+                </button>
+              )}
+            </div>
             <PlanCard planDetail={planDetail} />
           </div>
         </div>
-        <QuotationCardList />
+        <QuotationCardList planDetail={planDetail} />
       </Layout>
     </>
   );

리뷰: 뒤로가기 인가요??
@@ -1,16 +1,36 @@
-import MyPlanNav from "./MyPlanNav";
-import Layout from "../Common/Layout";
-import PlanCard from "./Cards/PlanCard";
-import QuotationCardList from "./QuotationCardList";
-import { Plan } from "@/services/planService";
+import { useRouter } from "next/router";
+import MyPlanNav from "@/components/MyPlans/MyPlanNav";
+import Layout from "@/components/Common/Layout";
+import PlanCard from "@/components/MyPlans/Cards/PlanCard";
+import QuotationCardList from "@/components/MyPlans/QuotationCardList";
+import planService, { Plan } from "@/services/planService";
+import Image from "next/image";
+import loading from "@public/assets/icon_loading.gif";
 
 interface PlanData {
   planDetail: Plan;
 }
 
 export default function MyPlanDetail({ planDetail }: PlanData) {
+  const router = useRouter();
+
+  async function handleDeletePlan() {
+    try {
+      await planService.deletePlan(planDetail.id);
+      alert("플랜을 취소하였습니다.");
+    } catch (error) {
+      alert(`플랜 취소를 실패했습니다. 다시 시도해주세요. ${error}`);
+    } finally {
+      router.push("/mytrip-manage/ongoing-plan");
+    }
+  }
+
   if (!planDetail) {
-    return <div>Loading...</div>;
+    return (
+      <div className="flex h-screen items-center justify-center">
+        <Image src={loading} alt="로딩 중" />
+      </div>
+    );
   }
 
   return (
@@ -19,11 +39,23 @@ export default function MyPlanDetail({ planDetail }: PlanData) {
       <Layout bodyClass="bg-gray">
         <div className="my-10 flex-col">
           <div className="my-10 rounded-2xl border-gray-300 bg-color-gray-50 p-5 shadow">
-            <div className="semibold mb-3 text-2xl text-color-black-500">{planDetail.title}</div>
+            <div className="flex justify-between">
+              <div className="semibold mb-3 content-center text-2xl text-color-black-500 mobile-tablet:text-xl">
+                {planDetail.title}
+              </div>
+              {planDetail.status === "PENDING" && (
+                <button
+                  className="semibold mb-3 text-nowrap rounded-lg border-[1px] border-solid border-color-blue-300 bg-color-blue-100 px-[16px] py-[11px] text-lg text-color-blue-300 mobile:text-md tablet:text-lg"
+                  onClick={handleDeletePlan}
+                >
+                  플랜 취소
+                </button>
+              )}
+            </div>
             <PlanCard planDetail={planDetail} />
           </div>
         </div>
-        <QuotationCardList />
+        <QuotationCardList planDetail={planDetail} />
       </Layout>
     </>
   );

리뷰: 절대 경로로 써주시면 나중에 파일 위치가 변해도 문제가 없습니다!
@@ -19,7 +19,8 @@
     "dev": "next dev",
     "build": "next build",
     "start": "next start",
-    "lint": "next lint"
+    "lint": "next lint",
+    "check": "npm run lint && tsc --noEmit && npm run build"
   },
   "eslintConfig": {
     "extends": [

리뷰: next로도 lint를 사용할 수 있군요?
@@ -0,0 +1,22 @@
+import * as yup from 'yup';
+import {ShippingFormData} from '@/features/cart/cartProcess/shoppingCheckOut/ShoppingCheckOut';
+
+const phoneRegex = /^[0-9]{10,11}$/;
+const zipcodeRegex = /^[0-9]{5}$/;
+export const shippingSchema = yup.object().shape({
+  name: yup
+    .string()
+    .required('이름을 입력해주세요')
+    .min(2, '2자 이상 입력해주세요'),
+  phone: yup
+    .string()
+    .required('전화번호를 입력해주세요')
+    .matches(phoneRegex, '올바른 전화번호를 입력해주세요'),
+  address: yup.string().required('주소를 입력해주세요'),
+  city: yup.string().required('도/시를 입력해주세요'),
+  zipcode: yup
+    .string()
+    .required('우편번호를 입력해주세요')
+    .matches(zipcodeRegex, '올바른 우편번호를 입력해주세요'),
+  memo: yup.string(),
+}) as yup.ObjectSchema<ShippingFormData>;

리뷰: 굿. 이거 해내셨네요
@@ -0,0 +1,22 @@
+import * as yup from 'yup';
+import {ShippingFormData} from '@/features/cart/cartProcess/shoppingCheckOut/ShoppingCheckOut';
+
+const phoneRegex = /^[0-9]{10,11}$/;
+const zipcodeRegex = /^[0-9]{5}$/;
+export const shippingSchema = yup.object().shape({
+  name: yup
+    .string()
+    .required('이름을 입력해주세요')
+    .min(2, '2자 이상 입력해주세요'),
+  phone: yup
+    .string()
+    .required('전화번호를 입력해주세요')
+    .matches(phoneRegex, '올바른 전화번호를 입력해주세요'),
+  address: yup.string().required('주소를 입력해주세요'),
+  city: yup.string().required('도/시를 입력해주세요'),
+  zipcode: yup
+    .string()
+    .required('우편번호를 입력해주세요')
+    .matches(zipcodeRegex, '올바른 우편번호를 입력해주세요'),
+  memo: yup.string(),
+}) as yup.ObjectSchema<ShippingFormData>;

리뷰: 정규식을 상수화하면 좀 더 보기 편할 것 같아요
ex) `const MOBILE_REGEX = /^[0-9]{10,11}$/;`
@@ -1,11 +1,8 @@
 import styled from 'styled-components';
 import Image from 'next/image';
 import confirmIcon from 'public/images/shop/confirm.svg';
-import {getRequest, getStoredToken} from '@/utils/apiClient';
-import {API_ENDPOINTS} from '@/config/ApiEndPoints';
-import {OrderResponse} from '@/types/order';
-import {useQuery} from '@tanstack/react-query';
-
+import {useOrderDetail} from '@/hooks/useOrderDetail';
+import ShoppingConfirmItem from './ShoppingConfirmItem';
 const Container = styled.div`
   display: flex;
   flex-direction: column;
@@ -56,16 +53,6 @@ const OrderTitle = styled.span`
   margin: 0 25px;
 `;
 
-const OrderItemContainer = styled.div`
-  display: flex;
-  flex-direction: column;
-  background-color: #fff;
-  border-radius: 15px;
-  padding: 25px 25px 15px;
-  margin-top: 15px;
-  gap: 15px;
-`;
-
 const OrderItemRow = styled.div`
   display: flex;
   flex-direction: row;
@@ -118,30 +105,8 @@ interface ShoppingConfirmProps {
   orderId: string;
 }
 
-const fetchOrderDetail = async (orderId: string): Promise<OrderResponse> => {
-  try {
-    const token = getStoredToken();
-    const response = await getRequest<OrderResponse>(
-      `${API_ENDPOINTS.ORDERS}/${orderId}`,
-      {},
-      token,
-    );
-    return response.data;
-  } catch (error) {
-    throw new Error('주문 정보를 불러오는데 실패했습니다.');
-  }
-};
-
 export default function ShoppingConfirm({orderId}: ShoppingConfirmProps) {
-  const {
-    data: orderDetail,
-    isLoading,
-    error,
-  } = useQuery({
-    queryKey: ['fetchOrderDetail', orderId],
-    queryFn: () => fetchOrderDetail(orderId),
-    enabled: !!orderId,
-  });
+  const {data: orderDetail, isLoading, error} = useOrderDetail(orderId);
 
   if (isLoading) return <div>로딩 중...</div>;
   if (error) return <div>주문 정보를 불러오는데 실패했습니다.</div>;
@@ -163,28 +128,7 @@ export default function ShoppingConfirm({orderId}: ShoppingConfirmProps) {
       </ConfirmContainer>
       <OrderContainer>
         <OrderTitle>주문 내역</OrderTitle>
-        <OrderItemContainer>
-          {orderDetail.items.map(item => (
-            <OrderItemRow key={item.id}>
-              <OrderItemTitle>상품명</OrderItemTitle>
-              <OrderItemValue>{item.product.name}</OrderItemValue>
-            </OrderItemRow>
-          ))}
-          <OrderItemRow>
-            <OrderItemTitle>총 상품 금액</OrderItemTitle>
-            <OrderItemValue>
-              {orderDetail.total_amount.toLocaleString()}원
-            </OrderItemValue>
-          </OrderItemRow>
-          <OrderItemRow>
-            <OrderItemTitle>결제 수단</OrderItemTitle>
-            <OrderItemValue>
-              {orderDetail.payment_method === 'BANK_TRANSFER'
-                ? '현금 결제'
-                : '카드 결제'}
-            </OrderItemValue>
-          </OrderItemRow>
-        </OrderItemContainer>
+        <ShoppingConfirmItem orderDetail={orderDetail} />
         <NameAddressContainer>
           <OrderItemRow>
             <OrderItemTitle>주문 일시</OrderItemTitle>

리뷰: 크으 커스텀훅으로 분리하셨군요
@@ -1,11 +1,8 @@
 import styled from 'styled-components';
 import Image from 'next/image';
 import confirmIcon from 'public/images/shop/confirm.svg';
-import {getRequest, getStoredToken} from '@/utils/apiClient';
-import {API_ENDPOINTS} from '@/config/ApiEndPoints';
-import {OrderResponse} from '@/types/order';
-import {useQuery} from '@tanstack/react-query';
-
+import {useOrderDetail} from '@/hooks/useOrderDetail';
+import ShoppingConfirmItem from './ShoppingConfirmItem';
 const Container = styled.div`
   display: flex;
   flex-direction: column;
@@ -56,16 +53,6 @@ const OrderTitle = styled.span`
   margin: 0 25px;
 `;
 
-const OrderItemContainer = styled.div`
-  display: flex;
-  flex-direction: column;
-  background-color: #fff;
-  border-radius: 15px;
-  padding: 25px 25px 15px;
-  margin-top: 15px;
-  gap: 15px;
-`;
-
 const OrderItemRow = styled.div`
   display: flex;
   flex-direction: row;
@@ -118,30 +105,8 @@ interface ShoppingConfirmProps {
   orderId: string;
 }
 
-const fetchOrderDetail = async (orderId: string): Promise<OrderResponse> => {
-  try {
-    const token = getStoredToken();
-    const response = await getRequest<OrderResponse>(
-      `${API_ENDPOINTS.ORDERS}/${orderId}`,
-      {},
-      token,
-    );
-    return response.data;
-  } catch (error) {
-    throw new Error('주문 정보를 불러오는데 실패했습니다.');
-  }
-};
-
 export default function ShoppingConfirm({orderId}: ShoppingConfirmProps) {
-  const {
-    data: orderDetail,
-    isLoading,
-    error,
-  } = useQuery({
-    queryKey: ['fetchOrderDetail', orderId],
-    queryFn: () => fetchOrderDetail(orderId),
-    enabled: !!orderId,
-  });
+  const {data: orderDetail, isLoading, error} = useOrderDetail(orderId);
 
   if (isLoading) return <div>로딩 중...</div>;
   if (error) return <div>주문 정보를 불러오는데 실패했습니다.</div>;
@@ -163,28 +128,7 @@ export default function ShoppingConfirm({orderId}: ShoppingConfirmProps) {
       </ConfirmContainer>
       <OrderContainer>
         <OrderTitle>주문 내역</OrderTitle>
-        <OrderItemContainer>
-          {orderDetail.items.map(item => (
-            <OrderItemRow key={item.id}>
-              <OrderItemTitle>상품명</OrderItemTitle>
-              <OrderItemValue>{item.product.name}</OrderItemValue>
-            </OrderItemRow>
-          ))}
-          <OrderItemRow>
-            <OrderItemTitle>총 상품 금액</OrderItemTitle>
-            <OrderItemValue>
-              {orderDetail.total_amount.toLocaleString()}원
-            </OrderItemValue>
-          </OrderItemRow>
-          <OrderItemRow>
-            <OrderItemTitle>결제 수단</OrderItemTitle>
-            <OrderItemValue>
-              {orderDetail.payment_method === 'BANK_TRANSFER'
-                ? '현금 결제'
-                : '카드 결제'}
-            </OrderItemValue>
-          </OrderItemRow>
-        </OrderItemContainer>
+        <ShoppingConfirmItem orderDetail={orderDetail} />
         <NameAddressContainer>
           <OrderItemRow>
             <OrderItemTitle>주문 일시</OrderItemTitle>

리뷰: 이렇게 map 을 도는 대상이 길어질 경우 컴포넌트로 분리하면 가독성이 아주 좋아집니다(+렌더링 격리)
@@ -1,11 +1,15 @@
-import {useForm} from 'react-hook-form';
+import {Resolver, useForm, FormProvider} from 'react-hook-form';
 import styled from 'styled-components';
 import {CartResponse} from '@/types/cart';
 import {postRequest} from '@/utils/apiClient';
 import {API_ENDPOINTS} from '@/config/ApiEndPoints';
+import * as yup from 'yup';
+import {yupResolver} from '@hookform/resolvers/yup';
 import ShoppingCheckOutOrder from '../shoppingCheckOut/ShoppingCheckOutOrder';
 import {useState} from 'react';
 import ShoppingCheckOutInfo from '../shoppingCheckOut/ShoppingCheckOutInfo';
+import {shippingSchema} from '@/config/ValidationSchema';
+
 const Container = styled.div`
   display: flex;
   gap: 25px;
@@ -49,14 +53,16 @@ export default function ShoppingCheckOut({
   cart,
   setOrderId,
 }: ShoppingCheckOutProps) {
+  const methods = useForm<ShippingFormData>({
+    resolver: yupResolver(shippingSchema) as Resolver<ShippingFormData>,
+    mode: 'onChange',
+  });
+
   const {
-    register,
-    handleSubmit,
     formState: {errors},
     watch,
-  } = useForm<ShippingFormData>({
-    mode: 'onChange',
-  });
+    handleSubmit,
+  } = methods;
 
   const [isFormError, setIsFormError] = useState(false);
 
@@ -67,7 +73,6 @@ export default function ShoppingCheckOut({
   const zipCode = watch('zipcode');
 
   const hasEmptyFields = !name || !phone || !address || !city || !zipCode;
-
   const hasFormErrors = !!(
     errors.name ||
     errors.phone ||
@@ -79,7 +84,10 @@ export default function ShoppingCheckOut({
   const onSubmit = async (data: ShippingFormData) => {
     try {
       console.log(data);
-      await postRequest(API_ENDPOINTS.SHIPPING_ADDRESS, data);
+      await postRequest({
+        url: API_ENDPOINTS.SHIPPING_ADDRESS,
+        data,
+      });
       console.log('배송 정보 저장 성공');
 
       setIsFormError(false);
@@ -90,22 +98,19 @@ export default function ShoppingCheckOut({
   };
 
   return (
-    <Container>
-      <BillingContainer>
-        <BillingDetail>배송 정보</BillingDetail>
-        <ShoppingCheckOutInfo
-          onSubmit={onSubmit}
-          register={register}
-          errors={errors}
-          handleSubmit={handleSubmit}
+    <FormProvider {...methods}>
+      <Container>
+        <BillingContainer>
+          <BillingDetail>배송 정보</BillingDetail>
+          <ShoppingCheckOutInfo onSubmit={onSubmit} />
+        </BillingContainer>
+        <ShoppingCheckOutOrder
+          onNextStep={onNextStep}
+          cart={cart}
+          isFormError={isFormError || hasEmptyFields || hasFormErrors}
+          setOrderId={setOrderId}
         />
-      </BillingContainer>
-      <ShoppingCheckOutOrder
-        onNextStep={onNextStep}
-        cart={cart}
-        isFormError={isFormError || hasEmptyFields || hasFormErrors}
-        setOrderId={setOrderId}
-      />
-    </Container>
+      </Container>
+    </FormProvider>
   );
 }

리뷰: as로 타입단언 하는 방법밖에 없나요?
@@ -1,11 +1,15 @@
-import {useForm} from 'react-hook-form';
+import {Resolver, useForm, FormProvider} from 'react-hook-form';
 import styled from 'styled-components';
 import {CartResponse} from '@/types/cart';
 import {postRequest} from '@/utils/apiClient';
 import {API_ENDPOINTS} from '@/config/ApiEndPoints';
+import * as yup from 'yup';
+import {yupResolver} from '@hookform/resolvers/yup';
 import ShoppingCheckOutOrder from '../shoppingCheckOut/ShoppingCheckOutOrder';
 import {useState} from 'react';
 import ShoppingCheckOutInfo from '../shoppingCheckOut/ShoppingCheckOutInfo';
+import {shippingSchema} from '@/config/ValidationSchema';
+
 const Container = styled.div`
   display: flex;
   gap: 25px;
@@ -49,14 +53,16 @@ export default function ShoppingCheckOut({
   cart,
   setOrderId,
 }: ShoppingCheckOutProps) {
+  const methods = useForm<ShippingFormData>({
+    resolver: yupResolver(shippingSchema) as Resolver<ShippingFormData>,
+    mode: 'onChange',
+  });
+
   const {
-    register,
-    handleSubmit,
     formState: {errors},
     watch,
-  } = useForm<ShippingFormData>({
-    mode: 'onChange',
-  });
+    handleSubmit,
+  } = methods;
 
   const [isFormError, setIsFormError] = useState(false);
 
@@ -67,7 +73,6 @@ export default function ShoppingCheckOut({
   const zipCode = watch('zipcode');
 
   const hasEmptyFields = !name || !phone || !address || !city || !zipCode;
-
   const hasFormErrors = !!(
     errors.name ||
     errors.phone ||
@@ -79,7 +84,10 @@ export default function ShoppingCheckOut({
   const onSubmit = async (data: ShippingFormData) => {
     try {
       console.log(data);
-      await postRequest(API_ENDPOINTS.SHIPPING_ADDRESS, data);
+      await postRequest({
+        url: API_ENDPOINTS.SHIPPING_ADDRESS,
+        data,
+      });
       console.log('배송 정보 저장 성공');
 
       setIsFormError(false);
@@ -90,22 +98,19 @@ export default function ShoppingCheckOut({
   };
 
   return (
-    <Container>
-      <BillingContainer>
-        <BillingDetail>배송 정보</BillingDetail>
-        <ShoppingCheckOutInfo
-          onSubmit={onSubmit}
-          register={register}
-          errors={errors}
-          handleSubmit={handleSubmit}
+    <FormProvider {...methods}>
+      <Container>
+        <BillingContainer>
+          <BillingDetail>배송 정보</BillingDetail>
+          <ShoppingCheckOutInfo onSubmit={onSubmit} />
+        </BillingContainer>
+        <ShoppingCheckOutOrder
+          onNextStep={onNextStep}
+          cart={cart}
+          isFormError={isFormError || hasEmptyFields || hasFormErrors}
+          setOrderId={setOrderId}
         />
-      </BillingContainer>
-      <ShoppingCheckOutOrder
-        onNextStep={onNextStep}
-        cart={cart}
-        isFormError={isFormError || hasEmptyFields || hasFormErrors}
-        setOrderId={setOrderId}
-      />
-    </Container>
+      </Container>
+    </FormProvider>
   );
 }

리뷰: object로 해주셨군요!
@@ -1,4 +1,4 @@
-import axios, {AxiosResponse} from 'axios';
+import axios, {AxiosResponse, InternalAxiosRequestConfig} from 'axios';
 import {API_BASE_URL} from '@/config/ApiEndPoints';
 import Cookies from 'js-cookie';
 
@@ -9,15 +9,48 @@ const apiClient = axios.create({
   },
 });
 
+apiClient.interceptors.request.use(
+  (config: InternalAxiosRequestConfig) => {
+    const token = Cookies.get('jwt');
+    if (token) {
+      config.headers.Authorization = `Bearer ${token}`;
+    }
+    return config;
+  },
+  error => {
+    return Promise.reject(error);
+  },
+);
+
+apiClient.interceptors.response.use(
+  (response: AxiosResponse) => {
+    return response;
+  },
+  error => {
+    if (error.response?.status === 401) {
+      // TODO: 토큰 만료 처리
+    }
+    return Promise.reject(error);
+  },
+);
+
 export default apiClient;
 
-export const postRequest = async (url: string, data: any) => {
-  const token = Cookies.get('jwt');
-  const response = await apiClient.post(url, data, {
-    headers: {
-      Authorization: `Bearer ${token}`,
-    },
-  });
+interface RequestConfig {
+  params?: Record<string, any>;
+  headers?: Record<string, any>;
+}
+
+export const postRequest = async <T>({
+  url,
+  data,
+  config = {},
+}: {
+  url: string;
+  data: any;
+  config?: RequestConfig;
+}) => {
+  const response = await apiClient.post<T>(url, data, config);
   return response;
 };
 
@@ -27,17 +60,14 @@ export const formDataEntries = (event: React.FormEvent<HTMLFormElement>) => {
   return data;
 };
 
-export const getRequest = async <T>(
-  url: string,
-  params: Record<string, any> = {},
-  token: string = '',
-): Promise<AxiosResponse<T>> => {
-  const response = await apiClient.get<T>(url, {
-    headers: {
-      Authorization: token ? `Bearer ${token}` : '',
-    },
-    params,
-  });
+export const getRequest = async <T>({
+  url,
+  config = {},
+}: {
+  url: string;
+  config?: RequestConfig;
+}): Promise<AxiosResponse<T>> => {
+  const response = await apiClient.get<T>(url, config);
   return response;
 };
 

리뷰: 앞으로 모든 인증은 이쪽에서 처리하게 되겠군요 ㅎㅎ