diff --git a/src/app/api/suitable/list/route.ts b/src/app/api/suitable/list/route.ts
index d6d8f37..32eac33 100644
--- a/src/app/api/suitable/list/route.ts
+++ b/src/app/api/suitable/list/route.ts
@@ -1,6 +1,6 @@
import { NextRequest, NextResponse } from 'next/server'
import { prisma } from '@/libs/prisma'
-import { SUITABLE_HEAD_CODE } from '@/types/Suitable'
+import { SUITABLE_HEAD_CODE, type SuitableMain } from '@/types/Suitable'
export async function GET(request: NextRequest) {
try {
@@ -8,29 +8,24 @@ export async function GET(request: NextRequest) {
const category = searchParams.get('category')
const keyword = searchParams.get('keyword')
- let whereCondition: any = {}
+ let MainWhereCondition: any = {}
+ const whereCondition: string[] = []
+ const params: string[] = []
if (category) {
- whereCondition[SUITABLE_HEAD_CODE.ROOF_MT_CD] = category
+ whereCondition.push(`${SUITABLE_HEAD_CODE.ROOF_MT_CD} = @P1`)
+ params.push(category)
+ MainWhereCondition[SUITABLE_HEAD_CODE.ROOF_MT_CD] = category
}
if (keyword) {
- whereCondition['PRODUCT_NAME'] = {
+ whereCondition.push('PRODUCT_NAME LIKE @P2')
+ params.push(`%${keyword}%`)
+ MainWhereCondition['PRODUCT_NAME'] = {
contains: keyword,
}
}
+ const startTime = performance.now()
+ console.log(`쿼리 (main table) 시작 시간: ${startTime}ms`)
- // // 1 include 사용
- // // @ts-ignore
- // const suitable = await prisma.MS_SUITABLE_MAIN.findMany({
- // where: whereCondition,
- // include: {
- // MS_SUITABLE_DETAIL: true,
- // },
- // orderBy: {
- // PRODUCT_NAME: 'asc',
- // },
- // })
-
- // 2 include 안쓰고 따로 쿼리, 쓸거만 골라서 조회
// @ts-ignore
const suitable = await prisma.MS_SUITABLE_MAIN.findMany({
select: {
@@ -38,31 +33,50 @@ export async function GET(request: NextRequest) {
PRODUCT_NAME: true,
ROOF_MT_CD: true,
},
- where: whereCondition,
+ where: MainWhereCondition,
orderBy: {
PRODUCT_NAME: 'asc',
},
})
- // @ts-ignore
- const suitableDetail = await prisma.MS_SUITABLE_DETAIL.findMany({
- select: {
- ID: true,
- MAIN_ID: true,
- TRESTLE_MFPC_CD: true,
- TRESTLE_MANUFACTURER_PRODUCT_NAME: true,
- MEMO: true,
- },
- where: whereCondition
- ? {
- MAIN_ID: {
- in: suitable.map((suitable: { ID: number }) => suitable.ID),
- },
- }
- : undefined,
- orderBy: [{ MAIN_ID: 'asc' }, { TRESTLE_MANUFACTURER_PRODUCT_NAME: 'asc' }],
- })
- return NextResponse.json({ suitable, suitableDetail })
+ 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 })
} 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
new file mode 100644
index 0000000..e4688bd
--- /dev/null
+++ b/src/app/api/suitable/list/test/route.ts
@@ -0,0 +1,71 @@
+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
new file mode 100644
index 0000000..e5e7c3f
--- /dev/null
+++ b/src/app/suitable-test/layout.tsx
@@ -0,0 +1,24 @@
+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
new file mode 100644
index 0000000..a5299fe
--- /dev/null
+++ b/src/app/suitable-test/page.tsx
@@ -0,0 +1,9 @@
+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 fe2793d..36a397f 100644
--- a/src/components/suitable/Suitable.tsx
+++ b/src/components/suitable/Suitable.tsx
@@ -48,8 +48,8 @@ export default function Suitable() {
diff --git a/src/components/suitable/SuitableList.tsx b/src/components/suitable/SuitableList.tsx
index d436bc9..18d94e5 100644
--- a/src/components/suitable/SuitableList.tsx
+++ b/src/components/suitable/SuitableList.tsx
@@ -1,32 +1,90 @@
'use client'
import Image from 'next/image'
-import { useState } from 'react'
+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, filterSuitableDetail } = useSuitable()
+ const { toCodeName, suitableSearchResults, isSearchLoading, toSuitableDetail } = 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 handleItemClick = (itemId: number) => {
- selectedItems.some((selected) => selected === itemId) ? removeSelectedItem(itemId) : addSelectedItem(itemId)
- }
+ // 선택된 아이템 확인 함수 메모이제이션
+ const isItemSelected = useCallback(
+ (itemId: number) => {
+ return selectedItems.some((selected) => selected === itemId)
+ },
+ [selectedItems],
+ )
- const toggleItemOpen = (itemId: number) => {
+ // 초기 데이터 로드
+ 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 = (value: string) => {
+ const suitableCheck = useCallback((value: string) => {
if (value === '×') {
return (
@@ -46,52 +104,71 @@ export default function SuitableList() {
)
}
+ }, [])
+
+ // 메모이제이션된 아이템 렌더링
+ const renderItem = useCallback(
+ (item: SuitableMain) => {
+ const isSelected = isItemSelected(item.id)
+ const isOpen = openItems.has(item.id)
+
+ return (
+
+
+
+ handleItemClick(item.id)} />
+
+
+
+
+
+
+
+ {toSuitableDetail(item.id).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?.suitable.length) {
+ return
}
return (
<>
- {isSearchLoading ? (
- Loading...
- ) : suitableSearchResults && suitableSearchResults.suitable.length > 0 ? (
- <>
- {suitableSearchResults.suitable.map((item: SuitableMain) => (
-
-
-
- handleItemClick(item.ID)} />
-
-
-
-
-
-
-
- {filterSuitableDetail(item.ID)?.map((subItem: SuitableDetail) => (
- -
-
-
-
-
-
-
- {suitableCheck(subItem.TRESTLE_MANUFACTURER_PRODUCT_NAME)}
- {subItem.MEMO && (
-
-
-
- )}
-
-
-
- ))}
-
-
- ))}
-
- >
- ) : (
-
- )}
+ {renderedItems}
+
+ {isLoadingMore &&
데이터를 불러오는 중...
}
+
+
>
)
}
diff --git a/src/components/suitable/SuitableListRaw.tsx b/src/components/suitable/SuitableListRaw.tsx
new file mode 100644
index 0000000..6dc7f36
--- /dev/null
+++ b/src/components/suitable/SuitableListRaw.tsx
@@ -0,0 +1,173 @@
+'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/SuitableRaw.tsx b/src/components/suitable/SuitableRaw.tsx
new file mode 100644
index 0000000..d48dfea
--- /dev/null
+++ b/src/components/suitable/SuitableRaw.tsx
@@ -0,0 +1,118 @@
+'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 && }
+
+
+
+
+
+
+
凡例
+
+
+
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+
+
+
+
+
+ )
+}
diff --git a/src/hooks/useSuitable.ts b/src/hooks/useSuitable.ts
index 36c1b3f..4bdd9b2 100644
--- a/src/hooks/useSuitable.ts
+++ b/src/hooks/useSuitable.ts
@@ -1,23 +1,20 @@
import { useQuery } from '@tanstack/react-query'
-import { axiosInstance } from '@/libs/axios'
+import { axiosInstance, transformObjectKeys } from '@/libs/axios'
import { useSuitableStore } from '@/store/useSuitableStore'
import { useCommCode } from './useCommCode'
-import { SUITABLE_HEAD_CODE, type SuitableData, type SuitableMain, type SuitableDetail } from '@/types/Suitable'
+import { SUITABLE_HEAD_CODE, type SuitableDetailGroup, type SuitableMain, type Suitable, type SuitableDetail } from '@/types/Suitable'
export function useSuitable() {
const { getCommCode } = useCommCode()
const { selectedCategory, searchValue, suitableCommCode, setSuitableCommCode, isSearch } = useSuitableStore()
- const getSuitables = async (): Promise => {
+ const getSuitables = async (): Promise => {
try {
- const response = await axiosInstance(null).get('/api/suitable/list')
+ const response = await axiosInstance(null).get('/api/suitable/list')
return response.data
} catch (error) {
console.error('지붕재 데이터 로드 실패:', error)
- return {
- suitable: [],
- suitableDetail: [],
- }
+ return { suitable: [], detail: [] }
}
}
@@ -42,10 +39,27 @@ export function useSuitable() {
const toCodeName = (headCode: string, code: string): string => {
const commCode = suitableCommCode.get(headCode)
- return commCode?.find((item) => item.CODE === code)?.CODE_JP || ''
+ return commCode?.find((item) => item.code === code)?.codeJp || ''
}
- const { data: suitableList, isLoading: isInitialLoading } = useQuery({
+ const toSuitableDetail = (mainId: number): 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')
+ }
+ 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분
@@ -56,25 +70,23 @@ export function useSuitable() {
data: suitableSearchResults,
refetch: refetchBySearch,
isLoading: isSearchLoading,
- } = useQuery({
- queryKey: ['suitables', 'search', selectedCategory, searchValue],
+ } = useQuery({
+ queryKey: ['suitables', 'search', selectedCategory, isSearch],
queryFn: async () => {
if (!isSearch && !selectedCategory) {
- return suitableList ?? (await getSuitables()) // 검색 상태가 아니면 초기 데이터 반환 임시처리
+ // 검색 상태가 아니면 초기 데이터 반환 임시처리
+ return isInitialLoading ? await getSuitables() : suitableList ?? { suitable: [], detail: [] }
} else {
- const suitable = suitableList?.suitable.filter((item: SuitableMain) => {
- const categoryMatch = !selectedCategory || item.ROOF_MT_CD === selectedCategory
- const searchMatch = !searchValue || item.PRODUCT_NAME.includes(searchValue)
+ 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 = suitable?.map((item: SuitableMain) => item.ID)
- const suitableDetail = suitableList?.suitableDetail.filter((item: SuitableDetail) => {
- return mainIds?.includes(item.MAIN_ID)
- })
- return {
- suitable: suitable ?? [],
- suitableDetail: suitableDetail ?? [],
- }
+ }) ?? []
+ 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 }
}
},
staleTime: 1000 * 60 * 10,
@@ -82,26 +94,14 @@ export function useSuitable() {
enabled: true,
})
- const filterSuitableDetail = (mainId: number): SuitableDetail[] | undefined => {
- const result: SuitableDetail[] = []
- for (const subItem of suitableSearchResults?.suitableDetail ?? []) {
- if (subItem.MAIN_ID > mainId) break
- if (subItem.MAIN_ID === mainId) {
- result.push(subItem)
- }
- }
- return result
- }
-
return {
getSuitables,
getSuitableCommCode,
toCodeName,
+ toSuitableDetail,
suitableList,
- isInitialLoading,
suitableSearchResults,
refetchBySearch,
isSearchLoading,
- filterSuitableDetail,
}
}
diff --git a/src/hooks/useSuitableRaw.ts b/src/hooks/useSuitableRaw.ts
new file mode 100644
index 0000000..a962244
--- /dev/null
+++ b/src/hooks/useSuitableRaw.ts
@@ -0,0 +1,109 @@
+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/libs/axios.ts b/src/libs/axios.ts
index d973f9d..39bd17b 100644
--- a/src/libs/axios.ts
+++ b/src/libs/axios.ts
@@ -68,7 +68,7 @@ export const transferResponse = (response: any) => {
}
// camel case object 반환
-const transformObjectKeys = (obj: any): any => {
+export const transformObjectKeys = (obj: any): any => {
if (Array.isArray(obj)) {
return obj.map(transformObjectKeys)
}
diff --git a/src/types/CommCode.ts b/src/types/CommCode.ts
index 19daecd..5847047 100644
--- a/src/types/CommCode.ts
+++ b/src/types/CommCode.ts
@@ -1,5 +1,5 @@
export type CommCode = {
- HEAD_CD: string
- CODE: string
- CODE_JP: string
+ headCd: string
+ code: string
+ codeJp: string
}
diff --git a/src/types/Suitable.ts b/src/types/Suitable.ts
index 5df0add..2e3563b 100644
--- a/src/types/Suitable.ts
+++ b/src/types/Suitable.ts
@@ -9,32 +9,36 @@ export enum SUITABLE_HEAD_CODE {
TRESTLE_MFPC_CD = 'TRESTLE_MFPC_CD',
}
-export type SuitableIncludeDetail = {
- ID: number
- PRODUCT_NAME: string
- MANU_FT_CD: string
- ROOF_MT_CD: string
- ROOF_SH_CD: string
- MS_SUITABLE_DETAIL: SuitableDetail[]
-}
-
-export type SuitableData = {
- suitable: SuitableMain[]
- suitableDetail: SuitableDetail[]
-}
-
export type SuitableMain = {
- ID: number
- PRODUCT_NAME: string
- MANU_FT_CD: string
- ROOF_MT_CD: string
- ROOF_SH_CD: string
+ id: number
+ productName: string
+ manuFtCd: string
+ roofMtCd: string
+ roofShCd: string
}
export type SuitableDetail = {
- ID: number
- MAIN_ID: number
- TRESTLE_MFPC_CD: string
- TRESTLE_MANUFACTURER_PRODUCT_NAME: string
- MEMO: string
+ id: number
+ mainId: number
+ trestleMfpcCd: string
+ trestleManufacturerProductName: string
+ memo: string
}
+
+// export type Suitable = {
+// id: number
+// productName: string
+// manuFtCd: string
+// roofMtCd: string
+// roofShCd: string
+// detail: string
+// }
+
+export type SuitableDetailGroup = {
+ mainId: number
+ detail: string
+}
+export type Suitable = {
+ suitable: SuitableMain[]
+ detail: SuitableDetailGroup[]
+}
\ No newline at end of file