diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 8f6480e..71aa5ad 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -12,6 +12,7 @@ "framer-motion": "^11.0.8", "lucide-react": "^0.344.0", "react": "^18.2.0", + "react-calendar": "^6.0.0", "react-colorful": "^5.6.1", "react-device-detect": "^2.2.3", "react-dom": "^18.2.0", @@ -1259,6 +1260,15 @@ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, + "node_modules/@wojtekmaj/date-utils": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@wojtekmaj/date-utils/-/date-utils-2.0.2.tgz", + "integrity": "sha512-Do66mSlSNifFFuo3l9gNKfRMSFi26CRuQMsDJuuKO/ekrDWuTTtE4ZQxoFCUOG+NgxnpSeBq/k5TY8ZseEzLpA==", + "license": "MIT", + "funding": { + "url": "https://github.com/wojtekmaj/date-utils?sponsor=1" + } + }, "node_modules/any-promise": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", @@ -1463,6 +1473,15 @@ "node": ">= 6" } }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/commander": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", @@ -1717,6 +1736,18 @@ "node": ">=6.9.0" } }, + "node_modules/get-user-locale": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/get-user-locale/-/get-user-locale-3.0.0.tgz", + "integrity": "sha512-iJfHSmdYV39UUBw7Jq6GJzeJxUr4U+S03qdhVuDsR9gCEnfbqLy9gYDJFBJQL1riqolFUKQvx36mEkp2iGgJ3g==", + "license": "MIT", + "dependencies": { + "memoize": "^10.0.0" + }, + "funding": { + "url": "https://github.com/wojtekmaj/get-user-locale?sponsor=1" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -1898,6 +1929,21 @@ "react": "^16.5.1 || ^17.0.0 || ^18.0.0" } }, + "node_modules/memoize": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/memoize/-/memoize-10.2.0.tgz", + "integrity": "sha512-DeC6b7QBrZsRs3Y02A6A7lQyzFbsQbqgjI6UW0GigGWV+u1s25TycMr0XHZE4cJce7rY/vyw2ctMQqfDkIhUEA==", + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sindresorhus/memoize?sponsor=1" + } + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -1922,6 +1968,18 @@ "node": ">=8.6" } }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/motion-dom": { "version": "11.18.1", "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-11.18.1.tgz", @@ -2265,6 +2323,31 @@ "node": ">=0.10.0" } }, + "node_modules/react-calendar": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/react-calendar/-/react-calendar-6.0.0.tgz", + "integrity": "sha512-6wqaki3Us0DNDjZDr0DYIzhSFprNoy4FdPT9Pjy5aD2hJJVjtJwmdMT9VmrTUo949nlk35BOxehThxX62RkuRQ==", + "license": "MIT", + "dependencies": { + "@wojtekmaj/date-utils": "^2.0.2", + "clsx": "^2.0.0", + "get-user-locale": "^3.0.0", + "warning": "^4.0.0" + }, + "funding": { + "url": "https://github.com/wojtekmaj/react-calendar?sponsor=1" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/react-colorful": { "version": "5.6.1", "resolved": "https://registry.npmjs.org/react-colorful/-/react-colorful-5.6.1.tgz", @@ -2906,6 +2989,15 @@ } } }, + "node_modules/warning": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", + "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index e0f6a9b..57e3e39 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -13,6 +13,7 @@ "framer-motion": "^11.0.8", "lucide-react": "^0.344.0", "react": "^18.2.0", + "react-calendar": "^6.0.0", "react-colorful": "^5.6.1", "react-device-detect": "^2.2.3", "react-dom": "^18.2.0", diff --git a/frontend/src/index.css b/frontend/src/index.css index 75d36eb..6766d91 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -98,3 +98,105 @@ body { -ms-overflow-style: none; scrollbar-width: none; } + +/* Swiper autoHeight 지원 */ +.swiper-slide { + height: auto !important; +} + +.mobile-calendar-wrapper .react-calendar__navigation button:hover { + background-color: #f3f4f6; + border-radius: 0.5rem; +} + +.mobile-calendar-wrapper .react-calendar__navigation__label { + font-weight: 600; + font-size: 0.875rem; + color: #374151; +} + +.mobile-calendar-wrapper .react-calendar__month-view__weekdays { + text-align: center; + font-size: 0.75rem; + font-weight: 500; + color: #6b7280; +} + +.mobile-calendar-wrapper .react-calendar__month-view__weekdays__weekday { + padding: 0.5rem 0; +} + +.mobile-calendar-wrapper .react-calendar__month-view__weekdays__weekday abbr { + text-decoration: none; +} + +/* 일요일 (빨간색) */ +.mobile-calendar-wrapper + .react-calendar__month-view__weekdays__weekday:first-child { + color: #f87171; +} + +/* 토요일 (파란색) */ +.mobile-calendar-wrapper + .react-calendar__month-view__weekdays__weekday:last-child { + color: #60a5fa; +} + +.mobile-calendar-wrapper .react-calendar__tile { + display: flex; + flex-direction: column; + align-items: center; + padding: 0.25rem; + background: none; + border: none; + font-size: 0.75rem; + color: #374151; +} + +.mobile-calendar-wrapper .react-calendar__tile:hover { + background-color: #f3f4f6; + border-radius: 9999px; +} + +.mobile-calendar-wrapper .react-calendar__tile abbr { + display: flex; + align-items: center; + justify-content: center; + width: 1.75rem; + height: 1.75rem; + border-radius: 9999px; +} + +/* 이웃 달 날짜 (흐리게) */ +.mobile-calendar-wrapper + .react-calendar__month-view__days__day--neighboringMonth { + color: #d1d5db; +} + +/* 일요일 */ +.mobile-calendar-wrapper .react-calendar__tile.sunday abbr { + color: #ef4444; +} + +/* 토요일 */ +.mobile-calendar-wrapper .react-calendar__tile.saturday abbr { + color: #3b82f6; +} + +/* 오늘 */ +.mobile-calendar-wrapper .react-calendar__tile--now abbr { + background-color: #548360; + color: white; + font-weight: 700; +} + +/* 선택된 날짜 */ +.mobile-calendar-wrapper .react-calendar__tile--active abbr { + background-color: #548360; + color: white; +} + +.mobile-calendar-wrapper .react-calendar__tile--active:enabled:hover abbr, +.mobile-calendar-wrapper .react-calendar__tile--active:enabled:focus abbr { + background-color: #456e50; +} diff --git a/frontend/src/pages/mobile/Schedule.jsx b/frontend/src/pages/mobile/Schedule.jsx index 818ff3b..571162b 100644 --- a/frontend/src/pages/mobile/Schedule.jsx +++ b/frontend/src/pages/mobile/Schedule.jsx @@ -3,8 +3,6 @@ import { motion, AnimatePresence } from 'framer-motion'; import { Clock, Tag, Link2, ChevronLeft, ChevronRight, ChevronDown, Search, X, Calendar } from 'lucide-react'; import { useInfiniteQuery } from '@tanstack/react-query'; import { useInView } from 'react-intersection-observer'; -import { Swiper, SwiperSlide } from 'swiper/react'; -import 'swiper/css'; // 모바일 일정 페이지 function MobileSchedule() { @@ -399,7 +397,10 @@ function TimelineScheduleCard({ schedule, categoryColor, categories, delay = 0 } // 달력 선택기 컴포넌트 function CalendarPicker({ selectedDate, schedules = [], categories = [], onSelectDate }) { const [viewDate, setViewDate] = useState(new Date(selectedDate)); - const swiperRef = useRef(null); + + // 터치 스와이프 핸들링 + const touchStartX = useRef(0); + const touchEndX = useRef(0); // 날짜별 일정 존재 여부 및 카테고리 색상 const scheduleDates = useMemo(() => { @@ -490,33 +491,42 @@ function CalendarPicker({ selectedDate, schedules = [], categories = [], onSelec return () => { document.body.style.overflow = ''; }; }, []); - // 슬라이드에 표시할 3개월 데이터 (이전, 현재, 다음) - const slides = useMemo(() => { - return [-1, 0, 1].map(offset => { - const d = new Date(year, month + offset, 1); - return { - year: d.getFullYear(), - month: d.getMonth(), - days: getCalendarDays(d.getFullYear(), d.getMonth()) - }; - }); + // 현재 달 캘린더 데이터 + const currentMonthDays = useMemo(() => { + return getCalendarDays(year, month); }, [year, month, getCalendarDays]); - // 스와이프 핸들러 - const handleSlideChange = (swiper) => { - const direction = swiper.activeIndex - 1; // -1, 0, 1 - if (direction !== 0) { - changeMonth(direction); - // 다시 중앙으로 리셋 (무한 스와이프를 위해) - setTimeout(() => { - swiper.slideTo(1, 0); - }, 0); + // 터치 핸들러 + const handleTouchStart = (e) => { + touchStartX.current = e.touches[0].clientX; + }; + + const handleTouchMove = (e) => { + touchEndX.current = e.touches[0].clientX; + }; + + const handleTouchEnd = () => { + const diff = touchStartX.current - touchEndX.current; + const threshold = 50; + + if (Math.abs(diff) > threshold) { + if (diff > 0) { + changeMonth(1); + } else { + changeMonth(-1); + } } + touchStartX.current = 0; + touchEndX.current = 0; }; // 월 렌더링 컴포넌트 - const renderMonth = (slideData) => ( -
+ const renderMonth = (days) => ( +
{/* 요일 헤더 */}
{['일', '월', '화', '수', '목', '금', '토'].map((day, i) => ( @@ -533,7 +543,7 @@ function CalendarPicker({ selectedDate, schedules = [], categories = [], onSelec {/* 날짜 그리드 */}
- {slideData.days.map((item, index) => { + {days.map((item, index) => { const dayOfWeek = index % 7; const isSunday = dayOfWeek === 0; const isSaturday = dayOfWeek === 6; @@ -671,9 +681,7 @@ function CalendarPicker({ selectedDate, schedules = [], categories = [], onSelec {/* 달력 헤더 */}
- {/* Swiper 달력 */} - { swiperRef.current = swiper; }} - initialSlide={1} - onSlideChangeTransitionEnd={handleSlideChange} - spaceBetween={20} - slidesPerView={1} - speed={200} - touchRatio={1} - resistance={true} - resistanceRatio={0.5} - > - {slides.map((slide, idx) => ( - - {renderMonth(slide)} - - ))} - + {/* 달력 (터치 스와이프 지원) */} + {renderMonth(currentMonthDays)} {/* 오늘 버튼 */}