onsitesurvey/src/components/suitable/SuitableList.tsx

159 lines
5.6 KiB
TypeScript

'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,
suitableCheckIcon,
} = useSuitable()
const { selectedItems, addSelectedItem, removeSelectedItem, setSelectedItemsSearching } = useSuitableStore()
const [openItems, setOpenItems] = useState<Set<number>>(new Set())
const observerTarget = useRef<HTMLDivElement>(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<number>): void => {
setSelectedItemsSearching(false)
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 (
<div className={`compliance-check-bx ${openItems.has(item.id) ? 'act' : ''}`} key={item.id}>
<div className="check-name-wrap">
<div className={`check-form-box ${isMainIndeterminate(item.id, item.detailCnt) ? 'space' : ''}`}>
<input
type="checkbox"
id={`ch${item.id}`}
checked={isItemSelected(item.id)}
onChange={() => handleItemClick(item.id, undefined, toSuitableDetailIds(item.detail))}
/>
<label htmlFor={`ch${item.id}`}>{item.productName}</label>
</div>
<div className="check-name-btn">
<button className="bx-btn" onClick={() => toggleItemOpen(item.id)}></button>
</div>
</div>
<ul className="reference-list check">
{toSuitableDetail(item.detail).map((subItem: SuitableDetail) => (
<li className="reference-item" key={subItem.id}>
<div className="check-item-wrap">
<div className="check-form-box light">
<input
type="checkbox"
id={`ch${subItem.id}`}
checked={isItemSelected(item.id, subItem.id)}
onChange={() => handleItemClick(item.id, subItem.id)}
/>
<label htmlFor={`ch${subItem.id}`}>{toCodeName(SUITABLE_HEAD_CODE.TRESTLE_MFPC_CD, subItem.trestleMfpcCd)}</label>
</div>
<div className="compliance-icon-wrap">
<div className="compliance-icon">
<Image src={suitableCheckIcon(subItem.trestleManufacturerProductName)} width={22} height={22} alt="" />
</div>
{subItem.memo && (
<div className="compliance-icon">
<Image src={'/assets/images/sub/compliance_tip_icon.svg'} width={22} height={22} alt=""></Image>
</div>
)}
</div>
</div>
</li>
))}
</ul>
</div>
)
},
[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])
/* 조회 데이터 없는 경우 */
if (!suitableList.length) return <SuitableNoData />
return (
<>
{suitableList.map(renderItem)}
<div ref={observerTarget} />
<SuitableButton />
</>
)
}