Skip to content

Commit 19c2294

Browse files
committed
태그 버튼화
1 parent d74b190 commit 19c2294

4 files changed

Lines changed: 297 additions & 77 deletions

File tree

src/components/community/Community.css

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,11 @@
159159
gap: 12px;
160160
}
161161

162+
.tag-search-row {
163+
align-items: flex-start;
164+
flex-wrap: wrap;
165+
}
166+
162167
.search-row input {
163168
flex: 1;
164169
min-width: 0;
@@ -186,6 +191,60 @@
186191
transition: transform 0.18s ease, box-shadow 0.18s ease, background-color 0.18s ease;
187192
}
188193

194+
.tag-search-selector {
195+
flex: 1 1 0;
196+
min-width: 0;
197+
display: flex;
198+
flex-wrap: wrap;
199+
gap: 8px;
200+
}
201+
202+
.tag-search-option {
203+
padding: 10px 16px;
204+
border-radius: 999px;
205+
border: 1px solid rgba(124, 92, 255, 0.22);
206+
background: rgba(124, 92, 255, 0.08);
207+
color: var(--community-muted);
208+
font-size: 13px;
209+
font-weight: 600;
210+
cursor: pointer;
211+
-webkit-appearance: none;
212+
appearance: none;
213+
transition: transform 0.16s ease, box-shadow 0.16s ease, background-color 0.16s ease, color 0.16s ease, border-color 0.16s ease;
214+
}
215+
216+
.tag-search-option:hover:not(:disabled) {
217+
transform: translateY(-1px);
218+
background: rgba(124, 92, 255, 0.16);
219+
border-color: rgba(124, 92, 255, 0.34);
220+
color: var(--community-text);
221+
box-shadow: 0 14px 24px -20px rgba(124, 92, 255, 0.4);
222+
}
223+
224+
.tag-search-option.is-active {
225+
background: var(--community-accent);
226+
border-color: var(--community-accent);
227+
color: #ffffff;
228+
box-shadow: 0 16px 32px -24px rgba(124, 92, 255, 0.6);
229+
}
230+
231+
.tag-search-option:disabled {
232+
cursor: not-allowed;
233+
opacity: 0.55;
234+
box-shadow: none;
235+
}
236+
237+
.tag-search-option:focus-visible {
238+
outline: 2px solid rgba(124, 92, 255, 0.55);
239+
outline-offset: 2px;
240+
}
241+
242+
.tag-search-helper {
243+
margin: 4px 0 0;
244+
font-size: 12px;
245+
color: var(--community-muted);
246+
}
247+
189248
.search-btn {
190249
background: var(--community-accent);
191250
color: #ffffff;
@@ -604,6 +663,18 @@
604663
flex-direction: column;
605664
}
606665

666+
.tag-search-row {
667+
align-items: stretch;
668+
}
669+
670+
.tag-search-selector {
671+
width: 100%;
672+
}
673+
674+
.tag-search-helper {
675+
text-align: left;
676+
}
677+
607678
.search-row button {
608679
width: 100%;
609680
}

src/components/community/Community.jsx

Lines changed: 61 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ import { useNavigate } from "react-router-dom";
33
import "./Community.css";
44
import config from "../../config";
55

6+
const ALLOWED_TAGS = [
7+
"JAVA", "C", "CPP", "JPA", "JAVASCRIPT", "PYTHON", "OOP", "BIGDATA", "SPRING", "TYPESCRIPT", "ML"
8+
];
9+
610
export 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">

src/components/community/CommunityWrite.css

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,12 +141,69 @@
141141
box-shadow: 0 18px 40px -30px rgba(124, 92, 255, 0.5);
142142
}
143143

144+
.tag-selector {
145+
display: flex;
146+
flex-wrap: wrap;
147+
gap: 10px;
148+
}
149+
150+
.tag-option {
151+
padding: 10px 16px;
152+
border-radius: 999px;
153+
border: 1px solid rgba(124, 92, 255, 0.22);
154+
background: rgba(124, 92, 255, 0.08);
155+
color: var(--write-muted);
156+
font-size: 13px;
157+
font-weight: 600;
158+
cursor: pointer;
159+
-webkit-appearance: none;
160+
appearance: none;
161+
transition: transform 0.16s ease, box-shadow 0.16s ease, background-color 0.16s ease, color 0.16s ease, border-color 0.16s ease;
162+
}
163+
164+
.tag-option:hover:not(:disabled) {
165+
transform: translateY(-1px);
166+
background: rgba(124, 92, 255, 0.16);
167+
border-color: rgba(124, 92, 255, 0.34);
168+
color: var(--write-text);
169+
box-shadow: 0 14px 24px -20px rgba(124, 92, 255, 0.4);
170+
}
171+
172+
.tag-option.is-active {
173+
background: var(--write-accent);
174+
border-color: var(--write-accent);
175+
color: #ffffff;
176+
box-shadow: 0 16px 32px -24px rgba(124, 92, 255, 0.6);
177+
}
178+
179+
.tag-option:disabled {
180+
cursor: not-allowed;
181+
opacity: 0.55;
182+
box-shadow: none;
183+
}
184+
144185
.field-helper {
145186
margin: 0;
146187
font-size: 12px;
147188
color: var(--write-muted);
148189
}
149190

191+
.tag-helper-count {
192+
margin-left: 8px;
193+
font-weight: 600;
194+
color: var(--write-accent);
195+
}
196+
197+
.tag-limit-notice {
198+
display: inline-flex;
199+
align-items: center;
200+
gap: 6px;
201+
margin: 4px 0 0;
202+
font-size: 12px;
203+
font-weight: 600;
204+
color: var(--write-accent);
205+
}
206+
150207
.editor-field {
151208
display: flex;
152209
flex-direction: column;
@@ -338,10 +395,41 @@
338395
align-items: center;
339396
padding: 6px 12px;
340397
border-radius: 999px;
398+
border: 1px solid transparent;
341399
background: var(--write-soft);
342400
color: var(--write-accent);
343401
font-size: 13px;
344402
font-weight: 600;
403+
cursor: pointer;
404+
-webkit-appearance: none;
405+
appearance: none;
406+
transition: transform 0.16s ease, box-shadow 0.16s ease, background-color 0.16s ease, color 0.16s ease, border-color 0.16s ease;
407+
}
408+
409+
.tag-chip:hover:not(:disabled) {
410+
transform: translateY(-1px);
411+
background: rgba(124, 92, 255, 0.16);
412+
border-color: rgba(124, 92, 255, 0.32);
413+
color: var(--write-text);
414+
}
415+
416+
.tag-chip.is-active {
417+
background: var(--write-accent);
418+
border-color: var(--write-accent);
419+
color: #ffffff;
420+
box-shadow: 0 16px 32px -26px rgba(124, 92, 255, 0.6);
421+
}
422+
423+
.tag-chip:disabled {
424+
opacity: 0.55;
425+
cursor: not-allowed;
426+
box-shadow: none;
427+
}
428+
429+
.tag-option:focus-visible,
430+
.tag-chip:focus-visible {
431+
outline: 2px solid rgba(124, 92, 255, 0.55);
432+
outline-offset: 2px;
345433
}
346434

347435
.side-reminder {

0 commit comments

Comments
 (0)