[이연수] Sprint5#132
Hidden character warning
Conversation
…, 컴포넌트 버튼/카드/드롭다운/페이지네이션 수정)
|
스프리트 미션 하시느라 수고 많으셨어요. |
검토해주셔서 감사합니다.
|
| <meta property="og:type" content="website" /> | ||
| <meta property="og:site_name" content="판다마켓" /> | ||
| <meta property="og:title" content="판다마켓" /> | ||
| <meta property="og:description" content="일상의 모든 물건을 거래해보세요" /> | ||
| <meta property="og:image" content="/og-img.png" /> | ||
| <!-- Favicon --> | ||
| <link rel="icon" href="/favicon.ico" /> | ||
| <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" /> | ||
| <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" /> | ||
| <link rel="apple-touch-icon" sizes="180x180" href="/apple-icon-180x180.png" /> |
There was a problem hiding this comment.
크으 ~ 메타 태그까지 빠트리지 않고 꼼꼼하시군요 ! 👍
| const getItems = async ({ page = 1, pageSize = 10, orderBy = "recent", keyword = "" }) => { | ||
| try { | ||
| const { data } = await instance.get("/products", { | ||
| params: { | ||
| page, | ||
| pageSize, | ||
| orderBy, | ||
| keyword, | ||
| }, | ||
| }); | ||
| return data; | ||
| } catch (error) { | ||
| console.error(error); | ||
| throw error; | ||
| } | ||
| }; |
There was a problem hiding this comment.
훌륭합니다 ! 더 할 말이 없군요 ! 👍
파라메터의 기본값도 적절하고, 받아야 할 파라메터도 필요한 것들만 잘 받고 있네요.
axios의 params 활용도 좋고 try ... catch를 통한 예외처리도 ! ㅎㅎㅎ
너무 칭찬만 했는데.. 칭찬 받을 명분이 충분하셨습니다 👍
| const CardItemWrapper = styled.li` | ||
| position: relative; | ||
| width: 100%; | ||
| `; | ||
|
|
||
| const CardThumbnail = styled.div` | ||
| width: 100%; | ||
| aspect-ratio: 1 / 1; | ||
| border-radius: 16px; | ||
| overflow: hidden; | ||
| transition: all 0.2s; | ||
| & img { | ||
| width: 100%; | ||
| height: 100%; | ||
| object-fit: cover; | ||
| } | ||
| ${CardItemWrapper}:hover & { | ||
| opacity: 0.8; | ||
| } | ||
| ${Midia("sm")} { | ||
| border-radius: 12px; | ||
| } | ||
| `; | ||
|
|
||
| const CardInfo = styled.div` | ||
| margin-top: 16px; | ||
| display: grid; | ||
| gap: 6px 0; | ||
| word-break: break-all; | ||
| `; | ||
|
|
||
| const CardTitle = styled.div` | ||
| ${typography["text-md-medium"]}; | ||
| white-space: nowrap; | ||
| overflow: hidden; | ||
| text-overflow: ellipsis; | ||
| `; | ||
|
|
||
| const CardPrice = styled.div` | ||
| ${typography["text-lg-bold"]}; | ||
| `; | ||
|
|
||
| const CardLikes = styled.div` | ||
| display: flex; | ||
| align-items: center; | ||
| gap: 0 4px; | ||
| & .count { | ||
| ${typography["text-xs-medium"]}; | ||
| } | ||
| `; |
There was a problem hiding this comment.
크으 .. 이런게 styled-components의 매력인 것 같아요.
스타일과 컴포넌트의 영역이 명확히 구분되어서 가독성도 좋고, 자바스크립트를 유연하게 사용할 수 있는 점이 참 매력적으로 느껴지네요.
잘 활용하고 계십니다 👍
| const CardItem = ({ images, name, price, favoriteCount }) => { | ||
| const thumbnailUrl = images && images.length > 0 ? images[0] : defaultImg; |
There was a problem hiding this comment.
해당 컴포넌트는 images가 필요한 것이 아닌 thumbnail이 필요한 것 같군요?
그렇다면 다음과 같이 바꿔볼 수 있을거예요:
| const CardItem = ({ images, name, price, favoriteCount }) => { | |
| const thumbnailUrl = images && images.length > 0 ? images[0] : defaultImg; | |
| const CardItem = ({ thumbnail = defaultImg, name, price, favoriteCount }) => { |
그리고 해당 컴포넌트를 호출하는 곳에서 결정해줄 수 있어요 😉:
<CardItem thumbnail={(images && images.length > 0) && images[0]}이렇게 하면 카드 컴포넌트가 진짜 필요한 매개인자로 구성할 수 있겠군요 !
| const useClickOutside = (ref, onClickOutside) => { | ||
| useEffect(() => { | ||
| const handleClickOutside = e => { | ||
| if (!ref.current || ref.current.contains(e.target)) { | ||
| return; | ||
| } | ||
| onClickOutside(e); | ||
| }; | ||
|
|
||
| document.addEventListener("mousedown", handleClickOutside); | ||
| document.addEventListener("touchstart", handleClickOutside); | ||
| return () => { | ||
| document.removeEventListener("mousedown", handleClickOutside); | ||
| document.removeEventListener("touchstart", handleClickOutside); | ||
| }; | ||
| }, [ref, onClickOutside]); | ||
| }; |
There was a problem hiding this comment.
크으~~~~ 재사용성을 고려한 커스텀 훅 !
이렇게 인터랙션을 훅으로 담아두니까 모달, 드롭다운, 팝오버 등 여러 곳에서 사용되기 너무 좋을 것 같아요.
재사용성을 위해서 커스텀 훅을 설계하시다니.
미션 5를 진행하시는게 아까울 정도네요. 😂😂😂
| const useResponsiveView = () => { | ||
| const [view, setView] = useState(() => { | ||
| const width = window.innerWidth; | ||
| if (width <= BREAKPOINTS.mobileMax) return "mobile"; | ||
| if (width <= BREAKPOINTS.tabletMax) return "tablet"; | ||
| return "desktop"; | ||
| }); | ||
|
|
||
| useEffect(() => { | ||
| const handleResize = () => { | ||
| const width = window.innerWidth; | ||
| if (width <= BREAKPOINTS.mobileMax) { | ||
| setView("mobile"); | ||
| } else if (width <= BREAKPOINTS.tabletMax) { | ||
| setView("tablet"); | ||
| } else { | ||
| setView("desktop"); | ||
| } | ||
| }; | ||
|
|
||
| window.addEventListener("resize", handleResize); | ||
| return () => window.removeEventListener("resize", handleResize); | ||
| }, []); | ||
|
|
||
| return view; | ||
| }; |
There was a problem hiding this comment.
useResponsiveView도 우리 판다마켓에서 재사용성이 좋아보이네요 👍👍👍
| const toggle = useCallback(() => setState(prev => !prev), []); | ||
| const setOn = useCallback(() => setState(true), []); | ||
| const setOff = useCallback(() => setState(false), []); |
There was a problem hiding this comment.
setter(setState)를 그대로 반환하지 않고 핸들러로 감싸서 반환했군요 ! 👍
훌륭합니다. 이렇게 콜백으로 감싸서 전달하면 추 후 유지관리에 용이할거예요. 👍👍👍
| const ITEMS_DISPLAY_COUNT = { | ||
| desktop: 10, | ||
| tablet: 6, | ||
| mobile: 4, | ||
| }; |
There was a problem hiding this comment.
굿굿. 컴포넌트와 관련없는 상수 및 변수를 컴포넌트 바깥에 선언하셨군요 !
훌륭합니다. 전반적으로 연수님은 리액트 활용에 대해서 잘 적응하신 것으로 보이는군요 ! 👍
|
간만에 연수님 코드를 보네요 ㅎㅎㅎ 반가운 마음으로 리뷰하였습니다. 🤣 |
URL
배포 주소 | 판다마켓
요구사항
기본
중고마켓
중고마켓 반응형
Desktop : 4개 보이기
Tablet : 2개 보이기
Mobile : 1개 보이기
Desktop : 12개 보이기
Tablet : 6개 보이기
Mobile : 4개 보이기
심화
주요 변경사항
스크린샷
멘토에게