ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 케이커 웹/앱 - 1 (가게 정보 등록 및 수정, 가게 정보 미리보기)
    프론트엔드 2022. 8. 14. 10:37

     

    가게 정보 입력 구조

     

    const.js

    상수 데이터 모음 

    default 생략 시, 한 파일에 여러 export 구현 가능

    export const SHOP_DATA = [
      {
        key: "name",
        height: "60px",
        placeholder: "     정확한 상호명을 입력해주세요.",
        keyword: "가게 이름",
      },
      {
        key: "readme",
        height: "125px",
        placeholder: "     자신의 가게를 자유롭게 소개해주세요! (최대 300자)",
        keyword: "가게 소개",
      },
      {
        key: "phoneNumber",
        height: "60px",
        placeholder: "     ex. 02-0000-0000",
        keyword: "전화번호",
      },
      {
        key: "address",
        height: "60px",
        placeholder: "     ex. 서울특별시 서대문구 대현동 11-11층",
        keyword: "주소",
      },
      {
        key: "openTime",
        height: "60px",
        placeholder: "     ex. 매일 12:00~20:00",
        keyword: "운영 시간",
      },
      {
        key: "kakaoUrl",
        height: "60px",
        placeholder: "     운영중인 카카오톡 채널이 있다면 링크를 첨부해주세요.",
        keyword: "카카오톡 채널",
      },
      {
        key: "instagram",
        height: "60px",
        placeholder: "     운영중인 인스타그램이 있다면 링크를 첨부해주세요.",
        keyword: "인스타그램",
      },
    ];

    export const DEFAULT_STORE_DATA = {
      name: "", //(string)
      readme: "", //string
      address: "", //string
      kakaoUrl: "", //string
      instagram: "", //string
      certifyFlag: 0, //가게 인증 여부 (boolean)
      openTime: "", //string
      phoneNumber: "", //string
      mainImg: "",
    };
     
     
     
    index.js
     
    import { SHOP_DATA, DEFAULT_STORE_DATA } from "./const";
    import {
      WrapBox,
      ShopRegistering,
      ShopIntroduceName,
      ShopIntroduce,
      RegisterBtn,
    } from "./styles";
    스타일 컴포넌트 따로 만들어서 추출해온다.
    import http from "../../../common/http";
    import { useNavigate } from "react-router-dom";
     
    const token = JSON.parse(localStorage.getItem("token"));
    카카오에서 받아온 토큰을 localStorage에 저장 후, 가져온다. => 회원 정보 구분
    JSON.parse() 메소드는 인수로 전달받은 문자열을 자바스크립트 객체로 변환하여 반환한다.

    //ui 구현
    const ShopInformationRegister = () => {
      const nav = useNavigate(); 
    useNavigate() 메소드는 url을 조작할 수 있는 interface를 제공한다.
      const formData = new FormData();
      const [storeData, setStoreData] = useState(DEFAULT_STORE_DATA);

      const updateStoreData = partialStoreData => {
        setStoreData(prev => ({ ...prev, ...partialStoreData }));
      };
    이전값들은 prev로, 현재 객체 형태의 값은 partialStoreData로 들어간다. => 새로운 State로 반환
     
    ES6 spread 연산자 {...} 에 대해 알아보자.

    spread 연산자는 배열 및 객체를 간편하게 '복제'한다.
    특히, 객체에서 특정한 값만을 변경(update)해야 할 때, 자주 쓰인다.

    const object1 = { a:1, b:2, c:3};
    const object2 = { ...object1, b:4}; 

    a,c는 그대로 유지되고 b만 바뀐다.
      const [MainFile, setMainFile] = useState([]);
      const [MenuFile, setMenuFile] = useState([]);
    파일 업로드(DND)는 컴포넌트로 따로 분리해 작성해뒀다. => 포스팅 4 참고
     
      const MainFileHandler = e => {
        e.preventDefault();
        setShopFile(e.tartget.value);
      };

      const MenuFileHandler = e => {
        e.preventDefault();
        setMenuFile(e.tartget.value);
      };

      //제출 -> 백엔드로 post
      const submitHandler = e => {
        console.log(storeData);

        http
          .post("/stores/myStore", storeData, {
            headers: {
              "Content-type": "application/json",
            },
          })
          .then(res => postImg(res.data))
          .catch(err => console.log("json 포스트 실패", err));
    카카오에서 토큰을 받았다면, axios 헤더에 토큰을 추가해 사용자를 구분해냈다.
     
    http.js

        const postImg = async id => {
         
          formData.append("mainImg", MainFile[0].original, "mainImage.png");
          formData.append("menuImg", MenuFile[0].original, "menuImage.png");
          formData.append("storeId", id);

          for (const keyValue of formData) console.log(keyValue);
          axios
            .patch("https://caker.shop/stores/myStore/image", formData, {
              headers: {
                "Content-Type": "multipart/form-data",
                "X-AUTH-TOKEN": token,
              },
            })
            .then(res => console.log("파일 포스트 성공", res))
            .catch(err => console.log("파일 포스트 실패", err));
          nav("/caker/mypage");
        };
    처음에는 application/json 타입과 multipart/form-data 타입을 formData에 묶어 백엔드로 한 번에 보내려 했으나, 실패했다. (엄청 어려운 작업이라고 들었다,,) 그래서 application/json 타입을 먼저 보내고 얻어온(회원 구분) id를 multipart/form-data에 추가해 보내는 방법을 선택했다. 
       
      };

      return (
        <WrapBox>
          <TopBar />
          <div>
            <ShopRegistering>가게 정보 등록</ShopRegistering>
            {SHOP_DATA.map(data => {
              return (
                <React.Fragment key={data.key}>
                  <ShopIntroduceName>{data.keyword}</ShopIntroduceName>
                  <ShopIntroduce
                    type="text"
                    height={data.height}
                    value={storeData[data.key]} 키에 해당하는 현재 값
                    onChange={e => {
                      e.preventDefault();
                      updateStoreData({ [data.key]: e.target.value }); 객체 형태(키: 현재 인풋 창에 입력된 값)
                    }}
                    placeholder={data.placeholder}
                  />
                </React.Fragment>
              );
            })}
    SHOP_DATA는 const.js에 저장해두었던 데이터셋으로, map() 메소드를 이용해 뿌려준다.
     
    map() 메소드를 알아보자.

    반복되는 컴포넌트를 렌더링 하기위해 사용한다.
    새로운 배열의 요소를 생성하는 함수로서 세가지 인수를 가질 수 있다.

    ㆍcurrenVlaue : 현재 배열(arr) 내의 값들을 의미
    ㆍindex : 현재 배열 내 값의 인덱스를 의미
    ㆍarray : 현재 배열

    cf. 이차원 배열로도 응용 가능하다.
    const MapEx = () => {
         const names = [
                           ["a", 1],
                           ["b", 2],
                           ["c", 3]
         ]

         const nameList = names.map((name) = (<div> {name[0]} {name[1]} </div>} 
         return(
                        <>
                               {nameList}
                       </>
         );
    }
            {/* 드래그앤 드롭 파일 컴포넌트 2개 */}
            <ShopImgUpload
              MainFile={MainFile}
              setMainFile={setMainFile}
              MainFileHandler={MainFileHandler}
            />
            <ShopMenuUpload
              MenuFile={MenuFile}
              setMenuFile={setMenuFile}
              MenuFileHandler={MenuFileHandler}
            />
            <RegisterBtn onClick={submitHandler}>등록하기</RegisterBtn>
          </div>
        </WrapBox>
      );
    };
     
     
    ShopInformationModify > index.js
    cf. 위 코드에서 if-else 구문을 추가해 "가게 정보 미리보기" 코드를 구현
     
    //메인 이미지
    const MainImages = styled.div`
      border-radius: 10px;
      width: 340px;
      height: 300px;
      display: flex;
      align-items: center;
      justify-content: center;
    `;

    const MainImg = styled.div`
      width: 300px;
      height: 300px;
      background: url(${props => props.preview});
      background-repeat: no-repeat;
      background-size: cover;
      box-sizing: border-box;
      background-size: contain;
      border-radius: 6px;
    `;

    배경에  props를 전달받아 이미지를 띄운다.

    const [flip, setFlip] = useState(true);
    flip의 참/거짓을 통해 미리보기 창의 유무를 선택
    const onPreview = e => {
        e.preventDefault();
        setFlip(!flip);
      };
    미리보기 버튼을 누르면 !flip되면서 else문의 코드가 실행된다.

     
    if (flip == true) {
        return (
          <WrapBox>
            <TopBar />
            <div>
              <ShopRegistering>가게 정보 수정</ShopRegistering>
              {SHOP_DATA.map(data => {
                return (
                  <React.Fragment key={data.key}>
                    <ShopIntroduceName>{data.keyword}</ShopIntroduceName>
                    <ShopIntroduce
                      type="text"
                      height={data.height}
                      value={storeData[data.key]}
                      onChange={e => {
                        e.preventDefault();
                        updateStoreData({ [data.key]: e.target.value });
                      }}
                      placeholder={data.placeholder}
                    />
                  </React.Fragment>
                );
              })}

              {/* 드래그앤 드롭 파일 컴포넌트 2개 */}
              <ShopImgUpload
                MainFile={MainFile}
                setMainFile={setMainFile}
                MainFileHandler={MainFileHandler}
              />
              <ShopMenuUpload
                MenuFile={MenuFile}
                setMenuFile={setMenuFile}
                MenuFileHandler={MenuFileHandler}
              />

              <BeforeShowBtn onClick={onPreview}>미리보기</BeforeShowBtn>

              <ModifyBtn onClick={submitHandler}>수정하기</ModifyBtn>
    버튼에 제출 함수를 걸어도 되고, form태그를 이용해 제출해도 된다. 
            </div>
          </WrapBox>
        );
      } else {
        return (
          <WrapBox>
            <TopBar />
            <form onSubmit={submitHandler}>
              <ShopRegistering>가게 정보 수정</ShopRegistering>
              <WhiteModal>
                <MainImages>
                  {MainFile.map((img, idx) => {
                    return (
                      <MainImg
                        key={idx}
                        preview={window.URL.createObjectURL(img)}
    Blob 객체를 url로 바꾸어 이미지를 띄운다. 
                      ></MainImg>
                    );
                  })}
                </MainImages>
                <ShopDetailHeader>
                  <ShopName>{storeData.name}</ShopName>
                  <IsRegistered>등록가게</IsRegistered>
                </ShopDetailHeader>{" "}
                <SubTitle>소개</SubTitle>
                <ShopDesc>{storeData.readme}</ShopDesc>
                <DetailInfoCard>
                  <DetailInfoItem
                    category="전화번호"
                    content={storeData.phoneNumber}
                    fontSize="14px"
                  />
                  <DetailInfoItem
                    category="주소"
                    content={storeData.address}
                    fontSize="14px"
                  />
                  <DetailInfoItem
                    category="운영시간"
                    content={storeData.openTime}
                    fontSize="14px"
                  />
                  <DetailInfoItem
                    category="문의"
                    content="카카오톡 | 인스타그램"
                    fontSize="14px"
                  />
                </DetailInfoCard>
                <SubTitle>대표 케이크</SubTitle>
                <CakeImages>
                  {MenuFile.map((img, idx) => {
                    return (
                      <CakeProductImage
                        key={idx}
                        preview={window.URL.createObjectURL(img)}
                      ></CakeProductImage>
                    );
                  })}
                </CakeImages>
                <CloseButton onClick={onPreview}>닫기</CloseButton>
              </WhiteModal>
              <BeforeShowBtn>미리보기</BeforeShowBtn>
              <ModifyBtn type="submit">수정하기</ModifyBtn>
            </form>
          </WrapBox>
        );
      }
    };

    컴포넌트에 ctrl+마우스 좌클릭을 하면, 해당 컴포넌트로 이동한다.

    DetailInfoItem.js

    import
    styled from "styled-components";

    // 세부 정보
    const ItemBox = styled.div`
      display: flex;
      font-size: ${(props) => props.fontSize};
      & + & {
        margin-top: 30px;
      }
    부모 선택자 사이의 간격을 30px로 준다는 의미이다.
    `;
    const Category = styled.div`
      width: 100px;
      font-weight: 600;
      color: var(--main-pink);
    `;
    const Content = styled.div`
      width: 100%;
      color: var(--black-text);
      word-break: keep-all; 어절이 끊기지 않고 줄바꿈한다.
    `;

    const DetailInfoItem = ({ category, content, fontSize }) => {
      return (
        <ItemBox fontSize={fontSize}>
          <Category>{category}</Category>
          <Content>{content}</Content>
        </ItemBox>
      );
    };

    export default DetailInfoItem;
     
    styles.js
    import styled from "styled-components";

    //전체 428px;
    export const WrapBox = styled.div`
      width: 100%;
      height: 1850px;
    `;
    반응형 ui로 작업했기에, position이 아닌 margin을 사용했고 전체 ui의 크기를 %로 두었다.
    //가게 정보 등록
    export const ShopRegistering = styled.div`
      width: 162px;
      height: 29px;
      margin: 56px 0px 60px 24px;
      font-family: "Apple SD Gothic Neo";
      font-style: normal;
      font-weight: 700;
      font-size: 24px;
      line-height: 29px;
      z-index: 1;
    `;

    //이름
    export const ShopIntroduceName = styled.div`
      width: 120px;
      height: 22px;
      margin: 30px 0px 0px 24px;
      font-family: "Apple SD Gothic Neo";
      font-style: normal;
      font-weight: 700;
      font-size: 18px;
      line-height: 22px;
      text-transform: uppercase;
      color: #202020;
      z-index: 1;
    `;

    //인풋 칸
    export const ShopIntroduce = styled.input`
      width: 90%;
      height: ${props => props.height};
      margin: 10px 0px 0px 4.5%;
      background: var(--sub-lightgray);
      border-radius: 6px;
      border: 1px solid var(--sub-lightgray);
      text-align: justify;
    `;
    내용 양에 따라 입력칸의 높이가 변하도록 props를 이용했다 .

    //등록하기 버튼
    export const RegisterBtn = styled.button`
      margin: 70px 0px 0px 24px;
      width: 90%;
      height: 60px;
      left: 24px;
      top: 1593.19px;
      background: var(--main-pink);
      border-radius: 6px;
      border: 1px solid var(--main-pink);
      color: white;
      font-family: "Apple SD Gothic Neo";
      font-style: normal;
      font-weight: 700;
      font-size: 16px;
      line-height: 19px;
    `;
     

     

     

Designed by Tistory.