Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions vite-project/public/ic_X.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions vite-project/public/ic_plus.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 9 additions & 3 deletions vite-project/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,20 @@ import "./App.css";
import ProductList from "./pages/ProductList";
import Home from "./pages/Home";
import NotFound from "./pages/NotFound";
import Header from "./components/common/Header";
import AddItems from "./pages/AddItems";
import Layout from "./components/style/Layout";

function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Home />}></Route>
<Route path="/items" element={<ProductList />}></Route>
<Route path="*" element={<NotFound />}></Route>
<Route path="/" element={<Layout />}>
<Route index element={<Home />}></Route>
<Route path="/items" element={<ProductList />}></Route>
<Route path="/additem" element={<AddItems />}></Route>
<Route path="*" element={<NotFound />}></Route>
</Route>
</Routes>
</BrowserRouter>
);
Expand Down
19 changes: 0 additions & 19 deletions vite-project/src/components/BestProductSection/BestProduct.jsx

This file was deleted.

19 changes: 19 additions & 0 deletions vite-project/src/components/BestProductSection/BestProducts.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { useResponsivePage } from "../../hooks/useResponsivePage";
import ProductGrid from "../common/ProductGrid";

const BestProducts = ({ products }) => {
const responsiveValues = useResponsivePage();

return (
<div className="flex flex-col gap-3 ">
<h2 className="text-xl font-bold ">베스트 상품</h2>
<ProductGrid
products={products}
gridSize={responsiveValues.bestPicContainerSize}
picSize={responsiveValues.bestPicSize}
/>
Comment on lines +10 to +14
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(참고만 해도 됩니다 😉)ProductGridgridSize, picSize가 드릴링이 되는 것 같아서 한 번 리팩토링 해봤습니다 !

diff --git a/vite-project/src/components/BestProductSection/BestProducts.jsx b/vite-project/src/components/BestProductSection/BestProducts.jsx
index 82f5c22..468fddc 100644
--- a/vite-project/src/components/BestProductSection/BestProducts.jsx
+++ b/vite-project/src/components/BestProductSection/BestProducts.jsx
@@ -1,17 +1,30 @@
-import { useResponsivePage } from "../../hooks/useResponsivePage";
 import ProductGrid from "../common/ProductGrid";
+import ProductCard from "../common/ProductCard";
+import { useResponsivePage } from "../../hooks/useResponsivePage";
+import { DEVICE_STYLES } from "../common/DeviceClasses";

 const BestProducts = ({ products }) => {
-  const responsiveValues = useResponsivePage();
+  const { device } = useResponsivePage();
+
+  const containerClass = DEVICE_STYLES[device].best.container;
+  const pictureClass = DEVICE_STYLES[device].best.picture;

   return (
     <div className="flex flex-col gap-3 ">
       <h2 className="text-xl font-bold ">베스트 상품</h2>
-      <ProductGrid
-        products={products}
-        gridSize={responsiveValues.bestPicContainerSize}
-        picSize={responsiveValues.bestPicSize}
-      />
+      <ProductGrid>
+        {products.map((product) => (
+          <ProductCard
+            key={product.id}
+            className={containerClass}
+            name={product.name}
+            price={product.price}
+            favoriteCount={product.favoriteCount}
+            images={product.images}
+            picSizeClass={pictureClass}
+          />
+        ))}
+      </ProductGrid>
     </div>
   );
 };
diff --git a/vite-project/src/components/ProductListSection/Container.jsx b/vite-project/src/components/ProductListSection/Container.jsx
index 0d02c77..905a802 100644
--- a/vite-project/src/components/ProductListSection/Container.jsx
+++ b/vite-project/src/components/ProductListSection/Container.jsx
@@ -2,12 +2,14 @@ import { useState } from "react";
 import useGetProducts from "../../hooks/useGetProducts";
 import usePagination from "../../hooks/usePagination";
 import ProductGrid from "../common/ProductGrid";
+import ProductCard from "../common/ProductCard";
 import Pagination from "../ProductListSection/Pagination";
 import searchIcon from "../../assets/searchIcon.svg";
 import Dropdown from "./Dropdown";
-import { useResponsivePage } from "../../hooks/useResponsivePage";
 import { Link } from "react-router-dom";
 import { SORT_OPTIONS } from "../../constant/SORT_OPTIONS";
+import { useResponsivePage } from "../../hooks/useResponsivePage";
+import { DEVICE_STYLES } from "../common/DeviceClasses";

 const Container = ({ pageSize, isMobile }) => {
   const [orderBy, setOrderBy] = useState(SORT_OPTIONS[0].value);
@@ -17,7 +19,10 @@ const Container = ({ pageSize, isMobile }) => {
     orderBy,
     currentPage,
   });
-  const ResponsiveValues = useResponsivePage();
+  const { device } = useResponsivePage();
+
+  const containerClass = DEVICE_STYLES[device].normal.container;
+  const pictureClass = DEVICE_STYLES[device].normal.picture;

   const handleSelect = (val) => {
     console.log("$$", val);
@@ -92,11 +97,19 @@ const Container = ({ pageSize, isMobile }) => {
         </div>
       )}

-      <ProductGrid
-        products={products}
-        picSize={ResponsiveValues.normalPicSize}
-        gridSize={ResponsiveValues.normalPicContainerSize}
-      />
+      <ProductGrid>
+        {products.map((product) => (
+          <ProductCard
+            key={product.id}
+            className={containerClass}
+            name={product.name}
+            price={product.price}
+            favoriteCount={product.favoriteCount}
+            images={product.images}
+            picSizeClass={pictureClass}
+          />
+        ))}
+      </ProductGrid>
       <Pagination
         currentPage={currentPage}
         setCurrentPage={setCurrentPage}
diff --git a/vite-project/src/components/common/ProductGrid.jsx b/vite-project/src/components/common/ProductGrid.jsx
index 3b5835d..871a009 100644
--- a/vite-project/src/components/common/ProductGrid.jsx
+++ b/vite-project/src/components/common/ProductGrid.jsx
@@ -1,30 +1,7 @@
-import ProductCard from "./ProductCard";
-import { GRID_SIZES } from "./ProductGridClasses";
-import { PIC_SIZES } from "./ProductGridClasses";
-
-const ProductGrid = ({
-  gridSize = "grid224",
-  picSize = "normalPicSize",
-  products,
-}) => {
-  const sizeClass = GRID_SIZES[gridSize] || "";
-  const picSizeClass = PIC_SIZES[picSize] || "";
-
+const ProductGrid = ({ children }) => {
   return (
     <div className="flex flex-wrap justify-center gap-5">
-      {products.map((product) => {
-        return (
-          <ProductCard
-            key={product.id}
-            className={`${sizeClass}`}
-            name={product.name}
-            price={product.price}
-            favoriteCount={product.favoriteCount}
-            images={product.images}
-            picSizeClass={picSizeClass}
-          />
-        );
-      })}
+      {children}
     </div>
   );
 };
diff --git a/vite-project/src/components/common/ProductGridClasses.js b/vite-project/src/components/common/ProductGridClasses.js
deleted file mode 100644
index 8ad09e3..0000000
--- a/vite-project/src/components/common/ProductGridClasses.js
+++ /dev/null
@@ -1,13 +0,0 @@
-export const GRID_SIZES = {
-  grid168: "w-[168px]",
-  grid224: "w-56 h-72",
-  grid228: "w-72 h-[378px]",
-  grid343: "w-[343px] h-[434px]",
-};
-
-export const PIC_SIZES = {
-  normalPicSize: "w-56 h-56 ",
-  normalPicMobile: "w-[168px] h-[168px]",
-  bestPicSize: "w-[282px] h-[282px]",
-  bestPicTabletMobile: "w-[343px] h-[343px]",
-};
diff --git a/vite-project/src/hooks/useResponsivePage.js b/vite-project/src/hooks/useResponsivePage.js
index edf831d..d4ca7ed 100644
--- a/vite-project/src/hooks/useResponsivePage.js
+++ b/vite-project/src/hooks/useResponsivePage.js
@@ -17,6 +17,6 @@ export const useResponsivePage = () => {
     return () => window.removeEventListener("resize", onResize);
   }, []);

-  const values = useMemo(() => CONFIG[device], [device]);
+  const values = useMemo(() => ({ ...CONFIG[device], device }), [device]);
   return values;
 };

핵심은 ProductGrid의 역할이 프롭스를 전달만 해주는 것 같아서요 !
일관적인 스타일 용도로만 사용하도록 바꿔봤습니다 !

참고만 하시고 사용하지 않으셔도 됩니다 😉

</div>
);
};

export default BestProducts;
39 changes: 25 additions & 14 deletions vite-project/src/components/ProductListSection/Container.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,31 +7,35 @@ import searchIcon from "../../assets/searchIcon.svg";
import Dropdown from "./Dropdown";
import { useResponsivePage } from "../../hooks/useResponsivePage";
import { Link } from "react-router-dom";
import { SORT_OPTIONS } from "../../constant/SORT_OPTIONS";

const Container = ({ pageSize, isMobile }) => {
const [orderBy, setOrderBy] = useState("recent");
const [orderBy, setOrderBy] = useState(SORT_OPTIONS[0].value);
const { setCurrentPage, getPageNumber, currentPage } = usePagination();
const { products, totalProductCount } = useGetProducts({
pageSize,
orderBy,
currentPage,
});
const { normalPicSize, normalPicContainerSize } = useResponsivePage();
const ResponsiveValues = useResponsivePage();

const handleSelect = (e) => {
return setOrderBy(e.target.dataset.value);
const handleSelect = (val) => {
console.log("$$", val);
setOrderBy(val);
};

const { pages, totalPages } = getPageNumber(totalProductCount);
return (
<div className="flex flex-col items-center gap-6 mt-6 max-w-[1200px] h-">
<div className="flex flex-col items-center gap-6 mt-6 max-w-[1200px]">
{isMobile ? (
<div className="flex flex-col w-full gap-4">
<div className="flex justify-between">
<h2 className="text-xl font-bold">전체 상품</h2>
<button className="px-6 py-3 text-white bg-blue-500 rounded-lg">
상품 등록하기
</button>
<Link
className="px-6 py-3 text-white bg-blue-500 rounded-lg"
to="/addItems"
>
상품등록하기
</Link>
</div>
<div className="flex w-full gap-3">
<div className="flex items-center flex-1 gap-2">
Expand All @@ -46,7 +50,11 @@ const Container = ({ pageSize, isMobile }) => {
placeholder="검색할 상품을 입력해주세요"
/>
</div>
<Dropdown handleSelect={handleSelect} orderBy={orderBy} />
<Dropdown
onChange={handleSelect}
value={orderBy}
options={SORT_OPTIONS}
/>
</div>
</div>
</div>
Expand Down Expand Up @@ -74,17 +82,20 @@ const Container = ({ pageSize, isMobile }) => {
>
상품등록하기
</Link>

<Dropdown handleSelect={handleSelect} orderBy={orderBy} />
<Dropdown
onChange={handleSelect}
value={orderBy}
options={SORT_OPTIONS}
/>
</div>
</div>
</div>
)}

<ProductGrid
products={products}
picSize={normalPicSize}
gridSize={normalPicContainerSize}
picSize={ResponsiveValues.normalPicSize}
gridSize={ResponsiveValues.normalPicContainerSize}
/>
<Pagination
currentPage={currentPage}
Expand Down
58 changes: 27 additions & 31 deletions vite-project/src/components/ProductListSection/Dropdown.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,52 +2,48 @@ import { useState } from "react";
import arrow from "../../assets/arrow.svg";
import dropdown from "../../assets/dropdown.svg";

const VALUES = {
recent: "최신순",
favorite: "좋아요순",
};

const Dropdown = ({ handleSelect, orderBy }) => {
const [isDropdown, setIsDropdown] = useState(false);
const [dropdownValue, setDropdownValue] = useState(VALUES[orderBy]);
const Dropdown = ({ onChange, options, value }) => {
const [open, setOpen] = useState(false);
const selected =
options.find((option) => option.value === value) ?? options[0];

const handleClick = (e) => {
handleSelect(e);
setDropdownValue(VALUES[e.target.dataset.value]);
const handleClick = (val) => {
onChange(val);
setOpen(false);
};

return (
<div className="relative order-3 cursor-pointer">
<div
onClick={() => setIsDropdown(!isDropdown)}
onClick={() => setOpen(!open)}
className="hidden w-32 gap-4 px-5 py-3 border xs:flex rounded-xl"
>
<span>{dropdownValue}</span>
<span>{selected.label}</span>
<img src={arrow} alt="드롭다운 화살표 아이콘" />
</div>
<div
onClick={() => setIsDropdown(!isDropdown)}
onClick={() => setOpen(!open)}
className="p-3 border rounded-xl xs:hidden"
>
<img src={dropdown} alt="드롭다운 아이콘" />
</div>
{isDropdown && (
<div
className="absolute flex flex-col items-center w-32 bg-white border top-16 right-[2px] rounded-xl"
onClick={(e) => {
handleClick(e);
setIsDropdown(!isDropdown);
}}
>
<div className="flex justify-center w-full border-b ">
<span className="px-5 py-3" data-value="recent">
최신순
</span>
</div>
<span className="px-5 py-3" data-value="favorite">
좋아요순
</span>
</div>
{open && (
<ul className="absolute flex flex-col items-center w-32 bg-white border top-16 right-[2px] rounded-xl">
{options.map((option) => {
return (
<li
key={option.value}
className="w-full px-5 py-3 text-center hover:bg-gray-100"
onClick={() => {
handleClick(option.value);
console.log("$", option.value);
}}
>
{option.label}
</li>
);
})}
</ul>
)}
</div>
);
Expand Down
68 changes: 68 additions & 0 deletions vite-project/src/components/addItem/Input.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { INPUT_TYPE_STYLE } from "./InputClasses";

const Input = ({
inputTypeStyle = "basic",
as,
type,
id,
imgUrl,
onChange,
onClick,
onKeyDown,
...props
}) => {
const styleClass = INPUT_TYPE_STYLE[inputTypeStyle] || "";
const Component = as || "input";

return (
<div>
{type === "file" ? (
<div className="flex gap-8">
<div className="flex flex-col gap-4">
<label className={`${styleClass} bg-gray-100 cursor-pointer`}>
<img src={props.src} alt={props.alt} />
<Component
accept="image/*"
type={type}
{...props}
id={id}
className="hidden"
onChange={onChange}
/>
</label>
{imgUrl && (
<p className="text-red-400">
*이미지 등록은 최대 1개까지 가능합니다
</p>
)}
</div>
{imgUrl && (
<div className="relative">
Comment on lines +3 to +40
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오호. input 외에 다른 컴포넌트로 사용될 수 있도록 해놓으셨군요.

확장성을 고려하신 것 같군요? 장희님을 보면 참 도전적이고 학습에 열려있는 수강생이란 느낌이 많이 들어요 ! 리스펙합니다 🥺

다만, 하나의 컴포넌트에서 많은 역할을 수행하려다 보니 조금 복잡해진 느낌이 없잖아 있군요 !

다음과 같이 분리해보는 것도 고려해보실 수 있을 것 같습니다 😊:

const InputText = ({ styleClass, ...props }) => (
  <input className={`${styleClass} bg-gray-100`} {...props} />
);

const InputFile = ({ styleClass, imgUrl, onDelete, ...props }) => (
  <div className="flex gap-8">
    <div className="flex flex-col gap-4">
      <label className={`${styleClass} bg-gray-100 cursor-pointer`}>
        <input type="file" className="hidden" {...props} />
      </label>
      {imgUrl && <p className="text-red-400">*이미지 등록은 최대 1개</p>}
    </div>
    {imgUrl && (
      <div className="relative">
        <img src={imgUrl} alt="preview" />
        <img src="/ic_X.svg" alt="삭제" onClick={onDelete} />
      </div>
    )}
  </div>
);

export default function Input({ type, ...props }) {
  const styleClass = INPUT_TYPE_STYLE[props.inputTypeStyle] || "";
  if (type === "file") return <InputFile styleClass={styleClass} {...props} />;
  return <InputText styleClass={styleClass} type={type} {...props} />;
}

<img
src={imgUrl}
alt="잠시대기"
className="h-[282px] rounded-xl object-cover aspect-square"
/>
<img
src="/ic_X.svg"
alt="X 아이콘"
className="absolute cursor-pointer top-3 right-3"
onClick={() => onClick()}
/>
</div>
)}
</div>
) : (
<Component
className={`${styleClass} bg-gray-100`}
type={type}
onKeyDown={onKeyDown}
onChange={onChange}
{...props}
/>
)}
</div>
);
};

export default Input;
6 changes: 6 additions & 0 deletions vite-project/src/components/addItem/InputClasses.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export const INPUT_TYPE_STYLE = {
basic: "px-6 py-4 rounded-xl h-[56px] w-full",
textarea: "px-6 py-4 rounded-xl h-[282px] w-full self-start ",
image:
"h-[282px] rounded-xl object-cover aspect-square flex items-center justify-center ",
};
4 changes: 4 additions & 0 deletions vite-project/src/components/common/ButtonClasses.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export const BTN_STYLE_TYPE = {
active: "rounded-lg px-[23px] py-3 bg-blue-500 text-white",
inactive: "bg-gray-400 rounded-lg px-[23px] py-3 text-white",
};
14 changes: 14 additions & 0 deletions vite-project/src/components/common/ButtonLink.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { BTN_STYLE_TYPE } from "./ButtonClasses";

const ButtonLink = ({ btnStyle = "inactive", as, children, ...props }) => {
const Component = as || "button";
const btnTypeClass = BTN_STYLE_TYPE[btnStyle] || "";

return (
<Component className={`${btnTypeClass}`} {...props}>
{children}
</Component>
);
};

export default ButtonLink;
4 changes: 2 additions & 2 deletions vite-project/src/components/common/ProductGrid.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import ProductCard from "./ProductCard";
import { GRID_SIZES } from "./ProductGridTokens";
import { PIC_SIZES } from "./ProductGridTokens";
import { GRID_SIZES } from "./ProductGridClasses";
import { PIC_SIZES } from "./ProductGridClasses";

const ProductGrid = ({
gridSize = "grid224",
Expand Down
15 changes: 15 additions & 0 deletions vite-project/src/components/style/Layout.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Outlet } from "react-router-dom";
import Header from "../common/Header";

const Layout = () => {
return (
<div>
<Header />
<main className="max-w-screen-xl px-4 mt-6 md:px-6 md:mx-auto">
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

크으 ~ 시맨틱 태그도 잊지 않았군요 👍

<Outlet />
</main>
</div>
);
};

export default Layout;
38 changes: 38 additions & 0 deletions vite-project/src/constant/INPUT_OPTIONS.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
export const INPUT_OPTIONS = [
{
id: "product_image",
title: "상품 이미지",
inputType: "file",
inputTypeStyle: "image",
src: "/ic_plus.svg",
alt: "십자가 모양 아이콘",
},
{
id: "product_name",
title: "상품명",
inputType: "input",
placeholder: "상품명을 입력해주세요",
inputTypeStyle: "basic",
},
{
id: "product_description",
title: "상품 소개",
placeholder: "상품 소개를 입력해주세요",
inputTypeStyle: "textarea",
tagName: "textarea",
},
{
id: "sales_price",
title: "판매가격",
inputType: "input",
placeholder: "판매 가격을 입력해주세요",
inputTypeStyle: "basic",
},
{
id: "hashtag",
title: "태그",
inputType: "input",
placeholder: "태그를 입력해주세요",
inputTypeStyle: "basic",
},
];
Loading
Loading