Skip to content

Commit

Permalink
카페 전체 리스트 페이지 구현 (#55)
Browse files Browse the repository at this point in the history
  • Loading branch information
Namyunha authored Jul 22, 2024
2 parents a50b205 + 0647ead commit e21ea3d
Show file tree
Hide file tree
Showing 27 changed files with 908 additions and 18 deletions.
17 changes: 17 additions & 0 deletions src/api/cafe/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { END_POINT } from '@/constants/api';
import {
CafeLikeReqType,
CafeLikeResType,
CafeListReqType,
CafeListResType,
CafeRecommendResType,
} from '@/types/cafe';
import { fetcher } from '../fetcher';
Expand All @@ -22,3 +24,18 @@ export const patchCafeLike = async ({ cafeId, isLike }: CafeLikeReqType) => {

return data;
};

export const postCafeList = async ({
tags,
location,
pagingData,
}: CafeListReqType) => {
// Todo: 쿼리키에 필터 추가
const { data } = await fetcher.post<CafeListResType>(END_POINT.CAFE.LIST, {
tags,
location,
pagingData,
});

return data;
};
37 changes: 34 additions & 3 deletions src/api/cafe/queries.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { useMutation, useQuery } from '@tanstack/react-query';
import { CafeLikeReqType } from '@/types/cafe';
import { useInfiniteQuery, useMutation, useQuery } from '@tanstack/react-query';
import { DEFAULT_CURSOR, DEFAULT_SIZE } from '@/constants/api';
import { CafeLikeReqType, CafeListReqType } from '@/types/cafe';
import { cafeKeys } from './../queryKeys';
import { getRecommendedCafeList, patchCafeLike } from '.';
import { getRecommendedCafeList, patchCafeLike, postCafeList } from '.';

export function useRecommendedCafeListQuery() {
return useQuery({
Expand Down Expand Up @@ -31,3 +32,33 @@ export function useCafeLikeMutation({
},
});
}

type CafeListQueryType = Pick<CafeListReqType, 'tags' | 'location'> & {
pageSize?: number;
};

export function useCafeListInfiniteQuery({
tags,
location,
pageSize = DEFAULT_SIZE,
}: CafeListQueryType) {
// Todo: 쿼리키에 필터 추가
return useInfiniteQuery({
...cafeKeys.cafeList,
initialPageParam: DEFAULT_CURSOR,
queryFn: ({ pageParam }) =>
postCafeList({
tags,
location,
pagingData: {
lastPostId: pageParam,
pageSize,
},
}),
getNextPageParam: (lastPage) => {
const lastReview = lastPage.data.cafeInfos.at(-1);
const nextCursor = lastReview?.cafeId;
return lastPage.data.meta.hasNext ? nextCursor : undefined;
},
});
}
1 change: 1 addition & 0 deletions src/api/queryKeys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ export const reviewKeys = createQueryKeys('review', {

export const cafeKeys = createQueryKeys('cafe', {
recommendedCafeList: { queryKey: ['getRecommendedCafeList'] },
cafeList: { queryKey: ['getCafeList'] },
});
4 changes: 4 additions & 0 deletions src/api/review/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,17 @@ import { fetcher } from '../fetcher';

export const postReviewList = async ({
tags,
sorting,
location,
pagingData,
}: ReviewListReqType) => {
// Todo: 쿼리키에 필터 추가
const { data } = await fetcher.post<ReviewListResType>(
END_POINT.REVIEW.LIST,
{
tags,
sorting,
location,
pagingData,
}
);
Expand Down
34 changes: 34 additions & 0 deletions src/app/(header)/cafe/_components/CafeFilter/CafeFilter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
'use client';

import { useState } from 'react';
import Icon from '@/components/Icon';
import CafeFilterList from './CafeFilterList';
import CafeFilterModal from './CafeFilterModal';

export default function CafeFilter() {
const [isFilterModalOpen, setIsFilterModalOpen] = useState(false);

const toggleFilterModalHandler = () => {
setIsFilterModalOpen((prev) => !prev);
};

return (
<>
<div className="flex gap-8">
<button
className="px-8 py-6 rounded-xl border border-stroke_grey shadow-elevation1"
onClick={toggleFilterModalHandler}>
<Icon name="filter" size={20} />
</button>
<div className="h-[34px] w-[1px] border-r border-stroke_grey" />
<div className="overflow-x-scroll scrollbar-none">
<CafeFilterList onClick={toggleFilterModalHandler} />
</div>
</div>
<CafeFilterModal
isOpen={isFilterModalOpen}
closeHandler={toggleFilterModalHandler}
/>
</>
);
}
36 changes: 36 additions & 0 deletions src/app/(header)/cafe/_components/CafeFilter/CafeFilterList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import FilterTab from '@/components/FilterTab';
import { LOCATION, TAGS } from '@/constants/reviewFilter';
import { useCafeFilterStore } from '@/store/filterStore';
import { LocationKey, SortingKey, TagsKey } from '@/types/review';

type CafeFilterListProps = {
onClick: () => void;
};

export default function CafeFilterList({ onClick }: CafeFilterListProps) {
const { activeLocation, activeTags } = useCafeFilterStore();
const activeFilters = [activeLocation, ...activeTags];

const getFilterTabValue = (filter: TagsKey | SortingKey | LocationKey) => {
switch (true) {
case filter in TAGS:
return TAGS[filter as TagsKey];
default:
return LOCATION[filter as LocationKey];
}
};

return (
<ul className="flex flex-nowrap items-center gap-8">
{activeFilters.map((filter) => (
<li key={filter}>
<FilterTab
isActive
value={getFilterTabValue(filter)}
onClick={onClick}
/>
</li>
))}
</ul>
);
}
44 changes: 44 additions & 0 deletions src/app/(header)/cafe/_components/CafeFilter/CafeFilterModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import clsx from 'clsx';
import Button from '@/components/Button';
import Dim from '@/components/Dim';
import CafeFilterModalLocation from './CafeFilterModalLocation';
import CafeFilterModalTags from './CafeFilterModalTags';

type CafeFilterModalProps = {
isOpen: boolean;
closeHandler: () => void;
};

export default function CafeFilterModal({
isOpen,
closeHandler,
}: CafeFilterModalProps) {
// Todo: 확인 눌러야 적용시킬지 기획과 논의 필요
return (
<>
{isOpen && <Dim closeHandler={closeHandler} />}
<div
className={clsx(
'fixed left-0 right-0 bottom-0 max-w-screen_max mx-auto my-0 p-16 flex flex-col z-modal gap-8 bg-bg_white border border-stroke_grey rounded-t-3xl transition-transform duration-300',
{
['translate-y-0']: isOpen,
['translate-y-full']: !isOpen,
}
)}>
<div className="flex justify-between items-center">
<div className="flex-1 text-center text-16 font-bold">검색 필터</div>
<Button
variant="secondary"
childrenType="iconOnly"
iconName="close"
size="xs"
className="absolute right-16"
onClick={closeHandler}
/>
</div>
<CafeFilterModalTags />
<CafeFilterModalLocation />
</div>
</>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import React from 'react';
import FilterTab from '@/components/FilterTab';
import { LOCATION, TAGS } from '@/constants/reviewFilter';
import { LocationKey, TagsKey } from '@/types/review';

type ToggleKeyType =
| ((location: LocationKey) => void)
| ((tags: TagsKey) => void);

type CafeFilterModalFilterListProps = {
filterList: typeof LOCATION | typeof TAGS;
activeKey: string | string[];
toggleKey: ToggleKeyType;
isActive: (key: string, activeKey: string | string[]) => boolean;
};

export default function CafeFilterModalFilterList({
filterList,
activeKey,
toggleKey,
isActive,
}: CafeFilterModalFilterListProps) {
return (
<ul className="flex flex-wrap gap-8 pt-16 border-t border-stroke_grey">
{Object.entries(filterList).map(([key, value]) => {
const filterKey = key as keyof typeof filterList;

return (
<li key={key}>
<FilterTab
value={value}
isActive={isActive(key, activeKey)}
onClick={() => toggleKey(filterKey)}
/>
</li>
);
})}
</ul>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { LOCATION } from '@/constants/reviewFilter';
import { useCafeFilterStore } from '@/store/filterStore';
import CafeFilterModalFilterList from './CafeFilterModalFilterList';
import CafeFilterModalTitle from './CafeFilterModalTitle';

export default function CafeFilterModalLocation() {
const { activeLocation, toggleLocation } = useCafeFilterStore();

return (
<div>
<CafeFilterModalTitle title="지역" subInfo="선택 1개" />
<CafeFilterModalFilterList
filterList={LOCATION}
activeKey={activeLocation}
toggleKey={toggleLocation}
isActive={(key, activeKey) => activeKey === key}
/>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { TAGS } from '@/constants/reviewFilter';
import { useCafeFilterStore } from '@/store/filterStore';
import { TagsKey } from '@/types/review';
import CafeFilterModalFilterList from './CafeFilterModalFilterList';
import CafeFilterModalTitle from './CafeFilterModalTitle';

export default function CafeFilterModalTags() {
const { activeTags, toggleTag } = useCafeFilterStore();

return (
<div>
<CafeFilterModalTitle title="키워드" subInfo="최대 3개" />
<CafeFilterModalFilterList
filterList={TAGS}
activeKey={activeTags}
toggleKey={toggleTag}
isActive={(key, activeKey) => activeKey.includes(key as TagsKey)}
/>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
type CafeFilterModalTitleProps = {
title: string;
subInfo: string;
};

export default function CafeFilterModalTitle({
title,
subInfo,
}: CafeFilterModalTitleProps) {
return (
<div className="flex items-center gap-8 px-8 py-12 text-14">
<span className="font-bold">{title}</span>
<span className="text-12 text-text_light_grey">{subInfo}</span>
</div>
);
}
32 changes: 32 additions & 0 deletions src/app/(header)/cafe/_components/CafeItem/CafeItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { useRouter } from 'next/navigation';
import { ROUTE_PATH } from '@/constants/route';
import { CafeItemType } from '@/types/cafe';
import CafeItemImages from './CafeItemImages';
import CafeItemRating from './CafeItemRating';
import CafeItemSubInfo from './CafeItemSubInfo';
import CafeItemTags from './CafeItemTags';
import CafeItemTitle from './CafeItemTitle';

export default function CafeItem({ ...cafeInfo }: CafeItemType) {
const router = useRouter();

const onClickCafeItem = () => {
router.push(ROUTE_PATH.CAFE_DETAIL(cafeInfo.cafeId));
};

return (
<li
className="flex flex-col gap-16 p-16 rounded-2xl shadow-elevation2 cursor-pointer"
onClick={onClickCafeItem}
tabIndex={0}>
<CafeItemTitle cafeName={cafeInfo.cafeName} />
<CafeItemSubInfo cafeLoca={cafeInfo.cafeLoca} />
<CafeItemRating
rating={cafeInfo.rating}
reviewCount={cafeInfo.reviewCount}
/>
<CafeItemImages cafeImageUrls={cafeInfo.cafeImageUrls} />
<CafeItemTags tags={cafeInfo.tags} />
</li>
);
}
27 changes: 27 additions & 0 deletions src/app/(header)/cafe/_components/CafeItem/CafeItemImages.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import Image from 'next/image';
import { CafeItemType } from '@/types/cafe';

type CafeItemImagesProps = Pick<CafeItemType, 'cafeImageUrls'>;

export default function CafeItemImages({ cafeImageUrls }: CafeItemImagesProps) {
return (
<ul className="flex gap-8 h-36 overflow-hidden">
{!!cafeImageUrls.length &&
cafeImageUrls.map((img) => (
<li
key={img.id}
className="relative basis-1/3 rounded-md overflow-hidden">
<Image
src={img.url}
alt={img.url}
fill={true}
// Todo: 이미지 사이즈 최적화하기
sizes="(max-width: 732px) 90vw, (max-width: 992px) 45vw, 320px"
className="object-cover"
priority
/>
</li>
))}
</ul>
);
}
Loading

0 comments on commit e21ea3d

Please sign in to comment.