ewhk9887/korean_code_reviews_from_github huggingface dataset:
@@ -0,0 +1,45 @@
+package calculator.controller.domain
+
+import calculator.constants.Delimiter.CLONE
+import calculator.constants.Delimiter.COMMA
+import calculator.constants.Delimiter.CUSTOM_DELIMITER_PREFIX
+import calculator.constants.Delimiter.CUSTOM_DELIMITER_SUFFIX
+
+class DelimiterController(
+    private val input: String
+) {
+    fun checkDelimiter(): List<String> {
+        if (hasCustomDelimiter()) {
+            val customDelimiter = getCustomDelimiter()
+            val noCustomDelimiter = deleteCustomDelimiter()
+            val numbers = splitCustomDelimiter(noCustomDelimiter, customDelimiter)
+            return numbers
+        }
+        val numbers = splitDelimiter()
+        return numbers
+    }
+
+    private fun hasCustomDelimiter(): Boolean {
+        return input.startsWith(CUSTOM_DELIMITER_PREFIX.value) && input.indexOf(CUSTOM_DELIMITER_SUFFIX.value) == 3
+    }
+
+    private fun getCustomDelimiter(): String {
+        val customDelimiter = input.substring(2, 3)
+        return customDelimiter
+    }
+
+    private fun deleteCustomDelimiter(): String {
+        val noCustomDelimiter = input.substring(5)
+        return noCustomDelimiter
+    }
+
+    private fun splitDelimiter(): List<String> {
+        val numbers = input.split(COMMA.value, CLONE.value)
+        return numbers
+    }
+
+    private fun splitCustomDelimiter(noCustomDelimiter: String, customDelimiter: String): List<String> {
+        val numbers = noCustomDelimiter.split(COMMA.value, CLONE.value, customDelimiter)
+        return numbers
+    }
+}
\ No newline at end of file

리뷰: 오 있는지 없는지 확인하고, 결과는 number로 통일하는 방법 배워갑니다!
@@ -0,0 +1,35 @@
+package calculator.controller.validation
+
+class UserInputValidator {
+    fun validateUserInput(numbers: List<String>): List<Int> {
+        val newNumbers = checkIsEmpty(numbers)
+        checkIsInteger(newNumbers)
+        val allNumbers = changeInteger(newNumbers)
+        changeNegativeNumber(allNumbers)
+        return allNumbers
+    }
+
+    private fun checkIsEmpty(numbers: List<String>): List<String> {
+        return numbers.map { it.ifEmpty { "0" } }
+    }
+
+    private fun checkIsInteger(newNumbers: List<String>) {
+        newNumbers.map {
+            if (it.toIntOrNull() == null) {
+                throw IllegalArgumentException(UserInputErrorType.NOT_INTEGER.errorMessage)
+            }
+        }
+    }
+
+    private fun changeInteger(newNumbers: List<String>): List<Int> {
+        return newNumbers.map { it.toInt() }
+    }
+
+    private fun changeNegativeNumber(allNumbers: List<Int>) {
+        allNumbers.forEach {
+            if (it < 0) {
+                throw IllegalArgumentException(UserInputErrorType.NEGATIVE_NUMBER.errorMessage)
+            }
+        }
+    }
+}
\ No newline at end of file

리뷰: 아...! any!! 좋은 함수 알려줘서 감사합니다!
@@ -0,0 +1,45 @@
+package calculator.controller.domain
+
+import calculator.constants.Delimiter.CLONE
+import calculator.constants.Delimiter.COMMA
+import calculator.constants.Delimiter.CUSTOM_DELIMITER_PREFIX
+import calculator.constants.Delimiter.CUSTOM_DELIMITER_SUFFIX
+
+class DelimiterController(
+    private val input: String
+) {
+    fun checkDelimiter(): List<String> {
+        if (hasCustomDelimiter()) {
+            val customDelimiter = getCustomDelimiter()
+            val noCustomDelimiter = deleteCustomDelimiter()
+            val numbers = splitCustomDelimiter(noCustomDelimiter, customDelimiter)
+            return numbers
+        }
+        val numbers = splitDelimiter()
+        return numbers
+    }
+
+    private fun hasCustomDelimiter(): Boolean {
+        return input.startsWith(CUSTOM_DELIMITER_PREFIX.value) && input.indexOf(CUSTOM_DELIMITER_SUFFIX.value) == 3
+    }
+
+    private fun getCustomDelimiter(): String {
+        val customDelimiter = input.substring(2, 3)
+        return customDelimiter
+    }
+
+    private fun deleteCustomDelimiter(): String {
+        val noCustomDelimiter = input.substring(5)
+        return noCustomDelimiter
+    }
+
+    private fun splitDelimiter(): List<String> {
+        val numbers = input.split(COMMA.value, CLONE.value)
+        return numbers
+    }
+
+    private fun splitCustomDelimiter(noCustomDelimiter: String, customDelimiter: String): List<String> {
+        val numbers = noCustomDelimiter.split(COMMA.value, CLONE.value, customDelimiter)
+        return numbers
+    }
+}
\ No newline at end of file

리뷰: 정답입니다. 일부러 구현 안했습니다! 저는 문제 해석하면서 구분자는 한글자만 와야한다라고 생각을 해서 위와 같이 입력하면 에러라고 판단했습니다! 문제 해석이 제일 어려운 거 같아요 ㅎㅎ
@@ -0,0 +1,45 @@
+package calculator.controller.domain
+
+import calculator.constants.Delimiter.CLONE
+import calculator.constants.Delimiter.COMMA
+import calculator.constants.Delimiter.CUSTOM_DELIMITER_PREFIX
+import calculator.constants.Delimiter.CUSTOM_DELIMITER_SUFFIX
+
+class DelimiterController(
+    private val input: String
+) {
+    fun checkDelimiter(): List<String> {
+        if (hasCustomDelimiter()) {
+            val customDelimiter = getCustomDelimiter()
+            val noCustomDelimiter = deleteCustomDelimiter()
+            val numbers = splitCustomDelimiter(noCustomDelimiter, customDelimiter)
+            return numbers
+        }
+        val numbers = splitDelimiter()
+        return numbers
+    }
+
+    private fun hasCustomDelimiter(): Boolean {
+        return input.startsWith(CUSTOM_DELIMITER_PREFIX.value) && input.indexOf(CUSTOM_DELIMITER_SUFFIX.value) == 3
+    }
+
+    private fun getCustomDelimiter(): String {
+        val customDelimiter = input.substring(2, 3)
+        return customDelimiter
+    }
+
+    private fun deleteCustomDelimiter(): String {
+        val noCustomDelimiter = input.substring(5)
+        return noCustomDelimiter
+    }
+
+    private fun splitDelimiter(): List<String> {
+        val numbers = input.split(COMMA.value, CLONE.value)
+        return numbers
+    }
+
+    private fun splitCustomDelimiter(noCustomDelimiter: String, customDelimiter: String): List<String> {
+        val numbers = noCustomDelimiter.split(COMMA.value, CLONE.value, customDelimiter)
+        return numbers
+    }
+}
\ No newline at end of file

리뷰: CUSTOM_DELIMITER_SUFFIX의 index가 3이면  CUSTOM_DELIMITER_PREFIX와 CUSTOM_DELIMITER_SUFFIX 사이에 커스텀 구분자를 입력을 했다고 판단을 했습니다!
@@ -0,0 +1,35 @@
+package calculator.controller.validation
+
+class UserInputValidator {
+    fun validateUserInput(numbers: List<String>): List<Int> {
+        val newNumbers = checkIsEmpty(numbers)
+        checkIsInteger(newNumbers)
+        val allNumbers = changeInteger(newNumbers)
+        changeNegativeNumber(allNumbers)
+        return allNumbers
+    }
+
+    private fun checkIsEmpty(numbers: List<String>): List<String> {
+        return numbers.map { it.ifEmpty { "0" } }
+    }
+
+    private fun checkIsInteger(newNumbers: List<String>) {
+        newNumbers.map {
+            if (it.toIntOrNull() == null) {
+                throw IllegalArgumentException(UserInputErrorType.NOT_INTEGER.errorMessage)
+            }
+        }
+    }
+
+    private fun changeInteger(newNumbers: List<String>): List<Int> {
+        return newNumbers.map { it.toInt() }
+    }
+
+    private fun changeNegativeNumber(allNumbers: List<Int>) {
+        allNumbers.forEach {
+            if (it < 0) {
+                throw IllegalArgumentException(UserInputErrorType.NEGATIVE_NUMBER.errorMessage)
+            }
+        }
+    }
+}
\ No newline at end of file

리뷰: 아 맞네요! 그럼 함수명이 더 통일성이 있어서 좋을거 같아요! 좋은 의견 감사합니다!
@@ -0,0 +1,17 @@
+package store.model;
+
+import store.view.InputView;
+
+public class MembershipDiscount {
+    public int applyMembershipDiscount(int totalNonPromotionPrice) {
+        if (InputView.askMemberShip()) {
+            return calculateMembershipDiscount(totalNonPromotionPrice);
+        }
+        return 0;
+    }
+
+    public int calculateMembershipDiscount(int totalAmount) {
+        int membershipDiscount = (int) (totalAmount * 0.3);
+        return Math.min(membershipDiscount, 8000);
+    }
+}
\ No newline at end of file

리뷰: 멤버십 할인 비율을 상수로 뽑으면 어떨까 싶습니다!!!
@@ -0,0 +1,17 @@
+package store.model;
+
+import store.view.InputView;
+
+public class MembershipDiscount {
+    public int applyMembershipDiscount(int totalNonPromotionPrice) {
+        if (InputView.askMemberShip()) {
+            return calculateMembershipDiscount(totalNonPromotionPrice);
+        }
+        return 0;
+    }
+
+    public int calculateMembershipDiscount(int totalAmount) {
+        int membershipDiscount = (int) (totalAmount * 0.3);
+        return Math.min(membershipDiscount, 8000);
+    }
+}
\ No newline at end of file

리뷰: 저도 이렇게 할걸 그랬습니다!! 잘 배우고 갑니다!!!!
@@ -0,0 +1,81 @@
+package store.model;
+
+import store.message.ViewMessage;
+
+import java.text.NumberFormat;
+import java.util.Locale;
+
+public class Product {
+    private final String name;
+    private final int price;
+    private int quantity;
+    private int promotionQuantity;
+    private final Promotion promotion;
+
+    public Product(String name, int price, int quantity, String promotion) {
+        this.name = name;
+        this.price = price;
+        this.quantity = quantity;
+        this.promotion = Promotion.fromString(promotion);
+    }
+
+    // getter 및 setter
+    public String getName() {
+        return name;
+    }
+
+    public int getPrice() {
+        return price;
+    }
+
+    public int getQuantity() {
+        return quantity;
+    }
+
+    public Promotion getPromotion() {
+        return promotion;
+    }
+
+    public int getPromotionQuantity() {
+        return promotionQuantity;
+    }
+
+    public int getTotalQuantity() {
+        return quantity + promotionQuantity;
+    }
+
+    public void setQuantity(int quantity) {
+        this.quantity = quantity;
+    }
+
+    public void setPromotionQuantity(int promotionQuantity) {
+        this.promotionQuantity = promotionQuantity;
+    }
+
+
+    public String formatPrice() {
+        NumberFormat currencyFormat = NumberFormat.getInstance(Locale.KOREA);
+        return currencyFormat.format(price);
+    }
+
+    @Override
+    public String toString() {
+        if (quantity > 0 && promotion != Promotion.NULL) {
+            return ViewMessage.PROMOTION_WITH_STOCK
+                    .format(name, formatPrice(), promotionQuantity, promotion.getName(),
+                            name, formatPrice(), quantity, Promotion.NULL.getName());
+        }
+        if (promotion == Promotion.NULL) {
+            return ViewMessage.NO_PROMOTION
+                    .format(name, formatPrice(), quantity, Promotion.NULL.getName());
+        }
+        if (quantity == 0 && promotionQuantity == 0) {
+            return ViewMessage.OUT_OF_STOCK_WITH_PROMOTION
+                    .format(name, formatPrice(), promotion.getName(),
+                            name, formatPrice(), Promotion.NULL.getName());
+        }
+        return ViewMessage.PROMOTION_ONLY_OUT_OF_STOCK
+                .format(name, formatPrice(), promotionQuantity, promotion.getName(),
+                        name, formatPrice(), Promotion.NULL.getName());
+    }
+}

리뷰: 재고수량의 변동을 setter 대신 수량 증가시 `increaseQuantity()` 재고 감소시 'decreaseQuantiy()` 이런식으로 의미를 부여하는 메서드를 만들면 더 좋을 것 같습니다!!!
@@ -0,0 +1,69 @@
+package store.model;
+
+import store.message.ReceiptMessage;
+import store.view.OutputView;
+
+import java.text.NumberFormat;
+import java.util.Locale;
+import java.util.Map;
+
+public class Receipt {
+    private final Map<String, PurchaseItem> purchase;
+    private final Map<String, PurchaseItem> freeItems;
+    private final int membershipDiscountPrice;
+    private int totalAmount;
+    private int totalPromotionDiscount;
+    private int totalPurchasePrice;
+
+    public Receipt(Map<String, PurchaseItem> purchase, Map<String, PurchaseItem> freeItems,
+                   int membershipDiscountPrice, int totalAmount) {
+        this.purchase = purchase;
+        this.freeItems = freeItems;
+        this.membershipDiscountPrice = membershipDiscountPrice;
+        this.totalAmount = totalAmount;
+        this.totalPromotionDiscount = 0;
+        this.totalPurchasePrice = 0;
+
+    }
+
+    public void print() {
+        NumberFormat currencyFormat = NumberFormat.getInstance(Locale.KOREA);
+
+        OutputView.printStore();
+
+        totalPurchasePrice = purchaseItems(currencyFormat, totalPurchasePrice);
+        totalPromotionDiscount = freeItems(totalPromotionDiscount);
+
+        OutputView.printLine();
+
+        printPriceDetails(currencyFormat);
+    }
+
+    private int purchaseItems(NumberFormat currencyFormat, int totalPurchasePrice) {
+        for (PurchaseItem purchaseItem : purchase.values()) {
+            totalPurchasePrice += purchaseItem.getPrice();
+            OutputView.printProductFormat(purchaseItem, currencyFormat);
+        }
+        return totalPurchasePrice;
+    }
+
+    private int freeItems(int totalPromotionDiscount) {
+        OutputView.printFreeItem();
+        for (PurchaseItem freeItem : freeItems.values()) {
+            totalPromotionDiscount += freeItem.getQuantity() * freeItem.getPrice();
+            if (freeItem.getQuantity() > 0) {
+                OutputView.printFreeProductFormat(freeItem);
+            }
+        }
+        return totalPromotionDiscount;
+    }
+
+    private void printPriceDetails(NumberFormat currencyFormat) {
+        totalAmount -= totalPromotionDiscount;
+        totalAmount -= membershipDiscountPrice;
+        OutputView.printTotalAmount(totalPurchasePrice,currencyFormat);
+        OutputView.printPromotionDiscount(totalPromotionDiscount,currencyFormat);
+        OutputView.printMembershipDiscount(membershipDiscountPrice,currencyFormat);
+        OutputView.printFinalAmount(totalAmount,currencyFormat);
+    }
+}

리뷰: MVC 패턴에 대한 내용을 살펴보면 모델은 뷰나 컨트롤러를 의존하면 안된다고 되어 있는 글을 보았습니다!! Recipt 에 대한 정보를 DTO 로 만든 뒤 컨트롤러에 넘겨 컨트롤러가 OutputView로 영수증에 대한 내용을 넘겼으면 어떨까 싶습니다!!
@@ -0,0 +1,34 @@
+package store.model;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+class MembershipTest {
+    private MembershipDiscount membershipDiscount;
+
+    @BeforeEach
+    void setUp() {
+        membershipDiscount = new MembershipDiscount();
+    }
+
+    @Test
+    void applyMembershipDiscount_WithMembership_ReturnsDiscountedAmount() {
+        int totalNonPromotionPrice = 20000;
+        int expectedDiscount = 6000;
+
+        int result = membershipDiscount.calculateMembershipDiscount(totalNonPromotionPrice);
+
+        assertEquals(expectedDiscount, result);
+    }
+
+    @Test
+    void applyMembershipDiscount_WithMembership_CapsAt8000() {
+        int totalNonPromotionPrice = 50000;
+        int expectedDiscount = 8000;
+
+        int result = membershipDiscount.calculateMembershipDiscount(totalNonPromotionPrice);
+
+        assertEquals(expectedDiscount, result);
+    }
+}

리뷰: 테스트 코드에 대한 많은 고민이 보이는 것 같습니다!!! 정말 고생하셨습니다!!!!
@@ -0,0 +1,61 @@
+name: Solution-friend Dev CI/CD
+
+on:
+  pull_request:
+    types: [ closed ]
+  workflow_dispatch: # (2).수동 실행도 가능하도록
+
+jobs:
+  build:
+    runs-on: ubuntu-latest # (3).OS환경
+    if: github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'develop'
+
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v2 # (4).코드 check out
+
+      - name: Set up JDK 11
+        uses: actions/setup-java@v3
+        with:
+          java-version: 11 # (5).자바 설치
+          distribution: 'adopt'
+
+      - name: Grant execute permission for gradlew
+        run: chmod +x ./gradlew
+        shell: bash # (6).권한 부여
+
+      - name: Build with Gradle
+        run: ./gradlew clean build -x test
+        shell: bash # (7).build시작
+
+      - name: Get current time
+        uses: 1466587594/get-current-time@v2
+        id: current-time
+        with:
+          format: YYYY-MM-DDTHH-mm-ss
+          utcOffset: "+09:00" # (8).build시점의 시간확보
+
+      - name: Show Current Time
+        run: echo "CurrentTime=$"
+        shell: bash # (9).확보한 시간 보여주기
+
+      - name: Generate deployment package
+        run: |
+          mkdir -p deploy
+          cp build/libs/*.jar deploy/application.jar
+          cp Procfile deploy/Procfile
+          cp -r .ebextensions-dev deploy/.ebextensions
+          cp -r .platform deploy/.platform
+          cd deploy && zip -r deploy.zip .
+
+      - name: Beanstalk Deploy
+        uses: einaregilsson/beanstalk-deploy@v20
+        with:
+          aws_access_key: ${{ secrets.AWS_ACTION_ACCESS_KEY_ID }}
+          aws_secret_key: ${{ secrets.AWS_ACTION_SECRET_ACCESS_KEY }}
+          application_name: solution-friend-dev
+          environment_name: Solution-friend-dev-env-1
+          version_label: github-action-${{ steps.current-time.outputs.formattedTime }}
+          region: ap-northeast-2
+          deployment_package: deploy/deploy.zip
+          wait_for_deployment: false

리뷰: PR이 기각되어도 해당 Action이 수행될 것 같네요.
@@ -0,0 +1,62 @@
+plugins {
+	id 'java'
+	id 'org.springframework.boot' version '2.7.17'
+	id 'io.spring.dependency-management' version '1.0.15.RELEASE'
+}
+
+group = 'friend'
+version = '0.0.1-SNAPSHOT'
+
+java {
+	sourceCompatibility = '11'
+}
+
+configurations {
+	compileOnly {
+		extendsFrom annotationProcessor
+	}
+}
+
+repositories {
+	mavenCentral()
+}
+
+dependencies {
+	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
+	implementation 'org.springframework.boot:spring-boot-starter-web'
+	compileOnly 'org.projectlombok:lombok'
+	runtimeOnly 'com.mysql:mysql-connector-j'
+	annotationProcessor 'org.projectlombok:lombok'
+	implementation 'org.springframework.boot:spring-boot-starter-validation'
+	testImplementation 'org.springframework.boot:spring-boot-starter-test'
+	implementation 'org.springdoc:springdoc-openapi-ui:1.6.15'
+	implementation 'io.springfox:springfox-swagger2:2.9.2'
+	implementation 'io.springfox:springfox-swagger-ui:2.9.2'
+
+	//user
+	implementation 'com.google.code.findbugs:jsr305:3.0.2'
+	//mail
+	implementation group: 'com.sun.mail', name: 'javax.mail', version: '1.6.2'
+	//jwt
+	implementation 'org.springframework.boot:spring-boot-starter-security'
+	implementation group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.4'
+	runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.4'
+	runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.4'
+// redis
+	implementation 'org.springframework.boot:spring-boot-starter-data-redis'
+	//kakao
+//	implementation 'org.springframework.boot:spring-boot-starter-webflux'
+	// S3
+	implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE'
+	implementation platform('software.amazon.awssdk:bom:2.20.56')
+	implementation 'software.amazon.awssdk:s3'
+
+}
+
+tasks.named('test') {
+	useJUnitPlatform()
+}
+
+jar {
+	enabled = false
+}

리뷰: gradle 형식을 통일해 주세요.
`com.sun.mail:javax.mail:1.6.2`

또한, sun 의존성은 매우 옛날 의존성을 가능성이 높습니다. 확인이 필요합니다.
@@ -0,0 +1,27 @@
+package friend.spring;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
+import org.springframework.scheduling.annotation.EnableScheduling;
+
+import javax.annotation.PostConstruct;
+import java.time.LocalDateTime;
+import java.util.TimeZone;
+
+@SpringBootApplication
+@EnableJpaAuditing
+@EnableScheduling
+public class Application {
+
+    @PostConstruct
+    public void started() {
+        TimeZone.setDefault(TimeZone.getTimeZone("Asia/Seoul"));
+    }
+
+    public static void main(String[] args) {
+        SpringApplication.run(Application.class, args);
+        System.out.println("현재시간 " + LocalDateTime.now());
+    }
+
+}

리뷰: System out 보다는 log 라이브러리로 출력하도록 설정해 주세요.
@@ -0,0 +1,15 @@
+package friend.spring.OAuth;
+
+import lombok.Data;
+
+@Data
+public class OAuthToken {
+
+    private String access_token;
+    private String token_type;
+    private String refresh_token;
+    private String id_token;
+    private int expires_in;
+    private String scope;
+    private int refresh_token_expires_in;
+}

리뷰: `@Data` 는 최대한 사용하지 마세요.
의도하지 않은 결과가 나올 수 있습니다.
필요한 필드에 따라 적절하게 필요한 어노테이션만 사용하도록 해 주세요.
@@ -0,0 +1,15 @@
+package friend.spring.OAuth;
+
+import lombok.Data;
+
+@Data
+public class OAuthToken {
+
+    private String access_token;
+    private String token_type;
+    private String refresh_token;
+    private String id_token;
+    private int expires_in;
+    private String scope;
+    private int refresh_token_expires_in;
+}

리뷰: Java에서는 가능하면 underscore를 사용하지 않도록 해 주세요. (캐멀 케이스 권장)

- 참고: https://google.github.io/styleguide/javaguide.html
- 참고2: https://naver.github.io/hackday-conventions-java/


+ 가능하면 checkstyle 등의 도구를 사용해서 코드 스타일을 정의하고, 해당 스타일에 맞추게 코드 형태를 강제하는 게 좋을 것 같아요.
@@ -0,0 +1,21 @@
+import { useRouter } from "next/navigation";
+import SolidButton from "@/components/common/buttons/SolidButton";
+
+export default function CardReview({ meetupId }: { meetupId: number }) {
+  const router = useRouter();
+
+  const handleClickReview = (e: React.MouseEvent, meetUpId: number) => {
+    e.stopPropagation();
+
+    router.push(`/user/create_review?meetupId=${meetUpId}`);
+  };
+
+  return (
+    <SolidButton
+      className='mt-6'
+      onClick={(e) => handleClickReview(e, meetupId as number)}
+    >
+      리뷰 작성
+    </SolidButton>
+  );
+}

리뷰: 위에서 언급한 부분입니다~
@@ -1,7 +1,17 @@
 package store;
 
+import store.controller.StoreController;
+import store.global.util.FileUtil;
+import store.view.InputView;
+import store.view.OutputView;
+
 public class Application {
     public static void main(String[] args) {
-        // TODO: 프로그램 구현
+        FileUtil fileInputView = new FileUtil();
+        StoreController storeController = new StoreController(
+                new OutputView(),
+                new InputView(fileInputView)
+        );
+        storeController.run();
     }
 }

리뷰: FileUtil 이라는 작명이 직관적이어서 좋네요! 저는 StoreInitializer로 했는데 뭔가 안 와닿아서 아쉬웠어요
@@ -0,0 +1,39 @@
+package store.domain.product;
+
+import static store.global.constant.ErrorMessage.INVALID_PRICE_NUMERIC;
+import static store.global.constant.ErrorMessage.INVALID_PRICE_OUT_OF_RANGE;
+import static store.global.validation.CommonValidator.validateNotNumeric;
+
+import java.text.DecimalFormat;
+
+public class Price {
+
+    private static final DecimalFormat DECIMAL_FORMAT = new DecimalFormat("###,###");
+    private static final int MINIMUM_PRICE = 1;
+    private static final int MAXIMUM_PRICE = 1_000_000;
+    private static final String UNIT = "원";
+
+    private final int price;
+
+    public Price(final String inputPrice) {
+        validateNotNumeric(inputPrice, INVALID_PRICE_NUMERIC);
+        int price = Integer.parseInt(inputPrice);
+        validateRange(price);
+        this.price = price;
+    }
+
+    public int getPrice() {
+        return price;
+    }
+
+    @Override
+    public String toString() {
+        return DECIMAL_FORMAT.format(price) + UNIT;
+    }
+
+    private void validateRange(final int price) {
+        if (price < MINIMUM_PRICE || price > MAXIMUM_PRICE) {
+            throw new IllegalArgumentException(INVALID_PRICE_OUT_OF_RANGE.getMessage());
+        }
+    }
+}

리뷰: Price객체의 행위가 따로 없는 것 같아 보이는데, 단순히 "가격"이라는 도메인 제약 검증과, 관련된 상수들을 묶어두기 위해 사용하신 건가요?