From 05e56833de8cf3310a344935e6c4257e101ea70d Mon Sep 17 00:00:00 2001 From: knoj014 Date: Sun, 19 Jan 2025 23:39:45 +0900 Subject: [PATCH 1/3] =?UTF-8?q?=EA=B2=80=EC=83=89=20=EA=B8=B0=EB=B3=B8=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 검색 api 연결해서 검색 결과 json을 띄우기만 함. 이제 검색 결과 json을 잘 예쁘게 보여주기만 하면 됨. --- src/App.tsx | 2 + src/assets/no_result.svg | 10 +++ src/components/SearchResultBlock.tsx | 79 +++++++++++++++++++ src/pages/Search.tsx | 109 +++++++++++++++++++++++++++ src/utils/Types.ts | 7 ++ 5 files changed, 207 insertions(+) create mode 100644 src/assets/no_result.svg create mode 100644 src/components/SearchResultBlock.tsx create mode 100644 src/pages/Search.tsx diff --git a/src/App.tsx b/src/App.tsx index 9894621..5cae8b7 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -14,6 +14,7 @@ import { Settings } from './pages/mypage/Settings'; import { News } from './pages/News'; import PeoplePage from './pages/PeoplePage'; import { Rating } from './pages/Rating'; +import { Search } from './pages/Search'; import { Signup } from './pages/Signup'; export const App = () => { @@ -33,6 +34,7 @@ export const App = () => { } /> } /> } /> + } /> diff --git a/src/assets/no_result.svg b/src/assets/no_result.svg new file mode 100644 index 0000000..def093a --- /dev/null +++ b/src/assets/no_result.svg @@ -0,0 +1,10 @@ + + + + + + + + \ No newline at end of file diff --git a/src/components/SearchResultBlock.tsx b/src/components/SearchResultBlock.tsx new file mode 100644 index 0000000..552714e --- /dev/null +++ b/src/components/SearchResultBlock.tsx @@ -0,0 +1,79 @@ +import NoResultSvg from '../assets/no_result.svg'; +import type { searchResult } from '../utils/Types'; + +type Category = '영화' | '인물' | '컬렉션' | '유저'; + +interface SearchResultBlockProps { + searchResults: searchResult; + selectedCategory: Category; + setSelectedCategory: (category: Category) => void; +} + +export const SearchResultBlock = ({ searchResults, selectedCategory, setSelectedCategory }: SearchResultBlockProps) => { + const categories: Category[] = ['영화', '인물', '컬렉션', '유저']; + + const getCategoryContent = () => { + const categoryMap = { + '영화': { + list: searchResults.movie_list, + render: (id: number) =>
  • Movie ID: {id}
  • + }, + '인물': { + list: searchResults.participant_list, + render: (id: number) =>
  • Participant ID: {id}
  • + }, + '컬렉션': { + list: searchResults.collection_list, + render: (id: number) =>
  • Collection ID: {id}
  • + }, + '유저': { + list: searchResults.user_list, + render: (id: number) =>
  • User ID: {id}
  • + } + }; + + const { list, render } = categoryMap[selectedCategory]; + + if (list.length === 0) { + return ( +
    + 검색 결과 없음 +

    검색 결과가 없어요. 다른 검색어를 입력해보세요.

    +
    + ); + } + + return ( +
      + {list.map(render)} +
    + ); + }; + + return ( + <> +
    + {categories.map((category) => ( + + ))} +
    +
    + {getCategoryContent()} +
    + + ); +}; diff --git a/src/pages/Search.tsx b/src/pages/Search.tsx new file mode 100644 index 0000000..78a7ce7 --- /dev/null +++ b/src/pages/Search.tsx @@ -0,0 +1,109 @@ +import { useEffect, useState } from 'react'; +import { useNavigate, useSearchParams } from 'react-router-dom'; + +import search from '../assets/search.svg'; +import { Footerbar } from '../components/Footerbar'; +import { SearchResultBlock } from '../components/SearchResultBlock'; +import type { searchResult } from '../utils/Types'; + +type Category = '영화' | '인물' | '컬렉션' | '유저'; + +export const Search = () => { + const navigate = useNavigate(); + const [searchParams] = useSearchParams(); + const [searchText, setSearchText] = useState(''); + const [searchResults, setSearchResults] = useState(null); + const [error, setError] = useState(null); + const [selectedCategory, setSelectedCategory] = useState('영화'); + + + useEffect(() => { + const query = searchParams.get('query'); + if (query != null) { + setSearchText(query); + void performSearch(query); + } + }, [searchParams]); + + const handleClear = () => { + setSearchText(''); + setSearchResults(null); + }; + + const performSearch = async (query: string) => { + setError(null); + try { + const response = await fetch(`/api/search?search_q=${encodeURIComponent(query)}`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }); + + if (!response.ok) { + throw new Error('Failed to fetch search results'); + } + + const data = await response.json() as searchResult; + setSearchResults(data); + } catch (err) { + setError((err as Error).message); + console.error('Search error:', err); + } + }; + + const handleSearch = () => { + if (searchText.trim().length > 0) { + void navigate(`/search?query=${encodeURIComponent(searchText.trim())}`); + } + }; + + return ( +
    +
    +
    + { setSearchText(e.target.value); }} + placeholder="콘텐츠, 인물, 컬렉션, 유저를 검색하세요" + className="w-full px-4 py-2 pl-12 pr-10 bg-gray-100 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500" + onKeyDown={(e) => { + if (e.key === 'Enter') handleSearch(); + }} + /> + search { handleSearch(); }} + /> + {(searchText !== "") && ( + + )} +
    + {error !== null && ( +
    {error}
    + )} +
    +
    + {(searchResults != null) && ( + + )} +
    + +
    + +
    +
    + ); +}; diff --git a/src/utils/Types.ts b/src/utils/Types.ts index ecf1f61..1a0b4ac 100644 --- a/src/utils/Types.ts +++ b/src/utils/Types.ts @@ -64,3 +64,10 @@ export type PeopleMovieCreditResponse = { role: string; movies: PeopleMovieCredit[]; }; + +export type searchResult = { + movie_list: Array; + user_list: Array; + participant_list: Array; + collection_list: Array; +}; \ No newline at end of file From c0364fc620bf218c6d12e54a868927efede9bb70 Mon Sep 17 00:00:00 2001 From: knoj014 Date: Mon, 20 Jan 2025 01:43:34 +0900 Subject: [PATCH 2/3] =?UTF-8?q?=EA=B2=80=EC=83=89=20=EA=B2=B0=EA=B3=BC=20?= =?UTF-8?q?=ED=91=9C=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 검색 결과를 잘 표시함 영화: 영화 페이지로 이동 인물: 인물 페이지로 이동 컬렉션: 컬렉션 페이지(구현 안 됨, /collection/id)로 이동 유저: 유저 페이지(구현 안 됨, /user/login_id)로 이동 --- src/components/SearchResultBlock.tsx | 191 +++++++++++++++++++++------ src/pages/Search.tsx | 28 ++-- src/utils/Functions.ts | 83 ++++++++++++ src/utils/Types.ts | 25 +++- 4 files changed, 276 insertions(+), 51 deletions(-) create mode 100644 src/utils/Functions.ts diff --git a/src/components/SearchResultBlock.tsx b/src/components/SearchResultBlock.tsx index 552714e..fd32d47 100644 --- a/src/components/SearchResultBlock.tsx +++ b/src/components/SearchResultBlock.tsx @@ -1,5 +1,9 @@ +import { useEffect, useState } from 'react'; + +import noProfile from '../assets/no_profile.svg'; import NoResultSvg from '../assets/no_result.svg'; -import type { searchResult } from '../utils/Types'; +import { fetchCollection, fetchMovie, fetchPeople, fetchUser } from '../utils/Functions'; +import type { Collection, Movie, People, searchResult, UserProfile } from '../utils/Types'; type Category = '영화' | '인물' | '컬렉션' | '유저'; @@ -9,48 +13,157 @@ interface SearchResultBlockProps { setSelectedCategory: (category: Category) => void; } -export const SearchResultBlock = ({ searchResults, selectedCategory, setSelectedCategory }: SearchResultBlockProps) => { - const categories: Category[] = ['영화', '인물', '컬렉션', '유저']; +export const SearchResultBlock = ({ + searchResults, + selectedCategory, + setSelectedCategory, +}: SearchResultBlockProps) => { + const [movieDetails, setMovieDetails] = useState([]); + const [peopleDetails, setPeopleDetails] = useState([]); + const [collectionDetails, setCollectionDetails] = useState([]); + const [userDetails, setUserDetails] = useState([]); + const [error, setError] = useState(null); - const getCategoryContent = () => { - const categoryMap = { - '영화': { - list: searchResults.movie_list, - render: (id: number) =>
  • Movie ID: {id}
  • - }, - '인물': { - list: searchResults.participant_list, - render: (id: number) =>
  • Participant ID: {id}
  • - }, - '컬렉션': { - list: searchResults.collection_list, - render: (id: number) =>
  • Collection ID: {id}
  • - }, - '유저': { - list: searchResults.user_list, - render: (id: number) =>
  • User ID: {id}
  • + const categories: Category[] = ['영화', '인물', '컬렉션', '유저']; + useEffect(() => { + const fetchSearchResults = async () => { + try { + const _movieDetails = await Promise.all( + searchResults.movie_list.map((id) => fetchMovie(id)), + ); + setMovieDetails( + _movieDetails.filter((detail): detail is Movie => detail !== null), + ); + const _peopleDetails = await Promise.all( + searchResults.participant_list.map((id) => fetchPeople(id)), + ); + setPeopleDetails( + _peopleDetails.filter((detail): detail is People => detail !== null), + ); + const _collectionDetails = await Promise.all( + searchResults.collection_list.map((id) => fetchCollection(id)), + ); + setCollectionDetails( + _collectionDetails.filter((detail): detail is Collection => detail !== null), + ); + const _userDetails = await Promise.all( + searchResults.user_list.map((id) => fetchUser(id)), + ); + setUserDetails( + _userDetails.filter((detail): detail is UserProfile => detail !== null), + ); + console.debug(_userDetails); + } catch (err) { + setError((err as Error).message); } }; - const { list, render } = categoryMap[selectedCategory]; - - if (list.length === 0) { + void fetchSearchResults(); + }, [searchResults]); + + const getCategoryContent = () => { + if (error != null) { + return

    Error: {error}

    ; + } + + if (selectedCategory === '영화' && movieDetails.length > 0) { return ( -
    - 검색 결과 없음 -

    검색 결과가 없어요. 다른 검색어를 입력해보세요.

    -
    + ); } - + if (selectedCategory === '인물' && peopleDetails.length > 0) { + return ( + + ); + } + if (selectedCategory === '컬렉션' && collectionDetails.length > 0) { + return ( + + ); + } + if (selectedCategory === '유저' && userDetails.length > 0) { + return ( + + ); + } return ( -
      - {list.map(render)} -
    +
    + 검색 결과 없음 +

    + 검색 결과가 없어요. 다른 검색어를 입력해보세요. +

    +
    ); }; @@ -60,7 +173,9 @@ export const SearchResultBlock = ({ searchResults, selectedCategory, setSelected {categories.map((category) => ( ))} -
    - {getCategoryContent()} -
    +
    {getCategoryContent()}
    ); }; diff --git a/src/pages/Search.tsx b/src/pages/Search.tsx index 78a7ce7..b3cf280 100644 --- a/src/pages/Search.tsx +++ b/src/pages/Search.tsx @@ -16,7 +16,6 @@ export const Search = () => { const [error, setError] = useState(null); const [selectedCategory, setSelectedCategory] = useState('영화'); - useEffect(() => { const query = searchParams.get('query'); if (query != null) { @@ -33,18 +32,21 @@ export const Search = () => { const performSearch = async (query: string) => { setError(null); try { - const response = await fetch(`/api/search?search_q=${encodeURIComponent(query)}`, { - method: 'GET', - headers: { - 'Content-Type': 'application/json', + const response = await fetch( + `/api/search?search_q=${encodeURIComponent(query)}`, + { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, }, - }); + ); if (!response.ok) { throw new Error('Failed to fetch search results'); } - const data = await response.json() as searchResult; + const data = (await response.json()) as searchResult; setSearchResults(data); } catch (err) { setError((err as Error).message); @@ -65,7 +67,9 @@ export const Search = () => { { setSearchText(e.target.value); }} + onChange={(e) => { + setSearchText(e.target.value); + }} placeholder="콘텐츠, 인물, 컬렉션, 유저를 검색하세요" className="w-full px-4 py-2 pl-12 pr-10 bg-gray-100 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500" onKeyDown={(e) => { @@ -76,9 +80,11 @@ export const Search = () => { src={search} alt="search" className="absolute left-4 top-1/2 transform -translate-y-1/2 w-5 h-5 opacity-50 cursor-pointer" - onClick={() => { handleSearch(); }} + onClick={() => { + handleSearch(); + }} /> - {(searchText !== "") && ( + {searchText !== '' && ( ))} diff --git a/src/pages/Search.tsx b/src/pages/Search.tsx index b3cf280..3db4914 100644 --- a/src/pages/Search.tsx +++ b/src/pages/Search.tsx @@ -4,20 +4,29 @@ import { useNavigate, useSearchParams } from 'react-router-dom'; import search from '../assets/search.svg'; import { Footerbar } from '../components/Footerbar'; import { SearchResultBlock } from '../components/SearchResultBlock'; -import type { searchResult } from '../utils/Types'; - -type Category = '영화' | '인물' | '컬렉션' | '유저'; +import type { SearchCategory, SearchResult } from '../utils/Types'; export const Search = () => { const navigate = useNavigate(); const [searchParams] = useSearchParams(); const [searchText, setSearchText] = useState(''); - const [searchResults, setSearchResults] = useState(null); + const [searchResults, setSearchResults] = useState(null); const [error, setError] = useState(null); - const [selectedCategory, setSelectedCategory] = useState('영화'); + const [selectedCategory, setSelectedCategory] = + useState('movie'); useEffect(() => { const query = searchParams.get('query'); + const category = searchParams.get('category') as SearchCategory; + console.debug(category); + + if ( + category.length > 0 && + ['movie', 'person', 'collection', 'user'].includes(category) + ) { + setSelectedCategory(category); + } + if (query != null) { setSearchText(query); void performSearch(query); @@ -46,7 +55,7 @@ export const Search = () => { throw new Error('Failed to fetch search results'); } - const data = (await response.json()) as searchResult; + const data = (await response.json()) as SearchResult; setSearchResults(data); } catch (err) { setError((err as Error).message); @@ -56,7 +65,10 @@ export const Search = () => { const handleSearch = () => { if (searchText.trim().length > 0) { - void navigate(`/search?query=${encodeURIComponent(searchText.trim())}`); + const searchQuery = `query=${encodeURIComponent(searchText.trim())}`; + const categoryQuery = + selectedCategory !== 'movie' ? `&category=${selectedCategory}` : ''; + void navigate(`/search?${searchQuery}${categoryQuery}`); } }; diff --git a/src/utils/Functions.ts b/src/utils/Functions.ts index f4d4ffb..8aabb7a 100644 --- a/src/utils/Functions.ts +++ b/src/utils/Functions.ts @@ -1,83 +1,80 @@ import type { Collection, Movie, People, UserProfile } from '../utils/Types'; export const fetchMovie = async (movieId: number) => { - try { - const response = await fetch(`/api/movies/${movieId}`, { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - }, - }); + try { + const response = await fetch(`/api/movies/${movieId}`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }); - if (!response.ok) { - throw new Error('Failed to fetch movie details'); - } - - return (await response.json()) as Movie; - } catch (err) { - console.error('Movie fetch error:', err); - return null; + if (!response.ok) { + throw new Error('Failed to fetch movie details'); } - }; - -export const fetchPeople = async (peopleId: number) => { - try { - const response = await fetch(`/api/participants/${peopleId}`, { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - }, - }); + return (await response.json()) as Movie; + } catch (err) { + console.error('Movie fetch error:', err); + return null; + } +}; - if (!response.ok) { - throw new Error('Failed to fetch people details'); - } +export const fetchPeople = async (peopleId: number) => { + try { + const response = await fetch(`/api/participants/${peopleId}`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }); - return (await response.json()) as People; - } catch (err) { - console.error('People fetch error:', err); - return null; + if (!response.ok) { + throw new Error('Failed to fetch people details'); } - }; - - -export const fetchCollection = async (collectionId: number) => { - try { - const response = await fetch(`/api/collections/${collectionId}`, { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - }, - }); - if (!response.ok) { - throw new Error('Failed to fetch collection details'); - } + return (await response.json()) as People; + } catch (err) { + console.error('People fetch error:', err); + return null; + } +}; - return (await response.json()) as Collection; - } catch (err) { - console.error('Collection fetch error:', err); - return null; +export const fetchCollection = async (collectionId: number) => { + try { + const response = await fetch(`/api/collections/${collectionId}`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }); + + if (!response.ok) { + throw new Error('Failed to fetch collection details'); } - }; - - export const fetchUser = async (userId: number) => { - try { - const response = await fetch(`/api/users/profile/${userId}`, { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - }, - }); - if (!response.ok) { - throw new Error('Failed to fetch user details'); - } - console.debug(response); - return (await response.json()) as UserProfile; - } catch (err) { - console.error('User fetch error:', err); - return null; + return (await response.json()) as Collection; + } catch (err) { + console.error('Collection fetch error:', err); + return null; + } +}; + +export const fetchUser = async (userId: number) => { + try { + const response = await fetch(`/api/users/profile/${userId}`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }); + + if (!response.ok) { + throw new Error('Failed to fetch user details'); } - }; \ No newline at end of file + return (await response.json()) as UserProfile; + } catch (err) { + console.error('User fetch error:', err); + return null; + } +}; diff --git a/src/utils/Types.ts b/src/utils/Types.ts index 3d46195..f0330aa 100644 --- a/src/utils/Types.ts +++ b/src/utils/Types.ts @@ -65,7 +65,7 @@ export type PeopleMovieCreditResponse = { movies: PeopleMovieCredit[]; }; -export type searchResult = { +export type SearchResult = { movie_list: Array; user_list: Array; participant_list: Array; @@ -81,7 +81,7 @@ export type Collection = { comments_count: number; created_at: string; movies: Movie[]; -} +}; export type UserProfile = { username: string; @@ -93,4 +93,6 @@ export type UserProfile = { review_count: number; comment_count: number; collection_count: number; -} \ No newline at end of file +}; + +export type SearchCategory = 'movie' | 'person' | 'collection' | 'user';