ewhk9887/korean_code_reviews_from_github huggingface dataset:
@@ -0,0 +1,104 @@
+"use client";
+
+import { FormProvider, useForm } from "react-hook-form";
+import SolidButton from "@/components/common/buttons/SolidButton";
+import CommonTextArea from "@/components/common/inputs/TextArea";
+import RatingInput from "@/components/create-reaview/RatingInput";
+import ReviewImageInput from "@/components/create-reaview/ReviewImageInput";
+import useCookie from "@/hooks/auths/useTokenState";
+import useReviewMutations from "@/hooks/review/useReviewMutation";
+
+interface ReviewFormData {
+  rating: number;
+  content: string;
+  image: File | null;
+  meetupId: number;
+}
+
+export default function CreateReviewForm({ meetupId }: { meetupId: string }) {
+  const token = useCookie("accessToken");
+  const { createReviewMutation } = useReviewMutations();
+  const methods = useForm<ReviewFormData>({
+    defaultValues: {
+      rating: -1,
+      content: "",
+      image: null,
+      meetupId: Number(meetupId),
+    },
+    mode: "onChange",
+  });
+
+  const { handleSubmit, setValue, watch } = methods;
+
+  const rating = watch("rating");
+  const content = watch("content");
+  const isFormValid = rating !== -1 && content.trim() !== "";
+
+  const handleRatingChange = (value: number) => {
+    setValue("rating", value);
+  };
+
+  const handleImageSelect = (image: File | null) => {
+    setValue("image", image);
+  };
+
+  const onSubmit = handleSubmit(async (data) => {
+    if (!token) return;
+
+    const formData = new FormData();
+
+    const requestData = {
+      meetupId: Number(data.meetupId),
+      rating: data.rating,
+      content: data.content,
+    };
+
+    formData.append(
+      "request",
+      new Blob([JSON.stringify(requestData)], { type: "application/json" }),
+    );
+
+    if (data.image) {
+      formData.append("image", data.image);
+    }
+
+    if (data.image === null) {
+      formData.append("image", new Blob(), "");
+    }
+
+    await createReviewMutation.mutateAsync({ formData, token });
+  });
+
+  return (
+    <FormProvider {...methods}>
+      <form onSubmit={onSubmit}>
+        <div className='flex flex-col gap-12'>
+          <RatingInput
+            value={methods.watch("rating")}
+            onChange={handleRatingChange}
+          />
+          <CommonTextArea
+            formClassName='h-40'
+            required={true}
+            name='content'
+            label='구체적인 경험을 알려주세요'
+            placeholder='모임의 장소, 환경, 진행, 구성 등 만족스러웠나요?'
+            maxLength={150}
+          />
+          <ReviewImageInput onImageSelect={handleImageSelect} />
+        </div>
+        <SolidButton
+          type='submit'
+          className='mt-10'
+          state={
+            createReviewMutation.isPending || !isFormValid || !token
+              ? "inactive"
+              : "activated"
+          }
+        >
+          {createReviewMutation.isPending ? "작성 중..." : "작성 완료"}
+        </SolidButton>
+      </form>
+    </FormProvider>
+  );
+}

리뷰: 그게 더 좋은 방식 같네요! 그렇게 수정해보겠습니다!
@@ -0,0 +1,104 @@
+"use client";
+
+import { FormProvider, useForm } from "react-hook-form";
+import SolidButton from "@/components/common/buttons/SolidButton";
+import CommonTextArea from "@/components/common/inputs/TextArea";
+import RatingInput from "@/components/create-reaview/RatingInput";
+import ReviewImageInput from "@/components/create-reaview/ReviewImageInput";
+import useCookie from "@/hooks/auths/useTokenState";
+import useReviewMutations from "@/hooks/review/useReviewMutation";
+
+interface ReviewFormData {
+  rating: number;
+  content: string;
+  image: File | null;
+  meetupId: number;
+}
+
+export default function CreateReviewForm({ meetupId }: { meetupId: string }) {
+  const token = useCookie("accessToken");
+  const { createReviewMutation } = useReviewMutations();
+  const methods = useForm<ReviewFormData>({
+    defaultValues: {
+      rating: -1,
+      content: "",
+      image: null,
+      meetupId: Number(meetupId),
+    },
+    mode: "onChange",
+  });
+
+  const { handleSubmit, setValue, watch } = methods;
+
+  const rating = watch("rating");
+  const content = watch("content");
+  const isFormValid = rating !== -1 && content.trim() !== "";
+
+  const handleRatingChange = (value: number) => {
+    setValue("rating", value);
+  };
+
+  const handleImageSelect = (image: File | null) => {
+    setValue("image", image);
+  };
+
+  const onSubmit = handleSubmit(async (data) => {
+    if (!token) return;
+
+    const formData = new FormData();
+
+    const requestData = {
+      meetupId: Number(data.meetupId),
+      rating: data.rating,
+      content: data.content,
+    };
+
+    formData.append(
+      "request",
+      new Blob([JSON.stringify(requestData)], { type: "application/json" }),
+    );
+
+    if (data.image) {
+      formData.append("image", data.image);
+    }
+
+    if (data.image === null) {
+      formData.append("image", new Blob(), "");
+    }
+
+    await createReviewMutation.mutateAsync({ formData, token });
+  });
+
+  return (
+    <FormProvider {...methods}>
+      <form onSubmit={onSubmit}>
+        <div className='flex flex-col gap-12'>
+          <RatingInput
+            value={methods.watch("rating")}
+            onChange={handleRatingChange}
+          />
+          <CommonTextArea
+            formClassName='h-40'
+            required={true}
+            name='content'
+            label='구체적인 경험을 알려주세요'
+            placeholder='모임의 장소, 환경, 진행, 구성 등 만족스러웠나요?'
+            maxLength={150}
+          />
+          <ReviewImageInput onImageSelect={handleImageSelect} />
+        </div>
+        <SolidButton
+          type='submit'
+          className='mt-10'
+          state={
+            createReviewMutation.isPending || !isFormValid || !token
+              ? "inactive"
+              : "activated"
+          }
+        >
+          {createReviewMutation.isPending ? "작성 중..." : "작성 완료"}
+        </SolidButton>
+      </form>
+    </FormProvider>
+  );
+}

리뷰: 넵 피드백 감사합니다!

말씀하신 대로 api 레벨의 토큰 체크는 제거하고, 컴포넌트 진입 시점에 처리하도록 수정했습니다!
@@ -0,0 +1,104 @@
+"use client";
+
+import { FormProvider, useForm } from "react-hook-form";
+import SolidButton from "@/components/common/buttons/SolidButton";
+import CommonTextArea from "@/components/common/inputs/TextArea";
+import RatingInput from "@/components/create-reaview/RatingInput";
+import ReviewImageInput from "@/components/create-reaview/ReviewImageInput";
+import useCookie from "@/hooks/auths/useTokenState";
+import useReviewMutations from "@/hooks/review/useReviewMutation";
+
+interface ReviewFormData {
+  rating: number;
+  content: string;
+  image: File | null;
+  meetupId: number;
+}
+
+export default function CreateReviewForm({ meetupId }: { meetupId: string }) {
+  const token = useCookie("accessToken");
+  const { createReviewMutation } = useReviewMutations();
+  const methods = useForm<ReviewFormData>({
+    defaultValues: {
+      rating: -1,
+      content: "",
+      image: null,
+      meetupId: Number(meetupId),
+    },
+    mode: "onChange",
+  });
+
+  const { handleSubmit, setValue, watch } = methods;
+
+  const rating = watch("rating");
+  const content = watch("content");
+  const isFormValid = rating !== -1 && content.trim() !== "";
+
+  const handleRatingChange = (value: number) => {
+    setValue("rating", value);
+  };
+
+  const handleImageSelect = (image: File | null) => {
+    setValue("image", image);
+  };
+
+  const onSubmit = handleSubmit(async (data) => {
+    if (!token) return;
+
+    const formData = new FormData();
+
+    const requestData = {
+      meetupId: Number(data.meetupId),
+      rating: data.rating,
+      content: data.content,
+    };
+
+    formData.append(
+      "request",
+      new Blob([JSON.stringify(requestData)], { type: "application/json" }),
+    );
+
+    if (data.image) {
+      formData.append("image", data.image);
+    }
+
+    if (data.image === null) {
+      formData.append("image", new Blob(), "");
+    }
+
+    await createReviewMutation.mutateAsync({ formData, token });
+  });
+
+  return (
+    <FormProvider {...methods}>
+      <form onSubmit={onSubmit}>
+        <div className='flex flex-col gap-12'>
+          <RatingInput
+            value={methods.watch("rating")}
+            onChange={handleRatingChange}
+          />
+          <CommonTextArea
+            formClassName='h-40'
+            required={true}
+            name='content'
+            label='구체적인 경험을 알려주세요'
+            placeholder='모임의 장소, 환경, 진행, 구성 등 만족스러웠나요?'
+            maxLength={150}
+          />
+          <ReviewImageInput onImageSelect={handleImageSelect} />
+        </div>
+        <SolidButton
+          type='submit'
+          className='mt-10'
+          state={
+            createReviewMutation.isPending || !isFormValid || !token
+              ? "inactive"
+              : "activated"
+          }
+        >
+          {createReviewMutation.isPending ? "작성 중..." : "작성 완료"}
+        </SolidButton>
+      </form>
+    </FormProvider>
+  );
+}

리뷰: `multipart/form-data`로 파일과 JSON 객체를 함께 전송할 때 발생하는 파싱 이슈를 해결하기 위해 사용했습니다. 
일전의 프로필수정 작업 때 같은 이슈가 있었는데, 이 방식이 잘 작동하여 동일한 패턴을 적용했습니다.
@@ -0,0 +1,104 @@
+"use client";
+
+import { FormProvider, useForm } from "react-hook-form";
+import SolidButton from "@/components/common/buttons/SolidButton";
+import CommonTextArea from "@/components/common/inputs/TextArea";
+import RatingInput from "@/components/create-reaview/RatingInput";
+import ReviewImageInput from "@/components/create-reaview/ReviewImageInput";
+import useCookie from "@/hooks/auths/useTokenState";
+import useReviewMutations from "@/hooks/review/useReviewMutation";
+
+interface ReviewFormData {
+  rating: number;
+  content: string;
+  image: File | null;
+  meetupId: number;
+}
+
+export default function CreateReviewForm({ meetupId }: { meetupId: string }) {
+  const token = useCookie("accessToken");
+  const { createReviewMutation } = useReviewMutations();
+  const methods = useForm<ReviewFormData>({
+    defaultValues: {
+      rating: -1,
+      content: "",
+      image: null,
+      meetupId: Number(meetupId),
+    },
+    mode: "onChange",
+  });
+
+  const { handleSubmit, setValue, watch } = methods;
+
+  const rating = watch("rating");
+  const content = watch("content");
+  const isFormValid = rating !== -1 && content.trim() !== "";
+
+  const handleRatingChange = (value: number) => {
+    setValue("rating", value);
+  };
+
+  const handleImageSelect = (image: File | null) => {
+    setValue("image", image);
+  };
+
+  const onSubmit = handleSubmit(async (data) => {
+    if (!token) return;
+
+    const formData = new FormData();
+
+    const requestData = {
+      meetupId: Number(data.meetupId),
+      rating: data.rating,
+      content: data.content,
+    };
+
+    formData.append(
+      "request",
+      new Blob([JSON.stringify(requestData)], { type: "application/json" }),
+    );
+
+    if (data.image) {
+      formData.append("image", data.image);
+    }
+
+    if (data.image === null) {
+      formData.append("image", new Blob(), "");
+    }
+
+    await createReviewMutation.mutateAsync({ formData, token });
+  });
+
+  return (
+    <FormProvider {...methods}>
+      <form onSubmit={onSubmit}>
+        <div className='flex flex-col gap-12'>
+          <RatingInput
+            value={methods.watch("rating")}
+            onChange={handleRatingChange}
+          />
+          <CommonTextArea
+            formClassName='h-40'
+            required={true}
+            name='content'
+            label='구체적인 경험을 알려주세요'
+            placeholder='모임의 장소, 환경, 진행, 구성 등 만족스러웠나요?'
+            maxLength={150}
+          />
+          <ReviewImageInput onImageSelect={handleImageSelect} />
+        </div>
+        <SolidButton
+          type='submit'
+          className='mt-10'
+          state={
+            createReviewMutation.isPending || !isFormValid || !token
+              ? "inactive"
+              : "activated"
+          }
+        >
+          {createReviewMutation.isPending ? "작성 중..." : "작성 완료"}
+        </SolidButton>
+      </form>
+    </FormProvider>
+  );
+}

리뷰: 앗, throw된 에러가 제대로 핸들링되지 않고 있었습니다🥹
`useReviewMutation`의 `onError`에서 모달 알림창을 띄우도록 수정했습니다.
@@ -0,0 +1,104 @@
+"use client";
+
+import { FormProvider, useForm } from "react-hook-form";
+import SolidButton from "@/components/common/buttons/SolidButton";
+import CommonTextArea from "@/components/common/inputs/TextArea";
+import RatingInput from "@/components/create-reaview/RatingInput";
+import ReviewImageInput from "@/components/create-reaview/ReviewImageInput";
+import useCookie from "@/hooks/auths/useTokenState";
+import useReviewMutations from "@/hooks/review/useReviewMutation";
+
+interface ReviewFormData {
+  rating: number;
+  content: string;
+  image: File | null;
+  meetupId: number;
+}
+
+export default function CreateReviewForm({ meetupId }: { meetupId: string }) {
+  const token = useCookie("accessToken");
+  const { createReviewMutation } = useReviewMutations();
+  const methods = useForm<ReviewFormData>({
+    defaultValues: {
+      rating: -1,
+      content: "",
+      image: null,
+      meetupId: Number(meetupId),
+    },
+    mode: "onChange",
+  });
+
+  const { handleSubmit, setValue, watch } = methods;
+
+  const rating = watch("rating");
+  const content = watch("content");
+  const isFormValid = rating !== -1 && content.trim() !== "";
+
+  const handleRatingChange = (value: number) => {
+    setValue("rating", value);
+  };
+
+  const handleImageSelect = (image: File | null) => {
+    setValue("image", image);
+  };
+
+  const onSubmit = handleSubmit(async (data) => {
+    if (!token) return;
+
+    const formData = new FormData();
+
+    const requestData = {
+      meetupId: Number(data.meetupId),
+      rating: data.rating,
+      content: data.content,
+    };
+
+    formData.append(
+      "request",
+      new Blob([JSON.stringify(requestData)], { type: "application/json" }),
+    );
+
+    if (data.image) {
+      formData.append("image", data.image);
+    }
+
+    if (data.image === null) {
+      formData.append("image", new Blob(), "");
+    }
+
+    await createReviewMutation.mutateAsync({ formData, token });
+  });
+
+  return (
+    <FormProvider {...methods}>
+      <form onSubmit={onSubmit}>
+        <div className='flex flex-col gap-12'>
+          <RatingInput
+            value={methods.watch("rating")}
+            onChange={handleRatingChange}
+          />
+          <CommonTextArea
+            formClassName='h-40'
+            required={true}
+            name='content'
+            label='구체적인 경험을 알려주세요'
+            placeholder='모임의 장소, 환경, 진행, 구성 등 만족스러웠나요?'
+            maxLength={150}
+          />
+          <ReviewImageInput onImageSelect={handleImageSelect} />
+        </div>
+        <SolidButton
+          type='submit'
+          className='mt-10'
+          state={
+            createReviewMutation.isPending || !isFormValid || !token
+              ? "inactive"
+              : "activated"
+          }
+        >
+          {createReviewMutation.isPending ? "작성 중..." : "작성 완료"}
+        </SolidButton>
+      </form>
+    </FormProvider>
+  );
+}

리뷰: JSON 객체를 함께 전송시 어떤 파싱이슈가 발생하나요?
@@ -0,0 +1,104 @@
+"use client";
+
+import { FormProvider, useForm } from "react-hook-form";
+import SolidButton from "@/components/common/buttons/SolidButton";
+import CommonTextArea from "@/components/common/inputs/TextArea";
+import RatingInput from "@/components/create-reaview/RatingInput";
+import ReviewImageInput from "@/components/create-reaview/ReviewImageInput";
+import useCookie from "@/hooks/auths/useTokenState";
+import useReviewMutations from "@/hooks/review/useReviewMutation";
+
+interface ReviewFormData {
+  rating: number;
+  content: string;
+  image: File | null;
+  meetupId: number;
+}
+
+export default function CreateReviewForm({ meetupId }: { meetupId: string }) {
+  const token = useCookie("accessToken");
+  const { createReviewMutation } = useReviewMutations();
+  const methods = useForm<ReviewFormData>({
+    defaultValues: {
+      rating: -1,
+      content: "",
+      image: null,
+      meetupId: Number(meetupId),
+    },
+    mode: "onChange",
+  });
+
+  const { handleSubmit, setValue, watch } = methods;
+
+  const rating = watch("rating");
+  const content = watch("content");
+  const isFormValid = rating !== -1 && content.trim() !== "";
+
+  const handleRatingChange = (value: number) => {
+    setValue("rating", value);
+  };
+
+  const handleImageSelect = (image: File | null) => {
+    setValue("image", image);
+  };
+
+  const onSubmit = handleSubmit(async (data) => {
+    if (!token) return;
+
+    const formData = new FormData();
+
+    const requestData = {
+      meetupId: Number(data.meetupId),
+      rating: data.rating,
+      content: data.content,
+    };
+
+    formData.append(
+      "request",
+      new Blob([JSON.stringify(requestData)], { type: "application/json" }),
+    );
+
+    if (data.image) {
+      formData.append("image", data.image);
+    }
+
+    if (data.image === null) {
+      formData.append("image", new Blob(), "");
+    }
+
+    await createReviewMutation.mutateAsync({ formData, token });
+  });
+
+  return (
+    <FormProvider {...methods}>
+      <form onSubmit={onSubmit}>
+        <div className='flex flex-col gap-12'>
+          <RatingInput
+            value={methods.watch("rating")}
+            onChange={handleRatingChange}
+          />
+          <CommonTextArea
+            formClassName='h-40'
+            required={true}
+            name='content'
+            label='구체적인 경험을 알려주세요'
+            placeholder='모임의 장소, 환경, 진행, 구성 등 만족스러웠나요?'
+            maxLength={150}
+          />
+          <ReviewImageInput onImageSelect={handleImageSelect} />
+        </div>
+        <SolidButton
+          type='submit'
+          className='mt-10'
+          state={
+            createReviewMutation.isPending || !isFormValid || !token
+              ? "inactive"
+              : "activated"
+          }
+        >
+          {createReviewMutation.isPending ? "작성 중..." : "작성 완료"}
+        </SolidButton>
+      </form>
+    </FormProvider>
+  );
+}

리뷰: 처음에 FormData에 request 객체를 JSON.stringify로 append했을 때는 요청이 실패했는데, 찾아보니 이미지 파일과 함께 보내는 멀티파트 폼데이터에서는 모든 데이터가 바이너리 형태여야 서버에서 제대로 파싱할 수 있다고 해서 Blob 객체로 변환하여 전송했더니 성공했습니다.
@@ -8,7 +8,7 @@ export default class PlanMapper {
   toDomain(): IPlan {
     if (!this.plan) return null;
 
-    return new Plan({
+    const returnPlan = new Plan({
       id: this.plan.id,
       createdAt: this.plan.createdAt,
       updatedAt: this.plan.updatedAt,
@@ -19,11 +19,19 @@ export default class PlanMapper {
       serviceArea: this.plan.serviceArea,
       details: this.plan.details,
       address: this.plan.address,
-      status: this.plan.status ?? StatusEnum.PENDING,
-      quotes: this.plan.quotes,
+      status: this.plan.status,
+      quotes: this.plan.quotes?.map((quote) => ({
+        ...quote,
+        maker: {
+          id: quote.maker.id,
+          nickName: quote.maker.nickName,
+          image: quote.maker?.makerProfile?.image
+        }
+      })),
       assignees: this.plan.assignees,
       dreamer: this.plan.dreamer,
       dreamerId: this.plan.dreamerId
     });
+    return returnPlan;
   }
 }

리뷰: 이거 이전에 체크 안 한 것 같은데, status는 데이터베이스에서 default 값이 PENDING으로 주어지니까 mapper에는 필요없을 것 같아요. 이제 mapper는 repo에서만 쓰는 걸로 수정하셨다면 지워도 될 것 같습니다~!
@@ -8,7 +8,7 @@ export default class PlanMapper {
   toDomain(): IPlan {
     if (!this.plan) return null;
 
-    return new Plan({
+    const returnPlan = new Plan({
       id: this.plan.id,
       createdAt: this.plan.createdAt,
       updatedAt: this.plan.updatedAt,
@@ -19,11 +19,19 @@ export default class PlanMapper {
       serviceArea: this.plan.serviceArea,
       details: this.plan.details,
       address: this.plan.address,
-      status: this.plan.status ?? StatusEnum.PENDING,
-      quotes: this.plan.quotes,
+      status: this.plan.status,
+      quotes: this.plan.quotes?.map((quote) => ({
+        ...quote,
+        maker: {
+          id: quote.maker.id,
+          nickName: quote.maker.nickName,
+          image: quote.maker?.makerProfile?.image
+        }
+      })),
       assignees: this.plan.assignees,
       dreamer: this.plan.dreamer,
       dreamerId: this.plan.dreamerId
     });
+    return returnPlan;
   }
 }

리뷰: PlanMapperProperties를 보니 makerProfile 안에 image가 들어가는 것 같은데, image 필드는 별개인가요? makerProfile을 undefined로 추가하는 이유도 궁금합니다.
```
maker?: {
      id: string;
      nickName: string;
      makerProfile: { image: ProfileImage };
    };
```
@@ -8,7 +8,7 @@ export default class PlanMapper {
   toDomain(): IPlan {
     if (!this.plan) return null;
 
-    return new Plan({
+    const returnPlan = new Plan({
       id: this.plan.id,
       createdAt: this.plan.createdAt,
       updatedAt: this.plan.updatedAt,
@@ -19,11 +19,19 @@ export default class PlanMapper {
       serviceArea: this.plan.serviceArea,
       details: this.plan.details,
       address: this.plan.address,
-      status: this.plan.status ?? StatusEnum.PENDING,
-      quotes: this.plan.quotes,
+      status: this.plan.status,
+      quotes: this.plan.quotes?.map((quote) => ({
+        ...quote,
+        maker: {
+          id: quote.maker.id,
+          nickName: quote.maker.nickName,
+          image: quote.maker?.makerProfile?.image
+        }
+      })),
       assignees: this.plan.assignees,
       dreamer: this.plan.dreamer,
       dreamerId: this.plan.dreamerId
     });
+    return returnPlan;
   }
 }

리뷰: 받아올 때는 makerProfile에 담겨오지만 맵퍼에서 makerProfile 안에 있는 image를 밖으로 끄집어오고 makerProfile이란 필드를 없애주기 위해 undefined를 넣었습니다
@@ -8,7 +8,7 @@ export default class PlanMapper {
   toDomain(): IPlan {
     if (!this.plan) return null;
 
-    return new Plan({
+    const returnPlan = new Plan({
       id: this.plan.id,
       createdAt: this.plan.createdAt,
       updatedAt: this.plan.updatedAt,
@@ -19,11 +19,19 @@ export default class PlanMapper {
       serviceArea: this.plan.serviceArea,
       details: this.plan.details,
       address: this.plan.address,
-      status: this.plan.status ?? StatusEnum.PENDING,
-      quotes: this.plan.quotes,
+      status: this.plan.status,
+      quotes: this.plan.quotes?.map((quote) => ({
+        ...quote,
+        maker: {
+          id: quote.maker.id,
+          nickName: quote.maker.nickName,
+          image: quote.maker?.makerProfile?.image
+        }
+      })),
       assignees: this.plan.assignees,
       dreamer: this.plan.dreamer,
       dreamerId: this.plan.dreamerId
     });
+    return returnPlan;
   }
 }

리뷰: 네 수정하겠습니다
@@ -8,7 +8,7 @@ export default class PlanMapper {
   toDomain(): IPlan {
     if (!this.plan) return null;
 
-    return new Plan({
+    const returnPlan = new Plan({
       id: this.plan.id,
       createdAt: this.plan.createdAt,
       updatedAt: this.plan.updatedAt,
@@ -19,11 +19,19 @@ export default class PlanMapper {
       serviceArea: this.plan.serviceArea,
       details: this.plan.details,
       address: this.plan.address,
-      status: this.plan.status ?? StatusEnum.PENDING,
-      quotes: this.plan.quotes,
+      status: this.plan.status,
+      quotes: this.plan.quotes?.map((quote) => ({
+        ...quote,
+        maker: {
+          id: quote.maker.id,
+          nickName: quote.maker.nickName,
+          image: quote.maker?.makerProfile?.image
+        }
+      })),
       assignees: this.plan.assignees,
       dreamer: this.plan.dreamer,
       dreamerId: this.plan.dreamerId
     });
+    return returnPlan;
   }
 }

리뷰: undefined 대신 맵핑방식으로 변경