ewhk9887/korean_code_reviews_from_github huggingface dataset:
@@ -1,7 +1,65 @@
 package store;
 
+import static store.constant.Constant.ERROR_PREFIX;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.List;
+import java.util.Map;
+import store.constant.YesOrNo;
+import store.controller.PurchaseController;
+import store.dto.StockResponse;
+import store.entity.Product;
+import store.entity.Promotion;
+import store.entity.Stock;
+import store.service.CartService;
+import store.service.StockService;
+import store.util.parse.PromotionParser;
+import store.util.parse.ProductParser;
+import store.view.InputView;
+import store.view.OutputView;
+
 public class Application {
+    private static final String PRODUCT_FILE_PATH = "src/main/resources/products.md";
+    private static final String PROMOTION_FILE_PATH = "src/main/resources/promotions.md";
+    private static final String FILE_ERROR_MESSAGE = ERROR_PREFIX + "파일을 읽는데 실패했습니다.";
+
     public static void main(String[] args) {
-        // TODO: 프로그램 구현
+        List<Product> items = getInitProducts();
+        Stock stock = new Stock(items);
+        StockService stockService = new StockService(stock);
+        initializeApplication(stock, stockService);
+    }
+
+    private static void initializeApplication(Stock stock, StockService stockService) {
+        YesOrNo continueShopping = YesOrNo.YES;
+        while (continueShopping == YesOrNo.YES) {
+            CartService cartService = new CartService(stock);
+            PurchaseController purchaseController = new PurchaseController(cartService, stockService);
+            List<StockResponse> stockResult = purchaseController.getStock();
+            OutputView.printStartMessage(stockResult);
+            purchaseController.purchase();
+            continueShopping = InputView.askForAdditionalPurchase();
+        }
+    }
+
+    private static List<Product> getInitProducts() {
+        List<String> products = readFile(PRODUCT_FILE_PATH);
+        List<String> promotions = readFile(PROMOTION_FILE_PATH);
+        Map<String, Promotion> parsePromotions = PromotionParser.parse(promotions);
+        List<Product> items = ProductParser.parse(products, parsePromotions);
+        return items;
+    }
+
+    private static List<String> readFile(String filePath) {
+        try {
+            Path path = Paths.get(filePath);
+            return Files.readAllLines(path);
+        } catch (IOException e) {
+            throw new UncheckedIOException(FILE_ERROR_MESSAGE, e);
+        }
     }
 }

리뷰: 저도 동의합니다! 정적 팩토리 메서드 이용해보는건 어떨까요?
@@ -0,0 +1,65 @@
+package store.controller;
+
+import java.util.List;
+import java.util.Map;
+import store.dto.ProductItem;
+import store.dto.StockResponse;
+import store.service.CartService;
+import store.service.StockService;
+import store.util.RetryHandler;
+import store.util.parse.PurchaseItemParser;
+import store.view.InputView;
+import store.view.OutputView;
+
+public class PurchaseController {
+    private final CartService cartService;
+    private final StockService stockService;
+
+    public PurchaseController(CartService cartService, StockService stockService) {
+        this.cartService = cartService;
+        this.stockService = stockService;
+    }
+
+    public List<StockResponse> getStock() {
+        return stockService.getStock();
+    }
+
+    public void purchase() {
+        inputPurchaseItem();
+        inputAdditionalPromotionItem();
+        inputPromotionStock();
+        OutputView.printReceipt(cartService.pay(inputMembership()));
+    }
+
+    private void inputPromotionStock() {
+        RetryHandler.handleWithRetry(() -> {
+            List<ProductItem> nonPromotionItems = cartService.getNonPromotion();
+            if (!nonPromotionItems.isEmpty()) {
+                List<ProductItem> removeNonPromotionItems = InputView.askForNonPromotionItems(nonPromotionItems);
+                cartService.removeNonPromotionItems(removeNonPromotionItems);
+            }
+        });
+    }
+
+    private void inputPurchaseItem() {
+        RetryHandler.handleWithRetry(() -> {
+            String items = InputView.readItem();
+            Map<String, Integer> parseItems = PurchaseItemParser.parse(items);
+            cartService.purchase(parseItems);
+        });
+    }
+
+    private void inputAdditionalPromotionItem() {
+        RetryHandler.handleWithRetry(() -> {
+            List<ProductItem> additionalItems = cartService.getPromotionAdditionalItems();
+            if (!additionalItems.isEmpty()) {
+                List<ProductItem> items = InputView.askForPromotionItems(additionalItems);
+                cartService.addPromotionItems(items);
+            }
+        });
+    }
+
+    private boolean inputMembership() {
+        return RetryHandler.handleWithRetry(() -> InputView.askForMembership());
+    }
+}

리뷰: 전체적으로 코드의 indent가 깊다는 생각이 들어요! 미션 요구사항에는 indent를 2까지 허용한다고 봤는데 이에 대해 어떻게 생각하시나요?
@@ -0,0 +1,24 @@
+package store.dto;
+
+import store.entity.Product;
+
+public record StockResponse(String name, int price, String quantity, String promotionQuantity, String promotionName) {
+    private static final String NONE_STOCK = "재고 없음";
+    private static final String QUANTITY_UNIT = "개";
+
+    public static StockResponse from(Product product) {
+        String promotionName = "";
+        if (product.getPromotion() != null) {
+            promotionName = product.getPromotion().getName();
+        }
+        return new StockResponse(product.getName(), product.getPrice(), formatQuantity(product.getQuantity()),
+                formatQuantity(product.getPromotionQuantity()), promotionName);
+    }
+
+    private static String formatQuantity(int quantity) {
+        if (quantity == 0) {
+            return NONE_STOCK;
+        }
+        return quantity + QUANTITY_UNIT;
+    }
+}

리뷰: 빈 문자열로 초기화한 이유가 궁금합니다!
@@ -0,0 +1,61 @@
+package store.entity;
+
+import static store.constant.Constant.ERROR_PREFIX;
+
+import java.util.List;
+import java.util.Map;
+
+public class Stock {
+    private static final String NON_EXIST_PRODUCT_MESSAGE = ERROR_PREFIX + "존재하지 않는 상품입니다. 다시 입력해 주세요.";
+    private static final String STOCK_EXCEED_MESSAGE = ERROR_PREFIX + "재고 수량을 초과하여 구매할 수 없습니다. 다시 입력해 주세요.";
+
+    private final List<Product> stock;
+
+    public Stock(List<Product> stock) {
+        this.stock = stock;
+    }
+
+    public Product findByName(String name) {
+        return stock.stream()
+                .filter(product -> product.getName().equals(name))
+                .findFirst()
+                .orElseThrow(() -> new IllegalArgumentException(NON_EXIST_PRODUCT_MESSAGE));
+    }
+
+    public void validateAll(Map<String, Integer> items) {
+        items.forEach(this::validate);
+    }
+
+    public void update(Map<Product, Integer> cartItem) {
+        cartItem.forEach((product, quantity) -> {
+            Product stockProduct = findByName(product.getName());
+            if (product.isPromotionEligible()) {
+                stockProduct.reducePromotionQuantity(quantity);
+            } else {
+                stockProduct.reduceQuantity(quantity);
+            }
+        });
+    }
+
+    private void validate(String productName, Integer quantity) {
+        if (!isValidName(productName)) {
+            throw new IllegalArgumentException(NON_EXIST_PRODUCT_MESSAGE);
+        }
+        if (!isStockAvailable(productName, quantity)) {
+            throw new IllegalArgumentException(STOCK_EXCEED_MESSAGE);
+        }
+    }
+
+    private boolean isValidName(String name) {
+        return stock.stream().anyMatch(product -> product.getName().equals(name));
+    }
+
+    private boolean isStockAvailable(String productName, Integer quantity) {
+        Product product = findByName(productName);
+        return product.isStockAvailable(quantity);
+    }
+
+    public List<Product> getStock() {
+        return stock.stream().toList();
+    }
+}

리뷰: 예외를 `isValidName`메서드와 `isStockAvailable`메서드에서 던지는게 어떨까요?
@@ -0,0 +1,65 @@
+package store.controller;
+
+import java.util.List;
+import java.util.Map;
+import store.dto.ProductItem;
+import store.dto.StockResponse;
+import store.service.CartService;
+import store.service.StockService;
+import store.util.RetryHandler;
+import store.util.parse.PurchaseItemParser;
+import store.view.InputView;
+import store.view.OutputView;
+
+public class PurchaseController {
+    private final CartService cartService;
+    private final StockService stockService;
+
+    public PurchaseController(CartService cartService, StockService stockService) {
+        this.cartService = cartService;
+        this.stockService = stockService;
+    }
+
+    public List<StockResponse> getStock() {
+        return stockService.getStock();
+    }
+
+    public void purchase() {
+        inputPurchaseItem();
+        inputAdditionalPromotionItem();
+        inputPromotionStock();
+        OutputView.printReceipt(cartService.pay(inputMembership()));
+    }
+
+    private void inputPromotionStock() {
+        RetryHandler.handleWithRetry(() -> {
+            List<ProductItem> nonPromotionItems = cartService.getNonPromotion();
+            if (!nonPromotionItems.isEmpty()) {
+                List<ProductItem> removeNonPromotionItems = InputView.askForNonPromotionItems(nonPromotionItems);
+                cartService.removeNonPromotionItems(removeNonPromotionItems);
+            }
+        });
+    }
+
+    private void inputPurchaseItem() {
+        RetryHandler.handleWithRetry(() -> {
+            String items = InputView.readItem();
+            Map<String, Integer> parseItems = PurchaseItemParser.parse(items);
+            cartService.purchase(parseItems);
+        });
+    }
+
+    private void inputAdditionalPromotionItem() {
+        RetryHandler.handleWithRetry(() -> {
+            List<ProductItem> additionalItems = cartService.getPromotionAdditionalItems();
+            if (!additionalItems.isEmpty()) {
+                List<ProductItem> items = InputView.askForPromotionItems(additionalItems);
+                cartService.addPromotionItems(items);
+            }
+        });
+    }
+
+    private boolean inputMembership() {
+        return RetryHandler.handleWithRetry(() -> InputView.askForMembership());
+    }
+}

리뷰: 저 코드는 indent가 2 아닌가요?!
@@ -0,0 +1,24 @@
+package store.dto;
+
+import store.entity.Product;
+
+public record StockResponse(String name, int price, String quantity, String promotionQuantity, String promotionName) {
+    private static final String NONE_STOCK = "재고 없음";
+    private static final String QUANTITY_UNIT = "개";
+
+    public static StockResponse from(Product product) {
+        String promotionName = "";
+        if (product.getPromotion() != null) {
+            promotionName = product.getPromotion().getName();
+        }
+        return new StockResponse(product.getName(), product.getPrice(), formatQuantity(product.getQuantity()),
+                formatQuantity(product.getPromotionQuantity()), promotionName);
+    }
+
+    private static String formatQuantity(int quantity) {
+        if (quantity == 0) {
+            return NONE_STOCK;
+        }
+        return quantity + QUANTITY_UNIT;
+    }
+}

리뷰: OutputView 에서 해당 promotionName의 상태에 따라 다른 포맷을 적용시키려고  했습니다!
@@ -0,0 +1,7 @@
+package store.entity;
+
+import java.time.LocalDate;
+
+public interface DateProvider {
+    LocalDate now();
+}

리뷰: 테스트 코드에서 실제 오늘날짜를 반환하는 now()를 사용하게되면 테스트코드가 해당 날짜에 의존하지 않을까요??
@@ -0,0 +1,61 @@
+package store.entity;
+
+import static store.constant.Constant.ERROR_PREFIX;
+
+import java.util.List;
+import java.util.Map;
+
+public class Stock {
+    private static final String NON_EXIST_PRODUCT_MESSAGE = ERROR_PREFIX + "존재하지 않는 상품입니다. 다시 입력해 주세요.";
+    private static final String STOCK_EXCEED_MESSAGE = ERROR_PREFIX + "재고 수량을 초과하여 구매할 수 없습니다. 다시 입력해 주세요.";
+
+    private final List<Product> stock;
+
+    public Stock(List<Product> stock) {
+        this.stock = stock;
+    }
+
+    public Product findByName(String name) {
+        return stock.stream()
+                .filter(product -> product.getName().equals(name))
+                .findFirst()
+                .orElseThrow(() -> new IllegalArgumentException(NON_EXIST_PRODUCT_MESSAGE));
+    }
+
+    public void validateAll(Map<String, Integer> items) {
+        items.forEach(this::validate);
+    }
+
+    public void update(Map<Product, Integer> cartItem) {
+        cartItem.forEach((product, quantity) -> {
+            Product stockProduct = findByName(product.getName());
+            if (product.isPromotionEligible()) {
+                stockProduct.reducePromotionQuantity(quantity);
+            } else {
+                stockProduct.reduceQuantity(quantity);
+            }
+        });
+    }
+
+    private void validate(String productName, Integer quantity) {
+        if (!isValidName(productName)) {
+            throw new IllegalArgumentException(NON_EXIST_PRODUCT_MESSAGE);
+        }
+        if (!isStockAvailable(productName, quantity)) {
+            throw new IllegalArgumentException(STOCK_EXCEED_MESSAGE);
+        }
+    }
+
+    private boolean isValidName(String name) {
+        return stock.stream().anyMatch(product -> product.getName().equals(name));
+    }
+
+    private boolean isStockAvailable(String productName, Integer quantity) {
+        Product product = findByName(productName);
+        return product.isStockAvailable(quantity);
+    }
+
+    public List<Product> getStock() {
+        return stock.stream().toList();
+    }
+}

리뷰: 그래도 될것같네요!
@@ -1,7 +1,65 @@
 package store;
 
+import static store.constant.Constant.ERROR_PREFIX;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.List;
+import java.util.Map;
+import store.constant.YesOrNo;
+import store.controller.PurchaseController;
+import store.dto.StockResponse;
+import store.entity.Product;
+import store.entity.Promotion;
+import store.entity.Stock;
+import store.service.CartService;
+import store.service.StockService;
+import store.util.parse.PromotionParser;
+import store.util.parse.ProductParser;
+import store.view.InputView;
+import store.view.OutputView;
+
 public class Application {
+    private static final String PRODUCT_FILE_PATH = "src/main/resources/products.md";
+    private static final String PROMOTION_FILE_PATH = "src/main/resources/promotions.md";
+    private static final String FILE_ERROR_MESSAGE = ERROR_PREFIX + "파일을 읽는데 실패했습니다.";
+
     public static void main(String[] args) {
-        // TODO: 프로그램 구현
+        List<Product> items = getInitProducts();
+        Stock stock = new Stock(items);
+        StockService stockService = new StockService(stock);
+        initializeApplication(stock, stockService);
+    }
+
+    private static void initializeApplication(Stock stock, StockService stockService) {
+        YesOrNo continueShopping = YesOrNo.YES;
+        while (continueShopping == YesOrNo.YES) {
+            CartService cartService = new CartService(stock);
+            PurchaseController purchaseController = new PurchaseController(cartService, stockService);
+            List<StockResponse> stockResult = purchaseController.getStock();
+            OutputView.printStartMessage(stockResult);
+            purchaseController.purchase();
+            continueShopping = InputView.askForAdditionalPurchase();
+        }
+    }
+
+    private static List<Product> getInitProducts() {
+        List<String> products = readFile(PRODUCT_FILE_PATH);
+        List<String> promotions = readFile(PROMOTION_FILE_PATH);
+        Map<String, Promotion> parsePromotions = PromotionParser.parse(promotions);
+        List<Product> items = ProductParser.parse(products, parsePromotions);
+        return items;
+    }
+
+    private static List<String> readFile(String filePath) {
+        try {
+            Path path = Paths.get(filePath);
+            return Files.readAllLines(path);
+        } catch (IOException e) {
+            throw new UncheckedIOException(FILE_ERROR_MESSAGE, e);
+        }
     }
 }

리뷰: 좋은 의견 감사합니다!
@@ -0,0 +1,29 @@
+package store.exception.store;
+
+public enum StoreErrorStatus {
+
+    INSUFFICIENT_STOCK("재고 수량을 초과하여 구매할 수 없습니다. 다시 입력해 주세요."),
+    NON_EXIST_PRODUCT("존재하지 않는 상품입니다. 다시 입력해 주세요."),
+    DUPLICATE_GENERAL_PRODUCT("상품이 중복되었습니다."),
+    DUPLICATE_PROMOTION_PRODUCT("하나의 상품에 두개의 프로모션이 적용될 수 없습니다."),
+
+    INVALID_PROMOTION_BUY_AMOUNT("구매 개수는 0 이하가 될 수 없습니다."),
+    INVALID_PROMOTION_GIVE_AMOUNT("추가 증정은 하나만 가능합니다."),
+    NON_EXIST_PROMOTION("존재하지 않는 프로모션이 있습니다."),
+    DUPLICATE_PROMOTION_NAME("프로모션의 이름은 중복될 수 없습니다."),
+
+    INVALID_PRODUCT_PRICE("가격은 0 이하가 될 수 없습니다."),
+    INVALID_PRODUCT_STOCK("재고는 음수가 될 수 없습니다."),
+
+    PRODUCT_PRICE_INCONSISTENT("일반 상품과 프로모션 상품의 가격은 동일해야합니다.");
+
+    private final String message;
+
+    StoreErrorStatus(String message) {
+        this.message = message;
+    }
+
+    public String getMessage() {
+        return message;
+    }
+}

리뷰: 반성하게 만드는 예외 리스트네요... 대단하십니다.
@@ -0,0 +1,94 @@
+package store.model.store.product;
+
+import static store.exception.store.StoreErrorStatus.INVALID_PRODUCT_PRICE;
+import static store.exception.store.StoreErrorStatus.INVALID_PRODUCT_STOCK;
+
+import java.time.LocalDateTime;
+import store.exception.store.StoreException;
+import store.model.store.promotion.Promotion;
+
+public class Product {
+    private final String name;
+    private final int price;
+    private int stock;
+    private final Promotion promotion;
+
+    private Product(String name, int price, int stock, Promotion promotion) {
+        validatePrice(price);
+        validateStock(stock);
+        this.name = name;
+        this.price = price;
+        this.stock = stock;
+        this.promotion = promotion;
+    }
+
+    public static Product of(String name, int price, int stock, Promotion promotion) {
+        return new Product(name, price, stock, promotion);
+    }
+
+    public static Product createEmptyGeneralProduct(Product product) {
+        return new Product(product.name, product.price, 0, null);
+    }
+
+    public void decreaseStock(int amount) {
+        stock -= amount;
+    }
+
+    public boolean isNameEquals(Product product) {
+        return this.name.equals(product.name);
+    }
+
+    public boolean isNameEquals(String name) {
+        return this.name.equals(name);
+    }
+
+    public boolean isPromotionPeriod(LocalDateTime now) {
+        if (this.promotion == null) {
+            return false;
+        }
+        return promotion.isPeriod(now);
+    }
+
+    public boolean isGeneralProduct() {
+        return promotion == null;
+    }
+
+    public boolean isPromotionProduct() {
+        return promotion != null;
+    }
+
+    private void validatePrice(int price) {
+        if (price <= 0) {
+            throw new StoreException(INVALID_PRODUCT_PRICE);
+        }
+    }
+
+    private void validateStock(int stock) {
+        if (stock < 0) {
+            throw new StoreException(INVALID_PRODUCT_STOCK);
+        }
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public int getPrice() {
+        return price;
+    }
+
+    public int getStock() {
+        return stock;
+    }
+
+    public String getPromotionName() {
+        if (promotion == null) {
+            return "";
+        }
+        return promotion.getName();
+    }
+
+    public int getPromotionUnit() {
+        return promotion.getUnit();
+    }
+}

리뷰: 이 부분이 저랑 다르네요! 저는 Product에 재고를 포함시키지 않고 재고는 repository에서 가져오도록 했습니다.
product가 재고 수량을 가져도 괜찮은지 여쭤보고 싶습니다~
@@ -0,0 +1,61 @@
+package store.model.store.promotion;
+
+import static store.exception.store.StoreErrorStatus.INVALID_PROMOTION_BUY_AMOUNT;
+import static store.exception.store.StoreErrorStatus.INVALID_PROMOTION_GIVE_AMOUNT;
+
+import java.time.LocalDateTime;
+import store.exception.store.StoreException;
+
+public class Promotion {
+    private final String name;
+    private final int buyAmount;
+    private final int giveAmount;
+    private final LocalDateTime startAt;
+    private final LocalDateTime endAt;
+
+    private Promotion(String name, int buyAmount, int giveAmount, LocalDateTime startAt, LocalDateTime endAt) {
+        validateBuyAmount(buyAmount);
+        validateGiveAmount(giveAmount);
+        this.name = name;
+        this.buyAmount = buyAmount;
+        this.giveAmount = giveAmount;
+        this.startAt = startAt;
+        this.endAt = endAt;
+    }
+
+    public static Promotion of(String name,
+                               int buyAmount,
+                               int giveAmount,
+                               LocalDateTime startAt,
+                               LocalDateTime endAt) {
+        return new Promotion(name, buyAmount, giveAmount, startAt, endAt);
+    }
+
+    public boolean isNameEquals(String name) {
+        return this.name.equals(name);
+    }
+
+    public boolean isPeriod(LocalDateTime now) {
+        return startAt.isBefore(now) && endAt.isAfter(now);
+    }
+
+    private void validateBuyAmount(int buyAmount) {
+        if (buyAmount < 1) {
+            throw new StoreException(INVALID_PROMOTION_BUY_AMOUNT);
+        }
+    }
+
+    private void validateGiveAmount(int giveAmount) {
+        if (giveAmount != 1) {
+            throw new StoreException(INVALID_PROMOTION_GIVE_AMOUNT);
+        }
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public int getUnit() {
+        return buyAmount + giveAmount;
+    }
+}

리뷰: LocalDate가 아닌 LocalDateTime을 사용하신 이유가 있나요?
@@ -0,0 +1,20 @@
+package store.util.reader;
+
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+public class RepeatableReader {
+    public static <T> T handle(Supplier<String> viewMethod,
+                               Function<String, T> converter,
+                               Consumer<String> errorHandler) {
+        while (true) {
+            try {
+                String userInput = viewMethod.get();
+                return converter.apply(userInput);
+            } catch (IllegalArgumentException e) {
+                errorHandler.accept(e.getMessage());
+            }
+        }
+    }
+}

리뷰: 함수형 프로그래밍을 되게 잘하시네요.
@@ -0,0 +1,10 @@
+package store.view;
+
+public class ErrorView {
+
+    private static final String ERROR_PREFIX = "[ERROR] ";
+
+    public void errorPage(String message) {
+        System.out.println(ERROR_PREFIX + message);
+    }
+}

리뷰: ErrorView는 처음 보는데, 잘 만드신 거 같아요!
@@ -0,0 +1,44 @@
+package store.util.reader;
+
+import static store.exception.reader.FileReadErrorStatus.INVALID_FILE_PATH;
+import static store.exception.reader.FileReadErrorStatus.UNEXPECTED_IO_ERROR;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.List;
+import store.exception.reader.FileReadException;
+
+public class StoreFileReader {
+    private static final String DELIMITER = ",";
+
+    public static List<List<String>> readFile(String fileName) {
+        validateFileReadable(fileName);
+        Path path = Paths.get(fileName);
+        try (BufferedReader reader = Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
+            return reader.lines().skip(1)
+                    .filter(line -> line != null && !line.trim().isEmpty())
+                    .map(StoreFileReader::parseLine)
+                    .toList();
+        } catch (IOException e) {
+            throw new FileReadException(UNEXPECTED_IO_ERROR);
+        }
+    }
+
+    private static List<String> parseLine(String line) {
+        return List.of(line.split(DELIMITER));
+    }
+
+    public static void validateFileReadable(String fileName) {
+        if (fileName == null || fileName.trim().isEmpty()) {
+            throw new FileReadException(INVALID_FILE_PATH);
+        }
+        Path path = Paths.get(fileName);
+        if (!(Files.exists(path) && Files.isReadable(path))) {
+            throw new FileReadException(INVALID_FILE_PATH);
+        }
+    }
+}

리뷰: `String.isBlank`메서드도 사용해보시면 좋을 거 같아요!