Skip to content

Commit 3a34edf

Browse files
authored
Merge pull request #110 from FE9-2/feat/albaList
feat: ๊ฒ€์ƒ‰ ์„น์…˜ ์ถ”๊ฐ€ ๋ฐ ์Šคํ† ๋ฆฌ๋ถ ๋ฐ˜์˜
2 parents 7457111 + 6eed0d4 commit 3a34edf

File tree

8 files changed

+360
-105
lines changed

8 files changed

+360
-105
lines changed
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
"use client";
2+
3+
import { useRouter, useSearchParams } from "next/navigation";
4+
import { useState } from "react";
5+
import SearchInput from "@/app/components/input/text/SearchInput";
6+
7+
export default function SearchSection() {
8+
const router = useRouter();
9+
const searchParams = useSearchParams();
10+
const [keyword, setKeyword] = useState(searchParams.get("keyword") || "");
11+
12+
const handleSubmit = (e: React.FormEvent) => {
13+
e.preventDefault();
14+
const params = new URLSearchParams(searchParams);
15+
16+
if (keyword.trim()) {
17+
params.set("keyword", keyword);
18+
} else {
19+
params.delete("keyword");
20+
}
21+
22+
router.push(`/albaList?${params.toString()}`);
23+
};
24+
25+
return (
26+
<form onSubmit={handleSubmit} className="w-full">
27+
<div className="mx-auto flex items-center justify-between gap-4">
28+
<div className="w-[270px] md:w-[500px] lg:w-[700px] xl:w-[900px] 2xl:w-[1100px]">
29+
<SearchInput
30+
value={keyword}
31+
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setKeyword(e.target.value)}
32+
className="h-10 w-full bg-background-200 hover:bg-background-300"
33+
/>
34+
</div>
35+
<button
36+
type="submit"
37+
className="rounded-lg bg-[#FFB800] px-4 py-2.5 text-sm font-semibold text-white transition-colors hover:bg-[#FFA800] md:px-8 md:text-base lg:px-12 lg:text-lg xl:px-16 xl:text-xl"
38+
>
39+
๊ฒ€์ƒ‰
40+
</button>
41+
</div>
42+
</form>
43+
);
44+
}

โ€Žsrc/app/(pages)/albaList/page.tsxโ€Ž

Lines changed: 74 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,25 @@ import { filterRecruitingOptions } from "@/constants/filterOptions";
88
import { useRouter, usePathname, useSearchParams } from "next/navigation";
99
import SortSection from "./components/SortSection";
1010
import AlbaListItem from "@/app/components/card/cardList/AlbaListItem";
11+
import SearchSection from "./components/SearchSection";
12+
import { useUser } from "@/hooks/queries/user/me/useUser";
13+
import Link from "next/link";
14+
import { IoAdd } from "react-icons/io5";
1115

1216
const FORMS_PER_PAGE = 10;
1317

1418
export default function AlbaList() {
1519
const router = useRouter();
1620
const pathname = usePathname();
1721
const searchParams = useSearchParams();
22+
const { user } = useUser();
23+
const isOwner = user?.role === "owner";
1824

19-
// URL ์ฟผ๋ฆฌ ํŒŒ๋ผ๋ฏธํ„ฐ์—์„œ ํ•„ํ„ฐ ์ƒํƒœ ๊ฐ€์ ธ์˜ค๊ธฐ
25+
// URL ์ฟผ๋ฆฌ ํŒŒ๋ผ๋ฏธํ„ฐ์—์„œ ํ•„ํ„ฐ ์ƒํƒœ์™€ ํ‚ค์›Œ๋“œ ๊ฐ€์ ธ์˜ค๊ธฐ
2026
const isRecruiting = searchParams.get("isRecruiting");
27+
const keyword = searchParams.get("keyword");
2128

22-
// ์ดˆ๊ธฐ ๋งˆ์šดํŠธ ์‹œ ํ•„ํ„ฐ ๊ฐ’ ์„ค์ •
29+
// ์ดˆ๊ธฐ ๋งˆ์šดํŠธ ์‹œ ํ•„ ๊ฐ’ ์„ค์ •
2330
useEffect(() => {
2431
const params = new URLSearchParams(searchParams);
2532
if (!params.has("isRecruiting")) {
@@ -39,6 +46,7 @@ export default function AlbaList() {
3946
const { data, isLoading, error, hasNextPage, fetchNextPage, isFetchingNextPage } = useForms({
4047
limit: FORMS_PER_PAGE,
4148
isRecruiting: isRecruiting === "true" ? true : isRecruiting === "false" ? false : undefined,
49+
keyword: keyword || undefined,
4250
});
4351

4452
// ๋ชจ์ง‘ ์—ฌ๋ถ€ ํ•„ํ„ฐ ๋ณ€๊ฒฝ ํ•จ์ˆ˜
@@ -89,47 +97,76 @@ export default function AlbaList() {
8997

9098
return (
9199
<div className="flex min-h-screen flex-col items-center">
92-
{/* ํ•„ํ„ฐ ๋“œ๋กญ๋‹ค์šด ์„น์…˜ */}
93-
<div className="w-full border-b border-grayscale-100 bg-white">
94-
<div className="mx-auto flex max-w-screen-2xl items-center justify-between gap-2 px-4 py-4 md:px-6 lg:px-8">
95-
<FilterDropdown
96-
options={filterRecruitingOptions.map((option) => option.label)}
97-
initialValue={getInitialRecruitingValue(isRecruiting)}
98-
onChange={handleRecruitingFilter}
99-
/>
100-
<SortSection />
100+
{/* ๊ฒ€์ƒ‰ ์„น์…˜๊ณผ ํ•„ํ„ฐ ๋“œ๋กญ๋‹ค์šด์„ ๊ณ ์ • ์œ„์น˜๋กœ ์„ค์ • */}
101+
<div className="fixed left-0 right-0 top-16 z-40 bg-white shadow-sm">
102+
{/* ๊ฒ€์ƒ‰ ์„น์…˜ */}
103+
<div className="w-full border-b border-grayscale-100">
104+
<div className="mx-auto flex max-w-screen-2xl flex-col gap-4 px-4 py-4 md:px-6 lg:px-8">
105+
<div className="flex items-center justify-between">
106+
<SearchSection />
107+
</div>
108+
</div>
101109
</div>
102-
</div>
103110

104-
{/* ์•Œ๋ฐ”ํผ ๋ชฉ๋ก ๋žœ๋”๋ง */}
105-
{!data?.pages?.[0]?.data?.length ? (
106-
<div className="flex h-[calc(100vh-200px)] items-center justify-center">
107-
<p className="text-grayscale-500">๋“ฑ๋ก๋œ ์•Œ๋ฐ” ๊ณต๊ณ ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.</p>
111+
{/* ํ•„ํ„ฐ ๋“œ๋กญ๋‹ค์šด ์„น์…˜ */}
112+
<div className="w-full border-b border-grayscale-100">
113+
<div className="mx-auto flex max-w-screen-2xl items-center justify-between gap-2 px-4 py-4 md:px-6 lg:px-8">
114+
<FilterDropdown
115+
options={filterRecruitingOptions.map((option) => option.label)}
116+
initialValue={getInitialRecruitingValue(isRecruiting)}
117+
onChange={handleRecruitingFilter}
118+
/>
119+
<div className="flex items-center gap-4">
120+
<SortSection />
121+
</div>
122+
</div>
108123
</div>
109-
) : (
110-
<div className="mx-auto mt-4 w-full max-w-screen-2xl px-4 md:px-6 lg:px-8">
111-
<div className="flex flex-wrap items-center justify-center gap-6 space-x-6">
112-
{data?.pages.map((page) => (
113-
<React.Fragment key={page.nextCursor}>
114-
{page.data.map((form) => (
115-
<div key={form.id}>
116-
<AlbaListItem {...form} />
117-
</div>
118-
))}
119-
</React.Fragment>
120-
))}
124+
</div>
125+
126+
{/* ๋ฉ”์ธ ์ฝ˜ํ…์ธ  ์˜์—ญ */}
127+
<div className="w-full pt-[224px]">
128+
{/* ํผ ๋งŒ๋“ค๊ธฐ ๋ฒ„ํŠผ - ๊ณ ์ • ์œ„์น˜ */}
129+
{isOwner && (
130+
<div className="fixed bottom-[28%] right-8 z-[9999] translate-y-1/2 md:right-12 lg:right-16 xl:right-20">
131+
<Link
132+
href="/addForm"
133+
className="flex items-center gap-2 rounded-lg bg-[#FFB800] px-4 py-3 text-base font-semibold text-white shadow-lg transition-all hover:bg-[#FFA800] md:px-6 md:text-lg"
134+
>
135+
<IoAdd className="size-6" />
136+
<span>ํผ ๋งŒ๋“ค๊ธฐ</span>
137+
</Link>
121138
</div>
139+
)}
122140

123-
{/* ๋ฌดํ•œ ์Šคํฌ๋กค ํŠธ๋ฆฌ๊ฑฐ ์˜์—ญ */}
124-
<div ref={ref} className="h-4 w-full">
125-
{isFetchingNextPage && (
126-
<div className="flex justify-center py-4">
127-
<div className="h-6 w-6 animate-spin rounded-full border-2 border-primary-orange-300 border-t-transparent" />
128-
</div>
129-
)}
141+
{!data?.pages?.[0]?.data?.length ? (
142+
<div className="flex h-[calc(100vh-200px)] flex-col items-center justify-center">
143+
<p className="text-grayscale-500">๋“ฑ๋ก๋œ ์•Œ๋ฐ” ๊ณต๊ณ ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.</p>
130144
</div>
131-
</div>
132-
)}
145+
) : (
146+
<div className="mx-auto mt-4 w-full max-w-screen-2xl px-4 md:px-6 lg:px-8">
147+
<div className="flex flex-wrap items-center justify-center gap-6">
148+
{data?.pages.map((page) => (
149+
<React.Fragment key={page.nextCursor}>
150+
{page.data.map((form) => (
151+
<div key={form.id}>
152+
<AlbaListItem {...form} />
153+
</div>
154+
))}
155+
</React.Fragment>
156+
))}
157+
</div>
158+
159+
{/* ๋ฌดํ•œ ์Šคํฌ๋กค ํŠธ๋ฆฌ๊ฑฐ ์˜์—ญ */}
160+
<div ref={ref} className="h-4 w-full">
161+
{isFetchingNextPage && (
162+
<div className="flex justify-center py-4">
163+
<div className="h-6 w-6 animate-spin rounded-full border-2 border-primary-orange-300 border-t-transparent" />
164+
</div>
165+
)}
166+
</div>
167+
</div>
168+
)}
169+
</div>
133170
</div>
134171
);
135172
}

โ€Žsrc/app/components/input/text/SearchInput.tsxโ€Ž

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,31 @@
11
import { FiSearch } from "react-icons/fi";
2-
import BaseInput from "./BaseInput";
2+
import { ChangeEventHandler } from "react";
33

4-
const SearchInput = () => {
4+
interface SearchInputProps {
5+
value?: string;
6+
onChange?: ChangeEventHandler<HTMLInputElement>;
7+
placeholder?: string;
8+
className?: string;
9+
}
10+
11+
const SearchInput = ({ value, onChange, placeholder, className }: SearchInputProps) => {
512
return (
6-
<div>
7-
<BaseInput
8-
name="search"
13+
<div className="relative w-full">
14+
{/* ๊ฒ€์ƒ‰ ์•„์ด์ฝ˜ */}
15+
<div className="absolute left-3 top-1/2 -translate-y-1/2">
16+
<FiSearch className="size-5 text-grayscale-400" />
17+
</div>
18+
19+
{/* ๊ฒ€์ƒ‰ ์ž…๋ ฅ์ฐฝ */}
20+
<input
921
type="text"
10-
variant="white"
11-
placeholder="์–ด๋–ค ์•Œ๋ฐ”๋ฅผ ์ฐพ๊ณ  ๊ณ„์„ธ์š”?"
12-
wrapperClassName="!rounded-2xl !lg:rounded-3xl"
13-
beforeIcon={<FiSearch className="size-6 text-grayscale-200 lg:size-9" />}
22+
name="search"
23+
value={value}
24+
onChange={onChange}
25+
placeholder={placeholder || "์–ด๋–ค ์•Œ๋ฐ”๋ฅผ ์ฐพ๊ณ  ๊ณ„์„ธ์š”?"}
26+
className={`rounded-lg border border-grayscale-200 pl-11 pr-4 text-sm placeholder:text-grayscale-400 focus:border-grayscale-300 focus:outline-none ${
27+
className || ""
28+
}`}
1429
/>
1530
</div>
1631
);
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
"use client";
2+
3+
import { useState } from "react";
4+
import SearchInput from "@/app/components/input/text/SearchInput";
5+
6+
export default function SearchSection() {
7+
const [keyword, setKeyword] = useState("");
8+
9+
const handleSubmit = (e: React.FormEvent) => {
10+
e.preventDefault();
11+
// ์Šคํ† ๋ฆฌ๋ถ์—์„œ๋Š” ์‹ค์ œ ๊ฒ€์ƒ‰ ๊ธฐ๋Šฅ์„ ์ˆ˜ํ–‰ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค
12+
console.log("Search keyword:", keyword);
13+
};
14+
15+
return (
16+
<form onSubmit={handleSubmit} className="w-full">
17+
<div className="mx-auto flex items-center justify-between gap-4">
18+
<div className="w-[270px] md:w-[500px] lg:w-[700px] xl:w-[900px] 2xl:w-[1100px]">
19+
<SearchInput
20+
value={keyword}
21+
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setKeyword(e.target.value)}
22+
className="h-10 w-full bg-background-200 hover:bg-background-300"
23+
/>
24+
</div>
25+
<button
26+
type="submit"
27+
className="rounded-lg bg-[#FFB800] px-4 py-2.5 text-sm font-semibold text-white transition-colors hover:bg-[#FFA800] md:px-8 md:text-base lg:px-12 lg:text-lg xl:px-16 xl:text-xl"
28+
>
29+
๊ฒ€์ƒ‰
30+
</button>
31+
</div>
32+
</form>
33+
);
34+
}
Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import SearchInput from "@/app/components/input/text/SearchInput";
22
import { Meta, StoryObj } from "@storybook/react";
3+
import { useState } from "react";
34

45
const meta = {
56
title: "Design System/Components/TextInput/SearchInput",
@@ -8,21 +9,44 @@ const meta = {
89
layout: "centered",
910
},
1011
argTypes: {
11-
errormessage: {
12-
description: "์—๋Ÿฌ ๋ฉ”์‹œ์ง€๋ฅผ ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค",
12+
value: {
13+
description: "๊ฒ€์ƒ‰์–ด ๊ฐ’",
1314
control: "text",
1415
},
15-
feedbackMessage: {
16-
description: "ํ”ผ๋“œ๋ฐฑ ๋ฉ”์‹œ์ง€๋ฅผ ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค",
16+
placeholder: {
17+
description: "ํ”Œ๋ ˆ์ด์Šคํ™€๋” ํ…์ŠคํŠธ",
18+
control: "text",
19+
},
20+
className: {
21+
description: "์ถ”๊ฐ€ ์Šคํƒ€์ผ ํด๋ž˜์Šค",
1722
control: "text",
1823
},
1924
},
2025
} satisfies Meta<typeof SearchInput>;
2126

2227
export default meta;
23-
2428
type Story = StoryObj<typeof SearchInput>;
2529

26-
export const Search: Story = {
27-
args: { name: "search" },
30+
// ์ƒํ˜ธ์ž‘์šฉ์„ ์œ„ํ•œ ๋ž˜ํผ ์ปดํฌ๋„ŒํŠธ
31+
const SearchInputWithHooks = (args: Story["args"]) => {
32+
const [value, setValue] = useState("");
33+
return <SearchInput {...args} value={value} onChange={(e) => setValue(e.target.value)} />;
34+
};
35+
36+
export const Default: Story = {
37+
render: (args) => <SearchInputWithHooks {...args} />,
38+
};
39+
40+
export const WithPlaceholder: Story = {
41+
render: (args) => <SearchInputWithHooks {...args} />,
42+
args: {
43+
placeholder: "๊ฒ€์ƒ‰์–ด๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”",
44+
},
45+
};
46+
47+
export const WithCustomStyle: Story = {
48+
render: (args) => <SearchInputWithHooks {...args} />,
49+
args: {
50+
className: "bg-gray-100",
51+
},
2852
};
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
"use client";
2+
3+
import Image from "next/image";
4+
import Link from "next/link";
5+
import { cn } from "@/lib/tailwindUtil";
6+
7+
export default function Header() {
8+
const getLinkClassName = (path: string) => {
9+
return cn(
10+
"font-medium transition-colors h-16 flex items-center",
11+
"hover:text-lime-900",
12+
"text-lime-700 text-sm sm:text-base"
13+
);
14+
};
15+
16+
return (
17+
<header className="fixed left-0 right-0 top-0 z-50 bg-lime-100 -tracking-widest md:tracking-normal">
18+
<div className="container mx-auto px-4">
19+
<nav className="flex h-16 items-center justify-between">
20+
{/* ๋กœ๊ณ ์™€ ๋ฉ”์ธ ๋„ค๋น„๊ฒŒ์ด์…˜ */}
21+
<div className="flex items-center">
22+
<Link href="/" className="text-xl text-white hover:text-blue-100">
23+
<Image
24+
src="/logo.svg"
25+
alt="Work Root Logo"
26+
width={200}
27+
height={60}
28+
className="w-32 hover:opacity-90 sm:w-40 md:w-[200px]"
29+
/>
30+
</Link>
31+
32+
<div className="ml-4 flex h-16 space-x-2 sm:ml-6 sm:space-x-4 md:ml-10 md:space-x-6">
33+
<Link href="/albaList" className={getLinkClassName("/albaList")}>
34+
์•Œ๋ฐ” ๋ชฉ๋ก
35+
</Link>
36+
<Link href="/albaTalk" className={getLinkClassName("/albaTalk")}>
37+
์•Œ๋ฐ” ํ† ํฌ
38+
</Link>
39+
<Link href="/myAlbaform" className={getLinkClassName("/myAlbaform")}>
40+
๋‚ด ์•Œ๋ฐ”ํผ
41+
</Link>
42+
</div>
43+
</div>
44+
45+
{/* ๋กœ๊ทธ์ธ/ํšŒ์›๊ฐ€์ž… ๋ฒ„ํŠผ */}
46+
<ul className="flex items-center space-x-2 sm:space-x-4 md:space-x-6">
47+
<li className="flex items-center">
48+
<Link
49+
href="/login"
50+
className="rounded-lg border-2 border-lime-700 px-2 py-1 text-sm text-lime-700 transition-colors hover:bg-lime-700 hover:text-white sm:px-3 sm:py-1.5 sm:text-base md:px-4 md:py-2"
51+
>
52+
๋กœ๊ทธ์ธ
53+
</Link>
54+
</li>
55+
<li className="flex items-center">
56+
<Link
57+
href="/signup"
58+
className="rounded-lg bg-lime-700 px-2 py-1 text-sm font-semibold text-white transition-colors hover:bg-lime-800 sm:px-3 sm:py-1.5 sm:text-base md:px-4 md:py-2"
59+
>
60+
ํšŒ์›๊ฐ€์ž…
61+
</Link>
62+
</li>
63+
</ul>
64+
</nav>
65+
</div>
66+
</header>
67+
);
68+
}

0 commit comments

Comments
ย (0)