diff --git a/public/wine.png b/public/wine.png
new file mode 100644
index 0000000..fa4966e
Binary files /dev/null and b/public/wine.png differ
diff --git a/src/assets/icons/dot.svg b/src/assets/icons/dot.svg
new file mode 100644
index 0000000..fb1a8c3
--- /dev/null
+++ b/src/assets/icons/dot.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/components/common/card/MyCard.tsx b/src/components/common/card/MyCard.tsx
index fdd23ae..1bfa16b 100644
--- a/src/components/common/card/MyCard.tsx
+++ b/src/components/common/card/MyCard.tsx
@@ -33,10 +33,10 @@ export function MyCard({ rating, timeAgo, title, review, rightSlot, className }:
{/* 제목 */}
-
{title}
+ {title}
{/* 리뷰 내용 */}
- {review}
+ {review}
);
}
diff --git a/src/components/common/dropdown/MenuDropdown.tsx b/src/components/common/dropdown/MenuDropdown.tsx
index fab6a29..aa410cf 100644
--- a/src/components/common/dropdown/MenuDropdown.tsx
+++ b/src/components/common/dropdown/MenuDropdown.tsx
@@ -28,7 +28,7 @@ interface MenuDropdownProps {
export default function MenuDropdown({ options, onSelect, trigger }: MenuDropdownProps) {
return (
-
+
{/* 드롭다운 트리거 버튼 */}
{trigger}
{/* 드롭다운 메뉴 영역 */}
diff --git a/src/components/myprofile/Profile.tsx b/src/components/myprofile/Profile.tsx
new file mode 100644
index 0000000..c0e2a0d
--- /dev/null
+++ b/src/components/myprofile/Profile.tsx
@@ -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({
+ defaultValues: { nickname }, // 초기값으로 기존 닉네임 설정
+ mode: 'onChange', // 입력 시마다 유효성 검사 실행
+ });
+
+ // 현재 입력된 값을 관찰
+ const current = watch('nickname');
+ // 기존 닉네임과 다르고 비어있지 않을 때만 true
+ const isChanged = current.trim().length > 0 && current !== nickname;
+
+ // 폼 제출 시 호출되는 함수
+ const onSubmit: SubmitHandler = 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 (
+
+ {/* 프로필 섹션: 이미지 & 현재 닉네임 */}
+
+
+ {/* 추후 이미지 업로드 기능 추가 필요 */}
+

+
+
{nickname}
+
+
+ {/* 닉네임 변경 폼 */}
+
+
+ );
+}
diff --git a/src/components/myprofile/ReviewList.tsx b/src/components/myprofile/ReviewList.tsx
new file mode 100644
index 0000000..89cf7b7
--- /dev/null
+++ b/src/components/myprofile/ReviewList.tsx
@@ -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 {
+ return mockMyReviewsPage1.list;
+}
+
+/**
+ * ReviewList 컴포넌트
+ * - React Query의 useQuery 훅을 사용해 리뷰 데이터를 패칭
+ * - 로딩 및 에러 상태를 처리한 뒤, MyCard 컴포넌트로 리스트를 렌더링
+ */
+export function ReviewList() {
+ // React Query로 리뷰 데이터 요청
+ const {
+ data: items = [],
+ isLoading,
+ isError,
+ } = useQuery({
+ queryKey: ['myReviews'],
+ queryFn: fetchReviews,
+ });
+
+ // 로딩 중 표시
+ if (isLoading) {
+ return 리뷰 불러오는 중…
;
+ }
+
+ // 에러 시 표시
+ if (isError) {
+ return 리뷰 불러오기 실패
;
+ }
+
+ // 실제 리뷰 리스트 렌더링
+ return (
+
+ {items.map((review) => (
+
+
+ ★ {review.rating.toFixed(1)}
+
+
+ }
+ // 작성일
+ timeAgo={new Date(review.createdAt).toLocaleDateString()}
+ // 작성자 닉네임
+ title={review.user.nickname}
+ // 리뷰 내용
+ review={review.content}
+ // dot 아이콘 클릭 시 드롭다운 오픈
+ rightSlot={
+
+
+
+ }
+ options={[
+ { label: '수정하기', value: 'edit' },
+ { label: '삭제하기', value: 'delete' },
+ ]}
+ onSelect={(value) => console.log(`${value} clicked for review id: ${review.id}`)}
+ />
+ }
+ />
+ ))}
+
+ );
+}
diff --git a/src/components/myprofile/Tab.tsx b/src/components/myprofile/Tab.tsx
new file mode 100644
index 0000000..24643ac
--- /dev/null
+++ b/src/components/myprofile/Tab.tsx
@@ -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 (
+
+ );
+}
diff --git a/src/components/myprofile/WineList.tsx b/src/components/myprofile/WineList.tsx
new file mode 100644
index 0000000..5b8c155
--- /dev/null
+++ b/src/components/myprofile/WineList.tsx
@@ -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 {
+ return mockMyWinesPage1.list;
+}
+
+/**
+ * WineList 컴포넌트
+ * - React Query의 useQuery 훅을 사용해 리뷰 데이터를 패칭
+ * - 로딩 및 에러 상태를 처리한 뒤, ImageCard 컴포넌트로 리스트를 렌더링
+ */
+export function WineList() {
+ // React Query로 와인 목록 패칭
+ const {
+ data: items = [],
+ isLoading,
+ isError,
+ } = useQuery({
+ queryKey: ['myWines'],
+ queryFn: fetchWines,
+ });
+
+ // 로딩 중 표시
+ if (isLoading) {
+ return 와인 불러오는 중…
;
+ }
+
+ // 에러 시 표시
+ if (isError) {
+ return 와인 불러오기 실패
;
+ }
+
+ return (
+
+ {items.map((w) => (
+
+
+
+ }
+ options={[
+ { label: '수정하기', value: 'edit' },
+ { label: '삭제하기', value: 'delete' },
+ ]}
+ onSelect={(value) => console.log(`${value} clicked for wine id: ${w.id}`)}
+ />
+ }
+ >
+ {/* 카드 내부: 와인 정보 */}
+
+
+ {w.name} {/* 와인 이름 */}
+
+
+ {w.region} {/* 생산 지역 */}
+
+
+
+ {/* 가격 표시 */}₩ {w.price.toLocaleString()}
+
+
+
+
+ ))}
+
+ );
+}
diff --git a/src/components/myprofile/mockUser.ts b/src/components/myprofile/mockUser.ts
new file mode 100644
index 0000000..10d36ce
--- /dev/null
+++ b/src/components/myprofile/mockUser.ts
@@ -0,0 +1,217 @@
+// get으로 응답받는 responese
+// 초기에 불러올 데이터
+export const mockMyReviewsPage1 = {
+ totalCount: 4,
+ nextCursor: 2,
+ list: [
+ {
+ id: 101,
+ rating: 4.8,
+ lightBold: 2,
+ smoothTannic: 4,
+ drySweet: 1,
+ softAcidic: 3,
+ aroma: ['CHERRY', 'OAK'],
+ content: '진한 체리향과 부드러운 탄닌이 좋았어요.',
+ createdAt: '2025-07-25T02:50:33.040669Z',
+ updatedAt: '2025-07-25T02:50:33.040669Z',
+ user: {
+ id: 1,
+ nickname: '와인러버',
+ image: 'https://picsum.photos/seed/me1/32',
+ },
+ isLiked: false,
+ },
+ {
+ id: 102,
+ rating: 4.2,
+ lightBold: 3,
+ smoothTannic: 2,
+ drySweet: 2,
+ softAcidic: 2,
+ aroma: ['BLACKCURRANT'],
+ content: '깔끔하고 가볍게 마시기 좋은 와인이에요.',
+ createdAt: '2025-07-25T02:50:33.040669Z',
+ updatedAt: '2025-07-25T02:50:33.040669Z',
+ user: {
+ id: 1,
+ nickname: '와인러버',
+ image: 'https://picsum.photos/seed/me1/32',
+ },
+ isLiked: true,
+ },
+ ],
+};
+
+export const mockMyReviewsPage2 = {
+ totalCount: 4,
+ nextCursor: null,
+ list: [
+ {
+ id: 103,
+ rating: 4.9,
+ lightBold: 4,
+ smoothTannic: 5,
+ drySweet: 1,
+ softAcidic: 4,
+ aroma: ['SPICE', 'PLUM'],
+ content: '풍미 깊고 고급스러운 맛이에요.',
+ createdAt: '2025-07-25T02:50:33.040669Z',
+ updatedAt: '2025-07-25T02:50:33.040669Z',
+ user: {
+ id: 1,
+ nickname: '와인러버',
+ image: 'https://picsum.photos/seed/me1/32',
+ },
+ isLiked: false,
+ },
+ {
+ id: 104,
+ rating: 3.8,
+ lightBold: 2,
+ smoothTannic: 1,
+ drySweet: 3,
+ softAcidic: 2,
+ aroma: ['FLORAL'],
+ content: '꽃향이 강하게 나서 인상 깊었어요.',
+ createdAt: '2025-07-25T02:50:33.040669Z',
+ updatedAt: '2025-07-25T02:50:33.040669Z',
+ user: {
+ id: 1,
+ nickname: '와인러버',
+ image: 'https://picsum.photos/seed/me1/32',
+ },
+ isLiked: true,
+ },
+ ],
+};
+
+export const mockMyWinesPage1 = {
+ totalCount: 4,
+ nextCursor: 2,
+ list: [
+ {
+ id: 201,
+ name: 'Opus One 2019',
+ region: 'Napa Valley, USA',
+ image: '/wine.png',
+ price: 350000,
+ type: 'Red',
+ avgRating: 4.6,
+ reviewCount: 8,
+ userId: 1,
+ recentReview: {
+ id: 101,
+ rating: 4.8,
+ lightBold: 2,
+ smoothTannic: 4,
+ drySweet: 1,
+ softAcidic: 3,
+ aroma: ['CHERRY', 'OAK'],
+ content: '진한 체리향과 부드러운 탄닌이 좋았어요.',
+ createdAt: '2025-07-25T02:50:33.040669Z',
+ updatedAt: '2025-07-25T02:50:33.040669Z',
+ user: {
+ id: 1,
+ nickname: '와인러버',
+ image: 'https://picsum.photos/seed/me1/32',
+ },
+ isLiked: false,
+ },
+ },
+ {
+ id: 202,
+ name: 'Sassicaia 2018',
+ region: 'Tuscany, Italy',
+ image: '/wine.png',
+ price: 290000,
+ type: 'Red',
+ avgRating: 4.7,
+ reviewCount: 10,
+ userId: 1,
+ recentReview: {
+ id: 102,
+ rating: 4.2,
+ lightBold: 3,
+ smoothTannic: 2,
+ drySweet: 2,
+ softAcidic: 2,
+ aroma: ['BLACKCURRANT'],
+ content: '깔끔하고 가볍게 마시기 좋은 와인이에요.',
+ createdAt: '2025-07-25T02:50:33.040669Z',
+ updatedAt: '2025-07-25T02:50:33.040669Z',
+ user: {
+ id: 1,
+ nickname: '와인러버',
+ image: 'https://picsum.photos/seed/me1/32',
+ },
+ isLiked: true,
+ },
+ },
+ ],
+};
+
+export const mockMyWinesPage2 = {
+ totalCount: 4,
+ nextCursor: null,
+ list: [
+ {
+ id: 203,
+ name: 'Château Latour 2010',
+ region: 'Bordeaux, France',
+ image: '/wine.png',
+ price: 410000,
+ type: 'Red',
+ avgRating: 4.9,
+ reviewCount: 12,
+ userId: 1,
+ recentReview: {
+ id: 103,
+ rating: 4.9,
+ lightBold: 4,
+ smoothTannic: 5,
+ drySweet: 1,
+ softAcidic: 4,
+ aroma: ['SPICE', 'PLUM'],
+ content: '풍미 깊고 고급스러운 맛이에요.',
+ createdAt: '2025-07-25T02:50:33.040669Z',
+ updatedAt: '2025-07-25T02:50:33.040669Z',
+ user: {
+ id: 1,
+ nickname: '와인러버',
+ image: 'https://picsum.photos/seed/me1/32',
+ },
+ isLiked: false,
+ },
+ },
+ {
+ id: 204,
+ name: 'Cloudy Bay Sauvignon Blanc 2021',
+ region: 'Marlborough, New Zealand',
+ image: '/wine.png',
+ price: 65000,
+ type: 'White',
+ avgRating: 4.3,
+ reviewCount: 6,
+ userId: 1,
+ recentReview: {
+ id: 104,
+ rating: 3.8,
+ lightBold: 2,
+ smoothTannic: 1,
+ drySweet: 3,
+ softAcidic: 2,
+ aroma: ['FLORAL'],
+ content: '꽃향이 강하게 나서 인상 깊었어요.',
+ createdAt: '2025-07-25T02:50:33.040669Z',
+ updatedAt: '2025-07-25T02:50:33.040669Z',
+ user: {
+ id: 1,
+ nickname: '와인러버',
+ image: 'https://picsum.photos/seed/me1/32',
+ },
+ isLiked: true,
+ },
+ },
+ ],
+};
diff --git a/src/pages/myprofile/index.tsx b/src/pages/myprofile/index.tsx
new file mode 100644
index 0000000..026b2c9
--- /dev/null
+++ b/src/pages/myprofile/index.tsx
@@ -0,0 +1,34 @@
+import React, { useState } from 'react';
+
+import { mockMyReviewsPage1, mockMyWinesPage1 } from '@/components/myprofile/mockUser';
+import Profile from '@/components/myprofile/Profile';
+import { ReviewList } from '@/components/myprofile/ReviewList';
+import { TabNav } from '@/components/myprofile/Tab';
+import { WineList } from '@/components/myprofile/WineList';
+
+export default function MyProfile() {
+ // 탭 상태: 'reviews' | 'wines'
+ const [tab, setTab] = useState<'reviews' | 'wines'>('reviews');
+
+ return (
+
+
+ {/* 프로필 섹션 */}
+
+
+ {/* 탭 & 리스트 섹션 */}
+
+
+
+ {/* 탭에 따라 ReviewList 또는 WineList에 props 전달 */}
+ {tab === 'reviews' ? : }
+
+
+
+ );
+}