ewhk9887/korean_code_reviews_from_github huggingface dataset:
@@ -0,0 +1,74 @@
+package christmas.domain.menu;
+
+import christmas.domain.menu.items.Appetizer;
+import christmas.domain.menu.items.Beverage;
+import christmas.domain.menu.items.Dessert;
+import christmas.domain.menu.items.MainDish;
+import christmas.domain.menu.items.MenuItem;
+import java.util.Arrays;
+import java.util.List;
+
+public class Menu {
+    private static final List<MenuItem> appetizers = Arrays.asList(Appetizer.values());
+    private static final List<MenuItem> mainCourses = Arrays.asList(MainDish.values());
+    private static final List<MenuItem> desserts = Arrays.asList(Dessert.values());
+    private static final List<MenuItem> beverages = Arrays.asList(Beverage.values());
+
+    private Menu() {
+    }
+
+    public static String getMenuCategory(String menu) {
+        if (isMenuInCategory(menu, appetizers)) {
+            return "Appetizer";
+        }
+        if (isMenuInCategory(menu, mainCourses)) {
+            return "MainDish";
+        }
+        if (isMenuInCategory(menu, desserts)) {
+            return "Dessert";
+        }
+        if (isMenuInCategory(menu, beverages)) {
+            return "Beverage";
+        }
+        return "None";
+    }
+
+    private static boolean isMenuInCategory(String menu, Iterable<? extends MenuItem> category) {
+        for (MenuItem item : category) {
+            if (item.getName().equals(menu)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    public static int getMenuPrice(String menuName, String menuType) {
+        List<MenuItem> category;
+        switch (menuType) {
+            case "Appetizer":
+                category = appetizers;
+                break;
+            case "MainDish":
+                category = mainCourses;
+                break;
+            case "Dessert":
+                category = desserts;
+                break;
+            case "Beverage":
+                category = beverages;
+                break;
+            default:
+                return -1;
+        }
+        return getPrice(menuName, category);
+    }
+
+    private static int getPrice(String menuName, List<MenuItem> category) {
+        for (MenuItem item : category) {
+            if (item.getName().equals(menuName)) {
+                return item.getPrice();
+            }
+        }
+        return -1;
+    }
+}

리뷰: 제가 왜 그랬을까요... 😨 쓴지도 몰랐네요ㅠ
코드를 구현할 때 더 주의 깊게 확인해야겠습니다! 감사합니다.
@@ -1,4 +1,4 @@
-<!DOCTYPE html>
+<!doctype html>
 <html lang="en">
   <head>
     <meta charset="UTF-8" />
@@ -7,6 +7,7 @@
     <title>react-shopping-products</title>
   </head>
   <body>
+    <div id="toast"></div>
     <div id="root"></div>
     <script type="module" src="/src/main.tsx"></script>
   </body>

리뷰: 요것도 fileChanged에 올라와서 신기해서 남겨봄 ㅋㅋ
@@ -0,0 +1,96 @@
+import objectToQueryString, { ObjectQueryParams } from '@/utils/objectToQueryString';
+import { generateBasicToken } from '../utils/auth';
+
+type Method = 'GET' | 'POST' | 'PATCH' | 'DELETE';
+
+type Body = ReadableStream | XMLHttpRequestBodyInit;
+type HeadersType = [string, string][] | Record<string, string> | Headers;
+
+const USER_ID = process.env.VITE_API_USER_ID || 'id';
+const USER_PASSWORD = process.env.VITE_API_USER_PASSWORD || 'password';
+
+type RequestProps = {
+  baseUrl: string;
+  endpoint: string;
+  headers?: HeadersType;
+  body?: Body | object | null;
+  queryParams?: ObjectQueryParams;
+};
+
+type FetcherProps = RequestProps & {
+  method: Method;
+};
+
+type Options = {
+  method: Method;
+  headers: HeadersType;
+  body?: Body | null;
+};
+
+export const requestGet = async <T>({
+  baseUrl,
+  endpoint,
+  headers = {},
+  queryParams,
+}: RequestProps): Promise<T> => {
+  const response = await fetcher({ method: 'GET', baseUrl, endpoint, headers, queryParams });
+  const data: T = await response.json();
+
+  return data;
+};
+
+export const requestPatch = ({
+  baseUrl,
+  endpoint,
+  headers = {},
+  body,
+  queryParams,
+}: RequestProps) => {
+  return fetcher({ method: 'PATCH', baseUrl, endpoint, headers, body, queryParams });
+};
+
+export const requestPost = ({
+  baseUrl,
+  endpoint,
+  headers = {},
+  body,
+  queryParams,
+}: RequestProps) => {
+  return fetcher({ method: 'POST', baseUrl, endpoint, headers, body, queryParams });
+};
+
+export const requestDelete = ({ baseUrl, endpoint, headers = {}, queryParams }: RequestProps) => {
+  return fetcher({ method: 'DELETE', baseUrl, endpoint, headers, queryParams });
+};
+
+const fetcher = ({ method, baseUrl, endpoint, headers, body, queryParams }: FetcherProps) => {
+  const token = generateBasicToken(USER_ID, USER_PASSWORD);
+  const options = {
+    method,
+    headers: {
+      'Content-Type': 'application/json',
+      Authorization: token,
+      ...headers,
+    },
+    body: body ? JSON.stringify(body) : null,
+  };
+
+  let url = `${baseUrl}${endpoint}`;
+
+  if (queryParams) url += `?${objectToQueryString(queryParams)}`;
+
+  return errorHandler(url, options, endpoint);
+};
+
+const errorHandler = async (url: string, options: Options, endpoint: string) => {
+  try {
+    const response = await fetch(url, options);
+
+    if (!response.ok) throw new Error('오류가 발생했습니다.');
+
+    return response;
+  } catch (error) {
+    console.error(`fail to fetch ${endpoint}\n error message: ${error}`);
+    throw new Error('데이터를 가져오는 중 오류가 발생했습니다.');
+  }
+};

리뷰: 이 파일에서 각 request를 추상화해준게 아주 인상깊었습니다!!!
배워감 총총총,,
@@ -0,0 +1,96 @@
+import objectToQueryString, { ObjectQueryParams } from '@/utils/objectToQueryString';
+import { generateBasicToken } from '../utils/auth';
+
+type Method = 'GET' | 'POST' | 'PATCH' | 'DELETE';
+
+type Body = ReadableStream | XMLHttpRequestBodyInit;
+type HeadersType = [string, string][] | Record<string, string> | Headers;
+
+const USER_ID = process.env.VITE_API_USER_ID || 'id';
+const USER_PASSWORD = process.env.VITE_API_USER_PASSWORD || 'password';
+
+type RequestProps = {
+  baseUrl: string;
+  endpoint: string;
+  headers?: HeadersType;
+  body?: Body | object | null;
+  queryParams?: ObjectQueryParams;
+};
+
+type FetcherProps = RequestProps & {
+  method: Method;
+};
+
+type Options = {
+  method: Method;
+  headers: HeadersType;
+  body?: Body | null;
+};
+
+export const requestGet = async <T>({
+  baseUrl,
+  endpoint,
+  headers = {},
+  queryParams,
+}: RequestProps): Promise<T> => {
+  const response = await fetcher({ method: 'GET', baseUrl, endpoint, headers, queryParams });
+  const data: T = await response.json();
+
+  return data;
+};
+
+export const requestPatch = ({
+  baseUrl,
+  endpoint,
+  headers = {},
+  body,
+  queryParams,
+}: RequestProps) => {
+  return fetcher({ method: 'PATCH', baseUrl, endpoint, headers, body, queryParams });
+};
+
+export const requestPost = ({
+  baseUrl,
+  endpoint,
+  headers = {},
+  body,
+  queryParams,
+}: RequestProps) => {
+  return fetcher({ method: 'POST', baseUrl, endpoint, headers, body, queryParams });
+};
+
+export const requestDelete = ({ baseUrl, endpoint, headers = {}, queryParams }: RequestProps) => {
+  return fetcher({ method: 'DELETE', baseUrl, endpoint, headers, queryParams });
+};
+
+const fetcher = ({ method, baseUrl, endpoint, headers, body, queryParams }: FetcherProps) => {
+  const token = generateBasicToken(USER_ID, USER_PASSWORD);
+  const options = {
+    method,
+    headers: {
+      'Content-Type': 'application/json',
+      Authorization: token,
+      ...headers,
+    },
+    body: body ? JSON.stringify(body) : null,
+  };
+
+  let url = `${baseUrl}${endpoint}`;
+
+  if (queryParams) url += `?${objectToQueryString(queryParams)}`;
+
+  return errorHandler(url, options, endpoint);
+};
+
+const errorHandler = async (url: string, options: Options, endpoint: string) => {
+  try {
+    const response = await fetch(url, options);
+
+    if (!response.ok) throw new Error('오류가 발생했습니다.');
+
+    return response;
+  } catch (error) {
+    console.error(`fail to fetch ${endpoint}\n error message: ${error}`);
+    throw new Error('데이터를 가져오는 중 오류가 발생했습니다.');
+  }
+};

리뷰: 이거 왜 안함 ㅋㅋㅋ
@@ -0,0 +1,96 @@
+import objectToQueryString, { ObjectQueryParams } from '@/utils/objectToQueryString';
+import { generateBasicToken } from '../utils/auth';
+
+type Method = 'GET' | 'POST' | 'PATCH' | 'DELETE';
+
+type Body = ReadableStream | XMLHttpRequestBodyInit;
+type HeadersType = [string, string][] | Record<string, string> | Headers;
+
+const USER_ID = process.env.VITE_API_USER_ID || 'id';
+const USER_PASSWORD = process.env.VITE_API_USER_PASSWORD || 'password';
+
+type RequestProps = {
+  baseUrl: string;
+  endpoint: string;
+  headers?: HeadersType;
+  body?: Body | object | null;
+  queryParams?: ObjectQueryParams;
+};
+
+type FetcherProps = RequestProps & {
+  method: Method;
+};
+
+type Options = {
+  method: Method;
+  headers: HeadersType;
+  body?: Body | null;
+};
+
+export const requestGet = async <T>({
+  baseUrl,
+  endpoint,
+  headers = {},
+  queryParams,
+}: RequestProps): Promise<T> => {
+  const response = await fetcher({ method: 'GET', baseUrl, endpoint, headers, queryParams });
+  const data: T = await response.json();
+
+  return data;
+};
+
+export const requestPatch = ({
+  baseUrl,
+  endpoint,
+  headers = {},
+  body,
+  queryParams,
+}: RequestProps) => {
+  return fetcher({ method: 'PATCH', baseUrl, endpoint, headers, body, queryParams });
+};
+
+export const requestPost = ({
+  baseUrl,
+  endpoint,
+  headers = {},
+  body,
+  queryParams,
+}: RequestProps) => {
+  return fetcher({ method: 'POST', baseUrl, endpoint, headers, body, queryParams });
+};
+
+export const requestDelete = ({ baseUrl, endpoint, headers = {}, queryParams }: RequestProps) => {
+  return fetcher({ method: 'DELETE', baseUrl, endpoint, headers, queryParams });
+};
+
+const fetcher = ({ method, baseUrl, endpoint, headers, body, queryParams }: FetcherProps) => {
+  const token = generateBasicToken(USER_ID, USER_PASSWORD);
+  const options = {
+    method,
+    headers: {
+      'Content-Type': 'application/json',
+      Authorization: token,
+      ...headers,
+    },
+    body: body ? JSON.stringify(body) : null,
+  };
+
+  let url = `${baseUrl}${endpoint}`;
+
+  if (queryParams) url += `?${objectToQueryString(queryParams)}`;
+
+  return errorHandler(url, options, endpoint);
+};
+
+const errorHandler = async (url: string, options: Options, endpoint: string) => {
+  try {
+    const response = await fetch(url, options);
+
+    if (!response.ok) throw new Error('오류가 발생했습니다.');
+
+    return response;
+  } catch (error) {
+    console.error(`fail to fetch ${endpoint}\n error message: ${error}`);
+    throw new Error('데이터를 가져오는 중 오류가 발생했습니다.');
+  }
+};

리뷰: 여기서 던지는 오류 메시지가 showToast에서 보이는 메시지에서는 활용을 안하고 있는 것 같아서
통일시키는 방식이나 사용자에게 보이는 오류 메시지를 어떻게 관리할지도 고민해보면 좋을 듯요!
(나도 해야함 ㅋ)
@@ -0,0 +1,75 @@
+import { CartItem } from '@/types/cartItem.type';
+import { BASE_URL } from '../baseUrl';
+import { requestGet, requestPost, requestDelete } from '../fetcher';
+import { ENDPOINT } from '../endpoints';
+
+type ResponseCartItemList = {
+  content: CartItem[];
+  pageable: {
+    sort: {
+      sorted: false;
+      unsorted: true;
+      empty: true;
+    };
+    pageNumber: 0;
+    pageSize: 20;
+    offset: 0;
+    paged: true;
+    unpaged: false;
+  };
+  last: true;
+  totalPages: 1;
+  totalElements: 6;
+  sort: {
+    sorted: false;
+    unsorted: true;
+    empty: true;
+  };
+  first: true;
+  number: 0;
+  numberOfElements: 6;
+  size: 20;
+  empty: false;
+};
+
+type QueryParams = {
+  page: number;
+  size: number;
+};
+
+export const requestCartItemList = async (
+  page: number = 0,
+  size: number = 20,
+): Promise<ResponseCartItemList> => {
+  const queryParams: QueryParams = {
+    page,
+    size,
+  };
+
+  return await requestGet<ResponseCartItemList>({
+    baseUrl: BASE_URL.SHOP,
+    endpoint: ENDPOINT.CART_ITEM,
+    queryParams,
+  });
+};
+
+export const requestAddCartItem = async (productId: number, quantity: number = 1) => {
+  await requestPost({
+    baseUrl: BASE_URL.SHOP,
+    endpoint: ENDPOINT.CART_ITEM,
+    body: {
+      productId,
+      quantity,
+    },
+  });
+};
+
+export const requestDeleteCartItem = async (cartItemId: number) => {
+  await requestDelete({
+    baseUrl: BASE_URL.SHOP,
+    endpoint: `${ENDPOINT.CART_ITEM}/${cartItemId}`,
+    body: {
+      id: cartItemId,
+    },
+  });
+};

리뷰: 사소한 의견인데, 저는 api 응답이 언제든 바뀔 수 있다고 생각해서 요런 타입은 type보다 interface로 선언하려는 편이긴 함다
그러면 pageable 같이 안쓰고 뭔가 다른 정보랑 중복되는 친구들은 안써줄 수도 있어서 오히려 리뷰어에게 쓸 친구들만 interface에 선언해줄 수 있다는 장점도 있다고 생각했슴다.
(interface의 단점은 없을까 생각도 되는데 함 찾아봐야겠다)
@@ -0,0 +1,29 @@
+.container {
+  position: relative;
+}
+
+.cartIcon {
+  cursor: pointer;
+}
+
+.amountContainer {
+  position: absolute;
+  bottom: 0;
+  right: 0;
+  width: 19px;
+  height: 19px;
+  background: #fff;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  border-radius: 50%;
+}
+
+.amount {
+  color: black;
+  font-size: 10px;
+  font-weight: 700;
+  line-height: 12.19px;
+  text-align: left;
+  margin-top: 2px;
+}

리뷰: 이거 갠적인 궁금증인데 이거 두개 필요함?!
@@ -1,4 +1,4 @@
-<!DOCTYPE html>
+<!doctype html>
 <html lang="en">
   <head>
     <meta charset="UTF-8" />
@@ -7,6 +7,7 @@
     <title>react-shopping-products</title>
   </head>
   <body>
+    <div id="toast"></div>
     <div id="root"></div>
     <script type="module" src="/src/main.tsx"></script>
   </body>

리뷰: 저도 이게 왜 된지 모르겠네요.... ㅋ.ㅋ
@@ -0,0 +1,96 @@
+import objectToQueryString, { ObjectQueryParams } from '@/utils/objectToQueryString';
+import { generateBasicToken } from '../utils/auth';
+
+type Method = 'GET' | 'POST' | 'PATCH' | 'DELETE';
+
+type Body = ReadableStream | XMLHttpRequestBodyInit;
+type HeadersType = [string, string][] | Record<string, string> | Headers;
+
+const USER_ID = process.env.VITE_API_USER_ID || 'id';
+const USER_PASSWORD = process.env.VITE_API_USER_PASSWORD || 'password';
+
+type RequestProps = {
+  baseUrl: string;
+  endpoint: string;
+  headers?: HeadersType;
+  body?: Body | object | null;
+  queryParams?: ObjectQueryParams;
+};
+
+type FetcherProps = RequestProps & {
+  method: Method;
+};
+
+type Options = {
+  method: Method;
+  headers: HeadersType;
+  body?: Body | null;
+};
+
+export const requestGet = async <T>({
+  baseUrl,
+  endpoint,
+  headers = {},
+  queryParams,
+}: RequestProps): Promise<T> => {
+  const response = await fetcher({ method: 'GET', baseUrl, endpoint, headers, queryParams });
+  const data: T = await response.json();
+
+  return data;
+};
+
+export const requestPatch = ({
+  baseUrl,
+  endpoint,
+  headers = {},
+  body,
+  queryParams,
+}: RequestProps) => {
+  return fetcher({ method: 'PATCH', baseUrl, endpoint, headers, body, queryParams });
+};
+
+export const requestPost = ({
+  baseUrl,
+  endpoint,
+  headers = {},
+  body,
+  queryParams,
+}: RequestProps) => {
+  return fetcher({ method: 'POST', baseUrl, endpoint, headers, body, queryParams });
+};
+
+export const requestDelete = ({ baseUrl, endpoint, headers = {}, queryParams }: RequestProps) => {
+  return fetcher({ method: 'DELETE', baseUrl, endpoint, headers, queryParams });
+};
+
+const fetcher = ({ method, baseUrl, endpoint, headers, body, queryParams }: FetcherProps) => {
+  const token = generateBasicToken(USER_ID, USER_PASSWORD);
+  const options = {
+    method,
+    headers: {
+      'Content-Type': 'application/json',
+      Authorization: token,
+      ...headers,
+    },
+    body: body ? JSON.stringify(body) : null,
+  };
+
+  let url = `${baseUrl}${endpoint}`;
+
+  if (queryParams) url += `?${objectToQueryString(queryParams)}`;
+
+  return errorHandler(url, options, endpoint);
+};
+
+const errorHandler = async (url: string, options: Options, endpoint: string) => {
+  try {
+    const response = await fetch(url, options);
+
+    if (!response.ok) throw new Error('오류가 발생했습니다.');
+
+    return response;
+  } catch (error) {
+    console.error(`fail to fetch ${endpoint}\n error message: ${error}`);
+    throw new Error('데이터를 가져오는 중 오류가 발생했습니다.');
+  }
+};

리뷰: 맞아요. 사실 어디서 던진 오류가 캐치되고 그런걸 확인을 안해서 에러 바운더리와 비슷한 설계로 이를 해결할 수 있지 않을까 싶습니다. 한번 시도해볼게요!
@@ -0,0 +1,29 @@
+.container {
+  position: relative;
+}
+
+.cartIcon {
+  cursor: pointer;
+}
+
+.amountContainer {
+  position: absolute;
+  bottom: 0;
+  right: 0;
+  width: 19px;
+  height: 19px;
+  background: #fff;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  border-radius: 50%;
+}
+
+.amount {
+  color: black;
+  font-size: 10px;
+  font-weight: 700;
+  line-height: 12.19px;
+  text-align: left;
+  margin-top: 2px;
+}

리뷰: 긁어온 것 같네요. 필요 없을듯...!
@@ -0,0 +1,17 @@
+package lotto
+
+import lotto.service.LottoService
+import lotto.view.Input
+import lotto.view.Output
+
+fun main() {
+    val money = Input.money()
+    val randomNums = Input.getRandomLottoNumsPerMoney(money)
+    val winNums = Input.winNums()
+
+    val lottoService = LottoService()
+    val lottoRanks = lottoService.getLottoRanks(randomNums, winNums)
+    val revenueRate = lottoService.getRevenueRate(money.toDouble(), lottoRanks)
+
+    Output.result(lottoRanks, revenueRate)
+}

리뷰: VO에 대해 알아보고 원시값을 포장해보면 어떨까요? [참고](https://tecoble.techcourse.co.kr/post/2020-05-29-wrap-primitive-type/)
@@ -0,0 +1,36 @@
+package lotto.view
+
+import lotto.domain.LottoTicket.Companion.TICKET_PRICE
+
+object Input {
+    fun money(): Int {
+        println("구입금액을 입력해 주세요.")
+        return readln().toInt()
+    }
+
+    fun getRandomLottoNumsPerMoney(money: Int): List<List<Int>> {
+        val ticketNumber = money.div(TICKET_PRICE)
+        println(ticketNumber)
+
+        return (1..ticketNumber)
+            .map {
+                val lottoNums = generateRandomLottoNums()
+                println(lottoNums)
+
+                lottoNums
+            }
+    }
+
+    fun winNums(): List<Int> {
+        println("지난 주 당첨 번호를 입력해 주세요.")
+        val split = readln().split(",")
+
+        return split.map { it.toInt() }
+    }
+
+    private fun generateRandomLottoNums(): List<Int> {
+        val lottoNumRange = 1..45
+
+        return lottoNumRange.shuffled().subList(0, 6)
+    }
+}

리뷰: 중첩 컬렉션보다는 의미를 담을 수 있는 객체를 만들어보면 어떨까요?
@@ -0,0 +1,36 @@
+package lotto.view
+
+import lotto.domain.LottoTicket.Companion.TICKET_PRICE
+
+object Input {
+    fun money(): Int {
+        println("구입금액을 입력해 주세요.")
+        return readln().toInt()
+    }
+
+    fun getRandomLottoNumsPerMoney(money: Int): List<List<Int>> {
+        val ticketNumber = money.div(TICKET_PRICE)
+        println(ticketNumber)
+
+        return (1..ticketNumber)
+            .map {
+                val lottoNums = generateRandomLottoNums()
+                println(lottoNums)
+
+                lottoNums
+            }
+    }
+
+    fun winNums(): List<Int> {
+        println("지난 주 당첨 번호를 입력해 주세요.")
+        val split = readln().split(",")
+
+        return split.map { it.toInt() }
+    }
+
+    private fun generateRandomLottoNums(): List<Int> {
+        val lottoNumRange = 1..45
+
+        return lottoNumRange.shuffled().subList(0, 6)
+    }
+}

리뷰: 구매할 티켓수량을 view에서 구하는것이 적절한 위치일지 고민해보면 좋을 것 같아요!
@@ -0,0 +1,36 @@
+package lotto.view
+
+import lotto.domain.LottoTicket.Companion.TICKET_PRICE
+
+object Input {
+    fun money(): Int {
+        println("구입금액을 입력해 주세요.")
+        return readln().toInt()
+    }
+
+    fun getRandomLottoNumsPerMoney(money: Int): List<List<Int>> {
+        val ticketNumber = money.div(TICKET_PRICE)
+        println(ticketNumber)
+
+        return (1..ticketNumber)
+            .map {
+                val lottoNums = generateRandomLottoNums()
+                println(lottoNums)
+
+                lottoNums
+            }
+    }
+
+    fun winNums(): List<Int> {
+        println("지난 주 당첨 번호를 입력해 주세요.")
+        val split = readln().split(",")
+
+        return split.map { it.toInt() }
+    }
+
+    private fun generateRandomLottoNums(): List<Int> {
+        val lottoNumRange = 1..45
+
+        return lottoNumRange.shuffled().subList(0, 6)
+    }
+}

리뷰: 매직넘버를 상수화해보면 어떨까요?!
@@ -9,3 +9,8 @@
         - [x] 문자열 계산기에 숫자 이외의 값 또는 음수를 전달하는 경우 RuntimeException 예외를 throw 한다.
     - [x] 원시값을 포장한 도메인객체 구현
         - [x] 연산자 오버라이딩 구현
+
+## 로또 (자동) 기능 요구사항
+- [x] 로또 구입 금액을 입력하면 구입 금액에 해당하는 로또를 발급해야 한다.
+  - [x] 금액은 0 이상이다.
+- [x] 로또 1장의 가격은 1000원이다.

리뷰: 해당 validation은 어디서 수행해주고 있나요?!
@@ -0,0 +1,23 @@
+package lotto.service
+
+import lotto.domain.LottoTicketMachine
+import lotto.domain.Rank
+
+class LottoService {
+    fun getLottoRanks(nums: List<List<Int>>, winNums: List<Int>): List<Rank> {
+        return nums.map {
+            val issue = LottoTicketMachine.issue(it)
+            val winLottoTicket = LottoTicketMachine.issue(winNums)
+
+            Rank.determineLottoTicket(winLottoTicket, issue)
+        }
+    }
+
+    fun getRevenueRate(investment: Double, ranks: List<Rank>): Double {
+        val totalPrizeSum = ranks.sumOf {
+            it.prize
+        }.toDouble()
+
+        return totalPrizeSum / investment
+    }
+}

리뷰: object 사용을 고려해보면 어떨까요?
@@ -0,0 +1,17 @@
+package lotto.domain
+
+object LottoTicketMachine {
+    fun issue(nums: List<Int>): LottoTicket {
+        require(nums.size == 6) {
+            "로또 개수를 잘못 입력했습니다"
+        }
+
+        val lottoBox = LottoBox()
+
+        val lottoNums = nums.map {
+            lottoBox.getLottoNum(it)
+        }.toList()
+
+        return LottoTicket(lottoNums)
+    }
+}

리뷰: issue라는 네이밍을 붙이신 이유가 궁금합니다!
@@ -0,0 +1,24 @@
+package lotto.domain
+
+class LottoBox {
+    private val lottoNums: MutableMap<Int, LottoNum>
+
+    init {
+        val intRange = 1..45
+
+        lottoNums = intRange
+            .map { LottoNum(it) }
+            .associateBy { it.num }
+            .toMutableMap()
+    }
+
+    fun getLottoNum(num: Int): LottoNum {
+        val lottoNum = lottoNums[num]
+        require(lottoNum != null) {
+            "로또 숫자는 중복해서 입력할 수 업습니다"
+        }
+
+        lottoNums.remove(lottoNum.num)
+        return lottoNum
+    }
+}

리뷰: LottoBox만 봤을때 lottoNum이 null이라고해서  중복된 숫자를 입력했다고 보장할 수 있을까요?
@@ -0,0 +1,28 @@
+package lotto.domain
+
+class LottoNum(num: Int) {
+    val num: Int
+
+    init {
+        require(num in 1..45) {
+            "로또 숫자 범위는 1 ~ 45 입니다."
+        }
+
+        this.num = num
+    }
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (javaClass != other?.javaClass) return false
+
+        other as LottoNum
+
+        if (num != other.num) return false
+
+        return true
+    }
+
+    override fun hashCode(): Int {
+        return num
+    }
+}

리뷰: inline class를 활용해봐도 좋을 것 같아요!! :) 

> 래퍼클래스로 inline class와 value class 를 사용하는거 같은데요!
잘 이해가 안가서요~
뭔가 좋은 기능같기도 한데, 실무에서 어떻게, 어떤상황에 사용하면 좋을지 질문드립니다!


사실 저도 잘 사용한다고 말씀드리기는 어렵지만, 저는 아래와 같은 기준을 충족시키는 경우 인라인 클래스를 사용하고 있어요

* 해당 래퍼타입이 여러곳에서 호출되는가?
* 해당 클래스가 data class의 특징을 가지고 있는가?

실무에서 적용은 필수가 아니고 팀원들마다 의견이 다를 수 있으니 팀 내부적으로도 이야기를 해보고 컨벤션을 잡아가보는것도 방법일 것 같아요!

[이런 글](https://github.com/Kotlin/KEEP/blob/master/notes/value-classes.md)도 읽어보면 도움이 될 것 같아요!! :)
@@ -0,0 +1,28 @@
+package lotto.domain
+
+class LottoNum(num: Int) {
+    val num: Int
+
+    init {
+        require(num in 1..45) {
+            "로또 숫자 범위는 1 ~ 45 입니다."
+        }
+
+        this.num = num
+    }
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (javaClass != other?.javaClass) return false
+
+        other as LottoNum
+
+        if (num != other.num) return false
+
+        return true
+    }
+
+    override fun hashCode(): Int {
+        return num
+    }
+}

리뷰: 여기도 매직넘버를 상수화해보면 어떨까요?
@@ -0,0 +1,13 @@
+export class NetworkError extends Error {
+  constructor(message) {
+    super(message);
+    this.name = "NetworkError";
+  }
+}
+
+export class HttpError extends Error {
+  constructor(message) {
+    super(message);
+    this.name = "HttpError";
+  }
+}

리뷰: 사용되지 않는 코드가 남아있네요, 혹시 어떤 의도로 만드셨을까요?!