Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 0 additions & 12 deletions src/app/(with-header)/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import Image from 'next/image';
import { Metadata } from 'next';
import Button from '@/components/Button';
import bannerDesktop from '@/assets/images/banner_desktop.png';
import bannerTablet from '@/assets/images/banner_tablet.png';
Expand All @@ -11,17 +10,6 @@ import section2Small from '@/assets/images/section2_sm.png';
import section3Large from '@/assets/images/section3_lg.png';
import section3Small from '@/assets/images/section3_sm.png';

export const metadata: Metadata = {
metadataBase: new URL('https://wine-lab.vercel.app/'),
title: 'WINE - 나만의 와인 창고',
description: '한 곳에서 관리하는 나만의 와인 창고',
openGraph: {
title: 'WINE - 나만의 와인 창고',
description: '한 곳에서 관리하는 나만의 와인 창고',
images: ['/thumbnail.png'],
},
};

export default function Home() {
return (
<div className='bg-gray-100'>
Expand Down
14 changes: 12 additions & 2 deletions src/app/(with-header)/wines/_components/FilterPrice.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ const MAX_PRICE = 2000000;
type FilterPriceProps = {
priceRange: [number, number];
onPriceChange: (values: number[]) => void;
onFinalChange: (values: number[]) => void;
};

const FilterPrice = ({ priceRange, onPriceChange }: FilterPriceProps) => {
const FilterPrice = ({ priceRange, onPriceChange, onFinalChange }: FilterPriceProps) => {
return (
<div>
<div className='mb-5 text-xl font-bold text-gray-800'>PRICE</div>
Expand All @@ -23,6 +24,7 @@ const FilterPrice = ({ priceRange, onPriceChange }: FilterPriceProps) => {
max={MAX_PRICE}
values={priceRange}
onChange={onPriceChange}
onFinalChange={onFinalChange}
renderTrack={({ props, children }) => (
<div {...props} className='relative h-[6px] w-full rounded-full bg-gray-100'>
<div
Expand All @@ -35,7 +37,15 @@ const FilterPrice = ({ priceRange, onPriceChange }: FilterPriceProps) => {
{children}
</div>
)}
renderThumb={({ props, index }) => <div {...props} key={index} className='absolute h-5 w-5 -translate-y-1/2 rounded-full border border-gray-300 bg-white' />}
renderThumb={({ props, index }) => {
return (
<div {...props} key={index} className='relative z-10 h-5 w-5 -translate-y-1/2 rounded-full border border-gray-300 bg-white shadow-md focus:outline-none'>
<div
className={`absolute inset-[-9px] -z-10 rounded-full bg-purple-100 opacity-0 transition-transform duration-300 ease-out hover:scale-100 hover:opacity-10 focus-visible:scale-100 focus-visible:opacity-10`}
/>
</div>
);
}}
/>
</div>
</div>
Expand Down
2 changes: 1 addition & 1 deletion src/app/(with-header)/wines/_components/FilterRating.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const FilterRating = ({ selectedRating, onRatingChange }: FilterRatingProps) =>
<input type='radio' name='rating' className='peer hidden' checked={selectedRating === item.value} onChange={() => onRatingChange(item.value)} />
<div className='hidden h-[10px] w-[10px] rounded-[3px] bg-purple-100 peer-checked:block'></div>
</div>
<span className={selectedRating === item.value ? 'text-purple-100' : ''}>{item.label}</span>
<span className={`${selectedRating === item.value ? 'text-purple-100' : ''} hover:text-purple-100`}>{item.label}</span>
</label>
))}
</div>
Expand Down
2 changes: 1 addition & 1 deletion src/app/(with-header)/wines/_components/FilterTypes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const FilterTypes = ({ selectedType, onTypeChange }: FilterTypesProps) => {
{['Red', 'White', 'Sparkling'].map((type) => (
<button
key={type}
className={`rounded-[100px] px-[18px] py-[10px] text-lg font-medium ${selectedType === type ? 'border border-purple-100 bg-purple-100 text-white' : 'border border-gray-300 bg-white text-gray-800'}`}
className={`rounded-[100px] border px-[18px] py-[10px] text-lg font-medium ${selectedType === type ? 'border border-purple-100 bg-purple-100 text-white' : 'border border-gray-300 bg-white text-gray-800 hover:border-purple-100 hover:bg-purple-100 hover:bg-opacity-10'}`}
onClick={() => handleTypeClick(type)}
>
{type}
Expand Down
46 changes: 29 additions & 17 deletions src/app/(with-header)/wines/_components/RecommendWineContainer.tsx
Original file line number Diff line number Diff line change
@@ -1,34 +1,46 @@
'use client';

import { useEffect, useState } from 'react';
import { useEffect, useState, useCallback } from 'react';
import { fetchRecommendWine } from '@/lib/fetchRecommendWine';
import { Wine } from '@/types/wine';
import RecommendWine from './RecommendWine';
import RecommendWineListSkeleton from './skeleton/RecommendWineListSkeleton';
import { Wine } from '@/types/wine';
import Refresh from '@/components/Refresh';

export default function RecommendWineContainer() {
const [recommendedList, setRecommendedList] = useState<Wine[]>([]);
const [loading, setLoading] = useState<boolean>(true);
const [isloading, setIsLoading] = useState<boolean>(true);
const [hasError, setHasError] = useState(false);

useEffect(() => {
const getRecommendedWines = async () => {
setLoading(true);
try {
const wines = await fetchRecommendWine();
setRecommendedList(wines);
} catch (error) {
console.error('추천 와인 목록을 가져오는 데 실패했습니다.', error);
} finally {
setLoading(false);
}
};
const getRecommendedWines = useCallback(async () => {
setIsLoading(true);
try {
const wines = await fetchRecommendWine();
setRecommendedList(wines);
setHasError(false);
} catch (error) {
setHasError(true);
console.error('추천 와인 목록을 가져오는 데 실패했습니다.', error);
} finally {
setIsLoading(false);
}
}, []);

useEffect(() => {
getRecommendedWines();
}, []);
}, [getRecommendedWines]);

if (loading) {
if (isloading) {
return <RecommendWineListSkeleton count={8} />;
}

if (hasError) {
return (
<div className='flex h-[299px] flex-col justify-center rounded-2xl bg-gray-100 mobile:h-[241px]'>
<Refresh handleLoad={getRecommendedWines} iconSize='w-0 h-0' iconTextGap='gap-[10px]' buttonStyle='px-[20px] py-[8px] mobile:px-[16px] mobile:py-[6px] mobile:text-md' />
</div>
);
}

return <RecommendWine recommendedList={recommendedList} />;
}
7 changes: 6 additions & 1 deletion src/app/(with-header)/wines/_components/WineFilter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ export default function WineFilter({ onChangeFilter }: WineFilterProps) {
const handlePriceChange = (values: number[]) => {
const [minPrice, maxPrice] = values;
setPriceRange([minPrice, maxPrice]);
};

const handleFinalPriceChange = (values: number[]) => {
const [minPrice, maxPrice] = values;
setPriceRange([minPrice, maxPrice]);
onChangeFilter({ type: selectedType, minPrice, maxPrice, rating: selectedRating });
};

Expand All @@ -35,7 +40,7 @@ export default function WineFilter({ onChangeFilter }: WineFilterProps) {
return (
<div className='flex flex-col gap-[56px]'>
<FilterTypes selectedType={selectedType} onTypeChange={handleTypeChange} />
<FilterPrice priceRange={priceRange} onPriceChange={handlePriceChange} />
<FilterPrice priceRange={priceRange} onPriceChange={handlePriceChange} onFinalChange={handleFinalPriceChange} />
<FilterRating selectedRating={selectedRating} onRatingChange={handleRatingChange} />
</div>
);
Expand Down
37 changes: 20 additions & 17 deletions src/app/(with-header)/wines/_components/WineFilterModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,28 @@ export default function WineFilterModal({ isOpen, onClose, onApply, onFilterChan
const [selectedRating, setSelectedRating] = useState<number | null>(initialFilters.rating);

const handlePriceChange = (values: number[]) => {
if (values.length !== 2) return;
setPriceRange([values[0], values[1]]);
};

const handleFinalPriceChange = (values: number[]) => {
setPriceRange([values[0], values[1]]);
};

const handleFilterApply = () => {
onApply({ type: selectedType, minPrice: priceRange[0], maxPrice: priceRange[1], rating: selectedRating });
onClose();
};

const handleReset = () => {
setSelectedType(null);
setPriceRange([0, MAX_PRICE]);
setSelectedRating(null);
};

useEffect(() => {
onFilterChange({ type: selectedType, minPrice: priceRange[0], maxPrice: priceRange[1], rating: selectedRating });
}, [selectedType, priceRange, selectedRating, onFilterChange]);

useEffect(() => {
document.body.style.overflow = isOpen ? 'hidden' : 'auto';
return () => {
Expand All @@ -51,21 +69,6 @@ export default function WineFilterModal({ isOpen, onClose, onApply, onFilterChan
};
}, [handleKeyDown]);

const handleFilterApply = () => {
onApply({ type: selectedType, minPrice: priceRange[0], maxPrice: priceRange[1], rating: selectedRating });
onClose();
};

const handleReset = () => {
setSelectedType(null);
setPriceRange([0, MAX_PRICE]);
setSelectedRating(null);
};

useEffect(() => {
onFilterChange({ type: selectedType, minPrice: priceRange[0], maxPrice: priceRange[1], rating: selectedRating });
}, [selectedType, priceRange, selectedRating, onFilterChange]);

if (!isOpen) return null;

return (
Expand All @@ -83,7 +86,7 @@ export default function WineFilterModal({ isOpen, onClose, onApply, onFilterChan

<div className='flex flex-col gap-[56px]'>
<FilterTypes selectedType={selectedType} onTypeChange={setSelectedType} />
<FilterPrice priceRange={priceRange} onPriceChange={handlePriceChange} />
<FilterPrice priceRange={priceRange} onPriceChange={handlePriceChange} onFinalChange={handleFinalPriceChange} />
<FilterRating selectedRating={selectedRating} onRatingChange={setSelectedRating} />
</div>

Expand Down
18 changes: 16 additions & 2 deletions src/app/(with-header)/wines/_components/WineListContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import WineFilter from './WineFilter';
import WineFilterModal from './WineFilterModal';
import WineCard from './WineCard';
import PostWineModal from '@/components/modal/PostWineModal';
import LoadingSpinner from '@/components/LoadingSpinner';
import Refresh from '@/components/Refresh';
import WineListSkeleton from './skeleton/WineListSkeleton';
import filterIcon from '@/assets/icons/filter.svg';

const MAX_PRICE = 2000000;
Expand All @@ -28,6 +29,7 @@ export default function WineListContainer() {
const [isFilterModalOpen, setFilterModalOpen] = useState<boolean>(false);
const [pendingFilters, setPendingFilters] = useState(filters);
const [isLoading, setIsLoading] = useState(false);
const [hasError, setHasError] = useState(false);
const [nextCursor, setNextCursor] = useState<number | null>(null);
const [hasMore, setHasMore] = useState(true);
const lastWineRef = useRef<HTMLDivElement | null>(null);
Expand All @@ -48,7 +50,9 @@ export default function WineListContainer() {
setWines((prev) => [...prev, ...response.list]);
setNextCursor(response.nextCursor);
setHasMore(response.nextCursor !== null);
setHasError(false);
} catch (error) {
setHasError(true);
console.error(error);
} finally {
setIsLoading(false);
Expand Down Expand Up @@ -146,10 +150,20 @@ export default function WineListContainer() {
<WineFilterModal isOpen={isFilterModalOpen} onClose={() => setFilterModalOpen(false)} onApply={applyFilters} initialFilters={pendingFilters} onFilterChange={setPendingFilters} />
<div className='flex min-h-0 flex-col'>
<div className='scrollbar-hidden flex h-[650px] max-h-screen flex-col gap-[62px] tablet:h-[550px]'>
{hasError && (
<div className='pc:mt-28 tablet:mt-20 mobile:mt-10'>
<Refresh
handleLoad={loadMoreWines}
buttonStyle='px-[20px] py-[8px] mobile:px-[16px] mobile:py-[6px] mobile:text-md mobile:font-semibold'
iconSize='w-[100px] h-[100px] mobile:w-[60px] mobile:h-[60px]'
iconTextGap='gap-[10px]'
/>
</div>
)}
{wines.map((wine, index) => (
<WineCard key={`${wine.id}-${index}`} ref={index === wines.length - 1 ? lastWineRef : null} wine={wine} />
))}
{isLoading && <LoadingSpinner />}
{isLoading && <WineListSkeleton count={2} />}
</div>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
export default function RecommendWineCardSkeleton() {
return (
<div className='flex h-[185px] w-[232px] animate-pulse justify-between gap-[28px] rounded-xl bg-white px-[30px] pt-6 mobile:h-[160px] mobile:w-[193px] mobile:gap-[25px] mobile:px-[25px]'>
<div className='h-[161px] w-[44px] flex-shrink-0 rounded bg-gray-200 mobile:h-[136px] mobile:w-[38px]'></div>
<div className='h-[161px] w-[44px] flex-shrink-0 rounded-t-md bg-gray-200 mobile:h-[136px] mobile:w-[38px]'></div>
<div className='flex w-full flex-col gap-[10px] mobile:h-[136px] mobile:w-[80px] mobile:gap-3'>
<div className='h-[40px] w-[60px] rounded bg-gray-200 mobile:h-[30px] mobile:w-[50px]'></div>
<div className='h-[24px] w-[100px] rounded bg-gray-200 mobile:h-[18px] mobile:w-[80px]'></div>
<div className='h-[18px] w-[100px] rounded bg-gray-200 mobile:h-[18px] mobile:w-[80px]'></div>
<div className='h-[40px] w-[60px] rounded-md bg-gray-200 mobile:h-[30px] mobile:w-[50px]'></div>
<div className='h-[24px] w-[100px] rounded-md bg-gray-200 mobile:h-[18px] mobile:w-[80px]'></div>
<div className='h-[18px] w-[100px] rounded-md bg-gray-200 mobile:h-[18px] mobile:w-[80px]'></div>
</div>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
export default function WineCardSkeleton() {
return (
<div className='rounded-2xl border border-gray-300 hover:shadow-lg pc:w-[796px]'>
<div className='flex justify-between gap-[81px] tablet:gap-[47px] mobile:gap-9'>
<div className='ml-[60px] mt-10 h-[208px] w-[60px] overflow-hidden rounded-t-md bg-gray-100 tablet:ml-10 mobile:ml-5' />
<div className='mb-[24px] mr-[60px] mt-[36px] flex flex-1 flex-col justify-between gap-4 mobile:mb-[28px] mobile:mr-[30px] mobile:mt-[40px]'>
<div className='flex justify-between tablet:gap-3 mobile:flex-col mobile:gap-[22px]'>
<div className='flex h-[162px] flex-col items-start justify-between mobile:h-[100px]'>
<div className='h-[42px] rounded-md bg-gray-100 pc:w-[350px] tablet:w-[380px] mobile:w-[200px]' />
<div className='mb-4 mt-5 h-[26px] w-[150px] rounded-md bg-gray-100 mobile:my-2' />
<div className='h-[42px] w-[124px] rounded-md bg-gray-100 mobile:h-[36px] mobile:w-[90px]' />
</div>
<div className='flex h-[162px] flex-col justify-between gap-1 mobile:mr-2 mobile:h-[51px] mobile:flex-row mobile:items-center mobile:gap-0'>
<div className='mobile:mr-1 mobile:flex mobile:flex-1 mobile:items-center mobile:gap-[15px]'>
<div className='mb-[10px] h-[48px] w-[80px] rounded-md bg-gray-100 mobile:mb-0 mobile:h-[48px] mobile:w-[60px]' />
<div className='flex-col mobile:flex'>
<div className='h-[26px] w-[120px] rounded-md bg-gray-100 mobile:h-[24px] mobile:w-[110px]' />
<div className='mt-[10px] h-[24px] w-[70px] max-w-[120px] rounded-md bg-gray-100 mobile:mt-[4px] mobile:h-[20px]' />
</div>
</div>
<div className='h-9 w-9 rounded-md bg-gray-100 pc:self-end tablet:self-end mobile:h-[32px] mobile:w-[32px] mobile:self-auto' />
</div>
</div>
</div>
</div>
<div className='flex flex-col gap-[10px] border-t-[1px] border-t-gray-300 mobile:gap-2'>
<div className='mx-[60px] my-5 h-[62px] rounded-md bg-gray-100 tablet:mx-10 mobile:mx-6 mobile:my-3 mobile:h-[58px]' />
</div>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import WineCardSkeleton from './WineCardSkeleton';

export default function WineListSkeleton({ count }: { count: number }) {
return (
<div className='flex h-[650px] max-h-screen animate-pulse flex-col gap-[62px] overflow-hidden tablet:h-[550px]'>
{new Array(count).fill(0).map((_, idx) => (
<WineCardSkeleton key={`wine-item-skeleton-${idx}`} />
))}
</div>
);
}
14 changes: 13 additions & 1 deletion src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,26 @@
import localFont from 'next/font/local';
import { Metadata } from 'next';
import { AuthProvider } from '@/contexts/AuthProvider';
import './globals.css';
import { ToastContainer, Slide } from 'react-toastify';
import './globals.css';

const pretendard = localFont({
src: '../fonts/PretendardVariable.woff2',
display: 'swap',
variable: '--font-pretendard',
});

export const metadata: Metadata = {
metadataBase: new URL('https://wine-lab.vercel.app/'),
title: 'WINE - 나만의 와인 창고',
description: '한 곳에서 관리하는 나만의 와인 창고',
openGraph: {
title: 'WINE - 나만의 와인 창고',
description: '한 곳에서 관리하는 나만의 와인 창고',
images: ['/thumbnail.png'],
},
};

export default function RootLayout({
children,
}: Readonly<{
Expand Down
4 changes: 2 additions & 2 deletions src/app/signin/_components/LoginForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export default function LoginForm() {
};

return (
<form>
<form onSubmit={handleSubmit(handleLogin)}>
<div className='flex flex-col pb-[28px] mobile:pb-4'>
<label htmlFor='email' className='pb-[10px] font-medium leading-[26px] text-gray-800 mobile:text-[14px]'>
이메일
Expand All @@ -92,7 +92,7 @@ export default function LoginForm() {
</div>

<div className='flex w-full flex-col gap-[15px] pb-8 pt-14 mobile:pb-6 mobile:pt-10'>
<Button type='button' onClick={handleSubmit(handleLogin)} text='로그인' className='h-[50px] rounded-2xl border mobile:text-[14px]'></Button>
<Button type='submit' onClick={handleSubmit(handleLogin)} text='로그인' className='h-[50px] rounded-2xl border mobile:text-[14px]'></Button>
<button type='button' onClick={handleGoogleLogin} className='flex h-[50px] items-center justify-center gap-3 rounded-2xl border border-gray-300 bg-white'>
<Image src={googleIcon} alt='구글아이콘' />
<p className='font-medium mobile:text-[14px]'>Google로 시작하기</p>
Expand Down
2 changes: 1 addition & 1 deletion src/app/signup/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export default function Signup() {
</Link>
<SignupForm></SignupForm>
<div className='flex gap-[14px]'>
<p className='font-normal text-gray-500 mobile:text-[14px]'>계정이 없으신가요?</p>
<p className='font-normal text-gray-500 mobile:text-[14px]'>계정이 이미 있으신가요?</p>
<Link href='/signin' className='font-medium text-purple-100 underline mobile:text-[14px]'>
로그인하기
</Link>
Expand Down
2 changes: 1 addition & 1 deletion src/components/Refresh.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export default function Refresh({ handleLoad, buttonStyle = 'px-[30px] py-[10px]
<p className='text-center text-lg text-gray-500 mobile:text-md'>실패했습니다.</p>
</div>
</div>
<Button onClick={onClickRefreshBtn} text='새로고침' className={`rounded-[100px] ${buttonStyle}`} />
<Button onClick={onClickRefreshBtn} text='새로고침' className={`rounded-[100px] font-semibold ${buttonStyle}`} />
</div>
);
}
Loading