Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
34 changes: 34 additions & 0 deletions src/apis/devices/searchDevices.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { axiosInstance } from '@/apis/axios/axios';
import type {
SearchDevicesParams,
GetDevicesSearchResponse,
DeviceSearchResult,
} from '@/types/devices';
import { useInfiniteQuery } from '@tanstack/react-query';
import { queryKey } from '@/constants/queryKey';

export const searchDevices = async (
params: SearchDevicesParams
): Promise<DeviceSearchResult> => {
const { data } = await axiosInstance.get<GetDevicesSearchResponse>(
'/api/devices/search',
{ params }
);
return data.result ?? { devices: [], nextCursor: null, hasNext: false };
};

export const useSearchDevices = (params: Omit<SearchDevicesParams, 'cursor'>) => {
return useInfiniteQuery<DeviceSearchResult, Error>({
queryKey: [queryKey.DEVICE_SEARCH, params],
queryFn: ({ pageParam }) =>
searchDevices({
...params,
cursor: pageParam as string | undefined,
}),
initialPageParam: undefined as string | undefined,
getNextPageParam: (lastPage) =>
lastPage?.hasNext ? lastPage.nextCursor : undefined,
enabled: true,
staleTime: 1000 * 60 * 5,
});
};
16 changes: 13 additions & 3 deletions src/components/ProductCard/ProductCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,29 @@ const ProductCard: React.FC<ProductCardProps> = ({ product, onClick }) => {
onClick={onClick}
>
{/* Image - 정사각형 */}
<div className="w-full aspect-square bg-gray-200 mb-20" />
<div className="w-full aspect-square bg-gray-200 mb-20 overflow-hidden relative">
{product.image ? (
<img
src={product.image}
alt={product.name}
className="absolute inset-0 w-full h-full object-cover"
/>
) : null}
</div>

{/* Content */}
<div className="flex flex-col gap-16">
{/* Name & Category */}
<div className="flex flex-col gap-4">
<p className="font-heading-4 text-black group-hover:text-blue-600 transition-colors">{product.name}</p>
<p className="font-heading-4 text-black group-hover:text-blue-600 transition-colors">
{product.name.length > 19 ? `${product.name.slice(0, 19)}...` : product.name}
</p>
<p className="font-body-2-sm text-gray-300">{product.category}</p>
</div>

{/* Price */}
<p className="font-body-1-sm text-gray-500">
{product.price.toLocaleString()}
{(product.price ?? 0).toLocaleString()}
</p>

{/* Color Chips */}
Expand Down
1 change: 1 addition & 0 deletions src/constants/queryKey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ export const queryKey = {
LIFESTYLE_DEVICE: 'lifestyle_device',
BRANDS: 'brands',
RECENTLY_VIEWED: 'recently_viewed',
DEVICE_SEARCH: 'device_search',
} as const;
38 changes: 38 additions & 0 deletions src/hooks/useIntersectionObserver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { useEffect, useRef, useState } from 'react';

interface UseIntersectionObserverOptions {
root?: Element | null;
rootMargin?: string;
threshold?: number | number[];
}

export const useIntersectionObserver = (
options: UseIntersectionObserverOptions = {}
) => {
const [isIntersecting, setIsIntersecting] = useState(false);
const targetRef = useRef<HTMLDivElement>(null);

useEffect(() => {
const target = targetRef.current;
if (!target) return;

const observer = new IntersectionObserver(
([entry]) => {
setIsIntersecting(entry.isIntersecting);
},
{
root: options.root ?? null,
rootMargin: options.rootMargin ?? '0px',
threshold: options.threshold ?? 0,
}
);

observer.observe(target);

return () => {
observer.disconnect();
};
}, [options.root, options.rootMargin, options.threshold]);

return { targetRef, isIntersecting };
};
Loading