쇼핑몰 구현 프로젝트

[쇼핑몰 구현 프로젝트] 03. Frontend 틀잡기

binning 2024. 6. 22. 22:58

Tailwind CSS의 라이브러리인 Flowbite React를 주로 사용하여 클라이언트 화면을 구성하였다.

Flowbite React는 디자인된 Component들을 import 한 번으로 쉽게 사용할 수 있어 매우 편리했다.

Mern 풀스택을 처음부터 끝까지 스스로 만들어보는 것이 주 목적이기 때문에 디자인에 크게 신경 쓰지는 않았다.

flex와 grid 다루는 것이 좀 힘들었던 것 빼면 무난하게 진행할 수 있었다.

파일 구성

 

① Layout

    <Nav fluid rounded>
      <NavbarBrand href="/">
        <img src={mainLogo} className="mr-3 h-6 sm:h-9" alt="Logo" />
        <span className="self-center whitespace-nowrap text-xl font-semibold dark:text-white">Men's Fashion</span>
      </NavbarBrand>
      <NavbarToggle />
      <NavbarCollapse>
        <NavbarLink href="/" active>
          Home
        </NavbarLink>
        {!auth.isLoggedIn && (
          <NavbarLink href="/signup">
            Signup
          </NavbarLink>
        )}
        {!auth.isLoggedIn && (
          <NavbarLink href="/login">Login</NavbarLink>
        )}
        {auth.isLoggedIn && (
          <NavbarLink href="/" onClick={auth.logout}>Logout</NavbarLink>
        )}
        <NavbarLink href={`/cart/${auth.userId}`}>Cart</NavbarLink>
        <NavbarLink href={`/user/${auth.userId}`}>MyPage</NavbarLink>
        {auth.isAdmin && (
          <NavbarLink href="/product/new">NewProduct</NavbarLink>
        )}
      </NavbarCollapse>
    </Nav>

    <div className="mt-48">
      <Foot className="fixed bottom-0" container>
        <div className="w-full text-center">
          <div className="w-full justify-between sm:flex sm:items-center sm:justify-between">
            <FooterBrand
              href="/"
              src={mainLogo}
              alt="Logo"
              name="Men's Fashion"
            />
            <FooterLinkGroup>
              <FooterLink href="/">About</FooterLink>
              <FooterLink href="/">Privacy Policy</FooterLink>
              <FooterLink href="/">Licensing</FooterLink>
              <FooterLink href="/">Contact</FooterLink>
            </FooterLinkGroup>
          </div>
          <FooterDivider />
          <h5 className="text-gray-500 dark:text-gray-400">쇼핑몰 구현 프로젝트</h5>
        </div>
      </Foot>
    </div>

 

② Home, Category 화면

 

    <div>
      <CategoryList />
      <div className="flex justify-center mt-8">
        <h1 className="mb-4 text-4xl font-extrabold leading-none tracking-tight text-gray-900 md:text-5xl lg:text-6xl dark:text-white">All Products</h1>
      </div>
      <div className="flex justify-center flex-wrap">
        {products.map((product, index) => (
          <OneProduct 
            key={product.id}
            id={product.id}
            image={product.image}
            name={product.name}
            price={product.price}
          />
        ))}
      </div>
    </div>

 

③ Signup, Login, Create, Edit 화면 (Form 형식) 

    <div className="flex justify-center">
      <Card className="w-1/2 max-w-96">
        <form className="flex max-w-md flex-col gap-4" onSubmit={submitHandler}>
          <div>
            <div className="mb-2 block">
              <Label htmlFor="name" value="Your name" />
            </div>
            <TextInput id="name" type="text" value={formData.name} onChange={handleChange} name="name" required shadow />
          </div>
          <div>
            <div className="mb-2 block">
              <Label htmlFor="email" value="Your email" />
            </div>
            <TextInput id="email" type="email" value={formData.email} onChange={handleChange} name="email" required shadow />
          </div>
          <div>
            <div className="mb-2 block">
              <Label htmlFor="password" value="Your password" />
            </div>
            <TextInput id="password" type="password" value={formData.password} onChange={handleChange} name="password" required shadow />
          </div>
          <div>
            <div className="mb-2 block">
              <Label htmlFor="repeatpassword" value="Repeat password" />
            </div>
            <TextInput id="repeatpassword" type="password" value={formData.repeatpassword} onChange={handleChange} name="repeatpassword" required shadow />
          </div>
          <div>
            <div className="mb-2 block">
              <Label htmlFor="address" value="Your address" />
            </div>
            <TextInput id="address" type="text" value={formData.address} onChange={handleChange} name="address" required shadow />
          </div>
          <div>
            <div className="mb-2 block">
              <Label htmlFor="phonenumber" value="Your phonenumber" />
            </div>
            <TextInput id="phonenumber" type="text" value={formData.phonenumber} onChange={handleChange} name="phonenumber" required shadow />
          </div>
          <Button type="submit">Signup new account</Button>
        </form>
      </Card>
    </div>

 

④ Cart, 구매내역 화면 (Table 형식)

      <div className="flex justify-center">
        <Table>
          <TableHead>
            <TableHeadCell>Product name</TableHeadCell>
            <TableHeadCell>Category</TableHeadCell>
            <TableHeadCell>Price</TableHeadCell>
            <TableHeadCell>Stock</TableHeadCell>
            <TableHeadCell>
              <span className="sr-only">Delete</span>
            </TableHeadCell>
          </TableHead>
          <TableBody className="divide-y">
            {userData?.cart.map((product) => (
              <CartProduct 
                key={product.id}
                id={product.id}
                name={product.name}
                category={categorySelector(Number(product.category))}
                price={product.price}
                stock={product.stock}
              />
            ))}
          </TableBody>
        </Table>
      </div>

 

⑤ Product 화면

      <CategoryList />
      <div className="flex flex-row my-8 justify-center">
        <Card className="max-w-xl basis-1/2 ml-8">
          <img
            alt="product"
            src={product?.image}
          />
        </Card>
        <Card className="max-w-xl basis-1/2 mr-8">
          <h2 className="text-4xl font-extrabold dark:text-white">{product?.name}</h2>
          <p className="my-4 text-lg text-gray-500">
            {product?.description}
          </p>
          <List>
            <ListItem>Category: {categorySelector(Number(product?.category))}</ListItem>
            <ListItem>Price: ${product?.price}</ListItem>
            <ListItem>Stock: {product?.stock}</ListItem>
          </List>
          {auth.isLoggedIn && (
            <Button onClick={putInCart}>Put in a Cart</Button>
          )}
          {auth.isAdmin && (
            <Button onClick={goToEdit} color="success">Edit</Button>
          )}
          {auth.isAdmin && (
            <Button onClick={deleteProduct} color="failure">Delete</Button>
          )}
        </Card>
      </div>
      <div className="flex flex-col">
        {auth.isLoggedIn && (  
          <NewReview productId={productId} userId={auth.userId}/>  
        )}
        {reviews.map((review, index) => (
          <ReviewBlock 
            key={review.id}
            id={review.id}
            user={review.user.name}
            star={review.star}
            comment={review.comment}
          />
        ))}
      </div>

 

⑥ My page 화면

    <Card className="w-1/3">
      <div className="flex flex-col items-center pb-10">
        <Avatar 
          alt="User Image"
          size="lg"
          img={UserImage}
          rounded
        />
        <h5 className="mb-1 text-3xl font-medium text-gray-900 dark:text-white">{userData?.name}</h5>
        <span className="text-sm text-gray-500 dark:text-gray-400">email: {userData?.email}</span>
        <span className="text-sm text-gray-500 dark:text-gray-400">address: {userData?.address}</span>
        <span className="text-sm text-gray-500 dark:text-gray-400">phone number: {userData?.phonenumber}</span>
        <div className="mt-4 flex space-x-3 lg:mt-6">
          <a
            href={`/review/${userId}`}
            className="inline-flex items-center rounded-lg bg-cyan-700 px-4 py-2 text-center text-sm font-medium text-white hover:bg-cyan-800 focus:outline-none focus:ring-4 focus:ring-cyan-300 dark:bg-cyan-600 dark:hover:bg-cyan-700 dark:focus:ring-cyan-800"
          >
            Show my reviews
          </a>
        </div>
        <div className="mt-4 flex space-x-3 lg:mt-6">
          <a
            href={`/history/${userId}`}
            className="inline-flex items-center rounded-lg bg-cyan-700 px-4 py-2 text-center text-sm font-medium text-white hover:bg-cyan-800 focus:outline-none focus:ring-4 focus:ring-cyan-300 dark:bg-cyan-600 dark:hover:bg-cyan-700 dark:focus:ring-cyan-800"
          >
            Show my purchase history
          </a>
        </div>
        <div className="mt-4 flex space-x-3 lg:mt-6">
          <a
            href={`/edit/${userId}`}
            className="inline-flex items-center rounded-lg bg-cyan-700 px-4 py-2 text-center text-sm font-medium text-white hover:bg-cyan-800 focus:outline-none focus:ring-4 focus:ring-cyan-300 dark:bg-cyan-600 dark:hover:bg-cyan-700 dark:focus:ring-cyan-800"
          >
            Edit my info
          </a>
        </div>
      </div>
    </Card>

 

⑦ send-request hook

export const useSendRequest = () => {
  const activeHttpRequests = useRef([]);

  const sendRequest = useCallback(
    async (url, method = 'GET', body = null, headers = {}) => {
      const httpAbortCtrl = new AbortController();
      activeHttpRequests.current.push(httpAbortCtrl);

      try {
        const response = await fetch(url, {
          method,
          body,
          headers,
          signal: httpAbortCtrl.signal
        });

        const responseData = await response.json();

        activeHttpRequests.current = activeHttpRequests.current.filter(
          reqCtrl => reqCtrl !== httpAbortCtrl
        );

        if (!response.ok) {
          throw new Error(responseData.message);
        }

        return responseData;
      } catch (err) {
        throw err;
      }
    },
    []
  );


  useEffect(() => {
    return () => {
      activeHttpRequests.current.forEach(abortCtrl => abortCtrl.abort());
    };
  }, []);

  return { sendRequest };
};