ewhk9887/korean_code_reviews_from_github huggingface dataset:
@@ -2,12 +2,11 @@ import styled from '@emotion/styled';
 import { useRouter } from 'next/router';
 import React, { useCallback, useMemo } from 'react';
 import { FieldValues } from 'react-hook-form';
-import { useMutation } from 'react-query';
 import { useRecoilValue } from 'recoil';
 
 import { useGetFlavors } from '@/apis/flavors';
-import { ICreateReviewPayload, postReview } from '@/apis/review';
-import { uploadImage } from '@/apis/upload/uploadImage';
+import { ICreateReviewPayload, useCreateReviewMutation } from '@/apis/review';
+import { useUploadImageMutation } from '@/apis/upload/uploadImage';
 import BottomFloatingButtonArea from '@/components/BottomFloatingButtonArea';
 import { BOTTOM_FLOATING_BUTTON_AREA_HEIGHT } from '@/components/BottomFloatingButtonArea/BottomFloatingButtonArea';
 import Button, { ButtonCount } from '@/components/Button';
@@ -39,8 +38,8 @@ const ReviewDetailContainer: React.FC<RecordThirdStepContainerProps> = ({
   const router = useRouter();
   const reviewForm = useRecoilValue($reviewForm);
   const { data: flavors } = useGetFlavors();
-  const { mutateAsync: uploadImageMutation } = useMutation(uploadImage);
-  const { mutateAsync: createReviewMutation } = useMutation(postReview);
+  const { uploadImage } = useUploadImageMutation();
+  const { createReview } = useCreateReviewMutation();
   // const { mutateAsync: updateRecordMutation } = useMutation(updateRecord, {
   //   onSuccess: () => {
   //     router.back();
@@ -60,25 +59,25 @@ const ReviewDetailContainer: React.FC<RecordThirdStepContainerProps> = ({
 
   const handleImageUpload = useCallback(
     async (image: FormData) => {
-      const { imageUrl } = await uploadImageMutation(image);
+      const { imageUrl } = await uploadImage(image);
 
       return imageUrl;
     },
-    [uploadImageMutation],
+    [uploadImage],
   );
 
   const handleCreateSubmit = useCallback(
     (data: FieldValues) => {
-      createReviewMutation(
+      createReview(
         {
           ...reviewForm,
           ...data,
-          beerId: beer.id,
+          beerId: beer?.id,
         } as ICreateReviewPayload,
         { onSuccess: (_data) => router.push(`/record/ticket/${_data.id}?type=${NEW_TYPE}`) },
       );
     },
-    [createReviewMutation, reviewForm, beer.id, router],
+    [createReview, reviewForm, beer?.id, router],
   );
 
   // const handleUpdateSubmit = useCallback(
@@ -112,7 +111,7 @@ const ReviewDetailContainer: React.FC<RecordThirdStepContainerProps> = ({
     >
       <StyledWrapper>
         <h2>{'당신만의 맥주 이야기도 들려주세요'}</h2>
-        <p className="body-1">{beer.korName}</p>
+        <p className="body-1">{beer?.korName}</p>
         <ImageUploadField
           name="imageUrl"
           beer={beer}

리뷰: @hy57in 

mutation도 한 번 래핑해서 사용하는건 어떠신가요?

```typescript
export const useCreateReviewMutation = () => {
  const cache = useQueryClient();

  const { mutateAsync: createReviewMutation, ...rest } = useMutation(createReview, {
    onSuccess: async () => {
      await cache.invalidateQueries(queryKeyFactory.GET_REVIEW);
    },
  });

  return {
    createReview: createReviewMutation,
    ...rest,
  };
};
```
@@ -2,12 +2,11 @@ import styled from '@emotion/styled';
 import { useRouter } from 'next/router';
 import React, { useCallback, useMemo } from 'react';
 import { FieldValues } from 'react-hook-form';
-import { useMutation } from 'react-query';
 import { useRecoilValue } from 'recoil';
 
 import { useGetFlavors } from '@/apis/flavors';
-import { ICreateReviewPayload, postReview } from '@/apis/review';
-import { uploadImage } from '@/apis/upload/uploadImage';
+import { ICreateReviewPayload, useCreateReviewMutation } from '@/apis/review';
+import { useUploadImageMutation } from '@/apis/upload/uploadImage';
 import BottomFloatingButtonArea from '@/components/BottomFloatingButtonArea';
 import { BOTTOM_FLOATING_BUTTON_AREA_HEIGHT } from '@/components/BottomFloatingButtonArea/BottomFloatingButtonArea';
 import Button, { ButtonCount } from '@/components/Button';
@@ -39,8 +38,8 @@ const ReviewDetailContainer: React.FC<RecordThirdStepContainerProps> = ({
   const router = useRouter();
   const reviewForm = useRecoilValue($reviewForm);
   const { data: flavors } = useGetFlavors();
-  const { mutateAsync: uploadImageMutation } = useMutation(uploadImage);
-  const { mutateAsync: createReviewMutation } = useMutation(postReview);
+  const { uploadImage } = useUploadImageMutation();
+  const { createReview } = useCreateReviewMutation();
   // const { mutateAsync: updateRecordMutation } = useMutation(updateRecord, {
   //   onSuccess: () => {
   //     router.back();
@@ -60,25 +59,25 @@ const ReviewDetailContainer: React.FC<RecordThirdStepContainerProps> = ({
 
   const handleImageUpload = useCallback(
     async (image: FormData) => {
-      const { imageUrl } = await uploadImageMutation(image);
+      const { imageUrl } = await uploadImage(image);
 
       return imageUrl;
     },
-    [uploadImageMutation],
+    [uploadImage],
   );
 
   const handleCreateSubmit = useCallback(
     (data: FieldValues) => {
-      createReviewMutation(
+      createReview(
         {
           ...reviewForm,
           ...data,
-          beerId: beer.id,
+          beerId: beer?.id,
         } as ICreateReviewPayload,
         { onSuccess: (_data) => router.push(`/record/ticket/${_data.id}?type=${NEW_TYPE}`) },
       );
     },
-    [createReviewMutation, reviewForm, beer.id, router],
+    [createReview, reviewForm, beer?.id, router],
   );
 
   // const handleUpdateSubmit = useCallback(
@@ -112,7 +111,7 @@ const ReviewDetailContainer: React.FC<RecordThirdStepContainerProps> = ({
     >
       <StyledWrapper>
         <h2>{'당신만의 맥주 이야기도 들려주세요'}</h2>
-        <p className="body-1">{beer.korName}</p>
+        <p className="body-1">{beer?.korName}</p>
         <ImageUploadField
           name="imageUrl"
           beer={beer}

리뷰: @hy57in 

`useGetReviewsByBeer` 도 queryKey를 queryKeyFactory에서 가져오는 방식으로 변경하는거 어떨까요!?

ref
- https://github.com/beerair/beerair-web/blob/8a3fb178139646a813eac410820e31846c3460c3/src/commons/queryKeyFactory.ts
@@ -2,12 +2,11 @@ import styled from '@emotion/styled';
 import { useRouter } from 'next/router';
 import React, { useCallback, useMemo } from 'react';
 import { FieldValues } from 'react-hook-form';
-import { useMutation } from 'react-query';
 import { useRecoilValue } from 'recoil';
 
 import { useGetFlavors } from '@/apis/flavors';
-import { ICreateReviewPayload, postReview } from '@/apis/review';
-import { uploadImage } from '@/apis/upload/uploadImage';
+import { ICreateReviewPayload, useCreateReviewMutation } from '@/apis/review';
+import { useUploadImageMutation } from '@/apis/upload/uploadImage';
 import BottomFloatingButtonArea from '@/components/BottomFloatingButtonArea';
 import { BOTTOM_FLOATING_BUTTON_AREA_HEIGHT } from '@/components/BottomFloatingButtonArea/BottomFloatingButtonArea';
 import Button, { ButtonCount } from '@/components/Button';
@@ -39,8 +38,8 @@ const ReviewDetailContainer: React.FC<RecordThirdStepContainerProps> = ({
   const router = useRouter();
   const reviewForm = useRecoilValue($reviewForm);
   const { data: flavors } = useGetFlavors();
-  const { mutateAsync: uploadImageMutation } = useMutation(uploadImage);
-  const { mutateAsync: createReviewMutation } = useMutation(postReview);
+  const { uploadImage } = useUploadImageMutation();
+  const { createReview } = useCreateReviewMutation();
   // const { mutateAsync: updateRecordMutation } = useMutation(updateRecord, {
   //   onSuccess: () => {
   //     router.back();
@@ -60,25 +59,25 @@ const ReviewDetailContainer: React.FC<RecordThirdStepContainerProps> = ({
 
   const handleImageUpload = useCallback(
     async (image: FormData) => {
-      const { imageUrl } = await uploadImageMutation(image);
+      const { imageUrl } = await uploadImage(image);
 
       return imageUrl;
     },
-    [uploadImageMutation],
+    [uploadImage],
   );
 
   const handleCreateSubmit = useCallback(
     (data: FieldValues) => {
-      createReviewMutation(
+      createReview(
         {
           ...reviewForm,
           ...data,
-          beerId: beer.id,
+          beerId: beer?.id,
         } as ICreateReviewPayload,
         { onSuccess: (_data) => router.push(`/record/ticket/${_data.id}?type=${NEW_TYPE}`) },
       );
     },
-    [createReviewMutation, reviewForm, beer.id, router],
+    [createReview, reviewForm, beer?.id, router],
   );
 
   // const handleUpdateSubmit = useCallback(
@@ -112,7 +111,7 @@ const ReviewDetailContainer: React.FC<RecordThirdStepContainerProps> = ({
     >
       <StyledWrapper>
         <h2>{'당신만의 맥주 이야기도 들려주세요'}</h2>
-        <p className="body-1">{beer.korName}</p>
+        <p className="body-1">{beer?.korName}</p>
         <ImageUploadField
           name="imageUrl"
           beer={beer}

리뷰: [bf34ee2](https://github.com/beerair/beerair-web/pull/162/commits/bf34ee2ba98545a4e64603f0bdad6d5195f50a47)

만들어두신 queryKeyFactory 를 잊고 있었네요. 해당부분 수정했습니다!

mutaion도 래핑해서 수정했습니다!
@@ -0,0 +1,45 @@
+import { useMutation, useQueryClient } from 'react-query';
+
+import request from '@/commons/axios';
+import { queryKeyFactory } from '@/commons/queryKeyFactory';
+import { IBaseResponse, IReview } from '@/types';
+
+/**
+ * 리뷰 등록
+ */
+
+export interface ICreateReviewResponseData extends IBaseResponse<IReview> {}
+
+export interface ICreateReviewPayload {
+  beerId: number;
+  content: string;
+  feelStatus: number;
+  imageUrl: string;
+  isPublic: boolean;
+  flavorIds: number[];
+}
+
+export const createReview = async (payload: ICreateReviewPayload) => {
+  const res = await request<ICreateReviewResponseData>({
+    method: 'post',
+    url: '/api/v1/reviews',
+    data: { payload },
+  });
+
+  return res.data;
+};
+
+export const useCreateReviewMutation = () => {
+  const cache = useQueryClient();
+
+  const { mutateAsync: createReviewMutation, ...rest } = useMutation(createReview, {
+    onSuccess: async () => {
+      await cache.invalidateQueries(queryKeyFactory.GET_REVIEW());
+    },
+  });
+
+  return {
+    createReview: createReviewMutation,
+    ...rest,
+  };
+};

리뷰: @hy57in 

리뷰 생성되고 호출되는 success 함수 내부에서는 리뷰 리스트를 invalidate 시켜야하지 않나요..?
@@ -0,0 +1,45 @@
+import { useMutation, useQueryClient } from 'react-query';
+
+import request from '@/commons/axios';
+import { queryKeyFactory } from '@/commons/queryKeyFactory';
+import { IBaseResponse, IReview } from '@/types';
+
+/**
+ * 리뷰 등록
+ */
+
+export interface ICreateReviewResponseData extends IBaseResponse<IReview> {}
+
+export interface ICreateReviewPayload {
+  beerId: number;
+  content: string;
+  feelStatus: number;
+  imageUrl: string;
+  isPublic: boolean;
+  flavorIds: number[];
+}
+
+export const createReview = async (payload: ICreateReviewPayload) => {
+  const res = await request<ICreateReviewResponseData>({
+    method: 'post',
+    url: '/api/v1/reviews',
+    data: { payload },
+  });
+
+  return res.data;
+};
+
+export const useCreateReviewMutation = () => {
+  const cache = useQueryClient();
+
+  const { mutateAsync: createReviewMutation, ...rest } = useMutation(createReview, {
+    onSuccess: async () => {
+      await cache.invalidateQueries(queryKeyFactory.GET_REVIEW());
+    },
+  });
+
+  return {
+    createReview: createReviewMutation,
+    ...rest,
+  };
+};

리뷰: 아핫..! 리뷰 리스트를 invalidate 하는 것으로 수정했습니다. 리뷰 감사합니다 🫢
@@ -97,12 +97,12 @@ const BeerTicketTitle: React.FC<BeerTicketTitleProps> = ({
           {sliceAndUpperCase(beer?.country?.engName || 'non', 3)}
         </span>
         <div className="ticket-detail">
-          {`${beer?.alcohol?.toFixed(1)}%`}
+          {beer?.alcohol ? `${beer.alcohol?.toFixed(1)}%` : '-'}
           <span className="ticket-detail-split-dot">{'•'}</span>
-          {beer?.type?.korName}
+          {beer?.type?.korName ?? '-'}
         </div>
       </div>
-      <span className="ticket-beer-name">{beer?.korName}</span>
+      <span className="ticket-beer-name">{beer?.korName ?? '-'}</span>
     </StyledBeerTicketTitle>
   );
 };

리뷰: '?' 없어도 괜찮을 것 같아요!
```suggestion
          {beer?.alcohol ? `${beer.alcohol.toFixed(1)}%` : '-'}
```
@@ -5,16 +5,18 @@ export default class ReviewMapper {
   constructor(private readonly review: ReviewAllProperties) {}
 
   toDomain() {
+    if (!this.review) return null;
+
     return new Review({
       id: this.review.id,
-      writerId: this.review.writerId,
-      writer: this.review.writer,
-      ownerId: this.review.ownerId,
+      writerId: this.review?.writerId,
+      writer: this.review?.writer,
+      ownerId: this.review?.ownerId,
       rating: this.review.rating,
       content: this.review.content,
-      planId: this.review.planId,
+      planId: this.review?.planId,
       createdAt: this.review.createdAt,
-      updatedAt: this.review.updatedAt
+      updatedAt: this.review?.updatedAt
     });
   }
 }

리뷰: 저도 이렇게 했는거 같긴 한데 if문 안에서 null 값과 undefined값을 체크해서 리턴문 이후로 옵셔널로 줄 필요는 없는거 같아요
@@ -1,4 +1,5 @@
 import { IsInt, IsNotEmpty, IsPositive, IsString, IsUUID } from 'class-validator';
+import { Type } from 'class-transformer';
 
 export class CreateReviewDTO {
   @IsUUID()
@@ -18,3 +19,13 @@ export class CreateReviewDTO {
   @IsNotEmpty()
   planId: string;
 }
+
+export class GetReviewsQueryDTO {
+  @Type(() => Number)
+  @IsInt()
+  page: number = 1;
+
+  @Type(() => Number)
+  @IsInt()
+  pageSize: number = 5;
+}

리뷰: 기본값을 주려면 isOptional이 있어야되지 않나요? 테스트해서 괜찮던가요?
@@ -14,13 +14,13 @@ export interface ReviewProperties {
 
 export interface ReviewAllProperties {
   id?: string;
-  writerId: string;
+  writerId?: string;
   writer?: UserReference;
-  ownerId: string;
+  ownerId?: string;
   owner?: UserReference;
   rating: number;
   content: string;
-  planId: string;
+  planId?: string;
   plan?: PlanReference;
   createdAt?: Date;
   updatedAt?: Date;

리뷰: 플랜이 삭제되더라도 리뷰는 남아있는 프로세스인가요?
그렇다는 전제하에 저희는 논리삭제라서 planId는 옵셔널이 아니여도 될거같아요.
플랜은 옵셔널로 유지해야겠네요.
여기 createdAt과 updatedAt이 옵셔널이 아니여도 괜찮지 않나요?
@@ -5,16 +5,18 @@ export default class ReviewMapper {
   constructor(private readonly review: ReviewAllProperties) {}
 
   toDomain() {
+    if (!this.review) return null;
+
     return new Review({
       id: this.review.id,
-      writerId: this.review.writerId,
-      writer: this.review.writer,
-      ownerId: this.review.ownerId,
+      writerId: this.review?.writerId,
+      writer: this.review?.writer,
+      ownerId: this.review?.ownerId,
       rating: this.review.rating,
       content: this.review.content,
-      planId: this.review.planId,
+      planId: this.review?.planId,
       createdAt: this.review.createdAt,
-      updatedAt: this.review.updatedAt
+      updatedAt: this.review?.updatedAt
     });
   }
 }

리뷰: 리턴문에 옵셔널을 안 넣으면, 예를 들면 지금 PR에 올린 리뷰 목록 조회 때는 writer 데이터는 include를 하지 않고 매핑하니까 값이 없는데 문제 없나요???
@@ -1,4 +1,5 @@
 import { IsInt, IsNotEmpty, IsPositive, IsString, IsUUID } from 'class-validator';
+import { Type } from 'class-transformer';
 
 export class CreateReviewDTO {
   @IsUUID()
@@ -18,3 +19,13 @@ export class CreateReviewDTO {
   @IsNotEmpty()
   planId: string;
 }
+
+export class GetReviewsQueryDTO {
+  @Type(() => Number)
+  @IsInt()
+  page: number = 1;
+
+  @Type(() => Number)
+  @IsInt()
+  pageSize: number = 5;
+}

리뷰: 네 다시 테스트해봤는데 문제 없어요! 저 참고로 PlanQueryOptionDTO 참고해서 작성했습니다 ㅎㅎㅎ
@@ -14,13 +14,13 @@ export interface ReviewProperties {
 
 export interface ReviewAllProperties {
   id?: string;
-  writerId: string;
+  writerId?: string;
   writer?: UserReference;
-  ownerId: string;
+  ownerId?: string;
   owner?: UserReference;
   rating: number;
   content: string;
-  planId: string;
+  planId?: string;
   plan?: PlanReference;
   createdAt?: Date;
   updatedAt?: Date;

리뷰: 사실 이 인터페이스는 실제 프로세스에서 리뷰 객체를 이루는 데에 필요한 필드를 기준으로 만들어서 rating, content 말고는 다 옵셔널입니다.. 예를 들면 지금 만든 API의 경우 plan에 대한 내용이 필요없기 때문에 planId를 db에서 가져오지도 않거든요. 이제 만들게 될 dreamer의 내가 작성한 리뷰 목록에는 필요하고요. 옵셔널이 너무 많아서 타입 안정성이 좀 애매하다면 인터페이스를 아예 나누거나 해야할 것 같습니다.
@@ -5,16 +5,18 @@ export default class ReviewMapper {
   constructor(private readonly review: ReviewAllProperties) {}
 
   toDomain() {
+    if (!this.review) return null;
+
     return new Review({
       id: this.review.id,
-      writerId: this.review.writerId,
-      writer: this.review.writer,
-      ownerId: this.review.ownerId,
+      writerId: this.review?.writerId,
+      writer: this.review?.writer,
+      ownerId: this.review?.ownerId,
       rating: this.review.rating,
       content: this.review.content,
-      planId: this.review.planId,
+      planId: this.review?.planId,
       createdAt: this.review.createdAt,
-      updatedAt: this.review.updatedAt
+      updatedAt: this.review?.updatedAt
     });
   }
 }

리뷰: 저 옵셔널은 review가 있는지 체크하는거지 않나요?
@@ -1,4 +1,5 @@
 import { IsInt, IsNotEmpty, IsPositive, IsString, IsUUID } from 'class-validator';
+import { Type } from 'class-transformer';
 
 export class CreateReviewDTO {
   @IsUUID()
@@ -18,3 +19,13 @@ export class CreateReviewDTO {
   @IsNotEmpty()
   planId: string;
 }
+
+export class GetReviewsQueryDTO {
+  @Type(() => Number)
+  @IsInt()
+  page: number = 1;
+
+  @Type(() => Number)
+  @IsInt()
+  pageSize: number = 5;
+}

리뷰: 그렇군요,,
@@ -14,13 +14,13 @@ export interface ReviewProperties {
 
 export interface ReviewAllProperties {
   id?: string;
-  writerId: string;
+  writerId?: string;
   writer?: UserReference;
-  ownerId: string;
+  ownerId?: string;
   owner?: UserReference;
   rating: number;
   content: string;
-  planId: string;
+  planId?: string;
   plan?: PlanReference;
   createdAt?: Date;
   updatedAt?: Date;

리뷰: 이해했습니다
@@ -14,13 +14,13 @@ export interface ReviewProperties {
 
 export interface ReviewAllProperties {
   id?: string;
-  writerId: string;
+  writerId?: string;
   writer?: UserReference;
-  ownerId: string;
+  ownerId?: string;
   owner?: UserReference;
   rating: number;
   content: string;
-  planId: string;
+  planId?: string;
   plan?: PlanReference;
   createdAt?: Date;
   updatedAt?: Date;

리뷰: 나누기보단 그냥 옵셔널로 타입개수를 줄이죠
@@ -5,16 +5,18 @@ export default class ReviewMapper {
   constructor(private readonly review: ReviewAllProperties) {}
 
   toDomain() {
+    if (!this.review) return null;
+
     return new Review({
       id: this.review.id,
-      writerId: this.review.writerId,
-      writer: this.review.writer,
-      ownerId: this.review.ownerId,
+      writerId: this.review?.writerId,
+      writer: this.review?.writer,
+      ownerId: this.review?.ownerId,
       rating: this.review.rating,
       content: this.review.content,
-      planId: this.review.planId,
+      planId: this.review?.planId,
       createdAt: this.review.createdAt,
-      updatedAt: this.review.updatedAt
+      updatedAt: this.review?.updatedAt
     });
   }
 }

리뷰: this.review에 해당 값이 없으면 undefined 값을 할당한다고 합니다 (혹시나 해서 다시 찾아봤습니다 ㅎㅎㅎ)
@@ -0,0 +1,18 @@
+const categoryDao = require('../models/categoryDao');
+const error = require('../utils/error');
+
+const getCategory = async (type) => {
+  const categoryTypes = {
+    '지출' : [1, 2, 3],
+    '수입' : [4]
+  }
+
+  const categories = await categoryDao.getCategoriesByIds(categoryTypes[type]);
+  categories.map((category) => { category.type = type })
+
+  return categories;
+}
+
+module.exports = {
+  getCategory
+}
\ No newline at end of file

리뷰: ```suggestion
const getCategory = async (type) => {
  const categoryTypes = {
    '지출' : [1, 2, 4],
    '수입' : [3]
  }
  
  const categories = await categoryDao.getCategoriesByIds(categoryTypes[type]);
  categories.map((category) => { category.type = type })
  
  return categories
}

module.exports = { getCategory }
```

1. 수입/지출의 타입과 categoryId를 모두 하드코딩 해두기 보다는 타입으로 모아서 정리하시는 방법이 보다 확장성 있고, 가독성도 높을 것 같습니다.

2. category를 dao에서 하나씩 for loop으로 getCategory dao에 query를 날리시기보다는 getCategoriesByIds 이라는 dao 함수를 만드셔서 배열을 통째로 인자로 보내는 것이 좋을 것 같습니다.
그래서 dao단 SQL 쿼리문에서도 인자를 배열로 받으셔서 여러 카테고리를 한 번에 SELECT 하는 방식을 추천드립니다.
Query 문을 보내는 횟수를 3회에서 1회로 줄이기 위함입니다
@@ -0,0 +1,20 @@
+const categoryService = require('../services/categoryService');
+const error = require('../utils/error');
+
+const getCategory = async (req, res) => {
+  try {
+    const { type } = req.query;
+    if (!type) {
+      error.throwErr(400, 'KEY_ERROR');
+    }
+    const categories = await categoryService.getCategory(type);
+    return res.status(200).json({message: 'GET_SUCCESS', 'category': categories});
+  } catch (err) {
+    console.error(err);
+    return res.status(err.statusCode || 500).json({message: err.message || 'INTERNAL_SERVER_ERROR'});
+  }
+}
+
+module.exports = {
+  getCategory
+}
\ No newline at end of file

리뷰: 에러 발생시켜 보셨나요? 모든 에러가 'internal_server_error'로 날텐데, 의도하신 바가 맞을까요?
@@ -0,0 +1,18 @@
+const categoryDao = require('../models/categoryDao');
+const error = require('../utils/error');
+
+const getCategory = async (type) => {
+  const categoryTypes = {
+    '지출' : [1, 2, 3],
+    '수입' : [4]
+  }
+
+  const categories = await categoryDao.getCategoriesByIds(categoryTypes[type]);
+  categories.map((category) => { category.type = type })
+
+  return categories;
+}
+
+module.exports = {
+  getCategory
+}
\ No newline at end of file

리뷰: 3. 근본적인 질문이 하나 있는데, category가 flow_type_id를 FK로 가지고 있으면, 하드코딩을 안해도 될 것 같은데 FK 생성하시는게 좋지 않을까요? 이부분은 백엔드 팀 같이 라운지에서 저 한 번 찾아와 주세요!
@@ -0,0 +1,20 @@
+const categoryService = require('../services/categoryService');
+const error = require('../utils/error');
+
+const getCategory = async (req, res) => {
+  try {
+    const { type } = req.query;
+    if (!type) {
+      error.throwErr(400, 'KEY_ERROR');
+    }
+    const categories = await categoryService.getCategory(type);
+    return res.status(200).json({message: 'GET_SUCCESS', 'category': categories});
+  } catch (err) {
+    console.error(err);
+    return res.status(err.statusCode || 500).json({message: err.message || 'INTERNAL_SERVER_ERROR'});
+  }
+}
+
+module.exports = {
+  getCategory
+}
\ No newline at end of file

리뷰: 아 해당  **error message** 부분 수정하겠습니다. 
`=> 'INTERNAL_SERVER_ERROR' || err.message}`
@@ -0,0 +1,14 @@
+const {DataSource} = require('typeorm');
+const dotenv = require('dotenv');
+dotenv.config();
+
+const appDataSource = new DataSource({
+  type : process.env.TYPEORM_CONNECTION,
+  host: process.env.TYPEORM_HOST,
+  port: process.env.TYPEORM_PORT,
+  username: process.env.TYPEORM_USERNAME,
+  password: process.env.TYPEORM_PASSWORD,
+  database: process.env.TYPEORM_DATABASE
+});
+
+module.exports = { appDataSource }
\ No newline at end of file

리뷰: 반영하겠습니다!
@@ -1,9 +1,14 @@
+import java.util.List;
 import java.util.Scanner;
 
 public class InputView {
 
     private static final String INPUT_MONEY_MESSAGE = "구입금액을 입력해 주세요.";
     private static final String INPUT_WINNING_LOTTO_MESSAGE = "지난 주 당첨 번호를 입력해 주세요.";
+    private static final String MANUAL_LOTTO_COUNT_MESSAGE = "수동으로 구매할 로또 수를 입력해 주세요.";
+    private static final String MANUAL_LOTTO_NUMBER_MESSAGE = "수동으로 구매할 번호를 입력해 주세요.";
+
+    private static final String INPUT_BONUS_NUMBER_MESSAGE = "보너스 볼을 입력해 주세요.";
 
     private static Scanner scanner = new Scanner(System.in);
 
@@ -12,6 +17,30 @@ public static int inputMoney() {
         return Integer.parseInt(scanner.nextLine());
     }
 
+    public static int manualLottoCount() {
+        System.out.println(MANUAL_LOTTO_COUNT_MESSAGE);
+        return Integer.parseInt(scanner.nextLine());
+    }
+
+    public static List<LottoNumber> manualLotto(int i, int manualLottoCount) {
+        if (i == 0) {
+            System.out.println(MANUAL_LOTTO_NUMBER_MESSAGE);
+        }
+
+        String inputs = scanner.nextLine();
+        if (i != manualLottoCount - 1) {
+            scanner.nextLine();
+        }
+
+        return StringParsingUtils.parseToLottoNumber(inputs);
+    }
+
+    public static BonusNumber inputBonusNumber() {
+        System.out.println(INPUT_BONUS_NUMBER_MESSAGE);
+        int bonusNumber = Integer.parseInt(scanner.nextLine());
+        return new BonusNumber(new LottoNumber(bonusNumber));
+    }
+
     public static String inputWinningLottoNumber() {
         System.out.println(INPUT_WINNING_LOTTO_MESSAGE);
         return scanner.nextLine();

리뷰: 애플리케이션 실행을 시켜보니 입력을 연달아 받기 때문에 한 줄 입력을 받은 후 입력을 한 번 더 기다리고 있습니다! 어떤 의도로 사용하신 걸까요?