diff --git a/src/app/(with-header)/myprofile/_components/MyWIneKebabDropDown .tsx b/src/app/(with-header)/myprofile/_components/MyWIneKebabDropDown .tsx index 79b513b..77d8842 100644 --- a/src/app/(with-header)/myprofile/_components/MyWIneKebabDropDown .tsx +++ b/src/app/(with-header)/myprofile/_components/MyWIneKebabDropDown .tsx @@ -12,7 +12,7 @@ import kebab from '@/assets/icons/menu.svg'; export interface WineDataProps { name: string; - price: number; + price: number | null; region: string; type: 'RED' | 'WHITE' | 'SPARKLING'; image: string; diff --git a/src/app/(with-header)/myprofile/_components/MyWineListContainer.tsx b/src/app/(with-header)/myprofile/_components/MyWineListContainer.tsx index 5ec48be..0978c41 100644 --- a/src/app/(with-header)/myprofile/_components/MyWineListContainer.tsx +++ b/src/app/(with-header)/myprofile/_components/MyWineListContainer.tsx @@ -131,7 +131,7 @@ export default function MyWineListContainer({ setDataCount }: { setDataCount: Re name={value.name} region={value.region} image={value.image} - price={value.price} + price={value.price ?? 0} size='midium' isKebab onClick diff --git a/src/app/(with-header)/wines/_components/WineCard.tsx b/src/app/(with-header)/wines/_components/WineCard.tsx index 769cd58..9b61cb1 100644 --- a/src/app/(with-header)/wines/_components/WineCard.tsx +++ b/src/app/(with-header)/wines/_components/WineCard.tsx @@ -36,7 +36,7 @@ const WineCard = forwardRef(({ wine }, ref) => {

{wine.name}

{wine.region}

- ₩ {wine.price.toLocaleString()} + ₩ {wine.price?.toLocaleString() ?? '가격 미제공'}
diff --git a/src/app/(with-header)/wines/_components/WineListContainer.tsx b/src/app/(with-header)/wines/_components/WineListContainer.tsx index 8825b18..a623a3e 100644 --- a/src/app/(with-header)/wines/_components/WineListContainer.tsx +++ b/src/app/(with-header)/wines/_components/WineListContainer.tsx @@ -12,6 +12,7 @@ import WineCard from './WineCard'; import PostWineModal from '@/components/modal/PostWineModal'; import Refresh from '@/components/Refresh'; import WineListSkeleton from './skeleton/WineListSkeleton'; +import LoadingSpinner from '@/components/LoadingSpinner'; import filterIcon from '@/assets/icons/filter.svg'; const MAX_PRICE = 2000000; @@ -32,6 +33,7 @@ export default function WineListContainer() { const [hasError, setHasError] = useState(false); const [nextCursor, setNextCursor] = useState(null); const [hasMore, setHasMore] = useState(true); + const [isInitialLoading, setIsInitialLoading] = useState(true); const lastWineRef = useRef(null); const loadMoreWines = useCallback(async () => { @@ -56,9 +58,23 @@ export default function WineListContainer() { console.error(error); } finally { setIsLoading(false); + setIsInitialLoading(false); } }, [isLoading, hasMore, nextCursor, filters, searchQuery]); + useEffect(() => { + setWines([]); + setNextCursor(null); + setHasMore(true); + setIsInitialLoading(true); + }, [filters, searchQuery]); + + useEffect(() => { + if (isInitialLoading) { + loadMoreWines(); + } + }, [isInitialLoading, loadMoreWines]); + useEffect(() => { if (!hasMore || !lastWineRef.current) return; const observer = new IntersectionObserver( @@ -160,10 +176,16 @@ export default function WineListContainer() { />
)} - {wines.map((wine, index) => ( - - ))} - {isLoading && } + {isInitialLoading ? ( + + ) : ( + <> + {wines.map((wine, index) => ( + + ))} + {isLoading && } + + )} diff --git a/src/components/Button.tsx b/src/components/Button.tsx index 782792d..17e08bd 100644 --- a/src/components/Button.tsx +++ b/src/components/Button.tsx @@ -15,7 +15,7 @@ interface ButtonProps { export default function Button({ type = 'button', onClick, href, text, disabled = false, className = '', variant = 'primary' }: ButtonProps) { const variantStyles = variant === 'primary' - ? 'bg-purple-100 font-bold text-white' + ? 'bg-purple-100 font-bold text-white transition-all duration-300 hover:bg-purple-200' : variant === 'oauth' ? 'rounded-2xl border border-gray-300 bg-white px-[120px] py-[14px] font-medium text-gray-800' : 'rounded-xl bg-purple-10 px-[36px] py-[16px] text-lg font-bold text-purple-100'; diff --git a/src/components/modal/PatchWineModal.tsx b/src/components/modal/PatchWineModal.tsx index 854e05a..8c856eb 100644 --- a/src/components/modal/PatchWineModal.tsx +++ b/src/components/modal/PatchWineModal.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import { useRouter } from 'next/navigation'; import Image from 'next/image'; import { useForm, SubmitHandler } from 'react-hook-form'; @@ -15,7 +15,7 @@ interface FormValues { name: string; region: string; image: string; - price: number; + price: number | null; type: string; } @@ -28,7 +28,8 @@ interface postWinePorps { editMyWine: (id: number, editWineData: WineDataProps) => void; } -export default function PatchWineForm({ onClose, id, wineInitialData, editMyWine }: postWinePorps) { +export default function PatchWineModal({ onClose, id, wineInitialData, editMyWine }: postWinePorps) { + const [formattedPrice, setFormattedPrice] = useState(''); const [preview, setPreview] = useState(wineInitialData.image); const router = useRouter(); @@ -40,6 +41,35 @@ export default function PatchWineForm({ onClose, id, wineInitialData, editMyWine { value: () => setValue('type', 'SPARKLING'), label: 'Sparkling' }, ]; + const handlePriceChange = (event: React.ChangeEvent) => { + const rawValue = event.target.value.replaceAll(',', ''); + + if (rawValue === '') { + setFormattedPrice(''); + setValue('price', null); + return; + } + + if (!/^\d*$/.test(rawValue)) return; + + const numericValue = Number(rawValue); + + if (numericValue > 2000000) { + alert('가격은 200만원 이하로 입력해 주세요.'); + return; + } + + setFormattedPrice(numericValue.toLocaleString()); + setValue('price', numericValue); + }; + + useEffect(() => { + if (wineInitialData.price !== null) { + setFormattedPrice(wineInitialData.price.toLocaleString()); + setValue('price', wineInitialData.price); + } + }, [wineInitialData.price, setValue]); + const handlePatchWine: SubmitHandler = async (data) => { const { name, region, image, price, type } = data; @@ -114,12 +144,12 @@ export default function PatchWineForm({ onClose, id, wineInitialData, editMyWine 가격 diff --git a/src/components/modal/PostReviewModal.tsx b/src/components/modal/PostReviewModal.tsx index ae5f6e5..f7daed5 100644 --- a/src/components/modal/PostReviewModal.tsx +++ b/src/components/modal/PostReviewModal.tsx @@ -1,5 +1,5 @@ 'use client'; -import { useState, useEffect } from 'react'; +import { useState, useEffect, useCallback } from 'react'; import { useParams } from 'next/navigation'; import Image from 'next/image'; import { useForm, SubmitHandler } from 'react-hook-form'; @@ -94,9 +94,7 @@ export default function PostReviewModal({ addReview }: { addReview: (newReview: setIsOpen(true); }; - const closeModal = () => { - setSelectedAroma([]); - setResetTrigger((prev) => !prev); + const closeModal = useCallback(() => { reset({ rating: 0, lightBold: 0, @@ -107,8 +105,14 @@ export default function PostReviewModal({ addReview }: { addReview: (newReview: content: '', wineId: wineData.id, }); + setSelectedAroma([]); + setResetTrigger((prev) => !prev); setIsOpen(false); - }; + }, [reset, wineData.id]); + + useEffect(() => { + if (!isOpen) closeModal(); + }, [isOpen, closeModal]); const handleAromaClick = (aroma: string) => { setSelectedAroma((prevSelectedAroma) => (prevSelectedAroma.includes(aroma) ? prevSelectedAroma.filter((a) => a !== aroma) : [...prevSelectedAroma, aroma])); diff --git a/src/components/modal/PostWineModal.tsx b/src/components/modal/PostWineModal.tsx index 79581c4..5aa6089 100644 --- a/src/components/modal/PostWineModal.tsx +++ b/src/components/modal/PostWineModal.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import { useRouter } from 'next/navigation'; import Image from 'next/image'; import { useForm, SubmitHandler } from 'react-hook-form'; @@ -15,7 +15,7 @@ interface FormValues { name: string; region: string; image: string; - price: number; + price: number | null; type: string; } @@ -23,6 +23,7 @@ type ImageValues = { image: FileList }; export default function PostWineModal() { const [isOpen, setIsOpen] = useState(false); + const [formattedPrice, setFormattedPrice] = useState(''); const [preview, setPreview] = useState(null); const router = useRouter(); @@ -43,11 +44,39 @@ export default function PostWineModal() { setIsOpen(true); }; - const closeModal = () => { + const closeModal = useCallback(() => { reset(); setValue('type', ''); + setValue('price', null); + setFormattedPrice(''); setPreview(null); setIsOpen(false); + }, [reset, setValue]); + + useEffect(() => { + if (!isOpen) closeModal(); + }, [isOpen, closeModal]); + + const handlePriceChange = (event: React.ChangeEvent) => { + const rawValue = event.target.value.replaceAll(',', ''); + + if (rawValue === '') { + setFormattedPrice(''); + setValue('price', null); + return; + } + + if (!/^\d*$/.test(rawValue)) return; + + const numericValue = Number(rawValue); + + if (numericValue > 2000000) { + alert('가격은 200만원 이하로 입력해 주세요.'); + return; + } + + setFormattedPrice(numericValue.toLocaleString()); + setValue('price', numericValue); }; const handlePostWine: SubmitHandler = async (data) => { @@ -142,11 +171,12 @@ export default function PostWineModal() { 가격 diff --git a/src/types/wine.ts b/src/types/wine.ts index 5adca0d..28f3fcc 100644 --- a/src/types/wine.ts +++ b/src/types/wine.ts @@ -11,7 +11,7 @@ export interface WineDetails { name: string; region: string; image: string; - price: number; + price: number | null; type: 'RED' | 'WHITE' | 'SPARKLING'; avgRating: number; reviewCount: number; diff --git a/tailwind.config.ts b/tailwind.config.ts index b7c65aa..8819b55 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -26,6 +26,7 @@ export default { purple: { 10: '#F1EDFC', 100: '#6A42DB', + 200: '#5C37C2', }, }, screens: {