@@ -3,6 +3,10 @@ import { useNavigate } from "react-router-dom";
33import "./Community.css" ;
44import config from "../../config" ;
55
6+ const ALLOWED_TAGS = [
7+ "JAVA" , "C" , "CPP" , "JPA" , "JAVASCRIPT" , "PYTHON" , "OOP" , "BIGDATA" , "SPRING" , "TYPESCRIPT" , "ML"
8+ ] ;
9+
610export default function Community ( ) {
711 const navigate = useNavigate ( ) ;
812 const tabs = [ "전체" , "미해결" , "해결됨" ] ;
@@ -13,13 +17,15 @@ export default function Community() {
1317 const [ loading , setLoading ] = useState ( true ) ;
1418 const [ error , setError ] = useState ( "" ) ;
1519 const [ searchInput , setSearchInput ] = useState ( "" ) ;
16- const [ tagInput , setTagInput ] = useState ( "" ) ;
1720 const [ keyword , setKeyword ] = useState ( "" ) ;
1821 const [ tagKeyword , setTagKeyword ] = useState ( [ ] ) ;
22+ const [ selectedTagFilters , setSelectedTagFilters ] = useState ( [ ] ) ;
1923 const [ currentPage , setCurrentPage ] = useState ( 1 ) ;
2024 const [ lastKnownPage , setLastKnownPage ] = useState ( 1 ) ;
2125
2226 const PAGE_SIZE = 10 ;
27+ const selectedTagCount = selectedTagFilters . length ;
28+ const reachedTagFilterLimit = selectedTagCount >= 10 ;
2329
2430 useEffect ( ( ) => {
2531 let ignore = false ;
@@ -90,8 +96,8 @@ export default function Community() {
9096 setPosts ( mapped ) ;
9197 setKeyword ( "" ) ;
9298 setTagKeyword ( [ ] ) ;
99+ setSelectedTagFilters ( [ ] ) ;
93100 setSearchInput ( "" ) ;
94- setTagInput ( "" ) ;
95101 setCurrentPage ( 1 ) ;
96102 setLastKnownPage ( 1 ) ;
97103 } catch ( e ) {
@@ -175,26 +181,40 @@ export default function Community() {
175181 const handleSearchSubmit = ( event ) => {
176182 event ?. preventDefault ?. ( ) ;
177183 const trimmedKeyword = searchInput . trim ( ) ;
178- const parsedTags = tagInput
179- . split ( / [ # , \s , ] + / )
180- . map ( ( token ) => token . trim ( ) . replace ( / ^ # / , "" ) . toLowerCase ( ) )
181- . filter ( Boolean ) ;
182184
183185 setKeyword ( trimmedKeyword ) ;
184- setTagKeyword ( parsedTags ) ;
186+ setTagKeyword ( selectedTagFilters . map ( ( tag ) => tag . toLowerCase ( ) ) ) ;
185187 setCurrentPage ( 1 ) ;
186188 setLastKnownPage ( 1 ) ;
187189 } ;
188190
189191 const handleReset = ( ) => {
190192 setSearchInput ( "" ) ;
191- setTagInput ( "" ) ;
192193 setKeyword ( "" ) ;
194+ setSelectedTagFilters ( [ ] ) ;
193195 setTagKeyword ( [ ] ) ;
194196 setCurrentPage ( 1 ) ;
195197 setLastKnownPage ( 1 ) ;
196198 } ;
197199
200+ const toggleTagFilter = ( tag ) => {
201+ setSelectedTagFilters ( ( prev ) => {
202+ if ( prev . includes ( tag ) ) {
203+ const next = prev . filter ( ( item ) => item !== tag ) ;
204+ setTagKeyword ( next . map ( ( item ) => item . toLowerCase ( ) ) ) ;
205+ return next ;
206+ }
207+ if ( prev . length >= 10 ) {
208+ return prev ;
209+ }
210+ const next = [ ...prev , tag ] ;
211+ setTagKeyword ( next . map ( ( item ) => item . toLowerCase ( ) ) ) ;
212+ return next ;
213+ } ) ;
214+ setCurrentPage ( 1 ) ;
215+ setLastKnownPage ( 1 ) ;
216+ } ;
217+
198218 const totalPages = useMemo ( ( ) => {
199219 const count = Math . ceil ( filteredPosts . length / PAGE_SIZE ) ;
200220 return count > 0 ? count : 1 ;
@@ -334,22 +354,42 @@ export default function Community() {
334354 />
335355 < button type = "submit" className = "search-btn" > 검색</ button >
336356 </ form >
337- < div className = "search-row" >
338- < input
339- type = "text"
340- placeholder = "# 태그로 검색해보세요!"
341- value = { tagInput }
342- onChange = { ( event ) => setTagInput ( event . target . value ) }
343- onKeyDown = { ( event ) => {
344- if ( event . key === "Enter" ) {
345- event . preventDefault ( ) ;
346- handleSearchSubmit ( ) ;
347- }
348- } }
357+ < div className = "search-row tag-search-row" role = "presentation" >
358+ < div
359+ className = "tag-search-selector"
360+ role = "group"
349361 aria-label = "태그 검색"
350- />
362+ >
363+ { ALLOWED_TAGS . map ( ( tag ) => {
364+ const isActive = selectedTagFilters . includes ( tag ) ;
365+ const isDisabled = ! isActive && reachedTagFilterLimit ;
366+ const printable = `#${ tag . toLowerCase ( ) } ` ;
367+ const buttonLabel = isActive
368+ ? `${ printable } 태그 필터 제거`
369+ : isDisabled
370+ ? "태그 필터는 최대 10개까지 선택할 수 있어요"
371+ : `${ printable } 태그 필터 추가` ;
372+ return (
373+ < button
374+ key = { tag }
375+ type = "button"
376+ className = { `tag-search-option ${ isActive ? "is-active" : "" } ` . trim ( ) }
377+ onClick = { ( ) => toggleTagFilter ( tag ) }
378+ aria-pressed = { isActive }
379+ disabled = { isDisabled }
380+ title = { buttonLabel }
381+ >
382+ { printable }
383+ </ button >
384+ ) ;
385+ } ) }
386+ </ div >
351387 < button type = "button" className = "reset-btn" onClick = { handleReset } > 초기화</ button >
352388 </ div >
389+ < p className = "tag-search-helper" aria-live = "polite" >
390+ 태그는 클릭해서 추가하거나 제거할 수 있어요. 선택 { selectedTagCount } 개
391+ { reachedTagFilterLimit ? " (최대 10개 선택됨)" : "" }
392+ </ p >
353393 </ div >
354394
355395 < div className = "filter-area" >
0 commit comments