diff --git a/src/app/api/suitable/list/route.ts b/src/app/api/suitable/list/route.ts
index 32eac33..005d556 100644
--- a/src/app/api/suitable/list/route.ts
+++ b/src/app/api/suitable/list/route.ts
@@ -1,82 +1,66 @@
import { NextRequest, NextResponse } from 'next/server'
import { prisma } from '@/libs/prisma'
-import { SUITABLE_HEAD_CODE, type SuitableMain } from '@/types/Suitable'
+import { type Suitable } from '@/types/Suitable'
export async function GET(request: NextRequest) {
try {
const searchParams = request.nextUrl.searchParams
+
+ const pageNumber = parseInt(searchParams.get('pageNumber') || '0')
+ const itemPerPage = parseInt(searchParams.get('itemPerPage') || '0')
+ if (pageNumber === 0 || itemPerPage === 0) {
+ return NextResponse.json({ error: '페이지 번호와 페이지당 아이템 수가 필요합니다' }, { status: 400 })
+ }
+
const category = searchParams.get('category')
const keyword = searchParams.get('keyword')
- let MainWhereCondition: any = {}
- const whereCondition: string[] = []
- const params: string[] = []
+ let query = `
+ SELECT
+ msm.id
+ , msm.product_name
+ , details.detail_cnt
+ , details.detail
+ FROM ms_suitable_main msm
+ LEFT JOIN (
+ SELECT
+ msd.main_id
+ , COUNT(msd.id) AS detail_cnt
+ , (
+ SELECT
+ msd_json.id
+ , msd_json.trestle_mfpc_cd
+ , msd_json.trestle_manufacturer_product_name
+ , msd_json.memo
+ FROM ms_suitable_detail msd_json
+ WHERE msd.main_id = msd_json.main_id
+ FOR JSON PATH
+ ) AS detail
+ FROM ms_suitable_detail msd
+ GROUP BY msd.main_id
+ ) AS details
+ ON msm.id = details.main_id
+ WHERE 1=1
+ --roofMtCd AND msm.roof_mt_cd = ':roofMtCd'
+ --productName AND msm.product_name LIKE '%:productName%'
+ ORDER BY msm.product_name
+ OFFSET (@P1 - 1) * @P2 ROWS
+ FETCH NEXT @P2 ROWS ONLY;
+ `
+
+ // 검색 조건 설정
if (category) {
- whereCondition.push(`${SUITABLE_HEAD_CODE.ROOF_MT_CD} = @P1`)
- params.push(category)
- MainWhereCondition[SUITABLE_HEAD_CODE.ROOF_MT_CD] = category
+ query = query.replace('--roofMtCd ', '')
+ query = query.replace(':roofMtCd', category)
}
if (keyword) {
- whereCondition.push('PRODUCT_NAME LIKE @P2')
- params.push(`%${keyword}%`)
- MainWhereCondition['PRODUCT_NAME'] = {
- contains: keyword,
- }
+ query = query.replace('--productName ', '')
+ query = query.replace(':productName', keyword)
}
- const startTime = performance.now()
- console.log(`쿼리 (main table) 시작 시간: ${startTime}ms`)
- // @ts-ignore
- const suitable = await prisma.MS_SUITABLE_MAIN.findMany({
- select: {
- ID: true,
- PRODUCT_NAME: true,
- ROOF_MT_CD: true,
- },
- where: MainWhereCondition,
- orderBy: {
- PRODUCT_NAME: 'asc',
- },
- })
+ const suitable: Suitable[] = await prisma.$queryRawUnsafe(query, pageNumber, itemPerPage)
- const endTime = performance.now()
- console.log(`쿼리 (main table) 종료 시간: ${endTime - startTime}ms`)
-
- const mainIds: number[] = suitable.map((item: SuitableMain) => item.id)
-
-
- const startTime2 = performance.now()
- console.log(`쿼리 (detail table) 시작 시간: ${startTime2}ms`)
- let detailQuery = `
- SELECT
- msd.main_id
- , (
- SELECT
- msd_json.id
- , msd_json.trestle_mfpc_cd
- , msd_json.trestle_manufacturer_product_name
- , msd_json.memo
- FROM ms_suitable_detail msd_json
- WHERE msd.main_id = msd_json.main_id
- FOR JSON PATH
- ) AS detail
- FROM ms_suitable_detail msd
- -- WHERE 1=1
- GROUP BY msd.main_id
- `
- if (whereCondition.length > 0) {
- detailQuery = detailQuery.replace('-- WHERE 1=1', `WHERE msd.main_id IN @P1`)
- }
- // @ts-ignore
- const detail = await prisma.$queryRawUnsafe(detailQuery, ...mainIds)
-
- const endTime2 = performance.now()
- console.log(`쿼리 (detail table) 종료 시간: ${endTime2 - startTime2}ms`)
-
- const endTime3 = performance.now()
- console.log(`쿼리 총 실행 시간: ${endTime3 - startTime}ms`)
-
- return NextResponse.json({ suitable, detail })
+ return NextResponse.json(suitable)
} catch (error) {
console.error('❌ 데이터 조회 중 오류가 발생했습니다:', error)
return NextResponse.json({ error: '데이터 조회 중 오류가 발생했습니다' }, { status: 500 })
diff --git a/src/app/api/suitable/list/test/route.ts b/src/app/api/suitable/list/test/route.ts
deleted file mode 100644
index e4688bd..0000000
--- a/src/app/api/suitable/list/test/route.ts
+++ /dev/null
@@ -1,71 +0,0 @@
-import { NextRequest, NextResponse } from 'next/server'
-import { prisma } from '@/libs/prisma'
-import { SUITABLE_HEAD_CODE, type Suitable } from '@/types/Suitable'
-
-export async function GET(request: NextRequest) {
- try {
- const searchParams = request.nextUrl.searchParams
- const category = searchParams.get('category')
- const keyword = searchParams.get('keyword')
-
- const whereCondition: string[] = []
- const params: string[] = []
- if (category) {
- whereCondition.push(`${SUITABLE_HEAD_CODE.ROOF_MT_CD} = @P1`)
- params.push(category)
- }
- if (keyword) {
- whereCondition.push('PRODUCT_NAME LIKE @P2')
- params.push(`%${keyword}%`)
- }
-
- const startTime = performance.now()
- console.log(`쿼리 시작 시간: ${startTime}ms`)
-
- let query = `
- SELECT
- msm.id
- , msm.product_name
- , msm.manu_ft_cd
- , msm.roof_mt_cd
- , msm.roof_sh_cd
- , details.detail
- FROM ms_suitable_main msm
- LEFT JOIN (
- SELECT
- msd.main_id
- , (
- SELECT
- msd_json.id
- , msd_json.trestle_mfpc_cd
- , msd_json.trestle_manufacturer_product_name
- , msd_json.memo
- FROM ms_suitable_detail msd_json
- WHERE msd.main_id = msd_json.main_id
- FOR JSON PATH
- ) AS detail
- FROM ms_suitable_detail msd
- GROUP BY msd.main_id
- ) AS details
- ON msm.id = details.main_id
- -- AND details.main_id IN (#mainIds)
- -- WHERE 1=1
- ORDER BY msm.product_name`
-
- // 검색 조건 추가
- if (whereCondition.length > 0) {
- query = query.replace('-- WHERE 1=1', `WHERE ${whereCondition.join(' AND ')}`)
- }
-
- // @ts-ignore
- const suitable: Suitable[] = await prisma.$queryRawUnsafe(query, ...params)
-
- const endTime = performance.now()
- console.log(`쿼리 실행 시간: ${endTime - startTime}ms`)
-
- return NextResponse.json(suitable)
- } catch (error) {
- console.error('❌ 데이터 조회 중 오류가 발생했습니다:', error)
- return NextResponse.json({ error: '데이터 조회 중 오류가 발생했습니다' }, { status: 500 })
- }
-}
diff --git a/src/app/suitable-test/layout.tsx b/src/app/suitable-test/layout.tsx
deleted file mode 100644
index e5e7c3f..0000000
--- a/src/app/suitable-test/layout.tsx
+++ /dev/null
@@ -1,24 +0,0 @@
-import type { ReactNode } from 'react'
-
-interface SuitableLayoutProps {
- children: ReactNode
-}
-
-export default function layout({ children }: SuitableLayoutProps) {
- return (
- <>
-
-
-
-
-
この適合表は参考資料として使用してください.
-
詳細やお問い合わせは1:1お問い合わせをご利用ください.
-
屋根材の選択or屋根材名を直接入力してください.
-
-
- {children}
-
-
- >
- )
-}
diff --git a/src/app/suitable-test/page.tsx b/src/app/suitable-test/page.tsx
deleted file mode 100644
index a5299fe..0000000
--- a/src/app/suitable-test/page.tsx
+++ /dev/null
@@ -1,9 +0,0 @@
-import SuitableRaw from '@/components/suitable/SuitableRaw'
-
-export default function page() {
- return (
- <>
-
- >
- )
-}
diff --git a/src/components/suitable/Suitable.tsx b/src/components/suitable/Suitable.tsx
index 36a397f..f5f22b2 100644
--- a/src/components/suitable/Suitable.tsx
+++ b/src/components/suitable/Suitable.tsx
@@ -11,7 +11,7 @@ import { SUITABLE_HEAD_CODE } from '@/types/Suitable'
export default function Suitable() {
const [reference, setReference] = useState(true)
- const { getSuitableCommCode, refetchBySearch } = useSuitable()
+ const { getSuitableCommCode } = useSuitable()
const { suitableCommCode, selectedCategory, setSelectedCategory, searchValue, setSearchValue, setIsSearch, clearSelectedItems } = useSuitableStore()
const handleInputSearch = async () => {
@@ -20,19 +20,13 @@ export default function Suitable() {
return
}
setIsSearch(true)
- refetchBySearch()
}
const handleInputClear = () => {
setSearchValue('')
setIsSearch(false)
- refetchBySearch()
}
- useEffect(() => {
- refetchBySearch()
- }, [selectedCategory])
-
useEffect(() => {
getSuitableCommCode()
return () => {
@@ -62,6 +56,11 @@ export default function Suitable() {
placeholder="屋根材 製品名を入力してください."
value={searchValue}
onChange={(e) => setSearchValue(e.target.value)}
+ onKeyDown={(e) => {
+ if (e.key === 'Enter') {
+ handleInputSearch()
+ }
+ }}
/>
{searchValue && }
diff --git a/src/components/suitable/SuitableList.tsx b/src/components/suitable/SuitableList.tsx
index 18d94e5..f55fa67 100644
--- a/src/components/suitable/SuitableList.tsx
+++ b/src/components/suitable/SuitableList.tsx
@@ -1,80 +1,49 @@
'use client'
import Image from 'next/image'
-import { useState, useEffect, useRef, useCallback, useMemo } from 'react'
+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 { SUITABLE_HEAD_CODE, type SuitableMain, type SuitableDetail } from '@/types/Suitable'
-
-// 한 번에 로드할 아이템 수
-const ITEMS_PER_PAGE = 100
+import { SUITABLE_HEAD_CODE, type Suitable, type SuitableDetail } from '@/types/Suitable'
export default function SuitableList() {
- const { toCodeName, suitableSearchResults, isSearchLoading, toSuitableDetail } = useSuitable()
+ const { toCodeName, toSuitableDetail, toSuitableDetailIds, suitables, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading } = useSuitable()
const { selectedItems, addSelectedItem, removeSelectedItem } = useSuitableStore()
-
const [openItems, setOpenItems] = useState>(new Set())
- const [visibleItems, setVisibleItems] = useState([])
- const [page, setPage] = useState(1)
- const [isLoadingMore, setIsLoadingMore] = useState(false)
const observerTarget = useRef(null)
- // 선택된 아이템 확인 함수 메모이제이션
- const isItemSelected = useCallback(
- (itemId: number) => {
- return selectedItems.some((selected) => selected === itemId)
+ // 선택된 아이템 확인 - 메인 하위 아이템 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],
)
- // 초기 데이터 로드
- 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 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(
- (itemId: number) => {
- isItemSelected(itemId) ? removeSelectedItem(itemId) : addSelectedItem(itemId)
+ (mainId: number, detailId?: number, detailIds?: Set): void => {
+ 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)
@@ -85,38 +54,31 @@ export default function SuitableList() {
// TODO: 추후 지붕재 적합성 데이터 CUD 구현 시 ×, ー 데이터 관리 필요
const suitableCheck = useCallback((value: string) => {
- if (value === '×') {
- return (
-
-
-
- )
- } else if (value === 'ー') {
- return (
-
-
-
- )
- } else {
- return (
-
-
-
- )
+ const iconMap: Record = {
+ '×': '/assets/images/sub/compliance_x_icon.svg',
+ ー: '/assets/images/sub/compliance_quest_icon.svg',
+ default: '/assets/images/sub/compliance_check_icon.svg',
}
+ return (
+
+
+
+ )
}, [])
- // 메모이제이션된 아이템 렌더링
+ // 아이템 렌더링
const renderItem = useCallback(
- (item: SuitableMain) => {
- const isSelected = isItemSelected(item.id)
- const isOpen = openItems.has(item.id)
-
+ (item: Suitable) => {
return (
-
+
-
- {toSuitableDetail(item.id).map((subItem: SuitableDetail) => (
+ {toSuitableDetail(item.detail).map((subItem: SuitableDetail) => (
-
-
+ handleItemClick(item.id, subItem.id)}
+ />
@@ -149,24 +116,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?.suitable.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/SuitableListRaw.tsx b/src/components/suitable/SuitableListRaw.tsx
deleted file mode 100644
index 6dc7f36..0000000
--- a/src/components/suitable/SuitableListRaw.tsx
+++ /dev/null
@@ -1,173 +0,0 @@
-'use client'
-
-import Image from 'next/image'
-import { useState, useEffect, useRef, useCallback, useMemo } from 'react'
-import SuitableButton from './SuitableButton'
-import SuitableNoData from './SuitableNoData'
-import { useSuitableRaw, type Suitable } from '@/hooks/useSuitableRaw'
-import { useSuitableStore } from '@/store/useSuitableStore'
-import { SUITABLE_HEAD_CODE, type SuitableDetail } from '@/types/Suitable'
-
-// 한 번에 로드할 아이템 수
-const ITEMS_PER_PAGE = 100
-
-export default function SuitableListRaw() {
- const { toCodeName, suitableSearchResults, isSearchLoading, toSuitableDetail } = useSuitableRaw()
- const { selectedItems, addSelectedItem, removeSelectedItem } = useSuitableStore()
- const [openItems, setOpenItems] = useState
>(new Set())
- const [visibleItems, setVisibleItems] = useState([])
- const [page, setPage] = useState(1)
- const [isLoadingMore, setIsLoadingMore] = useState(false)
- const observerTarget = useRef(null)
-
- // 선택된 아이템 확인 함수 메모이제이션
- const isItemSelected = useCallback(
- (itemId: number) => {
- return selectedItems.some((selected) => selected === itemId)
- },
- [selectedItems],
- )
-
- // 초기 데이터 로드
- useEffect(() => {
- if (suitableSearchResults) {
- const initialItems = suitableSearchResults.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.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 (
-
-
-
- )
- } else if (value === 'ー') {
- return (
-
-
-
- )
- } else {
- return (
-
-
-
- )
- }
- }, [])
-
- // 메모이제이션된 아이템 렌더링
- const renderItem = useCallback(
- (item: Suitable) => {
- const isSelected = isItemSelected(item.id)
- const isOpen = openItems.has(item.id)
-
- return (
-
-
-
- handleItemClick(item.id)} />
-
-
-
-
-
-
-
- {toSuitableDetail(item.detail).map((subItem: SuitableDetail) => (
- -
-
-
-
-
-
-
- {suitableCheck(subItem.trestleManufacturerProductName)}
- {subItem.memo && (
-
-
-
- )}
-
-
-
- ))}
-
-
- )
- },
- [isItemSelected, openItems, handleItemClick, toggleItemOpen, suitableCheck, toCodeName, toSuitableDetail],
- )
-
- // 메모이제이션된 아이템 리스트
- const renderedItems = useMemo(() => {
- return visibleItems.map(renderItem)
- }, [visibleItems, renderItem])
-
- if (isSearchLoading) {
- return Loading...
- }
-
- if (!suitableSearchResults?.length) {
- return
- }
-
- return (
- <>
- {renderedItems}
-
- {isLoadingMore &&
데이터를 불러오는 중...
}
-
-
- >
- )
-}
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/components/suitable/SuitableRaw.tsx b/src/components/suitable/SuitableRaw.tsx
deleted file mode 100644
index d48dfea..0000000
--- a/src/components/suitable/SuitableRaw.tsx
+++ /dev/null
@@ -1,118 +0,0 @@
-'use client'
-
-import Image from 'next/image'
-import { useEffect, useState } from 'react'
-import SuitableListRaw from './SuitableListRaw'
-import { useSuitableRaw } from '@/hooks/useSuitableRaw'
-import { useSuitableStore } from '@/store/useSuitableStore'
-import type { CommCode } from '@/types/CommCode'
-import { SUITABLE_HEAD_CODE } from '@/types/Suitable'
-
-export default function SuitableRaw() {
- const [reference, setReference] = useState(true)
-
- const { getSuitableCommCode, refetchBySearch } = useSuitableRaw()
- const { suitableCommCode, selectedCategory, setSelectedCategory, searchValue, setSearchValue, setIsSearch, clearSelectedItems } = useSuitableStore()
-
- const handleInputSearch = async () => {
- if (!searchValue.trim()) {
- alert('屋根材の製品名を入力してください。')
- return
- }
- setIsSearch(true)
- refetchBySearch()
- }
-
- const handleInputClear = () => {
- setSearchValue('')
- setIsSearch(false)
- refetchBySearch()
- }
-
- useEffect(() => {
- refetchBySearch()
- }, [selectedCategory])
-
- useEffect(() => {
- getSuitableCommCode()
- return () => {
- setSelectedCategory('')
- setSearchValue('')
- clearSelectedItems()
- }
- }, [])
-
- return (
-
-
테스트1 페이지
-
-
-
-
-
- setSearchValue(e.target.value)}
- />
- {searchValue && }
-
-
-
-
-
-
-
凡例
-
- setReference(!reference)}>
-
-
-
- -
-
-
- -
-
-
- -
-
-
- -
-
-
-
-
-
-
-
- )
-}
diff --git a/src/hooks/useCommCode.ts b/src/hooks/useCommCode.ts
index bb50240..6c78183 100644
--- a/src/hooks/useCommCode.ts
+++ b/src/hooks/useCommCode.ts
@@ -1,7 +1,8 @@
-import { axiosInstance } from '@/libs/axios'
+import { useAxios } from './useAxios'
import type { CommCode } from '@/types/CommCode'
export function useCommCode() {
+ const { axiosInstance } = useAxios()
const getCommCode = async (headCode: string): Promise
=> {
try {
const response = await axiosInstance(null).get('/api/comm-code', { params: { headCode: headCode } })
diff --git a/src/hooks/useSuitable.ts b/src/hooks/useSuitable.ts
index 4bdd9b2..f651b4d 100644
--- a/src/hooks/useSuitable.ts
+++ b/src/hooks/useSuitable.ts
@@ -1,33 +1,43 @@
-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 SuitableDetailGroup, type SuitableMain, type Suitable, type SuitableDetail } from '@/types/Suitable'
+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')
+ 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)
- return { suitable: [], detail: [] }
+ return []
}
}
- // 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) {
@@ -42,12 +52,8 @@ export function useSuitable() {
return commCode?.find((item) => item.code === code)?.codeJp || ''
}
- const toSuitableDetail = (mainId: number): SuitableDetail[] => {
+ const toSuitableDetail = (suitableDetailString: string): SuitableDetail[] => {
try {
- const suitableDetailString = suitableList?.detail.find((item) => item.mainId === mainId)?.detail
- if (!suitableDetailString) {
- return []
- }
const suitableDetailArray = transformObjectKeys(JSON.parse(suitableDetailString)) as SuitableDetail[]
if (!Array.isArray(suitableDetailArray)) {
throw new Error('suitableDetailArray is not an array')
@@ -59,39 +65,39 @@ 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 toSuitableDetailIds = (suitableDetailString: string): Set => {
+ try {
+ return new Set(JSON.parse(suitableDetailString).map(({ id }: { id: number }) => id))
+ } catch (error) {
+ console.error('지붕재 데이터 파싱 실패:', error)
+ return new Set()
+ }
+ }
const {
- data: suitableSearchResults,
- refetch: refetchBySearch,
- isLoading: isSearchLoading,
- } = useQuery({
- queryKey: ['suitables', 'search', selectedCategory, isSearch],
- queryFn: async () => {
- if (!isSearch && !selectedCategory) {
- // 검색 상태가 아니면 초기 데이터 반환 임시처리
- return isInitialLoading ? await getSuitables() : suitableList ?? { suitable: [], detail: [] }
- } else {
- const filteredSuitable = suitableList?.suitable.filter((item: SuitableMain) => {
- const categoryMatch = !selectedCategory || item.roofMtCd === selectedCategory
- const searchMatch = !searchValue || item.productName.includes(searchValue)
- return categoryMatch && searchMatch
- }) ?? []
- const mainIds = filteredSuitable.map((item: SuitableMain) => item.id)
- const filteredDetail = suitableList?.detail.filter((item: SuitableDetailGroup) => {
- return mainIds.includes(item.mainId)
- }) ?? []
- return { suitable: filteredSuitable, detail: filteredDetail }
- }
+ 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 +105,11 @@ export function useSuitable() {
getSuitableCommCode,
toCodeName,
toSuitableDetail,
- suitableList,
- suitableSearchResults,
- refetchBySearch,
- isSearchLoading,
+ toSuitableDetailIds,
+ suitables,
+ fetchNextPage,
+ hasNextPage,
+ isFetchingNextPage,
+ isLoading,
}
}
diff --git a/src/hooks/useSuitableRaw.ts b/src/hooks/useSuitableRaw.ts
deleted file mode 100644
index a962244..0000000
--- a/src/hooks/useSuitableRaw.ts
+++ /dev/null
@@ -1,109 +0,0 @@
-import { useQuery } from '@tanstack/react-query'
-import { axiosInstance, transformObjectKeys } from '@/libs/axios'
-import { useSuitableStore } from '@/store/useSuitableStore'
-import { useCommCode } from './useCommCode'
-import { SUITABLE_HEAD_CODE, type SuitableDetail } from '@/types/Suitable'
-
-export type Suitable = {
- id: number
- productName: string
- manuFtCd: string
- roofMtCd: string
- roofShCd: string
- detail: string
-}
-
-export function useSuitableRaw() {
- const { getCommCode } = useCommCode()
- const { selectedCategory, searchValue, suitableCommCode, setSuitableCommCode, isSearch } = useSuitableStore()
-
- const getSuitables = async (): Promise => {
- try {
- const response = await axiosInstance(null).get('/api/suitable/list/test')
- return response.data
- } catch (error) {
- console.error('지붕재 데이터 로드 실패:', error)
- return []
- }
- }
-
- // 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) {
- getCommCode(code).then((res) => {
- setSuitableCommCode(code, res)
- })
- }
- }
-
- const toCodeName = (headCode: string, code: string): string => {
- const commCode = suitableCommCode.get(headCode)
- return commCode?.find((item) => item.code === code)?.codeJp || ''
- }
-
- const toSuitableDetail = (suitableDetailString: string): SuitableDetail[] => {
- try {
- const suitableDetailArray = transformObjectKeys(JSON.parse(suitableDetailString)) as SuitableDetail[]
- if (!Array.isArray(suitableDetailArray)) {
- throw new Error('suitableDetailArray is not an array')
- }
- return suitableDetailArray
- } catch (error) {
- console.error('지붕재 데이터 파싱 실패:', error)
- return []
- }
- }
-
- 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({
- } = 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
- }) ?? []
- )
- }
- },
- staleTime: 1000 * 60 * 10,
- gcTime: 1000 * 60 * 10,
- enabled: true,
- })
-
- return {
- getSuitables,
- getSuitableCommCode,
- toCodeName,
- toSuitableDetail,
- suitableList,
- suitableSearchResults,
- refetchBySearch,
- isSearchLoading,
- }
-}
diff --git a/src/store/useSuitableStore.ts b/src/store/useSuitableStore.ts
index 5fa4cd0..74f4d0b 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, detailIds?: Set) => 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, detailIds?: Set) => {
+ 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, detailIds || 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 2e3563b..270dd46 100644
--- a/src/types/Suitable.ts
+++ b/src/types/Suitable.ts
@@ -25,20 +25,12 @@ export type SuitableDetail = {
memo: string
}
-// export type Suitable = {
-// id: number
-// productName: string
-// manuFtCd: string
-// roofMtCd: string
-// roofShCd: string
-// detail: string
-// }
-
-export type SuitableDetailGroup = {
- mainId: number
+export type Suitable = {
+ id: number
+ productName: string
+ manuFtCd: string
+ roofMtCd: string
+ roofShCd: string
+ detailCnt: number
detail: string
}
-export type Suitable = {
- suitable: SuitableMain[]
- detail: SuitableDetailGroup[]
-}
\ No newline at end of file