onsitesurvey/src/components/suitable/SuitableList.tsx

175 lines
6.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'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 />
</>
)
}