From 7a6b9cbf92de019c9e53f4fbcebc33fda9b76918 Mon Sep 17 00:00:00 2001 From: Daseul Kim Date: Tue, 20 May 2025 13:51:49 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=A7=80=EB=B6=95=EC=9E=AC=20=EC=A0=81?= =?UTF-8?q?=ED=95=A9=EC=84=B1=20=EC=A1=B0=ED=9A=8C=20=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=84=B0=20=ED=8F=AC=EB=A9=A7=20=EB=B3=80=EA=B2=BD=20=EB=B0=8F?= =?UTF-8?q?=20=EC=A1=B0=ED=9A=8C=20=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/api/suitable/list/route.ts | 88 ++++++---- src/app/api/suitable/list/test/route.ts | 71 ++++++++ src/app/suitable-test/layout.tsx | 24 +++ src/app/suitable-test/page.tsx | 9 + src/components/suitable/Suitable.tsx | 4 +- src/components/suitable/SuitableList.tsx | 177 ++++++++++++++------ src/components/suitable/SuitableListRaw.tsx | 173 +++++++++++++++++++ src/components/suitable/SuitableRaw.tsx | 118 +++++++++++++ src/hooks/useSuitable.ts | 76 ++++----- src/hooks/useSuitableRaw.ts | 109 ++++++++++++ src/libs/axios.ts | 2 +- src/types/CommCode.ts | 6 +- src/types/Suitable.ts | 52 +++--- 13 files changed, 754 insertions(+), 155 deletions(-) create mode 100644 src/app/api/suitable/list/test/route.ts create mode 100644 src/app/suitable-test/layout.tsx create mode 100644 src/app/suitable-test/page.tsx create mode 100644 src/components/suitable/SuitableListRaw.tsx create mode 100644 src/components/suitable/SuitableRaw.tsx create mode 100644 src/hooks/useSuitableRaw.ts 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