ewhk9887/korean_code_reviews_from_github huggingface dataset:
@@ -0,0 +1,67 @@
+package subway.controller;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import subway.command.Command;
+import subway.command.MainCommand;
+import subway.controller.handler.Handler;
+import subway.controller.handler.LineHandler;
+import subway.controller.handler.SectionHandler;
+import subway.controller.handler.StationHandler;
+import subway.controller.retryInputUtil.RetryInputUtil;
+import subway.dto.LineDto;
+import subway.service.LineService;
+import subway.service.SectionService;
+import subway.service.StationService;
+import subway.view.OutputView;
+
+public class SubwayController {
+    private final LineService lineService;
+    private final Map<MainCommand, Handler> commandHandlers;
+
+    public SubwayController(StationService stationService, LineService lineService, SectionService sectionService) {
+        this.lineService = lineService;
+
+        this.commandHandlers = new HashMap<>();
+        this.commandHandlers.put(MainCommand.STATION, new StationHandler(stationService));
+        this.commandHandlers.put(MainCommand.LINE, new LineHandler(lineService));
+        this.commandHandlers.put(MainCommand.SECTION, new SectionHandler(sectionService));
+    }
+
+    public void run() {
+        int state = 0;
+
+        while (state == 0) {
+            try {
+                OutputView.printMainMenu();
+                MainCommand mainCommand = Command.findCommand(RetryInputUtil.getMainCommand(), MainCommand.values());
+
+                state = mainLogic(mainCommand);
+            } catch (IllegalArgumentException error) {
+                OutputView.printError(error.getMessage());
+            }
+        }
+    }
+
+    private int mainLogic(MainCommand mainCommand) {
+        if (mainCommand == MainCommand.QUIT) {
+            return 1;
+        }
+
+        if (this.commandHandlers.containsKey(mainCommand)) {
+            this.commandHandlers.get(mainCommand).handle();
+        }
+        if (mainCommand == MainCommand.LINE_PRINT) {
+            printLineMap();
+        }
+
+        return 0;
+    }
+
+    private void printLineMap() {
+        List<LineDto> lines = lineService.retrieve().lines();
+        OutputView.printLineMap(lines);
+    }
+
+}

리뷰: 피드백 감사합니다! C를 많이 해서 그런지 습관적으로 int를 사용했네요! boolean으로 해도 좋을 것 같습니다!
@@ -0,0 +1,54 @@
+package subway.controller.handler;
+
+import java.util.List;
+import subway.command.Command;
+import subway.command.StationCommand;
+import subway.controller.retryInputUtil.StationRetryInput;
+import subway.dto.StationDto;
+import subway.dto.StationRegisterDto.StationRegisterInputDto;
+import subway.dto.StationRemoveDto.StationRemoveInputDto;
+import subway.service.StationService;
+import subway.view.OutputView;
+
+public class StationHandler implements Handler {
+    private final StationService stationService;
+
+    public StationHandler(StationService stationService) {
+        this.stationService = stationService;
+    }
+
+    @Override
+    public void handle() {
+        OutputView.printStationManageMenu();
+        StationCommand stationCommand = Command.findCommand(StationRetryInput.getCommand(), StationCommand.values());
+
+        if (stationCommand == StationCommand.REGISTER) {
+            registerStation();
+        }
+        if (stationCommand == StationCommand.REMOVE) {
+            removeStation();
+        }
+        if (stationCommand == StationCommand.RETRIEVE) {
+            retrieveStation();
+        }
+        if (stationCommand == StationCommand.BACK) {
+            // nothing to do.
+        }
+    }
+
+    private void registerStation() {
+        String stationName = StationRetryInput.getStationName();
+        stationService.register(new StationRegisterInputDto(stationName));
+    }
+
+    private void removeStation() {
+        String stationName = StationRetryInput.getRemoveStationName();
+        stationService.remove(new StationRemoveInputDto(stationName));
+        OutputView.printStationRemovedMessage();
+    }
+
+    private void retrieveStation() {
+        List<StationDto> stations = stationService.retrieve().stations();
+        OutputView.printStations(stations);
+    }
+}

리뷰: 리뷰 감사합니다. BACK부분은 처리하는 것이 없기 때문에 굳이 작성하지 않아도 흐름상 문제가 없습니다. 다만 아무런 처리를 하지 않더라도 "아무것도 처리하지 않는다"라는 메시지를 주고 싶었기 때문에 남겨두었습니다. 명시적으로 "BACK이라는 명령이 있는데 아무것도 처리하지 않고 종료할거야"라는 메시지를 주고 싶었습니다!
@@ -0,0 +1,52 @@
+package subway.config;
+
+import java.io.BufferedReader;
+import java.io.InputStreamReader;
+import subway.controller.SubwayController;
+import subway.repository.LineRepository;
+import subway.repository.StationRepository;
+import subway.service.LineService;
+import subway.service.SectionService;
+import subway.service.StationService;
+import subway.utils.CsvReader;
+import subway.utils.LineParser;
+import subway.utils.StationParser;
+
+public class DependencyInjector {
+    public SubwayController createSubwayController() {
+        StationRepository stationRepository = new StationRepository();
+        LineRepository lineRepository = new LineRepository();
+
+        StationService stationService = new StationService(stationRepository);
+        SectionService sectionService = new SectionService(lineRepository, stationRepository);
+        LineService lineService = new LineService(lineRepository, stationRepository);
+
+        SubwayFileInitializer subwayFileInitializer = createSubwayFileInitializer(stationRepository, lineRepository,
+                stationService, sectionService);
+        subwayFileInitializer.init();
+
+        return new SubwayController(stationService, lineService, sectionService);
+    }
+
+    public SubwayFileInitializer createSubwayFileInitializer(StationRepository stationRepository,
+                                                             LineRepository lineRepository,
+                                                             StationService stationService,
+                                                             SectionService sectionService) {
+        StationParser stationParser = new StationParser(
+                new CsvReader(this.createBufferedReader(Configuration.STATION_FILE_NAME.getString()),
+                        false));
+        LineParser lineParser = new LineParser(
+                new CsvReader(this.createBufferedReader(Configuration.LINE_FILE_NAME.getString()), false));
+
+        return new SubwayFileInitializer(
+                stationParser, lineParser, stationRepository, lineRepository
+        );
+    }
+
+
+    private BufferedReader createBufferedReader(String fileName) {
+        return new BufferedReader(new InputStreamReader(
+                DependencyInjector.class.getClassLoader().getResourceAsStream(fileName)
+        ));
+    }
+}

리뷰: 싱글톤으로 만든다는게 static으로 선언하는 것을 말씀하시는건가요??
@@ -0,0 +1,16 @@
+package subway.command;
+
+public interface Command {
+    static <T extends Command> T findCommand(String input, T[] commands) {
+        for (T command : commands) {
+            if (command.getCommand().equals(input)) {
+                return command;
+            }
+        }
+        throw new IllegalArgumentException("선택할 수 없는 기능입니다.");
+    }
+
+    String getCommand();
+
+    String getDescription();
+}

리뷰: 이유는... 없습니다. 최대한 빠르게 개발하는 것을 목표로 해서 디테일한 부분은 가져가지 못한 것 같습니다 ㅠㅠ
@@ -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);
+        }
     }
 }

리뷰: 여기서 추가구매에 대한 응답으로 'Y'를 입력하면 매 반복마다 CarService와 PurchaseController가 새로 생성되는 문제가
있을 것 같습니다.
@@ -0,0 +1,7 @@
+package store.entity;
+
+import java.time.LocalDate;
+
+public interface DateProvider {
+    LocalDate now();
+}

리뷰: 날짜에 대한 정보를 제공해주는 인터페이스를 따로 생성하신 이유를 알 수 있을까요?
@@ -0,0 +1,11 @@
+package store.entity;
+
+public class Membership {
+    private static final int discountPercent = 30;
+    private static final int maxDiscountAmount = 8000;
+
+    public int discount(Cart nonDiscountedCart) {
+        int discountAmount = nonDiscountedCart.getTotalPrice() * discountPercent / 100;
+        return Math.min(discountAmount, maxDiscountAmount);
+    }
+}

리뷰: `Math.min()`을 사용해서 할인최대치가 초과되지 않도록 깔끔하게 잘 구현해주신 것 같습니다.
@@ -0,0 +1,57 @@
+package store.entity;
+
+import java.time.LocalDate;
+import java.util.Objects;
+
+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(String name, int buy, int get, LocalDate startDate, LocalDate endDate) {
+        this.name = name;
+        this.buy = buy;
+        this.get = get;
+        this.startDate = startDate;
+        this.endDate = endDate;
+    }
+
+    public boolean isPromotionValid(LocalDate currentDate) {
+        return !currentDate.isBefore(startDate) && !currentDate.isAfter(endDate);
+    }
+
+    public boolean isInsufficientPromotion(Integer value) {
+        return value % (buy + get) >= buy;
+    }
+
+    public Integer getInsufficientPromotionQuantity(Integer value) {
+        return (buy + get) - value % (buy + get);
+    }
+
+    public Integer getCycle() {
+        return buy + get;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        Promotion promotion = (Promotion) o;
+        return buy == promotion.buy &&
+                get == promotion.get &&
+                Objects.equals(name, promotion.name) &&
+                Objects.equals(startDate, promotion.startDate) &&
+                Objects.equals(endDate, promotion.endDate);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(name, buy, get, startDate, endDate);
+    }
+}

리뷰: 개인적인 생각으로 앞에 !연산자가 들어가게 되면 코드를 읽는 도중 한번 더 생각을 하게 만드는 것 같습니다.
`currentDate.isAfter(startDate) && currentDate.isBefore(endDate);`과 같은 방법도 있을 것 같습니다!
@@ -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();
+    }
+}

리뷰: else문 사용은 가급적이면 지양해주시는 게 좋다고 들었습니다
@@ -0,0 +1,40 @@
+package store.util.parse;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import store.entity.DateProvider;
+import store.entity.Product;
+import store.entity.Promotion;
+import store.entity.RealDateProvider;
+
+public class ProductParser {
+    private static final DateProvider dateProvider = new RealDateProvider();
+
+    public static List<Product> parse(List<String> products, Map<String, Promotion> promotions) {
+        List<Product> stockProduct = new ArrayList<>();
+        for (int i = 1; i < products.size(); i++) { // 헤더 부분 제외
+            String[] fields = products.get(i).split(",");
+            int price = Integer.parseInt(fields[1]);
+            int quantity = Integer.parseInt(fields[2]);
+            Promotion promotion = promotions.get(fields[3]);
+            addOrUpdateProduct(stockProduct, fields[0], price, quantity, promotion);
+        }
+        return stockProduct;
+    }
+
+    private static void addOrUpdateProduct(List<Product> products, String name, int price, int quantity,
+                                           Promotion promotion) {
+        findProduct(products, name).ifPresentOrElse(
+                existingProduct -> existingProduct.addQuantity(quantity, promotion),
+                () -> products.add(new Product(name, price, quantity, promotion, dateProvider))
+        );
+    }
+
+    private static Optional<Product> findProduct(List<Product> products, String name) {
+        return products.stream()
+                .filter(p -> p.getName().equals(name))
+                .findFirst();
+    }
+}

리뷰: 보통 컬렉션이나 배열을 순회할 때 인덱스가 0부터 시작하는 걸로 알고 있습니다.
시작인덱스가 왜 1인지 상수로 표현해주시면 좋을 것 같단 생각이 듭니다!
@@ -0,0 +1,30 @@
+package store.util.parse;
+
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import store.entity.Promotion;
+
+public class PromotionParser {
+    private static final String SEPARATOR = ",";
+    private static final DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
+
+    public static Map<String, Promotion> parse(List<String> promotions) {
+        return promotions.stream()
+                .skip(1)  // 첫 번째 헤더 부분 제외
+                .map(PromotionParser::parsePromotionLine)
+                .collect(Collectors.toMap(Promotion::getName, promotion -> promotion));
+    }
+
+    private static Promotion parsePromotionLine(String promotion) {
+        String[] fields = promotion.split(SEPARATOR);
+        String name = fields[0];
+        int buy = Integer.parseInt(fields[1]);
+        int get = Integer.parseInt(fields[2]);
+        LocalDate startDate = LocalDate.parse(fields[3], dateTimeFormatter);
+        LocalDate endDate = LocalDate.parse(fields[4], dateTimeFormatter);
+        return new Promotion(name, buy, get, startDate, endDate);
+    }
+}

리뷰: 첫 번째 헤더를 제외한다는 주석 대신 1이 가지는 의미를 상수로 정의해주셔도 괜찮았을 것 같습니다.
@@ -0,0 +1,7 @@
+package store.entity;
+
+import java.time.LocalDate;
+
+public interface DateProvider {
+    LocalDate now();
+}

리뷰: 테스트 할때 해당 클래스를 그대로 사용하면 오늘 날짜로 테스트가 되기 때문입니다!
@@ -0,0 +1,57 @@
+package store.entity;
+
+import java.time.LocalDate;
+import java.util.Objects;
+
+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(String name, int buy, int get, LocalDate startDate, LocalDate endDate) {
+        this.name = name;
+        this.buy = buy;
+        this.get = get;
+        this.startDate = startDate;
+        this.endDate = endDate;
+    }
+
+    public boolean isPromotionValid(LocalDate currentDate) {
+        return !currentDate.isBefore(startDate) && !currentDate.isAfter(endDate);
+    }
+
+    public boolean isInsufficientPromotion(Integer value) {
+        return value % (buy + get) >= buy;
+    }
+
+    public Integer getInsufficientPromotionQuantity(Integer value) {
+        return (buy + get) - value % (buy + get);
+    }
+
+    public Integer getCycle() {
+        return buy + get;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        Promotion promotion = (Promotion) o;
+        return buy == promotion.buy &&
+                get == promotion.get &&
+                Objects.equals(name, promotion.name) &&
+                Objects.equals(startDate, promotion.startDate) &&
+                Objects.equals(endDate, promotion.endDate);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(name, buy, get, startDate, endDate);
+    }
+}

리뷰: 그렇네요! 좋은 의견감사합니다!
@@ -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();
+    }
+}

리뷰: 허걱.. 이걸 못봤네요..
@@ -0,0 +1,40 @@
+package store.util.parse;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import store.entity.DateProvider;
+import store.entity.Product;
+import store.entity.Promotion;
+import store.entity.RealDateProvider;
+
+public class ProductParser {
+    private static final DateProvider dateProvider = new RealDateProvider();
+
+    public static List<Product> parse(List<String> products, Map<String, Promotion> promotions) {
+        List<Product> stockProduct = new ArrayList<>();
+        for (int i = 1; i < products.size(); i++) { // 헤더 부분 제외
+            String[] fields = products.get(i).split(",");
+            int price = Integer.parseInt(fields[1]);
+            int quantity = Integer.parseInt(fields[2]);
+            Promotion promotion = promotions.get(fields[3]);
+            addOrUpdateProduct(stockProduct, fields[0], price, quantity, promotion);
+        }
+        return stockProduct;
+    }
+
+    private static void addOrUpdateProduct(List<Product> products, String name, int price, int quantity,
+                                           Promotion promotion) {
+        findProduct(products, name).ifPresentOrElse(
+                existingProduct -> existingProduct.addQuantity(quantity, promotion),
+                () -> products.add(new Product(name, price, quantity, promotion, dateProvider))
+        );
+    }
+
+    private static Optional<Product> findProduct(List<Product> products, String name) {
+        return products.stream()
+                .filter(p -> p.getName().equals(name))
+                .findFirst();
+    }
+}

리뷰: 좋은 의견 감사합니다!
@@ -0,0 +1,7 @@
+package store.entity;
+
+import java.time.LocalDate;
+
+public interface DateProvider {
+    LocalDate now();
+}

리뷰: > 테스트 할때 해당 클래스를 그대로 사용하면 오늘 날짜로 테스트가 되기 때문입니다!

이 부분에 대해 제가 잘 이해를 하지 못하였는데, 간단한 예시를 말씀해주실 수 있을까요??