diff --git a/src/api/suitable.ts b/src/api/suitable.ts index e8458c4..277bc7e 100644 --- a/src/api/suitable.ts +++ b/src/api/suitable.ts @@ -38,14 +38,31 @@ export interface Suitable { } export const suitableApi = { - getList: async (): Promise => { - const response = await axiosInstance.get('/api/suitable/list') + getList: async (category?: string, keyword?: string): Promise => { + let condition: any = {} + if (category) { + condition['category'] = category + } + if (keyword) { + condition['keyword'] = { + contains: keyword, + } + } + console.log('πŸš€ ~ getList: ~ condition:', condition) + const response = await axiosInstance.get('/api/suitable/list', { params: condition }) console.log('πŸš€ ~ getList: ~ response:', response) return response.data }, + getCategory: async (): Promise => { + const response = await axiosInstance.get('/api/suitable/category') + console.log('πŸš€ ~ getCategory: ~ response:', response) + return response.data + }, + getDetails: async (roofMaterial: string): Promise => { const response = await axiosInstance.get(`/api/suitable/details?roof-material=${roofMaterial}`) + console.log('πŸš€ ~ getDetails: ~ response:', response) return response.data }, diff --git a/src/app/api/suitable/category/route.ts b/src/app/api/suitable/category/route.ts new file mode 100644 index 0000000..288a74a --- /dev/null +++ b/src/app/api/suitable/category/route.ts @@ -0,0 +1,16 @@ +import { NextResponse } from 'next/server' +import { prisma } from '@/libs/prisma' + +export async function GET() { + // @ts-ignore + const roofMaterialCategory = await prisma.MS_SUITABLE.findMany({ + select: { + roof_material: true, + }, + distinct: ['roof_material'], + orderBy: { + roof_material: 'asc', + }, + }) + return NextResponse.json(roofMaterialCategory) +} diff --git a/src/app/api/suitable/list/route.ts b/src/app/api/suitable/list/route.ts index e1a44cb..22c3d8a 100644 --- a/src/app/api/suitable/list/route.ts +++ b/src/app/api/suitable/list/route.ts @@ -1,8 +1,35 @@ -import { NextResponse } from 'next/server' +import { NextRequest, NextResponse } from 'next/server' import { prisma } from '@/libs/prisma' -export async function GET() { - // @ts-ignore - const suitables = await prisma.MS_SUITABLE.findMany() - return NextResponse.json(suitables) + +export async function GET(request: NextRequest) { + try { + const searchParams = request.nextUrl.searchParams + const category = searchParams.get('category') + const keyword = searchParams.get('keyword') + + let whereCondition: any = {} + if (category) { + whereCondition['roof_material'] = category + } + if (keyword) { + whereCondition['product_name'] = { + contains: keyword, + } + } + console.log('πŸš€ ~ /api/suitable/list: ~ prisma where condition:', whereCondition) + + // @ts-ignore + const suitables = await prisma.MS_SUITABLE.findMany({ + where: whereCondition, + orderBy: { + product_name: 'asc', + }, + }) + + return NextResponse.json(suitables) + } catch (error) { + console.error('❌ 데이터 쑰회 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€:', error) + return NextResponse.json({ error: '데이터 쑰회 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€' }, { status: 500 }) + } } diff --git a/src/components/Suitable.tsx b/src/components/Suitable.tsx index f5766f1..1af6d86 100644 --- a/src/components/Suitable.tsx +++ b/src/components/Suitable.tsx @@ -1,16 +1,23 @@ 'use client' -import { suitableApi } from '@/api/suitable' +import { useSuitableStore } from '@/store/useSuitableStore' import { useQuery } from '@tanstack/react-query' import { useRouter } from 'next/navigation' +import type { Suitable as SuitableType } from '@/api/suitable' export default function Suitable() { const router = useRouter() - const { data, error, isPending } = useQuery({ - queryKey: ['suitable-list'], - queryFn: suitableApi.getList, + const { selectedItems, addSelectedItem } = useSuitableStore() + + const { data: suitableList, isLoading } = useQuery({ + queryKey: ['suitables', 'search'], + enabled: false, }) + if (isLoading || !suitableList) { + return
Loading...
+ } + return ( <>

Suitable

@@ -22,9 +29,23 @@ export default function Suitable() { > HOME - {error &&
Error: {error.message}
} - {isPending &&
Loading...
} - {data && data.map((item) =>
{item.product_name}
)} +
+

μ„ νƒλœ μ•„μ΄ν…œ

+ {selectedItems.length > 0 ? selectedItems.map((item) =>
{item.product_name}
) :
μ„ νƒλœ μ•„μ΄ν…œμ΄ μ—†μŠ΅λ‹ˆλ‹€.
} +
+
+
+

데이터 λͺ©λ‘

+ {suitableList ? ( + suitableList.map((item: SuitableType) => ( +
addSelectedItem(item)}> + {item.product_name} +
+ )) + ) : ( +
검색 κ²°κ³Όκ°€ μ—†μŠ΅λ‹ˆλ‹€.
+ )} +
) } diff --git a/src/components/SuitableSearch.tsx b/src/components/SuitableSearch.tsx index 9583ede..e339f80 100644 --- a/src/components/SuitableSearch.tsx +++ b/src/components/SuitableSearch.tsx @@ -1,21 +1,85 @@ 'use client' -import { useState } from 'react' +import { useEffect, useState } from 'react' import { useRouter } from 'next/navigation' +import { useQuery } from '@tanstack/react-query' +import { useSuitable } from '@/hooks/useSuitable' export default function SuitableSearch() { const router = useRouter() - const [selectedValue, setSelectedValue] = useState('') + const [selectedCategory, setSelectedCategory] = useState('') + const [searchValue, setSearchValue] = useState('') + const { getCategories, getSuitables } = useSuitable() + + const { data: categories } = useQuery({ + queryKey: ['categories'], + queryFn: getCategories, + staleTime: 1000 * 60 * 60, // 60λΆ„ + }) + + const { data: suitableList } = useQuery({ + queryKey: ['suitables', 'list'], + queryFn: async () => await getSuitables(), + staleTime: 1000 * 60 * 10, // 10λΆ„ + gcTime: 1000 * 60 * 10, // 10λΆ„ + }) + + const { data: suitableSearch, refetch: refetchBySearch } = useQuery({ + queryKey: ['suitables', 'search'], + queryFn: async () => { + // apiλ₯Ό ν˜ΈμΆœν•˜λŠ” 검색 방법 + // return await updateSearchResults(selectedCategory || undefined, searchValue || undefined) + // useQuery μΊμ‹œ 데이터λ₯Ό μ‚¬μš©ν•˜λŠ” 검색 방법 + return ( + suitableList?.filter((item) => { + const categoryMatch = !selectedCategory || item.roof_material === selectedCategory + const searchMatch = !searchValue || item.product_name.includes(searchValue) + return categoryMatch && searchMatch + }) ?? [] + ) + }, + staleTime: 1000 * 60 * 10, + gcTime: 1000 * 60 * 10, + enabled: false, + }) + + // 초기 데이터 λ‘œλ”©λœ ν›„ 검색 κ²°κ³Ό μ—†μœΌλ©΄ 검색 κ²°κ³Ό λ‘œλ”© + useEffect(() => { + if (suitableList && (!suitableSearch || suitableSearch.length === 0)) { + refetchBySearch() + } + }, [suitableList, suitableSearch]) + + // μΉ΄ν…Œκ³ λ¦¬ 선택 μ‹œ 검색 κ²°κ³Ό λ‘œλ”© + useEffect(() => { + refetchBySearch() + }, [selectedCategory]) + + const handleSearch = async () => { + if (!searchValue.trim()) { + alert('검색어λ₯Ό μž…λ ₯ν•˜μ„Έμš”.') + return + } + refetchBySearch() + } return ( <> - - +
+ + +
+
+ setSearchValue(e.target.value)} /> + +
) } diff --git a/src/hooks/useSuitable.ts b/src/hooks/useSuitable.ts new file mode 100644 index 0000000..62f00e2 --- /dev/null +++ b/src/hooks/useSuitable.ts @@ -0,0 +1,30 @@ +import { suitableApi } from '@/api/suitable' + +export function useSuitable() { + const getCategories = async () => { + try { + return await suitableApi.getCategory() + } catch (error) { + console.error('μΉ΄ν…Œκ³ λ¦¬ 데이터 λ‘œλ“œ μ‹€νŒ¨:', error) + return [] + } + } + + const getSuitables = async () => { + try { + return await suitableApi.getList() + } catch (error) { + console.error('μ§€λΆ•μž¬ 데이터 λ‘œλ“œ μ‹€νŒ¨:', error) + } + } + + const updateSearchResults = async (selectedCategory: string | undefined, searchValue: string | undefined) => { + try { + return await suitableApi.getList(selectedCategory, searchValue) + } catch (error) { + console.error('μ§€λΆ•μž¬ 데이터 검색 μ‹€νŒ¨:', error) + } + } + + return { getCategories, getSuitables, updateSearchResults } +} diff --git a/src/store/useSuitableStore.ts b/src/store/useSuitableStore.ts new file mode 100644 index 0000000..17b88c5 --- /dev/null +++ b/src/store/useSuitableStore.ts @@ -0,0 +1,68 @@ +import { create } from 'zustand' +import { Suitable, suitableApi } from '@/api/suitable' + +interface SuitableState { + // // 검색 κ²°κ³Ό 리슀트 + // searchResults: Suitable[] + // // 초기 데이터 λ‘œλ“œ + // fetchInitializeData: () => Promise + // // 검색 κ²°κ³Ό μ„€μ • + // setSearchResults: (results: Suitable[]) => void + // // 검색 κ²°κ³Ό μ΄ˆκΈ°ν™” + // resetSearchResults: () => void + + // μ„ νƒλœ μ•„μ΄ν…œ 리슀트 + selectedItems: Suitable[] + // μ„ νƒλœ μ•„μ΄ν…œ μΆ”κ°€ + addSelectedItem: (item: Suitable) => void + // μ„ νƒλœ μ•„μ΄ν…œ 제거 + removeSelectedItem: (itemId: number) => void + // μ„ νƒλœ μ•„μ΄ν…œ λͺ¨λ‘ 제거 + clearSelectedItems: () => void +} + +export const useSuitableStore = create((set) => ({ + // // 초기 μƒνƒœ + // searchResults: [], + + // // 초기 데이터 λ‘œλ“œ + // fetchInitializeData: async () => { + // const suitables = await fetchInitialSuitablee() + // set({ searchResults: suitables }) + // }, + + // // 검색 κ²°κ³Ό μ„€μ • + // setSearchResults: (results) => set({ searchResults: results }), + + // // 검색 κ²°κ³Ό μ΄ˆκΈ°ν™” + // resetSearchResults: () => set({ searchResults: [] }), + + // 초기 μƒνƒœ + selectedItems: [], + + // μ„ νƒλœ μ•„μ΄ν…œ μΆ”κ°€ (쀑볡 λ°©μ§€) + addSelectedItem: (item) => + set((state) => ({ + selectedItems: state.selectedItems.some((i) => i.id === item.id) ? state.selectedItems : [...state.selectedItems, item], + })), + + // μ„ νƒλœ μ•„μ΄ν…œ 제거 + removeSelectedItem: (itemId) => + set((state) => ({ + selectedItems: state.selectedItems.filter((item) => item.id !== itemId), + })), + + // μ„ νƒλœ μ•„μ΄ν…œ λͺ¨λ‘ 제거 + clearSelectedItems: () => set({ selectedItems: [] }), +})) + +// // 전체 데이터 μ΄ˆκΈ°ν™” ν•¨μˆ˜ +// async function fetchInitialSuitablee() { +// try { +// const suitable = await suitableApi.getList() +// return suitable +// } catch (error) { +// console.error('초기 데이터 λ‘œλ“œ μ‹€νŒ¨:', error) +// return [] +// } +// }