'use client' import Image from 'next/image' import { useState, useEffect, useRef, useCallback, useMemo } from 'react' import SuitableButton from './SuitableButton' import SuitableNoData from './SuitableNoData' import { useSuitable } from '@/hooks/useSuitable' import { useSuitableStore } from '@/store/useSuitableStore' import { SUITABLE_HEAD_CODE, type SuitableMain, type SuitableDetail } from '@/types/Suitable' // 한 번에 로드할 아이템 수 const ITEMS_PER_PAGE = 100 export default function SuitableList() { const { toCodeName, suitableSearchResults, isSearchLoading, toSuitableDetail } = useSuitable() const { selectedItems, addSelectedItem, removeSelectedItem } = useSuitableStore() const [openItems, setOpenItems] = useState>(new Set()) const [visibleItems, setVisibleItems] = useState([]) const [page, setPage] = useState(1) const [isLoadingMore, setIsLoadingMore] = useState(false) const observerTarget = useRef(null) // 선택된 아이템 확인 함수 메모이제이션 const isItemSelected = useCallback( (itemId: number) => { return selectedItems.some((selected) => selected === itemId) }, [selectedItems], ) // 초기 데이터 로드 useEffect(() => { if (suitableSearchResults) { const initialItems = suitableSearchResults.suitable.slice(0, ITEMS_PER_PAGE) setVisibleItems(initialItems) setPage(1) } }, [suitableSearchResults]) // Intersection Observer 설정 useEffect(() => { const observer = new IntersectionObserver( (entries) => { if (entries[0].isIntersecting && suitableSearchResults && !isLoadingMore) { const nextPage = page + 1 const startIndex = (nextPage - 1) * ITEMS_PER_PAGE const endIndex = startIndex + ITEMS_PER_PAGE const nextItems = suitableSearchResults.suitable.slice(startIndex, endIndex) if (nextItems.length > 0) { setIsLoadingMore(true) setVisibleItems((prev) => [...prev, ...nextItems]) setPage(nextPage) setIsLoadingMore(false) } } }, { threshold: 0.2, }, ) if (observerTarget.current) { observer.observe(observerTarget.current) } return () => observer.disconnect() }, [page, suitableSearchResults, isLoadingMore]) const handleItemClick = useCallback( (itemId: number) => { isItemSelected(itemId) ? removeSelectedItem(itemId) : addSelectedItem(itemId) }, [isItemSelected, addSelectedItem, removeSelectedItem], ) const toggleItemOpen = useCallback((itemId: number) => { setOpenItems((prev) => { const newOpenItems = new Set(prev) newOpenItems.has(itemId) ? newOpenItems.delete(itemId) : newOpenItems.add(itemId) return newOpenItems }) }, []) // TODO: 추후 지붕재 적합성 데이터 CUD 구현 시 ×, ー 데이터 관리 필요 const suitableCheck = useCallback((value: string) => { if (value === '×') { return (
) } else if (value === 'ー') { return (
) } else { return (
) } }, []) // 메모이제이션된 아이템 렌더링 const renderItem = useCallback( (item: SuitableMain) => { const isSelected = isItemSelected(item.id) const isOpen = openItems.has(item.id) return (
handleItemClick(item.id)} />
    {toSuitableDetail(item.id).map((subItem: SuitableDetail) => (
  • {suitableCheck(subItem.trestleManufacturerProductName)} {subItem.memo && (
    )}
  • ))}
) }, [isItemSelected, openItems, handleItemClick, toggleItemOpen, suitableCheck, toCodeName, toSuitableDetail], ) // 메모이제이션된 아이템 리스트 const renderedItems = useMemo(() => { return visibleItems.map(renderItem) }, [visibleItems, renderItem]) if (isSearchLoading) { return
Loading...
} if (!suitableSearchResults?.suitable.length) { return } return ( <> {renderedItems}
{isLoadingMore &&
데이터를 불러오는 중...
}
) }