From 2e0ff4ae6ff089da81805113b4de46150c9f9756 Mon Sep 17 00:00:00 2001 From: Daseul Kim Date: Thu, 22 May 2025 18:16:31 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=A7=80=EB=B6=95=EC=9E=AC=EC=A0=81?= =?UTF-8?q?=ED=95=A9=EC=84=B1=20=EC=A1=B0=ED=9A=8C=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=95&=EC=9D=B8=ED=94=BC=EB=8B=88=ED=8B=B0=EC=BF=BC?= =?UTF-8?q?=EB=A6=AC=20=EC=A0=81=EC=9A=A9,=20=EC=B2=B4=ED=81=AC=EB=B0=95?= =?UTF-8?q?=EC=8A=A4=20=EC=84=A0=ED=83=9D=20=EC=B2=98=EB=A6=AC=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/api/suitable/list/route.ts | 13 +- src/components/suitable/Suitable.tsx | 19 ++- src/components/suitable/SuitableList.tsx | 157 +++++++++------------ src/components/suitable/SuitableNoData.tsx | 7 +- src/hooks/useSuitable.ts | 103 +++++++------- src/store/useSuitableStore.ts | 59 ++++++-- src/types/Suitable.ts | 1 + 7 files changed, 182 insertions(+), 177 deletions(-) diff --git a/src/app/api/suitable/list/route.ts b/src/app/api/suitable/list/route.ts index edc5579..005d556 100644 --- a/src/app/api/suitable/list/route.ts +++ b/src/app/api/suitable/list/route.ts @@ -12,7 +12,6 @@ export async function GET(request: NextRequest) { return NextResponse.json({ error: '페이지 번호와 페이지당 아이템 수가 필요합니다' }, { status: 400 }) } - const ids = searchParams.get('ids') const category = searchParams.get('category') const keyword = searchParams.get('keyword') @@ -20,14 +19,13 @@ export async function GET(request: NextRequest) { SELECT msm.id , msm.product_name - , msm.manu_ft_cd - , msm.roof_mt_cd - , msm.roof_sh_cd + , 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 @@ -42,9 +40,7 @@ export async function GET(request: NextRequest) { GROUP BY msd.main_id ) AS details ON msm.id = details.main_id - --mainIds AND details.main_id IN (:mainIds) WHERE 1=1 - --mainIds AND msm.id IN (:mainIds) --roofMtCd AND msm.roof_mt_cd = ':roofMtCd' --productName AND msm.product_name LIKE '%:productName%' ORDER BY msm.product_name @@ -53,10 +49,6 @@ export async function GET(request: NextRequest) { ` // 검색 조건 설정 - if (ids) { - query = query.replaceAll('--mainIds ', '') - query = query.replaceAll(':mainIds', ids) - } if (category) { query = query.replace('--roofMtCd ', '') query = query.replace(':roofMtCd', category) @@ -66,7 +58,6 @@ export async function GET(request: NextRequest) { query = query.replace(':productName', keyword) } - // @ts-ignore const suitable: Suitable[] = await prisma.$queryRawUnsafe(query, pageNumber, itemPerPage) return NextResponse.json(suitable) diff --git a/src/components/suitable/Suitable.tsx b/src/components/suitable/Suitable.tsx index 15cda44..f5f22b2 100644 --- a/src/components/suitable/Suitable.tsx +++ b/src/components/suitable/Suitable.tsx @@ -2,16 +2,16 @@ import Image from 'next/image' import { useEffect, useState } from 'react' -import SuitableListRaw from './SuitableList' +import SuitableList from './SuitableList' import { useSuitable } from '@/hooks/useSuitable' import { useSuitableStore } from '@/store/useSuitableStore' import type { CommCode } from '@/types/CommCode' import { SUITABLE_HEAD_CODE } from '@/types/Suitable' -export default function SuitableRaw() { +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 SuitableRaw() { return } setIsSearch(true) - refetchBySearch() } const handleInputClear = () => { setSearchValue('') setIsSearch(false) - refetchBySearch() } - useEffect(() => { - refetchBySearch() - }, [selectedCategory]) - useEffect(() => { getSuitableCommCode() return () => { @@ -62,6 +56,11 @@ export default function SuitableRaw() { placeholder="屋根材 製品名を入力してください." value={searchValue} onChange={(e) => setSearchValue(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter') { + handleInputSearch() + } + }} /> {searchValue && 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 }