diff --git a/src/App.tsx b/src/App.tsx index a8f568c..7bdda37 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -3,15 +3,18 @@ import { RouterProvider } from 'react-router-dom'; import { queryClient } from './libs/queryClient'; import router from '@/routes/route'; import ToastProvider from './components/common/Toast/ToastProvider'; +import { FilterProvider } from '@/contexts/FilterContext'; function App() { return ( - + + + ); } -export default App; \ No newline at end of file +export default App; diff --git a/src/pages/search/ReviewFilter.tsx b/src/pages/search/ReviewFilter.tsx index 9c8eef8..7e8a5ed 100644 --- a/src/pages/search/ReviewFilter.tsx +++ b/src/pages/search/ReviewFilter.tsx @@ -1,4 +1,5 @@ import { Fragment, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; import { cn } from '@/utils/cn'; import { useFilter } from '@/contexts/FilterContext'; import { Header, AccordionSection, FilterCheckbox } from '@/components'; @@ -7,21 +8,30 @@ const sortOptions = ['가장 인기있는 순', '평점순', '최신순']; const cinemaOptions = { IMAX: ['용산아이파크몰', '왕심리', '천호'], 'Dolby Cinema': [ - '코에스점', - '남양주현대아울렌스페이스원', - '하른스타필드점', - '안성스타필드점', - '수원AK플라자점', - '송도점', - '대전신세계 아트애드사인스점', - '대구신세계점', + '코엑스점', '남양주현대아울렌스페이스원', '하른스타필드점', '안성스타필드점', + '수원AK플라자점', '송도점', '대전신세계 아트애드사인스페이스점', '대구신세계점', ], }; const soundOptions = ['Dolby Atmos', 'DTS:X']; const environmentOptions = ['리클라이너', '카포트']; +const auditoriumIdMap: Record = { + '용산아이파크몰': 1, + '왕심리': 2, + '천호': 3, + '코엑스점': 4, + '남양주현대아울렌스페이스원': 5, + '하른스타필드점': 6, + '안성스타필드점': 7, + '수원AK플라자점': 8, + '송도점': 9, + '대전신세계 아트애드사인스페이스점': 10, + '대구신세계점': 11, +}; + export default function ReviewFilter() { - const [activeSort, setActiveSort] = useState('인기순'); + const navigate = useNavigate(); + const [activeSort, setActiveSort] = useState('가장 인기있는 순'); const [openSections, setOpenSections] = useState(['Dolby Cinema', '음향', '관람 환경']); const [selectedCinemas, setSelectedCinemas] = useState>({}); const [selectedSounds, setSelectedSounds] = useState>({}); @@ -29,41 +39,70 @@ export default function ReviewFilter() { const { setIsFiltered } = useFilter(); + const handleBackClick = () => { + navigate(-1); + }; + const toggleSection = (sectionName: string) => { - setOpenSections((prev) => + setOpenSections(prev => prev.includes(sectionName) - ? prev.filter((name) => name !== sectionName) - : [...prev, sectionName], + ? prev.filter(name => name !== sectionName) + : [...prev, sectionName] ); }; const handleCheckboxChange = ( setter: React.Dispatch>>, - option: string, + option: string ) => { - setter((prev) => { + setter(prev => { const updated = { ...prev, [option]: !prev[option] }; setIsFiltered(true); return updated; }); }; + const handleApplyFilter = () => { + const selectedAuditoriumNames = Object.keys(selectedCinemas).filter(key => selectedCinemas[key]); + const selectedAuditoriumIds = selectedAuditoriumNames + .map(name => auditoriumIdMap[name]) + .filter(Boolean); // ID가 없는 값은 제거 + + const sortParamMap: Record = { + '가장 인기있는 순': 'POPULAR', + '평점순': 'RATING', + '최신순': 'LATEST', + }; + + const searchParams: Record = { + sort: sortParamMap[activeSort], + }; + + if (selectedAuditoriumIds.length > 0) { + searchParams.auditoriumId = selectedAuditoriumIds[0].toString(); // 단일 선택 가정 + } + + navigate(`/search/result?${new URLSearchParams(searchParams as any).toString()}`); + }; + return (
-
필터
+
+ 필터 +
{/* 정렬 */} -
-

정렬

+
+

정렬

{sortOptions.map((option, index) => (
); diff --git a/src/pages/search/ReviewSearch.tsx b/src/pages/search/ReviewSearch.tsx index c48b76f..1336a0a 100644 --- a/src/pages/search/ReviewSearch.tsx +++ b/src/pages/search/ReviewSearch.tsx @@ -1,59 +1,92 @@ -import { useState } from 'react'; -import { useNavigate } from 'react-router-dom'; -import {SearchInput, Badge, BottomNavigation} from '@/components' -const initialKeywords = [ - '뭔가검색했겠지...', '뭐가있지', '아무거나', - '두줄은', '채워야되니까', '일단써보기' -]; +import { useState, useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { SearchInput, Badge, BottomNavigation } from '@/components'; +import api from '@/api/api'; + +type Keyword = { + content: string; +}; export default function ReviewSearchPage() { const [search, setSearch] = useState(''); - const [recentKeywords, setRecentKeywords] = useState(initialKeywords); - const navigate = useNavigate(); + const [recentKeywords, setRecentKeywords] = useState([]); + const navigate = useNavigate(); - const handleRemove = (index: number) => { - setRecentKeywords(prev => prev.filter((_, i) => i !== index)); - }; + useEffect(() => { + const fetchRecentKeywords = async () => { + try { + const res = await api.get('/search'); + + if (res.data?.success && Array.isArray(res.data.data)) { + setRecentKeywords(res.data.data); + } else { + console.warn('유효하지 않은 검색어 응답:', res.data); + setRecentKeywords([]); + } + } catch (error) { + console.error('최근 검색어 조회 실패', error); + setRecentKeywords([]); + } + }; - const handleSearch = () => { + fetchRecentKeywords(); + }, []); + + const handleSearch = () => { if (!search.trim()) { alert('검색어를 입력해주세요.'); return; } - navigate(`/search/result?query=${search}`); + + navigate(`/search/result?query=${encodeURIComponent(search)}`); + }; + + const handleRemove = async (index: number) => { + const keyword = recentKeywords[index].content; + + try { + await api.delete('/search', { + params: { keyword }, + }); + setRecentKeywords((prev) => prev.filter((_, i) => i !== index)); + } catch (error) { + console.error('검색어 삭제 실패', error); + } }; - return ( -
+ return ( +

최근 검색어

- {recentKeywords.map((word, index) => ( - handleRemove(index)} - > - {word} - - ))} + {recentKeywords.length > 0 ? ( + recentKeywords.map((wordObj, index) => ( + handleRemove(index)} + > + {wordObj.content} + + )) + ) : ( +

최근 검색어가 없습니다.

+ )}
- +
); } - - diff --git a/src/pages/search/ReviewSearchResult.tsx b/src/pages/search/ReviewSearchResult.tsx index 5261628..5b6e33d 100644 --- a/src/pages/search/ReviewSearchResult.tsx +++ b/src/pages/search/ReviewSearchResult.tsx @@ -1,19 +1,74 @@ -import { useNavigate } from 'react-router-dom'; -import { useState } from 'react'; +import { useEffect, useState } from 'react'; +import { useSearchParams, useNavigate } from 'react-router-dom'; import { useFilter } from '@/contexts/FilterContext'; import { SearchInput, ReviewCard } from '@/components'; -import { FilterIcon } from '@/assets'; -import { mockMyReviews } from '@/__mocks/mockReviews'; +import { FilterIcon, ChevronIcon } from '@/assets'; +import api from '@/api/api'; + +interface Review { + reviewId: number; + content: string; + rating: number; + movieTitle: string; + thumbnailUrl:string; + likeCount: number; + likedByUser: boolean; + hashTags: string[]; +} export default function ReviewSearchResultPage() { - const navigate = useNavigate(); - const [search, setSearch] = useState(''); + const [searchParams, setSearchParams] = useSearchParams(); + const [search, setSearch] = useState(searchParams.get('query') ?? ''); const { isFiltered } = useFilter(); + const [results, setResults] = useState([]); + + const navigate = useNavigate(); + + useEffect(() => { + const fetchResults = async () => { + try { + const keyword = searchParams.get('query') || ''; + const sort = searchParams.get('sort') || 'POPULAR'; + const auditoriumId = searchParams.get('auditoriumId'); + + const res = await api.get('/search/reviews', { + params: { + keyword, + sort, + ...(auditoriumId ? { auditoriumId } : {}), + }, + }); + + setResults(res.data); + } catch (error) { + console.error('검색 결과 조회 실패', error); + } + }; + + fetchResults(); + }, [searchParams]); + + const handleGoBack = () => { + navigate(-1); + }; return (
- +
+ + +
+ setSearchParams({ query: search })} + /> +
+
+
- {mockMyReviews.map((result) => ( - {}} - /> - ))} + {results.length > 0 ? ( + results.map((result) => ( + {}} + /> + )) + ) : ( +

검색 결과가 없습니다.

+ )}