-
Notifications
You must be signed in to change notification settings - Fork 5
Feature/myprofile #59
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weโll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 17 commits
b1227b2
c30ebda
c09f183
a87cb1f
a3bb463
281f690
1eceab8
6f47394
3bc60ff
7301350
4dd3433
9aad1c7
8f49799
03b1ca6
184ca85
ca09395
be9c43c
1a4933b
2da0c5a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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: '๋๋ค์์ ์ ๋ ฅํด์ฃผ์ธ์.', | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ๐๐ ์ข์ต๋๋ค const ProfileSchema = z.object({
nickname: z
.string()
.min(2, { message: '์ต์ 2์ ์ด์ ์
๋ ฅํ์ธ์.' })
.max(20, { message: '์ต๋ 20์๊น์ง ๊ฐ๋ฅํฉ๋๋ค.' }),
});
type ProfileFormValues = z.infer<typeof ProfileSchema >
//useForm ํธ์ถ ์
const { register, handleSubmit, watch, reset,formState: { isSubmitting } } = useForm<ProfileFormValues >({
resolver: zodResolver(ProfileSchema ),//๋ฆฌ์กธ๋ฒ๋ฅผ ๊ฐ์ด ํด์ฃผ๋ฉด
mode: 'onChange',
})
//์ธํ์ ๋๊ธธ ๋
<Input {...register('nickname'} type='text' ... /> ์ด๋ฐ ์์ผ๋ก ๋๊ธธ ์ ์์ต๋๋ค
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ์กฐ๋ ์คํค๋ง๋ฅผ ์จ๋ณธ ์ ์ด ์์ด์ ์๊ฐ์ ๋ชปํ๋ค์!!! ์ข ๋ ๊ณต๋ถ ํ์ ์ด๋ค ๊ฒ ๋์์ง ๊ณ ๋ฏผํด๋ณด๊ณ ๋์ ๋ฐฉํฅ์ผ๋ก ์ ์ฉํด๋ณด๊ฒ ์ต๋๋ค. ์กฐ์ธ ๊ฐ์ฌํฉ๋๋ค! |
||
| minLength: { value: 2, message: '์ต์ 2์ ์ด์ ์ ๋ ฅํ์ธ์.' }, | ||
| maxLength: { value: 20, message: '์ต๋ 20์๊น์ง ๊ฐ๋ฅํฉ๋๋ค.' }, | ||
| })} | ||
| onInvalid={(e: React.FormEvent<HTMLInputElement>) => | ||
| // ๋ธ๋ผ์ฐ์ ์ ํจ์ฑ ์ค๋ฅ๋ฅผ ์ฝ์์๋ง ์ถ๋ ฅ | ||
| console.error( | ||
| '๋๋ค์ ์ ํจ์ฑ ์ค๋ฅ:', | ||
| (e.currentTarget as HTMLInputElement).validationMessage, | ||
| ) | ||
| } | ||
| /> | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ํ์๊ฐ์ ์ API์์ 500์๋ฌ ๋ฐ์ ์์๋ ์๋ฌ ๋ฉ์์ง๋ฅผ ๋ณด์ฌ์ฃผ๊ณ , ๊ทธ์ธ์ ์๋ฌ๊ฐ ๋ฐ์ํ์ ๋๋ ์๋ฌ ๋ชจ๋ฌ์ ๋ฉ์์ง๋ฅผ ๋์์ฃผ๋ ๋ฐฉ์์ผ๋ก ์ฌ์ฉ์ค์ ๋๋ค. ์ฐธ๊ณ ๋ถํ๋๋ฆฝ๋๋ค!
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ํ์ธํด๋ณด๊ฒ ์ต๋๋ค ๊ฐ์ฌํฉ๋๋ค! |
||
| </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> | ||
| ); | ||
| } | ||
| 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]; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ๋๋ฉ์ธ ํ์ ์ ์(+๋ชฉ๋ฐ์ดํฐ ์์ฑ)์ zod๋ก๋ถํฐ ์ถ๋ก (์์ฑ)ํ๊ธธ ์ถ์ฒ๋๋ ค์ ์์ // user.ts
import { z } from "zod";
import { faker } from "@faker-js/faker";
// 1. Zod ์คํค๋ง ์ ์
export const UserSchema = z.object({
id: z.string().uuid(),
name: z.string(),
email: z.string().email(),
age: z.number().int().min(18).max(99),
createdAt: z.date(),
});
// User
type User = z.infer<typeof UserSchema> // { id: string; name: string; email: string; age: number; createdAt: Date; }
// 2. Faker๋ก ๋ฐ์ดํฐ ์์ฑ ํจ์
export function generateFakeUser(): User {
const user = {
id: faker.string.uuid(),
name: faker.person.fullName(),
email: faker.internet.email(),
age: faker.number.int({ min: 18, max: 99 }),
createdAt: faker.date.past(),
};
// Zod๋ก ๊ฒ์ฆ (ํ์
์์ )
return UserSchema.parse(user);
}
// 3. ์ฌ๋ฌ ๊ฐ ์์ฑ
export function generateFakeUsers(count: number): User[] {
return Array.from({ length: count }, generateFakeUser);
}
// ์ฌ์ฉ ์์
const users = generateFakeUsers(5);
console.log(users);
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ํ์ ์ ์์ ์ ์ฉํด๋ณด๊ฒ ์ต๋๋ค ๊ฐ์ฌํฉ๋๋ค |
||
|
|
||
| /** | ||
| * ๋ฐ์ดํฐ ๊ฐ์ ธ์ค๋ ํจ์ (ํ์ฌ๋ 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, | ||
| }); | ||
|
|
||
| // ๋ก๋ฉ ์ค ํ์ | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ์ ๋ฒ์ ๊ตฌํ ์์ธ ๊ณํ ๋ ๋ฉํ ๋๊ป์ Suspense ์ฌ์ฉ ์ถ์ฒํด ์ฃผ์
์ ๊ณ ๋ คํด๋ด์ผ ํ ๊ฒ ๊ฐ์ต๋๋ค. ์ด๊ฑด ํ์ ๋ ์ข ๋ ์ด์ผ๊ธฐ ํด๋ด์ผ ํ ๊ฒ ๊ฐ์์!
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ์ด ๋ถ๋ถ์ ์ข ๋ ์ฐพ์๋ณด๊ณ ํ์์์ ์๊ธฐํด๋ณด๋ฉด ์ข์ ๊ฒ ๊ฐ๋ค์! ๊ฐ์ฌํฉ๋๋ค. |
||
| 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> | ||
| ); | ||
| } | ||
| 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> | ||
| ); | ||
| } |
| 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> | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| import React, { useState } from 'react'; | ||
|
|
||
| import { mockMyReviewsPage1, mockMyWinesPage1 } from './mockUser'; | ||
| import Profile from './Profile'; | ||
| import { ReviewList } from './ReviewList'; | ||
| import { TabNav } from './Tab'; | ||
| import { WineList } from './WineList'; | ||
|
|
||
| export default function MyProfile() { | ||
| // ํญ ์ํ: 'reviews' | 'wines' | ||
| const [tab, setTab] = useState<'reviews' | 'wines'>('reviews'); | ||
|
|
||
| return ( | ||
| <div className='min-h-screen'> | ||
| <main className='max-w-6xl mx-auto p-4 gap-6 flex flex-col xl:flex-row'> | ||
| {/* ํ๋กํ ์น์ */} | ||
| <Profile nickname='ํ๊ธธ๋' profileImageUrl='https://picsum.photos/64' /> | ||
|
|
||
| {/* ํญ & ๋ฆฌ์คํธ ์น์ */} | ||
| <div className='flex flex-col flex-1'> | ||
| <TabNav | ||
| current={tab} | ||
| onChange={setTab} | ||
| reviewsCount={mockMyReviewsPage1.totalCount} | ||
| winesCount={mockMyWinesPage1.totalCount} | ||
| /> | ||
|
|
||
| {/* ํญ์ ๋ฐ๋ผ ReviewList ๋๋ WineList์ props ์ ๋ฌ */} | ||
| {tab === 'reviews' ? <ReviewList /> : <WineList />} | ||
| </div> | ||
| </main> | ||
| </div> | ||
| ); | ||
| } |

Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nextjs๋ฅผ ํ์ฉ์ค์ด์๋ api์ ๋ง๋ค์ด๋์๊ณ ์ค์ ์์ฒญ์ ์์๊ณ , (ํ์ํ๋ค๋ฉด)api์์ ๋ชฉ๋ฐ์ดํฐ๋ฅผ ๋ฐํํ๋๊ฑด ์ด๋จ๊น์?