1- import React , { useEffect , useState } from "react" ;
1+ import React , { useEffect , useMemo , useState } from "react" ;
22import { useNavigate } from "react-router-dom" ;
33import "./Community.css" ;
44import 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