ewhk9887/korean_code_reviews_from_github huggingface dataset:
@@ -0,0 +1,57 @@
+package store.service;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import store.domain.Product;
+import store.domain.ShortageQuantity;
+import store.domain.request.PurchaseRequest;
+import store.repository.ProductRepository;
+
+public class ProductService {
+
+    private final ProductRepository productRepository;
+
+    public ProductService(ProductRepository productRepository) {
+        this.productRepository = productRepository;
+    }
+
+    public Product findProduct(String name) {
+        return productRepository.findProduct(name);
+    }
+
+    public Map<String, Product> getProducts() {
+        return productRepository.products();
+    }
+
+    public List<ShortageQuantity> getShortageQuantityForPromotion(List<PurchaseRequest> purchases) {
+        return getShortageQuantities(purchases);
+    }
+
+    private List<ShortageQuantity> getShortageQuantities(List<PurchaseRequest> purchases) {
+        return purchases.stream()
+                .map(purchase -> {
+                    Product product = findProduct(purchase.getName());
+
+                    if (!product.existsPromotion()) {
+                        return null;
+                    }
+
+                    int quantityRequested = purchase.getQuantity();
+                    int shortageQuantity = product.getShortageQuantityForPromotion(quantityRequested);
+
+                    if (shortageQuantity <= 0) {
+                        return null;
+                    }
+
+                    boolean isSufficient = product.getPromotionQuantity() >= quantityRequested + shortageQuantity;
+                    int actualShortageQuantity = isSufficient
+                            ? shortageQuantity
+                            : quantityRequested + shortageQuantity - product.getPromotionQuantity();
+
+                    return new ShortageQuantity(purchase, product.getName(), actualShortageQuantity, isSufficient);
+                })
+                .filter(Objects::nonNull)
+                .toList();
+    }
+}

리뷰: Map을 직접 꺼내주는 것은 데이터를 레포지토리를 통하지 않고 직접 데이터를 변경할 수 있다고 생각합니다. collections.unmodifiablemap을 사용하여 변경할 수 없도록 만들거나 이 메서드로 꺼낸 Map에서 사용한 기능들을 repository에서 직접 건드는 것이 더 좋을 것 같습니다.
@@ -0,0 +1,11 @@
+package store.repository;
+
+import java.util.Map;
+import store.domain.Product;
+
+public record ProductRepository(Map<String, Product> products) {
+
+    public Product findProduct(String name) {
+        return products.get(name);
+    }
+}

리뷰: repository는 정보를 저장하는 것이 아닌 정보 저장소에서 정보를 꺼내오는 것으로 알고 있습니다. 이 클래스는 정보를 저장하면 안될 것 같습니다.
@@ -0,0 +1,57 @@
+package store.service;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import store.domain.Product;
+import store.domain.ShortageQuantity;
+import store.domain.request.PurchaseRequest;
+import store.repository.ProductRepository;
+
+public class ProductService {
+
+    private final ProductRepository productRepository;
+
+    public ProductService(ProductRepository productRepository) {
+        this.productRepository = productRepository;
+    }
+
+    public Product findProduct(String name) {
+        return productRepository.findProduct(name);
+    }
+
+    public Map<String, Product> getProducts() {
+        return productRepository.products();
+    }
+
+    public List<ShortageQuantity> getShortageQuantityForPromotion(List<PurchaseRequest> purchases) {
+        return getShortageQuantities(purchases);
+    }
+
+    private List<ShortageQuantity> getShortageQuantities(List<PurchaseRequest> purchases) {
+        return purchases.stream()
+                .map(purchase -> {
+                    Product product = findProduct(purchase.getName());
+
+                    if (!product.existsPromotion()) {
+                        return null;
+                    }
+
+                    int quantityRequested = purchase.getQuantity();
+                    int shortageQuantity = product.getShortageQuantityForPromotion(quantityRequested);
+
+                    if (shortageQuantity <= 0) {
+                        return null;
+                    }
+
+                    boolean isSufficient = product.getPromotionQuantity() >= quantityRequested + shortageQuantity;
+                    int actualShortageQuantity = isSufficient
+                            ? shortageQuantity
+                            : quantityRequested + shortageQuantity - product.getPromotionQuantity();
+
+                    return new ShortageQuantity(purchase, product.getName(), actualShortageQuantity, isSufficient);
+                })
+                .filter(Objects::nonNull)
+                .toList();
+    }
+}

리뷰: 메서드를 따로 뽑아내는 것이 더 좋을 것 같습니다...
@@ -0,0 +1,18 @@
+package store.service;
+
+import java.util.Map;
+import store.domain.Promotion;
+import store.repository.PromotionRepository;
+
+public class PromotionService {
+
+    private final PromotionRepository promotionRepository;
+
+    public PromotionService(PromotionRepository promotionRepository) {
+        this.promotionRepository = promotionRepository;
+    }
+
+    public void setPromotion(Map<String, Promotion> promotions) {
+        promotionRepository.setPromotion(promotions);
+    }
+}

리뷰: 각 계층에서 역할이 제대로 분배되지않아 발생하는 의미없는 메서드가 된 것 같습니다.
@@ -0,0 +1,56 @@
+package store.config;
+
+import java.util.Map;
+import store.controller.ConvenienceController;
+import store.convenience.Convenience;
+import store.domain.Product;
+import store.domain.Promotion;
+import store.reader.StoreFileReader;
+import store.repository.ProductRepository;
+import store.repository.PromotionRepository;
+import store.service.ConvenienceService;
+import store.service.ProductService;
+import store.service.PromotionService;
+import store.view.InputView;
+import store.view.OutputView;
+
+public class ConvenienceFactory {
+
+    public static Convenience createConvenience() {
+        return new Convenience(convenienceController(), outputView(), inputView());
+    }
+
+    public static ConvenienceController convenienceController() {
+        return new ConvenienceController(convenienceService());
+    }
+
+    public static ConvenienceService convenienceService() {
+        return new ConvenienceService(productService(), promotionService());
+    }
+
+    public static OutputView outputView() {
+        return new OutputView();
+    }
+
+    public static InputView inputView() {
+        return new InputView();
+    }
+
+    public static ProductRepository productRepository() {
+        Map<String, Promotion> promotions = StoreFileReader.promotions();
+        Map<String, Product> products = StoreFileReader.product(promotions);
+        return new ProductRepository(products);
+    }
+
+    public static ProductService productService() {
+        return new ProductService(productRepository());
+    }
+
+    public static PromotionService promotionService() {
+        return new PromotionService(promotionRepository());
+    }
+
+    public static PromotionRepository promotionRepository() {
+        return new PromotionRepository(StoreFileReader.promotions());
+    }
+}

리뷰: 놓쳤던 부분이네요 감사합니다!
@@ -0,0 +1,57 @@
+import {
+  DetailReview,
+  PostReviewRequestBody,
+  PutReviewRequestBody,
+  Review,
+} from '@typings/types';
+import { authInstance, defaultInstance } from '.';
+
+// 상세페이지 리뷰 전체 목록 조회(비로그인)
+export const getAllReview = async (
+  workplaceId: number,
+  nextCursor?: number,
+): Promise<{ data: DetailReview[]; nextCursor: number }> => {
+  const response = await defaultInstance.get(
+    `/api/v1/review/workplace/${workplaceId}`,
+    {
+      params: nextCursor ? { lastId: nextCursor } : {},
+    },
+  );
+  return response.data;
+};
+
+// 내가 작성한 리뷰 조회 - 로그인
+export const getMyReview = async (): Promise<Review[]> => {
+  const response = await authInstance.get('/api/v1/review/me');
+  return response.data;
+};
+
+// 리뷰 작성
+export const postReview = async (
+  data: PostReviewRequestBody,
+): Promise<void> => {
+  await authInstance.post('/api/v1/review/register', data);
+};
+
+// 리뷰 수정
+export const putEditReview = async (
+  reviewId: number,
+  workplaceName: string,
+  data: PutReviewRequestBody,
+): Promise<Review> => {
+  const response = await authInstance.put(
+    `/api/v1/review/update/${reviewId}?workplaceName=${workplaceName}`,
+    data,
+  );
+  return response.data;
+};
+
+// 리뷰 삭제
+export const deleteReview = async (
+  reviewId: number,
+  workplaceName: string,
+): Promise<void> => {
+  await authInstance.delete(
+    `/api/v1/review/${reviewId}?workplaceName=${workplaceName}`,
+  );
+};

리뷰: - 1-2 개별 에러 처리의 경우 아래와 같이 처리하면 됩니다.
```js
export const postReview = async (data: ReviewData): Promise<Review> => {
  try {
    const response = await authInstance.post('/api/v1/review', data);
    return response.data;
  } catch (error) {
    // 리뷰 작성 시 특별히 처리해야 하는 에러만 여기서 처리
    if (error.response?.status === 400) {
      throw new Error('리뷰 내용을 확인해주세요.');
    }
    // 나머지는 공통 에러 처리로 전파
    throw error;
  }
};
```
@@ -0,0 +1,36 @@
+interface InputProps {
+  label: string;
+  placeholder?: string;
+  defaultValue?: string | number;
+  value?: string;
+  onChangeFunction?: (e: React.ChangeEvent<HTMLInputElement>) => void;
+  maxLength?: number;
+  name?: string;
+}
+
+const CommonInput = ({
+  label,
+  placeholder,
+  defaultValue,
+  value,
+  onChangeFunction,
+  maxLength,
+  name,
+}: InputProps) => {
+  return (
+    <div className='mx-auto flex w-custom flex-col gap-1.5'>
+      <p className='w-[100%] text-sm font-normal'>{label}</p>
+      <input
+        className='main-input'
+        placeholder={placeholder}
+        defaultValue={defaultValue}
+        value={value}
+        onChange={onChangeFunction}
+        maxLength={maxLength}
+        name={name}
+      />
+    </div>
+  );
+};
+
+export default CommonInput;

리뷰: - 게선제안
```js
// src/components/common/Input/types.ts
export interface BaseInputProps extends React.InputHTMLAttributes<HTMLInputElement> {
  label: string;
}

// src/components/common/Input/Input.tsx
import { BaseInputProps } from './types';

const Input = ({ label, className, ...props }: BaseInputProps) => {
  return (
    <div className='input-wrapper'>
      <label className='input-label'>{label}</label>
      <input className={clsx('input-base', className)} {...props} />
    </div>
  );
};
```
@@ -0,0 +1,77 @@
+package store.util;
+
+import store.domain.Product;
+import store.domain.Promotion;
+
+import java.io.BufferedReader;
+import java.io.FileReader;
+import java.io.IOException;
+import java.time.LocalDateTime;
+import java.util.*;
+
+public class ProductLoader {
+
+    private static final String FILE_PATH = "./src/main/resources/products.md";
+
+    public static  List<Product> loadProducts() {
+        Map<String, Promotion> promotions = PromotionLoader.loadPromotion();
+        Map<String, List<String[]>> groupedLines = getGroups();
+        return getProducts(groupedLines, promotions);
+    }
+
+    private static List<Product> getProducts(Map<String, List<String[]>> groupedLines, Map<String, Promotion> promotions) {
+        List<Product> products = new ArrayList<>();
+
+        for (Map.Entry<String, List<String[]>> entry : groupedLines.entrySet()) {
+            String name = entry.getKey();
+            Product product = createProduct(name, entry.getValue(), promotions);
+            products.add(product);
+        }
+
+        return products;
+    }
+
+    private static Product createProduct(String name, List<String[]> valuesList, Map<String, Promotion> promotions) {
+        int price = 0;
+        int promotionQuantity = 0;
+        int commonQuantity = 0;
+        Promotion promotion = createDefaultPromotion();
+
+        for (String[] values : valuesList) {
+            price = Integer.parseInt(values[1]);
+            int quantity = Integer.parseInt(values[2]);
+            String promotionName = values[3];
+
+            if ("null".equals(promotionName)) {
+                commonQuantity += quantity;
+            } else {
+                promotionQuantity += quantity;
+                promotion = promotions.getOrDefault(promotionName, promotions.get(promotionName));
+            }
+        }
+
+        return new Product(name, price, promotionQuantity, commonQuantity, promotion);
+    }
+
+    private static Promotion createDefaultPromotion() {
+        return new Promotion("", Integer.MAX_VALUE, 0, LocalDateTime.MIN, LocalDateTime.MIN);
+    }
+
+    private static Map<String, List<String[]>> getGroups() {
+        Map<String, List<String[]>> groupedLines = new HashMap<>();
+
+        try (BufferedReader br = new BufferedReader(new FileReader(FILE_PATH))) {
+            br.readLine(); // Skip header
+            String line;
+            while ((line = br.readLine()) != null) {
+                String[] values = line.split(",");
+                String name = values[0];
+                groupedLines.computeIfAbsent(name, k -> new ArrayList<>()).add(values);
+            }
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+        return groupedLines;
+    }
+
+}

리뷰: util 클래스라면 기본 생성자 private으로 해서 인스턴스 생성을 막아도 좋을것 같아요.
@@ -0,0 +1,37 @@
+package store.util;
+
+import store.domain.Promotion;
+
+import java.io.BufferedReader;
+import java.io.FileReader;
+import java.io.IOException;
+import java.time.LocalDateTime;
+import java.util.HashMap;
+import java.util.Map;
+
+public class PromotionLoader {
+
+    private static final String FILE_PATH = "./src/main/resources/promotions.md";
+
+    public static Map<String, Promotion> loadPromotion() {
+        Map<String, Promotion> promotions = new HashMap<>();
+        try (BufferedReader br = new BufferedReader(new FileReader(FILE_PATH))) {
+            br.readLine(); // 첫번쨰 헤더 생략
+            String line;
+            while ((line = br.readLine()) != null) {
+                String[] values = line.split(",");
+                String name = values[0];
+                int buy = Integer.parseInt(values[1]);
+                int get = Integer.parseInt(values[2]);
+                LocalDateTime startDate = LocalDateTime.parse(values[3]+ "T00:00:00");
+                LocalDateTime endDate = LocalDateTime.parse(values[4] + "T00:00:00");
+                Promotion promotion = new Promotion(name, buy, get, startDate, endDate);
+                promotions.put(name, promotion);
+            }
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+        return promotions;
+    }
+
+}

리뷰: `ClassLoader.getResource()`로 가져와야 빌드된 아티팩트에서도 제대로 resource를 가져올 수 있습니다
@@ -0,0 +1,80 @@
+package store.controller;
+
+import store.controller.dto.OrderStateDTO;
+import store.controller.dto.StateContextDTO;
+import store.domain.Command;
+import store.service.OrderService;
+import store.service.ProductService;
+import store.view.InputView;
+import store.view.OutputView;
+import store.view.dto.RequestOrder;
+import store.view.dto.ResponseOrder;
+import store.view.dto.ResponseProducts;
+
+import java.util.function.Supplier;
+
+public class OrderController {
+    private final OrderService orderService;
+    private final ProductService productService;
+    private final InputView inputView;
+    private final OutputView outputView;
+
+    public OrderController(OrderService orderService, ProductService productService, InputView inputView, OutputView outputView) {
+        this.orderService = orderService;
+        this.productService = productService;
+        this.inputView = inputView;
+        this.outputView = outputView;
+    }
+
+    public void createOrder() {
+        Command retryCommand;
+        do {
+            printProductInfo();
+            OrderStateDTO orderStateDTO = getOrderStateDTO();
+            checkOrderState(orderStateDTO);
+            Command command = inputCommand(inputView::askMemberShipDiscount);
+            orderService.determinedMembership(command);
+            getReceipt();
+            retryCommand = inputCommand(inputView::askAdditionalPurchase);
+        } while (retryCommand.isYes());
+    }
+
+    private void checkOrderState(OrderStateDTO orderState) {
+        if(orderState.state().equals("PENDING")) {
+            for (StateContextDTO stateContext : orderState.stateContexts()) {
+                Command command = inputCommand(() -> inputView.askReviewPending(stateContext));
+                orderService.solvePending(command, stateContext.name());
+            }
+        }
+    }
+
+    private void printProductInfo() {
+        ResponseProducts products = productService.findAll();
+        outputView.printProducts(products);
+    }
+
+    private OrderStateDTO getOrderStateDTO() {
+        try {
+            RequestOrder requestOrder = inputView.askItem();
+            return orderService.createOrder(requestOrder);
+        } catch (IllegalArgumentException e) {
+            outputView.printError(e.getMessage());
+            return getOrderStateDTO();
+        }
+    }
+
+    private Command inputCommand(Supplier<String> supplier) {
+        try {
+            String input = supplier.get();
+            return Command.find(input);
+        } catch (IllegalArgumentException e) {
+            outputView.printError(e.getMessage());
+            return inputCommand(supplier);
+        }
+    }
+
+    public void getReceipt() {
+        ResponseOrder orderReceipt = orderService.getOrderReceipt();
+        outputView.printOrder(orderReceipt);
+    }
+}

리뷰: 고정되어있는 상태는 enum으로 표현하면 의도가 명확해질 수 있을 것 같아요.
@@ -0,0 +1,27 @@
+package store.util;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.stream.Stream;
+
+public class StringParser {
+
+    private StringParser() {
+    }
+
+    public static Map<String, Integer> parseToMap(String input) {
+        Map<String, Integer> result = new HashMap<>();
+
+        Stream.of(input.split(","))
+                .forEach(item -> {
+                    item = item.replaceAll("[\\[\\]]", "");
+                    String[] nameAndQuantity = item.split("-");
+                    if (nameAndQuantity.length != 2) {
+                        throw new IllegalArgumentException("상품명과 수량을 정확히 입력해 주세요.");
+                    }
+                    result.put(nameAndQuantity[0], Integer.valueOf(nameAndQuantity[1]));
+                });
+
+        return result;
+    }
+}
\ No newline at end of file

리뷰: key - value 쌍이 맞는지 검사하는 부분 또한 하나의 메서드로 분리할 수 있을 것 같습니다!
@@ -0,0 +1,70 @@
+package store.model.repository;
+
+import store.Message.ErrorMessage;
+import store.config.DataInitializer;
+import store.config.ProductsData;
+import store.model.domain.Product;
+import store.constant.ProductType;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+public class ProductRepository {
+    public static final String DEFAULT_PRODUCT_FILE_PATH = "src/main/resources/products.md";
+
+    private final Map<ProductType, Map<String, Product>> info = new LinkedHashMap<>();
+    private final Map<ProductType, Map<String, Integer>> stock = new HashMap<>();
+
+    public ProductRepository(String filePath) {
+        DataInitializer initializer = DataInitializer.getInstance();
+        try {
+            ProductsData productsData = initializer.loadProducts(filePath);
+            this.info.putAll(productsData.info());
+            this.stock.putAll(productsData.stock());
+        } catch (IOException e) {
+            throw new RuntimeException(ErrorMessage.FILE_ERROR.getMessage(filePath));
+        }
+    }
+
+    public ProductRepository() {
+        this(DEFAULT_PRODUCT_FILE_PATH);
+    }
+
+    public List<Product> findAllInfo() {
+        return info.values().stream()
+                .flatMap(innerMap -> innerMap.values().stream())
+                .toList();
+    }
+
+    public Product findInfo(String name, ProductType productType) {
+        Map<String, Product> typeInfo = info.get(productType);
+        return typeInfo.getOrDefault(name, null);
+    }
+
+    public int findStock(String name, ProductType productType) {
+        Map<String, Integer> typeStock = stock.get(productType);
+        return typeStock.getOrDefault(name, 0);
+    }
+
+    public boolean reduceStock(String name, ProductType productType, int quantity) {
+        Map<String, Integer> typeStocks = stock.get(productType);
+        checkStock(name, typeStocks);
+
+        int currentStock = typeStocks.get(name);
+        if (currentStock < quantity) {
+            return false;
+        }
+
+        typeStocks.put(name, currentStock - quantity);
+        return true;
+    }
+
+    private static void checkStock(String name, Map<String, Integer> stock) {
+        if (stock == null || !stock.containsKey(name)) {
+            throw new IllegalStateException(ErrorMessage.NON_EXISTENT_PRODUCT.getMessage());
+        }
+    }
+}
\ No newline at end of file

리뷰: info라는 네이밍에서 어떤 정보가 들어있는지 직관적으로 다가오지 않는 것 같습니다!
@@ -0,0 +1,70 @@
+package store.model.repository;
+
+import store.Message.ErrorMessage;
+import store.config.DataInitializer;
+import store.config.ProductsData;
+import store.model.domain.Product;
+import store.constant.ProductType;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+public class ProductRepository {
+    public static final String DEFAULT_PRODUCT_FILE_PATH = "src/main/resources/products.md";
+
+    private final Map<ProductType, Map<String, Product>> info = new LinkedHashMap<>();
+    private final Map<ProductType, Map<String, Integer>> stock = new HashMap<>();
+
+    public ProductRepository(String filePath) {
+        DataInitializer initializer = DataInitializer.getInstance();
+        try {
+            ProductsData productsData = initializer.loadProducts(filePath);
+            this.info.putAll(productsData.info());
+            this.stock.putAll(productsData.stock());
+        } catch (IOException e) {
+            throw new RuntimeException(ErrorMessage.FILE_ERROR.getMessage(filePath));
+        }
+    }
+
+    public ProductRepository() {
+        this(DEFAULT_PRODUCT_FILE_PATH);
+    }
+
+    public List<Product> findAllInfo() {
+        return info.values().stream()
+                .flatMap(innerMap -> innerMap.values().stream())
+                .toList();
+    }
+
+    public Product findInfo(String name, ProductType productType) {
+        Map<String, Product> typeInfo = info.get(productType);
+        return typeInfo.getOrDefault(name, null);
+    }
+
+    public int findStock(String name, ProductType productType) {
+        Map<String, Integer> typeStock = stock.get(productType);
+        return typeStock.getOrDefault(name, 0);
+    }
+
+    public boolean reduceStock(String name, ProductType productType, int quantity) {
+        Map<String, Integer> typeStocks = stock.get(productType);
+        checkStock(name, typeStocks);
+
+        int currentStock = typeStocks.get(name);
+        if (currentStock < quantity) {
+            return false;
+        }
+
+        typeStocks.put(name, currentStock - quantity);
+        return true;
+    }
+
+    private static void checkStock(String name, Map<String, Integer> stock) {
+        if (stock == null || !stock.containsKey(name)) {
+            throw new IllegalStateException(ErrorMessage.NON_EXISTENT_PRODUCT.getMessage());
+        }
+    }
+}
\ No newline at end of file

리뷰: 저도 이번에는 사용하지 않았지만 Repository를 구현하셔서 의견 하나 드리면 `Optional<Product>` 로 리턴하는 것도 좋으셨을 것 같습니다 👍
@@ -0,0 +1,27 @@
+package store.util;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.stream.Stream;
+
+public class StringParser {
+
+    private StringParser() {
+    }
+
+    public static Map<String, Integer> parseToMap(String input) {
+        Map<String, Integer> result = new HashMap<>();
+
+        Stream.of(input.split(","))
+                .forEach(item -> {
+                    item = item.replaceAll("[\\[\\]]", "");
+                    String[] nameAndQuantity = item.split("-");
+                    if (nameAndQuantity.length != 2) {
+                        throw new IllegalArgumentException("상품명과 수량을 정확히 입력해 주세요.");
+                    }
+                    result.put(nameAndQuantity[0], Integer.valueOf(nameAndQuantity[1]));
+                });
+
+        return result;
+    }
+}
\ No newline at end of file

리뷰: 제가 봐도 불편한 코드네요 ㅎㅎ. 감사합니다