Skip to content

Commit df1d198

Browse files
committed
빌드 에러 수정
1 parent db326bd commit df1d198

File tree

2 files changed

+140
-69
lines changed

2 files changed

+140
-69
lines changed

src/pages/search/ReviewSearch.tsx

Lines changed: 45 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@ import { useNavigate } from 'react-router-dom';
55
import { Header, SearchInput, Badge, BottomNavigation } from '@/components';
66
import api from '@/api/api';
77

8-
export default function ReviewSearch() { // 컴포넌트 이름을 파일 이름과 맞춥니다.
8+
/**
9+
* 최근 검색어 목록을 보여주고, 새로운 검색을 시작하는 페이지
10+
*/
11+
export default function ReviewSearch() {
912
const [search, setSearch] = useState('');
1013
const [recentKeywords, setRecentKeywords] = useState<string[]>([]);
1114
const navigate = useNavigate();
@@ -14,78 +17,84 @@ export default function ReviewSearch() { // 컴포넌트 이름을 파일 이름
1417
navigate(-1);
1518
};
1619

17-
// 최근 검색어 불러오기
20+
// 최근 검색어 목록을 서버에서 불러오는 함수
1821
const fetchRecentKeywords = async () => {
1922
try {
20-
// [수정] baseURL을 사용하므로 상대 경로로 변경하고, .data로 데이터에 접근합니다.
21-
const response = await api.get<string[]>('/search');
23+
// API 명세에 맞는 정확한 엔드포인트로 수정해야 합니다. (예시: /api/v1/search/recent)
24+
const response = await api.get<string[]>('/api/v1/search/recent');
25+
// 최대 6개의 검색어만 보여줍니다.
2226
setRecentKeywords(response.data.slice(0, 6));
2327
} catch (err) {
2428
console.error('최근 검색어 조회 실패:', err);
25-
setRecentKeywords([]); // 오류 발생 시 빈 배열로 초기화
29+
setRecentKeywords([]); // 오류 발생 시 안전하게 빈 배열로 설정
2630
}
2731
};
2832

29-
// ✅ 검색어 삭제
30-
const handleRemove = async (index: number) => {
31-
const keyword = recentKeywords[index];
33+
// 컴포넌트 마운트 시 최근 검색어를 불러옵니다.
34+
useEffect(() => {
35+
fetchRecentKeywords();
36+
}, []);
37+
38+
// 특정 최근 검색어를 삭제하는 함수
39+
const handleRemove = async (keywordToRemove: string) => {
3240
try {
33-
await api.delete('/search', { data: { keyword } });
34-
setRecentKeywords(prev => prev.filter((_, i) => i !== index));
41+
// API 명세에 맞게 DELETE 요청을 보냅니다.
42+
await api.delete('/api/v1/search/recent', { data: { keyword: keywordToRemove } });
43+
// 상태에서도 해당 검색어를 제거합니다.
44+
setRecentKeywords(prev => prev.filter(keyword => keyword !== keywordToRemove));
3545
} catch (err) {
3646
console.error('검색어 삭제 실패:', err);
3747
}
3848
};
3949

40-
// ✅ 검색 및 저장
50+
// 검색을 실행하는 함수 (SearchInput의 onSearch prop으로 전달)
4151
const handleSearch = async () => {
42-
const trimmed = search.trim();
43-
if (!trimmed) {
52+
const trimmedSearch = search.trim();
53+
54+
// 검색어가 비어있거나 공백만 있으면 경고하고 함수를 종료합니다.
55+
if (!trimmedSearch) {
4456
alert('검색어를 입력해주세요.');
4557
return;
4658
}
4759

4860
try {
49-
await api.post('/search', { keyword: trimmed });
50-
51-
setRecentKeywords(prev => {
52-
const filtered = prev.filter(word => word !== trimmed);
53-
return [trimmed, ...filtered].slice(0, 6);
54-
});
55-
56-
navigate(`/search/result?query=${encodeURIComponent(trimmed)}`);
61+
// 서버에 검색어를 저장하는 API를 호출합니다 (필요하다면).
62+
// await api.post('/api/v1/search/recent', { keyword: trimmedSearch });
63+
64+
// 검색 결과 페이지로 이동합니다. 쿼리 파라미터로 검색어를 넘겨줍니다.
65+
navigate(`/search/result?query=${encodeURIComponent(trimmedSearch)}`);
5766
} catch (err) {
58-
console.error('검색어 저장 실패:', err);
67+
console.error('검색 실행 중 오류:', err);
68+
// 필요하다면 사용자에게 오류 발생을 알릴 수 있습니다.
69+
alert('검색 중 오류가 발생했습니다.');
5970
}
6071
};
6172

62-
useEffect(() => {
63-
fetchRecentKeywords();
64-
}, []);
65-
6673
return (
67-
<div className="relative flex justify-center min-h-screen text-white">
74+
<div className="relative flex justify-center min-h-screen bg-black text-white">
6875
<div className="w-full max-w-[400px] px-4 pt-16 pb-20">
6976
<Header leftSection="BACK" onBackClick={handleBackClick}>
7077
검색
7178
</Header>
7279

73-
<SearchInput
74-
value={search}
75-
onChange={setSearch}
76-
placeholder="검색어를 입력해주세요"
77-
onSearch={handleSearch}
78-
/>
80+
<div className="mt-4">
81+
<SearchInput
82+
value={search}
83+
onChange={setSearch} // input 값이 변경될 때마다 search 상태 업데이트
84+
placeholder="영화, 배우, 태그 검색"
85+
onSearch={handleSearch} // Enter 또는 돋보기 클릭 시 handleSearch 실행
86+
/>
87+
</div>
7988

80-
<div className="mt-6">
81-
<h2 className="mb-2 text-title-3">최근 검색어</h2>
89+
<div className="mt-8">
90+
<h2 className="mb-4 text-title-3 font-bold">최근 검색어</h2>
8291
{recentKeywords.length > 0 ? (
8392
<div className="flex flex-wrap gap-2">
8493
{recentKeywords.map((word, index) => (
8594
<Badge
8695
key={`${word}-${index}`}
8796
type="removable"
88-
onRemove={() => handleRemove(index)}
97+
onRemove={() => handleRemove(word)}
8998
>
9099
{word}
91100
</Badge>

src/pages/search/ReviewSearchResult.tsx

Lines changed: 95 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1+
// 파일 경로: src/pages/search/ReviewSearchResult.tsx
2+
13
import { useEffect, useState, useCallback } from 'react';
24
import { useNavigate, useSearchParams } from 'react-router-dom';
35
import { useFilter } from '@/contexts/FilterContext';
4-
import { SearchInput, ReviewCard } from '@/components';
6+
import { SearchInput, ReviewCard, Header } from '@/components'; // Header 추가
57
import { FilterIcon } from '@/assets';
68
import api from '@/api/api';
79

10+
// 서버로부터 받는 리뷰 데이터의 타입을 명확하게 정의합니다.
811
interface 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

Comments
 (0)