Skip to content

Commit 4c8dc50

Browse files
committed
커뮤니티 검색기능 추가
1 parent 18405c4 commit 4c8dc50

1 file changed

Lines changed: 127 additions & 9 deletions

File tree

src/components/community/Community.jsx

Lines changed: 127 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useEffect, useState } from "react";
1+
import React, { useEffect, useMemo, useState } from "react";
22
import { useNavigate } from "react-router-dom";
33
import "./Community.css";
44
import config from "../../config";
@@ -13,6 +13,13 @@ export default function Community() {
1313
const [loading, setLoading] = useState(true);
1414
const [error, setError] = useState("");
1515

16+
const [searchInput, setSearchInput] = useState("");
17+
const [tagInput, setTagInput] = useState("");
18+
const [keyword, setKeyword] = useState("");
19+
const [tagKeyword, setTagKeyword] = useState([]);
20+
const [currentPage, setCurrentPage] = useState(1);
21+
const ITEMS_PER_PAGE = 10;
22+
1623
useEffect(() => {
1724
let ignore = false;
1825
const controller = new AbortController();
@@ -71,6 +78,11 @@ export default function Community() {
7178
}));
7279

7380
setPosts(mapped);
81+
setKeyword("");
82+
setTagKeyword([]);
83+
setSearchInput("");
84+
setTagInput("");
85+
setCurrentPage(1);
7486
} catch (e) {
7587
if (!ignore) setError(e.message || "알 수 없는 오류");
7688
} finally {
@@ -84,6 +96,90 @@ export default function Community() {
8496
};
8597
}, []);
8698

99+
const filteredPosts = useMemo(() => {
100+
if (!keyword && (!tagKeyword || tagKeyword.length === 0)) {
101+
return posts;
102+
}
103+
104+
const lowerKeyword = keyword.toLowerCase();
105+
106+
return posts.filter((post) => {
107+
const matchesKeyword = lowerKeyword
108+
? [post.title, post.summary, post.author]
109+
.join(" ")
110+
.toLowerCase()
111+
.includes(lowerKeyword)
112+
: true;
113+
114+
const matchesTags = tagKeyword.length
115+
? tagKeyword.every((token) =>
116+
(post.tags || []).some((tag) => tag.toLowerCase().includes(token))
117+
)
118+
: true;
119+
120+
return matchesKeyword && matchesTags;
121+
});
122+
}, [keyword, tagKeyword, posts]);
123+
124+
useEffect(() => {
125+
setCurrentPage(1);
126+
}, [keyword, tagKeyword]);
127+
128+
useEffect(() => {
129+
const lastPage = Math.max(1, Math.ceil(filteredPosts.length / ITEMS_PER_PAGE));
130+
setCurrentPage((prev) => {
131+
const next = Math.min(prev, lastPage);
132+
return next === prev ? prev : next;
133+
});
134+
}, [filteredPosts.length, ITEMS_PER_PAGE]);
135+
136+
const handleSearchSubmit = (event) => {
137+
event?.preventDefault?.();
138+
const trimmedKeyword = searchInput.trim();
139+
const parsedTags = tagInput
140+
.split(/[#,\s,]+/)
141+
.map((token) => token.trim().replace(/^#/, "").toLowerCase())
142+
.filter(Boolean);
143+
144+
setKeyword(trimmedKeyword);
145+
setTagKeyword(parsedTags);
146+
setCurrentPage(1);
147+
};
148+
149+
const handleReset = () => {
150+
setSearchInput("");
151+
setTagInput("");
152+
setKeyword("");
153+
setTagKeyword([]);
154+
setCurrentPage(1);
155+
};
156+
157+
const totalPages = Math.max(1, Math.ceil(filteredPosts.length / ITEMS_PER_PAGE));
158+
const hasResults = filteredPosts.length > 0;
159+
const startIndex = (currentPage - 1) * ITEMS_PER_PAGE;
160+
const visiblePosts = useMemo(
161+
() => (hasResults ? filteredPosts.slice(startIndex, startIndex + ITEMS_PER_PAGE) : []),
162+
[filteredPosts, hasResults, startIndex, ITEMS_PER_PAGE]
163+
);
164+
165+
const goToPage = (page) => {
166+
const next = Math.min(Math.max(page, 1), Math.max(1, totalPages));
167+
setCurrentPage(next);
168+
};
169+
170+
const goToFirst = () => goToPage(1);
171+
const goToLast = () => goToPage(totalPages);
172+
const goToPreviousGroup = () => goToPage(currentPage - 5);
173+
const goToNextGroup = () => goToPage(currentPage + 5);
174+
175+
const pageNumbers = useMemo(() => {
176+
const numbers = [];
177+
for (let i = 1; i <= totalPages; i += 1) {
178+
numbers.push(i);
179+
}
180+
return numbers;
181+
}, [totalPages]);
182+
87183
return (
88184
<div className="community-wrapper">
89185
<div className="community-page">
@@ -118,13 +214,31 @@ export default function Community() {
118214
</div>
119215

120216
<div className="search-bar">
217+
<form className="search-row" onSubmit={handleSearchSubmit}>
218+
<input
219+
type="search"
220+
placeholder="궁금한 질문을 검색해보세요!"
221+
value={searchInput}
222+
onChange={(event) => setSearchInput(event.target.value)}
223+
aria-label="게시글 검색"
224+
/>
225+
<button type="submit" className="search-btn">검색</button>
226+
</form>
121227
<div className="search-row">
122-
<input type="text" placeholder="궁금한 질문을 검색해보세요!" />
123-
<button className="search-btn">검색</button>
124-
</div>
125-
<div className="search-row">
126-
<input type="text" placeholder="# 태그로 검색해보세요!" />
127-
<button className="reset-btn">초기화</button>
228+
<input
229+
type="text"
230+
placeholder="# 태그로 검색해보세요!"
231+
value={tagInput}
232+
onChange={(event) => setTagInput(event.target.value)}
233+
onKeyDown={(event) => {
234+
if (event.key === "Enter") {
235+
event.preventDefault();
236+
handleSearchSubmit();
237+
}
238+
}}
239+
aria-label="태그 검색"
240+
/>
241+
<button type="button" className="reset-btn" onClick={handleReset}>초기화</button>
128242
</div>
129243
</div>
130244

@@ -146,9 +260,13 @@ export default function Community() {
146260
<div className="post-list"><p>게시글이 없습니다.</p></div>
147261
)}
148262

149-
{!loading && !error && posts.length > 0 && (
263+
{!loading && !error && posts.length > 0 && filteredPosts.length === 0 && (
264+
<div className="post-list"><p>검색 결과가 없습니다.</p></div>
265+
)}
266+
267+
{!loading && !error && filteredPosts.length > 0 && (
150268
<div className="post-list">
151-
{posts.map((post) => (
269+
{filteredPosts.map((post) => (
152270
<div
153271
key={post.id}
154272
className="post-card"

0 commit comments

Comments
 (0)