diff --git a/vite-project/public/ic_X.svg b/vite-project/public/ic_X.svg new file mode 100644 index 00000000..a85baced --- /dev/null +++ b/vite-project/public/ic_X.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/vite-project/public/ic_plus.svg b/vite-project/public/ic_plus.svg new file mode 100644 index 00000000..b104c536 --- /dev/null +++ b/vite-project/public/ic_plus.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/vite-project/src/App.jsx b/vite-project/src/App.jsx index 5e7626d9..931a2cea 100644 --- a/vite-project/src/App.jsx +++ b/vite-project/src/App.jsx @@ -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 ( - }> - }> - }> + }> + }> + }> + }> + }> + ); diff --git a/vite-project/src/components/BestProductSection/BestProduct.jsx b/vite-project/src/components/BestProductSection/BestProduct.jsx deleted file mode 100644 index 03dd73a3..00000000 --- a/vite-project/src/components/BestProductSection/BestProduct.jsx +++ /dev/null @@ -1,19 +0,0 @@ -import { useResponsivePage } from "../../hooks/useResponsivePage"; -import ProductGrid from "./../common/ProductGrid"; - -const BestProduct = ({ products }) => { - const { bestPicSize, bestPicContainerSize } = useResponsivePage(); - - return ( -
-

베스트 상품

- -
- ); -}; - -export default BestProduct; diff --git a/vite-project/src/components/BestProductSection/BestProducts.jsx b/vite-project/src/components/BestProductSection/BestProducts.jsx new file mode 100644 index 00000000..82f5c227 --- /dev/null +++ b/vite-project/src/components/BestProductSection/BestProducts.jsx @@ -0,0 +1,19 @@ +import { useResponsivePage } from "../../hooks/useResponsivePage"; +import ProductGrid from "../common/ProductGrid"; + +const BestProducts = ({ products }) => { + const responsiveValues = useResponsivePage(); + + return ( +
+

베스트 상품

+ +
+ ); +}; + +export default BestProducts; diff --git a/vite-project/src/components/ProductListSection/Container.jsx b/vite-project/src/components/ProductListSection/Container.jsx index 61488f3f..0d02c770 100644 --- a/vite-project/src/components/ProductListSection/Container.jsx +++ b/vite-project/src/components/ProductListSection/Container.jsx @@ -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 ( -
+
{isMobile ? (

전체 상품

- + + 상품등록하기 +
@@ -46,7 +50,11 @@ const Container = ({ pageSize, isMobile }) => { placeholder="검색할 상품을 입력해주세요" />
- +
@@ -74,8 +82,11 @@ const Container = ({ pageSize, isMobile }) => { > 상품등록하기 - - +
@@ -83,8 +94,8 @@ const Container = ({ pageSize, isMobile }) => { { - 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 (
setIsDropdown(!isDropdown)} + onClick={() => setOpen(!open)} className="hidden w-32 gap-4 px-5 py-3 border xs:flex rounded-xl" > - {dropdownValue} + {selected.label} 드롭다운 화살표 아이콘
setIsDropdown(!isDropdown)} + onClick={() => setOpen(!open)} className="p-3 border rounded-xl xs:hidden" > 드롭다운 아이콘
- {isDropdown && ( -
{ - handleClick(e); - setIsDropdown(!isDropdown); - }} - > -
- - 최신순 - -
- - 좋아요순 - -
+ {open && ( +
    + {options.map((option) => { + return ( +
  • { + handleClick(option.value); + console.log("$", option.value); + }} + > + {option.label} +
  • + ); + })} +
)}
); diff --git a/vite-project/src/components/addItem/Input.jsx b/vite-project/src/components/addItem/Input.jsx new file mode 100644 index 00000000..2489c679 --- /dev/null +++ b/vite-project/src/components/addItem/Input.jsx @@ -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 ( +
+ {type === "file" ? ( +
+
+ + {imgUrl && ( +

+ *이미지 등록은 최대 1개까지 가능합니다 +

+ )} +
+ {imgUrl && ( +
+ 잠시대기 + X 아이콘 onClick()} + /> +
+ )} +
+ ) : ( + + )} +
+ ); +}; + +export default Input; diff --git a/vite-project/src/components/addItem/InputClasses.js b/vite-project/src/components/addItem/InputClasses.js new file mode 100644 index 00000000..33ef2656 --- /dev/null +++ b/vite-project/src/components/addItem/InputClasses.js @@ -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 ", +}; diff --git a/vite-project/src/components/common/ButtonClasses.js b/vite-project/src/components/common/ButtonClasses.js new file mode 100644 index 00000000..482b68a8 --- /dev/null +++ b/vite-project/src/components/common/ButtonClasses.js @@ -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", +}; diff --git a/vite-project/src/components/common/ButtonLink.jsx b/vite-project/src/components/common/ButtonLink.jsx new file mode 100644 index 00000000..6851d17c --- /dev/null +++ b/vite-project/src/components/common/ButtonLink.jsx @@ -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 ( + + {children} + + ); +}; + +export default ButtonLink; diff --git a/vite-project/src/components/common/ProductGrid.jsx b/vite-project/src/components/common/ProductGrid.jsx index d817f146..3b5835de 100644 --- a/vite-project/src/components/common/ProductGrid.jsx +++ b/vite-project/src/components/common/ProductGrid.jsx @@ -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", diff --git a/vite-project/src/components/common/ProductGridTokens.js b/vite-project/src/components/common/ProductGridClasses.js similarity index 100% rename from vite-project/src/components/common/ProductGridTokens.js rename to vite-project/src/components/common/ProductGridClasses.js diff --git a/vite-project/src/components/style/Layout.jsx b/vite-project/src/components/style/Layout.jsx new file mode 100644 index 00000000..72f07c32 --- /dev/null +++ b/vite-project/src/components/style/Layout.jsx @@ -0,0 +1,15 @@ +import { Outlet } from "react-router-dom"; +import Header from "../common/Header"; + +const Layout = () => { + return ( +
+
+
+ +
+
+ ); +}; + +export default Layout; diff --git a/vite-project/src/constant/INPUT_OPTIONS.js b/vite-project/src/constant/INPUT_OPTIONS.js new file mode 100644 index 00000000..1220dc64 --- /dev/null +++ b/vite-project/src/constant/INPUT_OPTIONS.js @@ -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", + }, +]; diff --git a/vite-project/src/constant/RESPONSIVE_CONFIG.js b/vite-project/src/constant/RESPONSIVE_CONFIG.js new file mode 100644 index 00000000..6c55d344 --- /dev/null +++ b/vite-project/src/constant/RESPONSIVE_CONFIG.js @@ -0,0 +1,29 @@ +export const CONFIG = { + desktop: { + pageSize: 10, + bestPageSize: 4, + isMobile: false, + normalPicSize: "normalPicSize", + normalPicContainerSize: "grid224", + bestPicContainerSize: "grid228", + bestPicSize: "bestPicSize", + }, + tablet: { + pageSize: 6, + bestPageSize: 2, + isMobile: false, + normalPicSize: "normalPicSize", + normalPicContainerSize: "grid224", + bestPicContainerSize: "grid343", + bestPicSize: "bestPicTabletMobile", + }, + mobile: { + pageSize: 4, + bestPageSize: 1, + isMobile: true, + normalPicSize: "normalPicMobile", + normalPicContainerSize: "grid168", + bestPicContainerSize: "grid343", + bestPicSize: "bestPicTabletMobile", + }, +}; diff --git a/vite-project/src/constant/SORT_OPTIONS.js b/vite-project/src/constant/SORT_OPTIONS.js new file mode 100644 index 00000000..8956a2d5 --- /dev/null +++ b/vite-project/src/constant/SORT_OPTIONS.js @@ -0,0 +1,4 @@ +export const SORT_OPTIONS = [ + { value: "recent", label: "최신순" }, + { value: "favorite", label: "좋아요순" }, +]; diff --git a/vite-project/src/hooks/useGetProducts.js b/vite-project/src/hooks/useGetProducts.js index 107d68c0..d35f4607 100644 --- a/vite-project/src/hooks/useGetProducts.js +++ b/vite-project/src/hooks/useGetProducts.js @@ -34,7 +34,6 @@ export default function useGetProducts({ loadImg(product.images[0]).catch(() => none_icon) ); const validUrls = await Promise.all(validImagePromises); - console.log(validUrls); const validProducts = recent.list.map((product, index) => ({ ...product, diff --git a/vite-project/src/hooks/useResponsivePage.js b/vite-project/src/hooks/useResponsivePage.js index c8699bcf..edf831df 100644 --- a/vite-project/src/hooks/useResponsivePage.js +++ b/vite-project/src/hooks/useResponsivePage.js @@ -1,56 +1,22 @@ -import { useEffect, useState } from "react"; -import { PIC_SIZES } from "../components/common/ProductGridTokens"; +import { useEffect, useMemo, useState } from "react"; +import { CONFIG } from "../constant/RESPONSIVE_CONFIG"; + +function getDevice() { + if (window.matchMedia("(min-width: 1200px)").matches) return "desktop"; + if (window.matchMedia("(min-width:680px)").matches) return "tablet"; + return "mobile"; +} export const useResponsivePage = () => { - const [pageSize, setPageSize] = useState(10); - const [bestPageSize, setBestPageSize] = useState(4); - const [isMobile, setIsMobile] = useState(false); - const [normalPicSize, setNormalPicSize] = useState("normalPicSize"); - const [normalPicContainerSize, setNormalPicContainerSize] = - useState("grid224"); - const [bestPicContainerSize, setBestPicContainerSize] = useState("grid228"); - const [bestPicSize, setBestPicSize] = useState("bestPicSize"); + const [device, setDevice] = useState(getDevice); useEffect(() => { - function updatePageSize() { - if (window.matchMedia("(min-width: 1200px)").matches) { - setPageSize(10); - setBestPageSize(4); - setIsMobile(false); - setNormalPicSize("normalPicSize"); - setNormalPicContainerSize("grid224"); - setBestPicContainerSize("grid228"); - setBestPicSize("bestPicSize"); - } else if (window.matchMedia("(min-width:680px)").matches) { - setPageSize(6); - setBestPageSize(2); - setIsMobile(false); - setNormalPicSize("normalPicSize"); - setNormalPicContainerSize("grid224"); - setBestPicContainerSize("grid343"); - setBestPicSize("bestPicTabletMobile"); - } else { - setPageSize(4); - setBestPageSize(1); - setIsMobile(true); - setNormalPicSize("normalPicMobile"); - setNormalPicContainerSize("grid168"); - setBestPicContainerSize("grid343"); - setBestPicSize("bestPicTabletMobile"); - } - } - updatePageSize(); - window.addEventListener("resize", updatePageSize); - return () => window.removeEventListener("resize", updatePageSize); + const onResize = () => setDevice(getDevice()); + onResize(); + window.addEventListener("resize", onResize); + return () => window.removeEventListener("resize", onResize); }, []); - return { - pageSize, - bestPageSize, - isMobile, - normalPicSize, - normalPicContainerSize, - bestPicSize, - bestPicContainerSize, - }; + const values = useMemo(() => CONFIG[device], [device]); + return values; }; diff --git a/vite-project/src/pages/AddItems.jsx b/vite-project/src/pages/AddItems.jsx index d60019ab..bd11ac58 100644 --- a/vite-project/src/pages/AddItems.jsx +++ b/vite-project/src/pages/AddItems.jsx @@ -1,7 +1,149 @@ -import React from "react"; +import { useEffect, useRef, useState } from "react"; +import ButtonLink from "../components/common/ButtonLink"; +import Input from "../components/addItem/Input"; +import { INPUT_OPTIONS } from "../constant/INPUT_OPTIONS"; + +const styles = { + fontBase: "text-lg font-bold", + inputContainer: "flex flex-col gap-4", + head: "flex justify-between", +}; const AddItems = () => { - return
상품등록하기
; + const [previewImage, setPreviewImage] = useState(null); + const [isValid, setIsValid] = useState(false); + const [hashtags, setHashtags] = useState([]); + const inputRefs = useRef({}); + + const handleFileChange = (e) => { + const file = e.target.files[0]; + if (file) { + if (previewImage) { + URL.revokeObjectURL(previewImage); + } + const newURL = URL.createObjectURL(file); + setPreviewImage(newURL); + } + }; + + const addToRefs = (el) => { + if (el) { + inputRefs.current[el.name] = el; + } + }; + + const handleRemoveImage = () => { + URL.revokeObjectURL(previewImage); + setPreviewImage(null); + inputRefs.current.product_image.value = ""; + }; + + const handleHashTagEnter = (e) => { + if (e.key === "Enter") { + e.preventDefault(); + const newTag = e.target.value.trim(); + if (newTag && !hashtags.includes(newTag)) { + setHashtags((prev) => [...prev, newTag]); + e.target.value = ""; + handleInputChange(); + } + } + }; + + const handleRemoveHashtag = (tagToRemove) => { + setHashtags(hashtags.filter((tag) => tag !== tagToRemove)); + handleInputChange(); + }; + + // 유효성 검사 함수 + const isFormValid = () => { + const refs = inputRefs.current; + const isNameValid = refs.product_name?.value.trim() !== ""; + const isDescriptionValid = refs.product_description?.value.trim() !== ""; + const isPriceValid = refs.sales_price?.value.trim() !== ""; + const areHashtagsValid = hashtags.length > 0; + + return ( + isNameValid && isDescriptionValid && isPriceValid && areHashtagsValid + ); + }; + + useEffect(() => { + setIsValid(isFormValid()); + }, [hashtags, previewImage]); + + // 텍스트 입력 시 유효성 검사 + const handleInputChange = () => { + setIsValid(isFormValid()); + }; + + const handleSubmit = (e) => { + e.preventDefault(); + const formData = { + product_image: inputRefs.current.product_image?.files[0], + product_name: inputRefs.current.product_name?.value, + product_description: inputRefs.current.product_description?.value, + sales_price: inputRefs.current.sales_price?.value, + hashtag: hashtags, + }; + console.log("$전송할 데이터", formData); + }; + + return ( +
+
+
+

상품 등록하기

+ + 등록 + +
+ {INPUT_OPTIONS.map((option) => ( +
+ + +
+ ))} +
+
+ {hashtags.map((tag, index) => ( +
+ {`#${tag}`} + +
+ ))} +
+
+ ); }; export default AddItems; diff --git a/vite-project/src/pages/ProductList.jsx b/vite-project/src/pages/ProductList.jsx index ecf674cd..f311f2c3 100644 --- a/vite-project/src/pages/ProductList.jsx +++ b/vite-project/src/pages/ProductList.jsx @@ -1,22 +1,24 @@ -import BestProduct from "../components/BestProductSection/BestProduct"; +import BestProduct from "../components/BestProductSection/BestProducts"; import useGetProducts from "../hooks/useGetProducts"; import Header from "../components/common/Header"; import Container from "../components/ProductListSection/Container"; import { useResponsivePage } from "../hooks/useResponsivePage"; const ProductList = () => { - const { pageSize, bestPageSize, isMobile } = useResponsivePage(); + const responsiveValues = useResponsivePage(); const { products: bestProducts } = useGetProducts({ - pageSize: bestPageSize, + pageSize: responsiveValues.bestPageSize, orderBy: "favorite", }); return ( <> -
-
+
- +
); diff --git a/vite-project/src/services/getProductLists.js b/vite-project/src/services/getProductLists.js index 9301015c..6976ef8c 100644 --- a/vite-project/src/services/getProductLists.js +++ b/vite-project/src/services/getProductLists.js @@ -6,9 +6,9 @@ export default async function getProductLists( orderBy = "recent" ) { try { - const response = await instance.get( - `products?page=${page}&pageSize=${pageSize}&orderBy=${orderBy}` - ); + const response = await instance.get("products", { + params: { page, pageSize, orderBy }, + }); return response.data; } catch (error) { throw new Error(`상품을 불러오는데 실패하였습니다. ${error.message}`);