From 50aa1f9ab8edaad4558bbdbc60b722f85b4a6f32 Mon Sep 17 00:00:00 2001 From: heonq Date: Fri, 1 Nov 2024 20:47:28 +0900 Subject: [PATCH 01/13] =?UTF-8?q?feat:=20Typescript=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/sprint6.md | 39 +++++++++ eslint.config.js | 41 ++++------ package.json | 7 +- src/{App.jsx => App.tsx} | 0 src/{Root.jsx => Root.tsx} | 0 src/apis/ProductService.js | 65 --------------- src/apis/ProductService.ts | 36 +++++++++ .../{axiosInstance.js => axiosInstance.ts} | 14 ++-- .../common/{Footer.jsx => Footer.tsx} | 0 src/components/common/{Nav.jsx => Nav.tsx} | 15 ++-- .../common/{PageIndex.jsx => PageIndex.tsx} | 13 ++- .../{RefreshButton.jsx => RefreshButton.tsx} | 11 ++- .../common/{Select.jsx => Select.tsx} | 39 ++++++--- .../products/BestProductContainer.jsx | 81 ------------------- .../products/{Product.jsx => Product.tsx} | 30 ++++--- .../{ProductsBar.jsx => ProductsBar.tsx} | 7 +- ...ctsContainer.jsx => ProductsContainer.tsx} | 14 +++- .../{mediaQuery.js => mediaQuery.ts} | 0 src/constants/{messages.js => messages.ts} | 8 +- .../{productSortBy.js => productSortBy.ts} | 0 .../{propValues.js => propValues.ts} | 0 src/constants/{url.js => url.ts} | 0 src/hooks/{useProducts.js => useProducts.ts} | 18 +++-- .../{useScreenWidth.js => useScreenWidth.ts} | 7 +- ...dState.js => productSearchKeywordState.ts} | 2 +- src/jotai/atoms/productSortByState.js | 6 -- src/jotai/atoms/productSortByState.ts | 7 ++ src/main.jsx | 2 +- src/pages/{Products.jsx => Products.tsx} | 24 +++--- src/{theme.js => theme.ts} | 0 src/types/custom.d.ts | 14 ++++ src/types/datas.ts | 7 ++ src/types/options.ts | 15 ++++ src/types/parameter.ts | 14 ++++ src/utils/formatter.js | 9 --- src/utils/formatter.ts | 8 ++ tsconfig.app.json | 26 ++++++ tsconfig.json | 7 ++ tsconfig.node.json | 24 ++++++ 39 files changed, 343 insertions(+), 267 deletions(-) create mode 100644 docs/sprint6.md rename src/{App.jsx => App.tsx} (100%) rename src/{Root.jsx => Root.tsx} (100%) delete mode 100644 src/apis/ProductService.js create mode 100644 src/apis/ProductService.ts rename src/apis/{axiosInstance.js => axiosInstance.ts} (50%) rename src/components/common/{Footer.jsx => Footer.tsx} (100%) rename src/components/common/{Nav.jsx => Nav.tsx} (86%) rename src/components/common/{PageIndex.jsx => PageIndex.tsx} (91%) rename src/components/common/{RefreshButton.jsx => RefreshButton.tsx} (70%) rename src/components/common/{Select.jsx => Select.tsx} (77%) delete mode 100644 src/components/products/BestProductContainer.jsx rename src/components/products/{Product.jsx => Product.tsx} (83%) rename src/components/products/{ProductsBar.jsx => ProductsBar.tsx} (94%) rename src/components/products/{ProductsContainer.jsx => ProductsContainer.tsx} (80%) rename src/constants/{mediaQuery.js => mediaQuery.ts} (100%) rename src/constants/{messages.js => messages.ts} (82%) rename src/constants/{productSortBy.js => productSortBy.ts} (100%) rename src/constants/{propValues.js => propValues.ts} (100%) rename src/constants/{url.js => url.ts} (100%) rename src/hooks/{useProducts.js => useProducts.ts} (67%) rename src/hooks/{useScreenWidth.js => useScreenWidth.ts} (83%) rename src/jotai/atoms/{productSearchKeywordState.js => productSearchKeywordState.ts} (58%) delete mode 100644 src/jotai/atoms/productSortByState.js create mode 100644 src/jotai/atoms/productSortByState.ts rename src/pages/{Products.jsx => Products.tsx} (87%) rename src/{theme.js => theme.ts} (100%) create mode 100644 src/types/custom.d.ts create mode 100644 src/types/datas.ts create mode 100644 src/types/options.ts create mode 100644 src/types/parameter.ts delete mode 100644 src/utils/formatter.js create mode 100644 src/utils/formatter.ts create mode 100644 tsconfig.app.json create mode 100644 tsconfig.json create mode 100644 tsconfig.node.json diff --git a/docs/sprint6.md b/docs/sprint6.md new file mode 100644 index 00000000..ba7f3efd --- /dev/null +++ b/docs/sprint6.md @@ -0,0 +1,39 @@ +# 요구사항 + +## 기본 요구사항 + +### 랜딩 페이지 + +- [ ] HTML과 CSS로 구현한 랜딩페이지를 React로 마이그레이션하세요. +- [ ] 랜딩 페이지 url path는 "/"로 설정하세요. + +### 중고마켓 페이지 + +- [ ] 중고마켓 페이지 url path를 "/items"으로 설정하세요. +- [ ] 페이지 주소가 "/items" 일 때 상단내비게이션바의 "중고마켓" 버튼의 색상은 "3692FF"입니다. +- [ ] 중고마켓 페이지 판매 중인 상품은 본인이 만든 GET 메서드를 사용해 주세요. +- [ ] 다만 좋아요 순 정렬 기능은 제외해 주세요. +- [ ] 사진은 디폴트 이미지로 프론트엔드에서 처리해주세요. +- [ ] 베스트 상품 목록 조회는 구현하지 않습니다. +- [ ] '상품 등록하기' 버튼을 누르면 "/registration" 로 이동합니다. ( 빈 페이지 ) + +### 상품 등록 페이지 + +- [ ] PC, Tablet, Mobile 디자인에 해당하는 상품 등록 페이지를 만들어 주세요. +- [ ] 상품 등록 url path는 "/registration"입니다. +- [ ] 상품 등록은 본인이 만든 POST 메서드를 사용해 주세요. +- [ ] 등록 성공 시, 해당 상품 상세 페이지로 이동합니다. (빈페이지) + +## 심화 요구사항 + +### 상품 등록 페이지 + +- [ ] 모든 입력 input box에 빈 값이 있을 경우, 등록 버튼이 비활성화됩니다. +- [ ] 태그를 입력한 후 엔터키를 누르면, 그 태그가 칩 형태로 쌓입니다. +- [ ] 상품명, 상품 소개, 판매 가격, 태그에 대한 유효성 검사 Custom Hook을 만들어주세요. 유효성 검사를 통과하지 않을 경우, 각 input에 빨간색 테두리와, 각각의 Input 아래에 빨간색 에러 메시지를 보여주세요. + +- 유효한 조건 +- 상품명: 1자 이상, 10자 이내 +- 상품 소개: 10자 이상, 100자 이내 +- 판매 가격: 1자 이상, 숫자 +- 태그: 5글자 이내 diff --git a/eslint.config.js b/eslint.config.js index 238d2e4e..1fe5accd 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,38 +1,25 @@ -import js from '@eslint/js' -import globals from 'globals' -import react from 'eslint-plugin-react' -import reactHooks from 'eslint-plugin-react-hooks' -import reactRefresh from 'eslint-plugin-react-refresh' +import js from "@eslint/js"; +import globals from "globals"; +import reactHooks from "eslint-plugin-react-hooks"; +import reactRefresh from "eslint-plugin-react-refresh"; +import tseslint from "typescript-eslint"; -export default [ - { ignores: ['dist'] }, +export default tseslint.config( + { ignores: ["dist"] }, { - files: ['**/*.{js,jsx}'], + extends: [js.configs.recommended, ...tseslint.configs.recommended], + files: ["**/*.{ts,tsx}"], languageOptions: { ecmaVersion: 2020, globals: globals.browser, - parserOptions: { - ecmaVersion: 'latest', - ecmaFeatures: { jsx: true }, - sourceType: 'module', - }, }, - settings: { react: { version: '18.3' } }, plugins: { - react, - 'react-hooks': reactHooks, - 'react-refresh': reactRefresh, + "react-hooks": reactHooks, + "react-refresh": reactRefresh, }, rules: { - ...js.configs.recommended.rules, - ...react.configs.recommended.rules, - ...react.configs['jsx-runtime'].rules, ...reactHooks.configs.recommended.rules, - 'react/jsx-no-target-blank': 'off', - 'react-refresh/only-export-components': [ - 'warn', - { allowConstantExport: true }, - ], + "react-refresh/only-export-components": ["warn", { allowConstantExport: true }], }, - }, -] + } +); diff --git a/package.json b/package.json index e8123fcd..8afe4ee5 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,8 @@ "lint": "eslint .", "preview": "vite preview", "storybook": "storybook dev -p 6006", - "build-storybook": "storybook build" + "build-storybook": "storybook build", + "push": "git push origin react-함헌규-sprint5" }, "dependencies": { "@fortawesome/fontawesome-svg-core": "^6.6.0", @@ -46,7 +47,9 @@ "globals": "^15.9.0", "prop-types": "^15.8.1", "storybook": "^8.3.6", - "vite": "^5.4.8" + "vite": "^5.4.8", + "typescript": "~5.6.2", + "typescript-eslint": "^8.11.0" }, "eslintConfig": { "extends": [ diff --git a/src/App.jsx b/src/App.tsx similarity index 100% rename from src/App.jsx rename to src/App.tsx diff --git a/src/Root.jsx b/src/Root.tsx similarity index 100% rename from src/Root.jsx rename to src/Root.tsx diff --git a/src/apis/ProductService.js b/src/apis/ProductService.js deleted file mode 100644 index f19659d3..00000000 --- a/src/apis/ProductService.js +++ /dev/null @@ -1,65 +0,0 @@ -import URLS from "../constants/url.js"; -import api from "./axiosInstance.js"; - -/** - * @param {object} body 상품목록 조회에 필요한 정보를 담은 객체 - * @param {number} page 조회할 페이지번호 - * @param {number} pageSize 조회했을 때 가져올 응답값의 개수 - * @param {favorite | recent} orderBy 조회할 때 정렬 기준 - * @param {string} keyword 상품 조회에 사용할 키워드 - * @returns {Promise} 비동기로 인한 Promise로 productList를 리턴 - * @throws {Error} API 요청 실패 시 에러 - */ -export const getProductList = async (params = {}) => { - const { data } = await api.get(URLS.products, { params }); - return data; -}; - -export const getProduct = async (id) => { - const { data } = await api.get(`${URLS.products}/${id}`); - return data; -}; - -/** - * @param {Object} body - 상품 생성에 필요한 정보를 담은 객체 - * @param {string} body.name - 상품의 이름 - * @param {string} body.description - 상품에 대한 설명 - * @param {number} body.price - 상품의 가격 - * @param {string[]} body.tags - 상품의 태그가 담긴 배열 - * @param {string[]} body.images - 상품의 이미지 경로가 담긴 배열 - * @returns {Promise} 비동기로 인한 Promise로 생성한 상품을 리턴 - * @throws {Error} API 요청 실패 시 에러 - */ -export const createProduct = async (body) => { - const { data } = await api.post(URLS.products, body); - return data; -}; - -/** - * @param {number} id - 수정할 상품의 id - * @param {Object} body - 상품 수정에 필요한 정보를 담은 객체 - * @param {string} body.name - 수정할 이름 - * @param {string} body.description - 수정할 설명 - * @param {number} body.price - 수정할 가격 - * @param {string[]} body.tags - 수정할 태그가 담긴 배열 - * @param {string[]} body.images - 수정할 이미지 경로가 담긴 배열 - * @returns {Promise} 비동기로 인한 Promise로 수정한 상품을 리턴 - * @throws {Error} API 요청 실패 시 에러 - */ -export const patchProduct = async (id, body) => { - const { data } = await api.patch(`${URLS.products}/${id}`, body); - return data; -}; - -export const deleteProduct = async (id) => { - await api.delete(`${URLS.products}/${id}`); -}; - -export const setFavoriteProduct = async (id) => { - const { data } = await api.post(`${URLS.products}/${id}/favorite`); - return data; -}; - -export const unsetFavoriteProduct = async (id) => { - await api.delete(`${URLS.products}/${id}/favorite`); -}; diff --git a/src/apis/ProductService.ts b/src/apis/ProductService.ts new file mode 100644 index 00000000..a5e950b7 --- /dev/null +++ b/src/apis/ProductService.ts @@ -0,0 +1,36 @@ +import URLS from "../constants/url"; +import { ICreateProductParams, ISearchProductsParams } from "../types/parameter"; +import api from "./axiosInstance"; + +export const getProductList = async (params: Partial = {}) => { + const { data } = await api.get(URLS.products, { params }); + return data; +}; + +export const getProduct = async (id: string) => { + const { data } = await api.get(`${URLS.products}/${id}`); + return data; +}; + +export const createProduct = async (body: ICreateProductParams) => { + const { data } = await api.post(URLS.products, body); + return data; +}; + +export const patchProduct = async (id: string, body: Partial) => { + const { data } = await api.patch(`${URLS.products}/${id}`, body); + return data; +}; + +export const deleteProduct = async (id: string) => { + await api.delete(`${URLS.products}/${id}`); +}; + +export const setFavoriteProduct = async (id: string) => { + const { data } = await api.post(`${URLS.products}/${id}/favorite`); + return data; +}; + +export const unsetFavoriteProduct = async (id: string) => { + await api.delete(`${URLS.products}/${id}/favorite`); +}; diff --git a/src/apis/axiosInstance.js b/src/apis/axiosInstance.ts similarity index 50% rename from src/apis/axiosInstance.js rename to src/apis/axiosInstance.ts index fbf1d2f8..2f8dfa8a 100644 --- a/src/apis/axiosInstance.js +++ b/src/apis/axiosInstance.ts @@ -1,6 +1,6 @@ -import axios from "axios"; -import URLS from "../constants/url.js"; -import { HTTP_ERROR_MESSAGE } from "../constants/messages.js"; +import axios, { AxiosError } from "axios"; +import URLS from "../constants/url"; +import { HTTP_ERROR_MESSAGE } from "../constants/messages"; const api = axios.create({ baseURL: URLS.baseUrl, @@ -12,10 +12,12 @@ const api = axios.create({ api.interceptors.response.use( (response) => response, (error) => { - if (error.response) { + if (error instanceof AxiosError) { throw new Error( - HTTP_ERROR_MESSAGE[error.response.status] ?? - `${HTTP_ERROR_MESSAGE.else} : ${error.response.status}` + error.response?.status + ? HTTP_ERROR_MESSAGE[error.response.status.toString()] || + `${HTTP_ERROR_MESSAGE.else} : ${error.response.status}` + : HTTP_ERROR_MESSAGE.else ); } if (error.request) { diff --git a/src/components/common/Footer.jsx b/src/components/common/Footer.tsx similarity index 100% rename from src/components/common/Footer.jsx rename to src/components/common/Footer.tsx diff --git a/src/components/common/Nav.jsx b/src/components/common/Nav.tsx similarity index 86% rename from src/components/common/Nav.jsx rename to src/components/common/Nav.tsx index ec76d084..e997db52 100644 --- a/src/components/common/Nav.jsx +++ b/src/components/common/Nav.tsx @@ -1,8 +1,14 @@ import styled from "styled-components"; -import PropTypes from "prop-types"; import logo from "../../../public/images/common/logo.png"; import mobileLogo from "../../../public/images/common/mobileLogo.png"; import { MEDIA_QUERY } from "../../constants/mediaQuery"; +import { ReactNode } from "react"; +import { ScreenWidth } from "../../types/options"; + +interface INavProps { + screenWidth: ScreenWidth; + children?: ReactNode; +} const NavComponent = styled.nav` width: 100%; @@ -51,7 +57,7 @@ const LoginButton = styled.button` height: 4.2rem; `; -function Nav({ screenWidth, children }) { +function Nav({ screenWidth, children }: INavProps) { return ( @@ -66,8 +72,3 @@ function Nav({ screenWidth, children }) { } export default Nav; - -Nav.propTypes = { - screenWidth: PropTypes.string, - children: PropTypes.node, -}; diff --git a/src/components/common/PageIndex.jsx b/src/components/common/PageIndex.tsx similarity index 91% rename from src/components/common/PageIndex.jsx rename to src/components/common/PageIndex.tsx index 8bcf521f..16eef9c1 100644 --- a/src/components/common/PageIndex.jsx +++ b/src/components/common/PageIndex.tsx @@ -1,9 +1,13 @@ import styled from "styled-components"; import { faAngleLeft, faAngleRight } from "@fortawesome/free-solid-svg-icons"; -import PropTypes from "prop-types"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { useEffect, useState } from "react"; +interface IPageIndexProps { + page: number; + setPage: (value: number) => void; +} + const ButtonsContainer = styled.div` display: flex; gap: 0.4rem; @@ -25,7 +29,7 @@ const CircleButton = styled.button` } `; -function PageIndex({ page, setPage }) { +function PageIndex({ page, setPage }: IPageIndexProps) { const PAGE_SIZE = 5; const [pageGroup, setPageGroup] = useState(0); const start = pageGroup * PAGE_SIZE + 1; @@ -63,8 +67,3 @@ function PageIndex({ page, setPage }) { } export default PageIndex; - -PageIndex.propTypes = { - page: PropTypes.number, - setPage: PropTypes.func, -}; diff --git a/src/components/common/RefreshButton.jsx b/src/components/common/RefreshButton.tsx similarity index 70% rename from src/components/common/RefreshButton.jsx rename to src/components/common/RefreshButton.tsx index ce389b58..d5bef480 100644 --- a/src/components/common/RefreshButton.jsx +++ b/src/components/common/RefreshButton.tsx @@ -1,8 +1,11 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faRotateRight } from "@fortawesome/free-solid-svg-icons"; -import PropTypes from "prop-types"; -const RefreshButton = ({ refresh }) => { +interface IRefreshButton { + refresh: () => void; +} + +const RefreshButton = ({ refresh }: IRefreshButton) => { return ( { }; export default RefreshButton; - -RefreshButton.propTypes = { - refresh: PropTypes.func, -}; diff --git a/src/components/common/Select.jsx b/src/components/common/Select.tsx similarity index 77% rename from src/components/common/Select.jsx rename to src/components/common/Select.tsx index 2ff17c41..b5218dbe 100644 --- a/src/components/common/Select.jsx +++ b/src/components/common/Select.tsx @@ -2,9 +2,26 @@ import { useState } from "react"; import styled from "styled-components"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faCaretDown } from "@fortawesome/free-solid-svg-icons"; -import PropTypes from "prop-types"; import DropDown from "../../../public/icons/DropDown.png"; import { MEDIA_QUERY } from "../../constants/mediaQuery"; +import { useSetAtom } from "jotai"; +import { WritableAtom } from "jotai"; +import { SetStateAction } from "jotai"; +import { ProductSortOption, ScreenWidth } from "../../types/options"; + +interface ISelectProps { + selectedOption: string; + setOption: ReturnType< + typeof useSetAtom], void>> + >; + optionsString: string[]; + optionsValue: ProductSortOption[]; + screenWidth: ScreenWidth; +} + +interface OptionContainerProps { + $isOpen: boolean; +} const SelectContainer = styled.div` position: relative; @@ -41,7 +58,7 @@ const DropdownButtonContainer = styled.div` } `; -const OptionContainer = styled.div` +const OptionContainer = styled.div` position: absolute; width: 100%; top: 100%; @@ -67,9 +84,15 @@ const Option = styled.div` } `; -function Select({ selectedOption, setOption, optionsString, optionsValue, screenWidth }) { +function Select({ + selectedOption, + setOption, + optionsString, + optionsValue, + screenWidth, +}: ISelectProps) { const [isOpen, setIsOpen] = useState(false); - const handleSelect = (option) => { + const handleSelect = (option: ProductSortOption) => { setOption(option); setIsOpen(false); }; @@ -97,12 +120,4 @@ function Select({ selectedOption, setOption, optionsString, optionsValue, screen ); } -Select.propTypes = { - selectedOption: PropTypes.string, - setOption: PropTypes.func, - optionsString: PropTypes.arrayOf(PropTypes.string), - optionsValue: PropTypes.arrayOf(PropTypes.string), - screenWidth: PropTypes.string, -}; - export default Select; diff --git a/src/components/products/BestProductContainer.jsx b/src/components/products/BestProductContainer.jsx deleted file mode 100644 index 42b3cf22..00000000 --- a/src/components/products/BestProductContainer.jsx +++ /dev/null @@ -1,81 +0,0 @@ -import styled from "styled-components"; -import Product from "./Product"; -import { useEffect, useMemo, useState } from "react"; -import { getProductList } from "../../apis/ProductService"; -import PropTypes from "prop-types"; -import { MEDIA_QUERY } from "../../constants/mediaQuery"; - -const BestProductSection = styled.section` - width: 100%; - display: flex; - flex-direction: column; - margin-top: 2.6rem; - h1 { - font-size: 2rem; - color: ${(props) => props.theme.color.subBlack}; - margin-bottom: 1.5rem; - line-height: 3.2rem; - font-weight: 700; - } -`; - -const BestProductsContainer = styled.div` - width: 100%; - display: flex; - gap: 2.4rem; - justify-content: center; -`; - -function BestProductContainer({ screenWidth }) { - const [bestProducts, setBestProducts] = useState([]); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - const pageSize = useMemo(() => MEDIA_QUERY.bestProductsPageSize[screenWidth], [screenWidth]); - - useEffect(() => { - const fetchBestProductsList = async () => { - try { - setLoading(true); - setError(null); - const { list } = await getProductList({ page: 1, pageSize, orderBy: "favorite" }); - setBestProducts(list); - } catch (e) { - setError(e.message); - } finally { - setLoading(false); - } - }; - pageSize && fetchBestProductsList(); - }, [pageSize]); - - return ( - <> - -

베스트 상품

- - {/* 로딩,에러 핸들 구현 예정 */} - {loading &&
로딩중...
} - {error &&
에러 발생
} - {!loading && - !error && - bestProducts.map((bestProduct) => { - const props = { - title: bestProduct.name, - price: bestProduct.price, - likes: bestProduct.favoriteCount, - image: bestProduct.images[0], - best: true, - }; - return ; - })} -
-
- - ); -} - -export default BestProductContainer; - -BestProductContainer.propTypes = { - screenWidth: PropTypes.string, -}; diff --git a/src/components/products/Product.jsx b/src/components/products/Product.tsx similarity index 83% rename from src/components/products/Product.jsx rename to src/components/products/Product.tsx index b8f0973b..f3bcd111 100644 --- a/src/components/products/Product.jsx +++ b/src/components/products/Product.tsx @@ -2,10 +2,22 @@ import styled from "styled-components"; import { faHeart } from "@fortawesome/free-regular-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import formatter from "../../utils/formatter"; -import PropTypes from "prop-types"; -import PROP_VALUES from "../../constants/propValues"; +import PROP_VALUES from "../../constants/propValues.ts"; +import { ProductSize } from "../../types/options.ts"; -const ProductContainer = styled.div` +interface ISizeProp { + $size: ProductSize; +} + +interface IProductProps { + title: string; + price: number; + likes: number; + image: string; + size: ProductSize; +} + +const ProductContainer = styled.div` display: flex; flex-direction: column; width: ${(props) => (props.$size === PROP_VALUES.product.big ? "28.2rem" : "22.1rem")}; @@ -14,7 +26,7 @@ const ProductContainer = styled.div` } `; -const ImageContainer = styled.div` +const ImageContainer = styled.div` border-radius: 1.6rem; width: 100%; height: ${(props) => (props.$size === PROP_VALUES.product.big ? "28.2rem" : "22.1rem")}; @@ -59,7 +71,7 @@ const TextContainer = styled.div` } `; -function Product({ title, price, likes, image, size = PROP_VALUES.product.small }) { +function Product({ title, price, likes, image, size = PROP_VALUES.product.small }: IProductProps) { return ( <> @@ -79,12 +91,4 @@ function Product({ title, price, likes, image, size = PROP_VALUES.product.small ); } -Product.propTypes = { - title: PropTypes.string, - price: PropTypes.number, - likes: PropTypes.number, - image: PropTypes.string, - size: PropTypes.oneOf([PROP_VALUES.product.small, PROP_VALUES.product.big]), -}; - export default Product; diff --git a/src/components/products/ProductsBar.jsx b/src/components/products/ProductsBar.tsx similarity index 94% rename from src/components/products/ProductsBar.jsx rename to src/components/products/ProductsBar.tsx index 0036a8bc..fb57dc53 100644 --- a/src/components/products/ProductsBar.jsx +++ b/src/components/products/ProductsBar.tsx @@ -9,6 +9,11 @@ import { faMagnifyingGlass } from "@fortawesome/free-solid-svg-icons"; import { MEDIA_QUERY } from "../../constants/mediaQuery"; import { useState } from "react"; import productSearchKeywordState from "../../jotai/atoms/productSearchKeywordState"; +import { ScreenWidth } from "../../types/options"; + +interface IProductsBarProps { + screenWidth: ScreenWidth; +} const Input = styled.input` padding: 0.9rem 2rem 0.9rem 3.5rem; @@ -63,7 +68,7 @@ const RegisterButton = styled.button` height: 4.5rem; `; -function ProductsBar({ screenWidth }) { +function ProductsBar({ screenWidth }: IProductsBarProps) { const [productSortBy, setProductSortBy] = useAtom(productSortByState); const setProductSearchKeyword = useSetAtom(productSearchKeywordState); const [searchInputValue, setSearchInputValue] = useState(""); diff --git a/src/components/products/ProductsContainer.jsx b/src/components/products/ProductsContainer.tsx similarity index 80% rename from src/components/products/ProductsContainer.jsx rename to src/components/products/ProductsContainer.tsx index cf91aeb0..f15cf0b9 100644 --- a/src/components/products/ProductsContainer.jsx +++ b/src/components/products/ProductsContainer.tsx @@ -3,6 +3,16 @@ import Product from "./Product"; import PropTypes from "prop-types"; import PROP_VALUES from "../../constants/propValues"; import RefreshButton from "../common/RefreshButton"; +import { IProduct } from "../../types/datas"; +import { ProductSize } from "../../types/options"; + +interface IProductsContainer { + products: IProduct[]; + loading: boolean; + error: string | null; + size: ProductSize; + refetch: () => void; +} const ProductsContainerComponent = styled.div` display: flex; @@ -18,11 +28,11 @@ const ProductsContainerComponent = styled.div` } `; -function ProductsContainer({ products, loading, error, size, refetch }) { +function ProductsContainer({ products, loading, error, size, refetch }: IProductsContainer) { return ( {/* 로딩,에러 핸들 구현 예정 */} - {error && } + {error && } {!loading && !error && products.map((product) => { diff --git a/src/constants/mediaQuery.js b/src/constants/mediaQuery.ts similarity index 100% rename from src/constants/mediaQuery.js rename to src/constants/mediaQuery.ts diff --git a/src/constants/messages.js b/src/constants/messages.ts similarity index 82% rename from src/constants/messages.js rename to src/constants/messages.ts index 70f2e255..88c2ee41 100644 --- a/src/constants/messages.js +++ b/src/constants/messages.ts @@ -1,4 +1,8 @@ -export const DOCUMENT_ERROR_MESSAGES = { +interface IMessage { + [key: string | number]: string; +} + +export const DOCUMENT_ERROR_MESSAGES: IMessage = { failCreatingDocument: "문서를 생성하는데 실패했습니다 : ", failGettingDocument: "문서를 불러오는데 실패했습니다 : ", failGettingDocumentList: "문서 목록을 불러오는데 실패했습니다 : ", @@ -7,7 +11,7 @@ export const DOCUMENT_ERROR_MESSAGES = { succededDeletingDocument: "성공적으로 문서를 삭제했습니다.", }; -export const HTTP_ERROR_MESSAGE = { +export const HTTP_ERROR_MESSAGE: IMessage = { 404: "문서를 찾을 수 없습니다.", 400: "잘못된 값이 입력되었습니다.", 401: "접근 권한이 없습니다.", diff --git a/src/constants/productSortBy.js b/src/constants/productSortBy.ts similarity index 100% rename from src/constants/productSortBy.js rename to src/constants/productSortBy.ts diff --git a/src/constants/propValues.js b/src/constants/propValues.ts similarity index 100% rename from src/constants/propValues.js rename to src/constants/propValues.ts diff --git a/src/constants/url.js b/src/constants/url.ts similarity index 100% rename from src/constants/url.js rename to src/constants/url.ts diff --git a/src/hooks/useProducts.js b/src/hooks/useProducts.ts similarity index 67% rename from src/hooks/useProducts.js rename to src/hooks/useProducts.ts index 5bf593ff..dfc06ef8 100644 --- a/src/hooks/useProducts.js +++ b/src/hooks/useProducts.ts @@ -1,17 +1,23 @@ import { useEffect, useState } from "react"; import { getProductList } from "../apis/ProductService"; import PRODUCT_SORT_BY from "../constants/productSortBy"; +import { IProduct } from "../types/datas"; const useProducts = ({ page = 1, - pageSize, + pageSize = 100, searchKeyword = "", orderBy = PRODUCT_SORT_BY.recent.parameter, +}: { + page?: number; + pageSize: number; + searchKeyword?: string; + orderBy?: "recent" | "favorite"; }) => { - const [products, setProducts] = useState([]); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - const [stale, setStale] = useState(true); + const [products, setProducts] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [stale, setStale] = useState(true); useEffect(() => { const fetchProductsList = async () => { @@ -28,7 +34,7 @@ const useProducts = ({ setProducts(list); } } catch (e) { - setError(e.message); + if (e instanceof Error) setError(e.message); } finally { setLoading(false); setStale(false); diff --git a/src/hooks/useScreenWidth.js b/src/hooks/useScreenWidth.ts similarity index 83% rename from src/hooks/useScreenWidth.js rename to src/hooks/useScreenWidth.ts index 1f38dc34..9ebdf263 100644 --- a/src/hooks/useScreenWidth.js +++ b/src/hooks/useScreenWidth.ts @@ -1,14 +1,15 @@ import { useEffect, useMemo, useState } from "react"; import { MEDIA_QUERY } from "../constants/mediaQuery"; +import { ScreenWidth } from "../types/options"; -const useMediaQuery = (query) => { +const useMediaQuery = (query: string) => { const [matches, setMatches] = useState(false); useEffect(() => { const mediaQuery = window.matchMedia(query); setMatches(mediaQuery.matches); - const handleMediaQuery = (e) => { + const handleMediaQuery = (e: MediaQueryListEvent) => { setMatches(e.matches); }; @@ -25,7 +26,7 @@ const useScreenWidth = () => { const mediumMediaQuery = useMediaQuery(MEDIA_QUERY.query.medium); const smallMediaQuery = useMediaQuery(MEDIA_QUERY.query.small); - const screenWidth = useMemo(() => { + const screenWidth = useMemo(() => { if (smallMediaQuery) return MEDIA_QUERY.value.small; if (mediumMediaQuery) return MEDIA_QUERY.value.medium; if (largeMediaQuery) return MEDIA_QUERY.value.large; diff --git a/src/jotai/atoms/productSearchKeywordState.js b/src/jotai/atoms/productSearchKeywordState.ts similarity index 58% rename from src/jotai/atoms/productSearchKeywordState.js rename to src/jotai/atoms/productSearchKeywordState.ts index d97a4aa3..629b245b 100644 --- a/src/jotai/atoms/productSearchKeywordState.js +++ b/src/jotai/atoms/productSearchKeywordState.ts @@ -1,5 +1,5 @@ import { atom } from "jotai"; -const productSearchKeywordState = atom(""); +const productSearchKeywordState = atom(""); export default productSearchKeywordState; diff --git a/src/jotai/atoms/productSortByState.js b/src/jotai/atoms/productSortByState.js deleted file mode 100644 index 0e297b9a..00000000 --- a/src/jotai/atoms/productSortByState.js +++ /dev/null @@ -1,6 +0,0 @@ -import { atom } from "jotai"; -import PRODUCT_SORT_BY from "../../constants/productSortBy"; - -const productSortByState = atom(PRODUCT_SORT_BY.recent.parameter); - -export default productSortByState; diff --git a/src/jotai/atoms/productSortByState.ts b/src/jotai/atoms/productSortByState.ts new file mode 100644 index 00000000..34945150 --- /dev/null +++ b/src/jotai/atoms/productSortByState.ts @@ -0,0 +1,7 @@ +import { atom } from "jotai"; +import { ProductSortOption } from "../../types/options"; +import PRODUCT_SORT_BY from "../../constants/productSortBy"; + +const productSortByState = atom(PRODUCT_SORT_BY.recent.parameter); + +export default productSortByState; diff --git a/src/main.jsx b/src/main.jsx index 0daebe8f..78a3db17 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -1,6 +1,6 @@ import { StrictMode } from "react"; import { createRoot } from "react-dom/client"; -import App from "./App.jsx"; +import App from "./App"; createRoot(document.getElementById("root")).render( diff --git a/src/pages/Products.jsx b/src/pages/Products.tsx similarity index 87% rename from src/pages/Products.jsx rename to src/pages/Products.tsx index d7c4d7d7..3281abf5 100644 --- a/src/pages/Products.jsx +++ b/src/pages/Products.tsx @@ -1,4 +1,4 @@ -import Footer from "../components/common/footer"; +import Footer from "../components/common/Footer"; import Nav from "../components/common/Nav"; import styled from "styled-components"; import ProductsContainer from "../components/products/ProductsContainer"; @@ -82,27 +82,31 @@ function Products() { const searchKeyword = useAtomValue(productSearchKeywordState); const productSortBy = useAtomValue(productSortByState); const [page, setPage] = useState(1); - const [pageSize, setPageSize] = useState({ - best: MEDIA_QUERY.bestProductsPageSize[screenWidth], - normal: MEDIA_QUERY.productsPageSize[screenWidth], + const [pageSize, setPageSize] = useState(() => { + if (screenWidth) + return { + best: MEDIA_QUERY.bestProductsPageSize[screenWidth], + normal: MEDIA_QUERY.productsPageSize[screenWidth], + }; }); const bestProducts = useProducts({ - pageSize: pageSize.best, + pageSize: pageSize?.best ?? 4, orderBy: PRODUCT_SORT_BY.favorite.parameter, }); const products = useProducts({ - pageSize: pageSize.normal, + pageSize: pageSize?.normal ?? 10, orderBy: productSortBy, searchKeyword, page, }); useEffect(() => { - setPageSize({ - best: MEDIA_QUERY.bestProductsPageSize[screenWidth], - normal: MEDIA_QUERY.productsPageSize[screenWidth], - }); + screenWidth && + setPageSize({ + best: MEDIA_QUERY.bestProductsPageSize[screenWidth], + normal: MEDIA_QUERY.productsPageSize[screenWidth], + }); }, [screenWidth]); useEffect(() => { diff --git a/src/theme.js b/src/theme.ts similarity index 100% rename from src/theme.js rename to src/theme.ts diff --git a/src/types/custom.d.ts b/src/types/custom.d.ts new file mode 100644 index 00000000..6c56c6e9 --- /dev/null +++ b/src/types/custom.d.ts @@ -0,0 +1,14 @@ +declare module "*.woff2" { + const content: string; + export default content; +} + +declare module "*.png" { + const content: string; + export default content; +} + +declare module "*.jpg" { + const content: string; + export default content; +} diff --git a/src/types/datas.ts b/src/types/datas.ts new file mode 100644 index 00000000..b8fc8a53 --- /dev/null +++ b/src/types/datas.ts @@ -0,0 +1,7 @@ +export interface IProduct { + id: number; + name: string; + price: number; + favoriteCount: number; + images: string[]; +} diff --git a/src/types/options.ts b/src/types/options.ts new file mode 100644 index 00000000..8afb7071 --- /dev/null +++ b/src/types/options.ts @@ -0,0 +1,15 @@ +import { MEDIA_QUERY } from "../constants/mediaQuery"; +import PRODUCT_SORT_BY from "../constants/productSortBy"; +import PROP_VALUES from "../constants/propValues"; + +export type ProductSortOption = + | typeof PRODUCT_SORT_BY.recent.parameter + | typeof PRODUCT_SORT_BY.favorite.parameter; + +export type ProductSize = typeof PROP_VALUES.product.big | typeof PROP_VALUES.product.small; + +export type ScreenWidth = + | typeof MEDIA_QUERY.value.large + | typeof MEDIA_QUERY.value.medium + | typeof MEDIA_QUERY.value.small + | undefined; diff --git a/src/types/parameter.ts b/src/types/parameter.ts new file mode 100644 index 00000000..18658ca9 --- /dev/null +++ b/src/types/parameter.ts @@ -0,0 +1,14 @@ +export interface ISearchProductsParams { + page: Number; + pageSize: Number; + orderBy: "favorite" | "recent"; + keyword: string; +} + +export interface ICreateProductParams { + name: string; + description?: string | undefined; + price: number; + tags?: string[]; + images?: string[]; +} diff --git a/src/utils/formatter.js b/src/utils/formatter.js deleted file mode 100644 index 8216c574..00000000 --- a/src/utils/formatter.js +++ /dev/null @@ -1,9 +0,0 @@ -const formatter = { - formatNumber(number) { - const numberToFormat = +number; - if (typeof numberToFormat !== "number") return number; - return numberToFormat.toLocaleString(); - }, -}; - -export default formatter; diff --git a/src/utils/formatter.ts b/src/utils/formatter.ts new file mode 100644 index 00000000..7db06f0e --- /dev/null +++ b/src/utils/formatter.ts @@ -0,0 +1,8 @@ +const formatter = { + formatNumber(number: number) { + const numberToFormat = number; + return numberToFormat.toLocaleString(); + }, +}; + +export default formatter; diff --git a/tsconfig.app.json b/tsconfig.app.json new file mode 100644 index 00000000..f867de0d --- /dev/null +++ b/tsconfig.app.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "Bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["src"] +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..1ffef600 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/tsconfig.node.json b/tsconfig.node.json new file mode 100644 index 00000000..abcd7f0d --- /dev/null +++ b/tsconfig.node.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "ES2022", + "lib": ["ES2023"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "Bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["vite.config.ts"] +} From 13bdccaea73722fc71d1e17a0bc1d7d54b88c05f Mon Sep 17 00:00:00 2001 From: heonq Date: Fri, 1 Nov 2024 20:54:05 +0900 Subject: [PATCH 02/13] =?UTF-8?q?chore:=20main.tsx=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/{main.jsx => main.tsx} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename src/{main.jsx => main.tsx} (74%) diff --git a/src/main.jsx b/src/main.tsx similarity index 74% rename from src/main.jsx rename to src/main.tsx index 78a3db17..77d159f8 100644 --- a/src/main.jsx +++ b/src/main.tsx @@ -2,7 +2,7 @@ import { StrictMode } from "react"; import { createRoot } from "react-dom/client"; import App from "./App"; -createRoot(document.getElementById("root")).render( +createRoot(document.getElementById("root")!).render( From a49caec3feeaa44cd83c881991045ecaf35471d7 Mon Sep 17 00:00:00 2001 From: heonq Date: Sat, 2 Nov 2024 01:27:33 +0900 Subject: [PATCH 03/13] =?UTF-8?q?chore:=20react-router-dom=20=EC=84=A4?= =?UTF-8?q?=EC=B9=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 210 ++++++++++++++++++++++++++++++++++++++++------ package.json | 5 +- src/App.tsx | 5 +- 3 files changed, 192 insertions(+), 28 deletions(-) diff --git a/package-lock.json b/package-lock.json index d5794f79..76e9fd07 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "jotai": "^2.10.1", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-router-dom": "^6.27.0", "styled-components": "^6.1.13", "styled-reset": "^4.5.2" }, @@ -42,6 +43,8 @@ "globals": "^15.9.0", "prop-types": "^15.8.1", "storybook": "^8.3.6", + "typescript": "~5.6.2", + "typescript-eslint": "^8.11.0", "vite": "^5.4.8" } }, @@ -1264,6 +1267,15 @@ "node": ">= 8" } }, + "node_modules/@remix-run/router": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.20.0.tgz", + "integrity": "sha512-mUnk8rPJBI9loFDZ+YzPGdeniYK+FTmRD1TMCz7ev2SNIozyKKpnGgsxO34u6Z4z/t0ITuu7voi/AshfsGsgFg==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@rollup/pluginutils": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.2.tgz", @@ -2782,15 +2794,98 @@ "dev": true, "license": "MIT" }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.12.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.12.2.tgz", + "integrity": "sha512-gQxbxM8mcxBwaEmWdtLCIGLfixBMHhQjBqR8sVWNTPpcj45WlYL2IObS/DNMLH1DBP0n8qz+aiiLTGfopPEebw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.12.2", + "@typescript-eslint/type-utils": "8.12.2", + "@typescript-eslint/utils": "8.12.2", + "@typescript-eslint/visitor-keys": "8.12.2", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "eslint": "^8.57.0 || ^9.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.12.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.12.2.tgz", + "integrity": "sha512-MrvlXNfGPLH3Z+r7Tk+Z5moZAc0dzdVjTgUgwsdGweH7lydysQsnSww3nAmsq8blFuRD5VRlAr9YdEFw3e6PBw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/scope-manager": "8.12.2", + "@typescript-eslint/types": "8.12.2", + "@typescript-eslint/typescript-estree": "8.12.2", + "@typescript-eslint/visitor-keys": "8.12.2", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.11.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.11.0.tgz", - "integrity": "sha512-Uholz7tWhXmA4r6epo+vaeV7yjdKy5QFCERMjs1kMVsLRKIrSdM6o21W2He9ftp5PP6aWOVpD5zvrvuHZC0bMQ==", + "version": "8.12.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.12.2.tgz", + "integrity": "sha512-gPLpLtrj9aMHOvxJkSbDBmbRuYdtiEbnvO25bCMza3DhMjTQw0u7Y1M+YR5JPbMsXXnSPuCf5hfq0nEkQDL/JQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.12.2", + "@typescript-eslint/visitor-keys": "8.12.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.12.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.12.2.tgz", + "integrity": "sha512-bwuU4TAogPI+1q/IJSKuD4shBLc/d2vGcRT588q+jzayQyjVK2X6v/fbR4InY2U2sgf8MEvVCqEWUzYzgBNcGQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.11.0", - "@typescript-eslint/visitor-keys": "8.11.0" + "@typescript-eslint/typescript-estree": "8.12.2", + "@typescript-eslint/utils": "8.12.2", + "debug": "^4.3.4", + "ts-api-utils": "^1.3.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2798,12 +2893,17 @@ "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, "node_modules/@typescript-eslint/types": { - "version": "8.11.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.11.0.tgz", - "integrity": "sha512-tn6sNMHf6EBAYMvmPUaKaVeYvhUsrE6x+bXQTxjQRp360h1giATU0WvgeEys1spbvb5R+VpNOZ+XJmjD8wOUHw==", + "version": "8.12.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.12.2.tgz", + "integrity": "sha512-VwDwMF1SZ7wPBUZwmMdnDJ6sIFk4K4s+ALKLP6aIQsISkPv8jhiw65sAK6SuWODN/ix+m+HgbYDkH+zLjrzvOA==", "dev": true, "license": "MIT", "engines": { @@ -2815,14 +2915,14 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.11.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.11.0.tgz", - "integrity": "sha512-yHC3s1z1RCHoCz5t06gf7jH24rr3vns08XXhfEqzYpd6Hll3z/3g23JRi0jM8A47UFKNc3u/y5KIMx8Ynbjohg==", + "version": "8.12.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.12.2.tgz", + "integrity": "sha512-mME5MDwGe30Pq9zKPvyduyU86PH7aixwqYR2grTglAdB+AN8xXQ1vFGpYaUSJ5o5P/5znsSBeNcs5g5/2aQwow==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/types": "8.11.0", - "@typescript-eslint/visitor-keys": "8.11.0", + "@typescript-eslint/types": "8.12.2", + "@typescript-eslint/visitor-keys": "8.12.2", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -2883,16 +2983,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.11.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.11.0.tgz", - "integrity": "sha512-CYiX6WZcbXNJV7UNB4PLDIBtSdRmRI/nb0FMyqHPTQD1rMjA0foPLaPUV39C/MxkTd/QKSeX+Gb34PPsDVC35g==", + "version": "8.12.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.12.2.tgz", + "integrity": "sha512-UTTuDIX3fkfAz6iSVa5rTuSfWIYZ6ATtEocQ/umkRSyC9O919lbZ8dcH7mysshrCdrAM03skJOEYaBugxN+M6A==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.11.0", - "@typescript-eslint/types": "8.11.0", - "@typescript-eslint/typescript-estree": "8.11.0" + "@typescript-eslint/scope-manager": "8.12.2", + "@typescript-eslint/types": "8.12.2", + "@typescript-eslint/typescript-estree": "8.12.2" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2906,13 +3006,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.11.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.11.0.tgz", - "integrity": "sha512-EaewX6lxSjRJnc+99+dqzTeoDZUfyrA52d2/HRrkI830kgovWsmIiTfmr0NZorzqic7ga+1bS60lRBUgR3n/Bw==", + "version": "8.12.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.12.2.tgz", + "integrity": "sha512-PChz8UaKQAVNHghsHcPyx1OMHoFRUEA7rJSK/mDhdq85bk+PLsUHUBqTQTFt18VJZbmxBovM65fezlheQRsSDA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.11.0", + "@typescript-eslint/types": "8.12.2", "eslint-visitor-keys": "^3.4.3" }, "engines": { @@ -5068,6 +5168,13 @@ "dev": true, "license": "ISC" }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, "node_modules/has-bigints": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", @@ -6972,6 +7079,38 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "license": "MIT" }, + "node_modules/react-router": { + "version": "6.27.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.27.0.tgz", + "integrity": "sha512-YA+HGZXz4jaAkVoYBE98VQl+nVzI+cVI2Oj/06F5ZM+0u3TgedN9Y9kmMRo2mnkSK2nCpNQn0DVob4HCsY/WLw==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.20.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.27.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.27.0.tgz", + "integrity": "sha512-+bvtFWMC0DgAFrfKXKG9Fc+BcXWRUO1aJIihbB79xaeq0v5UzfvnM5houGUm1Y461WVRcgAQ+Clh5rdb1eCx4g==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.20.0", + "react-router": "6.27.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, "node_modules/recast": { "version": "0.23.9", "resolved": "https://registry.npmjs.org/recast/-/recast-0.23.9.tgz", @@ -8009,7 +8148,6 @@ "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -8018,6 +8156,30 @@ "node": ">=14.17" } }, + "node_modules/typescript-eslint": { + "version": "8.12.2", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.12.2.tgz", + "integrity": "sha512-UbuVUWSrHVR03q9CWx+JDHeO6B/Hr9p4U5lRH++5tq/EbFq1faYZe50ZSBePptgfIKLEti0aPQ3hFgnPVcd8ZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.12.2", + "@typescript-eslint/parser": "8.12.2", + "@typescript-eslint/utils": "8.12.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/unbox-primitive": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", diff --git a/package.json b/package.json index 8afe4ee5..20367e19 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "jotai": "^2.10.1", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-router-dom": "^6.27.0", "styled-components": "^6.1.13", "styled-reset": "^4.5.2" }, @@ -47,9 +48,9 @@ "globals": "^15.9.0", "prop-types": "^15.8.1", "storybook": "^8.3.6", - "vite": "^5.4.8", "typescript": "~5.6.2", - "typescript-eslint": "^8.11.0" + "typescript-eslint": "^8.11.0", + "vite": "^5.4.8" }, "eslintConfig": { "extends": [ diff --git a/src/App.tsx b/src/App.tsx index 999dcf9c..9e56c77a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,14 +1,15 @@ import { ThemeProvider } from "styled-components"; import { defaultTheme } from "./theme"; -import Root from "./Root"; import { Provider } from "jotai"; +import { RouterProvider } from "react-router-dom"; +import router from "./router"; function App() { return ( <> - + From 97e706a7286c4e3d690c706c4ff30ecf5ef18c4e Mon Sep 17 00:00:00 2001 From: heonq Date: Sat, 2 Nov 2024 01:28:07 +0900 Subject: [PATCH 04/13] =?UTF-8?q?chore:=20html=20=EC=97=94=ED=8A=B8?= =?UTF-8?q?=EB=A6=AC=20=ED=8F=AC=EC=9D=B8=ED=8A=B8=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.html b/index.html index 197e15a3..ff85f213 100644 --- a/index.html +++ b/index.html @@ -8,6 +8,6 @@
- + From 3c39c190dedcc75a67d586e910d4cdde576ff530 Mon Sep 17 00:00:00 2001 From: heonq Date: Sat, 2 Nov 2024 01:29:47 +0900 Subject: [PATCH 05/13] =?UTF-8?q?feat:=20=EB=9E=9C=EB=94=A9=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EC=A0=9C=EC=9E=91=20=EB=B0=8F=20=EB=9D=BC?= =?UTF-8?q?=EC=9A=B0=ED=8C=85=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Root.tsx | 11 ++- src/components/common/Nav.tsx | 28 ++++++-- src/components/home/Banner.tsx | 43 ++++++++++++ src/components/home/MainPage.tsx | 115 +++++++++++++++++++++++++++++++ src/pages/Home.tsx | 61 ++++++++++++++++ src/pages/Products.tsx | 19 +---- src/theme.ts | 2 + 7 files changed, 255 insertions(+), 24 deletions(-) create mode 100644 src/components/home/Banner.tsx create mode 100644 src/components/home/MainPage.tsx create mode 100644 src/pages/Home.tsx diff --git a/src/Root.tsx b/src/Root.tsx index da889ff6..746e3beb 100644 --- a/src/Root.tsx +++ b/src/Root.tsx @@ -1,7 +1,10 @@ import { createGlobalStyle } from "styled-components"; import reset from "styled-reset"; -import Products from "./pages/Products"; import Pretendard from "../public/fonts/PretendardVariable.woff2"; +import { Outlet } from "react-router-dom"; +import Footer from "./components/common/Footer"; +import Nav from "./components/common/Nav"; +import useScreenWidth from "./hooks/useScreenWidth"; export const GlobalStyle = createGlobalStyle` ${reset} @@ -52,10 +55,14 @@ export const GlobalStyle = createGlobalStyle` `; function Root() { + const screenWidth = useScreenWidth(); + return ( <> - +