diff --git a/.pnp.cjs b/.pnp.cjs index 3f32b5a..c036ff2 100755 --- a/.pnp.cjs +++ b/.pnp.cjs @@ -63,6 +63,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ["react-router-dom", "virtual:429706adb15e35099964c7cd76f08e01c8fe72ac836e96927082266bf5c07b84ac6c3bd82481948dbf5d4de4034e4e9eed908e986f2f622051577eb971cce2dc#npm:6.23.1"],\ ["react-scripts", "virtual:429706adb15e35099964c7cd76f08e01c8fe72ac836e96927082266bf5c07b84ac6c3bd82481948dbf5d4de4034e4e9eed908e986f2f622051577eb971cce2dc#npm:5.0.1"],\ ["react-toastify", "virtual:429706adb15e35099964c7cd76f08e01c8fe72ac836e96927082266bf5c07b84ac6c3bd82481948dbf5d4de4034e4e9eed908e986f2f622051577eb971cce2dc#npm:10.0.5"],\ + ["react-tooltip", "virtual:429706adb15e35099964c7cd76f08e01c8fe72ac836e96927082266bf5c07b84ac6c3bd82481948dbf5d4de4034e4e9eed908e986f2f622051577eb971cce2dc#npm:5.28.0"],\ ["sass", "npm:1.77.4"],\ ["stylelint", "npm:14.16.1"],\ ["stylelint-config-standard", "virtual:429706adb15e35099964c7cd76f08e01c8fe72ac836e96927082266bf5c07b84ac6c3bd82481948dbf5d4de4034e4e9eed908e986f2f622051577eb971cce2dc#npm:26.0.0"],\ @@ -3879,6 +3880,36 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { "linkType": "HARD"\ }]\ ]],\ + ["@floating-ui/core", [\ + ["npm:1.6.8", {\ + "packageLocation": "./.yarn/cache/@floating-ui-core-npm-1.6.8-496cdfbb6e-82faa6ea9d.zip/node_modules/@floating-ui/core/",\ + "packageDependencies": [\ + ["@floating-ui/core", "npm:1.6.8"],\ + ["@floating-ui/utils", "npm:0.2.8"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@floating-ui/dom", [\ + ["npm:1.6.11", {\ + "packageLocation": "./.yarn/cache/@floating-ui-dom-npm-1.6.11-b81155e63e-d6413759ab.zip/node_modules/@floating-ui/dom/",\ + "packageDependencies": [\ + ["@floating-ui/dom", "npm:1.6.11"],\ + ["@floating-ui/core", "npm:1.6.8"],\ + ["@floating-ui/utils", "npm:0.2.8"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@floating-ui/utils", [\ + ["npm:0.2.8", {\ + "packageLocation": "./.yarn/cache/@floating-ui-utils-npm-0.2.8-01a00634a5-deb98bba01.zip/node_modules/@floating-ui/utils/",\ + "packageDependencies": [\ + ["@floating-ui/utils", "npm:0.2.8"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["@hookform/resolvers", [\ ["npm:3.4.2", {\ "packageLocation": "./.yarn/cache/@hookform-resolvers-npm-3.4.2-4fe00fb172-5045c3b1bf.zip/node_modules/@hookform/resolvers/",\ @@ -7771,6 +7802,15 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { "linkType": "HARD"\ }]\ ]],\ + ["classnames", [\ + ["npm:2.5.1", {\ + "packageLocation": "./.yarn/cache/classnames-npm-2.5.1-c7273f3423-da424a8a6f.zip/node_modules/classnames/",\ + "packageDependencies": [\ + ["classnames", "npm:2.5.1"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["clean-css", [\ ["npm:5.3.3", {\ "packageLocation": "./.yarn/cache/clean-css-npm-5.3.3-d2bb553a94-941987c148.zip/node_modules/clean-css/",\ @@ -13393,6 +13433,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ["react-router-dom", "virtual:429706adb15e35099964c7cd76f08e01c8fe72ac836e96927082266bf5c07b84ac6c3bd82481948dbf5d4de4034e4e9eed908e986f2f622051577eb971cce2dc#npm:6.23.1"],\ ["react-scripts", "virtual:429706adb15e35099964c7cd76f08e01c8fe72ac836e96927082266bf5c07b84ac6c3bd82481948dbf5d4de4034e4e9eed908e986f2f622051577eb971cce2dc#npm:5.0.1"],\ ["react-toastify", "virtual:429706adb15e35099964c7cd76f08e01c8fe72ac836e96927082266bf5c07b84ac6c3bd82481948dbf5d4de4034e4e9eed908e986f2f622051577eb971cce2dc#npm:10.0.5"],\ + ["react-tooltip", "virtual:429706adb15e35099964c7cd76f08e01c8fe72ac836e96927082266bf5c07b84ac6c3bd82481948dbf5d4de4034e4e9eed908e986f2f622051577eb971cce2dc#npm:5.28.0"],\ ["sass", "npm:1.77.4"],\ ["stylelint", "npm:14.16.1"],\ ["stylelint-config-standard", "virtual:429706adb15e35099964c7cd76f08e01c8fe72ac836e96927082266bf5c07b84ac6c3bd82481948dbf5d4de4034e4e9eed908e986f2f622051577eb971cce2dc#npm:26.0.0"],\ @@ -17250,6 +17291,34 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { "linkType": "HARD"\ }]\ ]],\ + ["react-tooltip", [\ + ["npm:5.28.0", {\ + "packageLocation": "./.yarn/cache/react-tooltip-npm-5.28.0-a698d2e4f8-4d1efae0fb.zip/node_modules/react-tooltip/",\ + "packageDependencies": [\ + ["react-tooltip", "npm:5.28.0"]\ + ],\ + "linkType": "SOFT"\ + }],\ + ["virtual:429706adb15e35099964c7cd76f08e01c8fe72ac836e96927082266bf5c07b84ac6c3bd82481948dbf5d4de4034e4e9eed908e986f2f622051577eb971cce2dc#npm:5.28.0", {\ + "packageLocation": "./.yarn/__virtual__/react-tooltip-virtual-80e0e5fce3/0/cache/react-tooltip-npm-5.28.0-a698d2e4f8-4d1efae0fb.zip/node_modules/react-tooltip/",\ + "packageDependencies": [\ + ["react-tooltip", "virtual:429706adb15e35099964c7cd76f08e01c8fe72ac836e96927082266bf5c07b84ac6c3bd82481948dbf5d4de4034e4e9eed908e986f2f622051577eb971cce2dc#npm:5.28.0"],\ + ["@floating-ui/dom", "npm:1.6.11"],\ + ["@types/react", "npm:18.3.3"],\ + ["@types/react-dom", "npm:18.3.0"],\ + ["classnames", "npm:2.5.1"],\ + ["react", "npm:18.3.1"],\ + ["react-dom", "virtual:429706adb15e35099964c7cd76f08e01c8fe72ac836e96927082266bf5c07b84ac6c3bd82481948dbf5d4de4034e4e9eed908e986f2f622051577eb971cce2dc#npm:18.3.1"]\ + ],\ + "packagePeers": [\ + "@types/react-dom",\ + "@types/react",\ + "react-dom",\ + "react"\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["read-cache", [\ ["npm:1.0.0", {\ "packageLocation": "./.yarn/cache/read-cache-npm-1.0.0-00fa89ed05-cffc728b9e.zip/node_modules/read-cache/",\ diff --git a/.yarn/cache/@esbuild-darwin-arm64-npm-0.20.2-e287d70c91-8.zip b/.yarn/cache/@esbuild-darwin-arm64-npm-0.20.2-e287d70c91-8.zip deleted file mode 100644 index 56e2864..0000000 Binary files a/.yarn/cache/@esbuild-darwin-arm64-npm-0.20.2-e287d70c91-8.zip and /dev/null differ diff --git a/.yarn/cache/@rollup-rollup-darwin-arm64-npm-4.18.0-683829554f-8.zip b/.yarn/cache/@rollup-rollup-darwin-arm64-npm-4.18.0-683829554f-8.zip deleted file mode 100644 index 4242cc7..0000000 Binary files a/.yarn/cache/@rollup-rollup-darwin-arm64-npm-4.18.0-683829554f-8.zip and /dev/null differ diff --git a/.yarn/cache/fsevents-patch-21ad2b1333-8.zip b/.yarn/cache/fsevents-patch-21ad2b1333-8.zip deleted file mode 100644 index c6a96df..0000000 Binary files a/.yarn/cache/fsevents-patch-21ad2b1333-8.zip and /dev/null differ diff --git a/package.json b/package.json index ae79d1d..1562d90 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "react-router-dom": "^6.9.0", "react-scripts": "5.0.1", "react-toastify": "^10.0.4", + "react-tooltip": "^5.28.0", "sass": "^1.60.0", "typescript": "^4.4.2", "vite": "^5.2.12", diff --git a/src/assets/svg/auth/menu.svg b/src/assets/svg/auth/menu.svg new file mode 100644 index 0000000..35d3e15 --- /dev/null +++ b/src/assets/svg/auth/menu.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/assets/svg/auth/search-icon.svg b/src/assets/svg/auth/search-icon.svg new file mode 100644 index 0000000..3a27278 --- /dev/null +++ b/src/assets/svg/auth/search-icon.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/assets/svg/common/arrow-left.svg b/src/assets/svg/common/arrow-left.svg new file mode 100644 index 0000000..832d4a0 --- /dev/null +++ b/src/assets/svg/common/arrow-left.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/assets/svg/common/arrow-right.svg b/src/assets/svg/common/arrow-right.svg new file mode 100644 index 0000000..9357fe9 --- /dev/null +++ b/src/assets/svg/common/arrow-right.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/assets/svg/common/info.svg b/src/assets/svg/common/info.svg new file mode 100644 index 0000000..7096992 --- /dev/null +++ b/src/assets/svg/common/info.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/assets/svg/common/loading.svg b/src/assets/svg/common/loading.svg new file mode 100644 index 0000000..dd281bc --- /dev/null +++ b/src/assets/svg/common/loading.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/index.scss b/src/index.scss index 5803e39..92b2ca6 100644 --- a/src/index.scss +++ b/src/index.scss @@ -36,3 +36,7 @@ button { ul { list-style: none; } + +input::-webkit-inner-spin-button { + appearance: none; +} diff --git a/src/layout/Header/Dropdown.module.scss b/src/layout/Header/Dropdown.module.scss new file mode 100644 index 0000000..d1f8dce --- /dev/null +++ b/src/layout/Header/Dropdown.module.scss @@ -0,0 +1,50 @@ +.container { + position: absolute; + display: flex; + top: 80px; + right: 0; + width: 100%; + background-color: #f5f5f5; + box-shadow: 0 5px 7px rgb(0 0 0 / 10%); + z-index: 10; + flex-direction: column; +} + +.setting-item__link { + text-decoration: none; + color: #333; + padding: 10px; + text-align: left; + width: calc(100% - 20px); + + &:hover { + background-color: #f0f0f0; + } +} + +.setting-item__button { + padding: 10px; + text-align: left; + width: 100%; + background: none; + border: none; + cursor: pointer; + + &:hover { + background-color: #f0f0f0; + } +} + +.setting-item__word { + font-size: 16px; + height: 30px; + color: #1f1f1f; + padding-left: 20px; +} + +.setting-item__warning { + font-size: 16px; + height: 30px; + color: #ec2d30; + padding-left: 20px; +} diff --git a/src/layout/Header/Header.module.scss b/src/layout/Header/Header.module.scss index 8be936a..4259fb8 100644 --- a/src/layout/Header/Header.module.scss +++ b/src/layout/Header/Header.module.scss @@ -19,8 +19,27 @@ } } +.logo__icon { + margin-left: 24px; +} + .header__content { display: flex; gap: 8px; align-items: center; } + +.menu-container { + display: flex; + gap: 8px; + margin-right: 24px; +} + +.menu-icon { + background: none; +} + +.search, +.menu { + cursor: pointer; +} diff --git a/src/pages/Coop/components/Setting/Setting.module.scss b/src/layout/Header/HeaderNavigation/HeaderNavigation.module.scss similarity index 91% rename from src/pages/Coop/components/Setting/Setting.module.scss rename to src/layout/Header/HeaderNavigation/HeaderNavigation.module.scss index 5ae1f76..f15308b 100644 --- a/src/pages/Coop/components/Setting/Setting.module.scss +++ b/src/layout/Header/HeaderNavigation/HeaderNavigation.module.scss @@ -2,7 +2,7 @@ display: flex; align-items: center; gap: 40px; - padding-right: 24px; + margin-right: 24px; } .setting-item__link, diff --git a/src/pages/Coop/components/Setting/index.tsx b/src/layout/Header/HeaderNavigation/index.tsx similarity index 84% rename from src/pages/Coop/components/Setting/index.tsx rename to src/layout/Header/HeaderNavigation/index.tsx index d2895e3..f643d7c 100644 --- a/src/pages/Coop/components/Setting/index.tsx +++ b/src/layout/Header/HeaderNavigation/index.tsx @@ -2,9 +2,9 @@ import { Link } from 'react-router-dom'; import { useLogout } from 'query/auth'; -import styles from './Setting.module.scss'; +import styles from './HeaderNavigation.module.scss'; -function Setting1() { +function HeaderNavigation() { const { logout } = useLogout(); const handleLogout = () => { @@ -23,4 +23,4 @@ function Setting1() { ); } -export default Setting1; +export default HeaderNavigation; diff --git a/src/layout/Header/MobileDropdown.tsx b/src/layout/Header/MobileDropdown.tsx new file mode 100644 index 0000000..cb5c899 --- /dev/null +++ b/src/layout/Header/MobileDropdown.tsx @@ -0,0 +1,34 @@ +import { Link } from 'react-router-dom'; + +import { useLogout } from 'query/auth'; + +import styles from './Dropdown.module.scss'; + +interface MobileDropdownProps { + view: boolean; +} + +function MobileDropdown({ view }: MobileDropdownProps) { + const { logout } = useLogout(); + + const handleLogout = () => { + logout(); + }; + + return ( +
+ {view && ( +
+ +
BCSD Lab에 문의하기
+ + +
+ )} +
+ ); +} + +export default MobileDropdown; diff --git a/src/layout/Header/index.tsx b/src/layout/Header/index.tsx index c7fecad..70d6234 100644 --- a/src/layout/Header/index.tsx +++ b/src/layout/Header/index.tsx @@ -1,16 +1,45 @@ +import { Link } from 'react-router-dom'; + +import Menu from 'assets/svg/auth/menu.svg?react'; import Logo from 'assets/svg/common/koin-logo.svg?react'; -import Setting1 from 'pages/Coop/components/Setting'; +import useBooleanState from 'hooks/useBooleanState'; +import useMediaQuery from 'hooks/useMediaQuery'; import styles from './Header.module.scss'; +import HeaderNavigation from './HeaderNavigation'; +import MobileDropdown from './MobileDropdown'; function Header() { + const { isMobile } = useMediaQuery(); + const [isOpen,,,, toggleIsOpen] = useBooleanState(false); + return ( -
- +
+
); diff --git a/src/pages/Coop/Coop.module.scss b/src/pages/Coop/Coop.module.scss index a94fe8a..c8e2625 100644 --- a/src/pages/Coop/Coop.module.scss +++ b/src/pages/Coop/Coop.module.scss @@ -2,13 +2,15 @@ .container { display: flex; + justify-content: center; max-width: 989px; width: 100%; margin: 0 auto; + gap: 10px; @include media.media-breakpoint-down(mobile) { flex-direction: column; - max-width: 480px; // 모바일 뷰에서는 너비 제한 + max-width: 100%; width: 100%; } } @@ -20,17 +22,35 @@ .content { display: flex; - flex-grow: 2; // 2배 공간 flex-direction: column; padding: 16px 24px; gap: 12px; + width: 100%; + + @include media.media-breakpoint-down(mobile) { + box-sizing: border-box; + } } .calendarWrapper { - flex-grow: 1; // 1:2 비율에서 1배의 공간 - max-width: 989px; + display: flex; + gap: 10px; + justify-content: center; + + @include media.media-breakpoint-down(mobile) { + flex-direction: column; + max-width: 480px; + width: 100%; + } } .diningTypeWrapper { - flex-grow: 2; // 1:2 비율에서 2배 공간 + @include media.media-breakpoint-down(mobile) { + display: flex; + justify-content: center; + flex-direction: column; + margin: 0 auto; + max-width: 100%; + width: 100%; + } } diff --git a/src/pages/Coop/components/Calendar/Calendar.module.scss b/src/pages/Coop/components/Calendar/Calendar.module.scss index 94fe6e8..32ef345 100644 --- a/src/pages/Coop/components/Calendar/Calendar.module.scss +++ b/src/pages/Coop/components/Calendar/Calendar.module.scss @@ -1,3 +1,5 @@ +@use "src/assets/styles/mediaQuery" as media; + .container { display: flex; width: 100%; @@ -15,7 +17,7 @@ font-size: 18px; font-style: normal; font-weight: 400; - line-height: 160%; /* 22.4px */ + line-height: 160%; } .title--sub { @@ -24,10 +26,15 @@ font-size: 32px; font-style: normal; font-weight: 700; - line-height: 160%; /* 32px */ + line-height: 160%; margin-bottom: 20px; } +.move-wrapper { + display: flex; + justify-content: space-between; +} + .button-container { display: flex; align-items: center; @@ -47,7 +54,7 @@ font-size: 14px; font-style: normal; font-weight: 400; - line-height: 160%; /* 22.4px */ + line-height: 160%; &--selected { color: #4b4b4b; @@ -79,7 +86,7 @@ font-size: 13px; font-style: normal; font-weight: 400; - line-height: 160%; /* 20.8px */ + line-height: 160%; } .date-list { @@ -101,7 +108,7 @@ font-size: 16px; font-style: normal; font-weight: 500; - line-height: 160%; /* 25.6px */ + line-height: 160%; background: none; border: none; @@ -125,3 +132,62 @@ color: #fff; } } + +.title-wrapper-mobile { + width: 100%; + margin: 8px 24px; + + @include media.media-breakpoint-down(mobile) { + margin: 0; + } +} + +.move-wrapper-mobile { + display: flex; + justify-content: space-between; + width: calc(100% - 48px); +} + +.button-container-mobile { + display: flex; + align-items: center; + justify-content: flex-end; + font-size: 14px; +} + +.title--main-mobile { + color: #727272; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 160%; +} + +.title--sub-mobile { + color: #000; + width: 273px; + font-size: 20px; + font-style: normal; + font-weight: 700; + line-height: 160%; + margin-bottom: 20px; +} + +.form-toggle-button-mobile { + display: flex; + padding: 6px 14px; + gap: 10px; + border-radius: 12px; + background: none; + color: #727272; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 160%; + white-space: nowrap; + + &--selected { + color: #4b4b4b; + background: #eee; + } +} diff --git a/src/pages/Coop/components/Calendar/hooks/useCalendar.ts b/src/pages/Coop/components/Calendar/hooks/useCalendar.ts index d0ffa86..96032d1 100644 --- a/src/pages/Coop/components/Calendar/hooks/useCalendar.ts +++ b/src/pages/Coop/components/Calendar/hooks/useCalendar.ts @@ -59,7 +59,13 @@ function useCalendar() { return setMonth((p) => p + 1); }; + const goToday = () => { + setYear(today.getFullYear()); + setMonth(today.getMonth() + 1); + }; + return { + goToday, today, year, month, diff --git a/src/pages/Coop/components/Calendar/index.tsx b/src/pages/Coop/components/Calendar/index.tsx index b258970..249ec36 100644 --- a/src/pages/Coop/components/Calendar/index.tsx +++ b/src/pages/Coop/components/Calendar/index.tsx @@ -1,7 +1,10 @@ import { useState } from 'react'; +import useMediaQuery from 'hooks/useMediaQuery'; import cn from 'utils/className'; +import DateMover from '../DateMover'; + import styles from './Calendar.module.scss'; import useCalendar from './hooks/useCalendar'; @@ -20,8 +23,15 @@ interface CalendarProps { } export default function Calendar({ selectedDate, setSelectedDate }: CalendarProps) { - const { dateList, isToday } = useCalendar(); + const { + dateList, + isToday, + prevMonth: prevCalendarMonth, + nextMonth: nextCalendarMonth, + goToday, + } = useCalendar(); const [dateListFormState, setdateListFormState] = useState<'week' | 'month'>('week'); + const { isMobile } = useMediaQuery(); const getDateList = (form: 'week' | 'month') => { if (form === 'week') { @@ -38,61 +48,198 @@ export default function Calendar({ selectedDate, setSelectedDate }: CalendarProp return []; }; + const prevMonth = () => { + const prevMonthDate = new Date(selectedDate); + prevMonthDate.setMonth(selectedDate.getMonth() - 1); + prevCalendarMonth(); + setSelectedDate(prevMonthDate); + }; + + const nextMonth = () => { + const nextMonthDate = new Date(selectedDate); + nextMonthDate.setMonth(selectedDate.getMonth() + 1); + nextCalendarMonth(); + setSelectedDate(nextMonthDate); + }; + + const prevWeek = () => { + const prevWeekDate = new Date(selectedDate); + prevWeekDate.setDate(selectedDate.getDate() - WEEK); + + const todayDateIndex = dateList.findIndex((date) => isSameDate(selectedDate, date)); + const rowIndex = Math.floor(todayDateIndex / WEEK); + + if (rowIndex === 0) { + prevCalendarMonth(); + } + + setSelectedDate(prevWeekDate); + }; + + const nextWeek = () => { + const prevWeekDate = new Date(selectedDate); + prevWeekDate.setDate(selectedDate.getDate() + WEEK); + + const todayDateIndex = dateList.findIndex((date) => isSameDate(selectedDate, date)); + const rowIndex = Math.floor(todayDateIndex / WEEK); + + if (rowIndex === 5) { + nextCalendarMonth(); + } + setSelectedDate(prevWeekDate); + }; + + const handleTodayClick = () => { + const today = new Date(); + goToday(); + setSelectedDate(today); + }; + + const getSelectedDate = () => `${selectedDate.getMonth() + 1}월 ${selectedDate.getDate()}일 ${DAYS[selectedDate.getDay()]}요일`; + return ( -
-
-
월간 식단
-
- {`${selectedDate.getMonth() + 1}월 ${selectedDate.getDate()}일 ${DAYS[selectedDate.getDay()]}요일`} -
-
- - -
-
-
-
- {DAYS.map((day) => ( -
{day}
- ))} +
+ {isMobile ? ( + // 모바일 뷰 +
+ +
+ +
+
+ {dateListFormState === 'month' ? '월간 식단' : '주간 식단'} +
+
+ {getSelectedDate()} +
+
+ +
+ + +
+
+ + + +
+
+ {DAYS.map((day) => ( +
{day}
+ ))} +
+
+ {getDateList(dateListFormState).map((date) => ( + + ))} +
+
-
- {getDateList(dateListFormState).map((date) => ( - - ))} + ) : ( + // 데스크탑 뷰 +
+
+
+ {dateListFormState === 'month' ? '월간 식단' : '주간 식단'} +
+
+ {getSelectedDate()} +
+
+ +
+ + +
+
+ +
+
+
+ {DAYS.map((day) => ( +
{day}
+ ))} +
+
+ {getDateList(dateListFormState).map((date) => ( + + ))} +
+
-
+ + )}
+ ); } diff --git a/src/pages/Coop/components/DateMover/DateMover.module.scss b/src/pages/Coop/components/DateMover/DateMover.module.scss new file mode 100644 index 0000000..311d017 --- /dev/null +++ b/src/pages/Coop/components/DateMover/DateMover.module.scss @@ -0,0 +1,35 @@ +.button-container { + display: flex; + gap: 16px; + align-items: center; + + & > button { + background: none; + } +} + +.button-prev, +.button-next { + &:hover { + background-color: #e1e1e1; + border-radius: 999px; + cursor: pointer; + } +} + +.button-today { + min-width: 48px; + text-align: center; + cursor: default; + color: #000; + font-family: Pretendard, sans-serif; + font-size: 18px; + font-weight: 500; + line-height: 160%; /* 28.8px */ + + &:hover { + background-color: #e1e1e1; + border-radius: 999px; + cursor: pointer; + } +} diff --git a/src/pages/Coop/components/DateMover/index.tsx b/src/pages/Coop/components/DateMover/index.tsx new file mode 100644 index 0000000..4137f0b --- /dev/null +++ b/src/pages/Coop/components/DateMover/index.tsx @@ -0,0 +1,35 @@ +import ArrowLeftIcon from 'assets/svg/common/arrow-left.svg?react'; +import ArrowRightIcon from 'assets/svg/common/arrow-right.svg?react'; + +import styles from './DateMover.module.scss'; + +interface DateMoverProps { + onPrevClick: () => void; + onNextClick: () => void; + onTodayClick: () => void; +} + +export default function DateMover({ onPrevClick, onNextClick, onTodayClick }: DateMoverProps) { + return ( +
+ + + + + + + +
+ ); +} diff --git a/src/pages/Coop/components/DiningBlocks/DiningBlocks.module.scss b/src/pages/Coop/components/DiningBlocks/DiningBlocks.module.scss index ff7c419..56ebe55 100644 --- a/src/pages/Coop/components/DiningBlocks/DiningBlocks.module.scss +++ b/src/pages/Coop/components/DiningBlocks/DiningBlocks.module.scss @@ -176,3 +176,8 @@ transform: rotate(180deg); } } + +.diet-image { + width: 100%; + -webkit-user-drag: none; +} diff --git a/src/pages/Coop/components/DiningBlocks/index.tsx b/src/pages/Coop/components/DiningBlocks/index.tsx index 46b3e3c..bd43376 100644 --- a/src/pages/Coop/components/DiningBlocks/index.tsx +++ b/src/pages/Coop/components/DiningBlocks/index.tsx @@ -107,7 +107,7 @@ export default function DiningBlocks({ diningType, date }: DiningBlocksProps) { className={styles['image-trigger']} onClick={() => fileInputRefs.current[dining.id]?.click()} > - {dining.image_url && 식단} + {dining.image_url && 식단} {!dining.image_url && (
diff --git a/src/pages/Coop/components/DiningDownload/DiningDownload.module.scss b/src/pages/Coop/components/DiningDownload/DiningDownload.module.scss index 9b7807a..053ae9d 100644 --- a/src/pages/Coop/components/DiningDownload/DiningDownload.module.scss +++ b/src/pages/Coop/components/DiningDownload/DiningDownload.module.scss @@ -1,25 +1,16 @@ .button-container { display: flex; background: #fff; - max-width: 173px; - width: 100%; padding: 8px 12px; gap: 8px; border-radius: 8px; - height: 26px; -} - -.dining-download-button { - display: flex; - background: #fff; - border: none; - color: #171717; font-size: 14px; font-weight: 400; - width: 119px; - height: 26px; - text-align: center; - padding: 2px 0 0 4px; + align-items: center; + + &:hover { + cursor: pointer; + } } .exel-icon { diff --git a/src/pages/Coop/components/DiningDownload/index.tsx b/src/pages/Coop/components/DiningDownload/index.tsx index 4b60f85..1e7ff7d 100644 --- a/src/pages/Coop/components/DiningDownload/index.tsx +++ b/src/pages/Coop/components/DiningDownload/index.tsx @@ -18,15 +18,17 @@ export default function DiningDownload() { }; return ( -
-
+ <> + + 식단 파일 다운로드 -
- + {isModalOpen && } - -
+ ); } diff --git a/src/pages/Coop/components/DiningTypeSelect/MobileDiningTypeSelect/MobileDiningTypeSelect.module.scss b/src/pages/Coop/components/DiningTypeSelect/MobileDiningTypeSelect/MobileDiningTypeSelect.module.scss new file mode 100644 index 0000000..ec80a79 --- /dev/null +++ b/src/pages/Coop/components/DiningTypeSelect/MobileDiningTypeSelect/MobileDiningTypeSelect.module.scss @@ -0,0 +1,73 @@ +.container { + display: flex; + align-items: center; + justify-content: space-between; + margin-left: 16px; +} + +.type-title { + color: #10477a; + font-size: 18px; + font-style: normal; + font-weight: 700; + line-height: 160%; /* 28.8px */ +} + +.dropdown { + position: relative; + + &__trigger { + display: flex; + align-items: center; + justify-content: center; + padding: 6px 16px; + border-radius: 8px; + background: none; + color: #000; + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: 160%; /* 25.6px */ + + &:active { + background: #eee; + } + } +} + +.arrow-icon { + transition: all 0.4s ease; + + &__transform { + transform: rotate(-180deg); + } +} + +.dropdown-list { + z-index: 5; + position: absolute; + bottom: -122px; + left: 0; + display: flex; + flex-direction: column; + width: 84px; + border-radius: 8px; + background: #fff; + box-shadow: 0 2px 20px 0 rgba(0 0 0 / 4%), 0 8px 32px 0 rgba(0 0 0 / 8%); +} + +.dropdown-item { + display: flex; + align-items: center; + color: #000; + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: 160%; /* 25.6px */ + padding: 6px 16px; + background: none; + + &--selected { + background: #f5f5f5; + } +} diff --git a/src/pages/Coop/components/DiningTypeSelect/MobileDiningTypeSelect/index.tsx b/src/pages/Coop/components/DiningTypeSelect/MobileDiningTypeSelect/index.tsx new file mode 100644 index 0000000..1c73249 --- /dev/null +++ b/src/pages/Coop/components/DiningTypeSelect/MobileDiningTypeSelect/index.tsx @@ -0,0 +1,59 @@ +import { useRef, useState } from 'react'; + +import ArrowDown from 'assets/svg/common/arrow-down.svg?react'; +import useOnClickOutside from 'hooks/useOnclickOutside'; +import { DiningType, DINING_TYPES, DINING_TYPE_MAP } from 'models/dinings'; +import cn from 'utils/className'; + +import styles from './MobileDiningTypeSelect.module.scss'; + +interface MobileDiningTypeSelectProps { + selectedDiningType: DiningType; + setSelectedDiningType: (diningType: DiningType) => void; +} + +export default function MobileDiningTypeSelect({ + selectedDiningType, setSelectedDiningType, +}: MobileDiningTypeSelectProps) { + const [typeOpen, setTypeOpen] = useState(false); + const ref = useRef(null); + + useOnClickOutside(ref, () => setTypeOpen(false)); + + return ( +
+
{`${DINING_TYPE_MAP[selectedDiningType]} 식단`}
+
+ + {typeOpen && ( +
+ {DINING_TYPES.map((type: DiningType) => ( + + ))} +
+ )} +
+
+ ); +} diff --git a/src/pages/Coop/components/DownloadModal/DownloadModal.module.scss b/src/pages/Coop/components/DownloadModal/DownloadModal.module.scss index 4601580..eae2edd 100644 --- a/src/pages/Coop/components/DownloadModal/DownloadModal.module.scss +++ b/src/pages/Coop/components/DownloadModal/DownloadModal.module.scss @@ -8,6 +8,7 @@ width: 100vw; height: 100vh; background-color: #00000066; + z-index: 1000; } .date-container { @@ -18,7 +19,6 @@ display: flex; flex-direction: column; width: 485px; - height: 345px; padding: 24px 32px; box-sizing: border-box; background-color: #fff; @@ -81,27 +81,36 @@ .toggle-container { display: flex; gap: 24px; - margin: 7px 0 30px; + justify-content: flex-end; + align-items: center; position: relative; - left: 162px; + margin: 8px 0; + + & > label { + display: flex; + align-items: center; + justify-content: center; + } } -.button-wrapper { +.button-container { display: flex; - justify-content: center; + justify-content: flex-end; align-items: center; - margin: 0 auto; - max-width: 160px; - width: 100%; - gap: 8px; + margin: 24px auto 0; + padding: 8px 36px; + color: #fff; background-color: #175c8e; border-radius: 4px; + gap: 8px; } -.buttons-confirm { - height: 42px; - border: none; - background-color: #175c8e; - color: #fff; - margin-left: 10px; +.button-title { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 24px; + font-size: 16px; + font-weight: 600; } diff --git a/src/pages/Coop/components/DownloadModal/index.tsx b/src/pages/Coop/components/DownloadModal/index.tsx index b722139..df3f0d1 100644 --- a/src/pages/Coop/components/DownloadModal/index.tsx +++ b/src/pages/Coop/components/DownloadModal/index.tsx @@ -1,7 +1,9 @@ -import { useState, useEffect, useCallback } from 'react'; +import { useState, useCallback } from 'react'; import { getExcel } from 'api/dinings'; import DownloadIcon from 'assets/svg/common/download-white.svg?react'; +import LoadingSpinner from 'assets/svg/common/loading.svg?react'; +import useBooleanState from 'hooks/useBooleanState'; import { toast, ToastContainer } from 'react-toastify'; import 'react-toastify/dist/ReactToastify.css'; @@ -15,15 +17,16 @@ interface DownloadModalProps { } interface DateInput { - year: string; - month: string; - day: string; + year: number | ''; + month: number | ''; + day: number | ''; } export default function DownloadModal({ closeModal }: DownloadModalProps) { const [startDate, setStartDate] = useState({ year: '', month: '', day: '' }); const [endDate, setEndDate] = useState({ year: '', month: '', day: '' }); const [isStudentCafeteriaOnly, setIsStudentCafeteriaOnly] = useState(false); + const [isDownloading, setIsDownloading] = useBooleanState(false); const handleDateChange = ( setDate: React.Dispatch>, @@ -32,7 +35,7 @@ export default function DownloadModal({ closeModal }: DownloadModalProps) { ) => { setDate((prev) => ({ ...prev, - [field]: e.target.value, + [field]: e.target.valueAsNumber || '', })); }; @@ -42,25 +45,11 @@ export default function DownloadModal({ closeModal }: DownloadModalProps) { } }, [closeModal]); - const handleKeyDown = useCallback((e: KeyboardEvent) => { - if (e.key === 'Escape') { - closeModal(); - } - }, [closeModal]); - - useEffect(() => { - window.addEventListener('keydown', handleKeyDown); - - return () => { - window.removeEventListener('keydown', handleKeyDown); - }; - }, [handleKeyDown]); - const formatDate = (date: DateInput) => { if (!date.year || !date.month || !date.day) { return ''; } - return `${date.year}-${date.month.padStart(2, '0')}-${date.day.padStart(2, '0')}`; + return `${date.year.toString()}-${date.month.toString().padStart(2, '0')}-${date.day.toString().padStart(2, '0')}`; }; const showToast = (message: string) => { @@ -79,11 +68,6 @@ export default function DownloadModal({ closeModal }: DownloadModalProps) { const formattedStartDate = formatDate(startDate); const formattedEndDate = formatDate(endDate); - if (!formattedStartDate || !formattedEndDate) { - showToast('날짜를 모두 입력해 주세요.'); - return; - } - const requestBody = { startDate: formattedStartDate, endDate: formattedEndDate, @@ -91,12 +75,14 @@ export default function DownloadModal({ closeModal }: DownloadModalProps) { }; try { + setIsDownloading(true); const response = await getExcel(requestBody); let filename = `dining_${requestBody.startDate}~${requestBody.endDate}.xlsx`; if (response && response.headers) { const contentDisposition = response.headers['content-disposition']; + if (contentDisposition) { const match = contentDisposition.match(/filename="?(.+)"?/); if (match) { @@ -107,6 +93,7 @@ export default function DownloadModal({ closeModal }: DownloadModalProps) { } const downloadUrl = URL.createObjectURL(response.data); + const link = document.createElement('a'); link.href = downloadUrl; link.download = filename; @@ -114,8 +101,16 @@ export default function DownloadModal({ closeModal }: DownloadModalProps) { link.click(); document.body.removeChild(link); URL.revokeObjectURL(downloadUrl); + setIsDownloading(false); } catch (error) { - showToast('파일 다운로드에 실패했습니다. 다시 시도해 주세요.'); + // backend 수정 시 반영 + // if (error?.response?.data instanceof Blob) { + // const errorText = await error.response.data.text(); + // const errorJson = JSON.parse(errorText); + // showToast(errorJson.message); + // } + + showToast('다운로드에 실패했습니다.'); } }; @@ -123,14 +118,13 @@ export default function DownloadModal({ closeModal }: DownloadModalProps) {
식단 파일 다운로드
-
다운로드 할 식단의 기간을 설정해 주세요
- +
식단은 2022/11/29 부터 다운받을 수 있어요.
시작일
handleDateChange(setStartDate, 'year', e)} className={styles['date-input-box']} @@ -138,7 +132,7 @@ export default function DownloadModal({ closeModal }: DownloadModalProps) { /> / handleDateChange(setStartDate, 'month', e)} className={styles['date-input-box']} @@ -146,7 +140,7 @@ export default function DownloadModal({ closeModal }: DownloadModalProps) { /> / handleDateChange(setStartDate, 'day', e)} className={styles['date-input-box']} @@ -159,7 +153,7 @@ export default function DownloadModal({ closeModal }: DownloadModalProps) {
종료일
handleDateChange(setEndDate, 'year', e)} className={styles['date-input-box']} @@ -167,7 +161,7 @@ export default function DownloadModal({ closeModal }: DownloadModalProps) { /> / handleDateChange(setEndDate, 'month', e)} className={styles['date-input-box']} @@ -175,7 +169,7 @@ export default function DownloadModal({ closeModal }: DownloadModalProps) { /> / handleDateChange(setEndDate, 'day', e)} className={styles['date-input-box']} @@ -183,11 +177,9 @@ export default function DownloadModal({ closeModal }: DownloadModalProps) { />
-
+
+ -
- -
-
- +
+
diff --git a/src/pages/Coop/index.tsx b/src/pages/Coop/index.tsx index bea53b0..db32bc4 100644 --- a/src/pages/Coop/index.tsx +++ b/src/pages/Coop/index.tsx @@ -1,5 +1,6 @@ import { Suspense, useState } from 'react'; +import useMediaQuery from 'hooks/useMediaQuery'; import ErrorBoundary from 'layout/ErrorBoundary'; import Calendar from 'pages/Coop/components/Calendar'; import DiningBlocks from 'pages/Coop/components/DiningBlocks'; @@ -7,6 +8,7 @@ import DiningDownload from 'pages/Coop/components/DiningDownload'; import DiningTypeSelect from 'pages/Coop/components/DiningTypeSelect'; import { getDiningTypeOnTime } from 'utils/operate'; +import MobileDiningTypeSelect from './components/DiningTypeSelect/MobileDiningTypeSelect'; import styles from './Coop.module.scss'; import type { DiningType } from 'models/dinings'; @@ -14,6 +16,7 @@ import type { DiningType } from 'models/dinings'; export default function Coop() { const [date, setDate] = useState(new Date()); const [diningType, setDiningType] = useState(getDiningTypeOnTime()); + const { isMobile } = useMediaQuery(); return (
@@ -21,15 +24,24 @@ export default function Coop() {
-
- -
-
- + +
+ )} + {isMobile ? ( + -
+ ) : ( +
+ +
+ )} }> diff --git a/yarn.lock b/yarn.lock index ecdec86..33fc0fe 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2072,6 +2072,32 @@ __metadata: languageName: node linkType: hard +"@floating-ui/core@npm:^1.6.0": + version: 1.6.8 + resolution: "@floating-ui/core@npm:1.6.8" + dependencies: + "@floating-ui/utils": ^0.2.8 + checksum: 82faa6ea9d57e466779324e51308d6d49c098fb9d184a08d9bb7f4fad83f08cc070fc491f8d56f0cad44a16215fb43f9f829524288413e6c33afcb17303698de + languageName: node + linkType: hard + +"@floating-ui/dom@npm:^1.6.1": + version: 1.6.11 + resolution: "@floating-ui/dom@npm:1.6.11" + dependencies: + "@floating-ui/core": ^1.6.0 + "@floating-ui/utils": ^0.2.8 + checksum: d6413759abd06a541edfad829c45313f930310fe76a3322e74a00eb655e283db33fe3e65b5265c4072eb54db7447e11225acd355a9a02cabd1d1b0d5fc8fc21d + languageName: node + linkType: hard + +"@floating-ui/utils@npm:^0.2.8": + version: 0.2.8 + resolution: "@floating-ui/utils@npm:0.2.8" + checksum: deb98bba017c4e073c7ad5740d4dec33a4d3e0942d412e677ac0504f3dade15a68fc6fd164d43c93c0bb0bcc5dc5015c1f4080dfb1a6161140fe660624f7c875 + languageName: node + linkType: hard + "@hookform/resolvers@npm:^3.1.0": version: 3.4.2 resolution: "@hookform/resolvers@npm:3.4.2" @@ -5089,6 +5115,13 @@ __metadata: languageName: node linkType: hard +"classnames@npm:^2.3.0": + version: 2.5.1 + resolution: "classnames@npm:2.5.1" + checksum: da424a8a6f3a96a2e87d01a432ba19315503294ac7e025f9fece656db6b6a0f7b5003bb1fbb51cbb0d9624d964f1b9bb35a51c73af9b2434c7b292c42231c1e5 + languageName: node + linkType: hard + "clean-css@npm:^5.2.2": version: 5.3.3 resolution: "clean-css@npm:5.3.3" @@ -9801,6 +9834,7 @@ __metadata: react-router-dom: ^6.9.0 react-scripts: 5.0.1 react-toastify: ^10.0.4 + react-tooltip: ^5.28.0 sass: ^1.60.0 stylelint: ^14.9.1 stylelint-config-standard: ^26.0.0 @@ -12461,6 +12495,19 @@ __metadata: languageName: node linkType: hard +"react-tooltip@npm:^5.28.0": + version: 5.28.0 + resolution: "react-tooltip@npm:5.28.0" + dependencies: + "@floating-ui/dom": ^1.6.1 + classnames: ^2.3.0 + peerDependencies: + react: ">=16.14.0" + react-dom: ">=16.14.0" + checksum: 4d1efae0fbd39ec7f1414bb325582cca2f7541a8d464b61bf3a7c5e119821868aa2d47c28509e7ea3cca844a9655feb56fc466def0edaa8db07ce427146ef3b0 + languageName: node + linkType: hard + "react@npm:^18.2.0": version: 18.3.1 resolution: "react@npm:18.3.1"