Skip to content

Commit c6c8ee5

Browse files
committed
feat: 커뮤니티 화면 UI 개선
1 parent 1f80bd5 commit c6c8ee5

7 files changed

Lines changed: 244 additions & 106 deletions

File tree

src/app/(main)/community/page.tsx

Lines changed: 66 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,74 @@
11
"use client";
22

3-
import { useState } from "react";
3+
import { useState, useCallback, useEffect } from "react";
4+
import { useSearchParams, useRouter } from "next/navigation";
45
import Category from "@/components/page/community/Category";
56
import PostList from "@/components/page/community/PostList";
67
import PopularPostsHighlight from "@/components/page/community/PopularPostsHighlight";
7-
import PageTitle from "@/components/commons/PageTitle";
8-
import { Button } from "@/components/ui/button";
98
import { Plus, Search, X } from "lucide-react";
109
import PostCreateModal from "@/components/page/community/PostCreateModal";
1110

1211
type SortType = "latest" | "popular" | "mostLiked";
1312

1413
function CommunityPage() {
14+
const searchParams = useSearchParams();
15+
const router = useRouter();
16+
const keyword = searchParams.get("q") || "";
17+
1518
const [selectedBoardId, setSelectedBoardId] = useState<number | undefined>(
1619
undefined,
1720
);
1821
const [sortOption, setSortOption] = useState<SortType>("latest");
1922
const [isModalOpen, setIsModalOpen] = useState(false);
20-
const [searchInput, setSearchInput] = useState("");
21-
const [keyword, setKeyword] = useState("");
22-
23-
const handleSearch = (e: React.FormEvent) => {
24-
e.preventDefault();
25-
setKeyword(searchInput.trim());
26-
};
23+
const [mobileSearchInput, setMobileSearchInput] = useState(keyword);
24+
25+
// URL의 검색어가 변경되면 모바일 input 동기화
26+
useEffect(() => {
27+
setMobileSearchInput(keyword);
28+
}, [keyword]);
29+
30+
const handleMobileSearch = useCallback(
31+
(e: React.FormEvent) => {
32+
e.preventDefault();
33+
const trimmedInput = mobileSearchInput.trim();
34+
if (trimmedInput) {
35+
router.push(`/community?q=${encodeURIComponent(trimmedInput)}`);
36+
} else {
37+
router.push("/community");
38+
}
39+
},
40+
[mobileSearchInput, router]
41+
);
2742

28-
const handleClearSearch = () => {
29-
setSearchInput("");
30-
setKeyword("");
31-
};
43+
const handleClearMobileSearch = useCallback(() => {
44+
setMobileSearchInput("");
45+
router.push("/community");
46+
}, [router]);
3247

3348
return (
34-
<main className="flex flex-col gap-6 pb-8">
35-
<PageTitle title="커뮤니티" />
49+
<main className="flex flex-col gap-6 pb-24">
50+
{/* Mobile Search Bar - Only visible on mobile */}
51+
<form onSubmit={handleMobileSearch} className="md:hidden">
52+
<div className="relative">
53+
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400 dark:text-gray-500" />
54+
<input
55+
type="text"
56+
value={mobileSearchInput}
57+
onChange={(e) => setMobileSearchInput(e.target.value)}
58+
placeholder="게시글 검색..."
59+
className="w-full pl-10 pr-10 py-2.5 text-sm border border-gray-200 dark:border-navy-600 rounded-xl bg-white dark:bg-navy-700 text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-[var(--color-ds-primary)]/20 focus:border-[var(--color-ds-primary)] transition-colors"
60+
/>
61+
{mobileSearchInput && (
62+
<button
63+
type="button"
64+
onClick={handleClearMobileSearch}
65+
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors"
66+
>
67+
<X className="w-4 h-4" />
68+
</button>
69+
)}
70+
</div>
71+
</form>
3672

3773
{/* Category Filter */}
3874
<Category
@@ -43,68 +79,29 @@ function CommunityPage() {
4379
{/* Popular Posts Highlight */}
4480
<PopularPostsHighlight boardId={selectedBoardId} />
4581

46-
{/* Search Bar */}
47-
<form onSubmit={handleSearch} className="relative">
48-
<div className="flex gap-2">
49-
<div className="relative flex-1">
50-
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400" />
51-
<input
52-
type="text"
53-
value={searchInput}
54-
onChange={(e) => setSearchInput(e.target.value)}
55-
placeholder="게시글 검색..."
56-
className="w-full pl-10 pr-10 py-2.5 border border-warm-300 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-forest-500/20 focus:border-forest-500 bg-white"
57-
/>
58-
{searchInput && (
59-
<button
60-
type="button"
61-
onClick={handleClearSearch}
62-
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600"
63-
>
64-
<X className="w-4 h-4" />
65-
</button>
66-
)}
67-
</div>
68-
<Button type="submit" className="bg-forest-500 hover:bg-forest-600 rounded-xl px-6">
69-
검색
70-
</Button>
71-
</div>
72-
</form>
73-
7482
{/* Search Result Info */}
7583
{keyword && (
76-
<div className="flex items-center gap-2 text-sm text-gray-600">
84+
<div className="flex items-center gap-2 text-sm text-gray-600 dark:text-gray-400">
7785
<span>&quot;{keyword}&quot; 검색 결과</span>
78-
<button
79-
onClick={handleClearSearch}
80-
className="text-forest-500 hover:text-forest-600 underline"
81-
>
82-
검색 초기화
83-
</button>
8486
</div>
8587
)}
8688

87-
{/* Actions Bar */}
88-
<div className="flex justify-between items-center">
89+
{/* Sort Options */}
90+
<div className="flex items-center">
8991
<select
9092
value={sortOption}
9193
onChange={(e) => setSortOption(e.target.value as SortType)}
92-
className="border border-warm-300 rounded-xl px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-forest-500/20 focus:border-forest-500 bg-white text-warm-700"
94+
className="border border-gray-200 dark:border-navy-600 rounded-xl px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-[var(--color-ds-primary)]/20 focus:border-[var(--color-ds-primary)] bg-white dark:bg-navy-700 text-gray-700 dark:text-gray-200"
9395
>
9496
<option value="latest">최신순</option>
9597
<option value="popular">인기순</option>
9698
<option value="mostLiked">좋아요순</option>
9799
</select>
98-
99-
<Button className="gap-2 bg-forest-500 hover:bg-forest-600 rounded-xl" onClick={() => setIsModalOpen(true)}>
100-
<Plus className="w-4 h-4" />
101-
글쓰기
102-
</Button>
103100
</div>
104101

105102
{/* Info: 인기순/좋아요순은 게시판 선택 필요 */}
106103
{(sortOption === "popular" || sortOption === "mostLiked") && !selectedBoardId && (
107-
<div className="text-sm text-amber-600 bg-amber-50 px-4 py-2 rounded-lg">
104+
<div className="text-sm text-amber-600 dark:text-amber-400 bg-amber-50 dark:bg-amber-900/20 px-4 py-2 rounded-lg">
108105
인기순/좋아요순은 게시판을 선택해야 적용됩니다.
109106
</div>
110107
)}
@@ -116,6 +113,15 @@ function CommunityPage() {
116113
sortType={sortOption}
117114
/>
118115

116+
{/* FAB - Write Post Button */}
117+
<button
118+
onClick={() => setIsModalOpen(true)}
119+
className="fixed bottom-20 right-4 md:bottom-6 md:right-6 z-40 w-14 h-14 bg-[var(--color-ds-primary)] hover:bg-[var(--color-ds-primary-hover)] text-white rounded-full shadow-lg hover:shadow-xl transition-all duration-200 flex items-center justify-center hover:scale-105 active:scale-95"
120+
aria-label="글쓰기"
121+
>
122+
<Plus className="w-6 h-6" />
123+
</button>
124+
119125
{/* Post Create Modal */}
120126
<PostCreateModal
121127
isOpen={isModalOpen}

src/components/bottom-nav.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export default function BottomNav() {
2424
};
2525

2626
return (
27-
<nav className="fixed bottom-0 left-0 right-0 z-50 bg-white border-t border-gray-200 safe-area-inset-bottom">
27+
<nav className="fixed bottom-0 left-0 right-0 z-50 bg-white dark:bg-navy-800 border-t border-gray-200 dark:border-navy-700 safe-area-inset-bottom">
2828
<div className="flex items-center justify-around h-14">
2929
{navItems.map(({ href, label, ready }) => {
3030
const isActive =
@@ -40,8 +40,8 @@ export default function BottomNav() {
4040
className={clsx(
4141
"relative flex items-center justify-center flex-1 h-full transition-all duration-200",
4242
isActive
43-
? "text-blue-600"
44-
: "text-gray-500 hover:text-gray-700",
43+
? "text-blue-600 dark:text-blue-400"
44+
: "text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200",
4545
)}
4646
>
4747
<span
@@ -53,7 +53,7 @@ export default function BottomNav() {
5353
{label}
5454
</span>
5555
{isActive && (
56-
<span className="absolute bottom-0 left-1/2 -translate-x-1/2 w-8 h-0.5 bg-blue-600 rounded-full" />
56+
<span className="absolute bottom-0 left-1/2 -translate-x-1/2 w-8 h-0.5 bg-blue-600 dark:bg-blue-400 rounded-full" />
5757
)}
5858
</Link>
5959
);

src/components/navigation/ThreeTierNav.tsx

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ export default function ThreeTierNav() {
6363
const isHornBuglePage = pathname.startsWith("/horn-bugle");
6464
// 아이템 정보 페이지인지 확인
6565
const isItemInfoPage = pathname.startsWith("/item-info");
66+
// 게시판 페이지인지 확인
67+
const isCommunityPage = pathname.startsWith("/community");
6668
// 검색 가능한 페이지 (경매장 + 아이템 정보)
6769
const isSearchablePage = isAuctionPage || isItemInfoPage;
6870

@@ -247,6 +249,17 @@ export default function ThreeTierNav() {
247249
setIsSearchFocused(false);
248250
}, [searchValue, router]);
249251

252+
// 게시판 검색 제출 로직
253+
const handleCommunitySearchSubmit = useCallback(() => {
254+
const trimmed = searchValue.trim();
255+
if (trimmed) {
256+
router.push(`/community?q=${encodeURIComponent(trimmed)}`);
257+
} else {
258+
router.push("/community");
259+
}
260+
setIsSearchFocused(false);
261+
}, [searchValue, router]);
262+
250263
const handleKeyDown = useCallback(
251264
(e: React.KeyboardEvent<HTMLInputElement>) => {
252265
// 뿔피리 페이지: Enter 입력 시 검색
@@ -261,6 +274,18 @@ export default function ThreeTierNav() {
261274
return;
262275
}
263276

277+
// 게시판 페이지: Enter 입력 시 검색
278+
if (isCommunityPage) {
279+
if (e.key === "Enter") {
280+
e.preventDefault();
281+
handleCommunitySearchSubmit();
282+
} else if (e.key === "Escape") {
283+
e.preventDefault();
284+
setIsSearchFocused(false);
285+
}
286+
return;
287+
}
288+
264289
if (!isSearchablePage) return;
265290

266291
if (e.key === "Enter") {
@@ -294,12 +319,14 @@ export default function ThreeTierNav() {
294319
[
295320
isSearchablePage,
296321
isHornBuglePage,
322+
isCommunityPage,
297323
searchValue,
298324
filteredItems,
299325
recentSearches,
300326
selectedIndex,
301327
handleSearchSubmit,
302328
handleHornBugleSearchSubmit,
329+
handleCommunitySearchSubmit,
303330
]
304331
);
305332

@@ -366,7 +393,15 @@ export default function ThreeTierNav() {
366393
</button>
367394
)}
368395
<button
369-
onClick={isHornBuglePage ? handleHornBugleSearchSubmit : (isSearchablePage ? handleSearchSubmit : undefined)}
396+
onClick={
397+
isHornBuglePage
398+
? handleHornBugleSearchSubmit
399+
: isCommunityPage
400+
? handleCommunitySearchSubmit
401+
: isSearchablePage
402+
? handleSearchSubmit
403+
: undefined
404+
}
370405
className="p-1.5 rounded-lg text-gray-500 dark:text-gray-300 hover:text-blaanid-600 dark:hover:text-coral-400 hover:bg-gray-100 dark:hover:bg-navy-600 transition-colors"
371406
aria-label="검색"
372407
type="button"

src/components/page/community/Category.tsx

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"use client";
22

33
import { useCallback, useEffect, useState } from "react";
4-
import { Button } from "@/components/ui/button";
4+
import clsx from "clsx";
55
import { Board, ApiResponse, BoardListData } from "@/types/community";
66
import CategorySkeleton from "./CategorySkeleton";
77
import DataFetchError from "@/components/commons/DataFetchError";
@@ -70,33 +70,41 @@ function Category({ selectedBoardId, setSelectedBoardId }: CategoryProps) {
7070
return (
7171
<div className="flex flex-nowrap gap-2 overflow-x-auto pb-2 scrollbar-hide">
7272
{/* 전체 버튼 */}
73-
<Button
74-
variant={selectedBoardId === undefined ? "default" : "outline"}
73+
<button
7574
onClick={() => setSelectedBoardId(undefined)}
76-
className="flex-shrink-0"
75+
className={clsx(
76+
"px-4 py-2 rounded-xl text-sm font-medium transition-all flex-shrink-0",
77+
selectedBoardId === undefined
78+
? "bg-[var(--color-ds-primary)] text-white shadow-md"
79+
: "bg-white dark:bg-navy-700 text-gray-700 dark:text-gray-200 border border-gray-200 dark:border-navy-600 hover:bg-gray-50 dark:hover:bg-navy-600"
80+
)}
7781
>
7882
전체
79-
</Button>
83+
</button>
8084

8185
{/* 카테고리별 게시판 */}
8286
{categories.map((category) => (
83-
<Button
87+
<button
8488
key={category.id}
85-
variant={selectedBoardId === category.id ? "default" : "outline"}
8689
onClick={() => setSelectedBoardId(category.id)}
87-
className="flex-shrink-0 relative group"
90+
className={clsx(
91+
"px-4 py-2 rounded-xl text-sm font-medium transition-all flex-shrink-0 relative group",
92+
selectedBoardId === category.id
93+
? "bg-[var(--color-ds-primary)] text-white shadow-md"
94+
: "bg-white dark:bg-navy-700 text-gray-700 dark:text-gray-200 border border-gray-200 dark:border-navy-600 hover:bg-gray-50 dark:hover:bg-navy-600"
95+
)}
8896
>
8997
<span>{category.name}</span>
9098
{/* 툴팁 */}
91-
<div className="hidden group-hover:block absolute bottom-full left-1/2 -translate-x-1/2 mb-2 px-3 py-2 bg-gray-900 text-white text-xs rounded-lg whitespace-nowrap z-10">
99+
<div className="hidden group-hover:block absolute bottom-full left-1/2 -translate-x-1/2 mb-2 px-3 py-2 bg-gray-900 dark:bg-gray-800 text-white text-xs rounded-lg whitespace-nowrap z-10 shadow-lg">
92100
<div>{category.description}</div>
93101
<div className="text-gray-400 text-[10px] mt-1">
94102
{category.topCategory} &gt; {category.subCategory}
95103
</div>
96104
{/* 화살표 */}
97-
<div className="absolute top-full left-1/2 -translate-x-1/2 -mt-1 border-4 border-transparent border-t-gray-900" />
105+
<div className="absolute top-full left-1/2 -translate-x-1/2 -mt-1 border-4 border-transparent border-t-gray-900 dark:border-t-gray-800" />
98106
</div>
99-
</Button>
107+
</button>
100108
))}
101109
</div>
102110
);

0 commit comments

Comments
 (0)