
9주차 이야기. 9주차에서 프론트엔드 자체의 작업은 거의 마무리 되어 있었고, 백엔드로부터 자료만 넘겨받으면 되는 상황에 가까워졌다. 이번 9주차에서 내가 했던 내용에 대해서 정리해본다.
도메인 타입 취합
관리자 화면은 `src/lib/adminEntityTypes.ts`, 북마크는 `BookmarkedEvent`, 카드는 props에 `status: string`… 같은 “체험/리뷰/문의”인데 타입이 파일마다 달라서, 나중에 API만 붙여도 필드명이 어긋날 게 뻔했다. 이에 따라 `types/event`, `types/inquiry`, `types/review`로 모으고, 쓰이던 곳을 전부 이쪽으로 맞췄다. `adminEntityTypes.ts`는 역할이 없어져서 삭제했다
// types/event.ts
export type EventType = {
id: number;
title: string;
status: 'UPCOMING' | 'OPEN' | 'CLOSED' | 'FINISHED';
version?: number;
thumbnailUrl?: string;
placeName?: string;
applyEndDateTime?: string;
capacity?: number;
currentParticipants?: number;
// ...
};
export type EventStatus = EventType['status'];
// types/inquiry.ts
export type Inquiry = {
id: number;
memberId: number;
title: string;
content: string;
replyEmail: string;
status: 'UNANSWERED' | 'ANSWERED'; // 예전 PENDING 정리
createdAt: string;
answeredAt: string | null;
};
카테고리 아이콘 지정
슬슬 마무리를 위해서 가운데 메뉴에 있는 카테고리 아이콘을 지정하기로 했다. 지정에 있어 좀 더 깔끔할 필요가 있어 보여 카테고리에 `eng` 키를 두고 PNG를 `categoryIcons.ts`에서 매핑했다.
// lib/categories.ts
export const categories = [
{ label: '맛집', id: 1, eng: 'restaurant' },
{ label: '뷰티', id: 2, eng: 'beauty' },
{ label: '여행', id: 3, eng: 'travel' },
{ label: '문화', id: 4, eng: 'culture' },
{ label: '식품', id: 5, eng: 'food' },
] as const;
// assets/icons/categoryIcons.ts
export const categoryIconByEng: Record<string, string> = {
restaurant, beauty, travel, culture, food,
};
오픈 예정 캐러셀 이벤트 카드 적용
오픈 예정 캐러셀 파트에 있던 카드들을 컴포넌트로 적용하여 수정했다.
// EventCard — size와 관계없이 위치, lg가 아니면 신청 인원
{placeName && (
<div className="text-xs text-muted-foreground flex items-center gap-1">
<MapPin className="h-3.5 w-3.5" />
<span>{placeName}</span>
</div>
)}
{size != 'lg' && (
<div className="text-xs text-muted-foreground">
신청 {currentParticipants} / {capacity}명
</div>
)}
검색 및 필터 기능 추가
백엔드에서 검색 및 필터에 대한 구현을 완성하여, 프론트에서도 검색 기능 및 필터 기능의 화면을 추가할 수 있게 되었다. 파라미터를 통일할 필요가 생겨, 목록 탐색의 단일 소스를 쿼리스트링으로 잡았다. 검색의 경우 `?keyword=&page=`로 잡았다.
// GlobalLayout
<form
onSubmit={(e) => {
e.preventDefault();
const next = keyword.trim();
navigate(`/search?keyword=${encodeURIComponent(next)}&page=1`);
}}
>
<Input value={keyword} onChange={(e) => setKeyword(e.target.value)} />
</form>
그리고 `SearchPage`를 새로 만들고 `mainRoutes`에 연결했다. 키워드로 `title/placeName`을 필터하고, `PAGE_SIZE`로 잘라 `Pagination`에 넘긴다.
const keyword = searchParams.get('keyword') ?? '';
const page = Math.max(1, Number(searchParams.get('page')) || 1);
const filtered = useMemo(() => {
const kw = keyword.trim().toLowerCase();
if (!kw) return mockEvents;
return mockEvents.filter((e) =>
`${e.title ?? ''} ${e.placeName ?? ''}`.toLowerCase().includes(kw),
);
}, [keyword]);
마이 페이지 내에서 필터 기능도 만들었다. `?keyword=&period=&page=` 형태로 쿼리스트링을 잡았다. 대표적으로 신청 내역은 다음과 같이 처리 되었다.
type PeriodKey = '7d' | '15d' | '1m' | '2m' | '3m' | 'all';
const keyword = searchParams.get('keyword') ?? '';
const period = (searchParams.get('period') as PeriodKey) || 'all';
const filteredItems = items.filter((x) => {
const kw = normalizeKeyword(keyword);
const keywordOk = kw
? `${x.title} ${x.content}`.toLowerCase().includes(kw)
: true;
const days = periodToDays(period);
const periodOk = days == null ? true : withinDays(x.appliedAt, days);
return keywordOk && periodOk;
});
기간 버튼·검색 submit·페이지 변경 모두 `setSearchParams({ keyword, period, page })`로 맞춰서, URL만 보면 현재 필터를 알 수 있게 했다.
목업·이미지 추가
메인·카테고리·관심·신청·회원관리에 `picsum` 시드 URL을 넣고, 프로필은 원형 이미지로 보이게 바꿨다.
thumbnailUrl: `https://picsum.photos/seed/experience${i}/250/160`
{item.thumbnailUrl && (
<img src={item.thumbnailUrl} alt="행사 이미지" />
)}
{row.profileImageUrl && (
<img src={row.profileImageUrl} alt="프로필 이미지" className="w-10 h-10 rounded-full" />
)}
이 부분은 어디까지나 목업이므로 백엔드와 연결된다면 삭제될 예정이다.
이렇게 프론트엔드들은 과정이 마무리되어가고 있다.
'개발일지 > 스터디:EZ-FillStack' 카테고리의 다른 글
| [팀 프로젝트] 마무리 트러블슈팅#1 (0) | 2026.05.14 |
|---|---|
| [팀 프로젝트] 마지막 정기회의 (0) | 2026.05.14 |
| [팀 프로젝트] 제8회차 정기회의 (0) | 2026.03.20 |
| [팀 프로젝트] 제7회차 정기회의 (0) | 2026.03.14 |
| [팀 프로젝트] 제6회차 정기회의 (0) | 2026.03.09 |
