+
+
자주 묻는 질문
+
{SINGLE_FAQ.map((qna, i) => (
- - onClickActiveQna(i)} className="cursor-pointer text-13-16-response font-semibold">
+
- onClickActiveQna(i)} className="cursor-pointer text-sm font-semibold">
{qna.question}
@@ -49,15 +49,15 @@ export default function QnaSlide() {
animate={activeIndex === i ? 'open' : 'closed'}
variants={bodyVariants}
transition={{ duration: 0.3, ease: 'easeOut' }}
- className="overflow-hidden"
+ className="overflow-hidden text-left"
>
- {qna.answer}
+ {qna.answer}
))}
-
diff --git a/src/components/main/Carousel/IntroduceSlide.tsx b/src/components/main/Carousel/IntroduceSlide.tsx
new file mode 100644
index 00000000..aa91c311
--- /dev/null
+++ b/src/components/main/Carousel/IntroduceSlide.tsx
@@ -0,0 +1,23 @@
+import Image from 'next/image';
+import Link from 'next/link';
+
+const cards = [
+ { bg: '#3FD9F9', id: 'server', img: '/images/position/server.min.svg' },
+ { bg: '#fd4872', id: 'web', img: '/images/position/web.min.svg' },
+ { bg: '#cdf86f', id: 'design', img: '/images/position/design.min.svg' },
+] as const;
+
+export default function IntroduceSlide() {
+ return (
+
+
▫ 만취 프로젝트 개발자 ▫
+
+ {cards.map(({ id, img, bg }) => (
+
+
+
+ ))}
+
+
+ );
+}
diff --git a/src/components/main/Carousel/IntroduceSlide/index.tsx b/src/components/main/Carousel/IntroduceSlide/index.tsx
deleted file mode 100644
index 8bea8980..00000000
--- a/src/components/main/Carousel/IntroduceSlide/index.tsx
+++ /dev/null
@@ -1,74 +0,0 @@
-import type { Variants } from 'framer-motion';
-import * as m from 'framer-motion/m';
-import Image from 'next/image';
-import { BACKEND_CARDS, DESIGNER_CARDS, FRONTEND_CARDS } from '@/constants/cards';
-import { POSITION_BASE } from '@/constants/image';
-import useInternalRouter from '@/hooks/useInternalRouter';
-
-export default function IntroduceSlide() {
- const router = useInternalRouter();
-
- const cards = [
- { ...BACKEND_CARDS[0], bg: '#3FD9F9', title: 'SERVER' },
- { ...FRONTEND_CARDS[0], bg: '#fd4872', title: 'WEB' },
- { ...DESIGNER_CARDS[0], bg: '#cdf86f', title: 'DESIGN' },
- ];
-
- const cardVariants: Variants = {
- hidden: { opacity: 0, y: 50 },
- visible: (i: number) => ({
- opacity: 1,
- y: 0,
- transition: {
- type: 'spring',
- stiffness: 100,
- damping: 30,
- delay: i * 0.4,
- },
- }),
- };
-
- return (
-
-
-
▫ 만취 프로젝트 개발자 ▫
- router.push('/introduce')}
- className="inline-block rounded-lg bg-white px-5 py-2 text-13-16-response font-bold text-black shadow-md transition hover:bg-gray-50"
- >
- 더 알아보기
-
-
-
- {cards.map((card, index) => (
- router.push('/introduce')}
- className="relative flex w-[300px] cursor-pointer flex-col items-center justify-center rounded-xl p-4 shadow-lg"
- style={{ backgroundColor: card.bg }}
- >
-
- {card.title}
-
- ))}
-
-
- );
-}
diff --git a/src/components/main/Carousel/NoticeBoardSlide.tsx b/src/components/main/Carousel/NoticeBoardSlide.tsx
new file mode 100644
index 00000000..c6d4b617
--- /dev/null
+++ b/src/components/main/Carousel/NoticeBoardSlide.tsx
@@ -0,0 +1,29 @@
+import useInternalRouter from '@/hooks/useInternalRouter';
+
+export default function NoticeBoardSlide() {
+ const router = useInternalRouter();
+
+ return (
+
+
🫧 공지사항 🫧
+
+ New! 만취에서 새로운 카테고리 추가!
+ '여행'을 즐겨보세요.
+
+
+ -
+ 🌍 테마 여행 모임으로 특별한 추억 만들기
+
+ -
+ 📸 사진부터 캠핑까지 다양한 여행 스타일 모임
+
+ -
+ 🤝 혼자가 아닌 함께 떠나는 소그룹 여행!
+
+
+
router.push('/noticeboard')} className="inline-block rounded-lg bg-white px-5 py-2 text-xs font-bold text-black">
+ 공지사항 보기
+
+
+ );
+}
diff --git a/src/components/main/Carousel/NoticeBoardSlide/index.tsx b/src/components/main/Carousel/NoticeBoardSlide/index.tsx
deleted file mode 100644
index c481cffb..00000000
--- a/src/components/main/Carousel/NoticeBoardSlide/index.tsx
+++ /dev/null
@@ -1,37 +0,0 @@
-/* eslint-disable tailwindcss/no-custom-classname */
-import { Gugi } from 'next/font/google';
-import useInternalRouter from '@/hooks/useInternalRouter';
-
-const gugi = Gugi({ weight: '400', subsets: ['latin'] });
-
-export default function NoticeBoardSlide() {
- const router = useInternalRouter();
-
- return (
-
-
🫧 공지사항 🫧
-
- New! 만취에서 새로운 카테고리 추가!
- '여행'을 즐겨보세요.
-
-
- -
- 🌍 테마 여행 모임으로 특별한 추억 만들기
-
- -
- 📸 사진부터 캠핑까지 다양한 여행 스타일 모임
-
- -
- 🤝 혼자가 아닌 함께 떠나는 소그룹 여행!
-
-
-
router.push('/noticeboard')}
- className="inline-block rounded-lg bg-white px-5 py-2 text-13-16-response font-bold text-black shadow-md"
- >
- 공지사항 보기
-
-
- );
-}
diff --git a/src/components/main/Carousel/PopularCategorySlide.tsx b/src/components/main/Carousel/PopularCategorySlide.tsx
new file mode 100644
index 00000000..3f07b610
--- /dev/null
+++ b/src/components/main/Carousel/PopularCategorySlide.tsx
@@ -0,0 +1,58 @@
+import { useCallback } from 'react';
+import Image from 'next/image';
+import { useSetCategory } from '@/store/useFilterStore';
+
+interface PopularCategorySlideProps {
+ base64: {
+ develop: string;
+ food: string;
+ study: string;
+ };
+}
+
+const categories = [
+ { rank: 1, category: '개발', img: 'develop', imageSrc: '/images/main/develop.webp' },
+ { rank: 2, category: '공부', img: 'study', imageSrc: '/images/main/study.webp' },
+ { rank: 3, category: '맛집', img: 'food', imageSrc: '/images/main/food.webp' },
+];
+
+export default function PopularCategorySlide({ base64 }: PopularCategorySlideProps) {
+ const setCategory = useSetCategory();
+
+ const handleCategoryClick = useCallback(
+ (category: string) => {
+ setCategory(category);
+ },
+ [setCategory],
+ );
+
+ return (
+
+
🔥 인기 카테고리 🔥
+
실시간으로 모임수가 증가하고 있어요!
+
+ {categories.map(({ rank, category, img, imageSrc }) => (
+
handleCategoryClick(category)}
+ className="relative flex cursor-pointer flex-col items-center justify-center gap-2 rounded-md duration-200 hover:scale-105"
+ >
+
+ {rank}위
+
+
+
{category} 카테고리
+
+ ))}
+
+
+ );
+}
diff --git a/src/components/main/Carousel/PopularCategorySlide/index.tsx b/src/components/main/Carousel/PopularCategorySlide/index.tsx
deleted file mode 100644
index d3e4ab27..00000000
--- a/src/components/main/Carousel/PopularCategorySlide/index.tsx
+++ /dev/null
@@ -1,56 +0,0 @@
-/* eslint-disable tailwindcss/no-custom-classname */
-import { useCallback } from 'react';
-import Image from 'next/image';
-import { useSetCategory } from '@/store/useFilterStore';
-
-interface PopularCategorySlideProps {
- handleScrollToFilter: () => void;
-}
-
-const categories = [
- { rank: 1, category: '개발', imageSrc: '/images/develop.webp' },
- { rank: 2, category: '공부', imageSrc: '/images/study.webp' },
- { rank: 3, category: '맛집', imageSrc: '/images/food.webp' },
-];
-
-export default function PopularCategorySlide({ handleScrollToFilter }: PopularCategorySlideProps) {
- const setCategory = useSetCategory();
-
- const handleCategoryClick = useCallback(
- (category: string) => {
- setCategory(category);
- handleScrollToFilter();
- },
- [handleScrollToFilter, setCategory],
- );
-
- return (
-
-
-
-
-
-
-
-
🔥 인기 카테고리 🔥
-
실시간으로 모임수가 증가하고 있어요!
-
- {categories.map(({ rank, category, imageSrc }) => (
-
handleCategoryClick(category)}
- className="relative flex cursor-pointer flex-col items-center justify-center gap-3 rounded-md transition-transform duration-300 hover:scale-105"
- >
-
- {rank}위
-
-
-
{category} 카테고리
-
- ))}
-
-
-
-
- );
-}
diff --git a/src/components/main/Carousel/TopSlide/index.tsx b/src/components/main/Carousel/TopSlide/index.tsx
deleted file mode 100644
index 10494005..00000000
--- a/src/components/main/Carousel/TopSlide/index.tsx
+++ /dev/null
@@ -1,27 +0,0 @@
-import Image from 'next/image';
-import DoubleArrow from 'public/icons/DoubleArrow';
-import useInternalRouter from '@/hooks/useInternalRouter';
-
-export default function TopSlide() {
- const router = useInternalRouter();
-
- return (
-
router.push('/main')} className="cursor-pointer bg-[#000000]">
-
-
-
실시간 업데이트!
-
무슨 모임에 가입해야할지 모르겠다면?
-
- 실시간
- TOP 10
- 모임 보러가기
-
-
-
-
-
-
-
-
- );
-}
diff --git a/src/components/main/Carousel/index.tsx b/src/components/main/Carousel/index.tsx
index 12b649a1..46683969 100644
--- a/src/components/main/Carousel/index.tsx
+++ b/src/components/main/Carousel/index.tsx
@@ -1,65 +1,63 @@
-import { useCallback, useEffect, useRef, useState } from 'react';
-import { type Variants } from 'framer-motion';
-import * as m from 'framer-motion/m';
-import ArrowBtn from 'public/icons/ArrowBtn';
-import FAQSlide from '@/components/main/Carousel/FAQSlide';
-import IntroduceSlide from '@/components/main/Carousel/IntroduceSlide';
-import NoticeBoardSlide from '@/components/main/Carousel/NoticeBoardSlide';
-import PopularCategorySlide from '@/components/main/Carousel/PopularCategorySlide';
-import TopSlide from '@/components/main/Carousel/TopSlide';
-
-const zoomVariants: Variants = {
- enter: {
- scale: 1.1,
- opacity: 0,
- },
- center: {
- scale: 1,
- opacity: 1,
- transition: { duration: 0.8 },
- },
-};
+import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
+import dynamic from 'next/dynamic';
+import Image from 'next/image';
+
+const PopularCategorySlide = dynamic(() => import('@/components/main/Carousel/PopularCategorySlide'), { loading: () =>
, ssr: true });
+const IntroduceSlide = dynamic(() => import('@/components/main/Carousel/IntroduceSlide'), { loading: () =>
, ssr: true });
+const NoticeBoardSlide = dynamic(() => import('@/components/main/Carousel/NoticeBoardSlide'), { loading: () =>
, ssr: true });
+const FAQSlide = dynamic(() => import('@/components/main/Carousel/FAQSlide'), { loading: () =>
, ssr: true });
interface CarouselProps {
- handleScrollToFilter: () => void;
+ base64: {
+ design: string;
+ develop: string;
+ food: string;
+ server: string;
+ study: string;
+ web: string;
+ };
}
-export default function Carousel({ handleScrollToFilter }: CarouselProps) {
- const [currentIndex, setCurrentIndex] = useState(0);
+const TOTAL_SLIDES = 4;
- const slides = [
-
,
-
,
-
,
-
,
-
,
- ];
+function Carousel({ base64 }: CarouselProps) {
+ const [currentIndex, setCurrentIndex] = useState(0);
const timeoutRef = useRef
(null);
const intervalRef = useRef(null);
const handleNext = useCallback(() => {
if (timeoutRef.current) return;
- setCurrentIndex((prev) => (prev + 1) % slides.length);
+ setCurrentIndex((prev) => (prev + 1) % TOTAL_SLIDES);
timeoutRef.current = setTimeout(() => {
timeoutRef.current = null;
}, 500);
- }, [slides.length]);
+ }, []);
- const handlePrev = () => {
+ const handlePrev = useCallback(() => {
if (timeoutRef.current) return;
- setCurrentIndex((prev) => (prev - 1 + slides.length) % slides.length);
+ setCurrentIndex((prev) => (prev - 1 + TOTAL_SLIDES) % TOTAL_SLIDES);
timeoutRef.current = setTimeout(() => {
timeoutRef.current = null;
}, 500);
- };
+ }, []);
+
+ const slides = useMemo(
+ () => ({
+ 0: ,
+ 1: ,
+ 2: ,
+ 3: ,
+ }),
+ [base64],
+ );
useEffect(() => {
intervalRef.current = setInterval(() => {
handleNext();
- }, 4000);
+ }, 5000);
return () => {
if (intervalRef.current) {
@@ -68,56 +66,50 @@ export default function Carousel({ handleScrollToFilter }: CarouselProps) {
};
}, [handleNext]);
- useEffect(() => {
- if (timeoutRef.current) {
- clearTimeout(timeoutRef.current);
- }
- if (intervalRef.current) {
- clearInterval(intervalRef.current);
- }
- }, []);
-
return (
-
-
- {slides[currentIndex]}
-
+
+
{slides[currentIndex as keyof typeof slides]}
-
+
-
+
-
- {slides.map((_, index) => (
- // eslint-disable-next-line jsx-a11y/control-has-associated-label
-
+ {Array.from({ length: TOTAL_SLIDES }).map((_, index) => (
+ setCurrentIndex(index)}
- className={`cursor-pointer rounded-full transition-all duration-500 ${index === currentIndex ? 'h-[10px] w-10 bg-white' : 'size-[10px] bg-gray-400'}`}
+ className={`cursor-pointer rounded-full transition-all duration-500 ${
+ index === currentIndex ? 'h-[10px] w-10 bg-white' : 'size-[10px] bg-gray-400'
+ }`}
style={{
transition: 'width 0.7s cubic-bezier(0.25, 0.8, 0.5, 1), background-color 0.5s',
}}
/>
))}
-
+
+ );
+}
+
+export function CarouselSkeleton() {
+ return (
+
);
}
+
+export default memo(Carousel);
diff --git a/src/components/main/Dropdown/index.tsx b/src/components/main/Dropdown/index.tsx
index 35165498..5f914399 100644
--- a/src/components/main/Dropdown/index.tsx
+++ b/src/components/main/Dropdown/index.tsx
@@ -1,5 +1,5 @@
import { useCallback, useEffect, useRef } from 'react';
-import DownArrow from 'public/icons/DownArrow';
+import Image from 'next/image';
interface DropdownProps {
buttonLabel: React.ReactNode;
@@ -44,7 +44,15 @@ export default function Dropdown({ buttonLabel, children, isOpen, setIsOpen, cla
className={`flex items-center rounded-lg border border-gray-100 p-2 text-13-16-response font-semibold text-gray-900 mobile:gap-1 tablet:px-4 ${dropOpen && 'bg-blue-800 text-white'} ${value && 'bg-blue-800 text-white'}`}
>
{buttonLabel}
-
+
{isOpen &&
{children}
}
diff --git a/src/components/main/FilterSection/CategoryList/CategoryItems/index.tsx b/src/components/main/FilterSection/CategoryList/CategoryItems/index.tsx
deleted file mode 100644
index f2ec673d..00000000
--- a/src/components/main/FilterSection/CategoryList/CategoryItems/index.tsx
+++ /dev/null
@@ -1,30 +0,0 @@
-import Image from 'next/image';
-import { useCategory, useSetCategory } from '@/store/useFilterStore';
-
-interface CategoryItemsProps {
- option: { icon: string; id: string; label: string };
-}
-
-export default function CategoryItems({ option }: CategoryItemsProps) {
- const category = useCategory();
- const setCategory = useSetCategory();
-
- return (
- {
- setCategory(option.id);
- }}
- >
-
-
-
- );
-}
diff --git a/src/components/main/FilterSection/CategoryList/index.tsx b/src/components/main/FilterSection/CategoryList/index.tsx
deleted file mode 100644
index cfc43dbb..00000000
--- a/src/components/main/FilterSection/CategoryList/index.tsx
+++ /dev/null
@@ -1,18 +0,0 @@
-import CategoryItems from '@/components/main/FilterSection/CategoryList/CategoryItems';
-import { FILTER_OPTIONS } from '@/constants/filter';
-
-export default function CategoryList() {
- return (
-
-
-
-
-
-
- );
-}
diff --git a/src/components/main/FilterSection/CloseDateToggle/index.tsx b/src/components/main/FilterSection/CloseDateToggle/index.tsx
deleted file mode 100644
index b6175304..00000000
--- a/src/components/main/FilterSection/CloseDateToggle/index.tsx
+++ /dev/null
@@ -1,32 +0,0 @@
-import { useState } from 'react';
-import Image from 'next/image';
-import { Toast } from '@/components/shared/Toast';
-import { useSetCloseDate } from '@/store/useFilterStore';
-
-export default function CloseDateToggle() {
- const [toggleValue, setToggleValue] = useState(false);
-
- const setCloseDate = useSetCloseDate();
-
- const handleCloseDateFilterToggle = () => {
- const updatedToggleValue = !toggleValue;
- setToggleValue(updatedToggleValue);
- setCloseDate(updatedToggleValue ? 'closeDate' : '');
-
- if (updatedToggleValue) {
- Toast('success', '마감 임박순 필터가 적용되었습니다.');
- } else {
- Toast('info', '마감 임박순 필터가 해제되었습니다.');
- }
- };
-
- return (
-
-
- 마감임박
-
- );
-}
diff --git a/src/components/main/FilterSection/DateDropdown/index.tsx b/src/components/main/FilterSection/DateDropdown/index.tsx
deleted file mode 100644
index 490dcb70..00000000
--- a/src/components/main/FilterSection/DateDropdown/index.tsx
+++ /dev/null
@@ -1,95 +0,0 @@
-import { useCallback, useState } from 'react';
-import Dropdown from '@/components/main/Dropdown';
-import Calendar from '@/components/shared/Calendar';
-import { Toast } from '@/components/shared/Toast';
-import { useSetDateEnd, useSetDateStart } from '@/store/useFilterStore';
-
-export default function DateDropdown() {
- const [startDate, setStartDate] = useState(null);
- const [endDate, setEndDate] = useState(null);
- const [dateDropOpen, setDateDropOpen] = useState(false);
- const [isApplyDisabled, setIsApplyDisabled] = useState(false);
-
- const setDateStart = useSetDateStart();
- const setDateEnd = useSetDateEnd();
-
- const handleDateChange = (data: { rangeEnd?: string; rangeStart?: string }) => {
- if (data.rangeStart) {
- setStartDate(data.rangeStart);
- setEndDate(null);
- setIsApplyDisabled(false);
- }
- if (data.rangeEnd) {
- setEndDate(data.rangeEnd);
- setIsApplyDisabled(false);
- }
- };
-
- const handleSubmit = useCallback(() => {
- if (startDate && endDate) {
- setDateStart(startDate);
- setDateEnd(endDate);
- setIsApplyDisabled(true);
- Toast('success', '날짜가 적용되었습니다.');
- setDateDropOpen(false);
- } else {
- Toast('error', '날짜 범위를 선택하세요');
- }
- }, [endDate, setDateEnd, setDateStart, startDate]);
-
- const handleInitClick = useCallback(() => {
- if (!startDate && !endDate) {
- Toast('error', '날짜를 선택하세요');
- return;
- }
-
- if (startDate && endDate) {
- setStartDate(null);
- setEndDate(null);
- setDateStart(undefined);
- setDateEnd(undefined);
- setIsApplyDisabled(false);
- setDateDropOpen(false);
- Toast('info', '날짜 선택이 초기화되었습니다.');
- }
- }, [startDate, endDate, setDateEnd, setDateStart]);
-
- return (
-
- {startDate.replace(/-/g, '.')} - {endDate.replace(/-/g, '.')}
-
- ) : (
- <>
- 모임
- 날짜
- >
- )
- }
- className="left-date-calendar"
- >
-
-
-
-
- 초기화 하기
-
-
- 적용하기
-
-
-
-
- );
-}
diff --git a/src/components/main/FilterSection/RegionDropdown/index.tsx b/src/components/main/FilterSection/RegionDropdown/index.tsx
deleted file mode 100644
index 65b4f9e5..00000000
--- a/src/components/main/FilterSection/RegionDropdown/index.tsx
+++ /dev/null
@@ -1,50 +0,0 @@
-import { useState } from 'react';
-import Dropdown from '@/components/main/Dropdown';
-import { Toast } from '@/components/shared/Toast';
-import { REGION_DATA } from '@/constants/filter';
-import { useLocation, useSetLocation } from '@/store/useFilterStore';
-
-export default function RegionDropdown() {
- const [regionDropOpen, setRegionDropOpen] = useState(false);
-
- const location = useLocation();
- const setLocation = useSetLocation();
-
- const handleInitClick = () => {
- setLocation(undefined);
- setRegionDropOpen(false);
-
- Toast('info', '지역 필터가 초기화되었습니다.');
- };
-
- const handleRegionSelect = (value: string) => {
- setLocation(value);
- setRegionDropOpen(false);
-
- Toast('success', `${value} 지역이 선택되었습니다.`);
- };
-
- return (
-
-
- -
- 전체
-
- {REGION_DATA.map((value) => (
- - handleRegionSelect(value)} className="p-2 hover:bg-gray-50">
- {value}
-
- ))}
-
-
- );
-}
diff --git a/src/components/main/FilterSection/index.tsx b/src/components/main/FilterSection/index.tsx
index 99280c1d..529fc64f 100644
--- a/src/components/main/FilterSection/index.tsx
+++ b/src/components/main/FilterSection/index.tsx
@@ -1,8 +1,8 @@
import { useCallback, useMemo } from 'react';
-import CategoryList from '@/components/main/FilterSection/CategoryList';
-import CloseDateToggle from '@/components/main/FilterSection/CloseDateToggle';
-import DateDropdown from '@/components/main/FilterSection/DateDropdown';
-import RegionDropdown from '@/components/main/FilterSection/RegionDropdown';
+import CategoryList from '@/components/main/HeaderSection/FilterList/CategoryList';
+import CloseDateToggle from '@/components/main/HeaderSection/FilterList/CloseDateToggle';
+import DateDropdown from '@/components/main/HeaderSection/FilterList/DateDropdown';
+import RegionDropdown from '@/components/main/HeaderSection/FilterList/RegionDropdown';
import { Toast } from '@/components/shared/Toast';
import { IS_SERVER } from '@/constants/server';
import useInternalRouter from '@/hooks/useInternalRouter';
diff --git a/src/components/main/HeaderSection/FilterList/CategoryList/index.tsx b/src/components/main/HeaderSection/FilterList/CategoryList/index.tsx
new file mode 100644
index 00000000..69fd456c
--- /dev/null
+++ b/src/components/main/HeaderSection/FilterList/CategoryList/index.tsx
@@ -0,0 +1,32 @@
+import { useCallback } from 'react';
+import { FILTER_OPTIONS } from '@/constants/filter';
+import { useCategory, useSetCategory } from '@/store/useFilterStore';
+
+export default function CategoryList() {
+ const category = useCategory();
+ const setCategory = useSetCategory();
+
+ const handleCategoryClick = useCallback(
+ (id: string) => {
+ setCategory(id);
+ },
+ [setCategory],
+ );
+
+ return (
+
+ {FILTER_OPTIONS.map((option) => (
+ handleCategoryClick(option.id)}
+ className={`flex h-9 cursor-pointer items-center rounded-lg border border-gray-100 px-3 duration-300 ${
+ (!category && option.id === '') || category === option.id ? 'bg-blue-800 text-white' : 'hover:bg-gray-50 hover:text-blue-800'
+ }`}
+ >
+ {option.label}
+
+ ))}
+
+ );
+}
diff --git a/src/components/main/HeaderSection/FilterList/CloseDateToggle/index.tsx b/src/components/main/HeaderSection/FilterList/CloseDateToggle/index.tsx
new file mode 100644
index 00000000..e55b17ea
--- /dev/null
+++ b/src/components/main/HeaderSection/FilterList/CloseDateToggle/index.tsx
@@ -0,0 +1,29 @@
+import { useCallback, useState } from 'react';
+import { Toast } from '@/components/shared/Toast';
+import { useSetCloseDate } from '@/store/useFilterStore';
+
+export default function CloseDateToggle() {
+ const [toggleValue, setToggleValue] = useState(false);
+ const setCloseDate = useSetCloseDate();
+
+ const handleCloseDateFilterToggle = useCallback(() => {
+ setToggleValue(!toggleValue);
+ setCloseDate(!toggleValue ? 'closeDate' : '');
+
+ if (!toggleValue) {
+ Toast('success', '마감순으로 정렬되었습니다.');
+ } else {
+ Toast('info', '최신순으로 정렬되었습니다.');
+ }
+ }, [setCloseDate, toggleValue]);
+
+ return (
+
+ 마감임박
+
+ );
+}
diff --git a/src/components/main/HeaderSection/FilterList/DateDropdown/index.tsx b/src/components/main/HeaderSection/FilterList/DateDropdown/index.tsx
new file mode 100644
index 00000000..b568002f
--- /dev/null
+++ b/src/components/main/HeaderSection/FilterList/DateDropdown/index.tsx
@@ -0,0 +1,116 @@
+import { useCallback, useState } from 'react';
+import dynamic from 'next/dynamic';
+import Image from 'next/image';
+import { Toast } from '@/components/shared/Toast';
+import { useAlertStore } from '@/store/useAlertStore';
+import { useSetDateEnd, useSetDateStart } from '@/store/useFilterStore';
+
+const Alert = dynamic(() => import('@/components/shared/Alert'), { loading: () => null, ssr: false });
+const Calendar = dynamic(() => import('@/components/shared/Calendar'), {
+ loading: () => Loading...
,
+ ssr: false,
+});
+
+export default function DateDropdown() {
+ const [startDate, setStartDate] = useState(null);
+ const [endDate, setEndDate] = useState(null);
+ const [isApplyDisabled, setIsApplyDisabled] = useState(false);
+
+ const setDateStart = useSetDateStart();
+ const setDateEnd = useSetDateEnd();
+
+ const { openDateAlert, closeDateAlert } = useAlertStore();
+
+ const handleDateChange = (data: { rangeEnd?: string; rangeStart?: string }) => {
+ if (data.rangeStart) {
+ setStartDate(data.rangeStart);
+ setEndDate(null);
+ setIsApplyDisabled(false);
+ }
+ if (data.rangeEnd) {
+ setEndDate(data.rangeEnd);
+ setIsApplyDisabled(false);
+ }
+ };
+
+ const handleSubmit = useCallback(() => {
+ if (startDate && endDate) {
+ setDateStart(startDate);
+ setDateEnd(endDate);
+ setIsApplyDisabled(true);
+ closeDateAlert();
+ Toast('success', '날짜가 적용되었습니다.');
+ } else {
+ Toast('error', '날짜 범위를 선택하세요');
+ }
+ }, [closeDateAlert, endDate, setDateEnd, setDateStart, startDate]);
+
+ const handleInitClick = useCallback(() => {
+ if (!startDate && !endDate) {
+ Toast('error', '날짜를 선택하세요');
+ return;
+ }
+
+ if (startDate && endDate) {
+ setStartDate(null);
+ setEndDate(null);
+ setDateStart(undefined);
+ setDateEnd(undefined);
+ setIsApplyDisabled(false);
+ closeDateAlert();
+ Toast('info', '날짜 선택이 초기화되었습니다.');
+ }
+ }, [startDate, endDate, setDateStart, setDateEnd, closeDateAlert]);
+
+ return (
+ <>
+
+ {startDate && endDate ? (
+
+ {startDate.replace(/-/g, '.')} - {endDate.replace(/-/g, '.')}
+
+ ) : (
+ <>
+ 모임
+ 날짜
+ >
+ )}
+
+
+
+
+
+
+
+ 초기화 하기
+
+
+ 적용하기
+
+
+
+
+ >
+ );
+}
diff --git a/src/components/main/HeaderSection/FilterList/RegionDropdown/index.tsx b/src/components/main/HeaderSection/FilterList/RegionDropdown/index.tsx
new file mode 100644
index 00000000..8d9e9e5e
--- /dev/null
+++ b/src/components/main/HeaderSection/FilterList/RegionDropdown/index.tsx
@@ -0,0 +1,60 @@
+import dynamic from 'next/dynamic';
+import Image from 'next/image';
+import { Toast } from '@/components/shared/Toast';
+import { REGION_DATA } from '@/constants/filter';
+import { useAlertStore } from '@/store/useAlertStore';
+import { useLocation, useSetLocation } from '@/store/useFilterStore';
+
+const Alert = dynamic(() => import('@/components/shared/Alert'), { ssr: false });
+
+export default function RegionDropdown() {
+ const { openAlert, closeAlert } = useAlertStore();
+
+ const location = useLocation();
+ const setLocation = useSetLocation();
+
+ const handleInitClick = () => {
+ setLocation(undefined);
+ closeAlert();
+ Toast('info', '지역 필터가 초기화되었습니다.');
+ };
+
+ const handleRegionSelect = (value: string) => {
+ setLocation(value);
+ closeAlert();
+ Toast('success', `${value} 지역이 선택되었습니다.`);
+ };
+
+ return (
+ <>
+
+ {location || '지역'}
+
+
+
+
+ -
+ 전체
+
+ {REGION_DATA.map((value) => (
+ - handleRegionSelect(value)} className="p-2 hover:bg-gray-50">
+ {value}
+
+ ))}
+
+
+ >
+ );
+}
diff --git a/src/components/main/HeaderSection/FilterList/index.tsx b/src/components/main/HeaderSection/FilterList/index.tsx
new file mode 100644
index 00000000..52e90ebe
--- /dev/null
+++ b/src/components/main/HeaderSection/FilterList/index.tsx
@@ -0,0 +1,49 @@
+import { useCallback } from 'react';
+import CategoryList from '@/components/main/HeaderSection/FilterList/CategoryList';
+import CloseDateToggle from '@/components/main/HeaderSection/FilterList/CloseDateToggle';
+import DateDropdown from '@/components/main/HeaderSection/FilterList/DateDropdown';
+import RegionDropdown from '@/components/main/HeaderSection/FilterList/RegionDropdown';
+import { Toast } from '@/components/shared/Toast';
+import useInternalRouter from '@/hooks/useInternalRouter';
+import { userStore } from '@/store/userStore';
+
+export default function FilterList() {
+ const router = useInternalRouter();
+ const isLoggedIn = userStore((state) => state.isLoggedIn);
+
+ const handleCreateButtonClick = useCallback(() => {
+ if (isLoggedIn) {
+ void router.push('/create');
+ } else {
+ Toast('error', '로그인이 필요합니다.');
+ }
+ }, [isLoggedIn, router]);
+
+ return (
+
+
+
+ {/* 필터 버튼들 */}
+
+
+
+
+
+
+ {/* 구분선 */}
+
+
+ {/* 카테고리 버튼들 */}
+
+
+
+
+ 모임 만들기
+
+
+ );
+}
diff --git a/src/components/main/HeaderSection/Header/index.tsx b/src/components/main/HeaderSection/Header/index.tsx
deleted file mode 100644
index baef1b6b..00000000
--- a/src/components/main/HeaderSection/Header/index.tsx
+++ /dev/null
@@ -1,16 +0,0 @@
-import Image from 'next/image';
-import { FILTER_OPTIONS } from '@/constants/filter';
-import { useCategory } from '@/store/useFilterStore';
-
-export default function Header() {
- const category = useCategory();
-
- const selectedOption = FILTER_OPTIONS.find((option) => option.id === (category ?? ''));
-
- return (
-
- {selectedOption && }
-
{category || '전체'}
-
- );
-}
diff --git a/src/components/main/HeaderSection/SearchBar/index.tsx b/src/components/main/HeaderSection/SearchBar/index.tsx
index 49bb09ed..58023c67 100644
--- a/src/components/main/HeaderSection/SearchBar/index.tsx
+++ b/src/components/main/HeaderSection/SearchBar/index.tsx
@@ -1,15 +1,16 @@
import type { ChangeEvent, FormEvent } from 'react';
import { useCallback, useEffect, useState } from 'react';
-import Search from 'public/icons/Search';
+import Image from 'next/image';
import { useKeyword, useSetKeyword, useSetPage } from '@/store/useFilterStore';
export default function SearchBar() {
const keyword = useKeyword();
- const setPage = useSetPage();
const setKeyword = useSetKeyword();
const [searchValue, setSearchValue] = useState(keyword || '');
+ const setPage = useSetPage();
+
const handleSearchChange = useCallback(
(e: ChangeEvent) => {
const { value } = e.target;
@@ -38,18 +39,18 @@ export default function SearchBar() {
}, [keyword]);
return (
-
);
}
diff --git a/src/components/main/HeaderSection/index.tsx b/src/components/main/HeaderSection/index.tsx
index 3827f494..0bebfb55 100644
--- a/src/components/main/HeaderSection/index.tsx
+++ b/src/components/main/HeaderSection/index.tsx
@@ -1,11 +1,33 @@
-import Header from '@/components/main/HeaderSection/Header';
+import { memo } from 'react';
+import FilterList from '@/components/main/HeaderSection/FilterList';
import SearchBar from '@/components/main/HeaderSection/SearchBar';
+import Skeleton from '@/components/shared/Skeleton';
-export default function HeaderSection() {
+function HeaderSection() {
return (
-
-
-
+
+ {/* 헤더 영역 */}
+
+
+ {/* 필터 영역 */}
+
+
+ );
+}
+
+export default memo(HeaderSection);
+
+export function HeaderSkeleton() {
+ return (
+
);
}
diff --git a/src/components/main/CardSection/CardContent/index.tsx b/src/components/main/MainCardSection/CardSection/CardContent/index.tsx
similarity index 92%
rename from src/components/main/CardSection/CardContent/index.tsx
rename to src/components/main/MainCardSection/CardSection/CardContent/index.tsx
index f57d24bd..a3da27b2 100644
--- a/src/components/main/CardSection/CardContent/index.tsx
+++ b/src/components/main/MainCardSection/CardSection/CardContent/index.tsx
@@ -50,8 +50,8 @@ export default function CardContent({ gathering }: CardContentProps) {
-
{groupName}
-
+ {groupName}
+
{category} | {location}
diff --git a/src/components/main/CardSection/CardImage/index.tsx b/src/components/main/MainCardSection/CardSection/CardImage/index.tsx
similarity index 83%
rename from src/components/main/CardSection/CardImage/index.tsx
rename to src/components/main/MainCardSection/CardSection/CardImage/index.tsx
index b36a3f70..37bdb225 100644
--- a/src/components/main/CardSection/CardImage/index.tsx
+++ b/src/components/main/MainCardSection/CardSection/CardImage/index.tsx
@@ -1,5 +1,4 @@
import { useMemo, useState } from 'react';
-import { Bagel_Fat_One } from 'next/font/google';
import Image from 'next/image';
import Tag from '@/components/shared/Tag';
import type { GetGatheringResponse } from '@manchui-api';
@@ -8,8 +7,6 @@ interface CardImageProps {
gathering: GetGatheringResponse['data']['gatheringList'][number];
}
-const bagelFatOne = Bagel_Fat_One({ weight: '400', subsets: ['latin'] });
-
export default function CardImage({ gathering }: CardImageProps) {
const { gatheringImage, currentUsers, maxUsers, closed, gatheringDate } = gathering;
@@ -26,10 +23,7 @@ export default function CardImage({ gathering }: CardImageProps) {
const [imageSrc, setImageSrc] = useState(gatheringImage);
- const handleImageError = () => {
- setImageSrc('/images/no-img.png');
- };
-
+ const handleImageError = () => setImageSrc('/images/no-img.png');
return (
{showTag && }
{(currentUsers >= maxUsers || closed) && (
- {closed ? 'CLOSED' : 'FULL'}
+ {closed ? 'CLOSED' : 'FULL'}
)}
diff --git a/src/components/main/MainCardSection/CardSection/index.tsx b/src/components/main/MainCardSection/CardSection/index.tsx
new file mode 100644
index 00000000..4dd2c64f
--- /dev/null
+++ b/src/components/main/MainCardSection/CardSection/index.tsx
@@ -0,0 +1,51 @@
+import { memo } from 'react';
+import Image from 'next/image';
+import Link from 'next/link';
+import CardContent from '@/components/main/MainCardSection/CardSection/CardContent';
+import CardImage from '@/components/main/MainCardSection/CardSection/CardImage';
+import type { GetGatheringResponse } from '@manchui-api';
+
+interface CardSectionProps {
+ gathering: GetGatheringResponse['data']['gatheringList'][number];
+}
+
+function CardSection({ gathering }: CardSectionProps) {
+ return (
+
+
+
+
+ );
+}
+
+export default memo(CardSection);
+
+export function CardSkeleton() {
+ return (
+
+ );
+}
+
+export function MessageWithLink({ message, buttonText, link, onClick }: { buttonText: string; link?: string; message?: string; onClick?: () => void }) {
+ return (
+
+ {message}
+ {link ? (
+
+ {buttonText}
+
+
+ ) : (
+
+ {buttonText}
+
+
+ )}
+
+ );
+}
diff --git a/src/components/main/MainCardSection/index.tsx b/src/components/main/MainCardSection/index.tsx
index 3daf13e1..e66de346 100644
--- a/src/components/main/MainCardSection/index.tsx
+++ b/src/components/main/MainCardSection/index.tsx
@@ -1,29 +1,32 @@
-import CardSection, { CardSkeleton, MessageWithLink } from '@/components/main/CardSection';
+import { useMemo } from 'react';
+import CardSection, { CardSkeleton, MessageWithLink } from '@/components/main/MainCardSection/CardSection';
import NoData from '@/components/shared/NoData';
+import PAGE_SIZE_BY_DEVICE from '@/constants/pageSize';
+import useDeviceState from '@/hooks/useDeviceState';
import type { GetGatheringResponse } from '@manchui-api';
interface MainCardSectionProps {
isError: boolean;
isLoading: boolean;
- mainData: GetGatheringResponse['data']['gatheringList'];
- pageSize: number;
+ mainData: GetGatheringResponse['data']['gatheringList'][number][] | undefined;
scrollRef?: React.RefObject
;
}
-export default function MainCardSection({ isLoading, isError, mainData, pageSize, scrollRef }: MainCardSectionProps) {
+export default function MainCardSection({ isLoading, isError, mainData, scrollRef }: MainCardSectionProps) {
+ const deviceState = useDeviceState();
+ const pageSize = useMemo(() => PAGE_SIZE_BY_DEVICE.MAIN[deviceState], [deviceState]);
+
return (
-
-
- {isLoading && !isError
- ? Array.from({ length: pageSize }).map((_, idx) =>
)
- : mainData.map((gathering) =>
)}
- {mainData.length === 0 && !isError && !isLoading &&
}
- {isError && (
-
- window.location.reload()} />
-
- )}
-
+
+ {isLoading
+ ? Array.from({ length: pageSize }).map((_, idx) =>
)
+ : mainData?.map((gathering) =>
)}
+ {mainData?.length === 0 && !isError && !isLoading &&
}
+ {isError && (
+
+ window.location.reload()} />
+
+ )}
);
}
diff --git a/src/components/main/MainContainer/index.tsx b/src/components/main/MainContainer/index.tsx
deleted file mode 100644
index 09d64830..00000000
--- a/src/components/main/MainContainer/index.tsx
+++ /dev/null
@@ -1,3 +0,0 @@
-export default function MainContainer({ children }: { children: React.ReactNode }) {
- return
{children}
;
-}
diff --git a/src/components/main/SpeedDial/SpeedCreate.tsx b/src/components/main/SpeedDial/SpeedCreate.tsx
deleted file mode 100644
index fd2686d7..00000000
--- a/src/components/main/SpeedDial/SpeedCreate.tsx
+++ /dev/null
@@ -1,11 +0,0 @@
-import type { Props } from '@/components/shared/Svg';
-import { Svg } from '@/components/shared/Svg';
-
-export default function SpeedCreate({ color = '#FFFFFF', className, ...props }: Props) {
- return (
-
- );
-}
diff --git a/src/components/main/SpeedDial/SpeedIntroduce.tsx b/src/components/main/SpeedDial/SpeedIntroduce.tsx
deleted file mode 100644
index da06e390..00000000
--- a/src/components/main/SpeedDial/SpeedIntroduce.tsx
+++ /dev/null
@@ -1,11 +0,0 @@
-import type { Props } from '@/components/shared/Svg';
-import { Svg } from '@/components/shared/Svg';
-
-export default function SpeedIntroduce({ color = '#FFFFFF', className, ...props }: Props) {
- return (
-
- );
-}
diff --git a/src/components/main/SpeedDial/SpeedReview.tsx b/src/components/main/SpeedDial/SpeedReview.tsx
deleted file mode 100644
index 311d803b..00000000
--- a/src/components/main/SpeedDial/SpeedReview.tsx
+++ /dev/null
@@ -1,10 +0,0 @@
-import type { Props } from '@/components/shared/Svg';
-import { Svg } from '@/components/shared/Svg';
-
-export default function SpeedReview({ color = '#FFFFFF', className, ...props }: Props) {
- return (
-
- );
-}
diff --git a/src/components/main/SpeedDial/index.tsx b/src/components/main/SpeedDial/index.tsx
deleted file mode 100644
index cb52bf74..00000000
--- a/src/components/main/SpeedDial/index.tsx
+++ /dev/null
@@ -1,79 +0,0 @@
-import { useState } from 'react';
-import { AnimatePresence } from 'framer-motion';
-import * as m from 'framer-motion/m';
-import Device from '@/constants/device';
-import { SPEED_DIAL_BUTTONS } from '@/constants/speedDial';
-import useDeviceState from '@/hooks/useDeviceState';
-import useInternalRouter from '@/hooks/useInternalRouter';
-
-export default function SpeedDial() {
- const [isOpen, setIsOpen] = useState(false);
-
- const router = useInternalRouter();
-
- const deviceState = useDeviceState();
-
- const toggleSpeedDial = () => setIsOpen((prev) => !prev);
-
- if (deviceState !== Device.Tablet && deviceState !== Device.Mobile) {
- return null;
- }
-
- return (
-
-
- {isOpen && (
-
- {SPEED_DIAL_BUTTONS.map((button, i) => (
-
-
-
-
router.push(button.link)}
- type="button"
- className="flex-center size-[50px] rounded-full bg-gray-700 shadow-md hover:bg-blue-700"
- >
- {button.icon}
-
-
-
- ))}
-
- )}
-
-
-
-
-
-
-
- );
-}
diff --git a/src/components/mypage/card-style/index.tsx b/src/components/mypage/card-style/index.tsx
index cb57ce8d..f4792b73 100644
--- a/src/components/mypage/card-style/index.tsx
+++ b/src/components/mypage/card-style/index.tsx
@@ -1,8 +1,8 @@
import { useState } from 'react';
-import Lottie from 'lottie-react';
+import dynamic from 'next/dynamic';
import getMyAttendance from '@/apis/mypage/get-mypage-attendance';
import getMyGathering from '@/apis/mypage/get-mypage-gathring';
-import { MessageWithLink } from '@/components/main/CardSection';
+import { MessageWithLink } from '@/components/main/MainCardSection/CardSection';
import PaginationBtn from '@/components/shared/PaginationBtn';
import useFilterStore from '@/store/useFilterStore';
import { useQuery, useQueryClient } from '@tanstack/react-query';
@@ -13,6 +13,8 @@ import MyReviewList from '../my-review-list';
import Empty from 'public/lottie/empty.json';
+const Lottie = dynamic(() => import('lottie-light-react'), { ssr: false });
+
export function CardComponents({ category }: { category: string }) {
const queryClient = useQueryClient();
const [review, setReview] = useState('작성 가능한 리뷰');
diff --git a/src/components/mypage/my-review-list/index.tsx b/src/components/mypage/my-review-list/index.tsx
index c5fd7ade..e655a969 100644
--- a/src/components/mypage/my-review-list/index.tsx
+++ b/src/components/mypage/my-review-list/index.tsx
@@ -1,7 +1,7 @@
-import Lottie from 'lottie-react';
+import dynamic from 'next/dynamic';
import getMyReviewable from '@/apis/mypage/get-mypage-reviewable';
import getMyReviews from '@/apis/mypage/get-mypage-reviews';
-import { MessageWithLink } from '@/components/main/CardSection';
+import { MessageWithLink } from '@/components/main/MainCardSection/CardSection';
import PaginationBtn from '@/components/shared/PaginationBtn';
import useFilterStore from '@/store/useFilterStore';
import { useQuery } from '@tanstack/react-query';
@@ -11,6 +11,8 @@ import { ReviewableCard } from '../card-style/reviewable-card';
import Empty from 'public/lottie/empty.json';
+const Lottie = dynamic(() => import('lottie-light-react'), { ssr: false });
+
export default function MyReviewList({ category, review, handleRemoveItem }: { category: string; handleRemoveItem: (id: number) => void; review: string }) {
const isReview = category === '나의 리뷰' && review === '작성 가능한 리뷰';
const { page } = useFilterStore();
diff --git a/src/components/review/FilterSection/index.tsx b/src/components/review/FilterSection/index.tsx
index 819af469..057c05c6 100644
--- a/src/components/review/FilterSection/index.tsx
+++ b/src/components/review/FilterSection/index.tsx
@@ -1,7 +1,7 @@
import { type Dispatch, type SetStateAction } from 'react';
-import CategoryList from '@/components/main/FilterSection/CategoryList';
-import DateDropdown from '@/components/main/FilterSection/DateDropdown';
-import RegionDropdown from '@/components/main/FilterSection/RegionDropdown';
+import CategoryList from '@/components/main/HeaderSection/FilterList/CategoryList';
+import DateDropdown from '@/components/main/HeaderSection/FilterList/DateDropdown';
+import RegionDropdown from '@/components/main/HeaderSection/FilterList/RegionDropdown';
import SortToggle from './SortToggle';
diff --git a/src/components/review/ReviewCardList/index.tsx b/src/components/review/ReviewCardList/index.tsx
index a6a3a2df..8c3f3a00 100644
--- a/src/components/review/ReviewCardList/index.tsx
+++ b/src/components/review/ReviewCardList/index.tsx
@@ -1,7 +1,7 @@
/* eslint-disable no-unused-vars */
/* eslint-disable @typescript-eslint/no-unused-vars */
// 스켈레톤 나중에 추가
-import { MessageWithLink } from '@/components/main/CardSection';
+import { MessageWithLink } from '@/components/main/MainCardSection/CardSection';
import type { GetReviewResponse } from '@manchui-api';
import { ReviewCard } from '../ReviewCard';
@@ -18,8 +18,8 @@ export default function ReviewCardList({ data, isLoading, isError, skeletonCount
{data?.reviewContentList.map((reviewContent) => )}
{data?.reviewCount === 0 && (
-
-
+
+
)}
diff --git a/src/components/shared/Alert/index.tsx b/src/components/shared/Alert/index.tsx
new file mode 100644
index 00000000..25c1fc1c
--- /dev/null
+++ b/src/components/shared/Alert/index.tsx
@@ -0,0 +1,29 @@
+import { createPortal } from 'react-dom';
+import { useAlertStore } from '@/store/useAlertStore';
+
+interface AlertProps {
+ children: React.ReactNode;
+ type?: 'region' | 'date';
+}
+
+export default function Alert({ children, type = 'region' }: AlertProps) {
+ const { isAlertOpen, isDateAlertOpen, closeAlert, closeDateAlert } = useAlertStore();
+
+ // type에 따라 서로 다른 상태와 핸들러 선택
+ const isOpen = type === 'region' ? isAlertOpen : isDateAlertOpen;
+ const closeHandler = type === 'region' ? closeAlert : closeDateAlert;
+
+ if (!isOpen) return null;
+
+ return createPortal(
+
{
+ if (e.target === e.currentTarget) closeHandler();
+ }}
+ >
+
{children}
+
,
+ document.body,
+ );
+}
diff --git a/src/components/shared/Calendar/CalendarGrid/index.tsx b/src/components/shared/Calendar/CalendarGrid/index.tsx
index 9ea09c6f..5bae5e40 100644
--- a/src/components/shared/Calendar/CalendarGrid/index.tsx
+++ b/src/components/shared/Calendar/CalendarGrid/index.tsx
@@ -67,7 +67,10 @@ export default function CalendarGrid({ currentDate, onDateSelect, rangeStart, ra
'text-gray-300': isPastDate,
});
- const hoverClasses = selectionType === 'range' && (!rangeStart || (rangeStart && !rangeEnd)) ? 'hover:bg-blue-700' : '';
+ const hoverClasses =
+ selectionType === 'range' && (!rangeStart || (rangeStart && !rangeEnd))
+ ? `hover:bg-blue-700 ${!isSunday && !isSaturday && !isToday ? 'hover:text-white' : ''}`
+ : '';
const dayClasses = twMerge(
'cursor-pointer rounded-lg py-[6px] text-center text-sm font-medium transition duration-200 ease-in-out',
diff --git a/src/components/shared/Calendar/CalendarSelector/index.tsx b/src/components/shared/Calendar/CalendarSelector/index.tsx
index 621bec4f..8f643520 100644
--- a/src/components/shared/Calendar/CalendarSelector/index.tsx
+++ b/src/components/shared/Calendar/CalendarSelector/index.tsx
@@ -1,6 +1,5 @@
import { useCallback, useMemo } from 'react';
-import ArrowBtn from 'public/icons/ArrowBtn';
-import DownArrow from 'public/icons/DownArrow';
+import Image from 'next/image';
interface CalendarSelectorProps {
currentDate: Date;
@@ -29,8 +28,9 @@ export default function CalendarSelector({ setDropOpen, dropOpen, currentDate, s
return (
-
setDropOpen(!dropOpen)} className="flex cursor-pointer items-center gap-1 text-13-15-response font-semibold text-gray-700">
- {currentDate.getFullYear()}년 {currentDate.getMonth() + 1}월
+ setDropOpen(!dropOpen)} className="flex cursor-pointer items-center text-md font-semibold tablet:text-lg">
+ {currentDate.getFullYear()}년 {currentDate.getMonth() + 1}월{' '}
+
{dropOpen && (
changeMonth('prev')}>
-
+
changeMonth('next')}>
-
+
diff --git a/src/components/shared/Calendar/index.tsx b/src/components/shared/Calendar/index.tsx
index 047330fc..4f2c9998 100644
--- a/src/components/shared/Calendar/index.tsx
+++ b/src/components/shared/Calendar/index.tsx
@@ -1,8 +1,15 @@
-/* eslint-disable tailwindcss/no-custom-classname */
import { useCallback, useState } from 'react';
import clsx from 'clsx';
-import CalendarGrid from '@/components/shared/Calendar/CalendarGrid';
-import CalendarSelector from '@/components/shared/Calendar/CalendarSelector';
+import dynamic from 'next/dynamic';
+
+const CalendarSelector = dynamic(() => import('@/components/shared/Calendar/CalendarSelector'), {
+ loading: () => Loading...
,
+ ssr: false,
+});
+const CalendarGrid = dynamic(() => import('@/components/shared/Calendar/CalendarGrid'), {
+ loading: () => Loading...
,
+ ssr: false,
+});
interface CalendarProps {
endDate?: string | null;
diff --git a/src/components/shared/ErrorBoundary/index.tsx b/src/components/shared/ErrorBoundary/index.tsx
new file mode 100644
index 00000000..3d1dfb8e
--- /dev/null
+++ b/src/components/shared/ErrorBoundary/index.tsx
@@ -0,0 +1,70 @@
+import type { ErrorInfo } from 'react';
+import React from 'react';
+
+interface Props {
+ children: React.ReactNode;
+ fallbackComponent?: React.ReactNode;
+}
+
+interface State {
+ hasError: boolean; // 에러가 발생했는지 여부
+}
+
+// ErrorBoundary는 라이프 사이클을 사용해야 하기 때문에 클래스형 컴포넌트를 사용해야 할 수 밖에 없습니다.
+class ErrorBoundary extends React.Component {
+ constructor(props: Props) {
+ super(props);
+
+ this.state = { hasError: false };
+ }
+
+ static getDerivedStateFromError() {
+ // 1. 이 라이프 사이클에서 에러가 발생하면 컴포넌트를 업데이트를 하면서 리턴된 값을 가지고 state를 업데이트 합니다.
+ // 2. 이렇게 업데이트된 state는
+ return { hasError: true };
+ }
+
+ componentDidCatch(error: Error, errorInfo: ErrorInfo) {
+ console.log({ error, errorInfo });
+ }
+
+ render() {
+ const { hasError } = this.state;
+ const { fallbackComponent, children } = this.props;
+
+ // 3. 다시 리렌더링이 될 거고 이 당시에 state를 바라보고 에러가 발생을 했다면 아래 컴포넌트로 대체를 해줍니다.
+ if (hasError) {
+ if (fallbackComponent != null) return fallbackComponent;
+
+ // 공통 에러 컴포넌트
+ return (
+
+
+
+
+
+
+
알 수 없는 문제가 발생했습니다.
+
잠시 후 다시 시도해주세요 :)
+
+
this.setState({ hasError: false })}
+ className="mt-6 rounded-xl bg-blue-500 px-10 py-2 duration-300 hover:bg-blue-600"
+ >
+ 재시도
+
+
+
+
+ );
+ }
+
+ // 4. 에러가 발생하지 않았다면 기존에 우리가 그려주고 싶은 컴포넌트를 그려주는 역할을 합니다.
+ return children;
+ }
+}
+
+export default ErrorBoundary;
diff --git a/src/components/shared/GNB/Notification/index.tsx b/src/components/shared/GNB/Notification/index.tsx
index 262f414a..35281fd3 100644
--- a/src/components/shared/GNB/Notification/index.tsx
+++ b/src/components/shared/GNB/Notification/index.tsx
@@ -28,9 +28,7 @@ export default function Notification() {
useEffect(
function handleScrollFetch() {
- if ((isIntersecting || isIntersectingInMobile) && hasNextPage) {
- void fetchNextPage();
- }
+ if ((isIntersecting || isIntersectingInMobile) && hasNextPage) void fetchNextPage();
},
[isIntersecting, hasNextPage, isIntersectingInMobile, fetchNextPage],
);
diff --git a/src/components/shared/GNB/index.tsx b/src/components/shared/GNB/index.tsx
index 53c22dc3..c8e8ec00 100644
--- a/src/components/shared/GNB/index.tsx
+++ b/src/components/shared/GNB/index.tsx
@@ -52,11 +52,11 @@ export default function GNB() {
}, [login, data, logoutStore, queryClient, updateUser]);
return (
-