[카테캠] 2단계 week 3, 4

werthers·2023년 7월 23일
0

카카오테크캠퍼스

목록 보기
15/16
post-thumbnail

week3 day1

강의 내용 정리

  • product 가져오는 api 개발
    • 2주차까지 api.js 로 진행된 내용을 index.js 와 각각의 api를 가져오는 파트로 분리하여 개발
  • 개발에 사용할 라이브러리 확정
    • 모든 라이브러리를 사용하진 않기 때문에 2주차에 사용했던 것들 중
    • redux + thunks 를 이용해 상태를 관리할 예정
  • layouts 디렉토리 생성
    • GNB content Footer 등으로 나뉘어진 레이아웃을 모아 각 페이지를 구성하기 위해 마지막으로 레이아웃을 구성하는 디렉토리
  • Intersecting 을 통한 스크롤 감지로 데이터를 더 불러오기
    • 가져올 데이터의 갯수가 부족할 경우 state 를 통해 더 이상 데이터를 불러오지 않도록 해야함.
  • 꼭 알아둬야할 prototype method
  • lodash 라이브러리 레포지토리 소스를 확인해서 공부를 해보는 것 중요함.
  • useSWRreact-query 에 대한 공부와 사용 준비
  • 웹 접근성을 위한 alt 태그 사용
  • css 너무 어렵다..
  • 페이지 네이션 이용해 페이지 값 증가 및 조회하기 !
  • 글로벌 로더는 하지 않아도 괜찮다 !
  • 에러 캐칭에서 상태 코드별 에러 캐칭하기

현재 코드에 적용된 css 변경하기

  • 현재 코드에 임의로 적용된 css를 카카오 쇼핑하기에 있는 스타일을 보고 tailwind로 만들어 적용해본다.
  • LoginForm.jsx
    import Container from "../atoms/Container";
    import InputGroup from "../molecules/InputGroup";
    import Button from "../atoms/Button";
    import useInput from "../../hooks/useInput";
    import Title from "../atoms/Title";
    import { useDispatch, useSelector } from "react-redux";
    import { loginRequest, setLogin } from "../../store/slices/userSlice";
    import { useNavigate } from "react-router-dom";
    import { emailExp, passwordExp } from "../../exp";
    
    const LoginForm = () => {
      const navigator = useNavigate();
      function gohome(url) {
        navigator(url);
      }
      const dispatch = useDispatch();
      const { value, handleOnChange } = useInput({
        email: "",
        password: "",
      });
      const email = useSelector((state) => state.email);
      const error = useSelector((state) => state.error);
    
      return (
        <Container className="leading-6 font-sans text-gray-700 m-0 p-0 h-full text-sm">
          <Container className="leading-6 font-sans text-gray-700 m-0 p-0 inline-block overflow-x-hidden w-full text-xs align-middle">
            <Container className="leading-6 font-sans text-gray-700 text-xs m-0 p-0 pt-12">
              <h1 className="leading-6 font-sans text-gray-700 m-0 p-0 block">
                kakao
              </h1>
            </Container>
            <article className="text-base leading-6 font-sans text-gray-700 w-580 h-full mx-auto my-40 mb-42 px-0 py-0 border border-gray-300 text-12 box-border">
              <Container className="text-xs leading-6 font-sans text-gray-700 m-0 relative h-full py-55 pb-50 box-border overflow-hidden">
                <Container className="text-xs leading-6 font-sans text-gray-700 m-0 relative h-full py-55 pb-50 box-border overflow-hidden">
                  <Container className="mb-4 text-lg">
                    <InputGroup
                      className="appearance-none bg-transparent border-0 text-base leading-6 text-gray-900 p-10 pb-8 w-full min-h-45px box-border focus:outline-none caret-black"
                      id="email"
                      type="email"
                      placeholder="이메일을 입력해주세요"
                      //label="이메일"
                      name="email"
                      value={value.email}
                      onChange={handleOnChange}
                    />
                  </Container>
    
                  <Container className="mb-4 text-lg">
                    <InputGroup
                      className="appearance-none bg-transparent border-0 text-sm leading-6 text-gray-900 p-12 pb-8 w-full min-h-45px box-border focus:outline-none caret-black tracking-wider font-small-caps"
                      id="password"
                      type="password"
                      name="password"
                      placeholder="********"
                      //label="비밀번호"
                      value={value.password}
                      onChange={handleOnChange}
                    />
                  </Container>
                  <Container className="text-xs leading-6 font-sans text-gray-700 m-0 p-0 pt-40 text-center">
                    <Button
                      className="appearance-none bg-yellow-400 text-base leading-[51px] font-normal text-gray-900 p-0 m-0 w-full h-50px rounded-md cursor-pointer"
                      onClick={() => {
                        new Promise((resolve, reject) => {
                          //프론트 엔드에서 진행하는 유효성 검사 프로미스
                          if (!emailExp(value.email))
                            reject("이메일 형식이 올바르지 않습니다.");
                          else if (!passwordExp(value.password))
                            reject("비밀번호 형식이 올바르지 않습니다.");
                          resolve(1);
                        })
                          .then(() => {
                            //유효성 검사 완료 후 api 요청
                            dispatch(
                              loginRequest({
                                email: value.email,
                                password: value.password,
                              })
                            ).then((res) => {
                              if (res.payload.success) {
                                //로그인 성공시
                                dispatch(setLogin(true)); //로그인이 됐다면 바로 (로그아웃)으로 버튼이 뜰 수 있게 상태 설정
                                gohome("/"); //성공 시 홈페이지 이동
                              }
                            });
                          })
                          .catch((error) => alert(error));
                      }}
                    >
                      Login
                    </Button>
                    <span className="font-sans text-gray-700 text-center m-0 relative inline-block p-15px font-size-0 line-height-0">
                      or
                    </span>
                  </Container>
                </Container>
              </Container>
            </article>
          </Container>
        </Container>
      );
    };
    export default LoginForm;
  • MainPage.jsx 를 바로 쓰지 않고 Gnb.jsxMainLayout.jsx 로 나누어 현재 로그인 페이지를 렌더링 하였다.
//Gnb.jsx
import { useNavigate } from "react-router-dom";
import Container from "../atoms/Container";
import { useSelector } from "react-redux";
import Button from "../atoms/Button";
import { setLogin } from "../../store/slices/userSlice";
import { deleteCookie } from "../../store/cookies";

const GNB = ({ children }) => {
  const movePage = useNavigate();
  const loginedUser = useSelector((state) => state.user.logined);
  function gohome(url) {
    movePage(url);
  }
  return (
    <Container className="pc_head inner_head fixed left-0 right-0 top-0 z-11000 border-b-2 border-gray-300 bg-white">
      <Container className="text-base leading-6 font-sans text-gray-700 mx-0 px-8"></Container>
      <Container className="text-base leading-6 font-sans text-gray-700 m-0 relative ml-auto p-14 pr-13 pt-0"></Container>
      <Container className="text-base leading-6 font-sans text-gray-700 p-0 flex w-1280 h-79 mx-auto">
        <h1 class="block text-2xl my-4 font-bold">
          <img
            alt="톡쇼핑하기"
            className="overflow-clip-margin-content overflow-clip overflow-x-auto overflow-y-auto"
            src="https://st.kakaocdn.net/commerce_ui/front-talkstore/real/20230707/130532/assets/images/pc/pc_logo.png"
          />
        </h1>
        <Container className="text-base leading-6 font-sans text-gray-700 relative py-3 px-6">
          <Button
            className="text-base leading-7 font-sans text-black no-underline block py-3 px-0 font-semibold"
            onClick={() => {
              let url = "/login";
              if (loginedUser) {
                setLogin(false); //로그아웃 상태로 만듦
                deleteCookie("token"); //유효시간 관리하는 토큰 쿠키 삭제
                alert("logout");
                url = "/";
              }
              gohome(url);
            }}
          >
            {loginedUser ? "로그아웃" : "로그인"}
          </Button>
          <Button
            className="text-base leading-7 font-sans text-black no-underline block py-3 px-0 font-semibold"
            onClick={() => {
              gohome("/signup");
            }}
          >
            회원가입 이동
          </Button>
        </Container>
      </Container>
    </Container>
  );
};

export default GNB;

week 3 day2

과제 1 구현하기

  • 기존의 services 디렉토리의 api.js 분리하기
    • instatnce 가져오는 index.js
    • user login/register 담당하는 user.js
    • 상품의 데이터를 가져오는 product.js 로 분리했다.
  • Product 를 가져오기 위한 jsx 개발
    • atoms/card.jsx
      import { Link } from "react-router-dom";
      import "../../styles/atoms/Card.css";
      
      const Card = ({ to, children }) => {
        return (
          <Link className="card" to={to}>
            {children}
          </Link>
        );
      };
      
      export default Card;
    • molecules/ProductCard.jsx
      import { comma } from "../../utils/convert";
      import Card from "../atoms/Card";
      
      const ProductCard = ({ product }) => {
        return (
          <Card to={`/product/${product.id}`}>
            <img src={product.image} alt={product.productName} />
            <h3>{product.productName}</h3>
            <p>{comma(product.price)}</p>
          </Card>
        );
      };
      
      export default ProductCard;
    • organisms/ProductGrid.js
      import ProductCard from "../molecules/ProductCard";
      
      const ProductGrid = ({ products }) => {
        return (
          <div className="product-grid">
            {products.map((product) => (
              <ProductCard key={product.id} product={product} />
            ))}
          </div>
        );
      };
      
      export default ProductGrid;
    • templates/ProductSection.jsx
      import { useEffect, useRef, useState } from "react";
      import { useDispatch, useSelector } from "react-redux";
      import { getProducts } from "../../store/slices/productSlice";
      import ProductGrid from "../organisms/ProductGrid";
      import Container from "../atoms/Container";
      
      const ProductSection = () => {
        const [page, setPage] = useState(0);
        const bottomObserver = useRef(null);
        const dispatch = useDispatch();
        const { products, loading, error, isEnd } = useSelector(
          (state) => state.products
        );
        const io = new IntersectionObserver(
          (entries, observer) => {
            entries.forEach((entry) => {
              if (entry.isIntersecting) {
                setPage((page) => page + 1);
              }
            });
          },
          {
            threshold: 1,
          }
        );
      
        useEffect(() => {
          io.observe(bottomObserver.current);
        }, []);
      
        useEffect(() => {
          dispatch(getProducts(page));
        }, [dispatch, page]);
      
        return (
          <Container className="product-section">
            {loading && <p>Loading...</p>}
            {error && <p>Error</p>}
            <ProductGrid products={products} />
            <div ref={bottomObserver}></div>
          </Container>
        );
      };
      
      export default ProductSection;
  • 페이지네이트와 무한스크롤
    • 페이지네이트된 데이터는 변경이 자주 있어서는 안되거나 무한 스크롤에는 완전히 적합하지 않다.
    • page를 로딩할 때 중복 값이 발생하기 때문이다.
    • 페이지 값을 전달하는 것이 아닌 lastIndex를 요청하는 방식으로 해결이 가능하다. (이 또한 RDS가 아닌 방법이라면 어렵다.)
  • lodash
    • 배열과 관련된 메서드 처리를 해준다.

    • .uniqBy 는 매개변수로 받은 배열을 2번째 인자로 받은 변수를 기준으로 유니크한 값만 배열형태로 리턴

    • 커서를 사용하지 않고 무한 스크롤에 중복을 제거하기 위한 방법 (코드 실행 중간에 백엔드에 데이터가 추가 됐을 때 제대로 못가져오는 상황이 있을 수 있다.)

      state.products = _.uniqBy(
              [...state.products, ...action.payload.response],
              "id"
            );
  • product 가져오기 성공
  • useEffect 를 통해 isEnd 가 true가 되면 서버에 요청을 하지 않도록 막고 무한 스크롤이 제대로 동작하는 것을 확인했다.
  • 하지만 page의 값은 계속해서 증가하는 것 또한 추후에 막아야하지 않을까 싶다 !

week3 day3

ProductDetailPage 만들기

  • 각각 상품에 대한 상세 페이지를 구현한다.
  • 현재 라우팅되지 않지만 각 상품 클릭시 /product/${id} 형식으로 요청이 들어오기 때문에 상세 페이지 구현 후 App.js 에서 각 id로 라우팅 해줘야한다.
  • 비동기 처리를 하는 reducer를 만들고 해당 페이지에 id값 변경 시 호출하도록 설정
    import { useEffect } from "react";
    import { useDispatch } from "react-redux";
    import { useParams } from "react-router-dom";
    import { getDetail } from "../store/slices/detailSlice";
    
    const ProductDetailPage = () => {
      const { id } = useParams(); //string
      const dispatch = useDispatch();
      useEffect(() => {
        dispatch(getDetail(id));
      }, [dispatch, id]);
      return (
        <div>
          <h1>Product Detail Page</h1>
        </div>
      );
    };
    export default ProductDetailPage;

loader 만들기

  • 어떤 페이지를 가져올 때 loading=true 일 경우 Loader.jsx를 통해 사용자에게 로딩 중임을 알린다.
  • loader.jsx를 만들고 해당 product/{id} 가 들어왔을 때 {loading && {Loader /> 와 같이 사용해 로딩 중이라면 로더 컴포넌트를 렌더링 하는 방식으로 사용했다.

react-query & useSWR

  • react-thunks 에서 구조 분해 할당을 통해 useSelector를 불러오게 되면 모두 리렌더링이 되기 때문에 Bad Case로 분류된다.
  • 한 줄씩 모두 가져오는 방법보다 효율적으로 작성할 수 있는 방법이 react-query를 통해 가져오는 방법이다.
  • Slice 제거, reducer 제거 후 get 함수 이후 직접 데이터를 가져온다.
  • 앱 전역에서 쿼리 클라이언트를 사용하기 위해 index.js 에서 QueryClientApp 을 감싸주고 Client 지정을 해준다.
    const {
        data: detail,
        error,
        isLoading,
      } = useQuery(`product/${id}`, () => getProductById(id));
    //와 같이 요청 함수와 라우트 경로를 지정해서 정보를 가져온다.
    //여러 개의 요청을 가져올 땐 useQueries를 사용해 동기적으로 가져올 수 있다.

과제 2 스켈레톤 로더

  • ProductCard ProductGrid 를 수정하여 테스트 이미지를 사진과 같은 크기로 10개씩 loading중일 때 불러오도록 만들었다.

week3 day4

글로벌 로더 설치

  • 관련된 모듈을 사용해도 괜찮다고 나와서 react-global-loader를 설치해서 사용하기로 했다.
  • HomePage.jsx
    import { useEffect } from "react";
    import ProductSection from "../components/templates/ProductSection";
    import { loader } from "react-global-loader";
    import { useSelector } from "react-redux";
    const HomePage = () => {
      const loading = useSelector((state) => state.product.loading);
      const showLoader = () => {
        loader.show();
      };
    
      const hideLoader = () => {
        loader.hide();
      };
    
      useEffect(() => {
        if (!loading) hideLoader();
        else showLoader();
      }, [loading]);
      return <ProductSection />;
    };
    
    export default HomePage;
  • global-loader의 DefaultSpinner를 가져와 loading이 true일 경우 로더를 보여주도록 변경하였다.

interceptor를 이용한 status code 에러 핸들링

  • response에 대한 interceptor로 각 100번대 에러 코드에 대한 console.log 처리를 했지만 각각 api 요청 별 필요한 status code 에러 핸들링이 다르기 때문에 여기서 조금 생각을 더 해봐야 할 것 같다.
  • 각각의 api에 대한 대응을 못하기 때문에 interceptor를 이용한 방법은 놔두고, 나머지 부분을 생각해봤다.
  • 로그인 : 이미 유효성 검사를 해서 401 에러를 제외하고는 api 호출조차 실행시키지 않지만 일단 함수를 만들어 관리하기로 했다.
  • statuscatch.js
    export const checkStatus = (error) => {
      const errorApiType = `error : ${error.type}`;
      const statusCode = error.payload.error.status;
      console.log(errorApiType);
      switch (statusCode) {
        case 101:
          console.log("switching Protocol");
          break;
        case 102:
          console.log("Processing (WedDAV) : 요청 수신 및 처리 중");
          break;
        case 300:
          console.log(
            "Mulitple Choice: request에 대해 하나 이상의 응답이 가능, 하나를 선택해야 합니다."
          );
          break;
        case 301:
          console.log(
            "301(Moved Permantly) : 요청한 리소스의 URI가 변경되었습니다."
          );
          break;
        case 302:
          console.log(
            "302(Found) : 요청한 리소스의 URI가 일시적으로 변경되었을 때를 의미합니다."
          );
          break;
        case 303:
          console.log(
            "303(See Other) : 클라이언트가 요청한 리소스를 다른 URI에서 GET 요청을 통해 얻어야 합니다."
          );
          break;
        case 304:
          console.log(
            "304(Not modified) : 마지막 요청 이후 요청한 페이지가 수정되지 않았습니다. 서버가 이 응답을 표시하면 페이지의 콘텐츠를 표시하지 않습니다."
          );
          break;
        case 305:
          console.log(
            "305(Use Proxy) : 요청한 응답은 반드시 프록시를 통해 접속해야 함을 알려줍니다."
          );
          break;
        case 308:
          console.log("3308(Permanent Redirect)");
          break;
        case 400:
          console.log(
            "400(Bad Request) : 클라이언트의 request가 유효하지 않은 상태를 의미합니다."
          );
          break;
        case 401:
          console.log(
            "401(Unauthorized) : 클라이언트가 권한이 없어 작업을 진행하지 못합니다. 이 요청은 인증이 필요합니다. 보통 서버는 로그인이 필요한 페이지에 대해 이 요청을 제공할 수 있습니다."
          );
          break;
        case 403:
          console.log(
            "403(Forbidden) : 서버가 요청을 거부할 때 입니다. 예를 들어 사용자가 리소스에 대한 필요 권한을 가지고 있지 않을 때를 의미합니다."
          );
          break;
        case 404:
          console.log(
            "404(Not Found) : 서버가 요청한 페이지(resource)를 찾지 못했을 때입니다. 서버에 존재하지 않는 페이지에 대한 요구를 할 때 다음과 같은 status code가 반환됩니다."
          );
          break;
        case 405:
          console.log(
            "405(Method Not Allowed) : 클라이언트의 요청이 허용되지 않은 메서드인 경우입니다. (예를 들어 POST 방식으로만 request가 가능한데 이를 지키지 않고 GET으로 보냈을 때)"
          );
          break;
        case 409:
          console.log(
            "409(Conflict) : 서버가 요청을 수행하는 중에 충돌이 발생했을 때 입니다."
          );
          break;
        case 414:
          console.log(
            "414 : 요청하는 URL(일반적으로는 URL)이 너무 길었을 때의 status code 입니다."
          );
          break;
        case 419:
          console.log(
            "419(Too Many Requests) : 사용자가 일정 시간 동안 너무 많은 request를 보냈을 때 입니다."
          );
          break;
        case 500:
          console.log(
            "500(Internal Server Error) : 서버에 오류가 발생하여 요청을 수행할 수 없을 경우"
          );
          break;
        case 501:
          console.log(
            "501(Not Implemented) : 서버에 해당 요청을 수행할 수 있는 기능이 없는 경우(서버가 요청 메소드를 인식하지 못하는 경우입니다.)"
          );
          break;
        case 502:
          console.log(
            "502(Bad Gateway) : 서버가 게이트웨이나 프록시 역할을 하고 있는 업스트림 서버에서 잘못된 응답을 받았을 경우"
          );
          break;
        case 504:
          console.log(
            "504(Gateway Timeout) : 서버가 게이트웨이나 프록시 역할을 하고 있거나 또는 업스트림 서버에서 제때 요청을 받지 못한 경우."
          );
          break;
        case 511:
          console.log(
            "511(Network Authentication Required) : 네트워크 인증이 필요한 경우입니다."
          );
          break;
        default:
          break;
      }
    }; 
    //login, Register에 적용했다. 
    //200번대의 error라고 보아야 하는 응답이라고 할 수 있을지 모르겠어서 200이 아닌 경우를 모두 처리해줘야하는 것인지 모르겠다.

상품별 상세 페이지 제작

  • ProductDetailPage 에서 로딩이 끝난 후 Detail 이 true라면 detail 객체를 넘겨 각 상품을 표시하는 사이트를 만들어야 한다.
  • pdf에 나와있는 대로 각 섹션을 나눴는데 아직 하나의 파일로 제작하기도 했고 px을 제대로 분할하지 않아서 손봐야 할 것 같다.
    import Container from "../atoms/Container";
    import Photo from "../atoms/Photo";
    
    const ProductDetail = (detail) => {
      console.log(detail);
      console.log(detail.detail.image, detail.detail.productName);
      return (
        <Container className="text-base leading-6 font-sans text-gray-700 text-sm lg:text-base font-HelveticaNeue AppleSDGothicNeo-Regular tahoma Malgun Gothic b9d1c740 \ace0\b515 dotum \b3cb\c6c0 sans-serif text-black">
          <h3 className="font-HelveticaNeue AppleSDGothicNeo-Regular tahoma Malgun Gothic b9d1c740 \ace0\b515 dotum \b3cb\c6c0 sans-serif text-black m-0 p-0 overflow-hidden absolute w-0 h-0 text-xs leading-0 -ml-9">
            제품 상세
          </h3>
          <Container className="text-base leading-6 font-sans text-gray-700 font-HelveticaNeue AppleSDGothicNeo-Regular tahoma Malgun Gothic b9d1c740 \ace0\b515 dotum \b3cb\c6c0 sans-serif text-black">
            <Container className="swiper-theme-color-blue-500 swiper-navigation-size-44 -webkit-text-size-adjust-none text-base leading-6 font-sans text-gray-700 font-HelveticaNeue AppleSDGothicNeo-Regular tahoma Malgun Gothic b9d1c740 \ace0\b515 dotum \b3cb\c6c0 sans-serif text-black p-0 m-auto w-full">
              <Container className="swiper-theme-color-blue-500 swiper-navigation-size-44 -webkit-text-size-adjust-none text-base leading-6 font-sans text-gray-700 font-HelveticaNeue AppleSDGothicNeo-Regular tahoma Malgun Gothic b9d1c740 \ace0\b515 dotum \b3cb\c6c0 sans-serif text-black p-0 flex w-1280 m-auto">
                {/* layout_split*/}
                <Container className="swiper-theme-color-blue-500 swiper-navigation-size-44 -webkit-text-size-adjust-none text-base leading-6 font-sans text-gray-700 font-HelveticaNeue AppleSDGothicNeo-Regular tahoma Malgun Gothic b9d1c740 \ace0\b515 dotum \b3cb\c6c0 sans-serif text-black m-0 w-890 p-30 pr-29 pb-150 border-r-1 border-gray-300">
                  {/* product section */}
                  <h3 className="font-HelveticaNeue AppleSDGothicNeo-Regular tahoma Malgun Gothic b9d1c740 \ace0\b515 dotum \b3cb\c6c0 sans-serif text-black m-0 p-0 overflow-hidden absolute w-0 h-0 text-xs leading-0 -ml-9">
                    제품 상세
                  </h3>
                  <Container className="relative z-20 flex pb-4">
                    <Container className="swiper-container flex-0 flex-shrink-0 w-430">
                      <Photo
                        src={detail.detail.image}
                        alt={detail.detail.productName}
                      ></Photo>
                    </Container>
                    <Container class="swiper-container flex-shrink-0 w-430 mx-auto">
                      {"설명란"}
                    </Container>
                  </Container>
                </Container>
                <Container className="swiper-theme-color-blue-500 swiper-navigation-size-44 -webkit-text-size-adjust-none text-base leading-6 font-sans text-gray-700 font-HelveticaNeue AppleSDGothicNeo-Regular tahoma Malgun Gothic b9d1c740 \ace0\b515 dotum \b3cb\c6c0 sans-serif text-black m-0 p-0 relative w-360 bg-white">
                  {"옵션란" /* purchase section */}
                </Container>
              </Container>
            </Container>
          </Container>
        </Container>
      );
    };
    export default ProductDetail;

week4 day1

강의 내용 정리

  1. 상세 페이지 (Products)
  • 저번 강의와 이번 강의를 다시 보며 만들어 봐야할 것 같다.
  1. UI 라이브러리 선택
    • tailwind css를 사용하기로 결정했다.
  2. 장바구니
    • 큰 일 났다.. 너무 복잡하다..
  3. 아이콘 / 폰트
    • Font Awesome / Remix Icon / Google Fonts / React Icons 사용

useRef useState 구분하여 사용하기

  • 렌더링에 관여하지 않는 상태 관리 → useRef
  • 렌더링에 관여하는 상태 관리, DOM에 접근할 때 → useState
    • 객체 변수로 사용하지 않고 훅을 사용하는 이유 ? 변수는 리마운트 (useEffect 등으로 코드가 다시 실행될 때) 될 때 초기화 되기 떄문이다. 훅은 리마운트시에 데이터의 변경이 생기지 않음.
  • 테일윈드로 ProductInformationColumn.jsx 스타일 설정
  • GNB 로그인 버튼도 정상적으로 고쳤다.
    • 아예 useSelector 사용하지 않고 token을 직접 가져와 확인하는 방식으로 렌더링 했더니 원하던 대로 클릭 후 바로 반영이 된다 !!
    • 로그인 버튼도 GNB의 가장 오른쪽으로 밀었다. (근데 boarder-left를 아무리 줘도 왼쪽으로 구분 선이 나오지 않는다)
  • 이외의 부분들이 css가 뜻대로 적용되지 않아서 회원가입에 tailwind css 입히기 시작
    • 로그인과 비슷한 결과가 나와버렸다..

week4 day2

OptionColumn.jsx

import { useState } from "react";
import Counter from "../atoms/Counter";
import { useMutation } from "react-query";
import { addCart } from "../../services/cart";
import Button from "../atoms/Button";
import { comma } from "../../utils/convert";
import OptionList from "../atoms/OptionList";

const OptionColumn = ({ product }) => {
  const [selectiedOptions, setSelectiedOptions] = useState([]);
  const handleOnClickOption = (option) => {
    //동일 옵션 클릭 방지 코드
    const isOptionSelected = selectiedOptions.find(
      (el) => el.optionId === option.id
    );
    if (isOptionSelected) {
      setSelectiedOptions((prev) =>
        prev.map((el) =>
          el.optionId === option.id ? { ...el, quantity: el.quantity + 1 } : el
        )
      );
      return;
    }
    setSelectiedOptions((prev) => [
      ...prev,
      {
        optionId: option.id,
        quantity: 1,
        price: option.price,
        name: option.optionName,
      },
    ]);
  };
  const handleOnChange = (count, optionId) => {
    setSelectiedOptions((prev) => {
      //추가예정
      return prev.map((el) => {
        if (el.optionId === optionId) {
          return {
            ...el,
            quantity: count,
          };
        }
        return el;
      });
    });
  };
  const { mutate } = useMutation({
    mutationFn: addCart,
  });
  return (
    <div className="option-column w-full h-full mt-10 pt-10 bg-white">
      <h3 className="text-left text-l font-bold boarder-2 border-inherit border-black block">
        옵션 선택
      </h3>
      {/*옵션 선택 영역 */}
      <OptionList options={product.options} onClick={handleOnClickOption} />
      <hr />
      {selectiedOptions.map((option) => (
        <ol key={`${option.id}`} className="selected-option-list">
          <div className="bg-slate-300 rounded-md my-2 px-5 py-3">
            <li className="selected-option">
              <div className="text-left">
                <span calssName="name">{option.name}</span>
              </div>
              <Counter
                className="float-left"
                onIncrease={(count) => handleOnChange(count, option.optionId)}
                onDecrease={(count) => handleOnChange(count, option.optionId)}
              />
              <span calssName="price float-right">{comma(option.price)}</span>
            </li>
          </div>
        </ol>
      ))}
      <div className="total-price mx-10">
        <span className=" float-left">
          총 수량:
          {comma(
            selectiedOptions.reduce((acc, cur) => {
              return acc + cur.quantity;
            }, 0)
          )}</span>
        <span className="float-right">
          총 상품 금액:
          {comma(
            selectiedOptions.reduce((acc, cur) => {
              return acc + cur.quantity * cur.price;
            }, 0)
          )}</span>
      </div>
      <div className="button-group">
        {/* 장바구니 담기 버튼 위치 */}
        <Button
          className="bg-yellow-500 hover:bg-yellow-600 h-auto text-white font-bold py-2 px-1 w-2/3 mt-10 rounded cursor-pointer transition-colors duration-300"
          onClick={() => {
            mutate(
              selectiedOptions.map((el) => {
                return {
                  optionId: el.optionId,
                  quantity: el.quantity,
                };
              }),
              {
                onSuccess: () => {
                  alert("success");
                },
                onError: (e) => {
                  console.log(e);
                  alert("failed");
                },
              }
            );
          }}
        >
          구매하기
        </Button>
        {/* 톡딜가 구매 : 개발 x */}
        <Button
          className="bg-gray-500 hover:bg-yellow-600 text-white font-bold py-2 px-1 w-2/3 rounded cursor-pointer transition-colors duration-300"
          onClick={() => {}}
        >
          톡딜가 구매하기
        </Button>
      </div>
    </div>
  );
};
export default OptionColumn;

OptionList.jsx

import { comma } from "../../utils/convert";

const OptionList = ({ options, onClick }) => {
  return (
    <div className="h-40 overflow-y-auto">
      <ol className="option-list text-left border-2 h-300 border-zinc-300 overflow-y-auto">
        <h4 className="text-left font-bold border-zinc-300 border-b-2">구성</h4>
        {options.map((option, index) => (
          <li
            key={option.id}
            className="option px-3 py-3 border-zinc-300 border-b-2 "
            onClick={() => onClick(option)}
          >
            <div className="">
              <span className="name mr-3 boarder-b-4 border-solid boarder-slate-300">
                {index + 1}. {option.optionName}
              </span>
            </div>
            <div className="font-bold">
              <span className="price">{comma(option.price)}</span>
            </div>
          </li>
        ))}
      </ol>
    </div>
  );
};
export default OptionList;

스크린샷 2023-07-18 오후 6.21.31.png

  • css 대략적으로 입힌 후 구매 버튼 입력 시 api에 올바른 형식으로 요청하는 것까지 확인했다. 아직 장바구니를 만들지 않아서 다음 페이지를 라우트 하진 못했다.

404 or 상품을 찾을 수 없습니다. 페이지 이동

  • 에러 페이지에 대한 CSS도 예쁘게 만들고 싶은데 솔직히 자신이 없어 템플릿 보고 따라치면서 이렇게 해야하는 구나 하며 만들었다..
  • 만든 사이트를 error 내용을 출력하는 대신 페이지 형태로 넣어줬다.
    return (
        <div>
          {isLoading && <Loader />}
          {error && <ErrorPage />}
          {data && <ProductDetailTemplate product={product} />}
        </div>
      );
  • error를 파라미터로 가지고 들어가는 경우 404가 아니라 다른 경우에도 사용할 수 있도록 페이지를 수정하였고, 없는 경우 404가 뜰 수 있도록 적용해놓았다.
    import "../styles/pages/error.css";
    const ErrorPage = ({ error }) => {
      let errorNum = 404;
      if (error) {
        errorNum = error.response.status;
      }
      return (
        <div id="notfound">
          <div class="notfound">
            <div class="notfound-404">
              <h3>Oops! Page not found</h3>
              <h1>
                <span>{error ? parseInt(errorNum / 100) : 4}</span>
                <span>{error ? parseInt((errorNum % 100) / 10) : 4}</span>
                <span>{error ? parseInt(errorNum % 10) : 4}</span>
              </h1>
            </div>
            {error ? (
              <h2>{error.message}</h2>
            ) : (
              <h2>we are sorry, but the page you requested was not found</h2>
            )}
          </div>
        </div>
      );
    };
    
    export default ErrorPage;

/product/18 요청 시 ( 없는 요청 )

  • Loader 작동
  • 404page

궁금한 점

  • 구매하기와 장바구니 담기를 나누어 구분하면 구매하기 클릭 시 주문하는 api 요청을 해야하는데 이 경우 토큰만 보내서 장바구니에 있는 전체를 구매 처리 후 장바구니가 비워진 채 응답받는 것으로 명세서에 적혀져 있는데 구매하기 버튼 클릭 시 → /carts/add 후 요청을 해야한다면 ? → 원래 장바구니에 있는 것도 다 같이 사짐

week4 day3

상세페이지 → 장바구니 담기

  • 이미 존재하는 옵션에 대해 장바구니 담기를 시도할 시 500번 에러 발생
  • selectionOptions 의 내용과 현재 /carts 로 조회한 장바구니와 겹치는 내용에 대해 비교해서 겹칠 경우 수량만 증가시켜 장바구니 수정 요청, 나머지는 담기 요청
    const checkCart = ({ products }) => {
        products.map((el) => {
          if (el.id === product.id) {
            el.carts.map((cart) => {
              selectiedOptions.map((item) => {
                console.log(cart);
                if (cart.option.id === item.optionId) {
                  const isOptionSelected = modifiedOptions.find(
                    (mod) => mod.cartId === cart.id
                  );
    
                  if (isOptionSelected) {
                    setModifiedOptions((prev) =>
                      prev.map((mod) =>
                        mod.cartId === cart.id
                          ? { ...mod, quantity: mod.quantity + cart.quantity }
                          : mod
                      )
                    );
                  } else {
                    setModifiedOptions((prev) => [
                      ...prev,
                      { cartId: cart.id, quantity: item.quantity + cart.quantity },
                    ]);
                  }
                }
              });
              console.log(cart.option.id);
              setSelectiedOptions(
                selectiedOptions.filter((item) => item.optionId !== cart.option.id)
              );
            });
          }
        });
      };
  • 아직 끝나지 않았지만 서버가 502 에러를 뿜는 동안 적어본다.
  • 되게 좋지 않아보이는 코드이지만.. 위에 적은 기능들을 구현하기 위해 노력해보았다.
  • 아직 완성은 아니지만 우선 수정으로 요청 보낼 아이들과 담기 할 친구들을 분리하였다. → post api 요청 하나 더 만들기
  • mutation을 두개 사용하는 방법을 몰라 해맸는데 그냥 단순하게 {mutate: mu2}와 같이 해결했다.

오잉 ..?

  • 갑자기 서버가 꺼졌다 켜지더니 로그아웃 상태에서도 장바구니에 담겨져서 확인이 안된다.. 무조건 onSuccess로 가는 중.. (현재 고쳐짐)

week4 day4

장바구니 페이지 만들기

  • CartList.jsx
    import Container from "../atoms/Container";
    import Box from "../atoms/Box";
    import { useCallback, useEffect, useState } from "react";
    import { comma } from "../../utils/convert";
    import Card from "../atoms/Card";
    import CartItem from "../atoms/CartItem";
    import Button from "../atoms/Button";
    import { useMutation } from "react-query";
    import { modifiedCart } from "../../services/cart";
    
    const CartList = ({ data }) => {
      const [cartItems, setCartItems] = useState([]);
      const [totalPrice, setTotalPrice] = useState(0);
      const { mutate } = useMutation({
        mutationFn: modifiedCart,
      });
      useEffect(() => {
        setCartItems(data?.data?.response?.products);
        setTotalPrice(data?.data?.response.totalPrice);
      }, [data]);
      const getTotalCart = useCallback(() => {
        let count = 0;
        cartItems.forEach((item) => {
          item.carts.forEach((cart) => {
            count += cart.quantity;
          });
        });
        return comma(count);
      }, [cartItems]);
      const handleOnChangeCount = (optionId, quantity, price) => {
        setTotalPrice((prev) => prev + price);
        setCartItems((prev) => {
          return prev.map((item) => {
            return {
              ...item,
              carts: item.carts.map((cart) => {
                if (cart.id === optionId) {
                  return { ...cart, quantity: quantity };
                }
                return cart;
              }),
            };
          });
        });
      };
      return (
        <Container className="cart-list">
          <Box>
            <h1>장바구니</h1>
          </Box>
          <Card>
            {/*상품 별 장바구니 항목 */}
            {Array.isArray(cartItems) &&
              cartItems.map((item) => {
                return (
                  <CartItem
                    key={item.id}
                    item={item}
                    onChage={handleOnChangeCount}
                  />
                );
              })}
          </Card>
          <Card>
            <div className="row">
              <span className="expect">예상 주문금액</span>
              <div className="sum-price">{comma(totalPrice)}</div>
            </div>
          </Card>
          <Button className="order-button" onClick={() => {}}>
            {
              //cart update
              mutate()
              //navigate to page
            }
          </Button>
          <span>{getTotalCart()}건 주문하기</span>
        </Container>
      );
    };
    export default CartList;
  • CartPage.jsx
    import { Suspense } from "react";
    import { inCart } from "../services/cart";
    import { useQuery } from "react-query";
    import Loader from "../components/atoms/Loader";
    import CartList from "../components/molecules/CartList";
    
    const CartPage = () => {
      const { data } = useQuery("cart", () => inCart());
      return (
        <Suspense fallback={<Loader />}>
          <CartList data={data} />
        </Suspense>
      );
    };
    export default CartPage;
  • CartItem.jsx
    import { comma } from "../../utils/convert";
    import Box from "./Box";
    import Card from "./Card";
    import Counter from "./Counter";
    const CartItem = ({ item, onChange }) => {
      return (
        <Box className="cart-item-box">
          <h5>{item.productName}</h5>
          {item.carts.map((cart) => {
            <Card key={cart.id} className="cart">
              <div className="option-name">
                <span>{cart.option.optionName}</span>
              </div>
              <div className="row">
                <Counter
                  onIncrease={(count) =>
                    onChange(cart.id, count, cart.option.price)
                  }
                  onDecrease={(count) =>
                    onChange(cart.id, count, cart.option.price)
                  }
                />
                <div className="price">
                  <span>{comma(cart.option.price * cart.quantity)}</span>
                </div>
              </div>
            </Card>;
          })}
          <Card className="total-price">
            <div className="row">
              <h5>주문금액</h5>
              <div className="price">
                {comma(
                  item.carts.reduce((acc, cur) => {
                    return acc + cur.option.price * cur.quantity;
                  }, 0)
                )}</div>
            </div>
          </Card>
        </Box>
      );
    };
    export default CartItem;
  • 강의를 보며 구현해봤다. 로직을 다듬기는 해야할듯.
  • aws의 코드위스퍼를 깔아봣따..
  • css 적용 !
     import Container from "../atoms/Container";
    import Box from "../atoms/Box";
    import { useEffect, useRef, useState } from "react";
    import { comma } from "../../utils/convert";
    import Card from "../atoms/Card";
    import CartItem from "../atoms/CartItem";
    import Button from "../atoms/Button";
    import { useMutation } from "react-query";
    import { modifiedCart } from "../../services/cart";
    import { useNavigate } from "react-router-dom";
    
    const CartList = ({ data }) => {
      const navigate = useNavigate();
      const initPayload = useRef([]);
      const [cartItems, setCartItems] = useState([]);
      const [totalPrice, setTotalPrice] = useState(0);
      const [updatePayload, setUpdatePayload] = useState([]);
    
      const { mutate } = useMutation({
        mutationFn: modifiedCart,
      });
    
      useEffect(() => {
        setCartItems(data?.data?.response?.products);
        setTotalPrice(data?.data?.response.totalPrice);
      }, [data]);
    
      useEffect(() => {
        initPayload.current = data?.data?.response?.products;
      }, []);
    
      const getTotalCart = () => {
        let count = 0;
        if (Array.isArray(cartItems)) {
          cartItems.forEach((item) => {
            item.carts.forEach((cart) => {
              count += cart.quantity;
            });
          });
        }
        return comma(count);
      };
    
      const handleOnChangeCount = (optionId, quantity, price) => {
        setUpdatePayload((prev) => {
          const isExist = prev.find((item) => item.cartId === optionId);
          if (isExist) {
            if (quantity < 1) {
              return [
                ...prev.filter((item) => item.cartId !== optionId),
                {
                  cartId: optionId,
                  quantity: 0,
                },
              ];
            }
            return [
              ...prev.filter((item) => item.cartId !== optionId),
              {
                cartId: optionId,
                quantity,
              },
            ];
          }
          return [
            ...prev,
            {
              cartId: optionId,
              quantity,
            },
          ];
        });
    
        setTotalPrice((prev) => prev + price);
        setCartItems((prev) => {
          return prev.map((item) => {
            return {
              ...item,
              carts: item.carts.map((cart) => {
                if (cart.id === optionId) {
                  return { ...cart, quantity: quantity };
                }
                return cart;
              }),
            };
          });
        });
      };
      return (
        <Container className="cart-list ">
          <Box className="pt-4">
            <h1>장바구니</h1>
          </Box>
          <Card>
            {/*상품 별 장바구니 항목 */}
            {Array.isArray(cartItems) &&
              cartItems.map((item) => {
                return (
                  <CartItem
                    key={item.id}
                    item={item}
                    onChange={handleOnChangeCount}
                  />
                );
              })}
          </Card>
          <Card>
            <div className="row border-2 border-slate-200 bg-white">
              <span className="expect font-bold">주문 예상금액</span>
              <div className="sum-price">{comma(totalPrice)}</div>
            </div>
          </Card>
          <Button
            className="order-button bg-gray-500 hover:bg-yellow-600 text-white font-bold py-2 px-1 w-full rounded cursor-pointer transition-colors duration-300"
            onClick={() => {
              mutate(updatePayload, {
                onSuccess: (data) => {
                  navigate("/order");
                },
                onError: (err) => {},
              });
            }}
          >
            <span>{getTotalCart()}건 주문하기</span>
          </Button>
        </Container>
      );
    };
    export default CartList; //CartList.jsx
    import { comma } from "../../utils/convert";
    import Box from "./Box";
    import Card from "./Card";
    import Counter from "./Counter";
    import Photo from "./Photo";
    const CartItem = ({ item, onChange }) => {
      return (
        <Box className="cart-item-box border-t-2 border-b-2 border-solid bg-white p-16 rounded-lg">
          <div className="flex ">
            <div className="w-20 h-20 mx-5 mt-2 ">
              <Photo src={`/images/${item.id}.jpg`} />
            </div>
            <h5 className=" ml-5 mt-5 h-5">{item.productName}</h5>
          </div>
          {item.carts.map((cart) => {
            return (
              <Card
                key={cart.id}
                className="cart block border-2 border-slate-200 m-2"
              >
                <div className="option-name">
                  <span className="text-black">{cart.option.optionName}</span>
                </div>
                <div className="row">
                  <Counter
                    onIncrease={(count) =>
                      onChange(cart.id, count, cart.option.price)
                    }
                    onDecrease={(count) =>
                      onChange(cart.id, count, cart.option.price)
                    }
                  />
                  <div className="price">
                    <span>{comma(cart.option.price * cart.quantity)}</span>
                  </div>
                </div>
              </Card>
            );
          })}
    
          <Card className="total-price ">
            <div className="row">
              <h5 className="px-10 text-center font-bold">주문금액</h5>
              <div className="price">
                {comma(
                  item.carts.reduce((acc, cur) => {
                    return acc + cur.option.price * cur.quantity;
                  }, 0)
                )}</div>
            </div>
          </Card>
        </Box>
      );
    };
    export default CartItem; //CartItem.jsx

3~4주차 회고

음 .. 굉장히 정신없고 힘든 두 주였다. 포스팅을 까먹었으니..
react 자체에 대해 다뤄본 적이 전혀 없어서 그럴 수 있겠지만 단순한 문법 하나 하나 익숙치 않아 서칭을 해야하는 경우가 많았다. 익숙해지겠지..

그래도 꽤 얻어 가는 부분이 많은 것 같다. useStateuseRef를 구분해 사용하는 것도 알게 되고 useCallback을 통해 함수 재사용시 렌더링을 효과적으로 하는 방법 등 cs적으로도 많이 배우며 프로젝트를 진행할 수 있고 멘토님의 피드백과 멘토링도 조만간 신청할 예정이다.

profile
Hello World !

0개의 댓글