Skip to content

Commit

Permalink
feat: 상세페이지 로그인 여부에 따라 알림 on/off 기능 구현
Browse files Browse the repository at this point in the history
  • Loading branch information
gyeongza committed Jul 1, 2024
1 parent 32cf50c commit 10e47a6
Show file tree
Hide file tree
Showing 6 changed files with 145 additions and 140 deletions.
10 changes: 9 additions & 1 deletion src/app/(iTracker)/products/[category]/[productId]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
import { CategoryType } from '@/features/category/constants';
import { ProductDetail } from '@/features/productDetail/components/ProductDetail';
import { Loading } from '@/shared/components/Loading';
import { Text } from '@/shared/components/shadcn/Text';
import { Suspense } from 'react';
import { ErrorBoundary } from 'react-error-boundary';

export default function ProductDetailPage({ params }: { params: { productId: number; category: CategoryType } }) {
return (
<main>
<ProductDetail productId={params.productId} category={params.category} />
<ErrorBoundary fallback={<Text className="text-badge">상세 정보를 불러오는 중 오류가 생겼습니다.</Text>}>
<Suspense fallback={<Loading />}>
<ProductDetail productId={params.productId} category={params.category} />
</Suspense>
</ErrorBoundary>
</main>
);
}
21 changes: 11 additions & 10 deletions src/features/productDetail/api/getProductDetail.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { CategoryType } from '@/features/category/constants';
import { Airpods, Macbook } from '@/features/product/api/getProductList';
import instance from '@/shared/api/axios/instance';
import { API_BASE_URL } from '@/shared/api/constants';

export type GetProductDetailResponse = Macbook & Airpods & ProductDetailInfo;

Expand All @@ -14,15 +17,13 @@ export type ProductDetailInfo = {
}[];
};

// 클라이언트 상태 관리를 위한 api 호출함수
// export const getProductDetailUrl = (productId: number) => `/api/products/${productId}`;
export const getProductDetail = async (
productId: number,
category: CategoryType,
): Promise<GetProductDetailResponse> => {
const response = await instance.get(`${API_BASE_URL}/api/v1/products/${category}/${productId}`);

// export const getProductDetail = async (productId: number): Promise<GetProductDetailResponse> => {
// const url = `${getProductDetailUrl(productId)}`;
const data = (await response.data) as GetProductDetailResponse;

// const response = await fetch(url);

// const data = (await response.json()) as GetProductDetailResponse;

// return data;
// };
return data;
};
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const Notification = ({ productId, category, isFavorite }: NotificationProps) =>
return (
<div>
{isFavorite ? (
<Button size="lg" className="w-full bg-badge" onClick={handleAddFavoritesButton}>
<Button size="lg" className="w-full bg-badge hover:bg-badge" onClick={handleAddFavoritesButton}>
🔔 가격 변동 알림 해제
</Button>
) : (
Expand Down
238 changes: 110 additions & 128 deletions src/features/productDetail/components/ProductDetail/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client';

import { Text } from '@/shared/components/shadcn/Text';
import { API_BASE_URL } from '@/shared/api/constants';
import { GetProductDetailResponse } from '../../api/getProductDetail';
import Image from 'next/image';
import { FixedBottomButton } from '@/shared/components/FixedBottomButton';
import { convertToLocalFormat } from '@/shared/utils';
Expand All @@ -11,152 +11,134 @@ import { Suspense } from 'react';
import PriceChart from '../LineChart';
import { CategoryType, categoryMap } from '@/features/category/constants';
import Notification from '../Notification';
import { useGetProductDetail } from '../../hooks/useGetProductDetail';

// server component

export const ProductDetail = async ({ productId, category }: { productId: number; category: CategoryType }) => {
export const ProductDetail = ({ productId, category }: { productId: number; category: CategoryType }) => {
const categoryName = categoryMap[category];
const { data } = useGetProductDetail(productId, category);

try {
const response = await fetch(`${API_BASE_URL}/api/v1/products/${category}/${productId}`, {
cache: 'no-store',
});
if (!response.ok) {
throw new Error(`서버에서 데이터를 가져오는 데 실패했습니다. 상태 코드: ${response.status}`);
}
const isMacbook = category === 'macbook_air' || category === 'macbook_pro';

const data = (await response.json()) as GetProductDetailResponse;
return (
<div>
<div className="mb-8 w-fit">
<div className="flex flex-col items-start md:flex-row md:items-center gap-12 md:gap-16">
<div className="flex items-center md:flex-col md:items-start gap-12 md:gap-4">
<div className="w-auto h-full rounded-md border-gray-200 border">
<Image
src={data.imageUrl}
alt={data.title}
width={120}
height={120}
className="object-contain w-auto h-auto p-4"
/>
</div>

const isMacbook = category === 'macbook_air' || category === 'macbook_pro';
<div className="w-full">
<div>
<Text typography="h4">{categoryName}</Text>
<Text>{data.title}</Text>
</div>

console.log(data.isFavorite);
{isMacbook ? (
<>
<div className="flex items-center justify-center bg-slate-950 rounded-md w-[35px] h-[35px] text-white my-2 mt-6">
<Text typography="xsmall" className="text-center">
{data.chip}
</Text>
</div>
<div className="flex flex-col gap-1">
<Text typography="small">{data.cpu}</Text>
<Text typography="small">{data.gpu}</Text>
<Text typography="small">{data.storage}</Text>
<Text typography="small">{data.memory}</Text>
<Text typography="small">{data.color}</Text>
</div>
</>
) : (
<Text typography="small">{data.color}</Text>
)}
</div>
</div>

return (
<div>
<div className="mb-8 w-fit">
<div className="flex flex-col items-start md:flex-row md:items-center gap-12 md:gap-16">
<div className="flex items-center md:flex-col md:items-start gap-12 md:gap-4">
<div className="w-auto h-full rounded-md border-gray-200 border">
<Image
src={data.imageUrl}
alt={data.title}
width={120}
height={120}
className="object-contain w-auto h-auto p-4"
/>
<div className="w-full">
<div className="flex flex-col gap-4">
<div className="flex items-center gap-2">
<Text typography="small" className="pt-1">
전체 평균가 대비
</Text>
<DiscountBadge discountPercentage={data.discountPercentage} />
</div>

<div className="w-full">
<div>
<Text typography="h4">{categoryName}</Text>
<Text>{data.title}</Text>
</div>

{isMacbook ? (
<>
<div className="flex items-center justify-center bg-slate-950 rounded-md w-[35px] h-[35px] text-white my-2 mt-6">
<Text typography="xsmall" className="text-center">
{data.chip}
</Text>
</div>
<div className="flex flex-col gap-1">
<Text typography="small">{data.cpu}</Text>
<Text typography="small">{data.gpu}</Text>
<Text typography="small">{data.storage}</Text>
<Text typography="small">{data.memory}</Text>
<Text typography="small">{data.color}</Text>
</div>
</>
<div>
{data.label === true ? (
<Badge label={'역대 최저가'} />
) : (
<Text typography="small">{data.color}</Text>
<div className="inline-flex py-1 px-2 mt-4 mb-2"></div>
)}
<Text typography="p" className="text-gray-500">
현재가
</Text>
<Text typography="h4" className="leading-none">
{convertToLocalFormat(Math.floor(data.currentPrice))}
</Text>
</div>
</div>

<div className="w-full">
<div className="flex flex-col gap-4">
<div className="flex items-center gap-2">
<Text typography="small" className="pt-1">
전체 평균가 대비
</Text>
<DiscountBadge discountPercentage={data.discountPercentage} />
</div>

<div>
{data.label === true ? (
<Badge label={'역대 최저가'} />
) : (
<div className="inline-flex py-1 px-2 mt-4 mb-2"></div>
)}
<Text typography="p" className="text-gray-500">
현재가
</Text>
<Text typography="h4" className="leading-none">
{convertToLocalFormat(Math.floor(data.currentPrice))}
</Text>
</div>
<div className="flex h-20 justify-between items-center my-8 border rounded py-2">
<div className="mx-auto">
<Text typography="small" className="text-gray-500">
최저가
</Text>
<Text className="leading-none font-bold text-[#EF6253]">
{convertToLocalFormat(Math.floor(data.allTimeLowPrice))}
</Text>
</div>

<div className="flex h-20 justify-between items-center my-8 border rounded py-2">
<div className="mx-auto">
<Text typography="small" className="text-gray-500">
최저가
</Text>
<Text className="leading-none font-bold text-[#EF6253]">
{convertToLocalFormat(Math.floor(data.allTimeLowPrice))}
</Text>
</div>
<Separator orientation="vertical" />
<div className="mx-auto">
<Text typography="small" className="text-gray-500">
평균가
</Text>
<Text className="leading-none font-bold">
{convertToLocalFormat(Math.floor(data.averagePrice))}
</Text>
</div>
<Separator orientation="vertical" />
<div className="mx-auto">
<Text typography="small" className="text-gray-500">
최고가
</Text>
<Text className="leading-none font-bold text-[#519CF4]">
{convertToLocalFormat(Math.floor(data.allTimeHighPrice))}
</Text>
</div>
<Separator orientation="vertical" />
<div className="mx-auto">
<Text typography="small" className="text-gray-500">
평균가
</Text>
<Text className="leading-none font-bold">{convertToLocalFormat(Math.floor(data.averagePrice))}</Text>
</div>
<Separator orientation="vertical" />
<div className="mx-auto">
<Text typography="small" className="text-gray-500">
최고가
</Text>
<Text className="leading-none font-bold text-[#519CF4]">
{convertToLocalFormat(Math.floor(data.allTimeHighPrice))}
</Text>
</div>

<Suspense>
<div className="max-w-[780px] h-[200px] my-10 mx-auto md:mx-0">
<PriceChart
priceInfos={data.priceInfos}
averagePrice={data.averagePrice}
allTimeHighPrice={data.allTimeHighPrice}
allTimeLowPrice={data.allTimeLowPrice}
/>
</div>
</Suspense>
</div>

<Suspense>
<div className="max-w-[780px] h-[200px] my-10 mx-auto md:mx-0">
<PriceChart
priceInfos={data.priceInfos}
averagePrice={data.averagePrice}
allTimeHighPrice={data.allTimeHighPrice}
allTimeLowPrice={data.allTimeLowPrice}
/>
</div>
</Suspense>
</div>
</div>

<Suspense>
<Notification productId={data.id} category={data.category} isFavorite={data.isFavorite} />
</Suspense>
<Suspense>
<Notification productId={data.id} category={data.category} isFavorite={data.isFavorite} />
</Suspense>

<div className="mt-12 mb-24">
<Text typography="small" className="text-[12px] block text-center md:text-end">
이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.
</Text>
<Text className="text-[10px] block text-center md:text-end">
* 쿠팡 정보와 동일하지 않을 수 있으니 쿠팡에서 가격을 직접 확인 후 이용바랍니다.
</Text>
</div>
<div className="mt-12 mb-24">
<Text typography="small" className="text-[12px] block text-center md:text-end">
이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.
</Text>
<Text className="text-[10px] block text-center md:text-end">
* 쿠팡 정보와 동일하지 않을 수 있으니 쿠팡에서 가격을 직접 확인 후 이용바랍니다.
</Text>
</div>
<FixedBottomButton title="🚀 구매하러가기" link={data.coupangUrl} bgColor="#EF6253" />
</div>
);
} catch (error) {
console.error(error);
return <Text>제품 정보를 불러오는 중 오류가 발생했습니다.</Text>;
}
<FixedBottomButton title="🚀 구매하러가기" link={data.coupangUrl} bgColor="#EF6253" />
</div>
);
};
13 changes: 13 additions & 0 deletions src/features/productDetail/hooks/useGetProductDetail.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { CategoryType } from '@/features/category/constants';
import { UseSuspenseQueryResult, useSuspenseQuery } from '@tanstack/react-query';
import { GetProductDetailResponse, getProductDetail } from '../api/getProductDetail';

export const useGetProductDetail = (
productId: number,
category: CategoryType,
): UseSuspenseQueryResult<GetProductDetailResponse> => {
return useSuspenseQuery({
queryKey: ['productDetail', productId, category],
queryFn: () => getProductDetail(productId, category),
});
};
1 change: 1 addition & 0 deletions src/features/productDetail/hooks/usePatchFavorites.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export const usePatchFavorites = (productId: number, category: CategoryType, isF
mutationFn: () => patchFavorites(productId, category),
onSuccess: async () => {
await queryClient.invalidateQueries({ queryKey: ['favorites'] });
await queryClient.refetchQueries({ queryKey: ['productDetail'] });

if (!isFavorite) {
toast({
Expand Down

0 comments on commit 10e47a6

Please sign in to comment.