관리 메뉴

CASSIE'S BLOG

5시간 싸움 백엔드가 도와준 멀티파트 3차 협업 본문

PROGRAMMING/삽질로그

5시간 싸움 백엔드가 도와준 멀티파트 3차 협업

ITSCASSIE1107 2024. 2. 20. 20:53

내 코드.. 안되는 거

 

 

import React, { useEffect, useState } from "react";
import styled from "styled-components";
import { useParams } from "react-router-dom";
import { useSelector } from "react-redux";
import { RootState } from "../../store/store";
import axios from "axios";

import Header from "../../shared/Header";
import Footer from "../../shared/Footer";
import PageTitle from "../../components/goodRestaurantEnrollPage/PageTitle";
import RestaurantInfoSection from "../../components/goodRestaurantEnrollPage/RestaurantInfoSection";
import RestaurantInfoInput from "../../components/goodRestaurantEnrollPage/RestaurantInfoInput";
import CategorySelect from "../../components/goodRestaurantEnrollPage/CategorySelect";
import AddressInput from "../../components/goodRestaurantEnrollPage/AddressInput";
import DetailAddressInfoInput from "../../components/goodRestaurantEnrollPage/DetailAddressInfoInput";
import MenuReviewSection from "../../components/goodRestaurantEnrollPage/MenuReviewSection";
import ButtonSection from "../../components/goodRestaurantEnrollPage/ButtonSection";
import ScrollToTopButton from "../../shared/ScrollTopButton";
import QuillEditor from "../../components/goodRestaurantEnrollPage/QuillEditor";
import FileUpload from "../../components/goodRestaurantEnrollPage/FileUpload";
import { DARK_GREY, WHITE, SOFT_BEIGE } from "../../styles/colors";
import ContactNumInfoInput from "../../components/goodRestaurantEnrollPage/ContactNumInfoInput";

const GoodRestaurantEnrollPage: React.FC = () => {
  const isAuthenticated = useSelector(
    (state: RootState) => state.auth.isAuthenticated
  );

  const [restaurantInfo, setRestaurantInfo] = useState({
    name: "",
    address: "",
    category: "",
    contactNum: "",
    detailAddress: "",
    menu: "",
    content: "",
    latitude: "",
    longitude: "",
  });

  // isAuthenticated가 true이면서 토큰이 존재하는 경우에만 토큰을 가져옴
  const token = isAuthenticated ? localStorage.getItem("token") : null;

  const [selectedimageFiles, setSelectedImageFiles] = useState<File[]>([]);

  const handleCategoryChange = (selectedCategory: string) => {
    setRestaurantInfo({
      ...restaurantInfo,
      category: selectedCategory,
    });
  };

  const [selectedAddress, setSelectedAddress] = useState("");

  const handleContentChange = (content: string) => {
    setRestaurantInfo({
      ...restaurantInfo,
      content: content,
    });
  };

  // 파일 선택 핸들러를 변경하여 selectedFiles 배열을 업데이트합니다.
  const handleFileChange = (files: FileList | null) => {
    if (files && files.length > 0) {
      const newFiles: File[] = Array.from(files);
      setSelectedImageFiles(newFiles);
    }
  };

  const handleRegister = async () => {
    const formData = new FormData();

      // 이미 selectedimageFiles 배열에 파일이 추가되어 있으므로 바로 FormData에 추가합니다.

    // formData.append("images", selectedimageFiles);
 
    for (const file of selectedimageFiles) {
      formData.append("images", file);
    }

    // JSON 데이터를 Blob으로 변환하여 formData에 추가
    const jsonBlob = new Blob([JSON.stringify(restaurantInfo)], {
      type: "application/json",
    });

    formData.append("postRequest", JSON.stringify(restaurantInfo));
    console.log(
      "JSON.stringify(restaurantInfo) test:",
      JSON.stringify(restaurantInfo)
    );

    console.log("formdata test제에발", formData);

    try {
      const response = await axios.post(
        formData,
        {
          headers: {
            Token: token,
          },
        }
      );
      console.log("백엔드로부터의 응답:", response.data);
      alert("맛집목록등록에 성공했습니다.");
      window.location.reload();
    } catch (error: any) {
      console.error("오류 발생:", error);

      if (error.response) {
        console.log("에러 응답:", error.response.data);
        console.log("사용자 입력 내용:", restaurantInfo);
        console.log("사용자 입력 내용 images:", selectedimageFiles);
      } else {
        console.log("오류 응답이 없습니다.");
      }
    }
  };

  const isDarkMode = useSelector(
    (state: RootState) => state.darkMode.isDarkMode
  );

  const { postId = "" } = useParams<{ postId?: string }>(); // postId의 초기값을 ''로 설정

  const handleInputChangeName = (e: React.ChangeEvent<HTMLInputElement>) => {
    setRestaurantInfo({
      ...restaurantInfo,
      [e.target.name]: e.target.value,
    });
  };

  const handleInputChangeAddress = (newAddress: string) => {
    setRestaurantInfo({
      ...restaurantInfo,
      address: newAddress,
    });
  };

  const handleInputChangeDetailAddress = (
    e: React.ChangeEvent<HTMLInputElement>
  ) => {
    setRestaurantInfo({
      ...restaurantInfo,
      detailAddress: e.target.value,
    });
  };

  const handleInputChangeContactNum = (
    e: React.ChangeEvent<HTMLInputElement>
  ) => {
    setRestaurantInfo({
      ...restaurantInfo,
      contactNum: e.target.value,
    });
  };

  const handleInputChangeMenu = (updatedMenu: string) => {
    setRestaurantInfo({
      ...restaurantInfo,
      menu: updatedMenu,
    });
  };

  const [selectedCoordinates, setSelectedCoordinates] = useState<{
    latitude: string;
    longitude: string;
  }>({
    latitude: "",
    longitude: "",
  });

  //NOTE: handleCoordinateChange함수안에 setSelectedCoordinates IMPO 경도 위도 추후에 업데이트하는 로직
  const handleCoordinateChange = (coordinates: {
    latitude: string;
    longitude: string;
  }) => {
    setSelectedCoordinates({
      latitude: coordinates.latitude,
      longitude: coordinates.longitude,
    });
  };

  useEffect(() => {
    setRestaurantInfo((prevInfo) => ({
      ...prevInfo,
      latitude: selectedCoordinates.latitude,
      longitude: selectedCoordinates.longitude,
    }));
  }, [selectedCoordinates]);

  return (
    <StyledGoodRestrauntPage isDarkMode={isDarkMode}>
      <Header />
      <Wrapper>
        <RestaurantInfoSectionWrapper>
          <PageTitle />
          <RestaurantInfoSection>
            <CategorySelect onCategoryChange={handleCategoryChange} />
            <RestaurantInfoInput
              label="가게명"
              name="name"
              value={restaurantInfo.name}
              onChange={handleInputChangeName}
            />
            <ContactNumInfoInput
              label="연락처"
              name="contactNum"
              value={restaurantInfo.contactNum}
              onChange={handleInputChangeContactNum}
            />
            <AddressInput
              onCoordinateChange={handleCoordinateChange} // 위도와 경도를 받아오는 핸들러 함수
              onChange={handleInputChangeAddress} // 주소가 변경될 때 호출되는 핸들러 함수
            />
            <DetailAddressInfoInput
              label="상세주소"
              name="detailAddress"
              value={restaurantInfo.detailAddress}
              onChange={handleInputChangeDetailAddress}
            />
          </RestaurantInfoSection>
        </RestaurantInfoSectionWrapper>
        <QuillAndFileUploadWrapper>
          <QuillEditorWrapper>
            <QuillEditor onContentChange={handleContentChange} />
          </QuillEditorWrapper>
          <FileUploadWrapper>
            <FileUpload
              selectedFiles={selectedimageFiles}
              onFileSelect={handleFileChange}
            />
            <FileUpload
              selectedFiles={selectedimageFiles}
              onFileSelect={handleFileChange}
            />
            <FileUpload
              selectedFiles={selectedimageFiles}
              onFileSelect={handleFileChange}
            />
          </FileUploadWrapper>
        </QuillAndFileUploadWrapper>
        <MenuReviewSection onChange={handleInputChangeMenu} />
        <ButtonSection postId={postId} onRegister={handleRegister} />
        <ScrollToTopButton />
      </Wrapper>
      <Footer />
    </StyledGoodRestrauntPage>
  );
};

export default GoodRestaurantEnrollPage;

const StyledGoodRestrauntPage = styled.div<{ isDarkMode: boolean }>`
  width: 100vw;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  background-color: ${(props) => (props.isDarkMode ? DARK_GREY : WHITE)};
`;

const RestaurantInfoSectionWrapper = styled.div`
  background-color: ${SOFT_BEIGE};
  padding: 20px;
  border-radius: 5px;
  width: 100%;
  display: flex;
  flex-direction: column;
  height: 80vh;
  width: 80vw;
  justify-content: center;
  align-items: center;
  margin: auto; /* 부모 컨테이너에 대해 가운데 정렬 */
  justify-content: space-evenly; /* 세로 방향 여백을 동일하게 설정 */
`;

const Wrapper = styled.div`
  padding: 50px 0px 50px 0px;
  border-radius: 8px;
  box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
  justify-content: center;
  align-items: center;
  margin: 0 auto;
`;

const QuillEditorWrapper = styled.div`
  height: 33vh;
  width: 30vw;
  overflow-y: auto; /* NOTE: 내용이 넘칠 때 스크롤이 생성되도록 설정합니다. */
  margin-right: 2%;
`;

const QuillAndFileUploadWrapper = styled.div`
  display: flex;
  flex-direction: row;
  justify-content: center;
  align-items: center;
  background-color: ${SOFT_BEIGE};
`;

const FileUploadWrapper = styled.div`
  display: flex;
  flex-direction: column;
`;

 

백엔드 개발자가 도와준 것 

 

import React, { useEffect, useState } from "react";
import styled from "styled-components";
import { useParams } from "react-router-dom";
import { useSelector } from "react-redux";
import { RootState } from "../../store/store";
import axios from "axios";

import Header from "../../shared/Header";
import Footer from "../../shared/Footer";
import PageTitle from "../../components/goodRestaurantEnrollPage/PageTitle";
import RestaurantInfoSection from "../../components/goodRestaurantEnrollPage/RestaurantInfoSection";
import RestaurantInfoInput from "../../components/goodRestaurantEnrollPage/RestaurantInfoInput";
import CategorySelect from "../../components/goodRestaurantEnrollPage/CategorySelect";
import AddressInput from "../../components/goodRestaurantEnrollPage/AddressInput";
import DetailAddressInfoInput from "../../components/goodRestaurantEnrollPage/DetailAddressInfoInput";
import MenuReviewSection from "../../components/goodRestaurantEnrollPage/MenuReviewSection";
import ButtonSection from "../../components/goodRestaurantEnrollPage/ButtonSection";
import ScrollToTopButton from "../../shared/ScrollTopButton";
import QuillEditor from "../../components/goodRestaurantEnrollPage/QuillEditor";
import FileUpload from "../../components/goodRestaurantEnrollPage/FileUpload";
import { DARK_GREY, WHITE, SOFT_BEIGE } from "../../styles/colors";
import ContactNumInfoInput from "../../components/goodRestaurantEnrollPage/ContactNumInfoInput";

const GoodRestaurantEnrollPage: React.FC = () => {
  const isAuthenticated = useSelector(
      (state: RootState) => state.auth.isAuthenticated
  );

  const [restaurantInfo, setRestaurantInfo] = useState({
    name: "",
    address: "",
    category: "",
    contactNum: "",
    detailAddress: "",
    menu: "",
    content: "",
    latitude: "",
    longitude: "",
  });

  // isAuthenticated가 true이면서 토큰이 존재하는 경우에만 토큰을 가져옴
  const token = isAuthenticated ? localStorage.getItem("token") : null;

  const [selectedimageFiles, setSelectedImageFiles] = useState<File[]>([]);

  const handleCategoryChange = (selectedCategory: string) => {
    setRestaurantInfo({
      ...restaurantInfo,
      category: selectedCategory,
    });
  };

  const [selectedAddress, setSelectedAddress] = useState("");

  const handleContentChange = (content: string) => {
    setRestaurantInfo({
      ...restaurantInfo,
      content: content,
    });
  };

  // GoodRestaurantEnrollPage 컴포넌트에서 selectedFiles 배열 상태와 해당 상태를 업데이트하는 함수를 추가합니다.
  const [selectedFiles, setSelectedFiles] = useState<File[]>([]);

  // 파일 선택 핸들러를 변경하여 selectedFiles 배열을 업데이트합니다.
  const handleFileChange = (files: FileList | null) => {
    if (files && files.length > 0) {
      const newFiles: File[] = Array.from(files);
      setSelectedImageFiles(newFiles);
    }
  };

  const handleRegister = async () => {
    const formData = new FormData();

    selectedimageFiles.forEach((file, index) => {
      console.log("file test제에발", file);
      formData.append("images", file);
    });

    // JSON 데이터를 Blob으로 변환하여 formData에 추가
    const jsonBlob = new Blob([JSON.stringify(restaurantInfo)], {
      type: "application/json",
    });

    formData.append("postRequest", jsonBlob);
    console.log("JSON.stringify(restaurantInfo) test", JSON.stringify(restaurantInfo));

    console.log("formdata test제에발", formData);

    try {
      const response = await axios.post(
          formData,
          {

            headers: {
              Token: token,
            },
          }
      );
      console.log("백엔드로부터의 응답:", response.data);
      alert("맛집목록등록에 성공했습니다.");
      window.location.reload();
    } catch (error: any) {
      console.error("오류 발생:", error);

      if (error.response) {
        console.log("에러 응답:", error.response.data);
        console.log("사용자 입력 내용:", restaurantInfo);
        console.log("사용자 입력 내용 images:", selectedimageFiles);
      } else {
        console.log("오류 응답이 없습니다.");
      }
    }
  };

  const isDarkMode = useSelector(
      (state: RootState) => state.darkMode.isDarkMode
  );

  const { postId = "" } = useParams<{ postId?: string }>(); // postId의 초기값을 ''로 설정

  const handleInputChangeName = (e: React.ChangeEvent<HTMLInputElement>) => {
    setRestaurantInfo({
      ...restaurantInfo,
      [e.target.name]: e.target.value,
    });
  };

  const handleInputChangeAddress = (newAddress: string) => {
    setRestaurantInfo({
      ...restaurantInfo,
      address: newAddress,
    });
  };

  const handleInputChangeDetailAddress = (
      e: React.ChangeEvent<HTMLInputElement>
  ) => {
    setRestaurantInfo({
      ...restaurantInfo,
      detailAddress: e.target.value,
    });
  };

  const handleInputChangeContactNum = (
      e: React.ChangeEvent<HTMLInputElement>
  ) => {
    setRestaurantInfo({
      ...restaurantInfo,
      contactNum: e.target.value,
    });
  };

  const handleInputChangeMenu = (updatedMenu: string) => {
    setRestaurantInfo({
      ...restaurantInfo,
      menu: updatedMenu,
    });
  };

  const [selectedCoordinates, setSelectedCoordinates] = useState<{
    latitude: string;
    longitude: string;
  }>({
    latitude: "",
    longitude: "",
  });

  //NOTE: handleCoordinateChange함수안에 setSelectedCoordinates IMPO 경도 위도 추후에 업데이트하는 로직
  const handleCoordinateChange = (coordinates: {
    latitude: string;
    longitude: string;
  }) => {
    setSelectedCoordinates({
      latitude: coordinates.latitude,
      longitude: coordinates.longitude,
    });
  };

  useEffect(() => {
    setRestaurantInfo((prevInfo) => ({
      ...prevInfo,
      latitude: selectedCoordinates.latitude,
      longitude: selectedCoordinates.longitude,
    }));
  }, [selectedCoordinates]);

  return (
      <StyledGoodRestrauntPage isDarkMode={isDarkMode}>
        <Header />
        <Wrapper>
          <RestaurantInfoSectionWrapper>
            <PageTitle />
            <RestaurantInfoSection>
              <CategorySelect onCategoryChange={handleCategoryChange} />
              <RestaurantInfoInput
                  label="가게명"
                  name="name"
                  value={restaurantInfo.name}
                  onChange={handleInputChangeName}
              />
              <ContactNumInfoInput
                  label="연락처"
                  name="contactNum"
                  value={restaurantInfo.contactNum}
                  onChange={handleInputChangeContactNum}
              />
              <AddressInput
                  onCoordinateChange={handleCoordinateChange} // 위도와 경도를 받아오는 핸들러 함수
                  onChange={handleInputChangeAddress} // 주소가 변경될 때 호출되는 핸들러 함수
              />
              <DetailAddressInfoInput
                  label="상세주소"
                  name="detailAddress"
                  value={restaurantInfo.detailAddress}
                  onChange={handleInputChangeDetailAddress}
              />
            </RestaurantInfoSection>
          </RestaurantInfoSectionWrapper>
          <QuillAndFileUploadWrapper>
            <QuillEditorWrapper>
              <QuillEditor onContentChange={handleContentChange} />
            </QuillEditorWrapper>
            <FileUploadWrapper>
              <FileUpload
                  selectedFiles={selectedimageFiles}
                  onFileSelect={handleFileChange}
              />
              <FileUpload
                  selectedFiles={selectedimageFiles}
                  onFileSelect={handleFileChange}
              />
              <FileUpload
                  selectedFiles={selectedimageFiles}
                  onFileSelect={handleFileChange}
              />
            </FileUploadWrapper>
          </QuillAndFileUploadWrapper>
          <MenuReviewSection onChange={handleInputChangeMenu} />
          <ButtonSection postId={postId} onRegister={handleRegister} />
          <ScrollToTopButton />
        </Wrapper>
        <Footer />
      </StyledGoodRestrauntPage>
  );
};

export default GoodRestaurantEnrollPage;

const StyledGoodRestrauntPage = styled.div<{ isDarkMode: boolean }>`
  width: 100vw;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  background-color: ${(props) => (props.isDarkMode ? DARK_GREY : WHITE)};
`;

const RestaurantInfoSectionWrapper = styled.div`
  background-color: ${SOFT_BEIGE};
  padding: 20px;
  border-radius: 5px;
  width: 100%;
  display: flex;
  flex-direction: column;
  height: 80vh;
  width: 80vw;
  justify-content: center;
  align-items: center;
  margin: auto; /* 부모 컨테이너에 대해 가운데 정렬 */
  justify-content: space-evenly; /* 세로 방향 여백을 동일하게 설정 */
`;

const Wrapper = styled.div`
  padding: 50px 0px 50px 0px;
  border-radius: 8px;
  box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
  justify-content: center;
  align-items: center;
  margin: 0 auto;
`;

const QuillEditorWrapper = styled.div`
  height: 33vh;
  width: 30vw;
  overflow-y: auto; /* NOTE: 내용이 넘칠 때 스크롤이 생성되도록 설정합니다. */
  margin-right: 2%;
`;

const QuillAndFileUploadWrapper = styled.div`
  display: flex;
  flex-direction: row;
  justify-content: center;
  align-items: center;
  background-color: ${SOFT_BEIGE};
`;

const FileUploadWrapper = styled.div`
  display: flex;
  flex-direction: column;
`;

 

 

  1. 파일 업로드 처리 방식:
    • 첫 번째 코드에서는 handleFileChange 함수를 통해 파일을 선택하고, 선택된 파일들을 selectedimageFiles 상태에 저장합니다. 그런 다음 selectedimageFiles를 직접 FormData에 추가하려고 시도하고 있습니다. 그러나 이렇게 하면 FormData에 올바르게 파일이 추가되지 않을 수 있습니다. FormData는 파일을 올바르게 처리하기 위해 파일을 개별적으로 추가해야 합니다.
    • 두 번째 코드에서는 selectedimageFiles 배열에 있는 각 파일을 반복문을 통해 개별적으로 FormData에 추가하고 있습니다. 이렇게 하면 각 파일이 정확하게 FormData에 추가되어 서버로 전송됩니다.
  2. 토큰 처리 방식:
    • 첫 번째 코드에서는 토큰을 headers 객체에 직접 추가하여 요청을 보내고 있습니다. 그러나 이러한 방식은 토큰이 노출될 수 있고, 보안에 취약합니다.
    • 두 번째 코드에서는 axios의 headers 속성을 이용하여 토큰을 전달하고 있습니다. 이 방식은 토큰을 안전하게 전달할 수 있고, 보안상 더 안전합니다. 또한, axios는 HTTP 요청을 보낼 때 헤더를 자동으로 설정하므로 토큰이 노출되는 위험이 줄어듭니다.

 

File 객체는 사용자의 로컬 파일 시스템에서 선택한 파일에 대한 정보를 가지고 있습니다. 따라서 실제로 파일을 업로드할 때는 File 객체를 사용해야 합니다. 하지만 FormData에 파일을 추가할 때는 파일 데이터만 필요하며 파일의 원본 이름이나 다른 정보는 필요하지 않습니다. 이런 이유로 File 객체 대신 Blob 객체를 사용하여 파일 데이터를 FormData에 추가합니다.

 

 

반응형