ewhk9887/korean_code_reviews_from_github huggingface dataset:
@@ -0,0 +1,28 @@
+package store.dto;
+
+import store.domain.Product;
+import store.domain.PurchaseItem;
+
+public record GivenItemDto(
+        String name,
+        int freeQuantity,
+        int price) {
+
+    public static GivenItemDto from(PurchaseItem purchaseItem) {
+        Product product = purchaseItem.getProduct();
+        int freeQuantity = calculateFreeQuantity(purchaseItem);
+
+        return new GivenItemDto(product.name(), freeQuantity, product.price());
+    }
+
+    private static int calculateFreeQuantity(PurchaseItem purchaseItem) {
+        Product product = purchaseItem.getProduct();
+
+        if (product.promotion() != null) {
+            return product.promotion()
+                    .calculateFreeQuantity(purchaseItem.getQuantity(), product.stock().getPromotionStock());
+        }
+
+        return 0;
+    }
+}

리뷰: DTO는 데이터를 전달하기 위한 객체로 알고 있는데 이렇게 서비스 관련 로직까지 들어있어 이 부분을 밖으로 빼는게 좋을거같은데 어떻게 생각하시나요?
@@ -0,0 +1,28 @@
+package store.dto;
+
+import store.domain.Product;
+import store.domain.PurchaseItem;
+
+public record GivenItemDto(
+        String name,
+        int freeQuantity,
+        int price) {
+
+    public static GivenItemDto from(PurchaseItem purchaseItem) {
+        Product product = purchaseItem.getProduct();
+        int freeQuantity = calculateFreeQuantity(purchaseItem);
+
+        return new GivenItemDto(product.name(), freeQuantity, product.price());
+    }
+
+    private static int calculateFreeQuantity(PurchaseItem purchaseItem) {
+        Product product = purchaseItem.getProduct();
+
+        if (product.promotion() != null) {
+            return product.promotion()
+                    .calculateFreeQuantity(purchaseItem.getQuantity(), product.stock().getPromotionStock());
+        }
+
+        return 0;
+    }
+}

리뷰: 정적 팩토리 메서드 패턴을 사용하시는 기준이 궁금합니다! 언제 생성자를 사용하시고 언제 정적 팩토리 메서드 패턴을 사용하시나요?
@@ -0,0 +1,64 @@
+package store.domain;
+
+import camp.nextstep.edu.missionutils.DateTimes;
+import java.time.LocalDate;
+import store.dto.PromotionDetailDto;
+
+public class Promotion {
+
+    private final String name;
+    private final int buy;
+    private final int get;
+    private final LocalDate startDate;
+    private final LocalDate endDate;
+
+    public Promotion(final String name, final int buy, final int get, final LocalDate startDate,
+                     final LocalDate endDate) {
+        this.name = name;
+        this.buy = buy;
+        this.get = get;
+        this.startDate = startDate;
+        this.endDate = endDate;
+    }
+
+    public boolean isWithinPromotionPeriod() {
+        LocalDate today = DateTimes.now().toLocalDate();
+        return (today.isEqual(startDate) || today.isAfter(startDate))
+                && (today.isEqual(endDate) || today.isBefore(endDate));
+    }
+
+    public int calculateAdditionalPurchase(int purchaseQuantity) {
+        int remain = purchaseQuantity % sumOfBuyAndGet();
+
+        if (remain != 0) {
+            return sumOfBuyAndGet() - remain;
+        }
+
+        return remain;
+    }
+
+    public PromotionDetailDto calculatePromotionAndFree(int requestedQuantity, int promotionStock) {
+        int availablePromotion = (promotionStock / sumOfBuyAndGet()) * sumOfBuyAndGet();
+        return new PromotionDetailDto(availablePromotion, requestedQuantity - availablePromotion);
+    }
+
+    public int calculateFreeQuantity(int requestedQuantity, int promotionStock) {
+        if (requestedQuantity > promotionStock) {
+            return promotionStock / sumOfBuyAndGet();
+        }
+
+        return requestedQuantity / sumOfBuyAndGet();
+    }
+
+    private int sumOfBuyAndGet() {
+        return buy + get;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public int getGet() {
+        return get;
+    }
+}

리뷰: 좋은 의견인거 같습니다! 저도 하나 배워가네요
@@ -0,0 +1,28 @@
+package store.domain;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class Products {
+    private List<Product> products;
+
+    public Products(
+            List<Product> products) {
+        this.products = new ArrayList<>(products);
+    }
+
+    public Product findProductByName(String name) {
+        return products.stream()
+                .filter(product -> product.name().equals(name))
+                .findFirst()
+                .orElse(null);
+    }
+
+    public void addProduct(Product product) {
+        products.add(product);
+    }
+
+    public List<Product> getProducts() {
+        return products.stream().toList();
+    }
+}

리뷰: 저는 null을 반환하는 것보다 Optional을 통해 null이 발생할 수 있음을 확실히 알리는 것을 선호합니다. 현재는 개인 혼자 개발하기에 null이 발생할 수 있음을 알고 로직을 작성할 수 있습니다. 하지만 만약 협업 상황에서 처음 클래스의 메소드를 보면 보통 메소드 명, 파라미터, 리턴 타입으로 메소드의 역할과 기능을 파악하게 되는데 null이 발생하는지 로직을 세부적으로 읽어봐야하는 상황이 발생합니다. 이에 대비하여 Optional을 통해 null이 발생할 수 있음을 알립니다! 저의 개인적인 생각이니 참고만 부탁드릴게요 ㅎㅎ 😊
@@ -0,0 +1,50 @@
+package store.exception;
+
+import java.util.function.Predicate;
+
+public class ValidatorBuilder<T> {
+
+    private final T value;
+    private int numericValue;
+
+    private ValidatorBuilder(final T Value) {
+        this.value = Value;
+    }
+
+    public static <T> ValidatorBuilder<T> from(final T Value) {
+        return new ValidatorBuilder<T>(Value);
+    }
+
+    public ValidatorBuilder<T> validate(final Predicate<T> condition, final ErrorMessage errorMessage) {
+        if (condition.test(value)) {
+            throw new IllegalArgumentException(errorMessage.message);
+        }
+
+        return this;
+    }
+
+    public ValidatorBuilder<T> validateInteger(final Predicate<Integer> condition, final ErrorMessage errorMessage) {
+        if (condition.test(numericValue)) {
+            throw new IllegalArgumentException(errorMessage.message);
+        }
+
+        return this;
+    }
+
+    public ValidatorBuilder<T> validateIsInteger() {
+        try {
+            numericValue = Integer.parseInt(value.toString());
+            return this;
+        } catch (NumberFormatException e) {
+            throw new IllegalArgumentException(ErrorMessage.PURCHASE_QUANTITY_NOT_INTEGER.message);
+        }
+    }
+
+    public T get() {
+        return value;
+    }
+
+    public int getNumericValue() {
+        return numericValue;
+    }
+}

리뷰: 제네릭 활용을 잘 하시네요! 👍
@@ -1,7 +1,15 @@
 package store;
 
+import java.io.IOException;
+import store.controller.StoreManager;
+
 public class Application {
     public static void main(String[] args) {
-        // TODO: 프로그램 구현
+        try {
+            StoreManager storeManager = new StoreManager();
+            storeManager.run();
+        } catch (IOException e) {
+            System.err.println("[ERROR] 프로그램 실행 중 오류가 발생했습니다: " + e.getMessage());
+        }
     }
 }

리뷰: 해당 예외는 Controller에서 처리해주는건 어떨까요?
@@ -0,0 +1,38 @@
+package store.view;
+
+import camp.nextstep.edu.missionutils.Console;
+
+public class InputView {
+
+    public String readPurchaseList() {
+        String message = "\n구매하실 상품명과 수량을 입력해 주세요. (예: [사이다-2],[감자칩-1])";
+        return prompt(message);
+    }
+
+    public String readAdditionalPromotion(String productName, int quantity) {
+        String message = String.format("\n현재 %s %d개를 무료로 더 받을 수 있습니다. 추가하시겠습니까? (Y/N)", productName, quantity);
+        return prompt(message);
+    }
+
+    public String readProceedWithoutPromotion(String productName, int quantity) {
+        String message = String.format("\n현재 %s %d개는 프로모션 할인이 적용되지 않습니다. 그래도 구매하시겠습니까? (Y/N)", productName, quantity);
+        return prompt(message);
+
+    }
+
+    public String readMembershipDiscount() {
+        String message = "\n멤버십 할인을 받으시겠습니까? (Y/N)";
+        return prompt(message);
+
+    }
+
+    public String readContinueShopping() {
+        String message = "\n감사합니다. 구매하고 싶은 다른 상품이 있나요? (Y/N)";
+        return prompt(message);
+    }
+
+    private String prompt(String message) {
+        System.out.println(message);
+        return Console.readLine();
+    }
+}

리뷰: 사용하는 문자열을 상수화 해주는건 어떨까요?
@@ -0,0 +1,19 @@
+name,price,quantity,promotion
+콜라,1000,10,탄산2+1
+콜라,1000,10,null
+사이다,1000,8,탄산2+1
+사이다,1000,7,null
+오렌지주스,1800,9,MD추천상품
+오렌지주스,1800,0,null
+탄산수,1200,5,탄산2+1
+탄산수,1200,0,null
+물,500,10,null
+비타민워터,1500,6,null
+감자칩,1500,5,반짝할인
+감자칩,1500,5,null
+초코바,1200,5,MD추천상품
+초코바,1200,5,null
+에너지바,2000,5,null
+정식도시락,6400,8,null
+컵라면,1700,1,MD추천상품
+컵라면,1700,10,null

리뷰: 파일 이름을 변경했을때 테스트가 실패하지는 않았나요?
@@ -0,0 +1,18 @@
+package store.handler;
+
+public enum ErrorHandler {
+    INVALID_FORMAT("올바르지 않은 형식으로 입력했습니다. 다시 입력해 주세요."),
+    PRODUCT_NOT_FOUND("존재하지 않는 상품입니다. 다시 입력해 주세요."),
+    QUANTITY_EXCEEDS_STOCK("재고 수량을 초과하여 구매할 수 없습니다. 다시 입력해 주세요."),
+    GENERIC_INVALID_INPUT("잘못된 입력입니다. 다시 입력해 주세요.");
+
+    private final String message;
+
+    ErrorHandler(String message) {
+        this.message = message;
+    }
+
+    public IllegalArgumentException getException() {
+        return new IllegalArgumentException(message);
+    }
+}

리뷰: enum을 이용한 예외처리 좋은 것 같습니다👍👍
@@ -0,0 +1,53 @@
+package store.handler;
+
+import camp.nextstep.edu.missionutils.DateTimes;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.time.LocalDate;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import store.domain.Promotion;
+
+public class PromotionsFileHandler {
+    private static final Path PROMOTIONS_FILE_PATH = Path.of("src/main/resources/promotions.md");
+    public static final String NON_PROMOTION_NAME = "null";
+    public static final Promotion NON_PROMOTION = new Promotion(NON_PROMOTION_NAME, 1, 0);
+    private static final Map<String, Promotion> promotions = new HashMap<>();
+
+    public void loadPromotions() throws IOException {
+        List<String> lines = Files.readAllLines(PROMOTIONS_FILE_PATH);
+        promotions.put(NON_PROMOTION_NAME, NON_PROMOTION);
+        lines.stream().skip(1).forEach(this::processPromotionLine); // Skip header line
+    }
+
+    private void processPromotionLine(String line) {
+        String[] parts = parseAndTrimLine(line);
+        String name = parts[0];
+        int discountRate = Integer.parseInt(parts[1]);
+        int limit = Integer.parseInt(parts[2]);
+
+        if (isPromotionValid(parts)) {
+            promotions.put(name, new Promotion(name, discountRate, limit));
+        }
+    }
+
+    private String[] parseAndTrimLine(String line) {
+        return Arrays.stream(line.split(","))
+                .map(String::trim)
+                .toArray(String[]::new);
+    }
+
+    private boolean isPromotionValid(String[] parts) {
+        LocalDate today = DateTimes.now().toLocalDate();
+        LocalDate startDate = LocalDate.parse(parts[3]);
+        LocalDate endDate = LocalDate.parse(parts[4]);
+        return !today.isBefore(startDate) && !today.isAfter(endDate);
+    }
+
+    public static Promotion getPromotionByName(String name) {
+        return promotions.getOrDefault(name, promotions.get(NON_PROMOTION_NAME));
+    }
+}

리뷰: 주석보다는 메소드의 이름으로 명시해보는건 어떨까요?
@@ -0,0 +1,34 @@
+package store.domain;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+public class Cart {
+    private final List<CartItem> items = new ArrayList<>();
+
+    public void addItem(CartItem item) {
+        items.add(item);
+    }
+
+    public List<CartItem> getItems() {
+        return Collections.unmodifiableList(items);
+    }
+
+    public int totalQuantity() {
+        return items.stream().mapToInt(CartItem::calcTotalQuantity).sum();
+    }
+
+    public int totalPrice() {
+        return items.stream().mapToInt(CartItem::totalPrice).sum();
+    }
+
+    public int totalRetailPrice() {
+        return items.stream().mapToInt(CartItem::totalRetailPrice).sum();
+    }
+
+    public int totalFreePrice() {
+        return items.stream().mapToInt(CartItem::totalFreePrice).sum();
+    }
+}
+

리뷰: 전체적으로 Model이 해당 도메인에 대한 비즈니스 로직을 수행하기 보다는 Dataclass처럼 사용되고 있는 것 같아요!

어울리는 로직들을 Model로 옮겨올 수 있을 것 같아요
@@ -0,0 +1,38 @@
+package store.service;
+
+import store.controller.CartManager;
+import store.domain.Cart;
+import store.domain.Receipt;
+
+public class PaymentService {
+    private static final double MEMBERSHIP_DISCOUNT_RATE = 0.3;
+    private final static int MEMBERSHIP_DISCOUNT_LIMIT = 8000;
+
+    private final CartManager cartManager;
+    private int membershipDiscountBalance;
+
+    public PaymentService(CartManager cartManager) {
+        this.cartManager = cartManager;
+        this.membershipDiscountBalance = MEMBERSHIP_DISCOUNT_LIMIT;
+    }
+
+    public Cart getCartItems(String purchaseList) {
+        return cartManager.generateCart(purchaseList);
+    }
+
+    public Receipt createReceipt(Cart cart, boolean applyMembership) {
+        int membershipDiscount = calcMembershipDiscount(applyMembership, cart.totalRetailPrice());
+        return new Receipt(cart, cart.totalPrice(), cart.totalFreePrice(), membershipDiscount);
+    }
+
+    private int calcMembershipDiscount(boolean apply, int eligibleAmount) {
+        if (apply) {
+            int discountAmount = Math.min((int) (eligibleAmount * MEMBERSHIP_DISCOUNT_RATE),
+                    membershipDiscountBalance);
+            membershipDiscountBalance -= discountAmount;
+
+            return discountAmount;
+        }
+        return 0;
+    }
+}

리뷰: 라인 포맷팅을 더 이쁘게 할 수 있을 것 같아요!
모듈화 하는 것도 방법인것 같아요😃
```suggestion
            int discountAmount = Math.min(
                    (int) (eligibleAmount * MEMBERSHIP_DISCOUNT_RATE),
                    membershipDiscountBalance
            );
```
@@ -1,7 +1,15 @@
 package store;
 
+import java.io.IOException;
+import store.controller.StoreManager;
+
 public class Application {
     public static void main(String[] args) {
-        // TODO: 프로그램 구현
+        try {
+            StoreManager storeManager = new StoreManager();
+            storeManager.run();
+        } catch (IOException e) {
+            System.err.println("[ERROR] 프로그램 실행 중 오류가 발생했습니다: " + e.getMessage());
+        }
     }
 }

리뷰: 출력은 OutputView에서 처리하는 것이 적절해보입니다!
@@ -0,0 +1,53 @@
+package store.handler;
+
+import camp.nextstep.edu.missionutils.DateTimes;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.time.LocalDate;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import store.domain.Promotion;
+
+public class PromotionsFileHandler {
+    private static final Path PROMOTIONS_FILE_PATH = Path.of("src/main/resources/promotions.md");
+    public static final String NON_PROMOTION_NAME = "null";
+    public static final Promotion NON_PROMOTION = new Promotion(NON_PROMOTION_NAME, 1, 0);
+    private static final Map<String, Promotion> promotions = new HashMap<>();
+
+    public void loadPromotions() throws IOException {
+        List<String> lines = Files.readAllLines(PROMOTIONS_FILE_PATH);
+        promotions.put(NON_PROMOTION_NAME, NON_PROMOTION);
+        lines.stream().skip(1).forEach(this::processPromotionLine); // Skip header line
+    }
+
+    private void processPromotionLine(String line) {
+        String[] parts = parseAndTrimLine(line);
+        String name = parts[0];
+        int discountRate = Integer.parseInt(parts[1]);
+        int limit = Integer.parseInt(parts[2]);
+
+        if (isPromotionValid(parts)) {
+            promotions.put(name, new Promotion(name, discountRate, limit));
+        }
+    }
+
+    private String[] parseAndTrimLine(String line) {
+        return Arrays.stream(line.split(","))
+                .map(String::trim)
+                .toArray(String[]::new);
+    }
+
+    private boolean isPromotionValid(String[] parts) {
+        LocalDate today = DateTimes.now().toLocalDate();
+        LocalDate startDate = LocalDate.parse(parts[3]);
+        LocalDate endDate = LocalDate.parse(parts[4]);
+        return !today.isBefore(startDate) && !today.isAfter(endDate);
+    }
+
+    public static Promotion getPromotionByName(String name) {
+        return promotions.getOrDefault(name, promotions.get(NON_PROMOTION_NAME));
+    }
+}

리뷰: `today.isAfter(startDate) && today.isBefore(endDate);`은 어떨까요?
개인적으로 ! 연산자가 들어가면 코드를 보는 입장에서 한번 더 생각을 하게 만드는 것 같습니다
@@ -0,0 +1,59 @@
+package store.handler;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import store.domain.Product;
+
+public class StocksFileHandler {
+    private static final Path STOCKS_FILE_PATH = Path.of("src/main/resources/stocks.md");
+
+    private final ProductsFileHandler productsFileHandler;
+
+    public StocksFileHandler() {
+        this.productsFileHandler = new ProductsFileHandler(STOCKS_FILE_PATH);
+    }
+
+    public List<Product> loadStocks() throws IOException {
+        ensureStocksFileExists();
+        productsFileHandler.loadLatestProductsFile();
+        updateStocksFile();
+        return productsFileHandler.parseItemsFromFile(STOCKS_FILE_PATH);
+    }
+
+    private void ensureStocksFileExists() throws IOException {
+        if (Files.notExists(STOCKS_FILE_PATH)) {
+            Files.createFile(STOCKS_FILE_PATH);
+        }
+    }
+
+    private void updateStocksFile() throws IOException {
+        Map<String, Product> productMap = buildProductsMap();
+        writeProductsToStocksFile(productMap);
+    }
+
+    private Map<String, Product> buildProductsMap() throws IOException {
+        List<Product> products = productsFileHandler.parseItemsFromFile(STOCKS_FILE_PATH);
+        Map<String, Product> productMap = new LinkedHashMap<>();
+        for (Product product : products) {
+            productMap.merge(product.getKeyForMap(), product, this::mergeProducts);
+        }
+        return productMap;
+    }
+
+    private Product mergeProducts(Product existing, Product toAdd) {
+        return new Product(existing.getName(), existing.getPrice(),
+                existing.getQuantity() + toAdd.getQuantity(), existing.getPromotion());
+    }
+
+    private void writeProductsToStocksFile(Map<String, Product> productMap) throws IOException {
+        List<String> lines = new ArrayList<>();
+        lines.add(ProductsFileHandler.HEADER);
+        productMap.values().forEach(product -> lines.add(productsFileHandler.formatProductLine(product)));
+        Files.write(STOCKS_FILE_PATH, lines);
+    }
+}

리뷰: 변수명에 자료형이 들어가는 것은 지양하는 것이 좋다고 들었습니다!
@@ -0,0 +1,38 @@
+package store.service;
+
+import store.controller.CartManager;
+import store.domain.Cart;
+import store.domain.Receipt;
+
+public class PaymentService {
+    private static final double MEMBERSHIP_DISCOUNT_RATE = 0.3;
+    private final static int MEMBERSHIP_DISCOUNT_LIMIT = 8000;
+
+    private final CartManager cartManager;
+    private int membershipDiscountBalance;
+
+    public PaymentService(CartManager cartManager) {
+        this.cartManager = cartManager;
+        this.membershipDiscountBalance = MEMBERSHIP_DISCOUNT_LIMIT;
+    }
+
+    public Cart getCartItems(String purchaseList) {
+        return cartManager.generateCart(purchaseList);
+    }
+
+    public Receipt createReceipt(Cart cart, boolean applyMembership) {
+        int membershipDiscount = calcMembershipDiscount(applyMembership, cart.totalRetailPrice());
+        return new Receipt(cart, cart.totalPrice(), cart.totalFreePrice(), membershipDiscount);
+    }
+
+    private int calcMembershipDiscount(boolean apply, int eligibleAmount) {
+        if (apply) {
+            int discountAmount = Math.min((int) (eligibleAmount * MEMBERSHIP_DISCOUNT_RATE),
+                    membershipDiscountBalance);
+            membershipDiscountBalance -= discountAmount;
+
+            return discountAmount;
+        }
+        return 0;
+    }
+}

리뷰: 접근 제어자 작성 순서가 `static final`로 통일되는 것이 좋을 것 같습니다
@@ -0,0 +1,53 @@
+package store.handler;
+
+import camp.nextstep.edu.missionutils.DateTimes;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.time.LocalDate;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import store.domain.Promotion;
+
+public class PromotionsFileHandler {
+    private static final Path PROMOTIONS_FILE_PATH = Path.of("src/main/resources/promotions.md");
+    public static final String NON_PROMOTION_NAME = "null";
+    public static final Promotion NON_PROMOTION = new Promotion(NON_PROMOTION_NAME, 1, 0);
+    private static final Map<String, Promotion> promotions = new HashMap<>();
+
+    public void loadPromotions() throws IOException {
+        List<String> lines = Files.readAllLines(PROMOTIONS_FILE_PATH);
+        promotions.put(NON_PROMOTION_NAME, NON_PROMOTION);
+        lines.stream().skip(1).forEach(this::processPromotionLine); // Skip header line
+    }
+
+    private void processPromotionLine(String line) {
+        String[] parts = parseAndTrimLine(line);
+        String name = parts[0];
+        int discountRate = Integer.parseInt(parts[1]);
+        int limit = Integer.parseInt(parts[2]);
+
+        if (isPromotionValid(parts)) {
+            promotions.put(name, new Promotion(name, discountRate, limit));
+        }
+    }
+
+    private String[] parseAndTrimLine(String line) {
+        return Arrays.stream(line.split(","))
+                .map(String::trim)
+                .toArray(String[]::new);
+    }
+
+    private boolean isPromotionValid(String[] parts) {
+        LocalDate today = DateTimes.now().toLocalDate();
+        LocalDate startDate = LocalDate.parse(parts[3]);
+        LocalDate endDate = LocalDate.parse(parts[4]);
+        return !today.isBefore(startDate) && !today.isAfter(endDate);
+    }
+
+    public static Promotion getPromotionByName(String name) {
+        return promotions.getOrDefault(name, promotions.get(NON_PROMOTION_NAME));
+    }
+}

리뷰: 메서드의 이름으로 어떻게 명시하면 될까요? 별도의 메서드로 분리하라는 의미이실까요?
@@ -0,0 +1,19 @@
+name,price,quantity,promotion
+콜라,1000,10,탄산2+1
+콜라,1000,10,null
+사이다,1000,8,탄산2+1
+사이다,1000,7,null
+오렌지주스,1800,9,MD추천상품
+오렌지주스,1800,0,null
+탄산수,1200,5,탄산2+1
+탄산수,1200,0,null
+물,500,10,null
+비타민워터,1500,6,null
+감자칩,1500,5,반짝할인
+감자칩,1500,5,null
+초코바,1200,5,MD추천상품
+초코바,1200,5,null
+에너지바,2000,5,null
+정식도시락,6400,8,null
+컵라면,1700,1,MD추천상품
+컵라면,1700,10,null

리뷰: 원본 products.md 파일은 파일 이름과 데이터를 아예 변경하지 않았습니다! product.md로 새로운 stocks.md 파일을 생성해 상품 데이터 처리를 했습니다!
@@ -26,11 +26,32 @@ repositories {
 dependencies {
     implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
     implementation 'org.springframework.boot:spring-boot-starter-web'
-    compileOnly 'org.projectlombok:lombok'
+    implementation 'org.springframework.boot:spring-boot-starter-security'
+    implementation 'org.springframework.boot:spring-boot-starter-validation'
+    implementation 'org.springframework.boot:spring-boot-starter-mail'
+
+    // jwt
+    implementation group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.5'
+    runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.5'
+    runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.5'
+
+    // MapStruct
+    implementation 'org.mapstruct:mapstruct:1.4.2.Final'
+    annotationProcessor "org.mapstruct:mapstruct-processor:1.4.2.Final"
+    annotationProcessor "org.projectlombok:lombok-mapstruct-binding:0.1.0"
+
+    // H2
     runtimeOnly 'com.h2database:h2'
+
+    // Lombok
+    compileOnly 'org.projectlombok:lombok'
     annotationProcessor 'org.projectlombok:lombok'
+
+    // Test
     testImplementation 'org.springframework.boot:spring-boot-starter-test'
     testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
+
+
 }
 
 tasks.named('test') {

리뷰: Gradle 형식이 일치하지 않습니다.
다른 곳과 동일하게 조정해주세요.

```groovy
implementaion 'io.jsonwebtoken:jjwt-api:0.11.5'
```