diff --git a/src/pages/MarketPage/AllProducts.tsx b/src/pages/MarketPage/AllProducts.tsx index e67af1f5..34795152 100644 --- a/src/pages/MarketPage/AllProducts.tsx +++ b/src/pages/MarketPage/AllProducts.tsx @@ -8,7 +8,9 @@ import getMediaCount from '@/utils/getMediaCount'; import ProductCard from '@/components/Cards/ProductCard'; import SkeletonCard from '@/components/Cards/SkeletonCard'; import useDebounce from '@/hooks/useDebounce'; +import { useNavigate } from 'react-router-dom'; function AllProducts() { + const navigate = useNavigate(); // 미디어 쿼리에 따라 페이지당 제품의 개수를 설정합니다. const { allProductsCount: pageSize } = getMediaCount(); @@ -43,9 +45,12 @@ function AllProducts() { setSort(newSort); setPage(1); // 정렬 변경 시에도 1페이지로 }; + // 상품 추가 버튼 클릭 핸들러 const handleAddClick = () => { - // todo: /additem 으로 이동 + // /additem 으로 이동 + navigate('/additem'); + console.log('상품 추가 버튼 클릭'); }; return ( diff --git a/src/pages/MarketPage/AllProductsHeader.tsx b/src/pages/MarketPage/AllProductsHeader.tsx index d5ec4d85..c8d748d1 100644 --- a/src/pages/MarketPage/AllProductsHeader.tsx +++ b/src/pages/MarketPage/AllProductsHeader.tsx @@ -29,7 +29,10 @@ function AllProductsHeader({ className={style['all-products-header__search']} onChange={(e) => onKeywordChange(e.currentTarget.value)} /> - diff --git a/src/pages/ProductAddPage/ProductAddPage.module.scss b/src/pages/ProductAddPage/ProductAddPage.module.scss new file mode 100644 index 00000000..e3e811f0 --- /dev/null +++ b/src/pages/ProductAddPage/ProductAddPage.module.scss @@ -0,0 +1,155 @@ +/* block */ +.product-add-page { + display: flex; + flex-direction: column; + gap: 1.6rem; + padding: 2rem; + font-size: 1.4rem; + + // ----- header ----- + &__header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1.2rem; + } + + &__title { + font-size: 2rem; + font-weight: 600; + } + + &__submit-btn { + background: #a6abb7; + color: #fff; + border: none; + border-radius: 6px; + padding: 0.6rem 1.4rem; + cursor: pointer; + + &:disabled { + opacity: 0.4; + cursor: not-allowed; + } + } + + // ----- images ----- + &__images { + display: flex; + gap: 1rem; + flex-wrap: wrap; + } + + // 업로더 카드 + &__uploader { + width: 160px; + height: 160px; + border: 2px dashed #d9d9d9; + border-radius: 8px; + background: #fafafa; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + cursor: pointer; + text-align: center; + transition: background 0.15s; + + &:hover { + background: #f0f0f0; + } + } + + &__uploader-plus { + font-size: 3.2rem; + line-height: 1; + } + &__uploader-text { + margin-top: 0.4rem; + font-size: 1.2rem; + color: #777; + } + &__images-error { + margin-top: 0.4rem; + font-size: 1.2rem; + color: #ff4d4f; // 빨간색 + } + + // 썸네일 + &__thumb { + position: relative; + width: 160px; + height: 160px; + border-radius: 8px; + overflow: hidden; + + img { + width: 100%; + height: 100%; + object-fit: cover; + } + } + + &__thumb-delete { + position: absolute; + top: 4px; + right: 4px; + width: 24px; + height: 24px; + border: none; + border-radius: 50%; + background: rgba(0, 0, 0, 0.6); + color: #fff; + line-height: 24px; + font-size: 1.4rem; + cursor: pointer; + } + + // 일반 인풋 / textarea + &__input, + &__textarea { + width: 100%; + border: none; + border-radius: 6px; + background: #f5f6f8; + padding: 1.2rem 1.4rem; + font-size: 1.4rem; + resize: none; + + &:focus { + outline: 2px solid #4f7cff; + background: #fff; + } + } + + // textarea만 별도 + &__textarea { + min-height: 140px; + } + + // ----- tags ----- + &__tags { + display: flex; + gap: 0.6rem; + flex-wrap: wrap; + } + + &__tag { + background: #f1f1f1; + border-radius: 9999px; + padding: 0.4rem 0.8rem 0.4rem 1rem; + font-size: 1.2rem; + position: relative; + display: flex; + align-items: center; + } + + &__tag-delete { + margin-left: 0.4rem; + border: none; + background: transparent; + cursor: pointer; + font-size: 1.2rem; + line-height: 1; + } +} diff --git a/src/pages/ProductAddPage/ProductAddPage.tsx b/src/pages/ProductAddPage/ProductAddPage.tsx new file mode 100644 index 00000000..633b2502 --- /dev/null +++ b/src/pages/ProductAddPage/ProductAddPage.tsx @@ -0,0 +1,157 @@ +import React, { useRef, useState, ChangeEvent, KeyboardEvent } from 'react'; +import style from './ProductAddPage.module.scss'; + +interface ImagePreview { + file: File; + url: string; +} + +const MAX_IMAGES = 1; + +export default function ProductAddPage() { + const [images, setImages] = useState([]); + const [errorMsg, setErrorMsg] = useState(''); + const [name, setName] = useState(''); + const [description, setDescription] = useState(''); + const [price, setPrice] = useState(''); + const [tagInput, setTagInput] = useState(''); + const [tags, setTags] = useState([]); + + const fileInputRef = useRef(null); + + const handleSelectImages = (e: ChangeEvent) => { + const files = e.target.files; + if (!files) return; + + if (images.length >= MAX_IMAGES) { + setErrorMsg('*이미지 등록은 최대 1개까지 가능합니다.'); + e.target.value = ''; + return; + } + + const [file] = files; + setImages([{ file, url: URL.createObjectURL(file) }]); + setErrorMsg(''); + e.target.value = ''; + }; + + const handleRemoveImage = () => { + images.forEach((i) => URL.revokeObjectURL(i.url)); + setImages([]); + setErrorMsg(''); + }; + + const handleTagKeyDown = (e: KeyboardEvent) => { + if (e.nativeEvent.isComposing) return; + + if (e.key !== 'Enter' && e.key !== ',') return; + e.preventDefault(); + + const value = tagInput.trim(); + if (!value || tags.includes(value)) return; + + setTags((prev) => [...prev, value]); + setTagInput(''); + }; + const removeTag = (t: string) => + setTags((prev) => prev.filter((x) => x !== t)); + + const handleSubmit = () => { + console.log({ images, name, description, price: Number(price), tags }); + }; + + return ( +
+ {/* === 헤더 === */} +
+

상품 등록하기

+ +
+ 상품 이미지 +
+ + + {images.map(({ url }) => ( +
+ preview + +
+ ))} +
+ {errorMsg && ( +

{errorMsg}

+ )} + 상품명 + setName(e.target.value)} + /> + 상품 소개 +