1+ // 파일 경로: src/pages/search/ReviewSearchResult.tsx
2+
13import { useEffect , useState , useCallback } from 'react' ;
24import { useNavigate , useSearchParams } from 'react-router-dom' ;
35import { useFilter } from '@/contexts/FilterContext' ;
4- import { SearchInput , ReviewCard } from '@/components' ;
6+ import { SearchInput , ReviewCard , Header } from '@/components' ; // Header 추가
57import { FilterIcon } from '@/assets' ;
68import api from '@/api/api' ;
79
10+ // 서버로부터 받는 리뷰 데이터의 타입을 명확하게 정의합니다.
811interface Review {
912 reviewId : number ;
1013 content : string ;
@@ -16,59 +19,118 @@ interface Review {
1619 hashTags : string [ ] ;
1720}
1821
19- export default function ReviewSearchResultPage ( ) {
22+ // 서버 응답 전체의 타입을 정의합니다.
23+ interface SearchResponse {
24+ content : Review [ ] ;
25+ // pageable, totalPages 등 페이지네이션 정보가 있다면 여기에 추가
26+ }
27+
28+ /**
29+ * 검색어에 대한 리뷰 검색 결과를 보여주는 페이지
30+ */
31+ export default function ReviewSearchResult ( ) { // 컴포넌트 이름 변경
2032 const navigate = useNavigate ( ) ;
2133 const { isFiltered } = useFilter ( ) ;
22- const [ searchParams ] = useSearchParams ( ) ;
23- const query = searchParams . get ( 'query' ) || '' ;
24- const [ search , setSearch ] = useState ( query ) ;
34+ const [ searchParams , setSearchParams ] = useSearchParams ( ) ;
35+ const queryFromUrl = searchParams . get ( 'query' ) || '' ;
36+
37+ const [ search , setSearch ] = useState ( queryFromUrl ) ; // input의 상태
2538 const [ results , setResults ] = useState < Review [ ] > ( [ ] ) ;
39+ const [ loading , setLoading ] = useState ( true ) ;
40+ const [ error , setError ] = useState < string | null > ( null ) ;
41+
42+ // 검색 결과를 불러오는 함수 (useCallback으로 불필요한 재생성 방지)
43+ const fetchResults = useCallback ( async ( currentQuery : string ) => {
44+ if ( ! currentQuery ) {
45+ setResults ( [ ] ) ;
46+ setLoading ( false ) ;
47+ return ;
48+ }
49+
50+ setLoading ( true ) ;
51+ setError ( null ) ;
2652
27- // ✅ 검색 결과 불러오기 (useCallback으로 최적화)
28- const fetchResults = useCallback ( async ( ) => {
29- if ( ! query ) return ; // query가 없으면 요청하지 않음
3053 try {
31- const res = await api . get < { content : Review [ ] } > ( '/api/v1/search/reviews' , {
32- params : { query } ,
54+ // params로 쿼리를 전달합니다.
55+ const res = await api . get < SearchResponse > ( '/api/v1/search/reviews' , {
56+ params : { query : currentQuery } ,
3357 } ) ;
34- // [오류 수정 1] axios 응답 데이터는 'data' 속성에 있습니다.
3558 setResults ( res . data . content ) ;
3659 } catch ( err ) {
3760 console . error ( '검색 결과 조회 실패:' , err ) ;
61+ setError ( '검색 결과를 불러오는 데 실패했습니다.' ) ;
62+ setResults ( [ ] ) ;
63+ } finally {
64+ setLoading ( false ) ;
3865 }
39- } , [ query ] ) ; // query가 변경될 때만 함수를 재생성합니다.
66+ } , [ ] ) ;
4067
68+ // URL의 쿼리 파라미터가 변경될 때마다 검색 결과를 다시 불러옵니다.
4169 useEffect ( ( ) => {
42- fetchResults ( ) ;
43- // [오류 수정 2] 의존성 배열에 fetchResults를 추가합니다.
44- } , [ fetchResults ] ) ;
70+ fetchResults ( queryFromUrl ) ;
71+ } , [ queryFromUrl , fetchResults ] ) ;
72+
73+ // 이 페이지에서 다시 검색을 실행하는 함수
74+ const handleSearch = ( ) => {
75+ const trimmedSearch = search . trim ( ) ;
76+ if ( ! trimmedSearch ) {
77+ alert ( '검색어를 입력해주세요.' ) ;
78+ return ;
79+ }
80+ // URL의 쿼리 파라미터를 업데이트하여 페이지를 리렌더링하고 useEffect를 트리거합니다.
81+ setSearchParams ( { query : trimmedSearch } ) ;
82+ } ;
4583
4684 return (
47- < div className = "min-h-screen text-white" >
48- < div className = "mx-auto w-full max-w-[400px] px-4" >
49- < SearchInput value = { search } onChange = { setSearch } placeholder = "검색어를 입력해주세요" />
50- < div className = "my-3 flex justify-start" >
85+ < div className = "min-h-screen bg-black text-white" >
86+ < div className = "mx-auto w-full max-w-[400px] px-4 pb-10" >
87+ < Header leftSection = "BACK" onBackClick = { ( ) => navigate ( - 1 ) } >
88+ 검색 결과
89+ </ Header >
90+
91+ < div className = "mt-4" >
92+ < SearchInput
93+ value = { search }
94+ onChange = { setSearch }
95+ placeholder = "검색어를 다시 입력해주세요"
96+ onSearch = { handleSearch } // ✅ 에러 해결: onSearch prop 추가
97+ />
98+ </ div >
99+
100+ < div className = "my-4 flex justify-between items-center" >
101+ < p className = "text-sm text-gray-400" >
102+ 총 < span className = "text-white font-semibold" > { results . length } </ span > 개의 결과
103+ </ p >
51104 < button
52105 onClick = { ( ) => navigate ( '/search/filter' ) }
53- className = "relative flex h-6 w-6 items-center justify-center rounded-md bg-gray-800"
106+ className = "relative flex items-center gap-x-1 rounded-md bg-gray-800 px-2 py-1 text-sm "
54107 >
55- < FilterIcon className = "h-5 w-5 text-gray-400" />
108+ < FilterIcon className = "h-4 w-4 text-gray-400" />
109+ < span > 필터</ span >
56110 { isFiltered && < span className = "absolute -top-1 -right-1 h-2 w-2 rounded-full bg-red-500" /> }
57111 </ button >
58112 </ div >
59113
60- < main className = "flex flex-col gap-y-2" >
61- { results . map ( ( review ) => (
62- < ReviewCard
63- key = { review . reviewId }
64- imageUrl = { review . thumbnailUrl }
65- tags = { review . hashTags }
66- title = { review . movieTitle }
67- description = { review . content }
68- likeCount = { review . likeCount }
69- onClick = { ( ) => { } }
70- />
71- ) ) }
114+ < main className = "flex flex-col gap-y-3" >
115+ { loading ? (
116+ < p className = "text-center text-gray-400" > 검색 중...</ p >
117+ ) : error ? (
118+ < p className = "text-center text-red-400" > { error } </ p >
119+ ) : results . length > 0 ? (
120+ results . map ( ( review ) => (
121+ < ReviewCard
122+ key = { review . reviewId }
123+ imageUrl = { review . thumbnailUrl }
124+ tags = { review . hashTags }
125+ title = { review . movieTitle }
126+ description = { review . content }
127+ likeCount = { review . likeCount }
128+ onClick = { ( ) => navigate ( `/review/${ review . reviewId } ` ) } // 리뷰 상세 페이지로 이동
129+ />
130+ ) )
131+ ) : (
132+ < p className = "text-center text-gray-400" > 검색 결과가 없습니다.</ p >
133+ ) }
72134 </ main >
73135 </ div >
74136 </ div >
0 commit comments