![{`Banner]({banner.imageUrl})
window.open(banner.redirectUrl, '_blank')}
diff --git a/frontend/src/components/Map/CourseItem.tsx b/frontend/src/components/Map/CourseItem.tsx
deleted file mode 100644
index 6413815e..00000000
--- a/frontend/src/components/Map/CourseItem.tsx
+++ /dev/null
@@ -1,42 +0,0 @@
-import { MapItemType } from '@/types';
-import { Link } from 'react-router-dom';
-import PinIcon from '@/components/PinIcon';
-import MapThumbnail from './MapThumbnail';
-
-type CourseItemProps = {
- courseItem: MapItemType;
-};
-
-const CourseItem = ({ courseItem }: CourseItemProps) => {
- return (
-
-
-
- {courseItem.thumbnailUrl.startsWith('https://example') ? (
-
- ) : (
-
![{`${courseItem.title}]({courseItem.thumbnailUrl})
- )}
-
-
{courseItem.title}
-
-
![]({courseItem.user.profileImageUrl})
-
{courseItem.user.nickname}
-
-
-
{courseItem.pinCount}
-
-
-
-
- );
-};
-
-export default CourseItem;
diff --git a/frontend/src/components/Map/CourseListPanel.tsx b/frontend/src/components/Map/CourseListPanel.tsx
deleted file mode 100644
index 2b1aefa2..00000000
--- a/frontend/src/components/Map/CourseListPanel.tsx
+++ /dev/null
@@ -1,40 +0,0 @@
-import React from 'react';
-
-import { getCourseList } from '@/api/course';
-import { useInfiniteScroll } from '@/hooks/useInfiniteScroll';
-import { CourseList, MapItemType } from '@/types';
-
-import CourseItem from './CourseItem';
-
-const CourseListPanel = () => {
- const infiniteScrollConfig = {
- queryKey: ['courseList'],
- queryFn: ({ pageParam }: { pageParam: number }) => getCourseList(pageParam),
- getNextPageParam: (lastPage: CourseList) => {
- return lastPage.currentPage < lastPage.totalPages
- ? lastPage.currentPage + 1
- : undefined;
- },
- fetchWithoutQuery: true,
- };
-
- const { data, isFetchingNextPage, hasNextPage, ref } =
- useInfiniteScroll
(infiniteScrollConfig);
-
- return (
- <>
-
- {data?.pages.map((page, index) => (
-
- {page.courses.map((map: MapItemType) => (
-
- ))}
-
- ))}
-
-
- >
- );
-};
-
-export default CourseListPanel;
diff --git a/frontend/src/components/Map/MapDetailBoard.tsx b/frontend/src/components/Map/MapDetailBoard.tsx
index a6a997af..e6418f9f 100644
--- a/frontend/src/components/Map/MapDetailBoard.tsx
+++ b/frontend/src/components/Map/MapDetailBoard.tsx
@@ -10,7 +10,7 @@ import SideContainer from '@/components/common/SideContainer';
import Marker from '@/components/Marker/Marker';
import DeleteMapButton from './DeleteMapButton';
import EditMapButton from './EditMapButton';
-import MapThumbnail from './MapThumbnail';
+import ListItemThumbnail from '@/components/common/List/ListItemThumbnail';
type MapDetailBoardProps = {
mapData: Map;
@@ -54,7 +54,7 @@ const MapDetailBoard = ({ mapData }: MapDetailBoardProps) => {
)}
{mapData.thumbnailUrl.startsWith('https://example') ? (
-
+
) : (
)}
diff --git a/frontend/src/components/Map/MapItem.tsx b/frontend/src/components/Map/MapItem.tsx
deleted file mode 100644
index 32c4258f..00000000
--- a/frontend/src/components/Map/MapItem.tsx
+++ /dev/null
@@ -1,38 +0,0 @@
-import { MapItemType } from '@/types';
-import { Link } from 'react-router-dom';
-import PinIcon from '@/components/PinIcon';
-import MapThumbnail from './MapThumbnail';
-
-type MapItemProps = {
- mapItem: MapItemType;
-};
-
-const MapItem = ({ mapItem }: MapItemProps) => {
- return (
-
-
-
- {mapItem.thumbnailUrl.startsWith('https://example') ? (
-
- ) : (
-
![]({mapItem.thumbnailUrl})
- )}
-
-
{mapItem.title}
-
-
![]({mapItem.user.profileImageUrl})
-
{mapItem.user.nickname}
-
-
-
-
-
- );
-};
-
-export default MapItem;
diff --git a/frontend/src/components/Map/MapListPanel.tsx b/frontend/src/components/Map/MapListPanel.tsx
deleted file mode 100644
index e4e17530..00000000
--- a/frontend/src/components/Map/MapListPanel.tsx
+++ /dev/null
@@ -1,39 +0,0 @@
-import React from 'react';
-
-import { getMapList } from '@/api/map';
-import { useInfiniteScroll } from '@/hooks/useInfiniteScroll';
-import { MapItemType, MapList } from '@/types';
-
-import MapItem from '@/components/Map/MapItem';
-import Box from '../common/Box';
-
-const MapListPanel = () => {
- const { data, isFetchingNextPage, hasNextPage, ref } =
- useInfiniteScroll({
- queryKey: ['mapList'],
- queryFn: ({ pageParam }) => getMapList(pageParam),
- getNextPageParam: (lastPage) => {
- return lastPage.currentPage < lastPage.totalPages
- ? lastPage.currentPage + 1
- : undefined;
- },
- fetchWithoutQuery: true,
- });
-
- return (
-
-
- {data?.pages.map((page, index) => (
-
- {page.maps.map((map: MapItemType) => (
-
- ))}
-
- ))}
-
-
-
- );
-};
-
-export default MapListPanel;
diff --git a/frontend/src/components/common/List/Course/CourseItem.tsx b/frontend/src/components/common/List/Course/CourseItem.tsx
new file mode 100644
index 00000000..3b439a74
--- /dev/null
+++ b/frontend/src/components/common/List/Course/CourseItem.tsx
@@ -0,0 +1,12 @@
+import { MapItemType } from '@/types';
+import ListItem from '@/components/common/List/ListItem';
+
+type CourseItemProps = {
+ courseItem: MapItemType;
+};
+
+const CourseItem = ({ courseItem }: CourseItemProps) => {
+ return ;
+};
+
+export default CourseItem;
diff --git a/frontend/src/components/common/List/Course/CourseListPanel.tsx b/frontend/src/components/common/List/Course/CourseListPanel.tsx
new file mode 100644
index 00000000..2423688a
--- /dev/null
+++ b/frontend/src/components/common/List/Course/CourseListPanel.tsx
@@ -0,0 +1,39 @@
+import React from 'react';
+
+import { getCourseList } from '@/api/course';
+import { useInfiniteScroll } from '@/hooks/useInfiniteScroll';
+import { CourseList } from '@/types';
+
+import CourseItem from './CourseItem';
+import InfiniteListPanel from '@/components/common/List/InfiniteListPanel';
+
+interface CourseListPanelProps {
+ query?: string;
+}
+
+const CourseListPanel: React.FC = ({ query }) => {
+ const { data, ref } = useInfiniteScroll({
+ queryKey: ['courseList'],
+ queryFn: ({ pageParam }) => getCourseList(pageParam, query),
+ getNextPageParam: (lastPage) =>
+ lastPage.currentPage < lastPage.totalPages
+ ? lastPage.currentPage + 1
+ : undefined,
+ fetchWithoutQuery: true,
+ });
+
+ const courseItems = data?.pages.flatMap((page) => page.courses) || [];
+
+ return (
+ (
+
+ )}
+ className="max-h-[700px] p-5"
+ />
+ );
+};
+
+export default CourseListPanel;
diff --git a/frontend/src/components/common/List/InfiniteListPanel.tsx b/frontend/src/components/common/List/InfiniteListPanel.tsx
new file mode 100644
index 00000000..747fcabd
--- /dev/null
+++ b/frontend/src/components/common/List/InfiniteListPanel.tsx
@@ -0,0 +1,41 @@
+import React from 'react';
+import emptyImage from '../../../assets/empty.svg';
+
+interface InfiniteListPanelProps {
+ data: T[] | undefined;
+ ref: React.Ref;
+ renderItem: (item: T) => React.ReactNode;
+ className?: string;
+}
+
+const InfiniteListPanel = ({
+ data,
+ ref,
+ renderItem,
+ className,
+}: InfiniteListPanelProps) => {
+ return (
+
+ {data && data.length > 0 ? (
+
+ {data.map((item, index) => (
+ {renderItem(item)}
+ ))}
+
+ ) : (
+
+
![리스트가 텅 비었습니다!]({emptyImage})
+
+ )}
+
+ );
+};
+
+export default InfiniteListPanel;
diff --git a/frontend/src/components/common/List/ListItem.tsx b/frontend/src/components/common/List/ListItem.tsx
new file mode 100644
index 00000000..05a54d49
--- /dev/null
+++ b/frontend/src/components/common/List/ListItem.tsx
@@ -0,0 +1,57 @@
+import { Link } from 'react-router-dom';
+import PinIcon from '@/components/PinIcon';
+import React from 'react';
+import ListItemThumbnail from '@/components/common/List/ListItemThumbnail';
+
+type ListItemProps = {
+ item: T;
+ linkPrefix: string;
+};
+
+const ListItem = <
+ T extends {
+ id: number;
+ title: string;
+ thumbnailUrl: string;
+ user: { profileImageUrl: string; nickname: string };
+ pinCount: number;
+ },
+>({
+ item,
+ linkPrefix,
+}: ListItemProps) => {
+ return (
+
+
+
+ {item.thumbnailUrl.startsWith('https://example') ? (
+
+ ) : (
+
![{item.title}]({item.thumbnailUrl})
+ )}
+
+
+
{item.title}
+
+
+
![{item.user.nickname}]({item.user.profileImageUrl})
+
{item.user.nickname}
+
+
+
+
+
+ );
+};
+
+export default ListItem;
diff --git a/frontend/src/components/Map/MapThumbnail.tsx b/frontend/src/components/common/List/ListItemThumbnail.tsx
similarity index 57%
rename from frontend/src/components/Map/MapThumbnail.tsx
rename to frontend/src/components/common/List/ListItemThumbnail.tsx
index 74c82fbd..d5750740 100644
--- a/frontend/src/components/Map/MapThumbnail.tsx
+++ b/frontend/src/components/common/List/ListItemThumbnail.tsx
@@ -1,17 +1,17 @@
-type MapThumbnailProps = {
+type ListItemThumbnailProps = {
className?: string;
thumbnailUrl?: string;
};
-const MapThumbnail = ({
+const ListItemThumbnail = ({
thumbnailUrl = `https://kr.object.ncloudstorage.com/ogil-public/uploads/default_thumbnail/default_3.webp`,
className,
-}: MapThumbnailProps) => {
+}: ListItemThumbnailProps) => {
return (
-
![지도 썸네일]({thumbnailUrl})
+
);
};
-export default MapThumbnail;
+export default ListItemThumbnail;
diff --git a/frontend/src/components/common/ListToggleButtons.tsx b/frontend/src/components/common/List/ListToggleButtons.tsx
similarity index 84%
rename from frontend/src/components/common/ListToggleButtons.tsx
rename to frontend/src/components/common/List/ListToggleButtons.tsx
index ecb69c28..7b379313 100644
--- a/frontend/src/components/common/ListToggleButtons.tsx
+++ b/frontend/src/components/common/List/ListToggleButtons.tsx
@@ -12,7 +12,7 @@ const ToggleButton: React.FC = ({
onSelect,
}) => {
return (
-
+
= ({
{options.map((option) => (