- {myWineData.map((value) => (
+
+ {myWineData.map((value, index) => (
))}
+ {isMoreLoading && }
);
}
diff --git a/src/app/(with-header)/myprofile/_components/skeleton/MyProfileSkeleton.tsx b/src/app/(with-header)/myprofile/_components/skeleton/MyProfileSkeleton.tsx
new file mode 100644
index 0000000..5e2f6ce
--- /dev/null
+++ b/src/app/(with-header)/myprofile/_components/skeleton/MyProfileSkeleton.tsx
@@ -0,0 +1,31 @@
+export default function MyProfileSkeleton() {
+ return (
+
+ );
+}
diff --git a/src/app/(with-header)/myprofile/_components/skeleton/MyReviewItemSkeleton.tsx b/src/app/(with-header)/myprofile/_components/skeleton/MyReviewItemSkeleton.tsx
new file mode 100644
index 0000000..c5d5d41
--- /dev/null
+++ b/src/app/(with-header)/myprofile/_components/skeleton/MyReviewItemSkeleton.tsx
@@ -0,0 +1,21 @@
+export default function MyReviewItemSkeleton() {
+ return (
+
+ );
+}
diff --git a/src/app/(with-header)/myprofile/_components/skeleton/MyWineItemSkeleton.tsx b/src/app/(with-header)/myprofile/_components/skeleton/MyWineItemSkeleton.tsx
new file mode 100644
index 0000000..663d554
--- /dev/null
+++ b/src/app/(with-header)/myprofile/_components/skeleton/MyWineItemSkeleton.tsx
@@ -0,0 +1,24 @@
+export default function MyWineItemSkeleton() {
+ return (
+
+ );
+}
diff --git a/src/app/(with-header)/myprofile/page.tsx b/src/app/(with-header)/myprofile/page.tsx
index 841c684..f60b655 100644
--- a/src/app/(with-header)/myprofile/page.tsx
+++ b/src/app/(with-header)/myprofile/page.tsx
@@ -6,6 +6,7 @@ import MyProfileContainer from './_components/MyProfileContainer';
import MyReviewListContainer from './_components/MyReviewListContainer';
import MyWineListContainer from './_components/MyWineListContainer';
import { getAccessToken } from '@/lib/auth';
+import { toast } from 'react-toastify';
export default function Page() {
const [category, setCategory] = useState('내가 쓴 후기');
@@ -24,8 +25,8 @@ export default function Page() {
useEffect(() => {
const token = getAccessToken();
if (!token) {
+ toast.error('로그인 후 이용 가능합니다.');
router.push('/signin');
- alert('로그인 후 이용 가능합니다.');
return;
}
}, [router]);
diff --git a/src/components/WineCard.tsx b/src/components/WineCard.tsx
index d3da9e0..84ab4d5 100644
--- a/src/components/WineCard.tsx
+++ b/src/components/WineCard.tsx
@@ -3,6 +3,7 @@
import Image from 'next/image';
import { useRouter } from 'next/navigation';
import MyWIneKebabDropDown, { WineDataProps } from '@/app/(with-header)/myprofile/_components/MyWIneKebabDropDown ';
+import { forwardRef } from 'react';
export interface WineCardProps {
id: number;
@@ -16,50 +17,64 @@ export interface WineCardProps {
type?: 'RED' | 'WHITE' | 'SPARKLING';
editMyWine?: (id: number, editWineData: WineDataProps) => void;
deleteMyWine?: (id: number) => void;
+ setDataCount?: React.Dispatch
>;
}
-export default function WineCard({ id, name, region, image, price, isKebab = false, size = 'large', onClick = false, type = 'RED', editMyWine, deleteMyWine }: WineCardProps) {
- const router = useRouter();
+const WineCard = forwardRef(
+ ({ id, name, region, image, price, isKebab = false, size = 'large', onClick = false, type = 'RED', editMyWine, deleteMyWine, setDataCount }, ref) => {
+ const router = useRouter();
- const cardWrapperStyle =
- size === 'large'
- ? 'max-w-[1140px] h-[260px] gap-[86px] pt-[52px] pb-[40px] pl-[100px] pr-[40px] tablet:pl-[60px] tablet:gap-[60px] mobile:pl-[40px] mobile:pt-[33px] mobile:pb-[29.5px] mobile:h-[190px]'
- : 'pc:w-[800px] h-[228px] gap-[40px] py-[30px] pl-[60px] pr-[40px] tablet:w-full mobile:w-full mobile:pb-[16.5px] mobile:pt-[20px] mobile:h-[164px] mobile:pl-[40px]';
+ const cardWrapperStyle =
+ size === 'large'
+ ? 'max-w-[1140px] h-[260px] gap-[86px] pt-[52px] pb-[40px] pl-[100px] pr-[40px] tablet:pl-[60px] tablet:gap-[60px] mobile:pl-[40px] mobile:pt-[33px] mobile:pb-[29.5px] mobile:h-[190px]'
+ : 'flex-shrink-0 pc:w-[800px] h-[228px] gap-[40px] py-[30px] pl-[60px] pr-[40px] tablet:w-full mobile:w-full mobile:pb-[16.5px] mobile:pt-[20px] mobile:h-[164px] mobile:pl-[40px]';
- const wineImgSize = size === 'large' ? 'h-[208px] mobile:h-[155px] mobile:w-[50px]' : 'h-[198px] mobile:h-[144px] mobile:w-[50px]';
+ const wineImgSize = size === 'large' ? 'h-[208px] mobile:h-[155px] mobile:w-[50px]' : 'h-[198px] mobile:h-[144px] mobile:w-[50px]';
- const wineClamp = size === 'large' ? 'max-w-[850px] mobile:max-w-[550px]' : 'max-w-[600px] tablet:max-w-[800px] mobile:max-w-[550px] pr-[24px]';
+ const wineClamp = size === 'large' ? 'max-w-[850px] mobile:max-w-[550px]' : 'max-w-[600px] tablet:max-w-[800px] mobile:max-w-[550px] pr-[24px]';
- const toWineDetailPage = (event: React.MouseEvent) => {
- if ((event.target as HTMLElement).closest('.ignore-click')) {
- event.stopPropagation();
- return;
- }
- router.push(`/wines/${id}`);
- };
+ const toWineDetailPage = (event: React.MouseEvent) => {
+ if ((event.target as HTMLElement).closest('.ignore-click')) {
+ event.stopPropagation();
+ return;
+ }
+ router.push(`/wines/${id}`);
+ };
- const onClickWineCard = onClick ? (event: React.MouseEvent) => toWineDetailPage(event) : undefined;
+ const onClickWineCard = onClick ? (event: React.MouseEvent) => toWineDetailPage(event) : undefined;
- return (
-
-
-
-
-
-
-
{name}
-
{region}
+ return (
+
+
+
+
+
+
+ {isKebab && editMyWine && deleteMyWine && (
+
+ )}
+
+ ₩ {Number(price).toLocaleString()}
+
- {isKebab && editMyWine && deleteMyWine && (
-
- )}
-
- ₩ {Number(price).toLocaleString()}
-
-
- );
-}
+ );
+ },
+);
+
+WineCard.displayName = 'WineCard';
+
+export default WineCard;
diff --git a/src/components/modal/PatchReviewForm.tsx b/src/components/modal/PatchReviewForm.tsx
index de0e270..64cbca6 100644
--- a/src/components/modal/PatchReviewForm.tsx
+++ b/src/components/modal/PatchReviewForm.tsx
@@ -3,6 +3,7 @@
import { useState, useEffect } from 'react';
import Image from 'next/image';
import { useForm, SubmitHandler } from 'react-hook-form';
+import { toast } from 'react-toastify';
import { fetchWithAuth } from '@/lib/auth';
import Button from '@/components/Button';
import close from '@/assets/icons/close.svg';
@@ -10,7 +11,6 @@ import wineIcon from '@/assets/icons/wine.svg';
import InteractiveRating from '../InteractiveRating';
import ControlBar from '../ControlBar';
import { MyReview } from '@/types/review-data';
-import { toast } from 'react-toastify';
export interface EditReviewData {
rating: number;
@@ -126,9 +126,10 @@ export default function PatchReviewForm({ name, id, onClose, reviewInitialData,
onClose();
}
} catch (error) {
- onClose();
- toast.error('리뷰 수정에 실패했습니다.');
- console.error('리뷰 수정 에러:', error);
+ if (error) {
+ onClose();
+ toast.error('리뷰 수정에 실패했습니다.');
+ }
}
};
diff --git a/src/components/modal/PatchWineForm.tsx b/src/components/modal/PatchWineForm.tsx
index d2507c4..854e05a 100644
--- a/src/components/modal/PatchWineForm.tsx
+++ b/src/components/modal/PatchWineForm.tsx
@@ -4,6 +4,7 @@ import { useState } from 'react';
import { useRouter } from 'next/navigation';
import Image from 'next/image';
import { useForm, SubmitHandler } from 'react-hook-form';
+import { toast } from 'react-toastify';
import { fetchWithAuth } from '@/lib/auth';
import Dropdown from '../Dropdown';
import Button from '../Button';
@@ -49,14 +50,19 @@ export default function PatchWineForm({ onClose, id, wineInitialData, editMyWine
body: JSON.stringify({ name, region, image, price: Number(price), type }),
});
- if (!response?.ok || response === null) return alert('와인 수정에 실패했습니다');
+ if (!response?.ok || response === null) {
+ throw new Error('와인 수정에 실패했습니다.');
+ }
const body = await response.json();
editMyWine(body.id, { ...data, type: data.type as 'RED' | 'WHITE' | 'SPARKLING' });
+ toast.success('와인 수정에 성공했습니다.');
router.push(`/wines/${body.id}`);
} catch (error) {
- console.error('와인 수정 에러:', error);
- console.log(data);
+ if (error) {
+ onClose();
+ toast.error('와인 수정에 실패했습니다.');
+ }
}
};
diff --git a/src/lib/fetchImage.ts b/src/lib/fetchImage.ts
new file mode 100644
index 0000000..3e554a4
--- /dev/null
+++ b/src/lib/fetchImage.ts
@@ -0,0 +1,21 @@
+import { fetchWithAuth } from './auth';
+
+export async function fetchImage(formData: FormData): Promise
{
+ try {
+ const response = await fetchWithAuth(`${process.env.NEXT_PUBLIC_BASE_URL}/images/upload`, {
+ method: 'POST',
+ body: formData,
+ });
+
+ if (!response?.ok || response === null) {
+ throw new Error('이미지를 업로드하는 데 실패했습니다.');
+ }
+ const data = await response.json();
+ return data.url as string;
+ } catch (error: unknown) {
+ if (error instanceof Error) {
+ throw new Error(`이미지 데이터 로드 실패: ${error.message}`);
+ }
+ throw new Error('알 수 없는 오류가 발생했습니다.');
+ }
+}
diff --git a/src/lib/fetchMyReivew.ts b/src/lib/fetchMyReivew.ts
new file mode 100644
index 0000000..f113f96
--- /dev/null
+++ b/src/lib/fetchMyReivew.ts
@@ -0,0 +1,44 @@
+import { MyReviewResponse } from '@/types/review-data';
+import { fetchWithAuth } from './auth';
+
+export async function fetchMyReview(limit: number, cursor?: number): Promise {
+ const params = new URLSearchParams();
+ params.append('limit', limit.toString());
+ if (cursor) params.append('cursor', cursor.toString());
+
+ try {
+ const response = await fetchWithAuth(`${process.env.NEXT_PUBLIC_BASE_URL}/users/me/reviews?${params.toString()}`);
+
+ if (!response?.ok || response === null) {
+ throw new Error('리뷰 데이터를 불러오는 데 실패했습니다.');
+ }
+
+ const data = await response.json();
+ return data;
+ } catch (error: unknown) {
+ if (error instanceof Error) {
+ throw new Error(`리뷰 데이터 로드 실패: ${error.message}`);
+ }
+ throw new Error('알 수 없는 오류가 발생했습니다.');
+ }
+}
+
+export async function fetchDeleteReview(id: number): Promise {
+ try {
+ const response = await fetchWithAuth(`${process.env.NEXT_PUBLIC_BASE_URL}/reviews/${id}`, {
+ method: 'DELETE',
+ });
+
+ if (!response?.ok || response === null) {
+ throw new Error('리뷰 삭제에 실패했습니다');
+ }
+
+ const body = await response.json();
+ return body.id;
+ } catch (error: unknown) {
+ if (error instanceof Error) {
+ throw new Error(`리뷰 데이터 로드 실패: ${error.message}`);
+ }
+ throw new Error('알 수 없는 오류가 발생했습니다.');
+ }
+}
diff --git a/src/lib/fetchMyWine.ts b/src/lib/fetchMyWine.ts
new file mode 100644
index 0000000..8cbc793
--- /dev/null
+++ b/src/lib/fetchMyWine.ts
@@ -0,0 +1,23 @@
+import { WineListResponse } from '@/types/wine';
+import { fetchWithAuth } from './auth';
+
+export async function fetchMyWine(limit: number, cursor?: number): Promise {
+ const params = new URLSearchParams();
+ params.append('limit', limit.toString());
+ if (cursor) params.append('cursor', cursor.toString());
+ try {
+ const response = await fetchWithAuth(`${process.env.NEXT_PUBLIC_BASE_URL}/users/me/wines?${params.toString()}`);
+
+ if (!response?.ok || response === null) {
+ throw new Error('와인 데이터를 불러오는 데 실패했습니다.');
+ }
+
+ const data = await response.json();
+ return data;
+ } catch (error: unknown) {
+ if (error instanceof Error) {
+ throw new Error(`와인 데이터 로드 실패: ${error.message}`);
+ }
+ throw new Error('알 수 없는 오류가 발생했습니다.');
+ }
+}
diff --git a/src/lib/fetchUser.ts b/src/lib/fetchUser.ts
new file mode 100644
index 0000000..de2eeaf
--- /dev/null
+++ b/src/lib/fetchUser.ts
@@ -0,0 +1,50 @@
+import { toast } from 'react-toastify';
+import { MyProfileData } from '@/app/(with-header)/myprofile/_components/MyProfile';
+import { fetchWithAuth } from './auth';
+
+export async function fetchUser(): Promise {
+ try {
+ const response = await fetchWithAuth(`${process.env.NEXT_PUBLIC_BASE_URL}/users/me`);
+
+ if (!response?.ok || response === null) {
+ throw new Error('유저 데이터를 불러오는 데 실패했습니다.');
+ }
+
+ const data = await response.json();
+ return data;
+ } catch (error: unknown) {
+ if (error instanceof Error) {
+ throw new Error(`유저 데이터 로드 실패: ${error.message}`);
+ }
+ throw new Error('알 수 없는 오류가 발생했습니다.');
+ }
+}
+
+export async function fetchUploadUser(image: string, nickname: string): Promise {
+ try {
+ const response = await fetchWithAuth(`${process.env.NEXT_PUBLIC_BASE_URL}/users/me`, {
+ method: 'PATCH',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({ image: image, nickname: nickname }),
+ });
+
+ if (response?.status === 400) {
+ toast.error('동일한 닉네임이 있습니다.');
+ return undefined;
+ }
+
+ if (!response?.ok || response === null) {
+ throw new Error('데이터를 업데이트 하는 데 실패했습니다.');
+ }
+
+ const result = await response.json();
+ return result;
+ } catch (error: unknown) {
+ if (error instanceof Error) {
+ throw new Error(`유저 데이터 로드 실패: ${error.message}`);
+ }
+ throw new Error('알 수 없는 오류가 발생했습니다.');
+ }
+}
diff --git a/src/lib/fetchWines.ts b/src/lib/fetchWines.ts
index 7ca069c..728a27e 100644
--- a/src/lib/fetchWines.ts
+++ b/src/lib/fetchWines.ts
@@ -1,4 +1,5 @@
import { WineListResponse } from '@/types/wine';
+import { fetchWithAuth } from './auth';
interface FetchWinesParams {
limit: number;
@@ -28,3 +29,23 @@ export async function fetchWines({ limit, cursor, type, minPrice, maxPrice, rati
return res.json();
}
+
+export async function fetchDeleteWine(id: number): Promise {
+ try {
+ const response = await fetchWithAuth(`${process.env.NEXT_PUBLIC_BASE_URL}/wines/${id}`, {
+ method: 'DELETE',
+ });
+
+ if (!response?.ok || response === null) {
+ throw new Error('와인 삭제에 실패했습니다');
+ }
+
+ const body = await response.json();
+ return body.id;
+ } catch (error: unknown) {
+ if (error instanceof Error) {
+ throw new Error(`이미지 데이터 로드 실패: ${error.message}`);
+ }
+ throw new Error('알 수 없는 오류가 발생했습니다.');
+ }
+}