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 src/assets/icons/delete.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions src/assets/icons/plus.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
62 changes: 62 additions & 0 deletions src/components/FileInput/FileInput.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import * as S from "./FileInput.styles";
import x from "../../assets/icons/delete.svg";
import plus from "../../assets/icons/plus.svg";
import { useState, useRef } from "react";

export default function FileInput({ lable, images, setValues }) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

오타 발견 ! 🔎

Suggested change
export default function FileInput({ lable, images, setValues }) {
export default function FileInput({ label, images, setValues }) {

const [preview, setPreview] = useState(null);
const inputRef = useRef();

const handleImageChange = (e) => {
const file = e.target.files[0];

if (file) {
setValues((prevState) => ({
...prevState,
images: file,
}));

const fileUrl = URL.createObjectURL(file);
setPreview(fileUrl);
}
};

const handleImageDelete = () => {
if (!inputRef.current) return;
inputRef.current.value = "";
setValues((prevState) => ({
...prevState,
images: null,
}));
setPreview(null);
};

return (
<>
<S.FileContainer>
<S.Label>{lable}</S.Label>
<S.File htmlFor="images">
<S.Div>
<S.PlusIcon src={plus} />
<S.AddImg>이미지 등록</S.AddImg>
</S.Div>
</S.File>
<input
id="images"
name="images"
type="file"
accept="image/jpeg, image/png"
style={{ display: "none" }}
onChange={handleImageChange}
ref={inputRef}
/>
</S.FileContainer>
{images && (
<S.Preview>
<S.PreviewImg src={preview} />
<S.DeleteImg src={x} onClick={handleImageDelete} />
</S.Preview>
)}
</>
);
}
76 changes: 76 additions & 0 deletions src/components/FileInput/FileInput.styles.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { styled } from "styled-components";

export const FileContainer = styled.div`
display: flex;
flex-direction: column;
gap: 16px;
`;

export const Label = styled.div`
font-size: 18px;
font-weight: 700;
line-height: 26px;
color: var(--gray800);
`;

export const File = styled.label`
width: 282px;
height: 282px;
display: flex;
justify-content: center;
align-items: center;
background-color: var(--gray100);
border-radius: 12px;
cursor: pointer;

@media (max-width: 1199px) {
width: 168px;
height: 168px;
}
`;

export const Div = styled.div`
width: 74px;
height: 86px;
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
`;

export const PlusIcon = styled.img`
width: 48px;
height: 48px;
`;

export const AddImg = styled.div`
font-size: 16px;
font-weight: 400;
line-height: 26px;
color: var(--gray400);
`;

export const Preview = styled.div`
display: flex;
position: relative;
`;

export const PreviewImg = styled.img`
width: 282px;
height: 282px;
border-radius: 12px;

@media (max-width: 1199px) {
width: 168px;
height: 168px;
}
`;

export const DeleteImg = styled.img`
width: 22px;
height: 24px;
position: absolute;
right: 10px;
top: 10px;
cursor: pointer;:
`;
109 changes: 81 additions & 28 deletions src/components/Items/AllItems/AllItems.jsx
Original file line number Diff line number Diff line change
@@ -1,39 +1,92 @@
import * as S from "./AllItems.styles";
import { useEffect, useState } from "react";
import { Link } from "react-router-dom";
import ItemCard from "../ItemCard/ItemCard";
import Dropdown from "../../common/Dropdown/Dropdown";
import { Link } from "react-router-dom";
import Search from "../../Search/Search";
import NoneItem from "../../NoneItem/NoneItem";
import { getProducts } from "../../../api/products";
import Paging from "../../Paging/Paging";

const LIST = ["최신순", "좋아요순"];

export default function AllItems() {
const [items, setItems] = useState([]);
const [currentPage, setCurrentPage] = useState(1);
const [pageSize, setPageSize] = useState(10);
const [sortOption, setSortOption] = useState("최신순");
const [keyword, setKeyword] = useState("");
const [totalItems, setTotalItems] = useState(0);

const orderByValue = sortOption === "최신순" ? "recent" : "favorite";

const updateItems = () => {
if (window.innerWidth <= 767) {
setPageSize(4);
} else if (window.innerWidth >= 768 && window.innerWidth <= 1199) {
setPageSize(6);
} else {
setPageSize(10);
}
};

useEffect(() => {
getProducts({
page: currentPage,
pageSize: pageSize,
orderBy: orderByValue,
keyword: keyword,
}).then((result) => {
if (!result) return;
setItems(result.list);
setTotalItems(result.totalCount);
});
}, [currentPage, pageSize, orderByValue, keyword]);

useEffect(() => {
updateItems();
window.addEventListener("resize", updateItems);

return () => {
window.removeEventListener("resize", updateItems);
};
}, []);

const list = ["최신순", "좋아요순"];
const handleChangeClick = (sortOption) => {
setSortOption(sortOption);
};

export default function AllItems({ items, sortOption, onChange, setKeyword }) {
return (
<S.AllContainer>
<S.AllHeader>
<S.Div>
<S.Title>전체 상품</S.Title>
<Link to="/addItem">
<S.AddBtnForMedia>상품 등록하기</S.AddBtnForMedia>
</Link>
</S.Div>
<S.Filter>
<Search onSearch={setKeyword} />
<Link to="/addItem">
<S.AddBtn>상품 등록하기</S.AddBtn>
</Link>
<Dropdown sortOption={sortOption} list={list} onChange={onChange} />
</S.Filter>
</S.AllHeader>
{items.length !== 0 ? (
<S.ItemCardContainer>
{items.map((items, idx) => (
<ItemCard key={idx} list="all" {...items} />
))}
</S.ItemCardContainer>
) : (
<NoneItem />
<S.AllItems>
<S.AllContainer>
<S.AllHeader>
<S.Div>
<S.Title>전체 상품</S.Title>
<Link to="/addItem">
<S.AddBtnForMedia>상품 등록하기</S.AddBtnForMedia>
</Link>
</S.Div>
<S.Filter>
<Search onSearch={setKeyword} />
<Link to="/addItem">
<S.AddBtn>상품 등록하기</S.AddBtn>
</Link>
<Dropdown sortOption={sortOption} list={LIST} onChange={handleChangeClick} />
</S.Filter>
</S.AllHeader>
{items.length !== 0 ? (
<S.ItemCardContainer>
{items.map((items, idx) => (
<ItemCard key={idx} list="all" {...items} />
))}
</S.ItemCardContainer>
) : (
<NoneItem />
)}
</S.AllContainer>
{items.length !== 0 && (
<Paging currentPage={currentPage} pageSize={pageSize} totalItemsCount={totalItems} setPage={setCurrentPage} />
)}
</S.AllContainer>
</S.AllItems>
);
}
6 changes: 6 additions & 0 deletions src/components/Items/AllItems/AllItems.styles.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import styled from "styled-components";

export const AllItems = styled.div`
display: flex;
flex-direction: column;
gap: 40px;
`;

export const AllContainer = styled.div`
display: flex;
flex-direction: column;
Expand Down
35 changes: 32 additions & 3 deletions src/components/Items/BestItems/BestItems.jsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,37 @@
import * as S from "./BestItems.styles";
import { useState, useEffect, useCallback } from "react";
import ItemCard from "../ItemCard/ItemCard";
import { useEffect } from "react";
import { getProducts } from "../../../api/products";

export default function BestItems() {
const [bestItems, setBestItems] = useState([]);
const [showItems, setShowItems] = useState(4);

useEffect(() => {
getProducts({
page: 1,
pageSize: showItems,
orderBy: "favorite",
keyword: "",
}).then((result) => {
if (!result) return;
const sortedBestItems = [...result.list].slice(0, 4);
setBestItems(sortedBestItems);
});
}, [showItems]);

const updateBestItems = useCallback(() => {
if (window.innerWidth <= 767) {
setShowItems(1);
} else if (window.innerWidth >= 768 && window.innerWidth <= 1199) {
setShowItems(2);
} else {
setShowItems(4);
}
}, []);
Comment on lines +23 to +31
Copy link
Collaborator

Choose a reason for hiding this comment

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

(제안/의견) showItems가 재사용이 많이 되는 것 같아요.

커스텀 훅을 만들어보는건 어떨까요?:

export function useResponsiveItems({ mobile = 767, tablet = 1199, items = { mobile: 1, tablet: 2, desktop: 4} }) {
  const [showItems, setShowItems] = useState(getItemsCount(window.innerWidth));

  const updateItemsCount = useCallback(() => {
    const newCount = getItemsCount(window.innerWidth);
    setShowItems(newCount);
  }, []);

  function getItemsCount(width) {
    if (width <= breakpoints.mobile) {
      return breakpoints.items.mobile;
    } else if (width <= breakpoints.tablet) {
      return breakpoints.items.tablet;
    } else {
      return breakpoints.items.desktop;
    }
  }

  useEffect(() => {
    updateItemsCount(); // 초기값 설정
    window.addEventListener("resize", updateItemsCount);

    return () => {
      window.removeEventListener("resize", updateItemsCount); // 이벤트 클린업
    };
  }, [updateItemsCount]);

  return showItems;
}

그리고 다음과 같이 사용해볼 수 있어요:

  const showItems = useResponsiveItems({
    mobile: 767,
    tablet: 1199,
    items: {
      mobile: 1,
      tablet: 2,
      desktop: 4,
    },
  });

Copy link
Collaborator

Choose a reason for hiding this comment

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

디바운싱/쓰로틀링도 적용해볼 수 있어요:

  useEffect(() => {
    updateItemsCount(); // 초기값 설정
    window.addEventListener("resize", updateItemsCount);

    return () => {
      window.removeEventListener("resize", updateItemsCount); // 이벤트 클린업
    };
  }, [updateItemsCount]);

해당 이벤트에 console.log를 호출해보면 리사이징이 될 때마다 정말정말 많은 호출하는 것을 볼 수 있을거예요 !
그만큼 성능에 좋지 못하다는 이야기겠지요?
따라서, 프론트엔드 개발자들은 이렇게 잦은 이벤트가 발생할 때(리사이징, 스크롤, 타이핑 등) 디바운싱/쓰로틀링을 통하여 최적화를 시키곤 합니다.

쓰로틀링(Throttling): 일정 시간 동안 하나의 함수만 호출되도록 하는 기법입니다. 예를 들어, 사용자가 스크롤을 할 때, 매번 이벤트를 처리하지 않고 일정 간격으로 한 번만 처리하게 합니다. 이를 통해 성능을 향상시킬 수 있습니다.

디바운싱(Debouncing): 여러 번 발생하는 이벤트 중 마지막 이벤트가 발생한 후 일정 시간이 지난 다음에 한 번만 실행되도록 하는 기법입니다. 예를 들어, 사용자가 검색어를 입력할 때, 입력이 끝난 후 일정 시간 동안 추가 입력이 없으면 검색 요청을 보냅니다.

Copy link
Collaborator

Choose a reason for hiding this comment

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

예를 들어서 다음과 같이 최적화를 시킬 수 있습니다 !:

function debounce(func, wait) {
  let timeout;
  return function executedFunction(...args) {
    const later = () => {
      clearTimeout(timeout);
      func(...args);
    };
    clearTimeout(timeout);
    timeout = setTimeout(later, wait);
  };
}

function Container() {
  const handleResize = () => {
    console.log('Window resized');
  };

  useEffect(() => {
    const debouncedHandleResize = debounce(handleResize, 300);
    window.addEventListener('resize', debouncedHandleResize);
    return () => {
      window.removeEventListener('resize', debouncedHandleResize);
    };
  }, []);

  // ... Some code
}

이렇게 하면 연속된 이벤트가 끝난 후 0.3초마다 호출하게 되어 기존보다 훨씬 최적화된 기능이 될 수 있습니다 !


const responsiveItems = bestItems.slice(0, showItems);

export default function BestItems({ bestItems, updateBestItems }) {
useEffect(() => {
updateBestItems();
window.addEventListener("resize", updateBestItems);
Expand All @@ -16,7 +45,7 @@ export default function BestItems({ bestItems, updateBestItems }) {
<S.BestContainer>
<S.Title>베스트 상품</S.Title>
<S.ItemCardContainer>
{bestItems.map((items, idx) => (
{responsiveItems.map((items, idx) => (
<ItemCard key={idx} list="best" {...items} />
))}
</S.ItemCardContainer>
Expand Down
10 changes: 8 additions & 2 deletions src/components/Items/ItemCard/ItemCard.jsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
import * as S from "./ItemCard.styles";
import heart from "../../../assets/icons/heart.svg";
import NoneImage from "../../NoneImage/NoneImage";
import { useState } from "react";

// images 값이 없으면 NoneImage 컴포넌트가 보이도록 구현했는데 images 배열 안에 값이 있지만 사진이 안 불러와지는 경우에는 어떻게 처리해야 하는지 고민입니다.
export default function ItemCard({ list = "best", images, name, price, favoriteCount }) {
const [isImgError, setIsImgError] = useState(false);

return (
<S.ItemContainer list={list}>
{images[0] ? <S.ItemImg src={images[0]} alt="productImage" list={list} /> : <NoneImage list={list} />}
{images[0] && !isImgError ? (
<S.ItemImg src={images[0]} alt="productImage" list={list} onError={() => setIsImgError(true)} />
) : (
<NoneImage list={list} />
)}
<S.ContentContainer>
<S.Title>{name}</S.Title>
<S.Price>{price.toLocaleString()}원</S.Price>
Expand Down
1 change: 0 additions & 1 deletion src/components/Paging/Paging.styles.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { styled } from "styled-components";
export const PagingContainer = styled.div`
width: 100%;
display: flex;
margin-bottom: 40px;

.pagination {
display: flex;
Expand Down
13 changes: 13 additions & 0 deletions src/components/Tag/Tag.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import * as S from "./Tag.styles";
import x from "../../assets/icons/delete.svg";

export default function Tag({ tag, onClick }) {
return (
<S.TagContainer>
<S.Tag>
<S.TagName>#{tag}</S.TagName>
<S.DeleteTag src={x} onClick={onClick} />
</S.Tag>
</S.TagContainer>
);
}
Loading
Loading