Skip to content

Commit c0a1e5a

Browse files
authored
Merge pull request #116 from FE9-2/feat/myAlbaform
feat: 내 알바폼 > 사장님 추가
2 parents a7ef50c + e898364 commit c0a1e5a

File tree

17 files changed

+990
-603
lines changed

17 files changed

+990
-603
lines changed

.storybook/main.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,18 @@ const config: StorybookConfig = {
2929
docs: {
3030
autodocs: "tag",
3131
},
32+
webpackFinal: async (config) => {
33+
// webpack 설정 수정
34+
config.resolve = {
35+
...config.resolve,
36+
fallback: {
37+
...config.resolve?.fallback,
38+
fs: false,
39+
path: false,
40+
},
41+
};
42+
return config;
43+
},
3244
};
3345

3446
export default config;

next.config.mjs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
/** @type {import('next').NextConfig} */
22

3+
import path from "path";
4+
import { fileURLToPath } from "url";
5+
6+
const __filename = fileURLToPath(import.meta.url);
7+
const __dirname = path.dirname(__filename);
8+
39
const nextConfig = {
410
reactStrictMode: true,
511
swcMinify: true,
@@ -19,6 +25,12 @@ const nextConfig = {
1925
use: ["@svgr/webpack"],
2026
});
2127

28+
config.module.rules.push({
29+
test: /\.css$/,
30+
use: ["style-loader", "css-loader", "postcss-loader"],
31+
include: [path.resolve(__dirname, "node_modules/react-datepicker"), path.resolve(__dirname, "src/app")],
32+
});
33+
2234
return config;
2335
},
2436
};

package-lock.json

Lines changed: 557 additions & 550 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@
3737
},
3838
"devDependencies": {
3939
"@chromatic-com/storybook": "^3.2.2",
40+
"@jridgewell/gen-mapping": "^0.3.7",
41+
"@jridgewell/source-map": "^0.3.6",
4042
"@storybook/addon-essentials": "^8.4.4",
4143
"@storybook/addon-interactions": "^8.4.4",
4244
"@storybook/addon-links": "^8.4.7",

src/app/(pages)/albaList/page.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ import { useForms } from "@/hooks/queries/form/useForms";
66
import FilterDropdown from "@/app/components/button/dropdown/FilterDropdown";
77
import { filterRecruitingOptions } from "@/constants/filterOptions";
88
import { useRouter, usePathname, useSearchParams } from "next/navigation";
9-
import SortSection from "./components/SortSection";
9+
import SortSection from "@/app/components/layout/forms/SortSection";
1010
import AlbaListItem from "@/app/components/card/cardList/AlbaListItem";
11-
import SearchSection from "./components/SearchSection";
11+
import SearchSection from "@/app/components/layout/forms/SearchSection";
1212
import { useUser } from "@/hooks/queries/user/me/useUser";
1313
import Link from "next/link";
1414
import { IoAdd } from "react-icons/io5";
@@ -26,6 +26,7 @@ export default function AlbaList() {
2626
// URL 쿼리 파라미터에서 필터 상태와 키워드 가져오기
2727
const isRecruiting = searchParams.get("isRecruiting");
2828
const keyword = searchParams.get("keyword");
29+
const orderBy = searchParams.get("orderBy");
2930

3031
// 초기 마운트 시 필 값 설정
3132
useEffect(() => {
@@ -48,6 +49,7 @@ export default function AlbaList() {
4849
limit: FORMS_PER_PAGE,
4950
isRecruiting: isRecruiting === "true" ? true : isRecruiting === "false" ? false : undefined,
5051
keyword: keyword || undefined,
52+
orderBy: orderBy || undefined,
5153
});
5254

5355
// 모집 여부 필터 변경 함수
@@ -118,7 +120,7 @@ export default function AlbaList() {
118120
onChange={handleRecruitingFilter}
119121
/>
120122
<div className="flex items-center gap-4">
121-
<SortSection />
123+
<SortSection pathname={pathname} searchParams={searchParams} />
122124
</div>
123125
</div>
124126
</div>
@@ -130,7 +132,7 @@ export default function AlbaList() {
130132
{isOwner && (
131133
<div className="fixed bottom-[50%] right-4 z-[9999] translate-y-1/2">
132134
<Link
133-
href="/addForm"
135+
href="/addform"
134136
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"
135137
>
136138
<IoAdd className="size-6" />
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
"use client";
2+
3+
import { useEffect } from "react";
4+
import { useRouter } from "next/navigation";
5+
import { useUser } from "@/hooks/queries/user/me/useUser";
6+
import { userRoles } from "@/constants/userRoles";
7+
8+
export default function ApplicantPage() {
9+
const router = useRouter();
10+
const { user, isLoading } = useUser();
11+
12+
useEffect(() => {
13+
if (!isLoading) {
14+
if (!user) {
15+
router.push("/login");
16+
} else if (user.role === userRoles.OWNER) {
17+
router.push("/myAlbaform/owner");
18+
}
19+
}
20+
}, [user, isLoading, router]);
21+
22+
if (isLoading) {
23+
return (
24+
<div className="flex h-[calc(100vh-200px)] items-center justify-center">
25+
<div>로딩 중...</div>
26+
</div>
27+
);
28+
}
29+
30+
// 지원자용 페이지 컨텐츠
31+
return (
32+
<div>
33+
<h1>지원자 페이지</h1>
34+
{/* 지원자용 컨텐츠 */}
35+
</div>
36+
);
37+
}
Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
"use client";
2+
3+
import React, { useEffect } from "react";
4+
import { useInView } from "react-intersection-observer";
5+
import { useMyForms } from "@/hooks/queries/user/me/useMyForms";
6+
import FilterDropdown from "@/app/components/button/dropdown/FilterDropdown";
7+
import { filterRecruitingOptions, filterPublicOptions } from "@/constants/filterOptions";
8+
import { useRouter, usePathname, useSearchParams } from "next/navigation";
9+
import SortSection from "@/app/components/layout/forms/SortSection";
10+
import AlbaListItem from "@/app/components/card/cardList/AlbaListItem";
11+
import SearchSection from "@/app/components/layout/forms/SearchSection";
12+
import { useUser } from "@/hooks/queries/user/me/useUser";
13+
import Link from "next/link";
14+
import { IoAdd } from "react-icons/io5";
15+
import { userRoles } from "@/constants/userRoles";
16+
17+
const FORMS_PER_PAGE = 10;
18+
19+
export default function AlbaList() {
20+
const router = useRouter();
21+
const pathname = usePathname();
22+
const searchParams = useSearchParams();
23+
const { user, isLoading } = useUser();
24+
const isOwner = user?.role === userRoles.OWNER;
25+
26+
useEffect(() => {
27+
if (!isLoading) {
28+
if (!user) {
29+
router.push("/login");
30+
} else if (user.role !== userRoles.OWNER) {
31+
router.push("/myAlbaform/applicant");
32+
}
33+
}
34+
}, [user, isLoading, router]);
35+
36+
// URL 쿼리 파라미터에서 필터 상태와 키워드 가져오기
37+
const isPublic = searchParams.get("isPublic");
38+
const isRecruiting = searchParams.get("isRecruiting");
39+
const keyword = searchParams.get("keyword");
40+
const orderBy = searchParams.get("orderBy");
41+
42+
// 초기 마운트 시 필터 값 설정
43+
useEffect(() => {
44+
const params = new URLSearchParams(searchParams);
45+
let needsUpdate = false;
46+
47+
if (!params.has("isPublic")) {
48+
params.set("isPublic", "true");
49+
needsUpdate = true;
50+
}
51+
if (!params.has("isRecruiting")) {
52+
params.set("isRecruiting", "true");
53+
needsUpdate = true;
54+
}
55+
if (needsUpdate) {
56+
router.push(`${pathname}?${params.toString()}`);
57+
}
58+
}, []);
59+
60+
// 무한 스크롤을 위한 Intersection Observer 설정
61+
const { ref, inView } = useInView({
62+
threshold: 0.1,
63+
triggerOnce: false,
64+
rootMargin: "100px",
65+
});
66+
67+
// 알바폼 목록 조회
68+
const {
69+
data,
70+
isLoading: isLoadingData,
71+
error,
72+
hasNextPage,
73+
fetchNextPage,
74+
isFetchingNextPage,
75+
} = useMyForms({
76+
limit: FORMS_PER_PAGE,
77+
isPublic: isPublic === "true" ? true : isPublic === "false" ? false : undefined,
78+
isRecruiting: isRecruiting === "true" ? true : isRecruiting === "false" ? false : undefined,
79+
keyword: keyword || undefined,
80+
orderBy: orderBy || undefined,
81+
});
82+
83+
// 공개 여부 필터 변경 함수
84+
const handlePublicFilter = (selected: string) => {
85+
const option = filterPublicOptions.find((opt) => opt.label === selected);
86+
if (option) {
87+
const params = new URLSearchParams(searchParams);
88+
if (selected === "전체") {
89+
params.delete("isPublic");
90+
} else {
91+
params.set("isPublic", String(option.value));
92+
}
93+
router.push(`${pathname}?${params.toString()}`);
94+
}
95+
};
96+
97+
// 현재 필터 상태에 따른 초기값 설정
98+
const getInitialPublicValue = (isPublic: string | null) => {
99+
if (!isPublic) return "전체";
100+
const option = filterPublicOptions.find((opt) => String(opt.value) === isPublic);
101+
return option?.label || "전체";
102+
};
103+
104+
// 모집 여부 필터 변경 함수
105+
const handleRecruitingFilter = (selected: string) => {
106+
const option = filterRecruitingOptions.find((opt) => opt.label === selected);
107+
if (option) {
108+
const params = new URLSearchParams(searchParams);
109+
if (selected === "전체") {
110+
params.delete("isRecruiting");
111+
} else {
112+
params.set("isRecruiting", String(option.value));
113+
}
114+
router.push(`${pathname}?${params.toString()}`);
115+
}
116+
};
117+
118+
// 현재 필터 상태에 따른 초기값 설정
119+
const getInitialRecruitingValue = (isRecruiting: string | null) => {
120+
if (!isRecruiting) return "전체";
121+
const option = filterRecruitingOptions.find((opt) => String(opt.value) === isRecruiting);
122+
return option?.label || "전체";
123+
};
124+
125+
// 스크롤이 하단에 도달하면 다음 페이지 로드
126+
useEffect(() => {
127+
if (inView && hasNextPage && !isFetchingNextPage) {
128+
fetchNextPage();
129+
}
130+
}, [inView, hasNextPage, fetchNextPage, isFetchingNextPage]);
131+
132+
// 에러 상태 처리
133+
if (error) {
134+
return (
135+
<div className="flex h-[calc(100vh-200px)] items-center justify-center">
136+
<p className="text-red-500">알바 목록을 불러오는데 실패했습니다.</p>
137+
</div>
138+
);
139+
}
140+
141+
// 로딩 상태 처리
142+
if (isLoadingData) {
143+
return (
144+
<div className="flex h-[calc(100vh-200px)] items-center justify-center">
145+
<div>로딩 중...</div>
146+
</div>
147+
);
148+
}
149+
150+
return (
151+
<div className="flex min-h-screen flex-col items-center">
152+
{/* 검색 섹션과 필터 드롭다운을 고정 위치로 설정 */}
153+
<div className="fixed left-0 right-0 top-16 z-40 bg-white shadow-sm">
154+
{/* 검색 섹션 */}
155+
<div className="w-full border-b border-grayscale-100">
156+
<div className="mx-auto flex max-w-screen-2xl flex-col gap-4 px-4 py-4 md:px-6 lg:px-8">
157+
<div className="flex items-center justify-between">
158+
<SearchSection />
159+
</div>
160+
</div>
161+
</div>
162+
163+
{/* 필터 드롭다운 섹션 */}
164+
<div className="w-full border-b border-grayscale-100">
165+
<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">
166+
<div className="flex items-center gap-2">
167+
<FilterDropdown
168+
options={filterPublicOptions.map((option) => option.label)}
169+
initialValue={getInitialPublicValue(isPublic)}
170+
onChange={handlePublicFilter}
171+
/>
172+
<FilterDropdown
173+
options={filterRecruitingOptions.map((option) => option.label)}
174+
initialValue={getInitialRecruitingValue(isRecruiting)}
175+
onChange={handleRecruitingFilter}
176+
/>
177+
</div>
178+
<div className="flex items-center gap-4">
179+
<SortSection pathname={pathname} searchParams={searchParams} />
180+
</div>
181+
</div>
182+
</div>
183+
</div>
184+
185+
{/* 메인 콘텐츠 영역 */}
186+
<div className="w-full pt-[132px]">
187+
{/* 폼 만들기 버튼 - 고정 위치 */}
188+
{isOwner && (
189+
<div className="fixed bottom-[50%] right-4 z-[9999] translate-y-1/2">
190+
<Link
191+
href="/addform"
192+
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"
193+
>
194+
<IoAdd className="size-6" />
195+
<span>폼 만들기</span>
196+
</Link>
197+
</div>
198+
)}
199+
200+
{!data?.pages?.[0]?.data?.length ? (
201+
<div className="flex h-[calc(100vh-200px)] flex-col items-center justify-center">
202+
<p className="text-grayscale-500">등록된 알바 공고가 없습니다.</p>
203+
</div>
204+
) : (
205+
<div className="mx-auto mt-4 w-full max-w-screen-xl px-3">
206+
<div className="flex flex-wrap justify-start gap-6">
207+
{data?.pages.map((page) => (
208+
<React.Fragment key={page.nextCursor}>
209+
{page.data.map((form) => (
210+
<div key={form.id}>
211+
<AlbaListItem {...form} />
212+
</div>
213+
))}
214+
</React.Fragment>
215+
))}
216+
</div>
217+
218+
{/* 무한 스크롤 트리거 영역 */}
219+
<div ref={ref} className="h-4 w-full">
220+
{isFetchingNextPage && (
221+
<div className="flex justify-center py-4">
222+
<div className="h-6 w-6 animate-spin rounded-full border-2 border-primary-orange-300 border-t-transparent" />
223+
</div>
224+
)}
225+
</div>
226+
</div>
227+
)}
228+
</div>
229+
</div>
230+
);
231+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import React, { Suspense } from "react";
2+
3+
export default function MyAlbaFormLayout({ children }: { children: React.ReactNode }) {
4+
return (
5+
<div className="mx-auto max-w-screen-xl px-4 py-8">
6+
<Suspense
7+
fallback={
8+
<div className="flex h-[calc(100vh-200px)] items-center justify-center">
9+
<div>로딩 중...</div>
10+
</div>
11+
}
12+
>
13+
{children}
14+
</Suspense>
15+
</div>
16+
);
17+
}

0 commit comments

Comments
 (0)