ewhk9887/korean_code_reviews_from_github huggingface dataset:
@@ -0,0 +1,25 @@
+package baseball.controller;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class InputParser {
+
+    public static int parseInt(String input) {
+        try {
+            return Integer.parseInt(input);
+        } catch (NumberFormatException e) {
+            throw new IllegalArgumentException("문자를 입력할 수 없습니다.", e);
+        }
+    }
+
+    public static List<Integer> parseIntList(String input) {
+        List<Integer> list = new ArrayList<>();
+        for (int i = 0; i < input.length(); i++) {
+            list.add(parseInt(input.substring(i, i + 1)));
+        }
+
+        return list;
+    }
+
+}

리뷰: 원천 예외를 넘기신 것 좋아요 🙂
@@ -0,0 +1,25 @@
+package baseball.controller;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class InputParser {
+
+    public static int parseInt(String input) {
+        try {
+            return Integer.parseInt(input);
+        } catch (NumberFormatException e) {
+            throw new IllegalArgumentException("문자를 입력할 수 없습니다.", e);
+        }
+    }
+
+    public static List<Integer> parseIntList(String input) {
+        List<Integer> list = new ArrayList<>();
+        for (int i = 0; i < input.length(); i++) {
+            list.add(parseInt(input.substring(i, i + 1)));
+        }
+
+        return list;
+    }
+
+}

리뷰: 프리코스 피드백에도 나와있듯, 변수명으로 자료형을 쓰는 것은 지양하는 것이 어떨까요?
@@ -0,0 +1,50 @@
+package baseball.controller;
+
+import baseball.config.Score;
+import baseball.dto.SubmitAnswerDto.SubmitAnswerInputDto;
+import baseball.dto.SubmitAnswerDto.SubmitAnswerOutputDto;
+import baseball.service.GameService;
+import baseball.view.OutputView;
+import java.util.List;
+import java.util.Map;
+
+public class GameController {
+    private final GameService gameService;
+
+    public GameController(GameService gameService) {
+        this.gameService = gameService;
+    }
+
+    public void run() {
+        do {
+            this.startGame();
+        } while (RetryInputUtil.getCommand() != 2);
+    }
+
+    private void startGame() {
+        OutputView.printStartMessage();
+        while (true) {
+            if (this.startTurn()) {
+                break;
+            }
+        }
+        OutputView.printEndMessage();
+    }
+
+    private boolean startTurn() {
+        try {
+            List<Integer> userNumbers = RetryInputUtil.getUserNumbers();
+            SubmitAnswerOutputDto submitAnswerOutputDto = this.gameService.submitAnswer(
+                    new SubmitAnswerInputDto(userNumbers));
+
+            Map<Score, Integer> score = submitAnswerOutputDto.score();
+            OutputView.printResult(score);
+
+            return score.get(Score.STRIKE) == 3;
+
+        } catch (IllegalArgumentException error) {
+            OutputView.printError(error.getMessage());
+            return false;
+        }
+    }
+}
\ No newline at end of file

리뷰: 피드백 감사합니다!
@@ -0,0 +1,21 @@
+package baseball.config;
+
+public enum Config {
+    MIN_RANDOM_NUMBER(1),
+    MAX_RANDOM_NUMBER(9),
+    NUMBER_OF_RANDOM_NUMBER(3);
+
+    private final Object value;
+
+    Config(Object value) {
+        this.value = value;
+    }
+
+    public int getInt() {
+        return (int) value;
+    }
+
+    public String getString() {
+        return (String) value;
+    }
+}

리뷰: 오..! 다음부터 제네릭을 사용하는편이 더 좋을 것 같습니다! 감사합니다!!!
@@ -0,0 +1,25 @@
+package baseball.controller;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class InputParser {
+
+    public static int parseInt(String input) {
+        try {
+            return Integer.parseInt(input);
+        } catch (NumberFormatException e) {
+            throw new IllegalArgumentException("문자를 입력할 수 없습니다.", e);
+        }
+    }
+
+    public static List<Integer> parseIntList(String input) {
+        List<Integer> list = new ArrayList<>();
+        for (int i = 0; i < input.length(); i++) {
+            list.add(parseInt(input.substring(i, i + 1)));
+        }
+
+        return list;
+    }
+
+}

리뷰: 으앗...!! 무의식적으로 사용한 것 같습니다.. 반성하겠습니다. 감사합니다!
@@ -0,0 +1,21 @@
+package baseball.config;
+
+public enum Config {
+    MIN_RANDOM_NUMBER(1),
+    MAX_RANDOM_NUMBER(9),
+    NUMBER_OF_RANDOM_NUMBER(3);
+
+    private final Object value;
+
+    Config(Object value) {
+        this.value = value;
+    }
+
+    public int getInt() {
+        return (int) value;
+    }
+
+    public String getString() {
+        return (String) value;
+    }
+}

리뷰: 그런데 제네릭을 어떻게 사용해야 할까요? 방법이 있을까요?
@@ -4,12 +4,23 @@ import { useRouter } from "next/navigation";
 import { useState } from "react";
 import ReviewContent from "./ReviewContent";
 import ArrowDown from "@/assets/images/icons/arrow_down.svg";
+import MenuCircle from "@/assets/images/icons/menu-circle.svg";
+import Dropdown from "@/components/common/Dropdown";
 import { type ReviewInfoProps } from "@/types/review";
+import { type RatingStyle } from "@/types/review";
 
 export default function Review({ reviewInfo }: ReviewInfoProps) {
   const router = useRouter();
   const [isOpen, setIsOpen] = useState(false);
 
+  const reviewTextStyle: RatingStyle = {
+    0: "text-purple-200",
+    1: "text-blue-200",
+    2: "text-orange-200",
+  };
+
+  const ratingArr = ["그냥그래요", "괜찮아요", "추천해요"];
+
   //클릭 시 모임 상세로 이동
   const handleClickReview = () => {
     if (reviewInfo.isMyReview) {
@@ -19,18 +30,58 @@ export default function Review({ reviewInfo }: ReviewInfoProps) {
     return;
   };
 
+  const handleClickModify = () => {
+    alert("리뷰 수정입니다.");
+  };
+
+  const handleClickDelete = () => {
+    alert("리뷰 삭제입니다.");
+  };
+
   const handleClickDetail = (e: React.MouseEvent<HTMLButtonElement>) => {
     e.stopPropagation();
     setIsOpen((prev) => !prev);
   };
   return (
     <div className='rounded-[12px] bg-gray-900 p-4' onClick={handleClickReview}>
       <div className='relative'>
-        <button onClick={handleClickDetail} className='absolute right-0 top-0'>
-          <ArrowDown
-            className={`size-6 text-gray-200 ${isOpen ? "rotate-180" : ""}`}
-          />
-        </button>
+        <div className='flex justify-between'>
+          <span
+            className={`inline-block px-2 py-[3px] text-caption-normal ${reviewTextStyle[reviewInfo.rating]} rounded-[20px] bg-gray-700`}
+          >
+            {ratingArr[reviewInfo.rating]}
+          </span>
+          {reviewInfo.isMyWritten ? (
+            <Dropdown
+              content={[
+                {
+                  label: "수정하기",
+                  value: "modify",
+                  onClick: () => handleClickModify(),
+                },
+                {
+                  label: "삭제하기",
+                  value: "delete",
+                  onClick: () => handleClickDelete(),
+                },
+              ]}
+              isReview={true}
+            >
+              <div className='z-10 cursor-pointer'>
+                <MenuCircle />
+              </div>
+            </Dropdown>
+          ) : (
+            <button
+              onClick={handleClickDetail}
+              className='absolute right-0 top-0'
+            >
+              <ArrowDown
+                className={`size-6 text-gray-200 ${isOpen ? "rotate-180" : ""}`}
+              />
+            </button>
+          )}
+        </div>
         <ReviewContent reviewContent={reviewInfo} isOpen={isOpen} />
       </div>
     </div>

리뷰: 적용해보니 드롭다운이 열리면서 이벤트버블링으로 `handleClickReview`이 실행되고 바로 상세페이지로 넘어가는 것 같아요.
이 부분 처리가 필요할 것 같습니당
(`Dropdown`을 `div`로 한 번 감싸서 `e.stopPropagation()`등으로 막는다거나..)
@@ -0,0 +1,18 @@
+package store;
+
+import store.controller.ProductController;
+import store.controller.PromotionController;
+
+public class StoreApplication {
+    private final ProductController productController;
+    private final PromotionController promotionController;
+
+    public StoreApplication(ProductController productController, PromotionController promotionController) {
+        this.productController = productController;
+        this.promotionController = promotionController;
+    }
+
+    public void run() {
+        productController.handlePurchase();
+    }
+}

리뷰: promotionController가 주입되었지만 사용하지 않고 있습니다!
@@ -0,0 +1,58 @@
+package store.config;
+
+import store.StoreApplication;
+import store.controller.ProductController;
+import store.controller.PromotionController;
+import store.domain.Product;
+import store.domain.Promotion;
+import store.domain.Promotions;
+import store.service.ProductService;
+import store.service.PromotionService;
+import store.utils.ProductLoader;
+import store.utils.PromotionLoader;
+import store.validator.InputConfirmValidator;
+import store.validator.InputConfirmValidatorImpl;
+import store.validator.InputPurchaseValidator;
+import store.validator.InputPurchaseValidatorImpl;
+import store.view.InputView;
+import store.view.OutputView;
+
+import java.util.List;
+
+public class AppConfig {
+    public ProductController productController() {
+        return new ProductController(productService(), outputView(), inputView(), inputPurchaseValidator(),promotionController(),inputConfirmValidator());
+    }
+    public ProductService productService() {
+        return new ProductService(productList());
+    }
+    public StoreApplication storeApplication() {
+        return new StoreApplication(productController(),promotionController());
+    }
+    public List<Product> productList() {
+        return ProductLoader.loadProductsFromFile("src/main/resources/products.md");
+    }
+
+    public List<Promotion> promotionsList() {
+        return PromotionLoader.loadPromotionsFromFile("src/main/resources/promotions.md");
+    }
+    public PromotionController promotionController() {
+        return new PromotionController(promotionService(), outputView(), inputView(),inputConfirmValidator());
+    }
+    public PromotionService promotionService() {
+        return new PromotionService(new Promotions(promotionsList()));
+    }
+
+    public OutputView outputView() {
+        return new OutputView();
+    }
+    public InputView inputView() {
+        return new InputView();
+    }
+    public InputPurchaseValidator inputPurchaseValidator() {
+        return new InputPurchaseValidatorImpl();
+    }
+    public InputConfirmValidator inputConfirmValidator(){
+    return new InputConfirmValidatorImpl();
+    }
+}

리뷰: StoreApplication을 Appconfig에 넣었어도 되지 않을까 생각이 듭니다!
@@ -0,0 +1,71 @@
+package store.controller;
+
+import store.domain.Promotion;
+import store.domain.PromotionResult;
+import store.service.PromotionService;
+import store.validator.InputConfirmValidator;
+import store.view.InputView;
+import store.view.OutputView;
+
+import java.math.BigDecimal;
+
+public class PromotionController {
+    private final PromotionService promotionService;
+    private final OutputView outputView;
+    private final InputView inputView;
+    private final InputConfirmValidator inputConfirmValidator;
+
+    public PromotionController(PromotionService promotionService, OutputView outputView, InputView inputView, InputConfirmValidator inputConfirmValidator) {
+        this.promotionService = promotionService;
+        this.outputView = outputView;
+        this.inputView = inputView;
+        this.inputConfirmValidator = inputConfirmValidator;
+    }
+    Promotion validatePromotionDate(String promotionName) {
+        return promotionService.validatePromotionDate(promotionName);
+    }
+
+    public PromotionResult applyPromotionLogic(String productName, int quantity, String promotionName, BigDecimal productPrice, int promoStock) {
+        Promotion applicablePromotion = promotionService.findPromotionName(promotionName);
+        if (applicablePromotion == null) {
+            return new PromotionResult(quantity, 0, BigDecimal.ZERO, BigDecimal.ZERO); // 유효하지 않은 경우, 수량 그대로 반환
+        }
+
+        int buyQuantity = applicablePromotion.getBuyQuantity();
+        int getQuantity = applicablePromotion.getGetQuantity();
+        int freeQuantity = 0;
+        int promotionQuantity = quantity;
+
+        if(promoStock<=quantity){
+            promotionQuantity = promoStock;
+        }
+
+        BigDecimal discountAmount = BigDecimal.ZERO;
+        if (shouldOfferFreeProduct(buyQuantity, quantity) && promoStock>quantity) {
+            String confirmInput = inputView.confirmPromotionAdditionMessage(productName,getQuantity);
+            inputConfirmValidator.validateConfirmation(confirmInput);
+            if (confirmInput.equalsIgnoreCase("Y")) {
+                quantity=++promotionQuantity;
+            }
+        }
+        if(buyQuantity==2){
+            freeQuantity = promotionQuantity/3;
+        }
+        if (buyQuantity==1){
+            freeQuantity = promotionQuantity/2;
+        }
+        discountAmount=productPrice.multiply(BigDecimal.valueOf(freeQuantity));
+        return new PromotionResult(quantity, freeQuantity, discountAmount, discountAmount.multiply(BigDecimal.valueOf(++buyQuantity)));
+    }
+
+
+    private boolean shouldOfferFreeProduct(int buyQuantity, int quantity) {
+        if (buyQuantity == 2 && (quantity % 3) == 2) {
+            return true; // 탄산 2+1의 경우
+        }
+        if (buyQuantity == 1 && (quantity % 2) == 1) {
+            return true; // MD추천 상품, 반짝할인의 경우
+        }
+        return false;
+    }
+}

리뷰: output view가 사용되지 않는데 제거해도 좋을 거 같습니다!
@@ -0,0 +1,23 @@
+package store.validator;
+
+import store.utils.ErrorMessages;
+
+public class InputPurchaseValidatorImpl implements InputPurchaseValidator{
+
+    private static final String PRODUCT_INPUT_REGEX = "\\[.*-\\d+\\]";
+
+    @Override
+    public void validateProductInput(String input) {
+        if (!input.matches(PRODUCT_INPUT_REGEX)) {
+            throw new IllegalArgumentException(ErrorMessages.INVALID_INPUT_MESSAGE);
+        }
+    }
+
+    @Override
+    public void validateQuantity(int quantity) {
+        if (quantity <= 0) {
+            throw new IllegalArgumentException(ErrorMessages.INVALID_QUANTITY_MESSAGE);
+        }
+    }
+}
+

리뷰: 정규표현식 이렇게 작성했으면 되는군요.. 배워갑니다
@@ -0,0 +1,41 @@
+package store.service;
+
+import camp.nextstep.edu.missionutils.DateTimes;
+import store.domain.Promotion;
+import store.domain.Promotions;
+
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+
+
+public class PromotionService {
+    private final Promotions promotions;
+
+    public PromotionService(Promotions promotions) {
+        this.promotions = promotions;
+    }
+
+    public Promotion validatePromotionDate(String promotionName) {
+        LocalDateTime currentDate = DateTimes.now();
+
+        Promotion promotion = promotions.getPromotions().stream()
+                .filter(p -> p.getPromotionName().equals(promotionName))
+                .findFirst()
+                .orElse(null);
+        if (promotion!=null) {
+            LocalDate startDate = LocalDate.parse(promotion.getStartDate());
+            LocalDate endDate = LocalDate.parse(promotion.getEndDate());
+            if (currentDate.isBefore(startDate.atStartOfDay()) || currentDate.isAfter(endDate.atStartOfDay())) {
+                return null;
+            }
+        }
+        return promotion;
+    }
+
+    public Promotion findPromotionName(String promotionName) {
+        return promotions.getPromotions().stream()
+                .filter(promotion -> promotion.getPromotionName().equals(promotionName))
+                .findFirst()
+                .orElse(null);
+    }
+}

리뷰: null 가능성이 있다면 Optional을 활용하는 것도 좋은 방법일 것 같아용 :)
@@ -0,0 +1,71 @@
+package store.controller;
+
+import store.domain.Promotion;
+import store.domain.PromotionResult;
+import store.service.PromotionService;
+import store.validator.InputConfirmValidator;
+import store.view.InputView;
+import store.view.OutputView;
+
+import java.math.BigDecimal;
+
+public class PromotionController {
+    private final PromotionService promotionService;
+    private final OutputView outputView;
+    private final InputView inputView;
+    private final InputConfirmValidator inputConfirmValidator;
+
+    public PromotionController(PromotionService promotionService, OutputView outputView, InputView inputView, InputConfirmValidator inputConfirmValidator) {
+        this.promotionService = promotionService;
+        this.outputView = outputView;
+        this.inputView = inputView;
+        this.inputConfirmValidator = inputConfirmValidator;
+    }
+    Promotion validatePromotionDate(String promotionName) {
+        return promotionService.validatePromotionDate(promotionName);
+    }
+
+    public PromotionResult applyPromotionLogic(String productName, int quantity, String promotionName, BigDecimal productPrice, int promoStock) {
+        Promotion applicablePromotion = promotionService.findPromotionName(promotionName);
+        if (applicablePromotion == null) {
+            return new PromotionResult(quantity, 0, BigDecimal.ZERO, BigDecimal.ZERO); // 유효하지 않은 경우, 수량 그대로 반환
+        }
+
+        int buyQuantity = applicablePromotion.getBuyQuantity();
+        int getQuantity = applicablePromotion.getGetQuantity();
+        int freeQuantity = 0;
+        int promotionQuantity = quantity;
+
+        if(promoStock<=quantity){
+            promotionQuantity = promoStock;
+        }
+
+        BigDecimal discountAmount = BigDecimal.ZERO;
+        if (shouldOfferFreeProduct(buyQuantity, quantity) && promoStock>quantity) {
+            String confirmInput = inputView.confirmPromotionAdditionMessage(productName,getQuantity);
+            inputConfirmValidator.validateConfirmation(confirmInput);
+            if (confirmInput.equalsIgnoreCase("Y")) {
+                quantity=++promotionQuantity;
+            }
+        }
+        if(buyQuantity==2){
+            freeQuantity = promotionQuantity/3;
+        }
+        if (buyQuantity==1){
+            freeQuantity = promotionQuantity/2;
+        }
+        discountAmount=productPrice.multiply(BigDecimal.valueOf(freeQuantity));
+        return new PromotionResult(quantity, freeQuantity, discountAmount, discountAmount.multiply(BigDecimal.valueOf(++buyQuantity)));
+    }
+
+
+    private boolean shouldOfferFreeProduct(int buyQuantity, int quantity) {
+        if (buyQuantity == 2 && (quantity % 3) == 2) {
+            return true; // 탄산 2+1의 경우
+        }
+        if (buyQuantity == 1 && (quantity % 2) == 1) {
+            return true; // MD추천 상품, 반짝할인의 경우
+        }
+        return false;
+    }
+}

리뷰: 이 메서드의 로직들이 굉장히 도메인에 많이 닿아있는 것 같아요. 복잡하기도 하고요!

도메인 객체로 따로 빼는건 어떻게 생각하세요?
@@ -0,0 +1,39 @@
+package store.domain;
+
+public class Promotion {
+    private String promotionName;
+    private int buyQuantity;
+    private int getQuantity;
+    private String startDate;
+    private String endDate;
+
+    public Promotion(String promotionName, int buyQuantity, int getQuantity, String startDate, String endDate) {
+        this.promotionName = promotionName;
+        this.buyQuantity = buyQuantity;
+        this.getQuantity = getQuantity;
+        this.startDate = startDate;
+        this.endDate = endDate;
+    }
+
+
+    public String getPromotionName() {
+        return promotionName;
+    }
+
+    public int getBuyQuantity() {
+        return buyQuantity;
+    }
+
+    public int getGetQuantity() {
+        return getQuantity;
+    }
+
+    public String getStartDate() {
+        return startDate;
+    }
+
+    public String getEndDate() {
+        return endDate;
+    }
+
+}

리뷰: date타입이라면 LocalDate를 활용해도 좋았을 것 같은데, String으로 가져가신 이유가 있나요?
@@ -0,0 +1,59 @@
+package store.inventory;
+
+import java.util.Collections;
+import java.util.List;
+import store.inventory.product.Product;
+import store.inventory.product.ProductProcessor;
+import store.inventory.promotion.PromotionType;
+
+public class Stock {
+    private static final int MAX_PROMOTION_COUNT = 1;
+
+    private final List<Product> products;
+
+    public Stock(ProductProcessor productProcessor) {
+        this.products = productProcessor.getProducts();
+    }
+
+    public List<Product> get() {
+        return Collections.unmodifiableList(products);
+    }
+
+    public boolean isContained(String productName) {
+        return products.stream()
+                .anyMatch(product -> product.getName().equals(productName));
+    }
+
+    public boolean hasPromotion(String productName) {
+        return products.stream()
+                .filter(product -> product.getName().equals(productName))
+                .count() > MAX_PROMOTION_COUNT;
+    }
+
+    public boolean isPromotionStockNotEnough(String productName, int desiredQuantity) {
+        return getPromotionProduct(productName).getQuantity() < desiredQuantity;
+    }
+
+    public Product getGeneralProduct(String productName) {
+        return products.stream()
+                .filter(product -> product.getName().equals(productName)
+                        && product.getPromotionType().equals(PromotionType.NONE))
+                .findAny()
+                .get();
+    }
+
+    public Product getPromotionProduct(String productName) {
+        return products.stream()
+                .filter(product -> product.getName().equals(productName)
+                        && !product.getPromotionType().equals(PromotionType.NONE))
+                .findAny()
+                .get();
+    }
+
+    public int getAvailableQuantity(String productName) {
+        return products.stream()
+                .filter(product -> product.getName().equals(productName))
+                .mapToInt(Product::getQuantity)
+                .sum();
+    }
+}

리뷰: 저번에도 느꼈지만 `stream()`을 정말 잘 사용하시는 것 같아요! `stream()`을 사용하니 코드가 깔끔하니 좋은 것 같아요!
@@ -0,0 +1,48 @@
+package store.inventory.product;
+
+import store.inventory.promotion.PromotionResult;
+import store.inventory.promotion.PromotionType;
+
+public class Product {
+    private final String name;
+    private final int price;
+    private int quantity;
+    private final PromotionType promotionType;
+    private final String promotionName;
+
+    public Product(String name, int price, int quantity, PromotionType promotionType, String promotionName) {
+        this.name = name;
+        this.price = price;
+        this.quantity = quantity;
+        this.promotionType = promotionType;
+        this.promotionName = promotionName;
+    }
+
+    public String getName() {
+        return this.name;
+    }
+
+    public int getPrice() {
+        return this.price;
+    }
+
+    public int getQuantity() {
+        return this.quantity;
+    }
+
+    public PromotionType getPromotionType() {
+        return this.promotionType;
+    }
+
+    public PromotionResult getPromotionDetails(int desiredQuantity) {
+        return this.promotionType.getDetails(desiredQuantity);
+    }
+
+    public String getPromotionName() {
+        return this.promotionName;
+    }
+
+    public void deduct(int minusQuantity) {
+        this.quantity -= minusQuantity;
+    }
+}

리뷰: Promotion 대신 PromotionType을 사용하신 이유가 궁금합니다!
@@ -0,0 +1,48 @@
+package store.inventory.product;
+
+import store.inventory.promotion.PromotionResult;
+import store.inventory.promotion.PromotionType;
+
+public class Product {
+    private final String name;
+    private final int price;
+    private int quantity;
+    private final PromotionType promotionType;
+    private final String promotionName;
+
+    public Product(String name, int price, int quantity, PromotionType promotionType, String promotionName) {
+        this.name = name;
+        this.price = price;
+        this.quantity = quantity;
+        this.promotionType = promotionType;
+        this.promotionName = promotionName;
+    }
+
+    public String getName() {
+        return this.name;
+    }
+
+    public int getPrice() {
+        return this.price;
+    }
+
+    public int getQuantity() {
+        return this.quantity;
+    }
+
+    public PromotionType getPromotionType() {
+        return this.promotionType;
+    }
+
+    public PromotionResult getPromotionDetails(int desiredQuantity) {
+        return this.promotionType.getDetails(desiredQuantity);
+    }
+
+    public String getPromotionName() {
+        return this.promotionName;
+    }
+
+    public void deduct(int minusQuantity) {
+        this.quantity -= minusQuantity;
+    }
+}

리뷰: 프로모션은 파일에서 읽어와 프로모션 이름과 프로모션 타입을 매칭한 이후에는, 즉 실제 비즈니스 로직에서는 1+1, 2+1와 같이 프로모션 타입 정보만 필요하다고 생각했기 때문입니다!