ewhk9887/korean_code_reviews_from_github huggingface dataset:
@@ -0,0 +1,89 @@
+package store.domain;
+
+import static store.global.constant.MessageConstant.FORMAT;
+
+import java.text.DecimalFormat;
+import java.util.ArrayList;
+import java.util.List;
+
+public class PaymentProductList {
+
+    private static final DecimalFormat PRICE_FORMAT = new DecimalFormat("###,###");
+
+    private List<PaymentProduct> paymentProducts;
+    private int totalPrice = 0;
+    private int totalPromotion = 0;
+    private int membershipPrice = 0;
+
+    public PaymentProductList() {
+        this.paymentProducts = new ArrayList<>();
+    }
+
+    public void addPaymentProduct(PaymentProduct product) {
+        paymentProducts.add(product);
+    }
+
+    public List<Integer> getAllProductQuantities() {
+        return paymentProducts.stream()
+                .map(PaymentProduct::getQuantity)
+                .toList();
+    }
+
+    public List<String> infoProduct() {
+        return paymentProducts.stream()
+                .map(PaymentProduct::toString)
+                .toList();
+    }
+
+    public List<String> infoPromotion() {
+        return paymentProducts.stream()
+                .filter(PaymentProduct::isPromotion)
+                .map(PaymentProduct::buildPromotion)
+                .toList();
+    }
+
+    public List<String> finalResult() {
+        List<String> result = new ArrayList<>();
+        result.add(String.format(FORMAT.getMessage(), "총구매액", totalQuantity(), totalPrice()));
+        result.add(String.format(FORMAT.getMessage(), "행사할인", "", totalPromotionPrice()));
+        result.add(String.format(FORMAT.getMessage(), "멤버십할인", "", "-" + PRICE_FORMAT.format(membershipPrice)));
+        result.add(String.format(FORMAT.getMessage(), "내실돈", "",
+                PRICE_FORMAT.format(totalPrice - totalPromotion - membershipPrice)));
+
+        return result;
+    }
+
+    private int totalQuantity() {
+        return paymentProducts.stream()
+                .mapToInt(PaymentProduct::getQuantity)
+                .sum();
+    }
+
+    private String totalPrice() {
+        this.totalPrice = paymentProducts.stream()
+                .mapToInt(PaymentProduct::getTotalPrice)
+                .sum();
+        return PRICE_FORMAT.format(totalPrice);
+    }
+
+    private String totalPromotionPrice() {
+        this.totalPromotion = paymentProducts.stream()
+                .filter(PaymentProduct::isPromotion)
+                .mapToInt(PaymentProduct::getPromotionPrice)
+                .sum();
+        return "-" + PRICE_FORMAT.format(totalPromotion);
+    }
+
+    public void applyMembershipDiscount() {
+        int price = (int) (paymentProducts.stream()
+                .filter(product -> !product.isPromotion())
+                .mapToInt(PaymentProduct::getTotalPrice)
+                .sum() * 0.3);
+
+        if (price > 8000) {
+            price = 8000;
+        }
+
+        this.membershipPrice = price;
+    }
+}

리뷰: 여기서 - 같은 것은 상수로 처리하는 것이 나을 것 같은데 어떻게 생각하시나요?
@@ -0,0 +1,89 @@
+package store.domain;
+
+import static store.global.constant.MessageConstant.FORMAT;
+
+import java.text.DecimalFormat;
+import java.util.ArrayList;
+import java.util.List;
+
+public class PaymentProductList {
+
+    private static final DecimalFormat PRICE_FORMAT = new DecimalFormat("###,###");
+
+    private List<PaymentProduct> paymentProducts;
+    private int totalPrice = 0;
+    private int totalPromotion = 0;
+    private int membershipPrice = 0;
+
+    public PaymentProductList() {
+        this.paymentProducts = new ArrayList<>();
+    }
+
+    public void addPaymentProduct(PaymentProduct product) {
+        paymentProducts.add(product);
+    }
+
+    public List<Integer> getAllProductQuantities() {
+        return paymentProducts.stream()
+                .map(PaymentProduct::getQuantity)
+                .toList();
+    }
+
+    public List<String> infoProduct() {
+        return paymentProducts.stream()
+                .map(PaymentProduct::toString)
+                .toList();
+    }
+
+    public List<String> infoPromotion() {
+        return paymentProducts.stream()
+                .filter(PaymentProduct::isPromotion)
+                .map(PaymentProduct::buildPromotion)
+                .toList();
+    }
+
+    public List<String> finalResult() {
+        List<String> result = new ArrayList<>();
+        result.add(String.format(FORMAT.getMessage(), "총구매액", totalQuantity(), totalPrice()));
+        result.add(String.format(FORMAT.getMessage(), "행사할인", "", totalPromotionPrice()));
+        result.add(String.format(FORMAT.getMessage(), "멤버십할인", "", "-" + PRICE_FORMAT.format(membershipPrice)));
+        result.add(String.format(FORMAT.getMessage(), "내실돈", "",
+                PRICE_FORMAT.format(totalPrice - totalPromotion - membershipPrice)));
+
+        return result;
+    }
+
+    private int totalQuantity() {
+        return paymentProducts.stream()
+                .mapToInt(PaymentProduct::getQuantity)
+                .sum();
+    }
+
+    private String totalPrice() {
+        this.totalPrice = paymentProducts.stream()
+                .mapToInt(PaymentProduct::getTotalPrice)
+                .sum();
+        return PRICE_FORMAT.format(totalPrice);
+    }
+
+    private String totalPromotionPrice() {
+        this.totalPromotion = paymentProducts.stream()
+                .filter(PaymentProduct::isPromotion)
+                .mapToInt(PaymentProduct::getPromotionPrice)
+                .sum();
+        return "-" + PRICE_FORMAT.format(totalPromotion);
+    }
+
+    public void applyMembershipDiscount() {
+        int price = (int) (paymentProducts.stream()
+                .filter(product -> !product.isPromotion())
+                .mapToInt(PaymentProduct::getTotalPrice)
+                .sum() * 0.3);
+
+        if (price > 8000) {
+            price = 8000;
+        }
+
+        this.membershipPrice = price;
+    }
+}

리뷰: 전체적으로 여기 8000 이라는 숫자도 그렇고, 상수화를 했으면 더 가독성이 좋았을 것 같아요!
@@ -0,0 +1,83 @@
+package store.domain;
+
+import java.time.LocalDate;
+import store.domain.product.Name;
+import store.domain.product.Price;
+import store.domain.product.Quantity;
+
+public class Product {
+
+    private static final String INFO_DELIMITER = " ";
+
+    private final Name name;
+    private final Price price;
+    private final Quantity quantity;
+    private final Promotions promotions;
+
+    public Product(Name name, Price price, Quantity quantity, Promotions promotions) {
+        this.name = name;
+        this.price = price;
+        this.quantity = quantity;
+        this.promotions = promotions;
+    }
+
+    @Override
+    public String toString() {
+        String info = buildInfo();
+
+        if (hasPromotion()) {
+            return String.join(INFO_DELIMITER,
+                    info,
+                    promotions.toString());
+        }
+        return info;
+    }
+
+    public int decreaseStock(int quantity) {
+        return this.quantity.decreaseStock(quantity);
+    }
+
+    public int calculateRemainingStock(int purchaseQuantity) {
+        return quantity.calculateDifference(purchaseQuantity);
+    }
+
+    public boolean hasSameName(String name) {
+        return this.name.toString().equals(name);
+    }
+
+    public boolean hasPromotion() {
+        return this.promotions != null;
+    }
+
+    private String buildInfo() {
+        return String.join(INFO_DELIMITER,
+                name.toString(),
+                price.toString(),
+                quantity.toString()
+        );
+    }
+
+    public int calculatePromotionRate(int quantity) {
+        return promotions.calculateRemainingItems(quantity);
+    }
+
+    public String getProductName() {
+        return name.toString();
+    }
+
+    public int getProductPrice() {
+        return price.getPrice();
+    }
+
+    public boolean isPromotionAdditionalProduct(int quantity) {
+        return promotions.isPromotionApplicable(quantity);
+    }
+
+    public int getPromotionUnits(int quantity) {
+        return promotions.calculatePromotionUnits(quantity);
+    }
+
+    public boolean isWithinPromotionPeriod(LocalDate date) {
+        return promotions.isWithinPromotionPeriod(date);
+    }
+}

리뷰: 모든 필드가 래핑되어 있는 것이 가독성과 로직 분리에 좋은 영향을 끼치는 것 같아요! 👍
@@ -0,0 +1,46 @@
+package store.domain;
+
+import java.time.LocalDate;
+
+public class Promotions {
+
+    private final String name;
+    private final int buy;
+    private final int get;
+    private final LocalDate startDate;
+    private final LocalDate endDate;
+
+    public Promotions(String name, String buy, String get, String startDate, String endDate) {
+        this.name = name;
+        this.buy = Integer.parseInt(buy);
+        this.get = Integer.parseInt(get);
+        this.startDate = LocalDate.parse(startDate);
+        this.endDate = LocalDate.parse(endDate);
+    }
+
+    @Override
+    public String toString() {
+        return name;
+    }
+
+    public boolean hasName(String name) {
+        return this.name.equals(name);
+    }
+
+    public boolean isPromotionApplicable(int quantity) {
+        return calculateRemainingItems(quantity) >= get;
+    }
+
+    public int calculateRemainingItems(int quantity) {
+        return quantity % (buy + get);
+    }
+
+    public int calculatePromotionUnits(int quantity) {
+        return quantity / (buy + get);
+    }
+
+    public boolean isWithinPromotionPeriod(LocalDate date) {
+        return (date.isEqual(startDate) || date.isAfter(startDate)) &&
+                (date.isEqual(endDate) || date.isBefore(endDate));
+    }
+}

리뷰: 저도 궁금합니다!
@@ -0,0 +1,32 @@
+package store.global.constant;
+
+public enum ErrorMessage {
+    INVALID_INPUT_PURCHASE("올바르지 않은 형식으로 입력했습니다. 다시 입력해 주세요."),
+    INVALID_INPUT("잘못된 입력입니다. 다시 입력해 주세요."),
+
+    INVALID_PRICE_NUMERIC("물품 가격은 숫자만 가능합니다. 다시 확인해 주세요."),
+    INVALID_PRICE_OUT_OF_RANGE("물품 가격은 범위내만 등록할 수 있습니다. 다시 확인해 주세요."),
+
+    INSUFFICIENT_STOCK("재고 수량을 초과하여 구매할 수 없습니다. 다시 입력해 주세요."),
+    INVALID_QUANTITY_NUMERIC("물품 수량은 숫자만 가능합니다. 다시 확인해 주세요."),
+    INVALID_QUANTITY_OUT_OF_RANGE("물품 수량은 범위내만 등록할 수 있습니다. 다시 확인해 주세요."),
+
+    PRODUCT_NOT_FOUND("존재하지 않는 상품입니다. 다시 입력해 주세요."),
+    INVALID_PRODUCT_ELEMENT("상품 정보의 요소가 올바르지 않았습니다. 파일을 확인해 주세요."),
+
+    FILE_NOT_FOUND("%s 파일을 찾을 수 없습니다. 파일 이름을 확인해 주세요."),
+    FILE_CONTAINS_BLANK_CONTENT("파일의 내용 중 공백인 부분이 있습니다. 파일을 확인해 주세요."),
+    FILE_CONTENT_NULL("파일의 내용이 NULL 입니다. 파일을 확인해 주세요."),
+    FILE_CONTENT_INSUFFICIENT("파일의 정보가 부족합니다. 파일을 확인해 주세요.");
+
+    private static final String ERROR_PREFIX = "[ERROR] ";
+    private final String message;
+
+    ErrorMessage(String message) {
+        this.message = message;
+    }
+
+    public String getMessage() {
+        return ERROR_PREFIX + message;
+    }
+}

리뷰: 이렇게 [ERROR]을 한번에 처리하는 방식 좋네요.
@@ -0,0 +1,13 @@
+package store.global.exception;
+
+import store.global.constant.ErrorMessage;
+
+public class FileException extends RuntimeException {
+    public FileException(ErrorMessage error, String fileName) {
+        super(String.format(error.getMessage(), fileName));
+    }
+
+    public FileException(ErrorMessage errorMessage) {
+        super(errorMessage.getMessage());
+    }
+}

리뷰: FileExceiption을 따로 정의하신 이유가 있을까요? 궁금합니다.
@@ -0,0 +1,61 @@
+package store.global.util;
+
+import java.util.ArrayList;
+import java.util.List;
+import store.domain.Product;
+import store.domain.PromotionsList;
+import store.domain.product.Name;
+import store.domain.product.Price;
+import store.domain.product.Quantity;
+
+public class ProductParser {
+    private static final String DEFAULT_QUANTITY = "0";
+    private static final String NO_PROMOTION = "null";
+
+    private static PromotionsList promotionsList;
+
+    public static List<Product> parseToProducts(final List<List<String>> inputData,
+                                                final PromotionsList promotionsList) {
+        ProductParser.promotionsList = promotionsList;
+
+        List<Product> products = new ArrayList<>();
+        for (List<String> productData : inputData) {
+            Product product = createProduct(productData);
+            products.add(product);
+            addDefaultProductIfNecessary(products, inputData, productData, product);
+        }
+        return products;
+    }
+
+    private static void addDefaultProductIfNecessary(List<Product> products, List<List<String>> inputData,
+                                                     List<String> productData, Product product) {
+        if (product.hasPromotion() && isSingleProductInInput(inputData, productData)) {
+            Product defaultProduct = createDefaultProduct(productData);
+            products.add(defaultProduct);
+        }
+    }
+
+    private static boolean isSingleProductInInput(List<List<String>> inputData, List<String> productData) {
+        return inputData.stream()
+                .filter(info -> info.contains(productData.get(0)))
+                .count() == 1;
+    }
+
+    private static Product createDefaultProduct(List<String> productData) {
+        return createProduct(List.of(
+                productData.get(0),
+                productData.get(1),
+                DEFAULT_QUANTITY,
+                NO_PROMOTION
+        ));
+    }
+
+    private static Product createProduct(List<String> productData) {
+        return new Product(
+                new Name(productData.get(0)),
+                new Price(productData.get(1)),
+                new Quantity(productData.get(2)),
+                promotionsList.findPromotion(productData.get(3))
+        );
+    }
+}

리뷰: 저는 FileUtil에서 사실 모든 파싱도 처리하도록 작성했는데, 이렇게 Parser을 따로 구분하는 것이 더 좋은 설계인 것 같습니다! 칭찬해요 :)
@@ -0,0 +1,34 @@
+package pairmatching.model;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+public class Pairs {
+
+    private Map<String, List<String>> pairs;
+
+    public Pairs(Map<String, List<String>> pair){
+        this.pairs = pair;
+    }
+
+    public Set<Map.Entry<String, List<String>>> entrySet(){
+        return pairs.entrySet();
+    }
+
+    public void display() {
+        for (String key : pairs.keySet()) {
+            String value = null;
+            if(pairs.get(key).size() == 2){
+                value = pairs.get(key).get(0);
+                value += " : ";
+                value += pairs.get(key).get(1);
+            }
+            if(pairs.get(key).size() == 1){
+                value = String.valueOf(pairs.get(key).get(0));
+            }
+            System.out.println(key+" : "+ value);
+        }
+    }
+
+}

리뷰: for-each와 string Joiner를 써서 해결했어도 됐을거 같아요!
@@ -54,11 +54,20 @@ export const REVIEW_GROUP_API_PARAMS = {
   },
 };
 
+export const WRITTEN_REVIEW_PARAMS = {
+  resource: 'reviews/authored',
+  queryString: {
+    lastReviewId: 'lastReviewId',
+    size: 'size',
+  },
+};
+
 export const REVIEW_WRITING_API_URL = `${serverUrl}/${VERSION2}/${REVIEW_WRITING_API_PARAMS.resource}`;
 export const REVIEW_LIST_API_URL = `${serverUrl}/${VERSION2}/${REVIEW_LIST_API_PARAMS.resource}`;
 export const DETAILED_REVIEW_API_URL = `${serverUrl}/${VERSION2}/${DETAILED_REVIEW_API_PARAMS.resource}`;
 export const REVIEW_GROUP_DATA_API_URL = `${serverUrl}/${VERSION2}/${REVIEW_GROUP_DATA_API_PARAMS.resource}`;
 export const REVIEW_GROUP_API_URL = `${serverUrl}/${VERSION2}/reviews/gather`;
+export const WRITTEN_REVIEW_LIST_API_URL = `${serverUrl}/${VERSION2}/${WRITTEN_REVIEW_PARAMS.resource}`;
 
 const endPoint = {
   postingReview: `${serverUrl}/${VERSION2}/reviews`,
@@ -80,6 +89,14 @@ const endPoint = {
   gettingGroupedReviews: (sectionId: number) =>
     `${REVIEW_GROUP_API_URL}?${REVIEW_GROUP_API_PARAMS.queryString.sectionId}=${sectionId}`,
   postingHighlight: `${serverUrl}/${VERSION2}/highlight`,
+
+  gettingWrittenReviewList: (lastReviewId: number | null, size: number) => {
+    const defaultEndpoint = `${WRITTEN_REVIEW_LIST_API_URL}?${WRITTEN_REVIEW_PARAMS.queryString.size}=${size}`;
+    if (lastReviewId) {
+      return defaultEndpoint + '&' + `${WRITTEN_REVIEW_PARAMS.queryString.lastReviewId}=${lastReviewId}`;
+    }
+    return defaultEndpoint;
+  },
 };
 
 export default endPoint;

리뷰: ```suggestion
    if (!lastReviewId) return defaultEndpoint;
    
      return defaultEndpoint + '&' + `${WRITTEN_REVIEW_PARAMS.queryString.lastReviewId}=${lastReviewId}`;

  },
```
이런 구조로 바꾸면 lastReviewId가 없을 때 endpoint 값을 파악하는데 더 쉬울 것 같네요
@@ -6,11 +6,17 @@ import {
   GroupedSection,
   GroupedReviews,
   ReviewInfoData,
+  WrittenReviewList,
 } from '@/types';
 
 import createApiErrorMessage from './apiErrorMessageCreator';
 import endPoint from './endpoints';
 
+export interface GetInfiniteReviewListApi {
+  lastReviewId: number | null;
+  size: number;
+}
+
 export const getDataToWriteReviewApi = async (reviewRequestCode: string) => {
   const response = await fetch(endPoint.gettingDataToWriteReview(reviewRequestCode), {
     method: 'GET',
@@ -79,12 +85,7 @@ export const getDetailedReviewApi = async ({ reviewId }: GetDetailedReviewApi) =
   return data as DetailReviewData;
 };
 
-interface GetReviewListApi {
-  lastReviewId: number | null;
-  size: number;
-}
-
-export const getReviewListApi = async ({ lastReviewId, size }: GetReviewListApi) => {
+export const getReviewListApi = async ({ lastReviewId, size }: GetInfiniteReviewListApi) => {
   const response = await fetch(endPoint.gettingReviewList(lastReviewId, size), {
     method: 'GET',
     headers: {
@@ -138,3 +139,20 @@ export const getGroupedReviews = async ({ sectionId }: GetGroupedReviewsProps) =
   const data = await response.json();
   return data as GroupedReviews;
 };
+
+export const getWrittenReviewList = async ({ lastReviewId, size }: GetInfiniteReviewListApi) => {
+  const response = await fetch(endPoint.gettingWrittenReviewList(lastReviewId, size), {
+    method: 'GET',
+    headers: {
+      'Content-Type': 'application/json',
+    },
+    credentials: 'include',
+  });
+
+  if (!response.ok) {
+    throw new Error(createApiErrorMessage(response.status));
+  }
+
+  const data = await response.json();
+  return data as WrittenReviewList;
+};

리뷰: 함수의 파라미터 타입이라서, 저는 이럴 때 타입명 뒤에 params를 추가하는 편이에요.
@@ -0,0 +1,78 @@
+import { useMemo } from 'react';
+
+import { useGetDetailedReview, useReviewId } from '@/hooks';
+import { substituteString } from '@/utils';
+
+import QuestionAnswerSection from './QuestionAnswerSection';
+import ReviewDescription from './ReviewDescription';
+import * as S from './styles';
+
+interface DetailedReviewProps {
+  $layoutStyle?: React.CSSProperties;
+  selectedReviewId?: number;
+}
+
+const DetailedReview = ({ selectedReviewId, $layoutStyle }: DetailedReviewProps) => {
+  const reviewId = useReviewId(selectedReviewId);
+
+  const { data: detailedReview } = useGetDetailedReview({
+    reviewId: reviewId,
+  });
+
+  const parsedDetailedReview = useMemo(() => {
+    return {
+      ...detailedReview,
+      sections: detailedReview.sections.map((section) => {
+        const newHeader = substituteString({
+          content: section.header,
+          variables: { revieweeName: detailedReview.revieweeName, projectName: detailedReview.projectName },
+        });
+
+        const newQuestions = section.questions.map((question) => {
+          const newContent = substituteString({
+            content: question.content,
+            variables: { revieweeName: detailedReview.revieweeName, projectName: detailedReview.projectName },
+          });
+
+          return {
+            ...question,
+            content: newContent,
+          };
+        });
+
+        return {
+          ...section,
+          header: newHeader,
+          questions: newQuestions,
+        };
+      }),
+    };
+  }, [detailedReview]);
+
+  return (
+    <S.DetailedReview $layoutStyle={$layoutStyle}>
+      <ReviewDescription
+        projectName={parsedDetailedReview.projectName}
+        date={new Date(parsedDetailedReview.createdAt)}
+        revieweeName={parsedDetailedReview.revieweeName}
+      />
+      <S.Separator />
+      <S.DetailedReviewContainer>
+        {parsedDetailedReview.sections.map((section) =>
+          section.questions.map((question) => (
+            <S.ReviewContentContainer key={question.questionId}>
+              <QuestionAnswerSection
+                question={question.content}
+                questionType={question.questionType}
+                answer={question.answer}
+                options={question.optionGroup?.options}
+              />
+            </S.ReviewContentContainer>
+          )),
+        )}
+      </S.DetailedReviewContainer>
+    </S.DetailedReview>
+  );
+};
+
+export default DetailedReview;

리뷰: sections 관련된 부분 코드가 길어서, header와 questions를 분리하고 각각 useMemo를 사용하는 것은 어떨까요'?
@@ -0,0 +1,28 @@
+import { Country } from "../types/country";
+
+const CountryCard = ({
+  country,
+  handleToggleCountry,
+}: {
+  country: Country;
+  handleToggleCountry: (country: Country) => void;
+}) => {
+  return (
+    <div
+      className="flex flex-col border rounded gap-2 px-4"
+      onClick={() => handleToggleCountry(country)}
+    >
+      <div className="flex justify-center mb-2">
+        <img
+          src={country.flags.svg}
+          alt={`${country.name.common} flag`}
+          className="w-32 h-20"
+        />
+      </div>
+      <h2>{country.name.common}</h2>
+      <p>{country.capital}</p>
+    </div>
+  );
+};
+
+export default CountryCard;

리뷰: h2 태그를 사용하기엔 너무 많아 질 것 같은데, 전체적인 관점으로 봤을때 h2의 레벨을 가진 텍스트는 아닌 것 같아서요! strong 태그대신 h2를 쓰신 이유가 궁금합니다!
@@ -0,0 +1,66 @@
+import { useQuery } from "@tanstack/react-query";
+import { getCountries } from "../api/countryAPI";
+import CountryCard from "./CountryCard";
+import { useEffect } from "react";
+import { Country } from "../types/country";
+import useCountryStore from "../zustand/countryStore";
+
+const CountryList = ({ isDone }: { isDone: boolean }) => {
+  const { countries, setCountries, favoriteCountries, setFavoriteCountries } =
+    useCountryStore();
+
+  const { data, error, isLoading } = useQuery({
+    queryKey: ["countries"],
+    queryFn: getCountries,
+  });
+
+  const handleToggleCountry = (country: Country) => {
+    if (isDone) {
+      setFavoriteCountries(
+        favoriteCountries.filter((toggle) => toggle.cca3 !== country.cca3)
+      );
+      setCountries([country, ...countries]);
+    } else {
+      setCountries(countries.filter((toggle) => toggle.cca3 !== country.cca3));
+      setFavoriteCountries([country, ...favoriteCountries]);
+    }
+  };
+
+  useEffect(() => {
+    if (data) {
+      setCountries(data);
+    }
+  }, [data, setCountries]);
+
+  if (isLoading) {
+    return (
+      <div className="flex justify-center items-center">
+        <h1 className="text-4xl">로딩중입니다!!</h1>
+      </div>
+    );
+  }
+
+  if (error) {
+    return (
+      <div className="flex justify-center items-center">
+        <h1 className="text-4xl">에러가 발생했습니다!</h1>
+      </div>
+    );
+  }
+
+  const isDoneCountries = isDone ? favoriteCountries : countries;
+
+  return (
+    <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-8 px-8">
+      {isDoneCountries.map((country) => (
+        <CountryCard
+          key={country.cca3}
+          country={country}
+          handleToggleCountry={handleToggleCountry}
+        />
+      ))}
+    </div>
+  );
+};
+
+export default CountryList;

리뷰: early if pattern  추천두립니다 .. >_<