ewhk9887/korean_code_reviews_from_github huggingface dataset:
@@ -2,7 +2,7 @@
 from dotenv import load_dotenv
 
 # load .env file
-load_dotenv()
+load_dotenv('app/backend/.env')
 
 from pydantic import (
     PostgresDsn,
@@ -73,6 +73,9 @@ class Settings(BaseSettings):
     # ALGORITHM : str
     # ACCESS_TOKEN_EXPIRE_MINUTES : int
 
+    # Groq API 
+    GROQ_API_KEY: str
+
     # 기본 이미지 URL
     # DEFAULT_IMAGE_URL : str
 

리뷰: ## app/backend/src/main/core/config.py 파일 리뷰

### 주요 기능
이 파일은 백엔드 애플리케이션의 설정을 관리하는 데 사용됩니다. 설정은 환경 변수에서 로드되며, 설정 항목은 `Settings` 클래스에 정의되어 있습니다.

### 좋은 점
* 이 파일은 설정을 관리하는 데 사용되는 `Settings` 클래스를 제공하며, 환경 변수에서 설정 값을 로드하는 기능을 제공합니다.
* 설정 항목은 명확하게 정의되어 있으며, 설정 항목의 타입이 명확하게 지정되어 있습니다.
* `parse_cors` 함수를 사용하여 CORS 설정 값을 파싱하는 기능을 제공합니다.
* `SQLALCHEMY_DATABASE_URI` 속성을 계산하여 데이터베이스 URI를 생성하는 기능을 제공합니다.

### 개선할 점
* 일부 설정 항목은 주석 처리되어 있습니다. 주석 처리된 설정 항목은 제거하거나 활성화해야 합니다.
* 설정 항목의 타입이 명확하게 지정되어 있지만, 일부 설정 항목의 설명이 부족합니다.
* 일부 설정 항목은 설정 항목의 설명이 없습니다.
* `Settings` 클래스는 `BaseSettings` 클래스를 상속하지만, `BaseSettings` 클래스의 속성과 메서드가 명확하게 사용되지 않습니다.
* `parse_cors` 함수는 CORS 설정 값을 파싱하는 기능을 제공하지만, CORS 설정 값이 유효하지 않은 경우 예외를 발생시키지 않습니다.

### 제안 사항
* 주석 처리된 설정 항목을 제거하거나 활성화하세요.
* 설정 항목의 설명을 추가하세요.
* `Settings` 클래스는 `BaseSettings` 클래스의 속성과 메서드를 명확하게 사용하세요.
* `parse_cors` 함수는 CORS 설정 값이 유효하지 않은 경우 예외를 발생시키도록 수정하세요.

```python
# 주석 처리된 설정 항목을 제거하거나 활성화하세요.
# BACKEND_CORS_ORIGINS: Annotated[
#     list[AnyUrl] | str, BeforeValidator(parse_cors)
# ]

# 설정 항목의 설명을 추가하세요.
PROJECT_NAME : str  # 프로젝트 이름
POSTGRES_USER: str  # PostgreSQL 사용자 이름
POSTGRES_PASSWORD: str  # PostgreSQL 비밀번호
POSTGRES_SERVER: str  # PostgreSQL 서버 주소
POSTGRES_PORT: int  # PostgreSQL 서버 포트
POSTGRES_DB: str  # PostgreSQL 데이터베이스 이름

# `parse_cors` 함수는 CORS 설정 값이 유효하지 않은 경우 예외를 발생시키도록 수정하세요.
def parse_cors(v: Any) -> list[str] | str:
    if isinstance(v, str) and not v.startswith("["):
        return [i.strip() for i in v.split(",")]
    elif isinstance(v, list | str):
        return v
    raise ValueError("Invalid CORS value")

# `Settings` 클래스는 `BaseSettings` 클래스의 속성과 메서드를 명확하게 사용하세요.
class Settings(BaseSettings):
    model_config = SettingsConfigDict(
        env_file=".env", env_ignore_empty=True, extra="ignore"
    )

    # ...
```

이러한 수정을 통해 `config.py` 파일은 더 명확하게 관리할 수 있으며, 설정 항목의 설명이 추가되어 개발자가 설정 항목을 이해하기更加容易합니다. 또한, `parse_cors` 함수는 CORS 설정 값을 유효하게 파싱할 수 있으며, 예외를 발생시키도록 수정되어 개발자가 오류를 쉽게 확인할 수 있습니다.
@@ -1,8 +1,12 @@
 package homeTry.chatting.interceptor;
 
+import homeTry.chatting.exception.badRequestException.InactivatedMemberWithValidTokenException;
 import homeTry.chatting.exception.badRequestException.InvalidChattingTokenException;
-import homeTry.common.auth.JwtAuth;
+import homeTry.chatting.exception.badRequestException.NoSuchMemberInDbWithValidTokenException;
+import homeTry.common.auth.jwt.JwtAuth;
 import homeTry.member.dto.MemberDTO;
+import homeTry.member.exception.badRequestException.InactivatedMemberException;
+import homeTry.member.exception.badRequestException.MemberNotFoundException;
 import homeTry.member.service.MemberService;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.messaging.Message;
@@ -14,6 +18,7 @@
 
 @Component
 public class StompInterceptor implements ChannelInterceptor {
+
     private final JwtAuth jwtAuth;
     private final MemberService memberService;
 
@@ -29,23 +34,27 @@ public Message<?> preSend(Message<?> message, MessageChannel channel) {
         StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message);
 
         handleConnectCommand(accessor);
-        //todo: 추후 다른 케이스 추가하기
+        //추후 다른 케이스 있다면 추가하기
 
         return message;
     }
 
     private void handleConnectCommand(StompHeaderAccessor accessor) {
         if (accessor.getCommand() == StompCommand.CONNECT) {
-            String token = String.valueOf(accessor.getNativeHeader("Authorization").getFirst());
-            if (token == null || !token.startsWith("Bearer "))
+            MemberDTO memberDTO;
+
+            try {
+                String token = String.valueOf(accessor.getNativeHeader("Authorization").getFirst())
+                        .substring(7); // Expect after B e a r e r _
+
+                memberDTO = memberService.getMember(jwtAuth.extractId(token));
+            } catch (MemberNotFoundException e) {
+                throw new NoSuchMemberInDbWithValidTokenException();
+            } catch (InactivatedMemberException e) {
+                throw new InactivatedMemberWithValidTokenException();
+            } catch (Exception e) {
                 throw new InvalidChattingTokenException();
-
-            token = token.substring(7);
-
-            if (!jwtAuth.validateToken(token))
-                throw new InvalidChattingTokenException();
-
-            MemberDTO memberDTO = memberService.getMember(jwtAuth.extractId(token));
+            }
 
             //세션에 저장
             accessor.getSessionAttributes().put("member", memberDTO);

리뷰: StompInterceptor는 토큰 정보를 보고, 멤버가 존재하지 않으면,, Exception을 뱉도록 하는 것 같습니다.
그렇다면, stompInterceptor는 토큰이 bearerToken인지, 다른 토큰의 종류인지? 에 대해서는 아무런 관심사가 없습니다. BearerToken이 유효한지? 도 관심사가 없습니다. 단순히 현재 interceptor에 넘어온 토큰 정보를 보고, 우리 사용자라면 세션에 그 값을 넣는 것에 관심이 있습니다.
이렇게 각 클래스의 관심사들을 생각하며 적절하게 분리하고, 캡슐화 해보시면 재미있을 것 같아요 😄
@@ -8,51 +8,65 @@
 public class Game {
 
     public static final String DELIMETER = "-";
+
+    public static final String CAR_NAME_DELIMETER = ",";
+
     private int gameCount;
+  
     private Car[] cars;
 
     public void start() {
-        cars =  createCar();
+
+        cars = createCar();
+
         gameCount = getGameCount();
         System.out.println("실행 결과");
         for (int i = 0; i < gameCount; i++) {
             job();
         }
 
+        Winner winner = new Winner();
+        System.out.println(FormattingUtil.formattingResult(winner.getWinners(cars)) + "가 최종 우승했습니다.");
+
     }
 
     private void job() {
         for (Car car : this.cars) {
             car.drive(car.getNumber(), DELIMETER);
-            System.out.println(car.getStatus());
+            System.out.println(car.getName() + ":" + car.getStatus());
         }
         System.out.println("                           ");
     }
 
     private int getGameCount() {
         Scanner scanner = new Scanner(System.in);
         System.out.println("시도할 회수는 몇 회 인가요?");
+      
         int gameCount = scanner.nextInt();
-        System.out.println("gameCount = " + gameCount);
+        System.out.println(gameCount);
+      
         return gameCount;
     }
 
 
     private Car[] createCar() {
-        int carCount = getCarCount();
-        Car[] cars = new Car[carCount];
-        for (int i = 0; i < carCount; i++) {
-            cars[i] = new SmallCar();
+        String[] carNameInputArr = getCarNameInputArr();
+        int length = carNameInputArr.length;
+        Car[] cars = new Car[length];
+        for (int i = 0; i < length; i++) {
+            cars[i] = new SmallCar(carNameInputArr[i]);
+
         }
         return cars;
     }
 
-    private int getCarCount() {
-        System.out.println("자동차 대수는 몇 대 인가요?");
+
+    private String[] getCarNameInputArr() {
+        System.out.println("경주할 자동차 이름을 입력하세요(이름은 " + CAR_NAME_DELIMETER + "를 기준으로 구분).");
         Scanner scanner = new Scanner(System.in);
-        int carCount = Integer.parseInt(scanner.nextLine());
-        System.out.println("자동차 대수 = " + carCount);
-        return carCount;
+        String carNameInput = scanner.nextLine();
+        System.out.println(carNameInput);
+        return carNameInput.split(CAR_NAME_DELIMETER);
     }
 
     public Car[] getCars() {

리뷰: ![image](https://github.com/next-step/java-racingcar/assets/27044096/d221b9bf-2cb7-4dcb-bafa-016f49e445a2)

d, e 가 우승했는데 제대로 출력되고있지 않네요 😅
@@ -0,0 +1,25 @@
+import java.util.List;
+
+/**
+ * @author jeongheekim
+ * @date 3/27/24
+ */
+public class FormattingUtil {
+    private FormattingUtil() {}
+
+    public static String formattingResult(List<String> list) {
+        StringBuilder sb = new StringBuilder();
+        int size = list.size();
+        for (int i = 0; i < size; i++) {
+            sb.append(list.get(i));
+            addComma(i, size, sb);
+        }
+        return sb.toString();
+    }
+
+    private static void addComma(int i, int size, StringBuilder sb) {
+        if (i < size - 1) {
+            sb.append(",");
+        }
+    }
+}

리뷰: > 규칙 1: 한 메서드에 오직 한 단계의 들여쓰기(indent)만 한다.
indent가 2이상인 메소드의 경우 메소드를 분리하면 indent를 줄일 수 있다.
else를 사용하지 않아 indent를 줄일 수 있다.

인덴트가 2이상이군요 🤔
@@ -0,0 +1,47 @@
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @author jeongheekim
+ * @date 3/26/24
+ */
+public class Winner {
+    private List<String> winners = new ArrayList<>();
+    private int maxLength;
+
+    public void addWinner(String name) {
+        this.winners.add(name);
+    }
+
+    public void updateMaxLength(int length) {
+        this.maxLength = length;
+    }
+
+    public List<String> getWinners(Car[] cars) {
+        this.filterMaxLength(cars);
+        for (Car car : cars) {
+            this.checkWinnerCondition(car);
+        }
+        return this.winners;
+    }
+
+    private void filterMaxLength (Car[] cars) {
+        for (Car car : cars) {
+            int carStatusLength = car.getStatus().length();
+            this.compareMaxLength(carStatusLength);
+        }
+    }
+
+    private void compareMaxLength(int carStatusLength) {
+        if (this.maxLength <= carStatusLength) {
+            this.updateMaxLength(carStatusLength);
+        }
+    }
+
+    private void checkWinnerCondition(Car car) {
+        int statusLength = car.getStatus().length();
+        if (this.maxLength <= statusLength) {
+            this.addWinner(car.getName());
+        }
+    }
+}

리뷰: 로직에 문제가 있는것 같아요 😅
우승자가 두번 출력되고 있습니다

![image](https://github.com/next-step/java-racingcar/assets/27044096/b9f03caf-3eb4-4243-b500-48b046458145)
@@ -0,0 +1,47 @@
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @author jeongheekim
+ * @date 3/26/24
+ */
+public class Winner {
+    private List<String> winners = new ArrayList<>();
+    private int maxLength;
+
+    public void addWinner(String name) {
+        this.winners.add(name);
+    }
+
+    public void updateMaxLength(int length) {
+        this.maxLength = length;
+    }
+
+    public List<String> getWinners(Car[] cars) {
+        this.filterMaxLength(cars);
+        for (Car car : cars) {
+            this.checkWinnerCondition(car);
+        }
+        return this.winners;
+    }
+
+    private void filterMaxLength (Car[] cars) {
+        for (Car car : cars) {
+            int carStatusLength = car.getStatus().length();
+            this.compareMaxLength(carStatusLength);
+        }
+    }
+
+    private void compareMaxLength(int carStatusLength) {
+        if (this.maxLength <= carStatusLength) {
+            this.updateMaxLength(carStatusLength);
+        }
+    }
+
+    private void checkWinnerCondition(Car car) {
+        int statusLength = car.getStatus().length();
+        if (this.maxLength <= statusLength) {
+            this.addWinner(car.getName());
+        }
+    }
+}

리뷰: 자동차의 길이를 꺼내어 처리하고 있네요!
디미터 법칙에 어긋나고 있습니다 
메세지를 보내어 처리해보는건 어떨까요? 🤔

https://mangkyu.tistory.com/147
@@ -1,7 +1,20 @@
 package lotto;
 
+import lotto.config.Configuration;
+import lotto.controller.BuyLottoController;
+import lotto.controller.LottoController;
+import lotto.dto.BuyLottoDto;
+
 public class Application {
     public static void main(String[] args) {
-        // TODO: 프로그램 구현
+        Configuration configuration = new Configuration();
+        LottoController lottoController = configuration.getLottoController();
+        BuyLottoController buyLottoController = configuration.getBuyLottoController();
+        executeControllers(buyLottoController, lottoController);
+    }
+
+    private static void executeControllers(BuyLottoController buyLottoController, LottoController lottoController) {
+        BuyLottoDto buyLottoDto = buyLottoController.buyLotto();
+        lottoController.getStatistics(buyLottoDto);
     }
 }

리뷰: 현재 작성하신 controller와 service를 모두 application클래스에서 생성하도록 구현하신것같은데 

우아한테크코스 3주차 공통 피드백을 보시면, 

> 함수(메서드) 라인에 대한 기준 프로그래밍 요구사항을 보면 함수 15라인으로 제한하는 요구사항이 있다. 이 기준은 main() 함수에도 해당된다. 공백 라인도 한 라인에 해당한다. 15라인이 넘어간다면 함수 분리를 위한 고민을 한다.

라고 제시되어있어요. 만약 이후 controller와 service, repository까지 많아지게 되는 프로그램이라면 위 요구사항을 어기게 될것같은데 어떻게 생각하실까요??
@@ -0,0 +1,34 @@
+package lotto.model;
+
+import static lotto.constant.Constant.THOUSAND;
+import static lotto.constant.Constant.ZERO;
+import static lotto.exception.ErrorInputException.ErrorMessage.PURCHASE_PRICE_CAN_DIVIDE_BY_THOUSAND;
+
+import lotto.exception.ErrorInputException;
+
+public class PurchasePrice {
+    private final int price;
+
+    private PurchasePrice(int price) {
+        this.price = price;
+        isDividedByThousand();
+    }
+
+    public static PurchasePrice createPurchasePrice(int price) {
+        return new PurchasePrice(price);
+    }
+
+    public int getPrice() {
+        return price;
+    }
+
+    private void isDividedByThousand() {
+        if (price % THOUSAND != ZERO) {
+            throw new ErrorInputException(PURCHASE_PRICE_CAN_DIVIDE_BY_THOUSAND);
+        }
+    }
+
+    public int calculateLottoCount() {
+        return price / THOUSAND;
+    }
+}

리뷰: calculateLottoCount를 호출할때마다 구입가격에서 1000을 나눌텐데 이렇게 하신 이유가 궁금합니다!!
@@ -0,0 +1,57 @@
+package lotto.model;
+
+import static lotto.constant.Constant.ZERO;
+
+import java.util.Collections;
+import java.util.EnumMap;
+import java.util.List;
+import java.util.Map;
+import lotto.constant.Rank;
+
+public class Statistics {
+    private final Map<Rank, Integer> result;
+
+    private Statistics() {
+        result = new EnumMap<>(Rank.class);
+        for (Rank rank : Rank.values()) {
+            result.put(rank, ZERO);
+        }
+    }
+
+    public static Statistics createStatistics() {
+        return new Statistics();
+    }
+
+    public Map<Rank, Integer> getResult() {
+        return Collections.unmodifiableMap(result);
+    }
+
+    public void calculateMatching(LottoNumbers lottoNumbers, WinnerNumber winnerNumber, BonusNumber bonusNumber) {
+        List<Lotto> lottos = lottoNumbers.getNumbers();
+
+        for (Lotto lotto : lottos) {
+            Rank rank = findRank(lotto, winnerNumber, bonusNumber);
+            int lottoCount = result.get(rank) + 1;
+
+            result.put(rank, lottoCount);
+        }
+    }
+
+    private Rank findRank(Lotto lotto, WinnerNumber winnerNumber, BonusNumber bonusNumber) {
+        long winnerMatch = countMatchingWinnerNumber(lotto, winnerNumber);
+        boolean bonusMatch = matchingBonusNumber(lotto, bonusNumber);
+
+        return Rank.findRank(winnerMatch, bonusMatch);
+    }
+
+    private boolean matchingBonusNumber(Lotto lotto, BonusNumber bonusNumber) {
+        return lotto.getNumbers().stream()
+                .anyMatch(number -> number.equals(bonusNumber.isSameBonusNumber(bonusNumber)));
+    }
+
+    private long countMatchingWinnerNumber(Lotto lotto, WinnerNumber winnerNumber) {
+        return lotto.getNumbers().stream()
+                .filter(winnerNumber.getWinnerNumbers()::contains)
+                .count();
+    }
+}

리뷰: int로 선언하셔도 됐을텐데 long으로 선언하신 이유가 궁금합니다!
@@ -0,0 +1,40 @@
+package lotto.dto;
+
+import lotto.model.BonusNumber;
+import lotto.model.LottoNumbers;
+import lotto.model.PurchasePrice;
+import lotto.model.Statistics;
+import lotto.model.WinnerNumber;
+
+public class BuyLottoDto {
+    private PurchasePrice purchasePrice;
+    private LottoNumbers lottoNumbers;
+    private WinnerNumber winnerNumber;
+    private BonusNumber bonusNumber;
+
+    private BuyLottoDto(PurchasePrice purchasePrice, LottoNumbers lottoNumbers, WinnerNumber winnerNumber,
+                        BonusNumber bonusNumber) {
+        this.purchasePrice = purchasePrice;
+        this.lottoNumbers = lottoNumbers;
+        this.winnerNumber = winnerNumber;
+        this.bonusNumber = bonusNumber;
+    }
+
+    public static BuyLottoDto createBuyLottoDto(PurchasePrice purchasePrice,
+                                                LottoNumbers lottoNumbers,
+                                                WinnerNumber winnerNumber,
+                                                BonusNumber bonusNumber) {
+        return new BuyLottoDto(purchasePrice, lottoNumbers, winnerNumber, bonusNumber);
+    }
+
+    public PurchasePrice getPurchasePrice() {
+        return purchasePrice;
+    }
+
+    public Statistics calculateMatching() {
+        Statistics statistics = Statistics.createStatistics();
+        statistics.calculateMatching(this.lottoNumbers, this.winnerNumber, this.bonusNumber);
+        return statistics;
+    }
+
+}

리뷰: 현재 calculateMatching메소드는 statics도메인에 접근하기 위한 메소드로 보여집니다! service계층이 있는 이유는, 프레젠테이션 계층과 데이터 엑세스 계층 사이를 연결하기 위함이죠!! 따라서 이 부분은 service쪽에서 구현되어야한다고 생각해요!
@@ -0,0 +1,57 @@
+package lotto.model;
+
+import static lotto.constant.Constant.ZERO;
+
+import java.util.Collections;
+import java.util.EnumMap;
+import java.util.List;
+import java.util.Map;
+import lotto.constant.Rank;
+
+public class Statistics {
+    private final Map<Rank, Integer> result;
+
+    private Statistics() {
+        result = new EnumMap<>(Rank.class);
+        for (Rank rank : Rank.values()) {
+            result.put(rank, ZERO);
+        }
+    }
+
+    public static Statistics createStatistics() {
+        return new Statistics();
+    }
+
+    public Map<Rank, Integer> getResult() {
+        return Collections.unmodifiableMap(result);
+    }
+
+    public void calculateMatching(LottoNumbers lottoNumbers, WinnerNumber winnerNumber, BonusNumber bonusNumber) {
+        List<Lotto> lottos = lottoNumbers.getNumbers();
+
+        for (Lotto lotto : lottos) {
+            Rank rank = findRank(lotto, winnerNumber, bonusNumber);
+            int lottoCount = result.get(rank) + 1;
+
+            result.put(rank, lottoCount);
+        }
+    }
+
+    private Rank findRank(Lotto lotto, WinnerNumber winnerNumber, BonusNumber bonusNumber) {
+        long winnerMatch = countMatchingWinnerNumber(lotto, winnerNumber);
+        boolean bonusMatch = matchingBonusNumber(lotto, bonusNumber);
+
+        return Rank.findRank(winnerMatch, bonusMatch);
+    }
+
+    private boolean matchingBonusNumber(Lotto lotto, BonusNumber bonusNumber) {
+        return lotto.getNumbers().stream()
+                .anyMatch(number -> number.equals(bonusNumber.isSameBonusNumber(bonusNumber)));
+    }
+
+    private long countMatchingWinnerNumber(Lotto lotto, WinnerNumber winnerNumber) {
+        return lotto.getNumbers().stream()
+                .filter(winnerNumber.getWinnerNumbers()::contains)
+                .count();
+    }
+}

리뷰: 재현님 덕분에 EnumMap에 대해서 처음으로 배웠네요!! ㅎㅎEnumMap이 해시충돌을 안일으킨다는데 덕분에 배워갑니다!!
@@ -1,7 +1,20 @@
 package lotto;
 
+import lotto.config.Configuration;
+import lotto.controller.BuyLottoController;
+import lotto.controller.LottoController;
+import lotto.dto.BuyLottoDto;
+
 public class Application {
     public static void main(String[] args) {
-        // TODO: 프로그램 구현
+        Configuration configuration = new Configuration();
+        LottoController lottoController = configuration.getLottoController();
+        BuyLottoController buyLottoController = configuration.getBuyLottoController();
+        executeControllers(buyLottoController, lottoController);
+    }
+
+    private static void executeControllers(BuyLottoController buyLottoController, LottoController lottoController) {
+        BuyLottoDto buyLottoDto = buyLottoController.buyLotto();
+        lottoController.getStatistics(buyLottoDto);
     }
 }

리뷰: 음.. 맞아요 저도 이 부분이 많이 걸렸어요. 하지만 모듈화 하면 할 수록 Application에서 생성해줘야 하는게 많아져서 고민이 됐어요...
다음에는 controller만 의존하게끔 수정해봐야겠어요!!
@@ -0,0 +1,34 @@
+package lotto.model;
+
+import static lotto.constant.Constant.THOUSAND;
+import static lotto.constant.Constant.ZERO;
+import static lotto.exception.ErrorInputException.ErrorMessage.PURCHASE_PRICE_CAN_DIVIDE_BY_THOUSAND;
+
+import lotto.exception.ErrorInputException;
+
+public class PurchasePrice {
+    private final int price;
+
+    private PurchasePrice(int price) {
+        this.price = price;
+        isDividedByThousand();
+    }
+
+    public static PurchasePrice createPurchasePrice(int price) {
+        return new PurchasePrice(price);
+    }
+
+    public int getPrice() {
+        return price;
+    }
+
+    private void isDividedByThousand() {
+        if (price % THOUSAND != ZERO) {
+            throw new ErrorInputException(PURCHASE_PRICE_CAN_DIVIDE_BY_THOUSAND);
+        }
+    }
+
+    public int calculateLottoCount() {
+        return price / THOUSAND;
+    }
+}

리뷰: 로또의 개수를 구하는 함수(calculateLottoCount)로 만들었습니다! 
듣고보니 만약 로또 가격이 바뀌게 된다면 나누는 가격을 수정해야 하니 `THOSAND` 대신 `LOTTOPRICE`로 바뀌는게 좋겠군요!
@@ -0,0 +1,57 @@
+package lotto.model;
+
+import static lotto.constant.Constant.ZERO;
+
+import java.util.Collections;
+import java.util.EnumMap;
+import java.util.List;
+import java.util.Map;
+import lotto.constant.Rank;
+
+public class Statistics {
+    private final Map<Rank, Integer> result;
+
+    private Statistics() {
+        result = new EnumMap<>(Rank.class);
+        for (Rank rank : Rank.values()) {
+            result.put(rank, ZERO);
+        }
+    }
+
+    public static Statistics createStatistics() {
+        return new Statistics();
+    }
+
+    public Map<Rank, Integer> getResult() {
+        return Collections.unmodifiableMap(result);
+    }
+
+    public void calculateMatching(LottoNumbers lottoNumbers, WinnerNumber winnerNumber, BonusNumber bonusNumber) {
+        List<Lotto> lottos = lottoNumbers.getNumbers();
+
+        for (Lotto lotto : lottos) {
+            Rank rank = findRank(lotto, winnerNumber, bonusNumber);
+            int lottoCount = result.get(rank) + 1;
+
+            result.put(rank, lottoCount);
+        }
+    }
+
+    private Rank findRank(Lotto lotto, WinnerNumber winnerNumber, BonusNumber bonusNumber) {
+        long winnerMatch = countMatchingWinnerNumber(lotto, winnerNumber);
+        boolean bonusMatch = matchingBonusNumber(lotto, bonusNumber);
+
+        return Rank.findRank(winnerMatch, bonusMatch);
+    }
+
+    private boolean matchingBonusNumber(Lotto lotto, BonusNumber bonusNumber) {
+        return lotto.getNumbers().stream()
+                .anyMatch(number -> number.equals(bonusNumber.isSameBonusNumber(bonusNumber)));
+    }
+
+    private long countMatchingWinnerNumber(Lotto lotto, WinnerNumber winnerNumber) {
+        return lotto.getNumbers().stream()
+                .filter(winnerNumber.getWinnerNumbers()::contains)
+                .count();
+    }
+}

리뷰: `countMatchingWinnerNumber()` 함수에서 stream을 쓰다보니 return 값이 long이 되었어요!
long이든 int든 크게 영향을 주지 않을 것이라고 생각하여 long으로 선언했습니다!
@@ -0,0 +1,40 @@
+package lotto.dto;
+
+import lotto.model.BonusNumber;
+import lotto.model.LottoNumbers;
+import lotto.model.PurchasePrice;
+import lotto.model.Statistics;
+import lotto.model.WinnerNumber;
+
+public class BuyLottoDto {
+    private PurchasePrice purchasePrice;
+    private LottoNumbers lottoNumbers;
+    private WinnerNumber winnerNumber;
+    private BonusNumber bonusNumber;
+
+    private BuyLottoDto(PurchasePrice purchasePrice, LottoNumbers lottoNumbers, WinnerNumber winnerNumber,
+                        BonusNumber bonusNumber) {
+        this.purchasePrice = purchasePrice;
+        this.lottoNumbers = lottoNumbers;
+        this.winnerNumber = winnerNumber;
+        this.bonusNumber = bonusNumber;
+    }
+
+    public static BuyLottoDto createBuyLottoDto(PurchasePrice purchasePrice,
+                                                LottoNumbers lottoNumbers,
+                                                WinnerNumber winnerNumber,
+                                                BonusNumber bonusNumber) {
+        return new BuyLottoDto(purchasePrice, lottoNumbers, winnerNumber, bonusNumber);
+    }
+
+    public PurchasePrice getPurchasePrice() {
+        return purchasePrice;
+    }
+
+    public Statistics calculateMatching() {
+        Statistics statistics = Statistics.createStatistics();
+        statistics.calculateMatching(this.lottoNumbers, this.winnerNumber, this.bonusNumber);
+        return statistics;
+    }
+
+}

리뷰: 맞아요! 이 로직은 service에 배치하는게 더 적절할 것 같군요! 👍