+
diff --git a/src/pages/notice/NoticeList.tsx b/src/pages/notice/NoticeList.tsx
index f3c7a8e..d2ca9b3 100644
--- a/src/pages/notice/NoticeList.tsx
+++ b/src/pages/notice/NoticeList.tsx
@@ -1,3 +1,282 @@
-export default function NoticeList() {
- return
공고 리스트
;
+import { useCallback, useContext, useEffect, useState } from 'react';
+import Post from '@/components/common/Post';
+import Dropdown from '@/components/common/Dropdown';
+import { SORT_OPTIONS } from '@/constants/dropdownOptions';
+import Pagination from '@/components/common/Pagination';
+import Filter from '@/components/common/Filter';
+import { getNotices } from '@/api/noticeApi';
+import type { NoticeShopItem } from '@/api/noticeApi';
+import Footer from '@/components/layout/Footer';
+import Modal from '@/components/common/Modal';
+import { getUser } from '@/api/userApi';
+import { AuthContext } from '@/context/AuthContext';
+
+type FilterValues = {
+ address?: string[] | null;
+ startsAt?: string | null;
+ hourlyPay?: number | null;
+};
+
+// 상수
+const ITEMS_PER_PAGE = 6;
+
+// 정렬 맵핑
+const sortMap: Record<
+ (typeof SORT_OPTIONS)[number],
+ 'time' | 'pay' | 'hour' | 'shop'
+> = {
+ 마감임박순: 'time',
+ 시급많은순: 'pay',
+ 시간적은순: 'hour',
+ 가나다순: 'shop',
+};
+
+// 필터가 몇 개 적용되었는지 계산해서 뱃지 등에 표시해줌
+function countAppliedFilters(filterValues: FilterValues): number {
+ let count = 0;
+ if (Array.isArray(filterValues.address) && filterValues.address.length > 0)
+ count += filterValues.address.length;
+ if (filterValues.startsAt) count += 1;
+ if (filterValues.hourlyPay) count += 1;
+ return count;
+}
+
+// 컴포넌트
+export default function NoticeList({ search = '' }: { search?: string }) {
+ const [allNotices, setAllNotices] = useState
([]); // 현재 페이지에 노출할 공고 목록
+ const [totalCount, setTotalCount] = useState(0); // 전체 공고 수
+ const [sort, setSort] = useState<(typeof SORT_OPTIONS)[number] | null>(
+ SORT_OPTIONS[0],
+ ); // 정렬(드롭다운 값)
+ const [filterValues, setFilterValues] = useState({}); // 상세필터 상태값
+ const [currentPage, setCurrentPage] = useState(1); // 현재 페이지(페이지네이션)
+ const [filterOpen, setFilterOpen] = useState(false); // 필터 모달 오픈
+ const [loading, setLoading] = useState(false); // 로딩 에러처리
+ const [error, setError] = useState(null); // 로딩 에러처리
+ const [recommendedNotices, setRecommendedNotices] = useState<
+ NoticeShopItem[]
+ >([]); // 맞춤 공고 보이는 목록
+ const [shouldShowEmpty, setShouldShowEmpty] = useState(false); // 깜빡임 방지용
+ const [showModal, setShowModal] = useState(false);
+ const { isLoggedIn } = useContext(AuthContext);
+
+ // 지난 공고 안보이게 처리
+ const getTomorrowISOString = () => {
+ const date = new Date();
+ date.setDate(date.getDate() + 1);
+ date.setHours(0, 0, 0, 0);
+ return date.toISOString();
+ };
+
+ // 기본 공고 가져오기
+ const fetchDefaultNotices = useCallback(async (): Promise<
+ NoticeShopItem[]
+ > => {
+ const result = await getNotices({
+ offset: 0,
+ limit: 9,
+ startsAtGte: getTomorrowISOString(),
+ sort: 'shop',
+ });
+ return result.items.map((i) => i.item).filter((item) => !item.closed);
+ }, []);
+
+ useEffect(() => {
+ const fetchNotices = async () => {
+ setLoading(true);
+ setError(null);
+ setShouldShowEmpty(false);
+
+ const rawQuery = {
+ offset: (currentPage - 1) * ITEMS_PER_PAGE,
+ limit: ITEMS_PER_PAGE,
+ address: filterValues.address?.[0],
+ keyword: search,
+ startsAtGte: filterValues.startsAt ?? getTomorrowISOString(),
+ hourlyPayGte: filterValues.hourlyPay,
+ sort: sort ? sortMap[sort] : undefined,
+ };
+
+ const cleanedQuery = Object.fromEntries(
+ Object.entries(rawQuery).filter(([, v]) => v !== undefined && v !== ''),
+ );
+
+ try {
+ const data = await getNotices(cleanedQuery);
+ setAllNotices(data.items.map((item) => item.item));
+ setTotalCount(data.count);
+ setShouldShowEmpty(true);
+ } catch (error) {
+ setError((error as Error).message);
+ setShowModal(true);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ fetchNotices();
+ }, [search, sort, filterValues, currentPage]);
+
+ // 유저 위치 기반 맞춤 공고 추천
+ useEffect(() => {
+ const fetchRecommended = async () => {
+ try {
+ const userId = localStorage.getItem('userId');
+
+ // 로그아웃 상태 -> 기본 공고
+ if (!userId || !isLoggedIn) {
+ const notices = await fetchDefaultNotices();
+ setRecommendedNotices(notices);
+ return;
+ }
+
+ // 로그인 상태 -> 선호 지역
+ const userInfo = await getUser(userId);
+ const preferredAddress = userInfo.item.address;
+
+ const result = await getNotices({
+ offset: 0,
+ limit: 9,
+ startsAtGte: getTomorrowISOString(),
+ ...(preferredAddress
+ ? { address: preferredAddress }
+ : { sort: 'shop' }),
+ });
+
+ setRecommendedNotices(
+ result.items.map((i) => i.item).filter((item) => !item.closed),
+ );
+ } catch (error) {
+ console.error('추천 공고 에러', error);
+ setRecommendedNotices([]);
+ }
+ };
+
+ fetchRecommended();
+ }, [search, fetchDefaultNotices, isLoggedIn]);
+
+ const totalPages = Math.ceil(totalCount / ITEMS_PER_PAGE); // 페이지네이션
+ const appliedFilterCount = countAppliedFilters(filterValues); // 필터 적용
+
+ // 필터 적용
+ const handleApplyFilter = (values: FilterValues) => {
+ setFilterValues(values);
+ setCurrentPage(1);
+ };
+
+ // 렌더
+ if (loading)
+ return (
+
+ );
+
+ return (
+ <>
+ {showModal && error && (
+ setShowModal(false)}
+ onButtonClick={() => setShowModal(false)}
+ >
+ {error}
+
+ )}
+
+ {/* 맞춤 공고 */}
+ {!search && (
+
+
+ 맞춤 공고
+
+ {recommendedNotices.slice(0, 9).map((notice) => (
+
+ ))}
+
+
+
+ )}
+
+ {/* 전체 공고 */}
+
+
+
+
+ {search ? (
+ <>
+
+ {search}
+
+
+ 에 대한 공고 목록
+
+ >
+ ) : (
+ '전체 공고'
+ )}
+
+
+
+
+
+ {filterOpen && (
+
+ setFilterOpen(false)}
+ onApply={handleApplyFilter}
+ defaultValues={filterValues}
+ />
+
+ )}
+
+
+
+ {shouldShowEmpty && allNotices.length === 0 ? (
+
+ 등록된 게시물이 없습니다.
+
+ ) : (
+ <>
+
+ {allNotices.map((notice) => (
+
+ ))}
+
+
+ >
+ )}
+
+
+
+
+ >
+ );
}
diff --git a/src/utils/formatWorkTime.ts b/src/utils/formatWorkTime.ts
index 1b96406..13e75dd 100644
--- a/src/utils/formatWorkTime.ts
+++ b/src/utils/formatWorkTime.ts
@@ -14,8 +14,7 @@ export default function formatWorkTime({
}
/* 시각이 24시를 넘으면 다음날로 설정되어 자동으로 처리 */
- const endDate = new Date(date);
- endDate.setHours(endDate.getHours() + workhour);
+ const endDate = new Date(date.getTime() + workhour * 60 * 60 * 1000);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');