Skip to content

Commit 928daa7

Browse files
committed
검색기능 확장
1 parent 4b13485 commit 928daa7

2 files changed

Lines changed: 183 additions & 30 deletions

File tree

src/components/community/Community.css

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,11 @@
146146
transform: translateY(-1px);
147147
}
148148

149+
.top-writer-item:focus-visible {
150+
outline: 2px solid rgba(124, 92, 255, 0.55);
151+
outline-offset: 3px;
152+
}
153+
149154
.top-writer-item.is-active {
150155
background: var(--community-accent);
151156
border-color: var(--community-accent);
@@ -222,6 +227,41 @@
222227
gap: 12px;
223228
}
224229

230+
.search-options {
231+
display: flex;
232+
flex-wrap: wrap;
233+
gap: 12px;
234+
}
235+
236+
.search-option {
237+
display: flex;
238+
flex-direction: column;
239+
gap: 6px;
240+
min-width: 140px;
241+
}
242+
243+
.search-option label {
244+
font-size: 12px;
245+
font-weight: 600;
246+
color: var(--community-muted);
247+
}
248+
249+
.search-option select {
250+
border: 1px solid var(--community-border);
251+
border-radius: 10px;
252+
padding: 10px 12px;
253+
font-size: 13px;
254+
background: var(--community-bg);
255+
color: var(--community-text);
256+
transition: border-color 0.18s ease, background-color 0.18s ease;
257+
}
258+
259+
.search-option select:focus {
260+
outline: none;
261+
border-color: rgba(124, 92, 255, 0.55);
262+
background: var(--community-surface);
263+
}
264+
225265
.tag-search-row {
226266
align-items: flex-start;
227267
flex-wrap: wrap;
@@ -619,6 +659,11 @@
619659
transform: none;
620660
}
621661

662+
.popular-tags .tag-item-button:focus-visible {
663+
outline: 2px solid rgba(124, 92, 255, 0.55);
664+
outline-offset: 3px;
665+
}
666+
622667
.popular-tags-empty {
623668
margin: 0;
624669
font-size: 13px;

src/components/community/Community.jsx

Lines changed: 138 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)