diff --git a/App.tsx b/App.tsx index 0abb01c..40550d2 100644 --- a/App.tsx +++ b/App.tsx @@ -1,12 +1,16 @@ -import * as React from "react"; import { NavigationContainer } from "@react-navigation/native"; import { useFonts } from "expo-font"; +import * as React from "react"; -import TabNavigation from "./src/components/navBar/TabNavigation"; -import OnboardingOverlay from "./src/components/libary/OnboardingOverlay"; import AsyncStorage from "@react-native-async-storage/async-storage"; +import OnboardingOverlay from "./src/components/libary/OnboardingOverlay"; +import TabNavigation from "./src/components/navBar/TabNavigation"; + +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; export default function App() { + const queryClient = new QueryClient(); + //처음 방문 시 온보딩 화면 실행 const [showOnboarding, setShowOnboarding] = React.useState(false); @@ -35,11 +39,13 @@ export default function App() { if (!fontsLoaded) return null; return ( - - - {showOnboarding && ( - - )} - + + + + {showOnboarding && ( + + )} + + ); } diff --git a/package-lock.json b/package-lock.json index c4c77cf..d41dc02 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@react-navigation/bottom-tabs": "^6.6.0", "@react-navigation/native": "^6.1.17", "@react-navigation/native-stack": "^6.10.0", + "@tanstack/react-query": "^5.64.2", "axios": "^1.7.9", "expo": "~51.0.24", "expo-camera": "~15.0.14", @@ -6958,6 +6959,32 @@ "node": ">=4" } }, + "node_modules/@tanstack/query-core": { + "version": "5.64.2", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.64.2.tgz", + "integrity": "sha512-hdO8SZpWXoADNTWXV9We8CwTkXU88OVWRBcsiFrk7xJQnhm6WRlweDzMD+uH+GnuieTBVSML6xFa17C2cNV8+g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.64.2", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.64.2.tgz", + "integrity": "sha512-3pakNscZNm8KJkxmovvtZ4RaXLyiYYobwleTMvpIGUoKRa8j8VlrQKNl5W8VUEfVfZKkikvXVddLuWMbcSCA1Q==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.64.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, "node_modules/@trysound/sax": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", diff --git a/package.json b/package.json index e37385a..45a7216 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "@react-navigation/bottom-tabs": "^6.6.0", "@react-navigation/native": "^6.1.17", "@react-navigation/native-stack": "^6.10.0", + "@tanstack/react-query": "^5.64.2", "axios": "^1.7.9", "expo": "~51.0.24", "expo-camera": "~15.0.14", diff --git a/src/api/book/deleteKeyword.ts b/src/api/book/deleteKeyword.ts new file mode 100644 index 0000000..b5b4269 --- /dev/null +++ b/src/api/book/deleteKeyword.ts @@ -0,0 +1,7 @@ +import api from ".."; + +//최근 검색어 삭제 +export const deleteKeyword = async (keywordId: number): Promise => { + const response = await api.delete(`/api/v1/book/keyword/${keywordId}`); + return response.data; +}; diff --git a/src/api/book/getBestSeller.ts b/src/api/book/getBestSeller.ts new file mode 100644 index 0000000..819ccae --- /dev/null +++ b/src/api/book/getBestSeller.ts @@ -0,0 +1,22 @@ +import api from ".."; +import { RequestBestSeller } from "../../types/search/bestbook"; +import { ResponseBookSearchResult } from "../../types/search/search"; + +// 베스트셀러 조회 +export const getBestSeller = async ({ + category = 0, + size = 12, +}: RequestBestSeller): Promise => { + try { + const response = await api.get(`/api/v1/book/best-seller`, { + params: { + category, + size, + }, + }); + return response.data; + } catch (error) { + console.error("Error fetching best sellers:", error); + return undefined; + } +}; diff --git a/src/api/book/getKeyword.ts b/src/api/book/getKeyword.ts new file mode 100644 index 0000000..9adda3f --- /dev/null +++ b/src/api/book/getKeyword.ts @@ -0,0 +1,15 @@ +import api from ".."; +import { ResponseResentSearch } from "../../types/search/resentSearch"; + +//최근 검색어 조회 +export const getKeyword = async (): Promise< + ResponseResentSearch | undefined +> => { + try { + const response = await api.get(`/api/v1/book/keyword`); + return response.data; + } catch (e) { + console.log(e); + return undefined; + } +}; diff --git a/src/api/book/getSearch.ts b/src/api/book/getSearch.ts new file mode 100644 index 0000000..375f7d2 --- /dev/null +++ b/src/api/book/getSearch.ts @@ -0,0 +1,15 @@ +import api from ".."; +import { ResponseBookSearchResult } from "../../types/search/search"; + +// 검색 조회 +export const getSearch = async ( + keyword: string +): Promise => { + try { + const response = await api.get(`api/v1/book/search?keyword=${keyword}`); + return response.data; + } catch (e) { + console.log(e); + return undefined; + } +}; diff --git a/src/assets/data/dummyRecentSearchList.ts b/src/assets/data/dummyRecentSearchList.ts deleted file mode 100644 index 2da144f..0000000 --- a/src/assets/data/dummyRecentSearchList.ts +++ /dev/null @@ -1,7 +0,0 @@ -export const dummyRecentSearchList = [ - "난", - "사실", - "책 같은 건", - "안 읽습니다.", - "ㅋㅋㅋㅋ", -]; diff --git a/src/components/search/BestSellerBook.tsx b/src/components/search/BestSellerBook.tsx index 5be058e..b2f24c1 100644 --- a/src/components/search/BestSellerBook.tsx +++ b/src/components/search/BestSellerBook.tsx @@ -1,7 +1,7 @@ import React from "react"; -import { Text, View, Image } from "react-native"; -import { BestBook } from "../../types/search/bestbook"; +import { Image, Text, View } from "react-native"; import { styles } from "../../styles/search/BestSellerBookStyle"; +import { BestBook } from "../../types/search/bestbook"; const BestSellerBook = ({ id, title, image, name }: BestBook) => { return ( @@ -10,7 +10,7 @@ const BestSellerBook = ({ id, title, image, name }: BestBook) => { {id} - + {title} diff --git a/src/components/search/BookCollection.tsx b/src/components/search/BookCollection.tsx index faece37..ab7dd79 100644 --- a/src/components/search/BookCollection.tsx +++ b/src/components/search/BookCollection.tsx @@ -1,25 +1,28 @@ import { ScrollView, View } from "react-native"; -import BestSellerBook from "./BestSellerBook"; -import { dummyList } from "../../assets/data/dummyBestBookList"; +import { useBestSeller } from "../../hooks/book/useBestSeller"; import { styles } from "../../styles/search/BestSellerStyle"; +import BestSellerBook from "./BestSellerBook"; const BookCollection = () => { + const { data } = useBestSeller({}); + const bestSellerList = data.item; + // 3*4 배열을 만들기 위해 3개씩 푸시함. const groupedBooks = []; - for (let i = 0; i < dummyList.length; i += 3) { - groupedBooks.push(dummyList.slice(i, i + 3)); + for (let i = 0; i < bestSellerList.length; i += 3) { + groupedBooks.push(bestSellerList.slice(i, i + 3)); } return ( - {groupedBooks.map((group, index) => ( - - {group.map((book) => ( + {groupedBooks.map((group, groupIndex) => ( + + {group.map((book, index) => ( ))} diff --git a/src/components/search/RecentSearch.tsx b/src/components/search/RecentSearch.tsx index a4267d9..0feebb0 100644 --- a/src/components/search/RecentSearch.tsx +++ b/src/components/search/RecentSearch.tsx @@ -1,16 +1,36 @@ -import React, { useState } from "react"; -import { View, Text, ScrollView } from "react-native"; +import { useFocusEffect } from "@react-navigation/native"; +import React, { useCallback } from "react"; +import { ScrollView, Text, View } from "react-native"; import { RecentSearchText } from "../../constans/search"; +import { useDeleteKeyword, useGetKeyword } from "../../hooks/book/useKeyword"; import { styles } from "../../styles/search/RecentSearchStyle"; -import { dummyRecentSearchList } from "../../assets/data/dummyRecentSearchList"; import RecentSearchCard from "./RecentSearchCard"; const RecentSearch = () => { - const [searchList, setSearchList] = useState(dummyRecentSearchList); + const { data, refetch } = useGetKeyword(); + const searchList = data.information; - // 같은 이름이 있을 경우 delete함수가 안 먹히는데 이는 어처피 api호출로 해결될 문제로 보임. - const handleDeleteCard = (text: string) => { - setSearchList(searchList.filter((item) => item !== text)); + const { mutate } = useDeleteKeyword(); + + useFocusEffect( + useCallback(() => { + refetch(); + }, []) + ); + + const handleDeleteKeyword = (keywordId: number) => { + mutate( + { keywordId }, + { + onSuccess: (data) => { + console.log("Success"); + refetch(); + }, + onError: (error) => { + console.log("Error", error.message); + }, + } + ); }; return ( @@ -23,9 +43,9 @@ const RecentSearch = () => { > {searchList.map((search) => ( handleDeleteKeyword(search.keywordId)} /> ))} diff --git a/src/components/search/ResultBookCard.tsx b/src/components/search/ResultBookCard.tsx index 1eaddef..0f340e6 100644 --- a/src/components/search/ResultBookCard.tsx +++ b/src/components/search/ResultBookCard.tsx @@ -1,6 +1,6 @@ -import { View, Image, Text } from "react-native"; -import { ResultBookProp } from "../../types/search"; +import { Image, Text, View } from "react-native"; import { styles } from "../../styles/search/ResultBookCardStyle"; +import { ResultBookProp } from "../../types/search"; const ResultBookCard = ({ image, title, @@ -9,7 +9,7 @@ const ResultBookCard = ({ }: ResultBookProp) => { return ( - + {title} diff --git a/src/hooks/book/useBestSeller.ts b/src/hooks/book/useBestSeller.ts new file mode 100644 index 0000000..5b66fc0 --- /dev/null +++ b/src/hooks/book/useBestSeller.ts @@ -0,0 +1,17 @@ +import { + useSuspenseQuery, + UseSuspenseQueryResult, +} from "@tanstack/react-query"; +import { ResponseBookSearchResult } from "../../types/search/bestbook"; +import { getBestSeller } from "./../../api/book/getBestSeller"; + +// 베스트 셀러 조회 +export function useBestSeller({ + category = 0, + size = 12, +}): UseSuspenseQueryResult { + return useSuspenseQuery({ + queryKey: ["GetBestSeller"], + queryFn: () => getBestSeller({ category, size }), + }); +} diff --git a/src/hooks/book/useKeyword.ts b/src/hooks/book/useKeyword.ts new file mode 100644 index 0000000..8c5db4d --- /dev/null +++ b/src/hooks/book/useKeyword.ts @@ -0,0 +1,26 @@ +import { + useMutation, + useSuspenseQuery, + UseSuspenseQueryResult, +} from "@tanstack/react-query"; +import { deleteKeyword } from "../../api/book/deleteKeyword"; +import { getKeyword } from "../../api/book/getKeyword"; +import { ResponseResentSearch } from "../../types/search/resentSearch"; + +//최근 검색어 조회 +export function useGetKeyword(): UseSuspenseQueryResult< + ResponseResentSearch, + Error +> { + return useSuspenseQuery({ + queryKey: ["GetResentKeyword"], + queryFn: () => getKeyword(), + }); +} + +// 최근 검색어 삭제 +export function useDeleteKeyword() { + return useMutation<{}, Error, { keywordId: number }>({ + mutationFn: ({ keywordId }) => deleteKeyword(keywordId), + }); +} diff --git a/src/hooks/book/useSearch.ts b/src/hooks/book/useSearch.ts new file mode 100644 index 0000000..27cb08e --- /dev/null +++ b/src/hooks/book/useSearch.ts @@ -0,0 +1,16 @@ +import { + useSuspenseQuery, + UseSuspenseQueryResult, +} from "@tanstack/react-query"; +import { getSearch } from "../../api/book/getSearch"; +import { ResponseBookSearchResult } from "../../types/search/search"; + +//최근 검색어 조회 +export function useSearch( + keyword: string +): UseSuspenseQueryResult { + return useSuspenseQuery({ + queryKey: ["GetSearch"], + queryFn: () => getSearch(keyword), + }); +} diff --git a/src/pages/searchPage/SearchResultPage.tsx b/src/pages/searchPage/SearchResultPage.tsx index fa64f36..73aa05e 100644 --- a/src/pages/searchPage/SearchResultPage.tsx +++ b/src/pages/searchPage/SearchResultPage.tsx @@ -1,8 +1,8 @@ import React from "react"; import { ScrollView, View } from "react-native"; -import { dummyList } from "../../assets/data/dummyBestBookList"; import ResultBookCard from "../../components/search/ResultBookCard"; import SearchHeader from "../../components/search/SearchHeader"; +import { useSearch } from "../../hooks/book/useSearch"; import { styles } from "../../styles/search/SearchResultPageStyle"; import { BestSellerPageRouteProp } from "../../types/navigation/navigation"; @@ -12,19 +12,20 @@ type Props = { const SearchResultPage: React.FC = ({ route }) => { const { query } = route.params; - + const { data } = useSearch(query); + const searchList = data.item; return ( - {dummyList.map((book) => ( + {searchList.map((book, index) => ( ))} diff --git a/src/styles/search/BestSellerStyle.ts b/src/styles/search/BestSellerStyle.ts index 8f59cf5..8d1595c 100644 --- a/src/styles/search/BestSellerStyle.ts +++ b/src/styles/search/BestSellerStyle.ts @@ -23,6 +23,6 @@ export const styles = StyleSheet.create({ marginVertical: 10, }, bestSellerContainer: { - marginBottom: 20, + marginBottom: 100, }, }); diff --git a/src/types/search/bestbook.ts b/src/types/search/bestbook.ts index b29db62..ef74e63 100644 --- a/src/types/search/bestbook.ts +++ b/src/types/search/bestbook.ts @@ -19,3 +19,26 @@ export interface BestSellerKeywordProp { onFocus: (text: string) => void; checked: boolean; } + +// 베스트 셀러 요청 props +export interface RequestBestSeller { + category?: number; + size?: number; +} + +// 각 책 항목에 대한 타입 +interface BookItem { + title: string; // 책 제목 + author: string; // 저자 + isbn13: string; // ISBN13 + cover: string; // 책 표지 이미지 URL + bestRank: number; // 베스트 순위 +} + +// 전체 응답 타입 +export interface ResponseBookSearchResult { + totalResults: number; // 총 결과 수 + startIndex: number; // 시작 인덱스 + itemsPerPage: number; // 한 페이지당 항목 수 + item: BookItem[]; // 책 항목 배열 +} diff --git a/src/types/search/resentSearch.ts b/src/types/search/resentSearch.ts new file mode 100644 index 0000000..4710383 --- /dev/null +++ b/src/types/search/resentSearch.ts @@ -0,0 +1,9 @@ +interface ResentSearchInformation { + keywordId: number; // 키워드의 고유 ID + content: string; // 키워드 내용 +} + +export interface ResponseResentSearch { + check: boolean; // 체크 여부 + information: ResentSearchInformation[]; // 키워드 정보를 포함하는 배열 +} diff --git a/src/types/search/search.ts b/src/types/search/search.ts new file mode 100644 index 0000000..65da20f --- /dev/null +++ b/src/types/search/search.ts @@ -0,0 +1,17 @@ +// 단일 책 정보에 대한 타입 +interface BookItem { + title: string; // 책 제목 + author: string; // 저자 + pubDate: string; // 출판일 + isbn13: string; // ISBN13 번호 + cover: string; // 표지 이미지 URL + publisher: string; // 출판사 +} + +// 검색 결과 전체에 대한 타입 +export interface ResponseBookSearchResult { + totalResults: number; // 전체 검색 결과 수 + startIndex: number; // 시작 인덱스 + itemsPerPage: number; // 페이지당 아이템 수 + item: BookItem[]; // 책 정보 배열 +}