Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
b125c62
✨feat: 드롭다운 정렬 기능 구현
Jun 19, 2025
777d17c
Merge branch 'develop' into feat/NoticeList
Jun 19, 2025
8cb405f
✨feat: 검색기능 구현 및 정렬 기능 구현 완료
Jun 19, 2025
d30fbed
✨feat: 포스트 api 연동 및 공고 클릭시 해당 공고로 이동하게 함
Jun 19, 2025
9f2fc0a
♻️feat: 주석 처리
Jun 19, 2025
a4af561
♻️refactor: 코드 주석 정리 및 일관성 있는 형식으로 수정
minimo-9 Jun 21, 2025
253dca9
🐛 fix: 포스트 카드 모바일에서 시계 위치 수정
minimo-9 Jun 21, 2025
edfd48e
🎨 style: 맞춤 공고 및 전체 공고 섹션 스타일 개선 및 표시 항목 수 조정
minimo-9 Jun 21, 2025
22362fa
Merge branch 'develop' into feat/noticeList
minimo-9 Jun 21, 2025
9885d5b
🐛 fix: getNotices 함수 제거 및 중복 import 삭제
minimo-9 Jun 21, 2025
3398663
🐛 fix: Post 컴포넌트에서 button을 div로 변경
minimo-9 Jun 21, 2025
be8360c
🐛 fix: getNotices 함수 호출 시 쿼리 정리 및 타입 수정
minimo-9 Jun 21, 2025
502f8b5
Merge branch 'develop' into feat/noticeList
minimo-9 Jun 21, 2025
5d1cf6f
🐛 fix: 필터 컴포넌트에서 스크롤바 숨김 추가
minimo-9 Jun 21, 2025
615b800
🐛 fix: 필터 컴포넌트의 max-w 속성을 수정하여 레이아웃 개선
minimo-9 Jun 21, 2025
6f6cbe0
✨ feat: 공고 리스트 페이지 구현
minimo-9 Jun 21, 2025
290b56d
🐛 fix: formatWorkTime 함수에서 종료 시간을 계산하는 로직 수정
minimo-9 Jun 21, 2025
95bd39f
🐛 fix: 시작 시간을 KST에서 UTC로 변환하는 로직 수정
minimo-9 Jun 21, 2025
ed7fb18
🐛 fix: 공고 목록에서 에러 발생 시 모달로 오류 메시지 표시 및 스크롤 카드 선택자 수정
minimo-9 Jun 21, 2025
0c7674d
Merge branch 'develop' into feat/noticeList
minimo-9 Jun 21, 2025
8d427fa
🐛 fix: 내일 날짜 기준으로 필터링 시작 시간 설정 로직 수정
minimo-9 Jun 21, 2025
9d591bb
🐛 fix: 추천 공고 로직 개선 및 자동 스크롤 기능 추가
minimo-9 Jun 22, 2025
af96c14
🐛 fix: 사용자 로그인 상태에 따라 추천 공고 로직 수정
minimo-9 Jun 22, 2025
3295a85
Merge branch 'develop' into feat/noticeList
minimo-9 Jun 22, 2025
50a4750
🐛 fix: NoticeList 컴포넌트의 props 타입 정의 수정 및 불필요한 타입 제거
minimo-9 Jun 22, 2025
0473cb1
🐛 fix: 자동 스크롤 관련 코드 제거 및 피드백 반영
minimo-9 Jun 22, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Routes, Route, useLocation } from 'react-router-dom';
import { Routes, Route, useLocation, useSearchParams } from 'react-router-dom';
import Navbar from './components/layout/Navbar';
import Login from './pages/account/Login';
import Signup from './pages/account/Signup';
Expand All @@ -11,6 +11,12 @@ import ProfileForm from './pages/profile/ProfileForm';
import NoticeList from './pages/notice/NoticeList';
import Notice from './pages/notice/Notice';

function SearchPage() {
const [params] = useSearchParams();
const query = params.get('query') || '';
return <NoticeList search={query} />;
}

export default function App() {
const { pathname } = useLocation();

Expand All @@ -20,6 +26,7 @@ export default function App() {
<Routes>
{/* 공통 페이지 */}
<Route path="/" element={<NoticeList />} />
<Route path="/search" element={<SearchPage />} />
Copy link
Contributor

@Moon-ju-young Moon-ju-young Jun 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❓ 이미 페이지 구성은 정리된지라 구성을 변경하실 거라면 논의 후에 진행하셨으면 좋았을 것 같습니다~ 그리고 SearchPage 없이 구현이 가능할 것 같은데 어렵나요?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

네 알겠습니다 merge이후에 리펙토링때 수정해놓겠습니다

<Route path=":id" element={<Notice />} />
<Route path="login" element={<Login />} />
<Route path="signup" element={<Signup />} />
Expand Down
19 changes: 19 additions & 0 deletions src/api/noticeApi.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,24 @@
import type { ApplicationItem } from './applicationApi';

Check failure on line 1 in src/api/noticeApi.ts

View workflow job for this annotation

GitHub Actions / build-test

Duplicate identifier 'api'.
import type { ShopInfo } from './shopApi';
import api from './api';

export const getNotices = async (params: {

Check failure on line 5 in src/api/noticeApi.ts

View workflow job for this annotation

GitHub Actions / build-test

Duplicate identifier 'api'.
offset?: number;
limit?: number;

Check failure on line 7 in src/api/noticeApi.ts

View workflow job for this annotation

GitHub Actions / build-test

Cannot redeclare block-scoped variable 'getNotices'.
address?: string;
keyword?: string;
startsAtGte?: string;
hourlyPayGte?: number;
sort?: 'time' | 'pay' | 'hour' | 'shop';
}) => {
const query = new URLSearchParams(
Object.entries(params)
.filter(([, v]) => v !== undefined && v !== '')
.map(([k, v]) => [k, String(v)]),
);
const response = await api.get<GetNoticesResponse>(`/notices?${query}`);
return response.data;
};

// Link (공통 링크 타입)
export interface LinkInfo {
Expand Down
12 changes: 12 additions & 0 deletions src/components/layout/Navbar.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Link } from 'react-router-dom';
import { useRef, useState, useEffect, useContext } from 'react';
import { useNavigate } from 'react-router-dom';
import { AuthContext } from '@/context/AuthContext';
import NotificationModal from '../common/notification-modal/NotificationModal';
import logo from '@/assets/images/logo.svg';
Expand All @@ -12,6 +13,8 @@ export default function Navbar() {
const [isShowModal, setIsShowModal] = useState(false);
const modalRef = useRef<HTMLDivElement>(null);
const buttonRef = useRef<HTMLButtonElement>(null);
const [searchValue, setSearchValue] = useState('');
const navigate = useNavigate();

// 바깥 클릭 시 모달 닫기
useEffect(() => {
Expand Down Expand Up @@ -60,6 +63,15 @@ export default function Navbar() {
<input
type="text"
placeholder="가게 이름으로 찾아보세요"
value={searchValue}
onChange={(e) => setSearchValue(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter' && searchValue.trim()) {
navigate(
`/search?query=${encodeURIComponent(searchValue.trim())}`,
);
}
}}
className="h-36 w-full rounded-[10px] bg-gray-10 pt-10 pb-10 pl-40 placeholder:text-body2 placeholder:text-gray-40 md:h-40 md:w-344 lg:w-450"
/>
</div>
Expand Down
205 changes: 203 additions & 2 deletions src/pages/notice/NoticeList.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,204 @@
export default function NoticeList() {
return <div>공고 리스트</div>;
import { 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 { NoticeWithShopItem } from '@/api/noticeApi';

Check failure on line 8 in src/pages/notice/NoticeList.tsx

View workflow job for this annotation

GitHub Actions / build-test

'"@/api/noticeApi"' has no exported member named 'NoticeWithShopItem'. Did you mean 'NoticeShopItem'?
import { Link } from 'react-router-dom';

type FilterValues = {
address?: string[] | null;
startsAt?: string | null;
hourlyPay?: number | null;
};

type NoticeListProps = {
search?: string;
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💬 prop이 간단할 경우 굳이 type을 분리하지 않아도 될 것 같습니다~

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

네 알겠습니다

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

요 부분도 바꿧습니다


// ===================== 상수 =====================
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// ===================== 상수 =====================
// 상수

💬 그냥 간단하게 주석을 적어주셔도 될 것 같습니다~

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

넵 이건 수정했습니다

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 = '' }: NoticeListProps) {
const [allNotices, setAllNotices] = useState<NoticeWithShopItem[]>([]); // 현재 페이지에 노출할 공고 목록
const [totalCount, setTotalCount] = useState(0); // 전체 공고 수
const [sort, setSort] = useState<(typeof SORT_OPTIONS)[number]>(
SORT_OPTIONS[0],
); //정렬(드롭다운값)
const [filterValues, setFilterValues] = useState<FilterValues>({}); // 상세필터 상태값
const [currentPage, setCurrentPage] = useState<number>(1); // 현재 페이지(페이지네이션)
const [filterOpen, setFilterOpen] = useState<boolean>(false); // 필터 모달 오픈
const [loading, setLoading] = useState(false); // 로딩 에러처리
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💬 어디 부분은 generic을 사용하고 어느 부분은 사용하고 있지 않네요~ 기본형 들은 generic을 빼고 자동으로 추론되게 해도 괜찮을 것 같습니다~

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

요부분은 제가 지워 놓겠습니다

const [error, setError] = useState<string | null>(null); // 로딩 에러처리

useEffect(() => {
setLoading(true);
setError(null);

getNotices({
offset: (currentPage - 1) * ITEMS_PER_PAGE,
limit: ITEMS_PER_PAGE,
address: filterValues.address?.[0],
keyword: search,
startsAtGte: filterValues.startsAt ?? undefined,
hourlyPayGte: filterValues.hourlyPay ?? undefined,
sort: sortMap[sort] ?? undefined,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❓ 이렇게 된다면 null 병합 연산자 (??)를 쓰는 의미가 없지 않나요?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

수정했습니다!

})
.then((data) => {
setAllNotices(data.items.map((item) => item.item));
setTotalCount(data.count);
})
.catch((err) => setError(err.message))
.finally(() => setLoading(false));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💬 현재 전체적으로 error 처리는 try ~ catch 문으로 하고 계신 거 같아서 try catch 문으로 바꿔주시면 좋을 것 같습니다!

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

변경하겠습니다

}, [search, sort, filterValues, currentPage]);

const totalPages = Math.ceil(totalCount / ITEMS_PER_PAGE); // 페이지네이션
const appliedFilterCount = countAppliedFilters(filterValues); // 필터 적용

// 필터 적용
const handleApplyFilter = (values: FilterValues) => {
setFilterValues(values);
setCurrentPage(1);
};

// ===== 렌더 =====
if (loading)
return (
<div className="flex h-500 items-center justify-center text-h3 text-black">
잠시만 기다려주세요
</div>
);
if (error)
return (
<div className="flex h-500 items-center justify-center text-h3 text-red-500">
{error}
</div>
);

return (
<main>
{/* 맞춤 공고 */}
{!search && (
<article className="bg-red-10 px-32 py-60">
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❗ mobile 일 때는 padding 값이 다릅니다~

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

네 알겠습니다

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

디자인은 다시해서 변경 되었습니다!

<section className="mx-auto flex max-w-964 flex-col">
<h2 className="mb-32 text-h1 font-bold">맞춤 공고</h2>
<div className="flex gap-14 overflow-hidden">
{allNotices.slice(0, 3).map((notice) => (
<Link
key={notice.id}
to={`/shops/${notice.shop.item.id}/notices/${notice.id}`}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
to={`/shops/${notice.shop.item.id}/notices/${notice.id}`}
to={`/${notice.id}`}

❗ 공고 상세 페이지의 주소는 그냥 /:id 입니다~ 프로젝트 정리 문서 참고하시면 좋을 것 같아요

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

네 알겠습니다 감사합니다

className="block"
>
<Post data={notice} />
</Link>
))}
</div>
</section>
</article>
)}

{/* 전체 공고 */}
<article className="px-32 py-60">
<section className="mx-auto flex max-w-full flex-col gap-32 sm:max-w-964">
<div className="flex flex-col gap-12 sm:flex-row sm:justify-between">
<h2 className="mb-4 text-h1 font-bold sm:mb-0">
{search ? (
<>
<span className="text-h1 font-bold text-primary">
{search}
</span>
<span className="text-h1 font-bold">에 대한 공고 목록</span>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
<span className="text-h1 font-bold text-primary">
{search}
</span>
<span className="text-h1 font-bold">에 대한 공고 목록</span>
<span className="text-primary">
{search}
</span>
대한 공고 목록

💬 이런식으로도 작성 가능할 것 같습니다~ 기본적으로 font 설정들은 상속이 되기 때문에 상위 태그에서 지정해주면 하위에서는 설정하지 않을 수도 있습니다

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

네 알겠습니다!

</>
) : (
'전체 공고'
)}
</h2>
<div className="flex gap-10">
<Dropdown
options={SORT_OPTIONS}
placeholder="마감임박순"
variant="filter"
selected={sort}
setSelect={setSort}

Check failure on line 146 in src/pages/notice/NoticeList.tsx

View workflow job for this annotation

GitHub Actions / build-test

Type 'Dispatch<SetStateAction<"마감임박순" | "시급많은순" | "시간적은순" | "가나다순">>' is not assignable to type 'Dispatch<SetStateAction<"마감임박순" | "시급많은순" | "시간적은순" | "가나다순" | null>>'.
/>
<div className="relative">
<button
className="rounded-md bg-red-30 px-12 py-8 text-body2 text-white"
type="button"
onClick={() => setFilterOpen(true)}
>
상세필터
{appliedFilterCount > 0 && (
<span className="ml-2">({appliedFilterCount})</span>
)}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
{appliedFilterCount > 0 && (
<span className="ml-2">({appliedFilterCount})</span>
)}
{appliedFilterCount > 0 && (
' ' + appliedFilterCount
)}

💬 이런 식으로도 가능하지 않을까요?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

간결하고 더 좋네요 리펙토링때 수정하겠습니다!

</button>
{filterOpen && (
<div className="absolute top-full right-365 z-50 mt-8">
<div className="absolute z-50 mt-2">
<Filter
open={filterOpen}
onClose={() => setFilterOpen(false)}
onApply={handleApplyFilter}
defaultValues={filterValues}
/>
</div>
</div>
)}
</div>
</div>
</div>
{allNotices.length === 0 ? (
<div className="flex h-500 items-center justify-center text-h3 text-black">
등록된 게시물이 없습니다.
</div>
) : (
<>
<div className="grid grid-cols-2 gap-x-8 gap-y-16 sm:grid-cols-2 sm:gap-x-14 sm:gap-y-32 lg:grid-cols-3">
{allNotices.map((notice) => (
<Link
key={notice.id}
to={`/shops/${notice.shop.item.id}/notices/${notice.id}`}
className="block"
>
<Post data={notice} />
</Link>
))}
</div>
<div className="mt-24">
<Pagination
totalPages={totalPages}
currentPage={currentPage}
setCurrentPage={setCurrentPage}
/>
</div>
</>
)}
</section>
</article>
</main>
);
}
Loading