175 lines
6.0 KiB
TypeScript
175 lines
6.0 KiB
TypeScript
'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<Set<number>>(new Set())
|
||
const [visibleItems, setVisibleItems] = useState<SuitableMain[]>([])
|
||
const [page, setPage] = useState(1)
|
||
const [isLoadingMore, setIsLoadingMore] = useState(false)
|
||
const observerTarget = useRef<HTMLDivElement>(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 (
|
||
<div className="compliance-icon">
|
||
<Image src={'/assets/images/sub/compliance_x_icon.svg'} width={22} height={22} alt=""></Image>
|
||
</div>
|
||
)
|
||
} else if (value === 'ー') {
|
||
return (
|
||
<div className="compliance-icon">
|
||
<Image src={'/assets/images/sub/compliance_quest_icon.svg'} width={22} height={22} alt=""></Image>
|
||
</div>
|
||
)
|
||
} else {
|
||
return (
|
||
<div className="compliance-icon">
|
||
<Image src={'/assets/images/sub/compliance_check_icon.svg'} width={22} height={22} alt=""></Image>
|
||
</div>
|
||
)
|
||
}
|
||
}, [])
|
||
|
||
// 메모이제이션된 아이템 렌더링
|
||
const renderItem = useCallback(
|
||
(item: SuitableMain) => {
|
||
const isSelected = isItemSelected(item.id)
|
||
const isOpen = openItems.has(item.id)
|
||
|
||
return (
|
||
<div className={`compliance-check-bx ${isOpen ? 'act' : ''}`} key={item.id}>
|
||
<div className="check-name-wrap">
|
||
<div className="check-form-box ">
|
||
<input type="checkbox" id={`ch${item.id}`} checked={isSelected} onChange={() => handleItemClick(item.id)} />
|
||
<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.id).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}`} />
|
||
<label htmlFor={`ch${subItem.id}`}>{toCodeName(SUITABLE_HEAD_CODE.TRESTLE_MFPC_CD, subItem.trestleMfpcCd)}</label>
|
||
</div>
|
||
<div className="compliance-icon-wrap">
|
||
{suitableCheck(subItem.trestleManufacturerProductName)}
|
||
{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, suitableCheck, toCodeName, toSuitableDetail],
|
||
)
|
||
|
||
// 메모이제이션된 아이템 리스트
|
||
const renderedItems = useMemo(() => {
|
||
return visibleItems.map(renderItem)
|
||
}, [visibleItems, renderItem])
|
||
|
||
if (isSearchLoading) {
|
||
return <div>Loading...</div>
|
||
}
|
||
|
||
if (!suitableSearchResults?.suitable.length) {
|
||
return <SuitableNoData />
|
||
}
|
||
|
||
return (
|
||
<>
|
||
{renderedItems}
|
||
<div ref={observerTarget} className="loading-indicator">
|
||
{isLoadingMore && <div className="loading-more">데이터를 불러오는 중...</div>}
|
||
</div>
|
||
<SuitableButton />
|
||
</>
|
||
)
|
||
}
|