'use client' import Image from 'next/image' import { useState, useEffect, useCallback, useRef, useMemo } from 'react' import SuitableButton from './SuitableButton' import SuitableNoData from './SuitableNoData' import { useSuitable } from '@/hooks/useSuitable' import { useSuitableStore } from '@/store/useSuitableStore' import { useSpinnerStore } from '@/store/spinnerStore' import { SUITABLE_HEAD_CODE, type Suitable, type SuitableDetail } from '@/types/Suitable' export default function SuitableList() { const { toCodeName, toSuitableDetail, toSuitableDetailIds, suitables, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading, isError, suitableCheckIcon, suitableErrorAlert, } = useSuitable() const { selectedItems, addSelectedItem, removeSelectedItem } = useSuitableStore() const [openItems, setOpenItems] = useState>(new Set()) const observerTarget = useRef(null) /* 선택된 아이템 확인 - 메인 하위 아이템 indeterminate 확인 */ const isMainIndeterminate = useMemo( () => (mainId: number, detailCnt: number) => { const mainItem = selectedItems.get(mainId) if (!mainItem) return false return mainItem.size > 0 && mainItem.size < detailCnt }, [selectedItems], ) /* 선택된 아이템 확인 */ const isItemSelected = useCallback( (mainId: number, detailId?: number): boolean => { const mainItem = selectedItems.get(mainId) if (!mainItem) return false if (!detailId) return true return mainItem.has(detailId) }, [selectedItems], ) /* 아이템 클릭 */ const handleItemClick = useCallback( (mainId: number, detailId?: number, detailIds?: Set): void => { isItemSelected(mainId, detailId) ? removeSelectedItem(mainId, detailId) : addSelectedItem(mainId, detailId, detailIds) }, [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 }) }, []) /* 아이템 렌더링 */ const renderItem = useCallback( (item: Suitable) => { return (
handleItemClick(item.id, undefined, toSuitableDetailIds(item.detail))} />
    {toSuitableDetail(item.detail)?.map((subItem: SuitableDetail) => (
  • handleItemClick(item.id, subItem.id)} />
    {subItem.memo && (
    )}
  • ))}
) }, [isItemSelected, openItems, handleItemClick, toggleItemOpen, toCodeName, toSuitableDetail], ) /* 조회 데이터 리스트 */ const suitableList = useMemo(() => suitables?.pages.flat() ?? [], [suitables?.pages]) /* Intersection Observer 설정 */ useEffect(() => { const observer = new IntersectionObserver( (entries) => { if (entries[0].isIntersecting && hasNextPage && !isFetchingNextPage) { fetchNextPage() } }, { threshold: 0, rootMargin: `${window.innerHeight * 0.2}px`, }, ) if (observerTarget.current) { observer.observe(observerTarget.current) } return () => observer.disconnect() }, [hasNextPage, isFetchingNextPage, fetchNextPage]) /* 데이터 로딩 상태 처리 */ useEffect(() => { useSpinnerStore.getState().setIsShow(isLoading || isFetchingNextPage) }, [isLoading, isFetchingNextPage]) /* 조회 데이터 오류 처리 */ useEffect(() => { if (isError) suitableErrorAlert() }, [isError]) /* 조회 데이터 없는 경우 */ if (!suitableList.length) return return ( <> {suitableList.map(renderItem)}
) }