Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
b1227b2
style: MyCard ์ปดํฌ๋„ŒํŠธ ์‹œ์•ˆ์— ๋งž๊ฒŒ style ์ˆ˜์ •
youdaeng2 Jul 23, 2025
c30ebda
feat: ๋ชฉ๋ฐ์ดํ„ฐ ๋ฐ ๋ชฉ์—… ์ด๋ฏธ์ง€, Dot ์•„์ด์ฝ˜ ์ถ”๊ฐ€
youdaeng2 Jul 23, 2025
c09f183
feat: myprofile page ๊ธฐ๋ณธ ui ๊ตฌํ˜„
youdaeng2 Jul 23, 2025
a87cb1f
feat: API ์‘๋‹ต ๊ตฌ์กฐ ๊ธฐ๋ฐ˜์œผ๋กœ ๋ชฉ์—… ๋ฐ์ดํ„ฐ ์ˆ˜์ • ๋ฐ ์—ฐ๋™
youdaeng2 Jul 24, 2025
a3bb463
feat: ๋ฆฌ์‹œํŠธ์— ๋“œ๋กญ๋‹ค์šด ์—ฐ๊ฒฐ ๋ฐ ๋ชฉ์—… ๋ฐ์ดํ„ฐ API ์‘๋‹ต ๊ตฌ์กฐ๋กœ ์—ฐ๋™
youdaeng2 Jul 24, 2025
281f690
feat: ํƒญ ๋ฉ”๋‰ด์— ๋ชฉ์—… ๋ฐ์ดํ„ฐ API ์‘๋‹ต ๊ตฌ์กฐ๋กœ ์—ฐ๋™ ๋ฐ ์ด ๋ชฉ๋ก ์ด ๊ฐœ์ˆ˜ ์ถ”๊ฐ€
youdaeng2 Jul 24, 2025
1eceab8
feat: ํŽ˜์ด์ง€ ๋ฐ˜์‘ํ˜• ์ถ”๊ฐ€ (md,xl)
youdaeng2 Jul 24, 2025
6f47394
refactor:Gnb z-index ์œ„์น˜ ์ˆ˜์ • ๋ฐ menuDropdown modal false ์†์„ฑ ์ถ”๊ฐ€
youdaeng2 Jul 25, 2025
3bc60ff
feat: ๋ฌดํ•œ์Šคํฌ๋กค ๊ตฌํ˜„์„ ์œ„ํ•œ ๋ชฉ์—… ๋ฐ์ดํ„ฐ page ๋ถ„๋ฆฌ, ๋ฆฌ์ŠคํŠธ ์ปดํฌ๋„ŒํŠธ์—์„œ ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๋„๋ก ์ˆ˜์ • (๊ธฐ์กด์€ prop์œผโ€ฆ
youdaeng2 Jul 25, 2025
7301350
feat: ๋ฆฌ์ŠคํŠธ ์ปดํฌ๋„ŒํŠธ์— ๋ฐ์ดํ„ฐ ํŒจ์นญ ์ถ”๊ฐ€ ๋ฐ ์ฃผ์„ ์ถ”๊ฐ€
youdaeng2 Jul 25, 2025
4dd3433
Merge branch 'dev' into feature/myprofile
youdaeng2 Jul 25, 2025
9aad1c7
refactor:Profile ์ปดํฌ๋„ŒํŠธ react-hook-form ๋„์ž… ๋ฐ ๋‹‰๋„ค์ž„ ํผ UX ๊ฐœ์„ 
youdaeng2 Jul 25, 2025
8f49799
feat: DotIcon hover ์‹œ์— ์ƒ‰ ๋ณ€๊ฒฝ ์ฝ”๋“œ ์ถ”๊ฐ€
youdaeng2 Jul 25, 2025
03b1ca6
style: ๋ฑƒ์ง€ ๋‚ด๋ถ€ ํ…์ŠคํŠธ ์„ธ๋กœ ์ •๋ ฌ ์กฐ์ •
youdaeng2 Jul 25, 2025
184ca85
Merge branch 'dev' into feature/myprofile
youdaeng2 Jul 25, 2025
ca09395
style: profile ์ปดํฌ๋„ŒํŠธ ๋ทฐํฌํŠธ xl์—์„œ ํฌ๊ธฐ ๋ณ€๊ฒฝ
youdaeng2 Jul 25, 2025
be9c43c
Merge branch 'dev' into feature/myprofile
youdaeng2 Jul 25, 2025
1a4933b
Merge branch 'dev' into feature/myprofile
youdaeng2 Jul 26, 2025
2da0c5a
chore: myprofile ํŽ˜์ด์ง€ ๋‚ด๋ถ€์— ์žˆ๋˜ ์ปดํฌ๋„ŒํŠธ๋“ค ๊ฒฝ๋กœ ์ˆ˜์ •(build error ์ฒ˜๋ฆฌ)
youdaeng2 Jul 26, 2025
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
Binary file added public/wine.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions src/assets/icons/dot.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions src/components/common/card/MyCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,10 @@ export function MyCard({ rating, timeAgo, title, review, rightSlot, className }:
</div>

{/* ์ œ๋ชฉ */}
<h3 className='text-md font-semibold text-gray-900'>{title}</h3>
<h3 className='custom-text-md-medium md:custom-text-lg-medium text-gray-300'>{title}</h3>

{/* ๋ฆฌ๋ทฐ ๋‚ด์šฉ */}
<p className='text-sm text-gray-700'>{review}</p>
<p className='custom-text-md-regular md:custom-text-lg-regular text-gray-800'>{review}</p>
</div>
);
}
2 changes: 1 addition & 1 deletion src/components/common/dropdown/MenuDropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ interface MenuDropdownProps {

export default function MenuDropdown({ options, onSelect, trigger }: MenuDropdownProps) {
return (
<DropdownMenu>
<DropdownMenu modal={false}>
{/* ๋“œ๋กญ๋‹ค์šด ํŠธ๋ฆฌ๊ฑฐ ๋ฒ„ํŠผ */}
<DropdownMenuTrigger asChild>{trigger}</DropdownMenuTrigger>
{/* ๋“œ๋กญ๋‹ค์šด ๋ฉ”๋‰ด ์˜์—ญ */}
Expand Down
109 changes: 109 additions & 0 deletions src/components/myprofile/Profile.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import React from 'react';

import { useForm, type SubmitHandler } from 'react-hook-form';

import Input from '@/components/common/Input';
import { Button } from '@/components/ui/button';

interface ProfileProps {
nickname: string; // ํ˜„์žฌ ์‚ฌ์šฉ์ž ๋‹‰๋„ค์ž„ (์ดˆ๊ธฐ๊ฐ’์œผ๋กœ ์‚ฌ์šฉ)
profileImageUrl: string; // ํ”„๋กœํ•„ ์ด๋ฏธ์ง€ URL (์ด๋ฏธ์ง€ ํ‘œ์‹œ์šฉ)
}

interface FormValues {
nickname: string; // ํผ์—์„œ ์ž…๋ ฅํ•  ๋‹‰๋„ค์ž„ ๊ฐ’
}

export default function Profile({ nickname, profileImageUrl }: ProfileProps) {
// useForm ํ›… ์ดˆ๊ธฐํ™”
const {
register, // input ๋“ฑ๋ก์šฉ ํ•จ์ˆ˜
handleSubmit, // ํผ ์ œ์ถœ ํ•ธ๋“ค๋Ÿฌ ๋ž˜ํผ
watch, // ํŠน์ • ํ•„๋“œ ๊ฐ’ ๊ด€์ฐฐ
reset, // ํผ ์ƒํƒœ ์ดˆ๊ธฐํ™”
formState: { isSubmitting }, // ์ œ์ถœ ์ค‘ ์ƒํƒœ
} = useForm<FormValues>({
defaultValues: { nickname }, // ์ดˆ๊ธฐ๊ฐ’์œผ๋กœ ๊ธฐ์กด ๋‹‰๋„ค์ž„ ์„ค์ •
mode: 'onChange', // ์ž…๋ ฅ ์‹œ๋งˆ๋‹ค ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ์‹คํ–‰
});

// ํ˜„์žฌ ์ž…๋ ฅ๋œ ๊ฐ’์„ ๊ด€์ฐฐ
const current = watch('nickname');
// ๊ธฐ์กด ๋‹‰๋„ค์ž„๊ณผ ๋‹ค๋ฅด๊ณ  ๋น„์–ด์žˆ์ง€ ์•Š์„ ๋•Œ๋งŒ true
const isChanged = current.trim().length > 0 && current !== nickname;

// ํผ ์ œ์ถœ ์‹œ ํ˜ธ์ถœ๋˜๋Š” ํ•จ์ˆ˜
const onSubmit: SubmitHandler<FormValues> = async (data) => {
try {
// ์‹ค์ œ API ์—ฐ๊ฒฐ ์‹œ axios/fetch ํ˜ธ์ถœ๋กœ ๊ต์ฒด
await new Promise((r) => setTimeout(r, 1000));
console.log(`๋‹‰๋„ค์ž„ ๋ณ€๊ฒฝ: ${nickname} โ†’ ${data.nickname}`);

// ์ œ์ถœ ์„ฑ๊ณต ํ›„ ํผ ์ƒํƒœ๋ฅผ ์ƒˆ ๊ธฐ๋ณธ๊ฐ’์œผ๋กœ ์ดˆ๊ธฐํ™”
reset({ nickname: data.nickname });
} catch (e) {
// ์—๋Ÿฌ UI ์—†์ด ์ฝ˜์†”์—๋งŒ ์ถœ๋ ฅ
console.error('๋‹‰๋„ค์ž„ ๋ณ€๊ฒฝ ์˜ค๋ฅ˜:', e);
}
};

return (
<div className='p-5 flex flex-col gap-5 rounded-xl border bg-white xl:justify-between xl:py-7 xl:h-[530px] shadow-md'>
{/* ํ”„๋กœํ•„ ์„น์…˜: ์ด๋ฏธ์ง€ & ํ˜„์žฌ ๋‹‰๋„ค์ž„ */}
<div className='flex items-center gap-4 xl:flex-col xl:gap-8'>
<div className='w-16 h-16 rounded-full overflow-hidden xl:w-40 xl:h-40'>
{/* ์ถ”ํ›„ ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ ๊ธฐ๋Šฅ ์ถ”๊ฐ€ ํ•„์š” */}
<img src={profileImageUrl} alt='ํ”„๋กœํ•„ ์ด๋ฏธ์ง€' className='w-full h-full object-cover' />
</div>
<div className='custom-text-xl-bold text-gray-800 md:custom-text-2xl-bold'>{nickname}</div>
</div>

{/* ๋‹‰๋„ค์ž„ ๋ณ€๊ฒฝ ํผ */}
<form
onSubmit={handleSubmit(onSubmit)} // react-hook-form ์ œ์ถœ ์ฒ˜๋ฆฌ
className='flex flex-col items-end gap-1.5 md:flex-row xl:flex-col'
>
{/* ์ž…๋ ฅ ํ•„๋“œ ๊ทธ๋ฃน */}
<div className='flex flex-col w-full gap-[10px]'>
<label
htmlFor='nickname'
className='custom-text-md-medium text-gray-800 md:custom-text-lg-medium'
>
๋‹‰๋„ค์ž„
</label>
<Input
id='nickname'
type='text'
variant='name'
placeholder='์ƒˆ ๋‹‰๋„ค์ž„์„ ์ž…๋ ฅํ•˜์„ธ์š”'
defaultValue={nickname} // ์ดˆ๊ธฐ๊ฐ’ ์„ค์ •
{...register('nickname', {
required: '๋‹‰๋„ค์ž„์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”.',
minLength: { value: 2, message: '์ตœ์†Œ 2์ž ์ด์ƒ ์ž…๋ ฅํ•˜์„ธ์š”.' },
maxLength: { value: 20, message: '์ตœ๋Œ€ 20์ž๊นŒ์ง€ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.' },
})}
onInvalid={(e: React.FormEvent<HTMLInputElement>) =>
// ๋ธŒ๋ผ์šฐ์ € ์œ ํšจ์„ฑ ์˜ค๋ฅ˜๋ฅผ ์ฝ˜์†”์—๋งŒ ์ถœ๋ ฅ
console.error(
'๋‹‰๋„ค์ž„ ์œ ํšจ์„ฑ ์˜ค๋ฅ˜:',
(e.currentTarget as HTMLInputElement).validationMessage,
)
}
/>
</div>

{/* ์ œ์ถœ ๋ฒ„ํŠผ: ๋ฒ„ํŠผ์ด ์ข€ ์ด์ƒํ•ด์„œ api ์—ฐ๊ฒฐ ํ›„ ์ˆ˜์ •ํ•ด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค๋‹ค */}
<Button
type='submit'
variant='purpleDark'
className='min-w-[89px] md:min-w-[116px] xl:min-w-[96px]'
size='sm'
fontSize='md'
disabled={!isChanged || isSubmitting} // ๋ณ€๊ฒฝ๋œ ์ƒํƒœ && ์ œ์ถœ ์ค‘ ์•„๋‹˜
>
{isSubmitting ? '๋ณ€๊ฒฝ ์ค‘โ€ฆ' : '๋ณ€๊ฒฝํ•˜๊ธฐ'} {/* ์ œ์ถœ ์ค‘ ํ…์ŠคํŠธ ํ† ๊ธ€ */}
</Button>
</form>
</div>
);
}
90 changes: 90 additions & 0 deletions src/components/myprofile/ReviewList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import React from 'react';

import { useQuery } from '@tanstack/react-query';

import DotIcon from '@/assets/icons/dot.svg';
import { MyCard } from '@/components/common/card/MyCard';
import MenuDropdown from '@/components/common/dropdown/MenuDropdown';
import { Badge } from '@/components/ui/badge';

import { mockMyReviewsPage1 } from './mockUser';

/**
* Review ํƒ€์ž… ์ •์˜ (mock ๋ฐ์ดํ„ฐ์—์„œ ์ถ”๋ก )
*/
type Review = (typeof mockMyReviewsPage1.list)[number];

/**
* ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๋Š” ํ•จ์ˆ˜ (ํ˜„์žฌ๋Š” mock, ์ถ”ํ›„ API ํ˜ธ์ถœ๋กœ ๊ต์ฒด)
* ๋ฐ์ดํ„ฐ ํŒจ์น˜ ๋‚ด์šฉ์€ ๋ฌดํ•œ์Šคํฌ๋กค ํ›… ๊ตฌํ˜„ ํ›„ ์ˆ˜์ •๋  ์˜ˆ์ •์ž…๋‹ˆ๋‹ค
*/
async function fetchReviews(): Promise<Review[]> {
return mockMyReviewsPage1.list;
}

/**
* ReviewList ์ปดํฌ๋„ŒํŠธ
* - React Query์˜ useQuery ํ›…์„ ์‚ฌ์šฉํ•ด ๋ฆฌ๋ทฐ ๋ฐ์ดํ„ฐ๋ฅผ ํŒจ์นญ
* - ๋กœ๋”ฉ ๋ฐ ์—๋Ÿฌ ์ƒํƒœ๋ฅผ ์ฒ˜๋ฆฌํ•œ ๋’ค, MyCard ์ปดํฌ๋„ŒํŠธ๋กœ ๋ฆฌ์ŠคํŠธ๋ฅผ ๋ Œ๋”๋ง
*/
export function ReviewList() {
// React Query๋กœ ๋ฆฌ๋ทฐ ๋ฐ์ดํ„ฐ ์š”์ฒญ
const {
data: items = [],
isLoading,
isError,
} = useQuery<Review[], Error>({
queryKey: ['myReviews'],
queryFn: fetchReviews,
});

// ๋กœ๋”ฉ ์ค‘ ํ‘œ์‹œ
if (isLoading) {
return <p className='text-center py-4'>๋ฆฌ๋ทฐ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘โ€ฆ</p>;
}

// ์—๋Ÿฌ ์‹œ ํ‘œ์‹œ
if (isError) {
return <p className='text-center py-4'>๋ฆฌ๋ทฐ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ ์‹คํŒจ</p>;
}

// ์‹ค์ œ ๋ฆฌ๋ทฐ ๋ฆฌ์ŠคํŠธ ๋ Œ๋”๋ง
return (
<div className='space-y-4 mt-4'>
{items.map((review) => (
<MyCard
key={review.id}
// ๋ณ„์  ๋ฑƒ์ง€
rating={
<Badge variant='star'>
<span className='inline-block w-full h-full pt-[2px]'>
โ˜… {review.rating.toFixed(1)}
</span>
</Badge>
}
// ์ž‘์„ฑ์ผ
timeAgo={new Date(review.createdAt).toLocaleDateString()}
// ์ž‘์„ฑ์ž ๋‹‰๋„ค์ž„
title={review.user.nickname}
// ๋ฆฌ๋ทฐ ๋‚ด์šฉ
review={review.content}
// dot ์•„์ด์ฝ˜ ํด๋ฆญ ์‹œ ๋“œ๋กญ๋‹ค์šด ์˜คํ”ˆ
rightSlot={
<MenuDropdown
trigger={
<button className='w-6 h-6 text-gray-500 hover:text-primary transition-colors'>
<DotIcon />
</button>
}
options={[
{ label: '์ˆ˜์ •ํ•˜๊ธฐ', value: 'edit' },
{ label: '์‚ญ์ œํ•˜๊ธฐ', value: 'delete' },
]}
onSelect={(value) => console.log(`${value} clicked for review id: ${review.id}`)}
/>
}
/>
))}
</div>
);
}
33 changes: 33 additions & 0 deletions src/components/myprofile/Tab.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
type Tab = 'reviews' | 'wines';

interface TabNavProps {
current: Tab;
onChange: (t: Tab) => void;
reviewsCount: number;
winesCount: number;
}

export function TabNav({ current, onChange, reviewsCount, winesCount }: TabNavProps) {
const count = current === 'reviews' ? reviewsCount : winesCount;

return (
<nav className='flex justify-between items-center'>
<div className='flex justify-start gap-4'>
{(['reviews', 'wines'] as Tab[]).map((tab) => (
<button
key={tab}
onClick={() => onChange(tab)}
className={`text-center custom-text-2lg-bold md:custom-text-xl-bold ${
current === tab ? 'text-gray-800' : 'text-gray-500'
}`}
>
{tab === 'reviews' ? '๋‚ด๊ฐ€ ์“ด ํ›„๊ธฐ' : '๋‚ด๊ฐ€ ๋“ฑ๋กํ•œ ์™€์ธ'}
</button>
))}
</div>
<span className='custom-text-xs-regular text-primary md:custom-text-md-regular '>
์ด {count}๊ฐœ
</span>
</nav>
);
}
93 changes: 93 additions & 0 deletions src/components/myprofile/WineList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import React from 'react';

import { useQuery } from '@tanstack/react-query';

import DotIcon from '@/assets/icons/dot.svg';
import { ImageCard } from '@/components/common/card/ImageCard';
import MenuDropdown from '@/components/common/dropdown/MenuDropdown';
import { Badge } from '@/components/ui/badge';

import { mockMyWinesPage1 } from './mockUser';

/**
* Wine ํƒ€์ž… ์ •์˜ (mock ๋ฐ์ดํ„ฐ์—์„œ ์ถ”๋ก )
*/
type Wine = (typeof mockMyWinesPage1.list)[number];

/**
* ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๋Š” ํ•จ์ˆ˜ (ํ˜„์žฌ๋Š” mock, ์ถ”ํ›„ API ํ˜ธ์ถœ๋กœ ๊ต์ฒด)
* ๋ฐ์ดํ„ฐ ํŒจ์น˜ ๋‚ด์šฉ์€ ๋ฌดํ•œ์Šคํฌ๋กค ํ›… ๊ตฌํ˜„ ํ›„ ์ˆ˜์ •๋  ์˜ˆ์ •์ž…๋‹ˆ๋‹ค
*/
async function fetchWines(): Promise<Wine[]> {
return mockMyWinesPage1.list;
}

/**
* WineList ์ปดํฌ๋„ŒํŠธ
* - React Query์˜ useQuery ํ›…์„ ์‚ฌ์šฉํ•ด ๋ฆฌ๋ทฐ ๋ฐ์ดํ„ฐ๋ฅผ ํŒจ์นญ
* - ๋กœ๋”ฉ ๋ฐ ์—๋Ÿฌ ์ƒํƒœ๋ฅผ ์ฒ˜๋ฆฌํ•œ ๋’ค, ImageCard ์ปดํฌ๋„ŒํŠธ๋กœ ๋ฆฌ์ŠคํŠธ๋ฅผ ๋ Œ๋”๋ง
*/
export function WineList() {
// React Query๋กœ ์™€์ธ ๋ชฉ๋ก ํŒจ์นญ
const {
data: items = [],
isLoading,
isError,
} = useQuery<Wine[], Error>({
queryKey: ['myWines'],
queryFn: fetchWines,
});

// ๋กœ๋”ฉ ์ค‘ ํ‘œ์‹œ
if (isLoading) {
return <p className='text-center py-4'>์™€์ธ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘โ€ฆ</p>;
}

// ์—๋Ÿฌ ์‹œ ํ‘œ์‹œ
if (isError) {
return <p className='text-center py-4'>์™€์ธ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ ์‹คํŒจ</p>;
}

return (
<div className='flex flex-col mt-9 space-y-9 md:space-y-16 md:mt-16'>
{items.map((w) => (
<ImageCard
key={w.id}
className='relative pl-24 min-h-[164px] md:min-h-[228px] md:pl-44 md:pt-10'
imageSrc={w.image}
imageClassName='object-contain absolute left-3 bottom-0 h-[185px] md:h-[270px] md:left-12'
rightSlot={
// dot ์•„์ด์ฝ˜ ํด๋ฆญ ์‹œ ๋“œ๋กญ๋‹ค์šด ์˜คํ”ˆ
<MenuDropdown
trigger={
<button className='w-6 h-6 text-gray-500 hover:text-primary transition-colors'>
<DotIcon />
</button>
}
options={[
{ label: '์ˆ˜์ •ํ•˜๊ธฐ', value: 'edit' },
{ label: '์‚ญ์ œํ•˜๊ธฐ', value: 'delete' },
]}
onSelect={(value) => console.log(`${value} clicked for wine id: ${w.id}`)}
/>
}
>
{/* ์นด๋“œ ๋‚ด๋ถ€: ์™€์ธ ์ •๋ณด */}
<div className='flex flex-col items-start justify-center h-full'>
<h4 className='text-xl/6 font-semibold text-gray-800 mb-4 md:text-3xl md:mb-5'>
{w.name} {/* ์™€์ธ ์ด๋ฆ„ */}
</h4>
<p className='custom-text-md-legular text-gray-500 mb-2 md:custom-text-lg-legular md:mb-4'>
{w.region} {/* ์ƒ์‚ฐ ์ง€์—ญ */}
</p>
<Badge variant='priceBadge'>
<span className='inline-block w-full h-full pt-[3px]'>
{/* ๊ฐ€๊ฒฉ ํ‘œ์‹œ */}โ‚ฉ {w.price.toLocaleString()}
</span>
</Badge>
</div>
</ImageCard>
))}
</div>
);
}
Loading