@@ -148,24 +111,38 @@ export default function SuitableList() {
[isItemSelected, openItems, handleItemClick, toggleItemOpen, suitableCheck, toCodeName, toSuitableDetail],
)
- // 메모이제이션된 아이템 리스트
- const renderedItems = useMemo(() => {
- return visibleItems.map(renderItem)
- }, [visibleItems, renderItem])
+ // 아이템 리스트
+ const suitableList = suitables?.pages.flat() ?? []
- if (isSearchLoading) {
- return
Loading...
- }
+ // Intersection Observer 설정
+ useEffect(() => {
+ const observer = new IntersectionObserver(
+ (entries) => {
+ if (entries[0].isIntersecting && hasNextPage && !isFetchingNextPage) {
+ fetchNextPage()
+ }
+ },
+ {
+ threshold: 0,
+ rootMargin: '100px',
+ },
+ )
- if (!suitableSearchResults?.length) {
- return
- }
+ if (observerTarget.current) {
+ observer.observe(observerTarget.current)
+ }
+
+ return () => observer.disconnect()
+ }, [hasNextPage, isFetchingNextPage, fetchNextPage])
+
+ if (isLoading) return
Loading...
+ if (!suitableList.length) return
return (
<>
- {renderedItems}
+ {suitableList.map(renderItem)}
- {isLoadingMore &&
데이터를 불러오는 중...
}
+ {isFetchingNextPage &&
데이터를 불러오는 중...
}
>
diff --git a/src/components/suitable/SuitableNoData.tsx b/src/components/suitable/SuitableNoData.tsx
index 1245fbf..f427b15 100644
--- a/src/components/suitable/SuitableNoData.tsx
+++ b/src/components/suitable/SuitableNoData.tsx
@@ -1,11 +1,16 @@
+'use client'
+
+import { useRouter } from 'next/navigation'
+
export default function SuitableNoData() {
+ const router = useRouter()
return (
<>
検索結果はありません。
屋根材適合性表にない製品の情報を入力してください。 今後返信いたします。
-
diff --git a/src/hooks/useSuitable.ts b/src/hooks/useSuitable.ts
index ece8d2e..57ff7b5 100644
--- a/src/hooks/useSuitable.ts
+++ b/src/hooks/useSuitable.ts
@@ -1,24 +1,37 @@
-import { useQuery } from '@tanstack/react-query'
-import { axiosInstance, transformObjectKeys } from '@/libs/axios'
+import { useInfiniteQuery } from '@tanstack/react-query'
+import { transformObjectKeys } from '@/libs/axios'
import { useSuitableStore } from '@/store/useSuitableStore'
+import { useAxios } from './useAxios'
import { useCommCode } from './useCommCode'
import { SUITABLE_HEAD_CODE, type Suitable, type SuitableDetail } from '@/types/Suitable'
export function useSuitable() {
+ const { axiosInstance } = useAxios()
const { getCommCode } = useCommCode()
- const { selectedCategory, searchValue, suitableCommCode, setSuitableCommCode, isSearch } = useSuitableStore()
+ const { itemPerPage, selectedCategory, searchValue, suitableCommCode, setSuitableCommCode, isSearch } = useSuitableStore()
+
- const getSuitables = async (): Promise => {
+ const getSuitables = async ({
+ pageNumber,
+ ids,
+ category,
+ keyword,
+ }: {
+ pageNumber?: number
+ ids?: string
+ category?: string
+ keyword?: string
+ }): Promise => {
try {
- const response = await axiosInstance(null).get('/api/suitable/list', {
- params: {
- pageNumber: 1,
- itemPerPage: 1000,
- ids: '',
- category: '',
- keyword: '',
- },
- })
+ const params: Record = {
+ pageNumber: pageNumber || 1,
+ itemPerPage: itemPerPage,
+ }
+ if (ids) params.ids = ids
+ if (category) params.category = category
+ if (keyword) params.keyword = keyword
+
+ const response = await axiosInstance(null).get('/api/suitable/list', { params })
return response.data
} catch (error) {
console.error('지붕재 데이터 로드 실패:', error)
@@ -26,16 +39,6 @@ export function useSuitable() {
}
}
- // const updateSearchResults = async (selectedCategory: string | undefined, searchValue: string | undefined): Promise => {
- // try {
- // const response = await axiosInstance(null).get('/api/suitable/list', { params: { selectedCategory, searchValue } })
- // return response.data
- // } catch (error) {
- // console.error('지붕재 데이터 검색 실패:', error)
- // return []
- // }
- // }
-
const getSuitableCommCode = () => {
const headCodes = Object.values(SUITABLE_HEAD_CODE) as SUITABLE_HEAD_CODE[]
for (const code of headCodes) {
@@ -63,35 +66,30 @@ export function useSuitable() {
}
}
- const { data: suitableList, isLoading: isInitialLoading } = useQuery({
- queryKey: ['suitables', 'list'],
- queryFn: async () => await getSuitables(),
- staleTime: 1000 * 60 * 10, // 10분
- gcTime: 1000 * 60 * 10, // 10분
- })
-
const {
- data: suitableSearchResults,
- refetch: refetchBySearch,
- isLoading: isSearchLoading,
- } = useQuery({
- queryKey: ['suitables', 'search', selectedCategory, isSearch],
- queryFn: async () => {
- if (!isSearch && !selectedCategory) {
- return isInitialLoading ? await getSuitables() : suitableList ?? [] // 검색 상태가 아니면 초기 데이터 반환 임시처리
- } else {
- return (
- suitableList?.filter((item: Suitable) => {
- const categoryMatch = !selectedCategory || item.roofMtCd === selectedCategory
- const searchMatch = !searchValue || item.productName.includes(searchValue)
- return categoryMatch && searchMatch
- }) ?? []
- )
- }
+ data: suitables,
+ fetchNextPage,
+ hasNextPage,
+ isFetchingNextPage,
+ isLoading,
+ isError,
+ error,
+ } = useInfiniteQuery({
+ queryKey: ['suitables', 'list', selectedCategory, isSearch],
+ queryFn: async (context) => {
+ const pageParam = context.pageParam as number
+ return await getSuitables({
+ pageNumber: pageParam,
+ ...(selectedCategory && { category: selectedCategory }),
+ ...(isSearch && { keyword: searchValue }),
+ })
},
+ getNextPageParam: (lastPage: Suitable[], allPages: Suitable[][]) => {
+ return lastPage.length === itemPerPage ? allPages.length + 1 : undefined
+ },
+ initialPageParam: 1,
staleTime: 1000 * 60 * 10,
gcTime: 1000 * 60 * 10,
- enabled: true,
})
return {
@@ -99,9 +97,10 @@ export function useSuitable() {
getSuitableCommCode,
toCodeName,
toSuitableDetail,
- suitableList,
- suitableSearchResults,
- refetchBySearch,
- isSearchLoading,
+ suitables,
+ fetchNextPage,
+ hasNextPage,
+ isFetchingNextPage,
+ isLoading,
}
}
diff --git a/src/store/useSuitableStore.ts b/src/store/useSuitableStore.ts
index 5fa4cd0..74fb115 100644
--- a/src/store/useSuitableStore.ts
+++ b/src/store/useSuitableStore.ts
@@ -2,6 +2,9 @@ import { create } from 'zustand'
import type { CommCode } from '@/types/CommCode'
interface SuitableState {
+ /* 초기 데이터 로드 개수*/
+ itemPerPage: number
+
/* 공통코드 */
suitableCommCode: Map
/* 공통코드 설정 */
@@ -23,21 +26,22 @@ interface SuitableState {
setSearchValue: (value: string) => void
/* 선택된 아이템 리스트 */
- selectedItems: number[]
+ selectedItems: Map>
/* 선택된 아이템 추가 */
- addSelectedItem: (itemId: number) => void
+ addSelectedItem: (mainId: number, detailId?: number) => void
/* 선택된 아이템 제거 */
- removeSelectedItem: (itemId: number) => void
+ removeSelectedItem: (mainId: number, detailId?: number) => void
/* 선택된 아이템 모두 제거 */
clearSelectedItems: () => void
}
export const useSuitableStore = create((set) => ({
+ itemPerPage: 100 as number,
suitableCommCode: new Map() as Map,
isSearch: false as boolean,
selectedCategory: '' as string,
searchValue: '' as string,
- selectedItems: [] as number[],
+ selectedItems: new Map() as Map>,
/* 공통코드 설정 */
setSuitableCommCode: (headCode: string, commCode: CommCode[]) =>
@@ -55,17 +59,46 @@ export const useSuitableStore = create((set) => ({
setSearchValue: (value: string) => set({ searchValue: value }),
/* 선택된 아이템 추가 */
- addSelectedItem: (itemId: number) =>
- set((state) => ({
- selectedItems: state.selectedItems.some((i) => i === itemId) ? state.selectedItems : [...state.selectedItems, itemId],
- })),
+ addSelectedItem: (mainId: number, detailId?: number) => {
+ if (detailId) {
+ // 디테일(하위) 아이템 추가
+ set((state) => {
+ const detailSet = state.selectedItems.get(mainId) || new Set()
+ detailSet.add(detailId)
+ state.selectedItems.set(mainId, detailSet)
+ return { selectedItems: state.selectedItems }
+ })
+ } else {
+ // 메인(상위) 아이템 추가
+ set((state) => {
+ state.selectedItems.set(mainId, new Set())
+ return { selectedItems: state.selectedItems }
+ })
+ }
+ },
/* 선택된 아이템 제거 */
- removeSelectedItem: (itemId: number) =>
- set((state) => ({
- selectedItems: state.selectedItems.filter((i) => i !== itemId),
- })),
+ removeSelectedItem: (mainId: number, detailId?: number) => {
+ set((state) => {
+ const newSelectedItems = new Map(state.selectedItems)
+
+ if (!detailId) {
+ // 메인(상위) 아이템 제거
+ newSelectedItems.delete(mainId)
+ return { selectedItems: newSelectedItems }
+ }
+
+ // 디테일(하위) 아이템 제거
+ const detailSet = state.selectedItems.get(mainId) || new Set()
+ detailSet.delete(detailId)
+
+ // 디테일(하위)하위 아이템이 모두 제거되면 메인 아이템도 제거
+ detailSet.size === 0 ? newSelectedItems.delete(mainId) : newSelectedItems.set(mainId, detailSet)
+
+ return { selectedItems: newSelectedItems }
+ })
+ },
/* 선택된 아이템 모두 제거 */
- clearSelectedItems: () => set({ selectedItems: [] }),
+ clearSelectedItems: () => set({ selectedItems: new Map() as Map> }),
}))
diff --git a/src/types/Suitable.ts b/src/types/Suitable.ts
index e3966c8..270dd46 100644
--- a/src/types/Suitable.ts
+++ b/src/types/Suitable.ts
@@ -31,5 +31,6 @@ export type Suitable = {
manuFtCd: string
roofMtCd: string
roofShCd: string
+ detailCnt: number
detail: string
}