ewhk9887/korean_code_reviews_from_github huggingface dataset:
@@ -1,19 +1,28 @@
+import './InputBox.css';
+
 type InputBoxProps = {
+  type: string;
   id: string;
-  checked: boolean;
-  onChange: any;
+  value: string;
+  onChange: (e : React.FormEvent<HTMLInputElement>) => void;
+  label: string;
+  required: boolean;
 }
 
-const InputBox = ({id, checked, onChange}: InputBoxProps) => {
+const InputBox = ({type, id, value, onChange, label, required}: InputBoxProps) => {
   return (
-    <>
+    <div className='inputBox'>
+      <div className={required ? 'inputBox__title inputBox__title__required' : 'inputBox__title'}>
+        {label}{required && '*'}
+      </div>
       <input
-        type="checkbox"
+        className='inputBox__input'
+        type={type}
         id={id}
-        checked={checked}
+        value={value}
         onChange={onChange}
         />
-    </>
+    </div>
   )
 }
 

리뷰: 컴포넌트 이름이 InputBox니까 checkbox용 input 요소를 위한 컴포넌트 같은데, type props를 추가하고 checked 속성 대신 value 속성으로 변경하신 이유가 있을까요?
@@ -1,19 +1,28 @@
+import './InputBox.css';
+
 type InputBoxProps = {
+  type: string;
   id: string;
-  checked: boolean;
-  onChange: any;
+  value: string;
+  onChange: (e : React.FormEvent<HTMLInputElement>) => void;
+  label: string;
+  required: boolean;
 }
 
-const InputBox = ({id, checked, onChange}: InputBoxProps) => {
+const InputBox = ({type, id, value, onChange, label, required}: InputBoxProps) => {
   return (
-    <>
+    <div className='inputBox'>
+      <div className={required ? 'inputBox__title inputBox__title__required' : 'inputBox__title'}>
+        {label}{required && '*'}
+      </div>
       <input
-        type="checkbox"
+        className='inputBox__input'
+        type={type}
         id={id}
-        checked={checked}
+        value={value}
         onChange={onChange}
         />
-    </>
+    </div>
   )
 }
 

리뷰: 아! 밑에 InputCheckBox가 따로 있고 원래의 Input이 InputBox로 변경된거였군요. 저는 수정 전 그대로 Input과 InputCheckBox가 더 나아보여요..!
@@ -0,0 +1,28 @@
+import './InputCheckBox.css';
+
+type InputCheckBoxProps = {
+  id: string;
+  checked: boolean;
+  onChange: (e : React.FormEvent<HTMLInputElement>) => void;
+  label?: string;
+  required?: boolean;
+}
+
+const InputCheckBox = ({id, checked, onChange, label, required}: InputCheckBoxProps) => {
+  return (
+    <div className='inputCheckBox'>
+      <input
+        className='inputCheckBox__input'
+        type="checkbox"
+        id={id}
+        checked={checked}
+        onChange={onChange}
+        />
+      <label htmlFor={id} className={required ? 'inputCheckBox__title inputCheckBox__title__required' : 'inputCheckBox__title'}>
+        {label}{required && '*'}
+      </label>
+    </div>
+  )
+}
+
+export default InputCheckBox;
\ No newline at end of file

리뷰: label의 className을 결정하는 부분이 inputBox와 inputCheckBox에서 반복됩니다. 이 부분을 필수 className과 플래그에 따라 추가여부가 정해지는 className, 그리고 플래그를 인자로 받아서 클래스 네임을 리턴해주는 함수로 바꿔보면 어떨까요?

```suggestion
const InputCheckBox = ({id, checked, onChange, label, required}: InputCheckBoxProps) => {
  const labelClassName = getClassNameByRequired('inputCheckBox__title', 'inputCheckBox__title__required', required);
  return (
    <div className='inputCheckBox'>
      <input
        className='inputCheckBox__input'
        type="checkbox"
        id={id}
        checked={checked}
        onChange={onChange}
        />
      <label htmlFor={id} className={labelClassName}>
        {label}{required && '*'}
      </label>
    </div>
  )
}
```
```js
const getClassNameByRequired = (core:string, suffix:string, required:boolean) => {
  return (required) ?  `${core} ${suffix}` : `${core}`
}
```
@@ -0,0 +1,65 @@
+
+package basball.view;
+
+import basball.baseball.model.GameStatus;
+
+import java.util.Arrays;
+import java.util.Scanner;
+import java.util.regex.Pattern;
+
+public final class InputView {
+
+    private static final String INPUT_NUMBER_MESSAGE = "숫자를 입력해 주세요 : ";
+    private static final Scanner sc = new Scanner(System.in);
+
+    public int[] getInputNumber() {
+        System.out.print(INPUT_NUMBER_MESSAGE);
+        String next = sc.next().trim();
+
+        Validator.hasThreeNumber(next);
+
+        return getNumberArray(next);
+    }
+
+    private int[] getNumberArray(String next) {
+        return Arrays.stream(next.split("")).mapToInt(value -> Integer.parseInt(value)).toArray();
+    }
+
+    public GameStatus getRestartInput() {
+        String answer = sc.next();
+        Validator.validateAnswer(answer);
+
+        if (answer.equals("1")) {
+            return GameStatus.RUNNING;
+        }
+        return GameStatus.END;
+    }
+
+    public static class Validator {
+        private static final Pattern PATTERN = Pattern.compile("[1-9]{3}");
+
+        public static void hasThreeNumber(String next) {
+
+            if (!PATTERN.matcher(next).matches()) {
+                throw new IllegalArgumentException("서로 다른 3자리 숫자만 입력 해주세요");
+            }
+        }
+
+        public static void isDifferentNumbers(int[] numbers) {
+
+            if (isSameNumber(numbers[0], numbers[1]) || isSameNumber(numbers[0], numbers[2]) || isSameNumber(numbers[1], numbers[2])) {
+                throw new IllegalArgumentException("서로 다른 수를 입력해주세요");
+            }
+        }
+
+        private static boolean isSameNumber(int number1, int number2) {
+            return number1 == number2;
+        }
+
+        public static void validateAnswer(String answer) {
+            if (!answer.equals("1") && !answer.equals("2")) {
+                throw new IllegalArgumentException("1 또는 2만 입력 해 주세요");
+            }
+        }
+    }
+}

리뷰: (nit) Inner class로 두실거면 Validator와 내부 메소드 모두 private으로 하는게 좋겠어요.
@@ -0,0 +1,65 @@
+
+package basball.view;
+
+import basball.baseball.model.GameStatus;
+
+import java.util.Arrays;
+import java.util.Scanner;
+import java.util.regex.Pattern;
+
+public final class InputView {
+
+    private static final String INPUT_NUMBER_MESSAGE = "숫자를 입력해 주세요 : ";
+    private static final Scanner sc = new Scanner(System.in);
+
+    public int[] getInputNumber() {
+        System.out.print(INPUT_NUMBER_MESSAGE);
+        String next = sc.next().trim();
+
+        Validator.hasThreeNumber(next);
+
+        return getNumberArray(next);
+    }
+
+    private int[] getNumberArray(String next) {
+        return Arrays.stream(next.split("")).mapToInt(value -> Integer.parseInt(value)).toArray();
+    }
+
+    public GameStatus getRestartInput() {
+        String answer = sc.next();
+        Validator.validateAnswer(answer);
+
+        if (answer.equals("1")) {
+            return GameStatus.RUNNING;
+        }
+        return GameStatus.END;
+    }
+
+    public static class Validator {
+        private static final Pattern PATTERN = Pattern.compile("[1-9]{3}");
+
+        public static void hasThreeNumber(String next) {
+
+            if (!PATTERN.matcher(next).matches()) {
+                throw new IllegalArgumentException("서로 다른 3자리 숫자만 입력 해주세요");
+            }
+        }
+
+        public static void isDifferentNumbers(int[] numbers) {
+
+            if (isSameNumber(numbers[0], numbers[1]) || isSameNumber(numbers[0], numbers[2]) || isSameNumber(numbers[1], numbers[2])) {
+                throw new IllegalArgumentException("서로 다른 수를 입력해주세요");
+            }
+        }
+
+        private static boolean isSameNumber(int number1, int number2) {
+            return number1 == number2;
+        }
+
+        public static void validateAnswer(String answer) {
+            if (!answer.equals("1") && !answer.equals("2")) {
+                throw new IllegalArgumentException("1 또는 2만 입력 해 주세요");
+            }
+        }
+    }
+}

리뷰: 멘토님 안녕하세요 ! 답변 남겨주셔서 감사합니다 그런데 Validator 를 private 로 바꾸면 Balls 랑 InputView 테스트 같은 곳 에서는 어떻게 사용해야 하나요 ?
제 생각으로는 InputView 안에 get 메서드로 접근 메서드를 제공하는 방법 뿐인데 이렇게 되면 InputView 테스트는 그렇다 쳐도 Balls 부분에서 Validator 를 사용하기 위해 불필요하게 InputView 객체를 만들어야 하는데 맞을까요 !?

아니면 저번에 남겨주신 static class 로 바꾸라는게 이렇게 바꾸라고 하신게 아니였나요 !??
@@ -0,0 +1,65 @@
+
+package basball.view;
+
+import basball.baseball.model.GameStatus;
+
+import java.util.Arrays;
+import java.util.Scanner;
+import java.util.regex.Pattern;
+
+public final class InputView {
+
+    private static final String INPUT_NUMBER_MESSAGE = "숫자를 입력해 주세요 : ";
+    private static final Scanner sc = new Scanner(System.in);
+
+    public int[] getInputNumber() {
+        System.out.print(INPUT_NUMBER_MESSAGE);
+        String next = sc.next().trim();
+
+        Validator.hasThreeNumber(next);
+
+        return getNumberArray(next);
+    }
+
+    private int[] getNumberArray(String next) {
+        return Arrays.stream(next.split("")).mapToInt(value -> Integer.parseInt(value)).toArray();
+    }
+
+    public GameStatus getRestartInput() {
+        String answer = sc.next();
+        Validator.validateAnswer(answer);
+
+        if (answer.equals("1")) {
+            return GameStatus.RUNNING;
+        }
+        return GameStatus.END;
+    }
+
+    public static class Validator {
+        private static final Pattern PATTERN = Pattern.compile("[1-9]{3}");
+
+        public static void hasThreeNumber(String next) {
+
+            if (!PATTERN.matcher(next).matches()) {
+                throw new IllegalArgumentException("서로 다른 3자리 숫자만 입력 해주세요");
+            }
+        }
+
+        public static void isDifferentNumbers(int[] numbers) {
+
+            if (isSameNumber(numbers[0], numbers[1]) || isSameNumber(numbers[0], numbers[2]) || isSameNumber(numbers[1], numbers[2])) {
+                throw new IllegalArgumentException("서로 다른 수를 입력해주세요");
+            }
+        }
+
+        private static boolean isSameNumber(int number1, int number2) {
+            return number1 == number2;
+        }
+
+        public static void validateAnswer(String answer) {
+            if (!answer.equals("1") && !answer.equals("2")) {
+                throw new IllegalArgumentException("1 또는 2만 입력 해 주세요");
+            }
+        }
+    }
+}

리뷰: 전에 남겨드린건 static 개별 클래스로 두라는 얘기였어요 ㅎㅎ
제가 private로 하라고 말씀드린건 사실 InputView 내에서만 쓰여서 그렇게 말씀드린거였어요. 만약 Validator 자체가 밖으로 나와있을 이유가 없다면 테스트도 필요없는거겠죠. (캡슐화의 의미였어요. Validator를 가져다 쓸 데가 InputView 외에 없다면 굳이 Validator가 어떻게 구현되었는지 테스트를 어떻게 할지도 고민할 필요가 없는거겠죠)
하지만 Validator가 다른데서도 쓰일거라면 개별 static 클래스로 빼두는게 더 좋을 것 같아요.
@@ -0,0 +1,65 @@
+
+package basball.view;
+
+import basball.baseball.model.GameStatus;
+
+import java.util.Arrays;
+import java.util.Scanner;
+import java.util.regex.Pattern;
+
+public final class InputView {
+
+    private static final String INPUT_NUMBER_MESSAGE = "숫자를 입력해 주세요 : ";
+    private static final Scanner sc = new Scanner(System.in);
+
+    public int[] getInputNumber() {
+        System.out.print(INPUT_NUMBER_MESSAGE);
+        String next = sc.next().trim();
+
+        Validator.hasThreeNumber(next);
+
+        return getNumberArray(next);
+    }
+
+    private int[] getNumberArray(String next) {
+        return Arrays.stream(next.split("")).mapToInt(value -> Integer.parseInt(value)).toArray();
+    }
+
+    public GameStatus getRestartInput() {
+        String answer = sc.next();
+        Validator.validateAnswer(answer);
+
+        if (answer.equals("1")) {
+            return GameStatus.RUNNING;
+        }
+        return GameStatus.END;
+    }
+
+    public static class Validator {
+        private static final Pattern PATTERN = Pattern.compile("[1-9]{3}");
+
+        public static void hasThreeNumber(String next) {
+
+            if (!PATTERN.matcher(next).matches()) {
+                throw new IllegalArgumentException("서로 다른 3자리 숫자만 입력 해주세요");
+            }
+        }
+
+        public static void isDifferentNumbers(int[] numbers) {
+
+            if (isSameNumber(numbers[0], numbers[1]) || isSameNumber(numbers[0], numbers[2]) || isSameNumber(numbers[1], numbers[2])) {
+                throw new IllegalArgumentException("서로 다른 수를 입력해주세요");
+            }
+        }
+
+        private static boolean isSameNumber(int number1, int number2) {
+            return number1 == number2;
+        }
+
+        public static void validateAnswer(String answer) {
+            if (!answer.equals("1") && !answer.equals("2")) {
+                throw new IllegalArgumentException("1 또는 2만 입력 해 주세요");
+            }
+        }
+    }
+}

리뷰: 안녕하세요 멘토님 ! `개별 static 클래스` 라는 말이 잘 이해가 안되는데 혹시  `Math` 클래스 처럼 `final 과 private 생성자` 를 통해 생성을 막고 단지 `Utils` 클래스 처럼 사용 을 의미 하시는건가요 ? ?아니면 

외부는 `public 클래스` 로 만들고 내부에 `public static class` 로 만들라고 하시는 건가요 ??

< final 사용 >
```
public final class Validator {

    private Validator() {

    }

    private static final Pattern PATTERN = Pattern.compile("[1-9]{3}");

    public static void hasThreeNumber(String next) {

        if (!PATTERN.matcher(next).matches()) {
            throw new IllegalArgumentException("서로 다른 3자리 숫자만 입력 해주세요");
        }
    }

    ...
}
```

< static 사용 >

```
public class Validators {
    
    public static class Validator {

        private static final Pattern PATTERN = Pattern.compile("[1-9]{3}");
    
        public static void hasThreeNumber(String next) {
    
            if (!PATTERN.matcher(next).matches()) {
                throw new IllegalArgumentException("서로 다른 3자리 숫자만 입력 해주세요");
            }
        }
    ...

  }
}
    
```
@@ -0,0 +1,65 @@
+
+package basball.view;
+
+import basball.baseball.model.GameStatus;
+
+import java.util.Arrays;
+import java.util.Scanner;
+import java.util.regex.Pattern;
+
+public final class InputView {
+
+    private static final String INPUT_NUMBER_MESSAGE = "숫자를 입력해 주세요 : ";
+    private static final Scanner sc = new Scanner(System.in);
+
+    public int[] getInputNumber() {
+        System.out.print(INPUT_NUMBER_MESSAGE);
+        String next = sc.next().trim();
+
+        Validator.hasThreeNumber(next);
+
+        return getNumberArray(next);
+    }
+
+    private int[] getNumberArray(String next) {
+        return Arrays.stream(next.split("")).mapToInt(value -> Integer.parseInt(value)).toArray();
+    }
+
+    public GameStatus getRestartInput() {
+        String answer = sc.next();
+        Validator.validateAnswer(answer);
+
+        if (answer.equals("1")) {
+            return GameStatus.RUNNING;
+        }
+        return GameStatus.END;
+    }
+
+    public static class Validator {
+        private static final Pattern PATTERN = Pattern.compile("[1-9]{3}");
+
+        public static void hasThreeNumber(String next) {
+
+            if (!PATTERN.matcher(next).matches()) {
+                throw new IllegalArgumentException("서로 다른 3자리 숫자만 입력 해주세요");
+            }
+        }
+
+        public static void isDifferentNumbers(int[] numbers) {
+
+            if (isSameNumber(numbers[0], numbers[1]) || isSameNumber(numbers[0], numbers[2]) || isSameNumber(numbers[1], numbers[2])) {
+                throw new IllegalArgumentException("서로 다른 수를 입력해주세요");
+            }
+        }
+
+        private static boolean isSameNumber(int number1, int number2) {
+            return number1 == number2;
+        }
+
+        public static void validateAnswer(String answer) {
+            if (!answer.equals("1") && !answer.equals("2")) {
+                throw new IllegalArgumentException("1 또는 2만 입력 해 주세요");
+            }
+        }
+    }
+}

리뷰: 앗 음..제가 말씀드린걸 너무 크게 생각하신 것 같군요..^^;
제가 말씀드린건 단순히 Validator.java 파일을 하나 더 만들어서 거기다 넣는다는 얘기였어요.
그러면 굳이 InputView에 종속적인 namespace를 가질 필요가 없으니까요 ㅎㅎ
@@ -0,0 +1,105 @@
+import { Helmet } from 'react-helmet-async';
+import titles from '../routes/titles';
+import styled from 'styled-components';
+import MainCard from './components/MainCard';
+import Tag from './components/Tag';
+import Card from './components/Card';
+import { useQuery } from 'react-query';
+import { getArticles } from '../api/ArticlesController';
+import { useState } from 'react';
+import { useNavigate } from 'react-router-dom';
+
+const BlogWrapper = styled.div`
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+  width: 100%;
+  margin: 0px 120px 0px 120px;
+`;
+
+const TagBox = styled.div`
+  display: flex;
+  gap: 10px;
+  margin-bottom: 44px;
+  margin: 0px 48px;
+`;
+
+const CardBox = styled.div`
+  display: grid;
+  grid-template-columns: repeat(2, 1fr);
+  row-gap: 3rem;
+  column-gap: 6rem;
+  margin: 44px 48px 0px 48px;
+
+  a {
+    display: flex;
+    justify-items: center;
+    justify-content: center;
+  }
+`;
+
+function BlogPage() {
+  const navigate = useNavigate();
+
+  const {
+    data: cardData,
+    isLoading,
+    error,
+  } = useQuery({
+    queryKey: ['articles'],
+    queryFn: getArticles,
+  });
+
+  const [activeTag, setActiveTag] = useState('전체');
+
+  if (isLoading) return 'Loading...';
+
+  if (error) return 'An error has occurred: ' + error;
+
+  if (!cardData || cardData.length === 0) return 'No data available';
+
+  const mainCardData = cardData[0];
+
+  const categories = [
+    { id: 0, text: '전체' },
+    { id: 1, text: '문화' },
+    { id: 2, text: '서비스' },
+    { id: 3, text: '커리어' },
+  ];
+
+  return (
+    <>
+      <Helmet>
+        <title>{titles.blog}</title>
+      </Helmet>
+      <BlogWrapper>
+        <MainCard
+          mainCard={mainCardData}
+          onClick={()=>navigate(`/blog/${mainCardData.articleId}`)}
+        />
+        <TagBox>
+          {categories.map((category) => (
+            <Tag
+              key={category.id}
+              text={category.text}
+              onClick={() => setActiveTag(category.text)}
+              isActive={category.text === activeTag}
+            />
+          ))}
+        </TagBox>
+        <CardBox>
+          {cardData.map((card) => (
+            <Card 
+              key={card.articleId} 
+              card={card} 
+              isActive={card.categories[0] === activeTag || '전체' === activeTag }
+              onClick={()=>navigate(`/blog/${card.articleId}`)}
+            />
+          ))}
+        </CardBox>
+      </BlogWrapper>
+    </>
+  );
+}
+
+export default BlogPage;

리뷰: P4: margin이랑 margin-bottom 같이 쓴 이유가 있을까요? 하나로 통일하면 될 것 같아요.
@@ -0,0 +1,105 @@
+import { Helmet } from 'react-helmet-async';
+import titles from '../routes/titles';
+import styled from 'styled-components';
+import MainCard from './components/MainCard';
+import Tag from './components/Tag';
+import Card from './components/Card';
+import { useQuery } from 'react-query';
+import { getArticles } from '../api/ArticlesController';
+import { useState } from 'react';
+import { useNavigate } from 'react-router-dom';
+
+const BlogWrapper = styled.div`
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+  width: 100%;
+  margin: 0px 120px 0px 120px;
+`;
+
+const TagBox = styled.div`
+  display: flex;
+  gap: 10px;
+  margin-bottom: 44px;
+  margin: 0px 48px;
+`;
+
+const CardBox = styled.div`
+  display: grid;
+  grid-template-columns: repeat(2, 1fr);
+  row-gap: 3rem;
+  column-gap: 6rem;
+  margin: 44px 48px 0px 48px;
+
+  a {
+    display: flex;
+    justify-items: center;
+    justify-content: center;
+  }
+`;
+
+function BlogPage() {
+  const navigate = useNavigate();
+
+  const {
+    data: cardData,
+    isLoading,
+    error,
+  } = useQuery({
+    queryKey: ['articles'],
+    queryFn: getArticles,
+  });
+
+  const [activeTag, setActiveTag] = useState('전체');
+
+  if (isLoading) return 'Loading...';
+
+  if (error) return 'An error has occurred: ' + error;
+
+  if (!cardData || cardData.length === 0) return 'No data available';
+
+  const mainCardData = cardData[0];
+
+  const categories = [
+    { id: 0, text: '전체' },
+    { id: 1, text: '문화' },
+    { id: 2, text: '서비스' },
+    { id: 3, text: '커리어' },
+  ];
+
+  return (
+    <>
+      <Helmet>
+        <title>{titles.blog}</title>
+      </Helmet>
+      <BlogWrapper>
+        <MainCard
+          mainCard={mainCardData}
+          onClick={()=>navigate(`/blog/${mainCardData.articleId}`)}
+        />
+        <TagBox>
+          {categories.map((category) => (
+            <Tag
+              key={category.id}
+              text={category.text}
+              onClick={() => setActiveTag(category.text)}
+              isActive={category.text === activeTag}
+            />
+          ))}
+        </TagBox>
+        <CardBox>
+          {cardData.map((card) => (
+            <Card 
+              key={card.articleId} 
+              card={card} 
+              isActive={card.categories[0] === activeTag || '전체' === activeTag }
+              onClick={()=>navigate(`/blog/${card.articleId}`)}
+            />
+          ))}
+        </CardBox>
+      </BlogWrapper>
+    </>
+  );
+}
+
+export default BlogPage;

리뷰: P1: suspense로 옮기는 것 공부

https://blog.jbee.io/react/React%EC%97%90%EC%84%9C+%EC%84%A0%EC%96%B8%EC%A0%81%EC%9C%BC%EB%A1%9C+%EB%B9%84%EB%8F%99%EA%B8%B0+%EB%8B%A4%EB%A3%A8%EA%B8%B0
@@ -0,0 +1,32 @@
+import styled from 'styled-components';
+
+const TagWrapper = styled.div<{ $isActive: boolean }>`
+  border-radius: 40px;
+  border: none;
+  padding: 14px 20px;
+  font-size: 18px;
+  cursor: pointer;
+  line-height: 22px;
+  color: ${(props) => (props.$isActive ? '#ffffff' : '#868b94')};
+  background-color: ${(props) => (props.$isActive ? '#212124' : '#f2f3f6')};
+  font-weight: ${(props) => (props.$isActive ? '600' : '400')};
+  &:hover {
+    background-color: #212124;
+    color: #ffffff;
+    font-weight: 600;
+  }
+`;
+
+interface TagProps {
+  text: string;
+  isActive: boolean;
+  onClick: () => void;
+};
+
+export default function Tag({ isActive, onClick, text }: TagProps) {
+  return (
+    <TagWrapper $isActive={isActive} onClick={onClick}>
+      {text}
+    </TagWrapper>
+  );
+}

리뷰: P3: 이후에 css variables로 빼서 관리하면 좋을 것 같아요.
@@ -0,0 +1,31 @@
+class VisitDateError {
+  #notInputDate(visitDate) {
+    if (visitDate.length === 0) {
+      throw new Error('[ERROR] 방문 날짜를 입력해 주세요.');
+    }
+  }
+
+  #dateNotNum(visitDate) {
+    if (
+      Number.isNaN(Number(visitDate)) ||
+      Number.isInteger(Number(visitDate)) === false ||
+      visitDate.includes('.')
+    ) {
+      throw new Error('[ERROR] 유효하지 않은 날짜입니다. 다시 입력해 주세요.');
+    }
+  }
+
+  #dateNotInRange(visitDate) {
+    if (Number(visitDate) < 1 || Number(visitDate) > 31) {
+      throw new Error('[ERROR] 유효하지 않은 날짜입니다. 다시 입력해 주세요.');
+    }
+  }
+
+  valid(visitDate) {
+    this.#notInputDate(visitDate);
+    this.#dateNotNum(visitDate);
+    this.#dateNotInRange(visitDate);
+  }
+}
+
+export default VisitDateError;

리뷰: 해당 부분 정규 표현식으로 하면 if문을 하나로 줄일 수 있을 것 같습니다 !
@@ -0,0 +1,31 @@
+class VisitDateError {
+  #notInputDate(visitDate) {
+    if (visitDate.length === 0) {
+      throw new Error('[ERROR] 방문 날짜를 입력해 주세요.');
+    }
+  }
+
+  #dateNotNum(visitDate) {
+    if (
+      Number.isNaN(Number(visitDate)) ||
+      Number.isInteger(Number(visitDate)) === false ||
+      visitDate.includes('.')
+    ) {
+      throw new Error('[ERROR] 유효하지 않은 날짜입니다. 다시 입력해 주세요.');
+    }
+  }
+
+  #dateNotInRange(visitDate) {
+    if (Number(visitDate) < 1 || Number(visitDate) > 31) {
+      throw new Error('[ERROR] 유효하지 않은 날짜입니다. 다시 입력해 주세요.');
+    }
+  }
+
+  valid(visitDate) {
+    this.#notInputDate(visitDate);
+    this.#dateNotNum(visitDate);
+    this.#dateNotInRange(visitDate);
+  }
+}
+
+export default VisitDateError;

리뷰: 해당 문구도 constant에 따로 error.js를 생성하셔서 하시면 좋을 것 같습니다 !