159 lines
5.6 KiB
TypeScript
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 />
|
|
</>
|
|
)
|
|
}
|