@@ -135,6 +135,8 @@ export default function Community() {
135135 setTagKeyword ( [ ] ) ;
136136 setSelectedTagFilters ( [ ] ) ;
137137 setSearchInput ( "" ) ;
138+ setSearchScope ( DEFAULT_SCOPE ) ;
139+ setSearchOperator ( DEFAULT_OPERATOR ) ;
138140 setCurrentPage ( 1 ) ;
139141 setLastKnownPage ( 1 ) ;
140142 } catch ( e ) {
@@ -151,29 +153,66 @@ export default function Community() {
151153 } , [ ] ) ;
152154
153155 const filteredPosts = useMemo ( ( ) => {
154- if ( ! keyword && ( ! tagKeyword || tagKeyword . length === 0 ) ) {
155- return posts ;
156- }
156+ const tokens = keyword
157+ ? keyword
158+ . split ( / \s + / )
159+ . map ( ( token ) => token . trim ( ) )
160+ . filter ( Boolean )
161+ . map ( ( token ) => token . toLowerCase ( ) )
162+ : [ ] ;
163+
164+ const matchTokens = ( post ) => {
165+ if ( ! tokens . length ) return true ;
166+
167+ const haystacks = ( ( ) => {
168+ switch ( searchScope ) {
169+ case "author" :
170+ return [ post . author ] ;
171+ case "title" :
172+ return [ post . title ] ;
173+ case "content" :
174+ return [ post . contentText ?? post . summary ?? "" ] ;
175+ case "tag" :
176+ return ( post . tags || [ ] ) . map ( ( tag ) => String ( tag || "" ) ) ;
177+ case "all" :
178+ default :
179+ return [
180+ post . title ,
181+ post . summary ,
182+ post . contentText ,
183+ post . author ,
184+ ...( post . tags || [ ] ) ,
185+ ] ;
186+ }
187+ } ) ( ) ;
188+
189+ const normalizedHaystacks = haystacks
190+ . flatMap ( ( value ) => ( Array . isArray ( value ) ? value : [ value ] ) )
191+ . map ( ( value ) => String ( value || "" ) . toLowerCase ( ) )
192+ . filter ( Boolean ) ;
193+
194+ if ( ! normalizedHaystacks . length ) return false ;
195+
196+ const containsToken = ( token ) =>
197+ normalizedHaystacks . some ( ( haystack ) => haystack . includes ( token ) ) ;
157198
158- const lowerKeyword = keyword . toLowerCase ( ) ;
199+ return searchOperator === "and"
200+ ? tokens . every ( containsToken )
201+ : tokens . some ( containsToken ) ;
202+ } ;
159203
160204 return posts . filter ( ( post ) => {
161- const matchesKeyword = lowerKeyword
162- ? [ post . title , post . summary , post . author ]
163- . join ( " " )
164- . toLowerCase ( )
165- . includes ( lowerKeyword )
166- : true ;
167-
168- const matchesTags = tagKeyword . length
169- ? tagKeyword . every ( ( token ) =>
170- ( post . tags || [ ] ) . some ( ( tag ) => tag . toLowerCase ( ) . includes ( token ) )
171- )
172- : true ;
173-
174- return matchesKeyword && matchesTags ;
205+ if ( ! matchTokens ( post ) ) return false ;
206+
207+ if ( ! tagKeyword . length ) return true ;
208+
209+ return tagKeyword . every ( ( token ) =>
210+ ( post . tags || [ ] ) . some ( ( tag ) =>
211+ String ( tag || "" ) . toLowerCase ( ) . includes ( token ) ,
212+ ) ,
213+ ) ;
175214 } ) ;
176- } , [ keyword , tagKeyword , posts ] ) ;
215+ } , [ keyword , tagKeyword , posts , searchScope , searchOperator ] ) ;
177216
178217 const [ activeFilter , setActiveFilter ] = useState ( filters [ 0 ] ) ;
179218
@@ -220,7 +259,7 @@ export default function Community() {
220259 const trimmedKeyword = searchInput . trim ( ) ;
221260
222261 setKeyword ( trimmedKeyword ) ;
223- setTagKeyword ( selectedTagFilters . map ( ( tag ) => tag . toLowerCase ( ) ) ) ;
262+ setTagKeyword ( selectedTagFilters . map ( ( tag ) => tag . toLowerCase ( ) ) ) ;
224263 setCurrentPage ( 1 ) ;
225264 setLastKnownPage ( 1 ) ;
226265 } ;
@@ -230,6 +269,8 @@ export default function Community() {
230269 setKeyword ( "" ) ;
231270 setSelectedTagFilters ( [ ] ) ;
232271 setTagKeyword ( [ ] ) ;
272+ setSearchScope ( DEFAULT_SCOPE ) ;
273+ setSearchOperator ( DEFAULT_OPERATOR ) ;
233274 setCurrentPage ( 1 ) ;
234275 setLastKnownPage ( 1 ) ;
235276 } ;
@@ -260,15 +301,25 @@ export default function Community() {
260301 const trimmed = ( writerName || "" ) . trim ( ) ;
261302 if ( ! trimmed ) return ;
262303
304+ const trimmedLower = trimmed . toLowerCase ( ) ;
305+ const currentKeywordLower = ( keyword || "" ) . toLowerCase ( ) ;
306+ const currentInputLower = searchInput . trim ( ) . toLowerCase ( ) ;
307+
263308 const isActive =
264- keyword === trimmed &&
265- searchInput . trim ( ) === trimmed &&
266- selectedTagFilters . length === 0 ;
309+ searchScope === "author" &&
310+ searchOperator === DEFAULT_OPERATOR &&
311+ selectedTagFilters . length === 0 &&
312+ currentKeywordLower === trimmedLower &&
313+ currentInputLower === trimmedLower ;
267314
268315 if ( isActive ) {
316+ setSearchScope ( DEFAULT_SCOPE ) ;
317+ setSearchOperator ( DEFAULT_OPERATOR ) ;
269318 setSearchInput ( "" ) ;
270319 setKeyword ( "" ) ;
271320 } else {
321+ setSearchScope ( "author" ) ;
322+ setSearchOperator ( DEFAULT_OPERATOR ) ;
272323 setSearchInput ( trimmed ) ;
273324 setKeyword ( trimmed ) ;
274325 }
@@ -277,19 +328,27 @@ export default function Community() {
277328 setTagKeyword ( [ ] ) ;
278329 setCurrentPage ( 1 ) ;
279330 setLastKnownPage ( 1 ) ;
280- } , [ keyword , searchInput , selectedTagFilters . length ] ) ;
331+ } , [ keyword , searchInput , searchScope , searchOperator , selectedTagFilters ] ) ;
281332
282333 const handlePopularTagSelect = ( tag ) => {
283334 const normalized = tag . toUpperCase ( ) ;
284- const isActive = selectedTagFilters . length === 1 && selectedTagFilters [ 0 ] === normalized ;
335+ const isActive =
336+ searchScope === "tag" &&
337+ searchOperator === DEFAULT_OPERATOR &&
338+ selectedTagFilters . length === 1 &&
339+ selectedTagFilters [ 0 ] === normalized ;
285340
286341 setSearchInput ( "" ) ;
287342 setKeyword ( "" ) ;
288343
289344 if ( isActive ) {
345+ setSearchScope ( DEFAULT_SCOPE ) ;
346+ setSearchOperator ( DEFAULT_OPERATOR ) ;
290347 setSelectedTagFilters ( [ ] ) ;
291348 setTagKeyword ( [ ] ) ;
292349 } else {
350+ setSearchScope ( "tag" ) ;
351+ setSearchOperator ( DEFAULT_OPERATOR ) ;
293352 setSelectedTagFilters ( [ normalized ] ) ;
294353 setTagKeyword ( [ normalized . toLowerCase ( ) ] ) ;
295354 }
@@ -442,11 +501,17 @@ export default function Community() {
442501 < ol >
443502 { topWriters . map ( ( { name, count } ) => {
444503 const trimmed = ( name || "" ) . trim ( ) ;
504+ const trimmedLower = trimmed . toLowerCase ( ) ;
505+ const currentKeywordLower = ( keyword || "" ) . toLowerCase ( ) ;
506+ const currentInputLower = searchInput . trim ( ) . toLowerCase ( ) ;
507+
445508 const isActive =
446- trimmed &&
447- keyword === trimmed &&
448- searchInput . trim ( ) === trimmed &&
449- selectedTagFilters . length === 0 ;
509+ Boolean ( trimmed ) &&
510+ searchScope === "author" &&
511+ searchOperator === DEFAULT_OPERATOR &&
512+ selectedTagFilters . length === 0 &&
513+ currentKeywordLower === trimmedLower &&
514+ currentInputLower === trimmedLower ;
450515
451516 return (
452517 < li key = { name } >
@@ -500,6 +565,44 @@ export default function Community() {
500565 />
501566 < button type = "submit" className = "search-btn" > 검색</ button >
502567 </ form >
568+ < div className = "search-options" >
569+ < div className = "search-option" >
570+ < label htmlFor = "community-search-scope" > 검색 대상</ label >
571+ < select
572+ id = "community-search-scope"
573+ value = { searchScope }
574+ onChange = { ( event ) => {
575+ setSearchScope ( event . target . value ) ;
576+ setCurrentPage ( 1 ) ;
577+ setLastKnownPage ( 1 ) ;
578+ } }
579+ >
580+ { SEARCH_SCOPE_OPTIONS . map ( ( option ) => (
581+ < option key = { option . value } value = { option . value } >
582+ { option . label }
583+ </ option >
584+ ) ) }
585+ </ select >
586+ </ div >
587+ < div className = "search-option" >
588+ < label htmlFor = "community-search-operator" > 조건</ label >
589+ < select
590+ id = "community-search-operator"
591+ value = { searchOperator }
592+ onChange = { ( event ) => {
593+ setSearchOperator ( event . target . value ) ;
594+ setCurrentPage ( 1 ) ;
595+ setLastKnownPage ( 1 ) ;
596+ } }
597+ >
598+ { SEARCH_OPERATOR_OPTIONS . map ( ( option ) => (
599+ < option key = { option . value } value = { option . value } >
600+ { option . label }
601+ </ option >
602+ ) ) }
603+ </ select >
604+ </ div >
605+ </ div >
503606 < div className = "search-row tag-search-row" role = "presentation" >
504607 < div
505608 className = "tag-search-selector"
@@ -694,7 +797,12 @@ export default function Community() {
694797 < ol className = "tag-list" >
695798 { popularTags . map ( ( tag ) => {
696799 const normalizedTag = tag . label . toUpperCase ( ) ;
697- const isActive = selectedTagFilters . length === 1 && selectedTagFilters [ 0 ] === normalizedTag ;
800+ const isActive =
801+ searchScope === "tag" &&
802+ searchOperator === DEFAULT_OPERATOR &&
803+ selectedTagFilters . length === 1 &&
804+ selectedTagFilters [ 0 ] === normalizedTag ;
805+
698806 return (
699807 < li key = { tag . label } >
700808 < button
0 commit comments