[Bettermall] 반려동물 전용 쇼핑몰 하이브리드앱 풀스택 개발 프로젝트

송나은·2021년 5월 2일
1

Project

목록 보기
5/6

Overview

  • 기술스택: Ruby on Rails, React.js, framework7, Tailwind CSS
  • 작업기간: 2021.04.12 ~ 2021.05.06

Tab1. 홈 & 메뉴

Panel

햄버거 모양의 메뉴탭이 왼쪽 상단에 제공됩니다.

  • 로그인 여부 확인
    const loggedIn = !!getToken().token;
    토큰여부를 확인하여 로그인 및 회원가입 버튼과 로그아웃 버튼 조건부 렌더됩니다.
  • 상품 카테고리 및 브랜드 정보를 제공합니다.

Tabbar

홈, 카테고리, 마이페이지, 문의하기의 4가지 view를 제공합니다.

  • 마이페이지와 장바구니, 상품구매, 찜하기는 로그인 한 사용자에게만 접근 권한이 주어집니다.

Swiper

이미지 슬라이드로 여러가지 이미지를 넘기면서 볼 수 있습니다.
이미지 클릭 시 상품 상세페이지로 이동됩니다.

회원가입 & 로그인 & 로그아웃

Yup 라이브러리

각각의 정보에 유효성 검사를 추가했습니다.

  • 이름, 이메일, 비밀번호, 비밀번호 확인은 필수 입력사항이며, 모두 빠짐없이 입력해야 회원가입 버튼이 활성화됩니다.
  • password는 4~50자로 제한을 두었습니다. 최소/최대 글자 수는 조절이 가능합니다.
  • password확인은 password와 비교하여 일치여부를 판단합니다.
const SignUpSchema = Yup.object().shape({
  name: Yup.string().required("필수 입력사항 입니다"),
  email: Yup.string().email().required("필수 입력사항 입니다"),
  password: Yup.string()
    .min(4, "길이가 너무 짧습니다")
    .max(50, "길이가 너무 깁니다")
    .required("필수 입력사항 입니다"),
  password_confirmation: Yup.string()
    .oneOf([Yup.ref("password"), null], "비밀번호가 일치하지 않습니다.")
    .required("필수 입력사항 입니다"),
});

Formik

initialvalues를 설정하고 Yup을 활용한 ValidationSchema를 적용했습니다.

  • 버튼을 눌러 submit이 실행되면 입력받은 값이 백엔드로 전송되며 User정보가 저장됩니다.

Tab2. 카테고리

쿼리 파라미터

카테고리 버튼을 클릭할 때마다 백엔드로 요청을 보내, 카테고리별 상품을 받아옵니다.

getItems(
  {
    q: {
      category_id_eq: animalNum,
      subcategory_id_eq: categoryNum,
    },
  })

Ransack

  • Ransack과 Ransacker를 이용하여 기존 로직을 사용하기 편하게 바꿔 줄 수 있습니다.
    ransacker :status, formatter: proc {|status| statuses[status]}
    enum으로 지정한 status값을 string이 아닌 integer로 검색할 수 있습니다.
  • /items?category_id_eq=1&category_id_eq=2 와 같이 보내는 형식이 같지만 파라미터에 SQL 문법이 노출되지 않기 때문에, 해커로부터의 공격(SQL-Injection)을 방지할 수 있습니다.

enum

현재 판매중인(=active) 상태인 아이템만 불러오기 위해 enum을 사용했습니다.
enum status: {active:0, disabled:1, pending: 2}

  • 판매중인 상품은 active, 절판된 상품은 disabled, 입고예정인 상품은 pending로 세 가지 상태를 관리할 수 있습니다.
  • 정수값으로 데이터를 입력하기 때문에 불필요한 string을 사용하지 않아 메모리를 save할 수 있고, 다양한 method를 지원합니다.

Tab 3. 마이페이지-찜한상품

Current_api_user

로그인한 사용자가 찜한 상품을 조회하기 위해 user의 id에 접근하여 liked_items을 마이페이지에 불러옵니다.

  • like는 user와 item에 각각 has_many 관계로 포함되고 있습니다.
  • User model에 liked_items를 추가하여 User의 liked_items를 불러왔습니다.
  • (=User.find(user_id)), user_id는 로그인 한 유저의 token에 포함된 payload에서 가져옵니다.

Recoil

  • 사용자가 좋아한 상품의 경우 찜버튼이 활성화됩니다.
  • 좋아한 상품의 아이디를 배열에 담아 전역상태로 관리하며, 상품리스트에서 배열에 담긴 상품의 아이디를 포함하는 경우 찜버튼을 활성화합니다.
  • 이미 찜한 상품인 경우 찜버튼 클릭 시 찜 리스트에서 삭제됩니다. 찜한 상품을 취소하는 경우, 배열의 filter기능을 이용하여 해당 상품의 아이디를 뺀 나머지 배열을 반환하고 찜버튼을 비활성화합니다.
  • 찜한 상품을 조회할 때 lilkeItems의 id값을 담은 배열을 LikeState에 할당합니다.
// LikeItem
const LikeItem = () => {
  const [likeList, setLike] = useState([]);
  const loggedIn = !!getToken().token;
  const [islike, setLikeState] = useRecoilState(LikeState);

  useEffect(async () => {
    if (loggedIn) {
      const likeItems = (await getLikeItems()).data;
      setLike([...likeItems]);
      setLikeState(likeItems.map(data => data.id));
    }
  }, []);
}

// LikeBtn
const LikeBtn = props => {
  const loggedIn = !!getToken().token;
  const [isLike, setIsLike] = useRecoilState(LikeState);

  const addWishList = async e => {
    if (loggedIn) {
      const id = Number(e.target.dataset.idx);
      if (e.target.innerText === "🤍") {
        await postLikeItem({
          item_id: id,
        });
        setIsLike([...isLike, id]);
        f7.dialog.alert("찜 리스트에 추가되었습니다.");
      }
      if (e.target.innerText === "💖") {
        await deleteLikeItems(id);
        const isUnLike = isLike.filter(el => el !== id);
        setIsLike(isUnLike);
        f7.dialog.alert("찜 리스트에서 제거되었습니다.");
      }
    } else {
      f7.dialog.alert("로그인이 필요한 서비스입니다.");
    }
  };
  return (
    <Link data-idx={props.id} onClick={addWishList}>
      {isLike.includes(props.id) ? "💖" : "🤍"}
    </Link>
  );
};

export default LikeBtn;

찜버튼은 재사용하기 위해 컴포넌트를 따로 분리했습니다.

상세페이지

  • 찜버튼과 구매하기 버튼은 재사용 하였습니다.
  • 구매하기 버튼 클릭 시 Action Sheet에서 전역 상태로로 할당했던 option과 수량을 reset합니다.

장바구니

enum

구매하기 전 상태인 LineItem을 가져오기 위해 enum을 사용했습니다.
enum status: {active: 0, disabled: 1. pending: 2}

  • 구매하기 전 Order는 pending으로 장바구니에서 보여질 수 있고, 주문이 완료된 Order는 active로 주문내역에서 보여질 수 있으며, 주문 취소 시 disabled로 관리할 수 있습니다.

Recoil

  • select된 option의 id값은 option을 선택했을 때 parentNode에서 받아온 option에서 options.find(el=>el).id 을 이용했습니다.

구매버튼은 재사용하기 위해 컴포넌트를 따로 분리했습니다.
props로 description,name,options,id,price,image,page을 받아와 사용합니다.

주문 & 결제

  • 제품 상세페이지에서 구매할수도 있고, 메인 화면에서 바로구매+장바구니 담기가 가능합니다.

Recoil

  • option과 수량을 선택할 때 option_name과 option_id, quantity를 전역 상태에 할당했습니다.
  • 구매버튼/장바구니 버튼을 클릭했을 때 order테이블이 생성되고, 선택한 옵션 및 수량, 주문금액 정보를 db에 저장합니다.
  • 그리고 db에 담은 정보를 다시 조회하여 LineItemState로 관리하며 주문페이지에서 상품정보를 표시할 수 있도록 합니다.
const ActionBuy = props => {
  const loggedIn = !!getToken().token;
  const [quantity, setQuantity] = useRecoilState(QuantityState);
  const [totalPrice, setTotalPrice] = useRecoilState(TotalPriceState);
  const [optionId, setoptionId] = useRecoilState(OptionIdState);
  const [optionName, setOptionName] = useRecoilState(OptionNameState);
  const [lineItem, setLineItem] = useRecoilState(LineItemState);
  const optionPrice = Number(
    optionName.slice(optionName.indexOf("(") + 2, optionName.indexOf("원"))
  );

  const createLineItems = async e => {
    await postOrder({
      receiver_name: "",
      receiver_address: "",
      receiver_phone: "",
      status: e.target.innerText === "장바구니" ? 2 : 1,
    });

    setTotalPrice(Number((props.price + optionPrice) * quantity));

    await postLineItems({
      option_id: optionId,
      quantity: quantity,
      total_price: Number((props.price + optionPrice) * quantity),
    });

    const lineItemOne = await showLineItem();
    setLineItem([lineItemOne.data]);
    f7.views.current.router.navigate("/orders");
    }
  
  };

구매버튼은 재사용하기 위해 컴포넌트를 따로 분리했습니다.

Formik & Yup

회원가입&로그인과 마찬가지로 Formik과 Yup 라이브러리를 이용하여 유효성 검사를 실시하고 submit시 입력받은 데이터를 백엔드로 전송합니다.

values = {
  {receiver_name:""},
  {receiver_addres: ""},
  {receiver_phone: ""}
}

반복되는 UI 재사용

변하지 않는 값들은 전역으로 변수를 선언하고, 반복되는 UI를 재사용했습니다.

Tab3. 마이페이지-주문내역 조회

  • Order 테이블에 status가 active인 order (=주문완료된 order)를 불러옵니다.

Common

recoil_atom / token 관리

Components

1. action_buy

2. likeBtn

3. CardItem

grid나 flex를 이용하지 않고, framework7에서 제공하는 카드 컴포넌트를 응용했습니다.
sns의 피드에 많이 사용하는 컴포넌트 이지만, 기획에 맞는 구조로 짜여져 있어 사용했습니다.

찜페이지, 메인페이지, 카테고리 페이지에서 재사용하기 위해 컴포넌트를 따로 분리했습니다.
(장바구니, 주문내역에서도 활용 가능)

+) 추가로 궁금하신 사항은 문의주세요 ✅

profile
그때그때 공부한 내용과 생각을 기록하는 블로그입니다.

0개의 댓글