├── .eslintrc.cjs ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── issue_template.md ├── pull_request_template.md └── workflows │ └── pr-build-check.yml ├── .gitignore ├── .prettierrc ├── README.md ├── index.html ├── package.json ├── pnpm-lock.yaml ├── postcss.config.js ├── public ├── logo_icon.svg ├── mockServiceWorker.js └── morib_miri.png ├── src ├── App.tsx ├── index.css ├── main.tsx ├── mocks │ ├── browser.ts │ ├── common │ │ ├── common.resolvers.ts │ │ └── common.responses.ts │ ├── handlers.ts │ ├── home │ │ ├── home.resolvers.ts │ │ └── home.responses.ts │ └── onboarding │ │ ├── onboarding.resolvers.ts │ │ └── onboarding.responses.ts ├── pages │ ├── AllowedServicePage │ │ ├── AllowedServiceGroupDetail │ │ │ ├── AllowedServiceGroupDetail.tsx │ │ │ ├── Content │ │ │ │ └── AllowedServiceGroupDetailContent.tsx │ │ │ ├── Header │ │ │ │ └── AllowedServiceGroupDetailHeader.tsx │ │ │ └── Tabs │ │ │ │ └── AllowedServiceGroupDetailTabs.tsx │ │ ├── AllowedServiceList │ │ │ └── AllowedServiceList.tsx │ │ ├── AllowedServicePage.tsx │ │ ├── ModalContentsAlert │ │ │ └── ModalContentsAlert.tsx │ │ └── RecommendService │ │ │ └── RecommendService.tsx │ ├── HomePage │ │ ├── BoxAddCategory │ │ │ └── BoxAddCategory.tsx │ │ ├── BoxCategory │ │ │ ├── BoxCategory.tsx │ │ │ ├── BoxTodoInput │ │ │ │ └── BoxTodoInput.tsx │ │ │ ├── StatusDefaultBoxCategory │ │ │ │ └── StatusDefaultBoxCategory.tsx │ │ │ └── hooks │ │ │ │ └── useCreateTodo.ts │ │ ├── BoxTodayTodo │ │ │ ├── BoxTodayTodo.tsx │ │ │ ├── StatusAddBoxTodayTodo │ │ │ │ ├── ButtonHomeSmall │ │ │ │ │ └── ButtonHomeSmall.tsx │ │ │ │ └── StatusAddBoxTodayTodo.tsx │ │ │ └── StatusDefaultBoxTodayTodo │ │ │ │ └── StatusDefaultBoxTodayTodo.tsx │ │ ├── ButtonMoreFriends │ │ │ └── ButtonMoreFriends.tsx │ │ ├── ButtonUserProfile │ │ │ └── ButtonUserProfile.tsx │ │ ├── DatePicker │ │ │ ├── ButtonDate │ │ │ │ └── ButtonDate.tsx │ │ │ ├── DatePicker.tsx │ │ │ └── hooks │ │ │ │ └── useDatePicker.ts │ │ ├── HomePage.tsx │ │ ├── ModalContentsAlert │ │ │ ├── ButtonAlert │ │ │ │ └── ButtonAlert.tsx │ │ │ ├── Complete │ │ │ │ └── Complete.tsx │ │ │ ├── DeleteAccount │ │ │ │ └── DeleteAccount.tsx │ │ │ ├── Logoout │ │ │ │ └── Logout.tsx │ │ │ ├── ModalContentsAlert.tsx │ │ │ ├── RegisterAllowedService │ │ │ │ └── RegisterAllowedService.tsx │ │ │ ├── TimerRestriction │ │ │ │ └── TimerRestriction.tsx │ │ │ └── types │ │ │ │ └── index.ts │ │ ├── StatusDefaultHome │ │ │ └── StatusDefaultHome.tsx │ │ ├── TooltipFriendInfo │ │ │ └── TooltipFriendInfo.tsx │ │ └── hooks │ │ │ └── useCalendar.ts │ ├── LoginPage │ │ ├── LoginPage.tsx │ │ └── hooks │ │ │ └── useLottieAnimation.ts │ ├── NotFoundPage │ │ └── NotFoundPage.tsx │ ├── OnboardingPage │ │ ├── ButtonSkip │ │ │ └── ButtonSkip.tsx │ │ ├── OnboardingPage.tsx │ │ ├── StepField │ │ │ └── StepField.tsx │ │ ├── StepService │ │ │ ├── AllowedServices │ │ │ │ └── AllowedServices.tsx │ │ │ ├── ButtonService │ │ │ │ └── ButtonService.tsx │ │ │ ├── StepService.tsx │ │ │ └── Tabs │ │ │ │ └── Tabs.tsx │ │ ├── StepStart │ │ │ └── StepStart.tsx │ │ ├── hooks │ │ │ └── useFunnel.tsx │ │ └── utils │ │ │ └── serviceUrl.ts │ ├── RedirectPage │ │ └── RedirectPage.tsx │ └── TimerPage │ │ ├── AllowedServices │ │ ├── AllowedServicesItem.tsx │ │ ├── AllowedServicesPopover.tsx │ │ ├── AllowedServicesTitle.tsx │ │ ├── AllowedServicesTooltip.tsx │ │ └── Checkbox.tsx │ │ ├── Carousel │ │ ├── Carousel.tsx │ │ └── CarouselFriend.tsx │ │ ├── MainTimer │ │ ├── MainTimer.tsx │ │ ├── TimerDisplay │ │ │ ├── ButtonTimerPlay │ │ │ │ └── ButtonTimerPlay.tsx │ │ │ ├── ProgressCircle │ │ │ │ └── ProgressCircle.tsx │ │ │ └── TimerDisplay.tsx │ │ └── TimerHeader │ │ │ └── TimerHeader.tsx │ │ ├── NavigationButtons │ │ └── NavigationButtons.tsx │ │ ├── SideMenuTimer │ │ └── SideMenuTimer.tsx │ │ ├── TimerPage.tsx │ │ ├── contexts │ │ └── TimerContext.tsx │ │ ├── hooks │ │ ├── useAllowedServices.ts │ │ ├── useTimerActions.ts │ │ ├── useTimerCount.ts │ │ ├── useTimerState.ts │ │ ├── useUIState.ts │ │ └── useUrlHandler.ts │ │ └── utils │ │ └── timeFormat.ts ├── router │ ├── ProtectedRoute.tsx │ ├── Router.tsx │ └── routesConfig.ts ├── shared │ ├── apisV2 │ │ ├── allowedService │ │ │ ├── allowedService.api.ts │ │ │ ├── allowedService.keys.ts │ │ │ ├── allowedService.mutations.ts │ │ │ └── allowedService.queries.ts │ │ ├── auth │ │ │ ├── auth.api.ts │ │ │ └── auth.queries.ts │ │ ├── client.ts │ │ ├── common │ │ │ ├── common.api.ts │ │ │ ├── common.keys.ts │ │ │ ├── common.mutations.ts │ │ │ └── common.queries.ts │ │ ├── friends │ │ │ ├── friends.api.ts │ │ │ ├── friends.keys.ts │ │ │ ├── friends.mutations.ts │ │ │ └── friends.queries.ts │ │ ├── home │ │ │ ├── home.api.ts │ │ │ ├── home.keys.ts │ │ │ ├── home.mutations.ts │ │ │ └── home.queries.ts │ │ ├── onboarding │ │ │ ├── onboarding.api.ts │ │ │ ├── onboarding.keys.ts │ │ │ ├── onboarding.mutations.ts │ │ │ └── onboarding.queries.ts │ │ ├── queryClient.ts │ │ ├── setting │ │ │ ├── setting.api.ts │ │ │ ├── setting.keys.ts │ │ │ ├── setting.mutations.ts │ │ │ └── setting.queries.ts │ │ └── timer │ │ │ ├── timer.api.ts │ │ │ ├── timer.keys.ts │ │ │ ├── timer.mutations.ts │ │ │ └── timer.queries.ts │ ├── assets │ │ ├── images │ │ │ ├── example.jpg │ │ │ ├── img_timer_bg.png │ │ │ ├── login_background.png │ │ │ ├── profile_image1.png │ │ │ ├── profile_image2.png │ │ │ ├── profile_image3.png │ │ │ └── profile_image4.png │ │ ├── lotties │ │ │ ├── loading.json │ │ │ └── morib_logo_motion.json │ │ └── svgs │ │ │ ├── 404.svg │ │ │ ├── add_btn.svg │ │ │ ├── arrow_circle_up_right.svg │ │ │ ├── arrow_right.svg │ │ │ ├── bell.svg │ │ │ ├── btn_add.svg │ │ │ ├── btn_arrow.svg │ │ │ ├── btn_arrow_bgNone.svg │ │ │ ├── btn_cal.svg │ │ │ ├── btn_cal_black.svg │ │ │ ├── btn_hamburger.svg │ │ │ ├── btn_home.svg │ │ │ ├── btn_inputClear.svg │ │ │ ├── btn_list.svg │ │ │ ├── btn_moribset_active.svg │ │ │ ├── btn_moribset_default.svg │ │ │ ├── btn_today.svg │ │ │ ├── button_inputSuccess.svg │ │ │ ├── check_box_blank.svg │ │ │ ├── check_box_fill.svg │ │ │ ├── common │ │ │ ├── ic_logo.svg │ │ │ └── ic_meatball_default.svg │ │ │ ├── connection_icon.svg │ │ │ ├── default_profile.svg │ │ │ ├── default_url_favicon.svg │ │ │ ├── defaultpause.svg │ │ │ ├── defaultplay.svg │ │ │ ├── description.svg │ │ │ ├── disabled_dropdown.svg │ │ │ ├── dropIcon.svg │ │ │ ├── elipse.svg │ │ │ ├── error.svg │ │ │ ├── error_input.svg │ │ │ ├── friend_delBtn.svg │ │ │ ├── friend_setting.svg │ │ │ ├── google_login.svg │ │ │ ├── gradient_circle.svg │ │ │ ├── header_delBtn.svg │ │ │ ├── home │ │ │ ├── ic_box.svg │ │ │ └── ic_plus.svg │ │ │ ├── home_default_icon.svg │ │ │ ├── hoverpause.svg │ │ │ ├── hoverplay.svg │ │ │ ├── ic_back_btn.svg │ │ │ ├── ic_delete_alert.svg │ │ │ ├── ic_description.svg │ │ │ ├── ic_folder.svg │ │ │ ├── ic_gear.svg │ │ │ ├── ic_line.svg │ │ │ ├── ic_logo.svg │ │ │ ├── ic_minus.svg │ │ │ ├── ic_pencil.svg │ │ │ ├── ic_service_design.svg │ │ │ ├── ic_service_design_sm.svg │ │ │ ├── icon_clock.svg │ │ │ ├── large_plus.svg │ │ │ ├── logo_icon.svg │ │ │ ├── mail.svg │ │ │ ├── mingcute_time-fill.svg │ │ │ ├── mingcute_time-line.svg │ │ │ ├── minus_btn.svg │ │ │ ├── more_friend.svg │ │ │ ├── moribSet.svg │ │ │ ├── onboarding │ │ │ ├── ic_business.svg │ │ │ ├── ic_design.svg │ │ │ ├── ic_development.svg │ │ │ ├── ic_marketing.svg │ │ │ ├── ic_planning.svg │ │ │ └── ic_studying.svg │ │ │ ├── onboarding_image.svg │ │ │ ├── plus.svg │ │ │ ├── popover_add_category.svg │ │ │ ├── popover_add_todo.svg │ │ │ ├── react.svg │ │ │ ├── selected_number_icon.svg │ │ │ ├── setting.svg │ │ │ ├── timer │ │ │ ├── ic_check_box_active.svg │ │ │ ├── ic_check_box_inactive.svg │ │ │ ├── ic_deactivated_clock.svg │ │ │ ├── ic_online.svg │ │ │ └── ic_timer_inner_circle.svg │ │ │ ├── todo_meatball_press.svg │ │ │ ├── todo_toggle.svg │ │ │ ├── tooltip_triangle.svg │ │ │ ├── triangle.svg │ │ │ ├── upIcon.svg │ │ │ └── user_circle.svg │ ├── components │ │ ├── AutoFixedGrid │ │ │ └── AutoFixedGrid.tsx │ │ ├── BoxTodo │ │ │ └── BoxTodo.tsx │ │ ├── ButtonArrowSVG │ │ │ └── ButtonArrowSVG.tsx │ │ ├── ButtonDropdownOptions │ │ │ └── ButtonDropdownOptions.tsx │ │ ├── ButtonHomeLarge │ │ │ └── ButtonHomeLarge.tsx │ │ ├── ButtonRadius5 │ │ │ └── ButtonRadius5.tsx │ │ ├── ButtonRadius8 │ │ │ └── ButtonRadius8.tsx │ │ ├── ButtonStatusToggle │ │ │ └── ButtonStatusToggle.tsx │ │ ├── ButtonTodayToggle │ │ │ └── ButtonTodoToggle.tsx │ │ ├── Calendar │ │ │ ├── ButtonCalendarAddRoutine │ │ │ │ └── ButtonCalendarAddRoutine.tsx │ │ │ ├── Calendar.tsx │ │ │ ├── HeaderCalendar │ │ │ │ └── HeaderCalendar.tsx │ │ │ └── calendar.css │ │ ├── CircleColorIcon │ │ │ └── CircleColorIcon.tsx │ │ ├── ColorPallete │ │ │ └── ColorPallete.tsx │ │ ├── Dropdown │ │ │ └── Dropdown.tsx │ │ ├── ErrorBoundary │ │ │ └── ErrorBoundary.tsx │ │ ├── FallbackApiError │ │ │ └── FallbackApiError.tsx │ │ ├── FaviconImage │ │ │ └── FaviconImage.tsx │ │ ├── HeartBeatBoundary │ │ │ └── HeartBeatBoundary.tsx │ │ ├── LoadingOverlay │ │ │ └── LoadingOverlay.tsx │ │ ├── ModalContentsFriends │ │ │ ├── FriendRequest │ │ │ │ ├── ButtonRequestAction │ │ │ │ │ └── ButtonRequestAction.tsx │ │ │ │ ├── FriendsListRequested │ │ │ │ │ └── FriendsListRequested.tsx │ │ │ │ └── FriendsRequest.tsx │ │ │ ├── FriendUserProfile │ │ │ │ └── FriendUserProfile.tsx │ │ │ ├── FriendsList │ │ │ │ ├── FriendsInfo │ │ │ │ │ └── FriendInfo.tsx │ │ │ │ └── FriendsList.tsx │ │ │ └── ModalContentsFriends.tsx │ │ ├── ModalWrapper │ │ │ ├── ModalWrapper.tsx │ │ │ └── styles │ │ │ │ └── dialog.css │ │ ├── NotificationPanel │ │ │ └── NotificationPanel.tsx │ │ ├── Portal │ │ │ └── Portal.tsx │ │ ├── Spacer │ │ │ └── Spacer.tsx │ │ └── TextField │ │ │ └── TextField.tsx │ ├── constants │ │ ├── btnText.ts │ │ ├── colorPalette.ts │ │ ├── emailRegex.ts │ │ ├── error.ts │ │ ├── fields.ts │ │ ├── timerPageText.ts │ │ └── weekDays.ts │ ├── hocs │ │ └── withAuthProtection.tsx │ ├── hooks │ │ ├── useCarousel.ts │ │ └── useClickOutside.ts │ ├── layout │ │ ├── Layout.tsx │ │ └── Sidebar │ │ │ ├── ModalContentsSetting │ │ │ ├── AccountContent │ │ │ │ └── AccountContent.tsx │ │ │ ├── ModalContentsSetting.tsx │ │ │ ├── Tabs │ │ │ │ └── Tabs.tsx │ │ │ └── WorkspaceSettingContent │ │ │ │ └── WorkspaceSettingContent.tsx │ │ │ └── Sidebar.tsx │ ├── mocks │ │ ├── categoryData.ts │ │ ├── faviconData.ts │ │ ├── homeData.ts │ │ ├── urlData.ts │ │ └── userData.ts │ ├── types │ │ ├── SSEEvent.ts │ │ ├── allowedService.ts │ │ ├── allowedSites.ts │ │ ├── api │ │ │ ├── allowedService.ts │ │ │ ├── auth.ts │ │ │ ├── common.ts │ │ │ ├── error.ts │ │ │ ├── friends.ts │ │ │ ├── home.ts │ │ │ ├── onboarding.ts │ │ │ ├── setting.ts │ │ │ └── timer.ts │ │ ├── common │ │ │ └── index.tsx │ │ ├── fileds.ts │ │ ├── friend.ts │ │ ├── global.ts │ │ ├── home │ │ │ └── index.tsx │ │ ├── profile.ts │ │ ├── tasks.ts │ │ ├── todoData.ts │ │ └── userData.ts │ └── utils │ │ ├── auth.ts │ │ ├── calendar │ │ └── index.ts │ │ ├── date │ │ └── index.ts │ │ ├── error.ts │ │ ├── path.ts │ │ ├── tasks.ts │ │ ├── time │ │ └── index.ts │ │ ├── timer │ │ └── index.ts │ │ ├── url.ts │ │ ├── url │ │ └── index.ts │ │ └── validation.ts └── vite-env.d.ts ├── tailwind.config.js ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.node.json ├── vercel.json └── vite.config.ts /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: 버그 리포트 이슈 템플릿 4 | title: "[ Fix ]" 5 | --- 6 | 7 | 8 | 9 | ## 💚 어떤 버그인가요? 10 | 11 | > 어떤 버그인지 간결하게 설명해주세요 12 | 13 | ## 어떤 상황에서 발생한 버그인가요? 14 | 15 | > (가능하면) Given-When-Then 형식으로 서술해주세요 16 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/issue_template.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: 기능 추가 이슈 템플릿 4 | title: "[ Feat ]" 5 | --- 6 | 7 | 8 | 9 | ## 💚 어떤 기능인가요? 10 | 11 | ## ✅ To Dos 12 | 13 | - [ ] 14 | - [ ] 15 | - [ ] -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## 🔥 Related Issues 4 | 5 | - close #issue_number 6 | 7 | ## ✅ 작업 리스트 8 | 9 | - [ ] 체크리스트 10 | - [ ] 체크리스트 11 | 12 | ## 🔧 작업 내용 13 | 14 | ## 🧐 새로 알게된 점 15 | 16 | ## 🤔 궁금한 점 17 | 18 | ## 📸 스크린샷 / GIF / Link 19 | -------------------------------------------------------------------------------- /.github/workflows/pr-build-check.yml: -------------------------------------------------------------------------------- 1 | name: Build PR 2 | 3 | on: 4 | pull_request: 5 | 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | node-version: [20] 12 | 13 | steps: 14 | - name: Checkout code 15 | uses: actions/checkout@v4 16 | 17 | - name: Install pnpm 18 | uses: pnpm/action-setup@v4 19 | 20 | - name: Use Node.js ${{ matrix.node-version }} 21 | uses: actions/setup-node@v4 22 | with: 23 | node-version: ${{ matrix.node-version }} 24 | cache: 'pnpm' 25 | 26 | - name: Install dependencies 27 | run: pnpm install 28 | 29 | - name: Clear TypeScript build info 30 | run: | 31 | echo "TypeScript 빌드 캐시 삭제" 32 | rm -rf node_modules/.tmp 33 | rm -rf node_modules/.cache 34 | 35 | - name: Check TypeScript compilation 36 | run: pnpm tsc -b 37 | 38 | - name: Build project 39 | run: pnpm run build 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | dist-electron 14 | dist-react 15 | release 16 | *.local 17 | 18 | # Editor directories and files 19 | .vscode/* 20 | !.vscode/extensions.json 21 | .idea 22 | .DS_Store 23 | *.suo 24 | *.ntvs* 25 | *.njsproj 26 | *.sln 27 | *.sw? 28 | 29 | .env -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "@trivago/prettier-plugin-sort-imports", 4 | "prettier-plugin-tailwindcss" 5 | ], 6 | "parser": "typescript", 7 | "printWidth": 120, 8 | "tabWidth": 2, 9 | "useTabs": true, 10 | "semi": true, 11 | "singleQuote": true, 12 | "quoteProps": "as-needed", 13 | "trailingComma": "all", 14 | "bracketSpacing": true, 15 | "arrowParens": "always", 16 | "endOfLine": "auto", 17 | "importOrder": [ 18 | "^react(.*)", 19 | "^@tanstack/(.*)$", 20 | "^axios(.*)", 21 | "^@/shared/pages/(.*)$", 22 | "^@/shared/components/(.*)$", 23 | "^@/shared/hooks/(.*)$", 24 | "^@/shared/apis/(.*)$", 25 | "^@/shared/utils/(.*)$", 26 | "^@/shared/router/(.*)$", 27 | "^@/shared/types/(.*)$", 28 | "^@/shared/constants/(.*)$", 29 | "^@/shared/assets/(.*)$", 30 | "^@/router/(.*)$", 31 | "^@/(.*)$", 32 | "^[./]" 33 | ], 34 | "importOrderSeparation": true, 35 | "importOrderSortSpecifiers": true, 36 | 37 | "overrides": [ 38 | { 39 | "files": "*.yml", 40 | "options": { 41 | "parser": "yaml" 42 | } 43 | }] 44 | } 45 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 모립 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 32 | 33 | 34 |
35 | 36 |
37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /public/logo_icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /public/morib_miri.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/morib-in/Morib-Client/e855b115b93b9f35a569fc7dab7f7daf41101cb6/public/morib_miri.png -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import { Provider } from 'jotai'; 2 | 3 | import { RouterProvider } from 'react-router-dom'; 4 | 5 | import { QueryClientProvider } from '@tanstack/react-query'; 6 | import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; 7 | 8 | import { queryClient } from '@/shared/apisV2/queryClient'; 9 | 10 | import router from './router/Router'; 11 | 12 | const App = () => { 13 | return ( 14 | 15 |
16 | 17 |
18 | 19 | 20 | 21 |
22 | ); 23 | }; 24 | 25 | export default App; 26 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | @font-face { 7 | font-family: 'Pretendard'; 8 | src: url("https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/variable/pretendardvariable.min.css"); 9 | } 10 | html { 11 | font-size: 62.5%; 12 | font-family: 'Pretendard'; 13 | } 14 | 15 | /* 스크롤바 디자인 제거 */ 16 | ::-webkit-scrollbar { 17 | display: none; /* Chrome, Safari, Opera에서 스크롤바 숨김 */ 18 | } 19 | 20 | * { 21 | -ms-overflow-style: none; /* IE, Edge에서 스크롤바 숨김 */ 22 | scrollbar-width: none; /* Firefox에서 스크롤바 숨김 */ 23 | } 24 | } 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import * as Sentry from '@sentry/react'; 2 | 3 | import React from 'react'; 4 | import { createRoot } from 'react-dom/client'; 5 | 6 | import { isAxiosError } from 'axios'; 7 | 8 | import App from './App.tsx'; 9 | import './index.css'; 10 | import { getErrorCategory } from './shared/utils/error.ts'; 11 | 12 | // NOTE: 서버 개발 완료로 잠시 주석처리 13 | async function enableMocking() { 14 | // if (import.meta.env.DEV) { 15 | // const { worker } = await import('./mocks/browser'); 16 | // return worker.start(); 17 | // } 18 | } 19 | 20 | Sentry.init({ 21 | dsn: import.meta.env.VITE_SENTRY_DSN, 22 | integrations: [Sentry.browserTracingIntegration(), Sentry.replayIntegration()], 23 | // Tracing 24 | tracesSampleRate: 1.0, // 트랜잭션 100% 캡쳐 25 | 26 | // NOTE: 분산 추적을 위한 대상 도메인 설정 27 | tracePropagationTargets: [ 28 | 'localhost', 29 | // 클라이언트 도메인 (morib.in 또는 www.morib.in) 30 | /^https:\/\/(www\.)?morib\.in/, 31 | // 서버 API 도메인 (api.morib.in의 /api로 시작하는 요청) 32 | /^https:\/\/api\.morib\.in\/api/, 33 | ], 34 | 35 | /* 36 | * 비동기 오류의 경우 센트리 글로벌 캡쳐가 error boundary 도달하기 전에 캡쳐하기 때문에, 37 | * tag나 context를 설정하기 위해서 beforeSend 활용 38 | */ 39 | beforeSend(event, hint) { 40 | const error = hint.originalException; 41 | if (error instanceof Error || isAxiosError(error)) { 42 | const category = getErrorCategory(error); 43 | event.tags = { ...event.tags, errorCategory: category }; 44 | } 45 | if (isAxiosError(error) && error.config) { 46 | const { method, url, params, data: requestData, headers } = error.config; 47 | event.contexts = { 48 | ...event.contexts, 49 | 'API 요청 디테일': { method, url, params, requestData, headers }, 50 | }; 51 | if (error.response) { 52 | const { data, status } = error.response; 53 | event.contexts['API 응답 디테일'] = { data, status }; 54 | } 55 | } 56 | return event; 57 | }, 58 | }); 59 | 60 | const container = document.getElementById('root'); 61 | const root = createRoot(container!); 62 | 63 | enableMocking().then(() => { 64 | root.render( 65 | 66 | 67 | , 68 | ); 69 | }); 70 | -------------------------------------------------------------------------------- /src/mocks/browser.ts: -------------------------------------------------------------------------------- 1 | import { setupWorker } from 'msw/browser'; 2 | 3 | import { handlers } from './handlers'; 4 | 5 | export const worker = setupWorker(...handlers); 6 | -------------------------------------------------------------------------------- /src/mocks/common/common.resolvers.ts: -------------------------------------------------------------------------------- 1 | import { HttpResponse, http } from 'msw'; 2 | 3 | import { COMMON_RES } from './common.responses'; 4 | 5 | export const COMMON_MOCK_URL = { 6 | GET_URL_NAME: 'api/v2/tabName', 7 | }; 8 | 9 | export const commonResolvers = [ 10 | http.get(COMMON_MOCK_URL.GET_URL_NAME, async ({ request }) => { 11 | const url = new URL(request.url); 12 | const tabName = url.searchParams.get('tabName'); 13 | 14 | if (!tabName) { 15 | console.error('경로 파라미터에 tabName이 없습니다.'); 16 | throw new HttpResponse(null, { status: 400 }); 17 | } 18 | 19 | return HttpResponse.json(COMMON_RES.GET_URL_NAME); 20 | }), 21 | ]; 22 | -------------------------------------------------------------------------------- /src/mocks/common/common.responses.ts: -------------------------------------------------------------------------------- 1 | export const COMMON_RES = { 2 | GET_URL_NAME: { 3 | status: 200, 4 | message: '요청이 성공했습니다.', 5 | data: { 6 | tabName: 'NAVER', 7 | }, 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /src/mocks/handlers.ts: -------------------------------------------------------------------------------- 1 | import { commonResolvers } from './common/common.resolvers'; 2 | import { homeResolvers } from './home/home.resolvers'; 3 | import { onboardingResolvers } from './onboarding/onboarding.resolvers'; 4 | 5 | export const handlers = [...homeResolvers, ...onboardingResolvers, ...commonResolvers]; 6 | -------------------------------------------------------------------------------- /src/mocks/onboarding/onboarding.resolvers.ts: -------------------------------------------------------------------------------- 1 | import { HttpResponse, http } from 'msw'; 2 | 3 | import { ONBOARDING_RES } from './onboarding.responses'; 4 | 5 | export const ONBOARDING_MOCK_URL = { 6 | POST_INTEREST_AREA: 'api/v2/onboard', 7 | }; 8 | 9 | export const onboardingResolvers = [ 10 | http.post( 11 | ONBOARDING_MOCK_URL.POST_INTEREST_AREA, 12 | async ({ request }) => { 13 | const url = new URL(request.url); 14 | const interestArea = url.searchParams.get('interestArea'); 15 | 16 | const body = await request.json(); 17 | 18 | if (body.length === 0) return HttpResponse.json(ONBOARDING_RES.POST_INTEREST_AREA); 19 | 20 | if ( 21 | !(body.length > 0 && (typeof body[0].siteName === 'string' || typeof body[0].siteUrl === 'string')) || 22 | !interestArea 23 | ) { 24 | console.error('요청 바디 값과, 쿼리스트링 값을 확인해주세요'); 25 | throw new HttpResponse(null, { status: 400 }); 26 | } 27 | 28 | return HttpResponse.json(ONBOARDING_RES.POST_INTEREST_AREA); 29 | }, 30 | ), 31 | ]; 32 | -------------------------------------------------------------------------------- /src/mocks/onboarding/onboarding.responses.ts: -------------------------------------------------------------------------------- 1 | export const ONBOARDING_RES = { 2 | POST_INTEREST_AREA: { 3 | status: 200, 4 | message: '요청이 성공했습니다.', 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /src/pages/AllowedServicePage/AllowedServiceGroupDetail/AllowedServiceGroupDetail.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react'; 2 | 3 | import Spacer from '@/shared/components/Spacer/Spacer'; 4 | 5 | import AllowedServiceGroupDetailContent from './Content/AllowedServiceGroupDetailContent'; 6 | import AllowedServiceGroupDetailHeader from './Header/AllowedServiceGroupDetailHeader'; 7 | import AllowedServiceGroupDetailTabs from './Tabs/AllowedServiceGroupDetailTabs'; 8 | 9 | interface AllowedServiceGroupDetailRootProps { 10 | children: ReactNode; 11 | } 12 | 13 | const AllowedServiceGroupDetailRoot = ({ children }: AllowedServiceGroupDetailRootProps) => { 14 | return {children}; 15 | }; 16 | 17 | const AllowedServiceGroupDetail = Object.assign(AllowedServiceGroupDetailRoot, { 18 | Header: AllowedServiceGroupDetailHeader, 19 | Input: AllowedServiceGroupDetailHeader.Input, 20 | ColorButton: AllowedServiceGroupDetailHeader.ColorButton, 21 | Tabs: AllowedServiceGroupDetailTabs, 22 | TabButton: AllowedServiceGroupDetailTabs.Button, 23 | Content: AllowedServiceGroupDetailContent, 24 | Table: AllowedServiceGroupDetailContent.Table, 25 | TableRow: AllowedServiceGroupDetailContent.TableRow, 26 | }); 27 | 28 | export default AllowedServiceGroupDetail; 29 | -------------------------------------------------------------------------------- /src/pages/AllowedServicePage/AllowedServiceGroupDetail/Tabs/AllowedServiceGroupDetailTabs.tsx: -------------------------------------------------------------------------------- 1 | import { ButtonHTMLAttributes, ReactNode } from 'react'; 2 | 3 | export interface AllowedServiceGroupDetailTabsRootProps { 4 | children: ReactNode; 5 | } 6 | 7 | const AllowedServiceGroupDetailTabsRoot = ({ children }: AllowedServiceGroupDetailTabsRootProps) => { 8 | return
{children}
; 9 | }; 10 | 11 | export interface AllowedServiceGroupDetailTabsButtonProps extends ButtonHTMLAttributes { 12 | isActive: boolean; 13 | } 14 | 15 | const AllowedServiceGroupDetailTabsButton = ({ isActive, ...props }: AllowedServiceGroupDetailTabsButtonProps) => { 16 | const notSelectedStyle = 'text-gray-03 subhead-bold-22 p-[1rem]'; 17 | const SelectedStyle = 'text-white subhead-bold-22 p-[1rem] border-b-[2px] border-text-white'; 18 | const tabBtnStyle = isActive ? SelectedStyle : notSelectedStyle; 19 | 20 | return ( 21 | 24 | ); 25 | }; 26 | 27 | const AllowedServiceGroupDetailTabs = Object.assign(AllowedServiceGroupDetailTabsRoot, { 28 | Button: AllowedServiceGroupDetailTabsButton, 29 | }); 30 | 31 | export default AllowedServiceGroupDetailTabs; 32 | -------------------------------------------------------------------------------- /src/pages/HomePage/BoxCategory/StatusDefaultBoxCategory/StatusDefaultBoxCategory.tsx: -------------------------------------------------------------------------------- 1 | const StatusDefaultBoxCategory = () => { 2 | return ( 3 |
4 | {/* 명확한 값이 없어서 임의로 mb-[20%로] 지정 */} 5 |

할 일을 추가하려면

6 |

+ 아이콘을 선택해주세요.

7 |
8 | ); 9 | }; 10 | 11 | export default StatusDefaultBoxCategory; 12 | -------------------------------------------------------------------------------- /src/pages/HomePage/BoxCategory/hooks/useCreateTodo.ts: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | 3 | export const useCreateTodo = () => { 4 | const [name, setName] = useState(''); 5 | const [isAdding, setIsAdding] = useState(false); 6 | const [editable, setEditable] = useState(false); 7 | 8 | const handleEditComplete = () => { 9 | setEditable(false); 10 | }; 11 | 12 | const handleInputChange = (name: string) => { 13 | setName(name); 14 | }; 15 | 16 | const startAddingTodo = () => { 17 | setIsAdding(true); 18 | setEditable(true); 19 | }; 20 | 21 | const cancelAddingTodo = () => { 22 | setName(''); 23 | setIsAdding(false); 24 | }; 25 | 26 | return { 27 | name, 28 | isAdding, 29 | editable, 30 | handleEditComplete, 31 | handleInputChange, 32 | startAddingTodo, 33 | cancelAddingTodo, 34 | setName, 35 | setIsAdding, 36 | }; 37 | }; 38 | -------------------------------------------------------------------------------- /src/pages/HomePage/BoxTodayTodo/StatusAddBoxTodayTodo/ButtonHomeSmall/ButtonHomeSmall.tsx: -------------------------------------------------------------------------------- 1 | import { ButtonHTMLAttributes } from 'react'; 2 | 3 | const ButtonHomeSmall = ({ children, disabled = false, ...props }: ButtonHTMLAttributes) => { 4 | const defaultStyle = 5 | 'subhead-bold-20 flex items-center justify-center rounded-[0.8rem] px-[3.6rem] py-[2rem] flex-shrink-0 '; 6 | const buttonStyle = disabled 7 | ? 'bg-gray-bg-06 text-gray-04 ' 8 | : 'bg-gray-bg-05 text-white hover:bg-gray-bg-04 active:bg-gray-bg-05 '; 9 | 10 | return ( 11 | 14 | ); 15 | }; 16 | 17 | export default ButtonHomeSmall; 18 | -------------------------------------------------------------------------------- /src/pages/HomePage/BoxTodayTodo/StatusDefaultBoxTodayTodo/StatusDefaultBoxTodayTodo.tsx: -------------------------------------------------------------------------------- 1 | import ButtonRadius8 from '@/shared/components/ButtonRadius8/ButtonRadius8'; 2 | 3 | interface StatusDefaultBoxTodayTodoProps { 4 | hasTodos: boolean; 5 | onEnableAddStatus: () => void; 6 | } 7 | 8 | const StatusDefaultBoxTodayTodo = ({ hasTodos, onEnableAddStatus }: StatusDefaultBoxTodayTodoProps) => { 9 | return ( 10 |
11 | {/* NOTE: 중앙에 정렬된 레이아웃이 아니어서 임의 값로 살짝 위로 올림, 추후 정확하게 수정 */} 12 |
13 |

아직 오늘 할 일이 없어요

14 |
15 |

할 일을 추가해

16 |

타이머를 시작해 보세요.

17 |
18 | 19 |
20 | 21 | 오늘 할 일 추가 22 | 23 |
24 |
25 |
26 | ); 27 | }; 28 | 29 | export default StatusDefaultBoxTodayTodo; 30 | -------------------------------------------------------------------------------- /src/pages/HomePage/ButtonMoreFriends/ButtonMoreFriends.tsx: -------------------------------------------------------------------------------- 1 | import { ButtonHTMLAttributes } from 'react'; 2 | 3 | import ButtonMoreFriendIcon from '@/shared/assets/svgs/more_friend.svg?react'; 4 | 5 | interface ButtonMoreFriendsProps extends ButtonHTMLAttributes { 6 | friendsCount: number; 7 | } 8 | 9 | const ButtonMoreFriends = ({ friendsCount, ...props }: ButtonMoreFriendsProps) => { 10 | return ( 11 | 17 | ); 18 | }; 19 | 20 | export default ButtonMoreFriends; 21 | -------------------------------------------------------------------------------- /src/pages/HomePage/ButtonUserProfile/ButtonUserProfile.tsx: -------------------------------------------------------------------------------- 1 | import { ButtonHTMLAttributes } from 'react'; 2 | 3 | import ConnectionIcon from '@/shared/assets/svgs/connection_icon.svg?react'; 4 | import defaultPorfileIcon from '@/shared/assets/svgs/default_profile.svg'; 5 | import GradientCircleIcon from '@/shared/assets/svgs/gradient_circle.svg?react'; 6 | 7 | interface ButtonUserProfile extends ButtonHTMLAttributes { 8 | isMyProfile?: boolean; 9 | isConnecting?: boolean; 10 | isSelectedUser?: boolean; 11 | isOnline?: boolean; 12 | imageUrl?: string; 13 | } 14 | 15 | const ButtonUserProfile = ({ 16 | isMyProfile = false, 17 | isSelectedUser = false, 18 | isOnline = false, 19 | imageUrl, 20 | }: ButtonUserProfile) => { 21 | const profileImage = imageUrl || defaultPorfileIcon; 22 | 23 | return ( 24 | 42 | ); 43 | }; 44 | 45 | export default ButtonUserProfile; 46 | -------------------------------------------------------------------------------- /src/pages/HomePage/DatePicker/ButtonDate/ButtonDate.tsx: -------------------------------------------------------------------------------- 1 | import { ButtonHTMLAttributes, ReactNode } from 'react'; 2 | 3 | interface DateBtnProps extends ButtonHTMLAttributes { 4 | isSelected: boolean; 5 | children: ReactNode; 6 | } 7 | 8 | const ButtonDate = ({ isSelected, children, ...props }: DateBtnProps) => { 9 | const commonBtnStyle = 'flex w-full h-[7.8rem] py-[0.8rem] 2xl:h-[7.6rem] items-center justify-center text-white '; 10 | const textStyle = isSelected ? 'subhead-bold-20 2xl:head-bold-24' : 'subhead-med-18 2xl:subhead-reg-22'; 11 | const borderStyle = isSelected ? 'border-b-[0.3rem] border-mint-01' : 'border-b-[0.2rem] border-gray-02'; 12 | 13 | const groupHoverBorderStyle = isSelected ? '' : 'group-hover:border-b-[0.3rem] group-hover:border-gray-03'; 14 | 15 | return ( 16 |
17 | 20 |
21 |
22 | ); 23 | }; 24 | 25 | export default ButtonDate; 26 | -------------------------------------------------------------------------------- /src/pages/HomePage/DatePicker/hooks/useDatePicker.ts: -------------------------------------------------------------------------------- 1 | import { Dayjs } from 'dayjs'; 2 | 3 | import { getWeekDates } from '@/shared/utils/date'; 4 | 5 | interface UseDatePickerProps { 6 | todayDate: Dayjs; 7 | selectedDate: Dayjs; 8 | onSelectedDateChange: (date: Dayjs) => void; 9 | } 10 | 11 | export const useDatePicker = ({ todayDate, selectedDate, onSelectedDateChange }: UseDatePickerProps) => { 12 | const weekDates = getWeekDates(selectedDate); 13 | 14 | const handleNextWeek = () => { 15 | onSelectedDateChange(selectedDate.add(1, 'week')); 16 | }; 17 | 18 | const handlePreviousWeek = () => { 19 | onSelectedDateChange(selectedDate.subtract(1, 'week')); 20 | }; 21 | 22 | const handleToday = () => { 23 | onSelectedDateChange(todayDate); 24 | }; 25 | 26 | const handleYearMonthClick = (yearMonthDate: Dayjs) => { 27 | if (yearMonthDate.isSame(todayDate, 'month')) { 28 | onSelectedDateChange(todayDate); 29 | } else { 30 | onSelectedDateChange(yearMonthDate); 31 | } 32 | }; 33 | 34 | return { 35 | weekDates, 36 | handleNextWeek, 37 | handlePreviousWeek, 38 | handleToday, 39 | handleYearMonthClick, 40 | }; 41 | }; 42 | -------------------------------------------------------------------------------- /src/pages/HomePage/ModalContentsAlert/ButtonAlert/ButtonAlert.tsx: -------------------------------------------------------------------------------- 1 | import { ButtonHTMLAttributes, ReactNode } from 'react'; 2 | 3 | interface ButtonAlertProps extends ButtonHTMLAttributes { 4 | variant?: 'primary' | 'danger' | 'mint'; 5 | children: ReactNode; 6 | className?: string; 7 | } 8 | 9 | const ButtonAlert = ({ children, variant = 'primary', className = '', ...props }: ButtonAlertProps) => { 10 | const primaryStyle = 'bg-gray-bg-06 hover:bg-gray-bg-04 text-white active:bg-gray-bg-05'; 11 | const dangerStyle = 'bg-error-01 hover:bg-error-03 active:bg-error-03 text-white active:text-gray-04'; 12 | const mintStyle = 'bg-mint-02 hover:bg-mint-01 active:bg-mint-03 text-black active:text-gray-01'; 13 | 14 | const buttonStyle = variant === 'primary' ? primaryStyle : variant === 'danger' ? dangerStyle : mintStyle; 15 | 16 | return ( 17 | 23 | ); 24 | }; 25 | 26 | export default ButtonAlert; 27 | -------------------------------------------------------------------------------- /src/pages/HomePage/ModalContentsAlert/Complete/Complete.tsx: -------------------------------------------------------------------------------- 1 | import ButtonAlert from '../ButtonAlert/ButtonAlert'; 2 | import { AlertModalProps } from '../types/index'; 3 | 4 | const Complete = ({ onCloseModal, userEmail }: AlertModalProps) => ( 5 |
6 |
7 |

{userEmail} 계정이

8 |

삭제되었습니다.

9 | 10 | 확인 11 | 12 |
13 |
14 | ); 15 | 16 | export default Complete; 17 | -------------------------------------------------------------------------------- /src/pages/HomePage/ModalContentsAlert/Logoout/Logout.tsx: -------------------------------------------------------------------------------- 1 | import ButtonAlert from '../ButtonAlert/ButtonAlert'; 2 | import { AlertModalProps } from '../types/index'; 3 | 4 | const Logout = ({ onCloseModal, onConfirm, userEmail }: AlertModalProps) => ( 5 |
6 |

{userEmail} 계정이

7 |

8 | 본 기기를 포함한 모든 기기에서 로그아웃됩니다. 9 |

10 |

로그아웃 하시겠습니까?

11 |
12 | 13 | 로그아웃 14 | 15 | 16 | 취소하기 17 | 18 |
19 |
20 | ); 21 | 22 | export default Logout; 23 | -------------------------------------------------------------------------------- /src/pages/HomePage/ModalContentsAlert/ModalContentsAlert.tsx: -------------------------------------------------------------------------------- 1 | import Complete from './Complete/Complete'; 2 | import DeleteAccount from './DeleteAccount/DeleteAccount'; 3 | import Logout from './Logoout/Logout'; 4 | 5 | const ModalContentsAlert = { 6 | Logout: Logout, 7 | DeleteAccount: DeleteAccount, 8 | Complete: Complete, 9 | }; 10 | 11 | export default ModalContentsAlert; 12 | -------------------------------------------------------------------------------- /src/pages/HomePage/ModalContentsAlert/RegisterAllowedService/RegisterAllowedService.tsx: -------------------------------------------------------------------------------- 1 | import ButtonAlert from '../ButtonAlert/ButtonAlert'; 2 | import { AlertModalProps } from '../types/index'; 3 | 4 | const RegisterAllowedService = ({ onCloseModal, onConfirm }: AlertModalProps) => ( 5 |
6 |
7 |

허용 서비스 세트를 먼저 등록해주세요.

8 |

허용 서비스를 등록하러 갈까요?

9 |
10 |
11 | 12 | 등록하기 13 | 14 | 15 | 나중에 하기 16 | 17 |
18 |
19 | ); 20 | 21 | export default RegisterAllowedService; 22 | -------------------------------------------------------------------------------- /src/pages/HomePage/ModalContentsAlert/TimerRestriction/TimerRestriction.tsx: -------------------------------------------------------------------------------- 1 | import ButtonAlert from '../ButtonAlert/ButtonAlert'; 2 | import { AlertModalProps } from '../types/index'; 3 | 4 | const TimerRestriction = ({ onConfirm }: AlertModalProps) => ( 5 |
6 |
7 |

오늘 날짜에 해당하는 할 일만

8 |

추가할 수 있어요.

9 |
10 | 11 | 확인 12 | 13 |
14 | ); 15 | 16 | export default TimerRestriction; 17 | -------------------------------------------------------------------------------- /src/pages/HomePage/ModalContentsAlert/types/index.ts: -------------------------------------------------------------------------------- 1 | export interface AlertModalProps { 2 | onCloseModal?: () => void; 3 | onConfirm?: () => void; 4 | userEmail?: string; 5 | } 6 | -------------------------------------------------------------------------------- /src/pages/HomePage/StatusDefaultHome/StatusDefaultHome.tsx: -------------------------------------------------------------------------------- 1 | import ButtonRadius8 from '@/shared/components/ButtonRadius8/ButtonRadius8'; 2 | 3 | import { LARGE_BTN_TEXT } from '@/shared/constants/btnText'; 4 | 5 | import BoxIcon from '@/shared/assets/svgs/home/ic_box.svg?react'; 6 | 7 | interface StatusDefaultHomeProps { 8 | onClick?: () => void; 9 | } 10 | 11 | const StatusDefaultHome = ({ onClick }: StatusDefaultHomeProps) => { 12 | return ( 13 |
14 | 15 |

16 | 당신의 몰입을 도와줄 17 |
18 | 작업 카테고리를 만들어 보세요! 19 |

20 | {LARGE_BTN_TEXT.CREATE_CATEGORY} 21 |
22 | ); 23 | }; 24 | 25 | export default StatusDefaultHome; 26 | -------------------------------------------------------------------------------- /src/pages/HomePage/hooks/useCalendar.ts: -------------------------------------------------------------------------------- 1 | import dayjs, { Dayjs } from 'dayjs'; 2 | 3 | import { useState } from 'react'; 4 | 5 | export const useCalendar = () => { 6 | const defaultDate = dayjs(); 7 | 8 | const [isDateToggleOn, setIsDateToggleOn] = useState(false); 9 | const [isPeriodOn, setIsPeriodOn] = useState(false); 10 | const [selectedStartDate, setSelectedStartDate] = useState(defaultDate); 11 | const [selectedEndDate, setSelectedEndDate] = useState(null); 12 | const [isCalendarOpened, setIsCalendarOpened] = useState(true); 13 | 14 | const handleDateToggle = () => { 15 | if (!isDateToggleOn) { 16 | setIsCalendarOpened(true); 17 | } else { 18 | setIsPeriodOn(false); 19 | setSelectedStartDate(null); 20 | setSelectedEndDate(null); 21 | } 22 | setIsDateToggleOn((prev) => !prev); 23 | }; 24 | 25 | const handlePeriodToggle = () => { 26 | if (isPeriodOn === true) { 27 | setSelectedEndDate(null); 28 | } 29 | setIsPeriodOn((prev) => !prev); 30 | }; 31 | 32 | const handlePeriodEnd = () => { 33 | setIsPeriodOn(false); 34 | }; 35 | 36 | const handleCalendarToggle = () => { 37 | setIsCalendarOpened((prev) => !prev); 38 | }; 39 | 40 | const handleStartDateInput = (date: Dayjs | null) => { 41 | setSelectedStartDate(date); 42 | }; 43 | 44 | const handleEndDateInput = (date: Dayjs | null) => { 45 | setSelectedEndDate(date); 46 | }; 47 | 48 | const handleClearDateInfo = () => { 49 | setSelectedStartDate(null); 50 | setSelectedEndDate(null); 51 | setIsDateToggleOn(false); 52 | }; 53 | 54 | return { 55 | isDateToggleOn, 56 | isPeriodOn, 57 | selectedStartDate, 58 | selectedEndDate, 59 | isCalendarOpened, 60 | defaultDate, 61 | handleDateToggle, 62 | handlePeriodToggle, 63 | handleCalendarToggle, 64 | handleStartDateInput, 65 | handleEndDateInput, 66 | handlePeriodEnd, 67 | handleClearDateInfo, 68 | }; 69 | }; 70 | -------------------------------------------------------------------------------- /src/pages/LoginPage/LoginPage.tsx: -------------------------------------------------------------------------------- 1 | import Lottie from 'react-lottie'; 2 | 3 | import LottieData from '@/shared/assets/lotties/morib_logo_motion.json'; 4 | import GoogleLoginIcon from '@/shared/assets/svgs/google_login.svg?react'; 5 | 6 | import { useLottieAnimation } from '@/pages/LoginPage/hooks/useLottieAnimation'; 7 | 8 | const defaultOptions = { 9 | autoplay: true, 10 | loop: false, 11 | animationData: LottieData, 12 | rendererSettings: { 13 | preserveAspectRatio: 'xMidYMid slice', 14 | }, 15 | }; 16 | 17 | const API_URL = `${import.meta.env.VITE_GOOGLE_URL}`; 18 | 19 | const LoginPage = () => { 20 | const { isAnimationComplete, lottieRef, handleAnimationComplete } = useLottieAnimation(); 21 | 22 | const handleClick = () => { 23 | window.location.href = API_URL; 24 | }; 25 | 26 | const handleMouseEnter = () => { 27 | import('@/pages/HomePage/HomePage').catch((error) => { 28 | console.error('홈페이지를 받아오는데 오류가 발생했습니다.', error); 29 | }); 30 | }; 31 | 32 | return ( 33 |
34 |
35 | 49 | {/* Todo: 추후 로그인 로직 추가 */} 50 | 57 |
58 |
59 | ); 60 | }; 61 | export default LoginPage; 62 | -------------------------------------------------------------------------------- /src/pages/LoginPage/hooks/useLottieAnimation.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef, useState } from 'react'; 2 | 3 | export const useLottieAnimation = () => { 4 | const [isAnimationComplete, setIsAnimationComplete] = useState(false); 5 | // Todo: 추후 정확한 타입 추가, lottie 라이브러리 Lottie 컴포넌트의 정확한 타입을 아직 확인하지 못하여 일단 any로 해당 엘리먼트를 받아왔음 6 | const lottieRef = useRef(null); 7 | 8 | const handleAnimationComplete = () => { 9 | setIsAnimationComplete(true); 10 | }; 11 | 12 | useEffect(() => { 13 | if (lottieRef.current) { 14 | lottieRef.current.play(); 15 | } 16 | }, []); 17 | 18 | return { 19 | isAnimationComplete, 20 | lottieRef, 21 | handleAnimationComplete, 22 | }; 23 | }; 24 | -------------------------------------------------------------------------------- /src/pages/NotFoundPage/NotFoundPage.tsx: -------------------------------------------------------------------------------- 1 | import { useNavigate } from 'react-router-dom'; 2 | 3 | import HomeLargeBtn from '@/shared/components/ButtonHomeLarge/ButtonHomeLarge'; 4 | 5 | import { HomeLargeBtnVariant } from '@/shared/types/global'; 6 | 7 | import NotFoundIcon from '@/shared/assets/svgs/404.svg?react'; 8 | import BellIcon from '@/shared/assets/svgs/bell.svg?react'; 9 | import FriendSettingIcon from '@/shared/assets/svgs/friend_setting.svg?react'; 10 | 11 | const NotFoundPage = () => { 12 | const navigate = useNavigate(); 13 | 14 | return ( 15 |
16 |
17 | 20 | 23 |
24 | 25 |
26 | 27 |

페이지를 찾을 수 없습니다.

28 |

올바른 URL을 입력하였는지 확인하세요.

29 | 30 |
31 | navigate('/home')} variant={HomeLargeBtnVariant.LARGE}> 32 | 홈으로 돌아가기 33 | 34 |
35 |
36 |
37 | ); 38 | }; 39 | 40 | export default NotFoundPage; 41 | -------------------------------------------------------------------------------- /src/pages/OnboardingPage/ButtonSkip/ButtonSkip.tsx: -------------------------------------------------------------------------------- 1 | import { useNavigate } from 'react-router-dom'; 2 | 3 | import type { PostInterestAreaReq } from '@/shared/types/api/onboarding'; 4 | 5 | import { ROUTES_CONFIG } from '@/router/routesConfig'; 6 | 7 | import { usePostInterestArea } from '@/shared/apisV2/onboarding/onboarding.mutations'; 8 | 9 | const ButtonSkip = () => { 10 | const { mutate: postInterestArea } = usePostInterestArea(); 11 | 12 | const navigate = useNavigate(); 13 | const handleNavigateToHome = () => { 14 | postInterestArea({} as PostInterestAreaReq, { 15 | onSuccess: () => { 16 | navigate(ROUTES_CONFIG.home.path); 17 | }, 18 | }); 19 | }; 20 | return ( 21 | 24 | ); 25 | }; 26 | 27 | export default ButtonSkip; 28 | -------------------------------------------------------------------------------- /src/pages/OnboardingPage/OnboardingPage.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { useNavigate } from 'react-router-dom'; 3 | 4 | import type { FieldType } from '@/shared/types/fileds'; 5 | 6 | import { ROUTES_CONFIG } from '@/router/routesConfig'; 7 | 8 | import StepField from './StepField/StepField'; 9 | import StepService from './StepService/StepService'; 10 | import StepStart from './StepStart/StepStart'; 11 | import { useFunnel } from './hooks/useFunnel'; 12 | 13 | const OnboardingPage = () => { 14 | const navigate = useNavigate(); 15 | const { Funnel, Step, setStep } = useFunnel(); 16 | 17 | const [selectedField, setSelectedField] = useState(null); 18 | 19 | const handleSelectField = (field: FieldType) => { 20 | setSelectedField(field); 21 | }; 22 | 23 | useEffect(() => { 24 | const 온보딩완료여부 = localStorage.getItem('isOnboardingCompleted'); 25 | 26 | if (온보딩완료여부 === 'true') { 27 | navigate(ROUTES_CONFIG.home.path, { replace: true }); 28 | } 29 | }); 30 | 31 | return ( 32 |
33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 |
45 | ); 46 | }; 47 | 48 | export default OnboardingPage; 49 | -------------------------------------------------------------------------------- /src/pages/OnboardingPage/StepField/StepField.tsx: -------------------------------------------------------------------------------- 1 | import HomeLargeBtn from '@/shared/components/ButtonHomeLarge/ButtonHomeLarge'; 2 | 3 | import type { FieldType } from '@/shared/types/fileds'; 4 | import { HomeLargeBtnVariant } from '@/shared/types/global'; 5 | 6 | import { FIELDS_WITH_ICONS } from '@/shared/constants/fields'; 7 | 8 | import ButtonSkip from '../ButtonSkip/ButtonSkip'; 9 | 10 | interface StepFieldProps { 11 | setStep: (step: string) => void; 12 | onSelectField: (field: FieldType) => void; 13 | selectedField: FieldType | null; 14 | } 15 | 16 | const StepField = ({ setStep, onSelectField, selectedField }: StepFieldProps) => { 17 | return ( 18 |
19 |

주로 어떤 분야에 집중하시나요?

20 |

21 | 업무 분야에 자주 쓰이는 서비스들을 추천 해드릴게요 22 |

23 | 24 |
25 |
    26 | {FIELDS_WITH_ICONS.map((field) => ( 27 |
  • 28 | 35 |
  • 36 | ))} 37 |
38 |
39 | 40 | setStep('service')} 43 | disabled={selectedField === null} 44 | className="mb-[2rem]" 45 | > 46 | 다음으로 넘어가기 47 | 48 | 49 | 50 |
51 | ); 52 | }; 53 | 54 | export default StepField; 55 | -------------------------------------------------------------------------------- /src/pages/OnboardingPage/StepService/ButtonService/ButtonService.tsx: -------------------------------------------------------------------------------- 1 | interface ButtonServiceProps { 2 | favicon: string; 3 | title: string; 4 | url: string; 5 | onAddSelectedService: () => void; 6 | isSelected: boolean; 7 | } 8 | 9 | const ButtonService = ({ title, url, favicon, onAddSelectedService, isSelected }: ButtonServiceProps) => { 10 | return ( 11 | 23 | ); 24 | }; 25 | 26 | export default ButtonService; 27 | -------------------------------------------------------------------------------- /src/pages/OnboardingPage/StepStart/StepStart.tsx: -------------------------------------------------------------------------------- 1 | import HomeLargeBtn from '@/shared/components/ButtonHomeLarge/ButtonHomeLarge'; 2 | 3 | import { HomeLargeBtnVariant } from '@/shared/types/global'; 4 | 5 | import OnboardingIcon from '@/shared/assets/svgs/onboarding_image.svg?react'; 6 | 7 | import ButtonSkip from '../ButtonSkip/ButtonSkip'; 8 | 9 | interface StepStartProps { 10 | setStep: (step: string) => void; 11 | } 12 | 13 | const StepStart = ({ setStep }: StepStartProps) => { 14 | return ( 15 |
16 |

17 | 집중을 도와줄 허용서비스 리스트를 만들어볼까요? 18 |

19 |

20 | 작업 할 때 필요한 서비스들만을 사용하며 오롯이 할 일에 집중해보세요. 21 |
22 | 작업할 때 자주 쓰는 서비스들을 추천해드릴게요! 23 |

24 | 25 | 26 | setStep('field')} className="mb-[2rem]"> 27 | 시작하기 28 | 29 | 30 | 31 |
32 | ); 33 | }; 34 | 35 | export default StepStart; 36 | -------------------------------------------------------------------------------- /src/pages/OnboardingPage/hooks/useFunnel.tsx: -------------------------------------------------------------------------------- 1 | import { ReactElement, ReactNode } from 'react'; 2 | import { useSearchParams } from 'react-router-dom'; 3 | 4 | interface StepProps { 5 | name: string; 6 | children: ReactNode; 7 | } 8 | 9 | interface FunnelProps { 10 | children: ReactElement[]; 11 | } 12 | 13 | export const useFunnel = () => { 14 | const [searchParams, setSearchParams] = useSearchParams(); 15 | 16 | const step = searchParams.get('step') || 'start'; 17 | 18 | const setStep = (step: string) => { 19 | searchParams.set('step', step); 20 | setSearchParams(searchParams); 21 | }; 22 | 23 | const Step = ({ children }: StepProps) => { 24 | return <>{children}; 25 | }; 26 | 27 | const Funnel = ({ children }: FunnelProps) => { 28 | const targetStep = children.find((childStep) => childStep.props.name === step); 29 | return <>{targetStep}; 30 | }; 31 | 32 | return { Funnel, Step, setStep }; 33 | }; 34 | -------------------------------------------------------------------------------- /src/pages/OnboardingPage/utils/serviceUrl.ts: -------------------------------------------------------------------------------- 1 | export const getServiceFavicon = (url: string): string => { 2 | return `https://www.google.com/s2/favicons?domain=${url}`; 3 | }; 4 | -------------------------------------------------------------------------------- /src/pages/RedirectPage/RedirectPage.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import { useLocation, useNavigate } from 'react-router-dom'; 3 | 4 | import LoadingOverlay from '@/shared/components/LoadingOverlay/LoadingOverlay'; 5 | 6 | import { setAccessToken, setRefreshToken } from '@/shared/utils/auth'; 7 | 8 | import { ROUTES_CONFIG } from '@/router/routesConfig'; 9 | 10 | const RedirectPage = () => { 11 | const { search } = useLocation(); 12 | const navigate = useNavigate(); 13 | 14 | useEffect(() => { 15 | // @ts-expect-error 16 | const params = window.electron 17 | ? new URLSearchParams(window.location.hash.split('?')[1]) 18 | : new URLSearchParams(search); 19 | const accessToken = params.get('accessToken'); 20 | const refreshToken = params.get('refreshToken'); 21 | const 온보딩완료여부 = params.get('isOnboardingCompleted'); 22 | 23 | if (!accessToken || !refreshToken || !온보딩완료여부) { 24 | navigate(`${ROUTES_CONFIG.login.path}`, { replace: true }); 25 | } else { 26 | setAccessToken(accessToken); 27 | setRefreshToken(refreshToken); 28 | } 29 | 30 | if (온보딩완료여부 === 'false') { 31 | localStorage.setItem('isOnboardingCompleted', 온보딩완료여부); 32 | navigate(`${ROUTES_CONFIG.onboarding.path}?step=start`, { replace: true }); 33 | } else if (온보딩완료여부 === 'true') { 34 | localStorage.setItem('isOnboardingCompleted', 온보딩완료여부); 35 | navigate(`${ROUTES_CONFIG.home.path}`, { replace: true }); 36 | } 37 | }, [navigate, search]); 38 | 39 | return ; 40 | }; 41 | 42 | export default RedirectPage; 43 | -------------------------------------------------------------------------------- /src/pages/TimerPage/AllowedServices/AllowedServicesItem.tsx: -------------------------------------------------------------------------------- 1 | import { COLOR_PALETTE_MAP } from '@/shared/constants/colorPalette'; 2 | 3 | interface AllowedServicesItemProps { 4 | id: number; 5 | name: string; 6 | colorCode: string; 7 | isActive: boolean; 8 | onSelect: (id: number) => void; 9 | } 10 | 11 | const AllowedServicesItem = ({ id, name, colorCode, isActive, onSelect }: AllowedServicesItemProps) => { 12 | const colorClass = 13 | colorCode in COLOR_PALETTE_MAP ? COLOR_PALETTE_MAP[colorCode as keyof typeof COLOR_PALETTE_MAP] : 'bg-gray-bg-07'; 14 | 15 | return ( 16 |
  • onSelect(id)} 21 | tabIndex={0} 22 | > 23 |
    24 | {name} 25 |
    26 | 27 | 28 |
  • 29 | ); 30 | }; 31 | 32 | export default AllowedServicesItem; 33 | -------------------------------------------------------------------------------- /src/pages/TimerPage/AllowedServices/AllowedServicesTitle.tsx: -------------------------------------------------------------------------------- 1 | import MoribSetBtnActiveIcon from '@/shared/assets/svgs/btn_moribset_active.svg?react'; 2 | import MoribSetBtnDefaultIcon from '@/shared/assets/svgs/btn_moribset_default.svg?react'; 3 | 4 | import AllowedServicesTooltip from './AllowedServicesTooltip'; 5 | 6 | interface AllowedServicesTitleProps { 7 | onClick: () => void; 8 | registeredNames: string[]; 9 | isAllowedServiceVisible: boolean; 10 | } 11 | 12 | /** 13 | * 허용 서비스 타이틀 컴포넌트 14 | * @param onClick 클릭 시 실행할 함수 15 | * @param registeredNames 등록된 서비스 이름 목록 16 | * @param isAllowedServiceVisible 허용 서비스 팝업이 표시 중인지 여부 17 | */ 18 | const AllowedServicesTitle = ({ onClick, registeredNames, isAllowedServiceVisible }: AllowedServicesTitleProps) => { 19 | const joinedNames = registeredNames.join(', '); 20 | const hasServices = registeredNames.length > 0; 21 | 22 | return ( 23 |
    30 | {hasServices ? ( 31 | // 서비스가 등록된 경우 32 | <> 33 | 34 |

    35 | [ 36 | {joinedNames} 37 | ] 허용 서비스 세트 실행 중 38 |

    39 | 40 | ) : ( 41 | // 서비스가 등록되지 않은 경우 42 |
    43 |
    44 | 45 |

    허용서비스 세트를 등록해주세요.

    46 |
    47 | {!isAllowedServiceVisible && ( 48 |
    49 | 50 |
    51 | )} 52 |
    53 | )} 54 |
    55 | ); 56 | }; 57 | 58 | export default AllowedServicesTitle; 59 | -------------------------------------------------------------------------------- /src/pages/TimerPage/AllowedServices/AllowedServicesTooltip.tsx: -------------------------------------------------------------------------------- 1 | import TriangleIcon from '@/shared/assets/svgs/triangle.svg?react'; 2 | 3 | const ToolTipAllowedService = () => { 4 | return ( 5 |
    6 | 7 |
    8 | 허용 서비스 세트를 먼저 설정해주세요. 9 |
    10 |
    11 | ); 12 | }; 13 | 14 | export default ToolTipAllowedService; 15 | -------------------------------------------------------------------------------- /src/pages/TimerPage/AllowedServices/Checkbox.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import ActiveCheckboxIcon from '@/shared/assets/svgs/timer/ic_check_box_active.svg?react'; 4 | import InactiveCheckboxIcon from '@/shared/assets/svgs/timer/ic_check_box_inactive.svg?react'; 5 | 6 | interface CheckboxProps { 7 | onClick?: () => void; 8 | checked?: boolean; 9 | } 10 | 11 | /** 12 | * 체크박스 컴포넌트 13 | * @param onClick 클릭 시 실행할 함수 14 | * @param checked 체크 여부 15 | */ 16 | const Checkbox = ({ onClick, checked = false }: CheckboxProps) => { 17 | return ( 18 |
    19 | {checked ? : } 20 |
    21 | ); 22 | }; 23 | 24 | export default Checkbox; 25 | -------------------------------------------------------------------------------- /src/pages/TimerPage/MainTimer/TimerDisplay/ButtonTimerPlay/ButtonTimerPlay.tsx: -------------------------------------------------------------------------------- 1 | import { MouseEvent } from 'react'; 2 | 3 | import PauseIcon from '@/shared/assets/svgs/defaultpause.svg?react'; 4 | import PlayIcon from '@/shared/assets/svgs/defaultplay.svg?react'; 5 | import HoverPauseIcon from '@/shared/assets/svgs/hoverpause.svg?react'; 6 | import HoverPlayIcon from '@/shared/assets/svgs/hoverplay.svg?react'; 7 | 8 | interface ButtonTimerPlayProps { 9 | onClick: () => void; 10 | isPlaying: boolean; 11 | disabled?: boolean; 12 | } 13 | 14 | const ButtonTimerPlay = ({ onClick, isPlaying, disabled = false }: ButtonTimerPlayProps) => { 15 | const IconComponent = isPlaying ? PauseIcon : PlayIcon; 16 | const HoverIconComponent = isPlaying ? HoverPauseIcon : HoverPlayIcon; 17 | 18 | return ( 19 | 23 | ); 24 | }; 25 | 26 | export default ButtonTimerPlay; 27 | -------------------------------------------------------------------------------- /src/pages/TimerPage/MainTimer/TimerDisplay/ProgressCircle/ProgressCircle.tsx: -------------------------------------------------------------------------------- 1 | interface ProgressCircleProps { 2 | isPlaying: boolean; 3 | timer: number; 4 | } 5 | 6 | const ProgressCircle = ({ timer }: ProgressCircleProps) => { 7 | const radius = 196; 8 | const circumference = 2 * Math.PI * radius; 9 | 10 | const progress = ((timer % 3600) / 3600) * 100; 11 | const offset = circumference - (progress / 100) * circumference; 12 | const angle = (progress / 100) * 2 * Math.PI - Math.PI / 2; 13 | const endX = 210 + radius * Math.cos(angle); 14 | const endY = 210 + radius * Math.sin(angle); 15 | 16 | return ( 17 | 18 | 19 | 35 | 36 | 37 | 45 | 46 | 47 | 48 | 49 | 50 | ); 51 | }; 52 | 53 | export default ProgressCircle; 54 | -------------------------------------------------------------------------------- /src/pages/TimerPage/MainTimer/TimerDisplay/TimerDisplay.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import InnerCircleIcon from '@/shared/assets/svgs/timer/ic_timer_inner_circle.svg?react'; 4 | 5 | import ButtonTimerPlay from './ButtonTimerPlay/ButtonTimerPlay'; 6 | import ProgressCircle from './ProgressCircle/ProgressCircle'; 7 | 8 | /** 9 | * 타이머 디스플레이 컴포넌트 props 타입 정의 10 | */ 11 | interface TimerDisplayProps { 12 | statusText: string; 13 | formattedTimeText: string; 14 | timer: number; 15 | isPlaying: boolean; 16 | onToggle: () => void; 17 | disabled?: boolean; 18 | } 19 | 20 | /** 21 | * 타이머 디스플레이 컴포넌트 22 | * 23 | * 타이머의 시간 표시, 상태 텍스트 및 재생/정지 버튼을 포함하는 UI 24 | */ 25 | const TimerDisplay = ({ 26 | statusText, 27 | formattedTimeText, 28 | timer, 29 | isPlaying, 30 | onToggle, 31 | disabled = false, 32 | }: TimerDisplayProps) => { 33 | return ( 34 |
    35 | 36 | 37 |
    38 |
    39 | {statusText} 40 | {formattedTimeText} 41 |
    42 | 43 | 44 |
    45 |
    46 | ); 47 | }; 48 | 49 | export default TimerDisplay; 50 | -------------------------------------------------------------------------------- /src/pages/TimerPage/MainTimer/TimerHeader/TimerHeader.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | /** 4 | * 타이머 헤더 컴포넌트 props 타입 정의 5 | */ 6 | interface TimerHeaderProps { 7 | selectedTaskName: string; 8 | selectedTaskCategoryName: string; 9 | hasSelectedTask: boolean; 10 | isCompleted?: boolean; 11 | } 12 | 13 | /** 14 | * 타이머 헤더 컴포넌트 15 | * 16 | * 선택된 할일의 이름과 카테고리를 표시하거나, 할일이 선택되지 않은 경우 안내 메시지를 표시. 17 | */ 18 | const TimerHeader = ({ 19 | selectedTaskName, 20 | selectedTaskCategoryName, 21 | hasSelectedTask, 22 | isCompleted = false, 23 | }: TimerHeaderProps) => { 24 | if (!hasSelectedTask) { 25 | return ( 26 |
    27 |

    할일을 선택해주세요

    28 |
    29 | ); 30 | } 31 | 32 | return ( 33 |
    34 |

    {selectedTaskName}

    35 |

    {selectedTaskCategoryName}

    36 |
    37 | ); 38 | }; 39 | 40 | export default TimerHeader; 41 | -------------------------------------------------------------------------------- /src/pages/TimerPage/NavigationButtons/NavigationButtons.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import HamburgerIcon from '@/shared/assets/svgs/btn_hamburger.svg?react'; 4 | import HomeIcon from '@/shared/assets/svgs/btn_home.svg?react'; 5 | 6 | interface NavigationButtonsProps { 7 | onHomeClick: () => void; 8 | onSidebarToggle: () => void; 9 | } 10 | 11 | /** 12 | * 네비게이션 버튼 컴포넌트 13 | */ 14 | const NavigationButtons = ({ onHomeClick, onSidebarToggle }: NavigationButtonsProps) => { 15 | return ( 16 |
    17 | 20 | 23 |
    24 | ); 25 | }; 26 | 27 | export default NavigationButtons; 28 | -------------------------------------------------------------------------------- /src/pages/TimerPage/hooks/useAllowedServices.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react'; 2 | 3 | import { getBaseUrl } from '@/shared/utils/url'; 4 | 5 | import { DEFAULT_URL } from '@/shared/constants/timerPageText'; 6 | 7 | import { useGetPopoverAllowedServiceList } from '@/shared/apisV2/timer/timer.queries'; 8 | 9 | /** 10 | * 허용된 서비스를 관리하는 커스텀 훅 11 | * React Query를 통해 받아온 데이터를 가공하여 필요한 형태로 반환 12 | * @returns 등록된 서비스 이름 목록, 허용된 사이트 URL 목록, 기본 URL을 포함한 URL 목록 13 | */ 14 | export const useAllowedServices = () => { 15 | // 허용된 서비스 목록 가져오기 16 | const { data: allowedServiceList } = useGetPopoverAllowedServiceList(); 17 | 18 | // 등록된 서비스 및 URL 정보 가공 19 | const { registeredNames, allowedSiteUrls } = useMemo(() => { 20 | if (!allowedServiceList?.data) { 21 | return { registeredNames: [], allowedSiteUrls: [] }; 22 | } 23 | 24 | // 선택된 서비스 그룹만 필터링 25 | const selectedGroups = allowedServiceList.data.filter((group) => group.selected); 26 | 27 | // 그룹 이름만 추출 28 | const names = selectedGroups.map((group) => group.name); 29 | 30 | // 모든 허용된 사이트 URL 수집 및 중복 제거 31 | const urls = Array.from( 32 | new Set(selectedGroups.flatMap((group) => group.allowedSites?.map((site) => site.siteUrl) || [])), 33 | ); 34 | 35 | return { 36 | registeredNames: names, 37 | allowedSiteUrls: urls, 38 | }; 39 | }, [allowedServiceList?.data]); 40 | 41 | // 정리된 URL에서 기본 URL 추출 42 | const baseUrls = useMemo(() => { 43 | const mappedUrls = allowedSiteUrls.map((url) => getBaseUrl(url.trim())); 44 | return [...mappedUrls, DEFAULT_URL]; 45 | }, [allowedSiteUrls]); 46 | 47 | return { 48 | registeredNames, 49 | allowedSiteUrls, 50 | baseUrls, 51 | }; 52 | }; 53 | -------------------------------------------------------------------------------- /src/pages/TimerPage/hooks/useUIState.ts: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | 3 | /** 4 | * 타이머 UI 상태를 관리하는 훅 5 | * 6 | * 사이드바 토글, 모립셋 가시성과 같은 UI 관련 상태를 관리. 7 | * 8 | * @returns UI 상태와 액션 함수들 9 | */ 10 | export function useUIState() { 11 | // 사이드바 관련 상태 12 | const [isSidebarOpen, setIsSidebarOpen] = useState(false); 13 | 14 | // 모립셋 허용 서비스 상태 15 | const [isAllowedServiceVisible, setIsAllowedServiceVisible] = useState(false); 16 | 17 | /** 18 | * 사이드바 토글 함수 19 | */ 20 | const toggleSidebar = () => { 21 | setIsSidebarOpen((prev) => !prev); 22 | }; 23 | 24 | /** 25 | * 모립셋 UI 핸들러 26 | */ 27 | const showAllowedServices = () => { 28 | setIsAllowedServiceVisible(true); 29 | }; 30 | 31 | const hideAllowedServices = () => { 32 | setIsAllowedServiceVisible(false); 33 | }; 34 | 35 | return { 36 | // 상태 37 | isSidebarOpen, 38 | isAllowedServiceVisible, 39 | 40 | // 액션 41 | actions: { 42 | toggleSidebar, 43 | showAllowedServices, 44 | hideAllowedServices, 45 | }, 46 | }; 47 | } 48 | -------------------------------------------------------------------------------- /src/pages/TimerPage/utils/timeFormat.ts: -------------------------------------------------------------------------------- 1 | import { formatSeconds } from '@/shared/utils/time'; 2 | 3 | /** 4 | * 타이머 시간 정보를 포맷팅하는 유틸리티 함수 5 | * 표시할 시간 값을 계산하고 포맷팅. 6 | * 7 | * @param timer 현재 타이머 시간 (초 단위) 8 | * @param elapsedTime 서버에서 받아온 총 경과 시간 (초 단위) 9 | * @param totalElapsedTimeOfToday 서버에서 받아온 오늘의 총 작업 시간 (초 단위) 10 | * @returns 포맷팅된 시간 정보 객체 11 | */ 12 | export const getFormattedTimeInfo = (timer: number, elapsedTime: number, totalElapsedTimeOfToday: number) => { 13 | const formattedTimeText = formatSeconds(timer); 14 | 15 | const totalTimeToday = totalElapsedTimeOfToday + (timer > elapsedTime ? timer - elapsedTime : 0); 16 | 17 | const hours = Math.floor(totalTimeToday / 3600); 18 | const minutes = Math.floor((totalTimeToday % 3600) / 60); 19 | 20 | const statusText = hours === 0 ? `오늘 ${minutes}분 몰입 중` : `오늘 ${hours}시간 ${minutes}분 몰입 중`; 21 | 22 | return { 23 | formattedTimeText, 24 | statusText, 25 | hours, 26 | minutes, 27 | seconds: timer % 60, 28 | totalElapsedTimeToday: totalTimeToday, 29 | }; 30 | }; 31 | 32 | /** 33 | * 타이머가 증가한 시간을 계산. 34 | * 선택된 작업에 따라 타이머 증가 시간을 동기화. 35 | * 36 | * @param todoId 할일 ID 37 | * @param todoElapsedTime 할일의 서버 저장 경과 시간 38 | * @param selectedTaskId 현재 선택된 작업 ID 39 | * @param currentTimer 현재 타이머 시간 40 | * @returns 증가한 시간 (초 단위) 41 | */ 42 | export const getTimerIncreasedTime = ( 43 | todoId: number, 44 | todoElapsedTime: number, 45 | selectedTaskId: number | null, 46 | currentTimer: number, 47 | ) => { 48 | if (todoId === selectedTaskId) { 49 | return currentTimer - todoElapsedTime; 50 | } 51 | return 0; 52 | }; 53 | -------------------------------------------------------------------------------- /src/router/ProtectedRoute.tsx: -------------------------------------------------------------------------------- 1 | import { Navigate, Outlet } from 'react-router-dom'; 2 | 3 | import ErrorBoundary from '@/shared/components/ErrorBoundary/ErrorBoundary'; 4 | 5 | import { getAccessToken } from '@/shared/utils/auth'; 6 | import { mapStatusToMessage } from '@/shared/utils/error'; 7 | 8 | import { ROUTES_CONFIG } from './routesConfig'; 9 | 10 | const ProtectedRoute = () => { 11 | const accessToken = getAccessToken(); 12 | if (!accessToken) { 13 | alert(mapStatusToMessage(401)); 14 | return ; 15 | } 16 | 17 | return ( 18 | 19 | 20 | 21 | ); 22 | }; 23 | 24 | export default ProtectedRoute; 25 | -------------------------------------------------------------------------------- /src/router/routesConfig.ts: -------------------------------------------------------------------------------- 1 | export const ROUTES_CONFIG = { 2 | home: { 3 | title: 'Home', 4 | path: '/home', 5 | }, 6 | login: { 7 | title: 'Login', 8 | path: '/', 9 | }, 10 | onboarding: { 11 | title: 'Onboarding', 12 | path: '/onboarding', 13 | }, 14 | allowedService: { 15 | title: 'AllowedService', 16 | path: '/allowedService', 17 | }, 18 | redirect: { 19 | title: 'Redirect', 20 | path: 'auth/redirect', 21 | }, 22 | timer: { 23 | title: 'Timer', 24 | path: '/timer', 25 | }, 26 | }; 27 | -------------------------------------------------------------------------------- /src/shared/apisV2/allowedService/allowedService.keys.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GetAllowedServiceGroupDetailReq, 3 | GetAllowedServiceListReq, 4 | GetRecommendedSitesReq, 5 | } from '@/shared/types/api/allowedService'; 6 | 7 | export const allowedServiceKeys = { 8 | allowedService: ['allowedService'] as const, 9 | allowedServiceList: ({ connectType }: GetAllowedServiceListReq) => 10 | [...allowedServiceKeys.allowedService, 'list', connectType] as const, 11 | allowedServiceGroupDetail: ({ allowedGroupId, connectType }: GetAllowedServiceGroupDetailReq) => 12 | [...allowedServiceKeys.allowedService, 'group', allowedGroupId, connectType] as const, 13 | recommendedSites: ({ allowedGroupId }: GetRecommendedSitesReq) => 14 | [...allowedServiceKeys.allowedService, 'recommendedSites', allowedGroupId] as const, 15 | }; 16 | -------------------------------------------------------------------------------- /src/shared/apisV2/allowedService/allowedService.queries.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@tanstack/react-query'; 2 | 3 | import { 4 | GetAllowedServiceGroupDetailReq, 5 | GetAllowedServiceListReq, 6 | GetRecommendedSitesReq, 7 | } from '@/shared/types/api/allowedService'; 8 | 9 | import { getAllowedServiceGroupDetail, getAllowedServiceList, getRecommendedSites } from './allowedService.api'; 10 | import { allowedServiceKeys } from './allowedService.keys'; 11 | 12 | export const useGetAllowedServiceList = ({ connectType }: GetAllowedServiceListReq) => { 13 | return useQuery({ 14 | queryKey: allowedServiceKeys.allowedServiceList({ connectType }), 15 | queryFn: () => getAllowedServiceList({ connectType }), 16 | }); 17 | }; 18 | 19 | export const useGetAllowedServiceGroupDetail = ({ allowedGroupId, connectType }: GetAllowedServiceGroupDetailReq) => { 20 | return useQuery({ 21 | queryKey: allowedServiceKeys.allowedServiceGroupDetail({ allowedGroupId, connectType }), 22 | queryFn: () => getAllowedServiceGroupDetail({ allowedGroupId, connectType }), 23 | enabled: allowedGroupId !== null, 24 | }); 25 | }; 26 | 27 | export const useGetRecommendedSites = ({ allowedGroupId }: GetRecommendedSitesReq) => { 28 | return useQuery({ 29 | queryKey: allowedServiceKeys.recommendedSites({ allowedGroupId }), 30 | queryFn: () => getRecommendedSites({ allowedGroupId }), 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /src/shared/apisV2/auth/auth.api.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | import { getAccessToken } from '@/shared/utils/auth'; 4 | 5 | import { reissueRes } from '@/shared/types/api/auth'; 6 | 7 | import { authClient } from '@/shared/apisV2/client'; 8 | 9 | const AUTH_ENDPOINT = { 10 | POST_REISSUE_TOKEN: 'api/v2/users/reissue', 11 | POST_LOGOUT: 'api/v2/users/logout', 12 | }; 13 | 14 | export const postReissueToken = async (): Promise => { 15 | const accessToken = getAccessToken(); 16 | 17 | const { data } = await axios.post(AUTH_ENDPOINT.POST_REISSUE_TOKEN, { 18 | headers: { 19 | Authorization: `Bearer ${accessToken}`, 20 | }, 21 | }); 22 | 23 | return data; 24 | }; 25 | 26 | export const postLogout = async () => { 27 | await authClient.post(AUTH_ENDPOINT.POST_LOGOUT); 28 | }; 29 | -------------------------------------------------------------------------------- /src/shared/apisV2/auth/auth.queries.ts: -------------------------------------------------------------------------------- 1 | import { useMutation } from '@tanstack/react-query'; 2 | 3 | import { postLogout } from './auth.api'; 4 | 5 | export const usePostLogout = () => { 6 | return useMutation({ 7 | mutationFn: postLogout, 8 | }); 9 | }; 10 | -------------------------------------------------------------------------------- /src/shared/apisV2/common/common.api.ts: -------------------------------------------------------------------------------- 1 | import { GetUrlInfoReq, GetUrlInfoRes } from '@/shared/types/api/common'; 2 | import { PostToggleTaskStatusReq } from '@/shared/types/api/home'; 3 | 4 | import { authClient } from '../client'; 5 | 6 | export const COMMON_ENDPOINT = { 7 | GET_URL_INFO: 'api/v2/onboard/allowedSite/info', 8 | POST_TOGGLE_TASK_STATUS: 'api/v2/tasks/:taskId/status', 9 | GET_HEART_BEAT: 'api/v2/heart-beat', 10 | }; 11 | 12 | export const getUrlInfo = async ({ siteUrl }: GetUrlInfoReq): Promise => { 13 | const { data } = await authClient.get(COMMON_ENDPOINT.GET_URL_INFO, { params: { siteUrl } }); 14 | return data; 15 | }; 16 | 17 | export const postToggleTaskStatus = async ({ taskId }: PostToggleTaskStatusReq) => { 18 | const { data } = await authClient.post(COMMON_ENDPOINT.POST_TOGGLE_TASK_STATUS.replace(':taskId', String(taskId))); 19 | return data; 20 | }; 21 | 22 | export const getHeartBeat = async () => { 23 | const { data } = await authClient.get(COMMON_ENDPOINT.GET_HEART_BEAT); 24 | return data; 25 | }; 26 | -------------------------------------------------------------------------------- /src/shared/apisV2/common/common.keys.ts: -------------------------------------------------------------------------------- 1 | /* 2 | NOTE: queryKey 작성 방식 3 | 첫 번째 요소: 도메인명(여기서는 'interestArea') 4 | 두 번째 요소: 리소스 (예: 'list', 'detail') 5 | 그 뒤: 해당 액션을 구분하는 파라미터(예: id, filter, req 객체) 6 | */ 7 | 8 | /* 9 | NOTE: queryKey 작성 예시 10 | export const interestAreaKeys = { 11 | all: ['interestArea'] as const, 12 | lists: () => [...interestAreaKeys.all, 'list'] as const, 13 | list: (filter: string) => [...interestAreaKeys.lists(), filter] as const, 14 | detail: (id: number) => [...interestAreaKeys.all, 'detail', id] as const, 15 | post: (req: PostInterestAreaReq) => [...interestAreaKeys.all, 'post', req] as const, 16 | } 17 | */ 18 | 19 | export const commonKeys = { 20 | common: ['common'] as const, 21 | heartBeat: () => [...commonKeys.common, 'heartBeat'] as const, 22 | }; 23 | -------------------------------------------------------------------------------- /src/shared/apisV2/common/common.mutations.ts: -------------------------------------------------------------------------------- 1 | import { useMutation, useQueryClient } from '@tanstack/react-query'; 2 | 3 | import { GetUrlInfoReq, GetUrlInfoRes } from '@/shared/types/api/common'; 4 | import { ApiErrorResponseType } from '@/shared/types/api/error'; 5 | 6 | import { homeKeys } from '../home/home.keys'; 7 | import { timerKeys } from '../timer/timer.keys'; 8 | import { getUrlInfo, postToggleTaskStatus } from './common.api'; 9 | 10 | export const useGetUrlInfo = () => { 11 | return useMutation({ 12 | mutationFn: getUrlInfo, 13 | onSuccess: (response) => { 14 | return response.data; 15 | }, 16 | }); 17 | }; 18 | 19 | export const usePostToggleTaskStatus = () => { 20 | const queryClient = useQueryClient(); 21 | 22 | return useMutation({ 23 | mutationFn: postToggleTaskStatus, 24 | onSuccess: () => { 25 | queryClient.invalidateQueries({ queryKey: timerKeys.timer }); 26 | queryClient.invalidateQueries({ queryKey: homeKeys.task }); 27 | }, 28 | }); 29 | }; 30 | -------------------------------------------------------------------------------- /src/shared/apisV2/common/common.queries.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@tanstack/react-query'; 2 | 3 | import { getHeartBeat } from './common.api'; 4 | import { commonKeys } from './common.keys'; 5 | 6 | export const useGetHeartBeat = () => { 7 | return useQuery({ 8 | queryKey: commonKeys.heartBeat(), 9 | queryFn: getHeartBeat, 10 | refetchInterval: 30000, 11 | refetchIntervalInBackground: true, 12 | }); 13 | }; 14 | -------------------------------------------------------------------------------- /src/shared/apisV2/friends/friends.keys.ts: -------------------------------------------------------------------------------- 1 | export const friendKeys = { 2 | friend: ['friend'] as const, 3 | friendList: () => [...friendKeys.friend, 'list'] as const, 4 | friendRequestList: () => [...friendKeys.friend, 'requestList'] as const, 5 | }; 6 | -------------------------------------------------------------------------------- /src/shared/apisV2/friends/friends.queries.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@tanstack/react-query'; 2 | 3 | import { getFriendList, getFriendRequestList } from './friends.api'; 4 | import { friendKeys } from './friends.keys'; 5 | 6 | export const useGetFriendList = () => { 7 | return useQuery({ 8 | queryKey: friendKeys.friendList(), 9 | queryFn: getFriendList, 10 | }); 11 | }; 12 | 13 | export const useGetFriendRequestList = () => { 14 | return useQuery({ 15 | queryKey: friendKeys.friendRequestList(), 16 | queryFn: getFriendRequestList, 17 | }); 18 | }; 19 | -------------------------------------------------------------------------------- /src/shared/apisV2/home/home.keys.ts: -------------------------------------------------------------------------------- 1 | import { GetCategoryTaskReq, GetWorkTimeReq } from '@/shared/types/api/home'; 2 | 3 | export const homeKeys = { 4 | task: ['task'] as const, 5 | categoryTask: ({ startDate, endDate }: GetCategoryTaskReq) => [...homeKeys.task, startDate, endDate] as const, 6 | workTime: ({ targetDate }: GetWorkTimeReq) => ['workTime', targetDate] as const, 7 | }; 8 | -------------------------------------------------------------------------------- /src/shared/apisV2/home/home.queries.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@tanstack/react-query'; 2 | 3 | import type { GetCategoryTaskReq, GetWorkTimeReq } from '@/shared/types/api/home'; 4 | 5 | import { getCategoryTask, getWorkTime } from './home.api'; 6 | import { homeKeys } from './home.keys'; 7 | 8 | export const useGetCategoryTask = ({ startDate, endDate }: GetCategoryTaskReq) => { 9 | return useQuery({ 10 | queryKey: homeKeys.categoryTask({ startDate, endDate }), 11 | queryFn: () => getCategoryTask({ startDate, endDate }), 12 | }); 13 | }; 14 | 15 | export const useGetWorkTime = ({ targetDate }: GetWorkTimeReq) => { 16 | return useQuery({ 17 | queryKey: homeKeys.workTime({ targetDate }), 18 | queryFn: () => getWorkTime({ targetDate }), 19 | }); 20 | }; 21 | -------------------------------------------------------------------------------- /src/shared/apisV2/onboarding/onboarding.api.ts: -------------------------------------------------------------------------------- 1 | import { GetSuugestedSitesRes, PostInterestAreaReq, PostInterestAreaRes } from '@/shared/types/api/onboarding'; 2 | 3 | import { authClient } from '../client'; 4 | 5 | export const ONBOARDING_URL = { 6 | POST_INTEREST_AREA: 'api/v2/onboard', 7 | GET_SUGGESTED_SITES: 'api/v2/onboard', 8 | }; 9 | 10 | export const postInterestArea = async ({ 11 | name, 12 | colorCode, 13 | allowedSites, 14 | interestArea, 15 | }: PostInterestAreaReq): Promise => { 16 | const { data } = await authClient.post( 17 | ONBOARDING_URL.POST_INTEREST_AREA, 18 | { name, colorCode, allowedSites }, 19 | { 20 | params: { interestArea }, 21 | }, 22 | ); 23 | return data; 24 | }; 25 | 26 | export const getSuggestedSites = async (): Promise => { 27 | const { data } = await authClient.get(ONBOARDING_URL.GET_SUGGESTED_SITES); 28 | return data; 29 | }; 30 | -------------------------------------------------------------------------------- /src/shared/apisV2/onboarding/onboarding.keys.ts: -------------------------------------------------------------------------------- 1 | export const onboardingKeys = { 2 | suggestedSites: ['suggestedSites'] as const, 3 | }; 4 | -------------------------------------------------------------------------------- /src/shared/apisV2/onboarding/onboarding.mutations.ts: -------------------------------------------------------------------------------- 1 | import { useMutation } from '@tanstack/react-query'; 2 | 3 | import { ApiErrorResponseType } from '@/shared/types/api/error'; 4 | import { PostInterestAreaReq, PostInterestAreaRes } from '@/shared/types/api/onboarding'; 5 | 6 | import { postInterestArea } from './onboarding.api'; 7 | 8 | export const usePostInterestArea = () => { 9 | return useMutation({ 10 | mutationFn: postInterestArea, 11 | }); 12 | }; 13 | -------------------------------------------------------------------------------- /src/shared/apisV2/onboarding/onboarding.queries.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@tanstack/react-query'; 2 | 3 | import { getSuggestedSites } from './onboarding.api'; 4 | import { onboardingKeys } from './onboarding.keys'; 5 | 6 | export const useGetSuggestedSites = () => { 7 | return useQuery({ 8 | queryKey: onboardingKeys.suggestedSites, 9 | queryFn: getSuggestedSites, 10 | }); 11 | }; 12 | -------------------------------------------------------------------------------- /src/shared/apisV2/queryClient.ts: -------------------------------------------------------------------------------- 1 | import { QueryClient } from '@tanstack/react-query'; 2 | 3 | export const queryClient = new QueryClient({ 4 | defaultOptions: { 5 | queries: { 6 | throwOnError: true, 7 | }, 8 | }, 9 | }); 10 | -------------------------------------------------------------------------------- /src/shared/apisV2/setting/setting.api.ts: -------------------------------------------------------------------------------- 1 | import { GetProfileRes, PutChangeProfileReq } from '@/shared/types/api/setting'; 2 | 3 | import { authClient } from '../client'; 4 | 5 | const SETTING_URL = { 6 | GET_PROFILE: 'api/v2/profiles', 7 | PUT_CHANGE_PROFILE: 'api/v2/profiles', 8 | DELETE_ACCOUNT: 'api/v2/users/withdraw', 9 | }; 10 | 11 | export const getProfile = async (): Promise => { 12 | const { data } = await authClient.get(SETTING_URL.GET_PROFILE); 13 | return data; 14 | }; 15 | 16 | export const putChangeProfile = async ({ name, imageUrl, isPushEnabled }: PutChangeProfileReq): Promise => { 17 | await authClient.put(SETTING_URL.PUT_CHANGE_PROFILE, { name, imageUrl, isPushEnabled }); 18 | }; 19 | 20 | export const deleteAccount = async () => { 21 | const { data } = await authClient.delete(SETTING_URL.DELETE_ACCOUNT); 22 | return data; 23 | }; 24 | -------------------------------------------------------------------------------- /src/shared/apisV2/setting/setting.keys.ts: -------------------------------------------------------------------------------- 1 | export const settingKeys = { 2 | setting: ['setting'] as const, 3 | }; 4 | -------------------------------------------------------------------------------- /src/shared/apisV2/setting/setting.mutations.ts: -------------------------------------------------------------------------------- 1 | import { useMutation, useQueryClient } from '@tanstack/react-query'; 2 | 3 | import { reloginWithoutLogout } from '@/shared/utils/auth'; 4 | 5 | import { deleteAccount, putChangeProfile } from './setting.api'; 6 | import { settingKeys } from './setting.keys'; 7 | 8 | export const usePutChangeProfile = () => { 9 | const queryClient = useQueryClient(); 10 | 11 | return useMutation({ 12 | mutationFn: putChangeProfile, 13 | onSuccess: () => { 14 | queryClient.invalidateQueries({ queryKey: settingKeys.setting }); 15 | }, 16 | }); 17 | }; 18 | 19 | export const useDeleteAccount = () => { 20 | const queryClient = useQueryClient(); 21 | 22 | return useMutation({ 23 | mutationFn: deleteAccount, 24 | onSuccess: () => { 25 | queryClient.invalidateQueries(); 26 | reloginWithoutLogout(); 27 | }, 28 | }); 29 | }; 30 | -------------------------------------------------------------------------------- /src/shared/apisV2/setting/setting.queries.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@tanstack/react-query'; 2 | 3 | import { getProfile } from './setting.api'; 4 | import { settingKeys } from './setting.keys'; 5 | 6 | export const useGetProfile = () => { 7 | return useQuery({ 8 | queryKey: settingKeys.setting, 9 | queryFn: getProfile, 10 | }); 11 | }; 12 | -------------------------------------------------------------------------------- /src/shared/apisV2/timer/timer.keys.ts: -------------------------------------------------------------------------------- 1 | import { GetSelectedTimerTaskReq, GetTimerHeartBeatReq, GetTimerTodosReq } from '@/shared/types/api/timer'; 2 | 3 | export const timerKeys = { 4 | timer: ['timer'] as const, 5 | todos: ({ targetDate }: GetTimerTodosReq) => [...timerKeys.timer, targetDate], 6 | friends: () => [...timerKeys.timer, 'friends'], 7 | popover: () => [...timerKeys.timer, 'popover'], 8 | selectedTimerTask: ({ targetDate }: GetSelectedTimerTaskReq) => [...timerKeys.timer, 'selectedTimerTask', targetDate], 9 | timerHeartBeat: ({ targetDate }: GetTimerHeartBeatReq) => [...timerKeys.timer, 'heartBeat', targetDate], 10 | }; 11 | -------------------------------------------------------------------------------- /src/shared/apisV2/timer/timer.mutations.ts: -------------------------------------------------------------------------------- 1 | import { useMutation, useQueryClient } from '@tanstack/react-query'; 2 | 3 | import { PostApplyAllowedServiceGroupReq } from '@/shared/types/api/timer'; 4 | 5 | import { postApplyAllowedServiceGroup, postSelectTimerTask, postTimerPause, postTimerRun } from './timer.api'; 6 | import { timerKeys } from './timer.keys'; 7 | 8 | export const usePostApplyAllowedServiceGroup = ({ allowedGroupIdList }: PostApplyAllowedServiceGroupReq) => { 9 | const queryClient = useQueryClient(); 10 | 11 | return useMutation({ 12 | mutationFn: () => postApplyAllowedServiceGroup({ allowedGroupIdList }), 13 | onSuccess: () => { 14 | queryClient.invalidateQueries({ queryKey: timerKeys.popover() }); 15 | }, 16 | }); 17 | }; 18 | 19 | export const usePostTimerRun = () => { 20 | const queryClient = useQueryClient(); 21 | 22 | return useMutation({ 23 | mutationFn: postTimerRun, 24 | onSuccess: () => { 25 | queryClient.invalidateQueries({ queryKey: timerKeys.timer }); 26 | }, 27 | }); 28 | }; 29 | 30 | export const usePostTimerPause = () => { 31 | const queryClient = useQueryClient(); 32 | 33 | return useMutation({ 34 | mutationFn: postTimerPause, 35 | onSuccess: () => { 36 | queryClient.invalidateQueries({ queryKey: timerKeys.timer }); 37 | }, 38 | }); 39 | }; 40 | 41 | export const usePostSelectTimerTask = () => { 42 | const queryClient = useQueryClient(); 43 | 44 | return useMutation({ 45 | mutationFn: postSelectTimerTask, 46 | onSuccess: () => { 47 | queryClient.invalidateQueries({ queryKey: timerKeys.timer }); 48 | }, 49 | }); 50 | }; 51 | -------------------------------------------------------------------------------- /src/shared/assets/images/example.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/morib-in/Morib-Client/e855b115b93b9f35a569fc7dab7f7daf41101cb6/src/shared/assets/images/example.jpg -------------------------------------------------------------------------------- /src/shared/assets/images/img_timer_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/morib-in/Morib-Client/e855b115b93b9f35a569fc7dab7f7daf41101cb6/src/shared/assets/images/img_timer_bg.png -------------------------------------------------------------------------------- /src/shared/assets/images/login_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/morib-in/Morib-Client/e855b115b93b9f35a569fc7dab7f7daf41101cb6/src/shared/assets/images/login_background.png -------------------------------------------------------------------------------- /src/shared/assets/images/profile_image1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/morib-in/Morib-Client/e855b115b93b9f35a569fc7dab7f7daf41101cb6/src/shared/assets/images/profile_image1.png -------------------------------------------------------------------------------- /src/shared/assets/images/profile_image2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/morib-in/Morib-Client/e855b115b93b9f35a569fc7dab7f7daf41101cb6/src/shared/assets/images/profile_image2.png -------------------------------------------------------------------------------- /src/shared/assets/images/profile_image3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/morib-in/Morib-Client/e855b115b93b9f35a569fc7dab7f7daf41101cb6/src/shared/assets/images/profile_image3.png -------------------------------------------------------------------------------- /src/shared/assets/images/profile_image4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/morib-in/Morib-Client/e855b115b93b9f35a569fc7dab7f7daf41101cb6/src/shared/assets/images/profile_image4.png -------------------------------------------------------------------------------- /src/shared/assets/svgs/404.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/shared/assets/svgs/add_btn.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/shared/assets/svgs/arrow_circle_up_right.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/shared/assets/svgs/arrow_right.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/shared/assets/svgs/bell.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/shared/assets/svgs/btn_add.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/shared/assets/svgs/btn_arrow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/shared/assets/svgs/btn_arrow_bgNone.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/shared/assets/svgs/btn_cal.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/shared/assets/svgs/btn_cal_black.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/shared/assets/svgs/btn_hamburger.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/shared/assets/svgs/btn_home.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/shared/assets/svgs/btn_list.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/shared/assets/svgs/btn_moribset_active.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/shared/assets/svgs/btn_moribset_default.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/shared/assets/svgs/btn_today.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/shared/assets/svgs/button_inputSuccess.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/shared/assets/svgs/check_box_blank.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/shared/assets/svgs/check_box_fill.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/shared/assets/svgs/common/ic_logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/shared/assets/svgs/common/ic_meatball_default.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/shared/assets/svgs/connection_icon.svg: -------------------------------------------------------------------------------- 1 | 8 | 9 | -------------------------------------------------------------------------------- /src/shared/assets/svgs/default_profile.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/shared/assets/svgs/default_url_favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/shared/assets/svgs/defaultpause.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/shared/assets/svgs/defaultplay.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/shared/assets/svgs/description.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/shared/assets/svgs/disabled_dropdown.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/shared/assets/svgs/dropIcon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/shared/assets/svgs/elipse.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/shared/assets/svgs/error_input.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/shared/assets/svgs/friend_delBtn.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/shared/assets/svgs/gradient_circle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/shared/assets/svgs/header_delBtn.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/shared/assets/svgs/home/ic_plus.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/shared/assets/svgs/home_default_icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/shared/assets/svgs/hoverpause.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/shared/assets/svgs/hoverplay.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/shared/assets/svgs/ic_back_btn.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/shared/assets/svgs/ic_delete_alert.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/shared/assets/svgs/ic_description.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/shared/assets/svgs/ic_folder.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/shared/assets/svgs/ic_line.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/shared/assets/svgs/ic_logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/shared/assets/svgs/ic_minus.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/shared/assets/svgs/ic_pencil.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/shared/assets/svgs/icon_clock.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/shared/assets/svgs/large_plus.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/shared/assets/svgs/logo_icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/shared/assets/svgs/mail.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/shared/assets/svgs/mingcute_time-fill.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/shared/assets/svgs/mingcute_time-line.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/shared/assets/svgs/minus_btn.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/shared/assets/svgs/more_friend.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/shared/assets/svgs/moribSet.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/shared/assets/svgs/onboarding/ic_business.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/shared/assets/svgs/onboarding/ic_design.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/shared/assets/svgs/onboarding/ic_development.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/shared/assets/svgs/onboarding/ic_marketing.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/shared/assets/svgs/onboarding/ic_planning.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/shared/assets/svgs/onboarding/ic_studying.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/shared/assets/svgs/plus.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/shared/assets/svgs/popover_add_category.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/shared/assets/svgs/selected_number_icon.svg: -------------------------------------------------------------------------------- 1 | 8 | 9 | -------------------------------------------------------------------------------- /src/shared/assets/svgs/timer/ic_check_box_active.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/shared/assets/svgs/timer/ic_check_box_inactive.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/shared/assets/svgs/timer/ic_deactivated_clock.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/shared/assets/svgs/timer/ic_online.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/shared/assets/svgs/timer/ic_timer_inner_circle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/shared/assets/svgs/todo_meatball_press.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/shared/assets/svgs/todo_toggle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/shared/assets/svgs/tooltip_triangle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/shared/assets/svgs/triangle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/shared/assets/svgs/upIcon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/shared/assets/svgs/user_circle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/shared/components/AutoFixedGrid/AutoFixedGrid.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react'; 2 | 3 | interface AutoFixedGridRootProps { 4 | type: 'onboarding' | 'home' | 'allowedService' | 'layout'; 5 | children: ReactNode; 6 | className?: string; 7 | } 8 | 9 | const AutoFixedGridRoot = ({ type, children, className }: AutoFixedGridRootProps) => { 10 | // NOTE: tailwind에서 빌드 타임에 동적 스타일 클래스를 인식하지 못하기 때문에 이미 정의된 클래스를 사용해야함 11 | const gridType = { 12 | layout: 'grid-cols-[7.4rem,1fr]', 13 | onboarding: 'grid-cols-[1fr,42rem]', 14 | home: 'grid-cols-[1fr,31.6rem]', 15 | allowedService: 'grid-cols-[31.6rem,1fr]', 16 | }; 17 | 18 | return
    {children}
    ; 19 | }; 20 | 21 | interface SlotProps { 22 | children: ReactNode; 23 | className?: string; 24 | } 25 | 26 | const Slot = ({ children, className }: SlotProps) => { 27 | return
    {children}
    ; 28 | }; 29 | 30 | export const AutoFixedGrid = Object.assign(AutoFixedGridRoot, { Slot }); 31 | 32 | export default AutoFixedGrid; 33 | -------------------------------------------------------------------------------- /src/shared/components/ButtonArrowSVG/ButtonArrowSVG.tsx: -------------------------------------------------------------------------------- 1 | import { ButtonHTMLAttributes } from 'react'; 2 | 3 | import { Direction } from '@/shared/types/global'; 4 | 5 | import ButtonArrowIcon from '@/shared/assets/svgs/btn_arrow.svg?react'; 6 | 7 | interface ArrowSVGButtonProps extends ButtonHTMLAttributes { 8 | direction: Direction; 9 | bg?: boolean; 10 | } 11 | 12 | const ButtonArrowSVG = ({ direction, bg = true, ...props }: ArrowSVGButtonProps) => { 13 | const backgroundStyle = bg ? 'bg-gray-bg-03 hover:bg-gray-bg-05 rounded-full' : ''; 14 | 15 | let rotationStyle = ''; 16 | 17 | switch (direction) { 18 | case Direction.LEFT: 19 | rotationStyle = 'rotate-90'; 20 | break; 21 | case Direction.RIGHT: 22 | rotationStyle = '-rotate-90'; 23 | break; 24 | case Direction.UP: 25 | rotationStyle = 'rotate-180'; 26 | break; 27 | case Direction.DOWN: 28 | rotationStyle = ''; 29 | break; 30 | } 31 | return ( 32 | 35 | ); 36 | }; 37 | 38 | export default ButtonArrowSVG; 39 | -------------------------------------------------------------------------------- /src/shared/components/ButtonDropdownOptions/ButtonDropdownOptions.tsx: -------------------------------------------------------------------------------- 1 | import { ButtonHTMLAttributes, ReactNode } from 'react'; 2 | 3 | interface OptionsBtnProps extends ButtonHTMLAttributes { 4 | children: ReactNode; 5 | } 6 | 7 | const DropdownOptionsBtn = ({ children, ...props }: OptionsBtnProps) => { 8 | return ( 9 | 16 | ); 17 | }; 18 | 19 | export default DropdownOptionsBtn; 20 | -------------------------------------------------------------------------------- /src/shared/components/ButtonHomeLarge/ButtonHomeLarge.tsx: -------------------------------------------------------------------------------- 1 | import { ButtonHTMLAttributes, ReactNode } from 'react'; 2 | 3 | import { HomeLargeBtnVariant } from '@/shared/types/global'; 4 | 5 | interface ButtonHomeLarge extends ButtonHTMLAttributes { 6 | variant: HomeLargeBtnVariant; 7 | children: ReactNode; 8 | className?: string; 9 | } 10 | 11 | const btnVariant = { 12 | middle: 'px-[4rem] py-[1.4rem] ', 13 | large: 'px-[6.2rem] py-[2rem] ', 14 | }; 15 | 16 | const ButtonHomeLarge = ({ variant, className, disabled = false, children, ...props }: ButtonHomeLarge) => { 17 | const defaultStyle = 'subhead-bold-20 flex items-center justify-center rounded-[0.8rem] flex-shrink-0 '; 18 | const buttonStyle = disabled 19 | ? 'bg-gray-bg-05 text-gray-04 ' 20 | : 'bg-main-gra-01 text-gray-01 hover:bg-main-gra-hover active:bg-main-gra-press '; 21 | 22 | return ( 23 | 26 | ); 27 | }; 28 | 29 | export default ButtonHomeLarge; 30 | -------------------------------------------------------------------------------- /src/shared/components/ButtonRadius8/ButtonRadius8.tsx: -------------------------------------------------------------------------------- 1 | import { ButtonHTMLAttributes } from 'react'; 2 | 3 | const commonStyles = 'flex flex-shrink-0 items-center justify-center rounded-[8px] subhead-semibold-20'; 4 | 5 | const paddingVariant = { 6 | md: 'px-[4rem] py-[1.4rem]', 7 | lg: 'px-[6.2rem] py-[2rem]', 8 | }; 9 | 10 | const bgVariant = { 11 | active: 'bg-main-gra-01 hover:bg-main-gra-hover active:bg-main-gra-press', 12 | disabled: 'bg-gray-bg-05', 13 | }; 14 | 15 | const textVariant = { 16 | active: 'text-gray-01', 17 | disabled: 'text-gray-04', 18 | }; 19 | 20 | type ButtonRadius8Props = ButtonHTMLAttributes; 21 | 22 | const ButtonRadius8Md = ({ ...props }: ButtonRadius8Props) => { 23 | const status = props.disabled ? 'disabled' : 'active'; 24 | 25 | return ( 26 | 29 | ); 30 | }; 31 | 32 | const ButtonRadius8Lg = ({ ...props }: ButtonRadius8Props) => { 33 | const status = props.disabled ? 'disabled' : 'active'; 34 | 35 | return ( 36 | 39 | ); 40 | }; 41 | 42 | export const ButtonRadius8 = { 43 | Md: ButtonRadius8Md, 44 | Lg: ButtonRadius8Lg, 45 | }; 46 | 47 | export default ButtonRadius8; 48 | -------------------------------------------------------------------------------- /src/shared/components/ButtonStatusToggle/ButtonStatusToggle.tsx: -------------------------------------------------------------------------------- 1 | interface ButtonStatusToggleProps { 2 | isToggleOn: boolean; 3 | onToggle: () => void; 4 | } 5 | 6 | const ButtonStatusToggle = ({ isToggleOn, onToggle }: ButtonStatusToggleProps) => { 7 | return ( 8 |
    9 |
    12 |
    15 |
    16 | ); 17 | }; 18 | 19 | export default ButtonStatusToggle; 20 | -------------------------------------------------------------------------------- /src/shared/components/ButtonTodayToggle/ButtonTodoToggle.tsx: -------------------------------------------------------------------------------- 1 | import { ButtonHTMLAttributes } from 'react'; 2 | 3 | import TodoToggleIcon from '@/shared/assets/svgs/todo_toggle.svg?react'; 4 | 5 | interface ButtonTodoToggle extends ButtonHTMLAttributes { 6 | isCompleted?: boolean; 7 | isToggled: boolean; 8 | } 9 | 10 | const ButtonTodoToggle = ({ children, isCompleted = false, isToggled, ...props }: ButtonTodoToggle) => { 11 | const title = isCompleted ? '할 일 목록' : '완료된 일'; 12 | const ToggleIcon = isToggled ? : ; 13 | 14 | return ( 15 | <> 16 | 20 | {isToggled && children} 21 | 22 | ); 23 | }; 24 | 25 | export default ButtonTodoToggle; 26 | -------------------------------------------------------------------------------- /src/shared/components/Calendar/ButtonCalendarAddRoutine/ButtonCalendarAddRoutine.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | 3 | const daysOfWeek = ['일', '월', '화', '수', '목', '금', '토']; 4 | 5 | const ButtonCalendarAddRoutine = () => { 6 | const defaultStyle = 7 | 'flex rounded-[5px] detail_reg_12 items-center text-center justify-center text-white hover:bg-gray-bg-06 flex h-[3.1rem] w-[2.6rem] bg-gray-07 px-[1rem] py-[0.5rem]'; 8 | const [selectedDays, setSelectedDays] = useState([]); 9 | 10 | const toggleDaySelection = (day: string) => { 11 | if (selectedDays.includes(day)) { 12 | setSelectedDays(selectedDays.filter((selected) => selected !== day)); 13 | } else { 14 | setSelectedDays([...selectedDays, day]); 15 | } 16 | }; 17 | 18 | return ( 19 |
    20 | {daysOfWeek.map((day) => ( 21 | 28 | ))} 29 |
    30 | ); 31 | }; 32 | 33 | export default ButtonCalendarAddRoutine; 34 | -------------------------------------------------------------------------------- /src/shared/components/Calendar/HeaderCalendar/HeaderCalendar.tsx: -------------------------------------------------------------------------------- 1 | import dayjs, { Dayjs } from 'dayjs'; 2 | 3 | import React from 'react'; 4 | 5 | import { formatDateInfo } from '@/shared/utils/calendar/index'; 6 | 7 | import { WEEK_DAYS } from '@/shared/constants/weekDays'; 8 | 9 | import ArrowIcon from '@/shared/assets/svgs/btn_arrow_bgNone.svg?react'; 10 | 11 | interface CustomHeaderProps { 12 | date: Dayjs; 13 | decreaseMonth: () => void; 14 | increaseMonth: () => void; 15 | prevMonthButtonDisabled: boolean; 16 | nextMonthButtonDisabled: boolean; 17 | } 18 | 19 | const HeaderCalendar: React.FC = ({ 20 | date, 21 | decreaseMonth, 22 | increaseMonth, 23 | prevMonthButtonDisabled, 24 | nextMonthButtonDisabled, 25 | }) => { 26 | const { year, month } = formatDateInfo(dayjs(date)); 27 | return ( 28 |
    29 |
    30 |

    31 | {year}년 {month}월 32 |

    33 | 36 | 39 |
    40 |
    41 |
    42 | {WEEK_DAYS.map((day) => ( 43 | {day} 44 | ))} 45 |
    46 |
    47 |
    48 | ); 49 | }; 50 | export default HeaderCalendar; 51 | -------------------------------------------------------------------------------- /src/shared/components/Calendar/calendar.css: -------------------------------------------------------------------------------- 1 | /* 스크린 리더 속성 없애기 */ 2 | .react-datepicker__aria-live { 3 | display: none; 4 | } 5 | 6 | /* 이전 달, 다음 달에 해당하는 날짜는 표시되지 않도록 */ 7 | .react-datepicker__day--outside-month { 8 | cursor: default; 9 | @apply text-gray-03; 10 | } 11 | 12 | /* 달 셀 크기 설정 */ 13 | .react-datepicker__month { 14 | @apply flex-col w-[25.5rem] mx-[1rem] mb-[0.5rem] items-start self-stretch; 15 | } 16 | 17 | /* days (week)의 각 행을 가로로 나란히 배치 */ 18 | .react-datepicker__week { 19 | @apply flex mb-[0.5rem]; 20 | } 21 | 22 | /* 날짜 셀 크기 설정 */ 23 | .react-datepicker__day { 24 | @apply relative w-[3.7rem] h-[3.7rem] text-white flex items-center justify-center; 25 | } 26 | 27 | /* 오늘 날짜 스타일 설정 */ 28 | .react-datepicker__day--today { 29 | @apply font-bold text-mint-01; 30 | } 31 | 32 | /* 선택된 날짜 스타일 설정 */ 33 | .react-datepicker__day--selected { 34 | @apply bg-mint-01 text-black rounded-full text-[18px] font-bold; 35 | } 36 | 37 | /* 호버된 날짜 스타일 설정 */ 38 | .react-datepicker__day:hover { 39 | cursor: default; 40 | @apply bg-gray-bg-04 rounded-full; 41 | } 42 | 43 | /* 선택된 날짜 호버 시 스타일 설정 */ 44 | .react-datepicker__day--selected:hover, 45 | .react-datepicker__day--pressed:hover { 46 | @apply bg-mint-01 text-black rounded-full; 47 | } 48 | 49 | /* 범위 선택 중일 때의 스타일 */ 50 | .react-datepicker__day--in-selecting-range, 51 | .react-datepicker__day--in-range { 52 | @apply bg-date-active rounded-full; 53 | } 54 | 55 | /* 범위 선택 중일 때의 호버 스타일 */ 56 | .react-datepicker__day--in-selecting-range:hover { 57 | @apply rounded-full; 58 | } 59 | 60 | /* 범위 시작일 스타일 */ 61 | .react-datepicker__day--selecting-range-start, 62 | .react-datepicker__day--range-start { 63 | @apply bg-mint-01 text-gray-bg-06 rounded-full text-[18px] font-bold; 64 | } 65 | 66 | /* 범위 종료일 스타일 */ 67 | .react-datepicker__day--range-end { 68 | @apply bg-mint-01 text-black rounded-full text-[18px] font-bold; 69 | } 70 | -------------------------------------------------------------------------------- /src/shared/components/CircleColorIcon/CircleColorIcon.tsx: -------------------------------------------------------------------------------- 1 | interface CircleColorIconProps { 2 | color: string; 3 | size?: string; 4 | onClick?: () => void; 5 | } 6 | 7 | const CircleColorIcon = ({ color, size, onClick }: CircleColorIconProps) => { 8 | return 22 | ); 23 | }; 24 | 25 | export default ButtonRequestAction; 26 | -------------------------------------------------------------------------------- /src/shared/components/ModalContentsFriends/FriendRequest/FriendsListRequested/FriendsListRequested.tsx: -------------------------------------------------------------------------------- 1 | import { HtmlHTMLAttributes, ReactNode } from 'react'; 2 | 3 | import type { FriendRequestListItemType } from '@/shared/types/friend'; 4 | 5 | import FriendUserProfile from '../../FriendUserProfile/FriendUserProfile'; 6 | 7 | interface FriendsListRequestedRootProps extends HtmlHTMLAttributes { 8 | children: ReactNode; 9 | } 10 | 11 | const FriendsListRequestedRoot = ({ children, ...props }: FriendsListRequestedRootProps) => { 12 | return ( 13 |
    17 | {children} 18 |
    19 | ); 20 | }; 21 | 22 | interface FriendListRequestedItemProp extends FriendRequestListItemType { 23 | children: ReactNode; 24 | } 25 | 26 | const FriendsListRequestedItem = ({ children, ...props }: FriendListRequestedItemProp) => { 27 | return ( 28 |
  • 29 |
    30 | 31 |
    32 |

    {props.name}

    33 |

    {props.email}

    34 |
    35 |
    36 |
    {children}
    37 |
  • 38 | ); 39 | }; 40 | 41 | interface FriendsListRequestedEmptyStateProps { 42 | children: ReactNode; 43 | } 44 | 45 | const FriendsListRequestedEmptyState = ({ children }: FriendsListRequestedEmptyStateProps) => { 46 | return
    {children}
    ; 47 | }; 48 | 49 | const FriendsListRequested = Object.assign(FriendsListRequestedRoot, { 50 | Item: FriendsListRequestedItem, 51 | EmptyState: FriendsListRequestedEmptyState, 52 | }); 53 | 54 | export default FriendsListRequested; 55 | -------------------------------------------------------------------------------- /src/shared/components/ModalContentsFriends/FriendUserProfile/FriendUserProfile.tsx: -------------------------------------------------------------------------------- 1 | import { ButtonHTMLAttributes } from 'react'; 2 | 3 | import ConnectionIcon from '@/shared/assets/svgs/connection_icon.svg?react'; 4 | import defaultPorfileIcon from '@/shared/assets/svgs/default_profile.svg'; 5 | 6 | interface FriendUserProfile extends ButtonHTMLAttributes { 7 | isConnecting?: boolean; 8 | isSelectedUser?: boolean; 9 | imgSrc: string; 10 | } 11 | 12 | const FriendUserProfile = ({ imgSrc, isConnecting = false }: FriendUserProfile) => { 13 | return ( 14 |
    15 |
    16 | 친구 캐러셀 이미지 21 | {isConnecting && ( 22 | 23 | )} 24 |
    25 |
    26 | ); 27 | }; 28 | 29 | export default FriendUserProfile; 30 | -------------------------------------------------------------------------------- /src/shared/components/ModalContentsFriends/FriendsList/FriendsList.tsx: -------------------------------------------------------------------------------- 1 | import { useGetFriendList } from '@/shared/apisV2/friends/friends.queries'; 2 | 3 | import ButtonRadius8 from '../../ButtonRadius8/ButtonRadius8'; 4 | import FriendInfo from './FriendsInfo/FriendInfo'; 5 | 6 | interface FriendsListProps { 7 | changeTabToFriendRequest: () => void; 8 | } 9 | 10 | const FriendsList = ({ changeTabToFriendRequest }: FriendsListProps) => { 11 | const { data: friendList } = useGetFriendList(); 12 | 13 | return ( 14 |
    15 | {friendList && friendList?.data.length > 0 ? ( 16 |
    17 |
    18 |
    사용자
    19 |
    현재 상태
    20 |
    오늘의 누적 집중 시간
    21 |
    22 |
      23 | 24 |
    25 |
    26 | ) : ( 27 |
    28 |

    함께 몰입할 친구를 추가해보세요!

    29 | 친구 추가하기 30 |
    31 | )} 32 |
    33 | ); 34 | }; 35 | 36 | export default FriendsList; 37 | -------------------------------------------------------------------------------- /src/shared/components/ModalWrapper/ModalWrapper.tsx: -------------------------------------------------------------------------------- 1 | import { MouseEvent, ReactNode, forwardRef, useImperativeHandle, useRef, useState } from 'react'; 2 | import { createPortal } from 'react-dom'; 3 | 4 | import './styles/dialog.css'; 5 | 6 | interface ModalWrapperProps { 7 | children: (props: { isModalOpen: boolean }) => ReactNode; 8 | backdrop?: boolean; 9 | } 10 | 11 | export interface ModalWrapperRef { 12 | open: () => void; 13 | close: () => void; 14 | } 15 | 16 | const ModalWrapper = forwardRef(function Modal( 17 | { children, backdrop = false }, 18 | ref, 19 | ) { 20 | const [isModalOpen, setIsModalOpen] = useState(false); 21 | const dialog = useRef(null); 22 | 23 | useImperativeHandle(ref, () => ({ 24 | open() { 25 | dialog.current?.showModal(); 26 | setIsModalOpen(true); 27 | }, 28 | close() { 29 | dialog.current?.close(); 30 | setIsModalOpen(false); 31 | }, 32 | })); 33 | const modalElement = document.getElementById('modal'); 34 | 35 | const handleClick = (e: MouseEvent) => { 36 | if (e.target === dialog.current) { 37 | dialog.current?.close(); 38 | setIsModalOpen(false); 39 | } 40 | }; 41 | 42 | if (!modalElement) { 43 | return null; 44 | } 45 | 46 | const content = 47 | typeof children === 'function' 48 | ? (children as (props: { isModalOpen: boolean }) => ReactNode)({ isModalOpen }) 49 | : children; 50 | 51 | return createPortal( 52 | 57 | {content} 58 | , 59 | modalElement, 60 | ); 61 | }); 62 | 63 | export default ModalWrapper; 64 | -------------------------------------------------------------------------------- /src/shared/components/ModalWrapper/styles/dialog.css: -------------------------------------------------------------------------------- 1 | .custom-dialog::backdrop { 2 | background-color: rgba(0, 0, 0, 0.7); 3 | } 4 | 5 | .custom-dialog:not(.with-backdrop)::backdrop { 6 | background-color: transparent; 7 | } -------------------------------------------------------------------------------- /src/shared/components/NotificationPanel/NotificationPanel.tsx: -------------------------------------------------------------------------------- 1 | import { forwardRef } from 'react'; 2 | 3 | const NotificationPanel = forwardRef((_, ref) => { 4 | return ( 5 |
    9 |

    알림

    10 | 11 |
    12 |

    아직 받은 알림이 없어요.

    13 |
    14 |
    15 | ); 16 | }); 17 | 18 | NotificationPanel.displayName = 'NotificationPanel'; 19 | 20 | export default NotificationPanel; 21 | -------------------------------------------------------------------------------- /src/shared/components/Portal/Portal.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react'; 2 | import { createPortal } from 'react-dom'; 3 | 4 | interface PortalProps { 5 | elementId: 'modal' | 'overlay'; 6 | children: ReactNode; 7 | } 8 | 9 | const Portal = ({ elementId, children }: PortalProps) => { 10 | const modalElement = document.getElementById(elementId); 11 | 12 | if (!modalElement) return null; 13 | 14 | return createPortal(children, modalElement); 15 | }; 16 | 17 | export default Portal; 18 | -------------------------------------------------------------------------------- /src/shared/components/Spacer/Spacer.tsx: -------------------------------------------------------------------------------- 1 | import { ElementType, ReactNode } from 'react'; 2 | 3 | interface SpacerProps { 4 | as?: T; 5 | className?: string; 6 | children?: ReactNode; 7 | } 8 | 9 | // NOTE: 남은 공간 만큼을 차지하게 하는 컴포넌트 Spacer -> overflow를 통해서 남은 넓이 만큼의 스크롤 생성 가능 10 | const SpacerRoot = ({ as, className, children }: SpacerProps) => { 11 | const Component = as || 'div'; 12 | return {children}; 13 | }; 14 | 15 | const SpacerWidth = ({ as, className, children }: SpacerProps) => { 16 | const Component = as || 'div'; 17 | return {children}; 18 | }; 19 | 20 | const SpacerHeight = ({ as, className, children }: SpacerProps) => { 21 | const Component = as || 'div'; 22 | return {children}; 23 | }; 24 | 25 | export const Spacer = Object.assign(SpacerRoot, { 26 | Width: SpacerWidth, 27 | Height: SpacerHeight, 28 | }); 29 | 30 | export default Spacer; 31 | -------------------------------------------------------------------------------- /src/shared/constants/btnText.ts: -------------------------------------------------------------------------------- 1 | export const LARGE_BTN_TEXT = { 2 | CREATE_CATEGORY: '작업 카테고리 만들기', 3 | ADD_TODAY_TODO: '오늘의 할 일 추가', 4 | START_TIMER: '타이머 시작하기', 5 | }; 6 | 7 | export const SMALL_BTN_TEXT = { 8 | CANCEL: '취소', 9 | COMPLETION: '완료', 10 | MODIFICATION: '수정', 11 | }; 12 | -------------------------------------------------------------------------------- /src/shared/constants/colorPalette.ts: -------------------------------------------------------------------------------- 1 | export const COLOR_PALETTE = [ 2 | 'bg-color-palette-red', 3 | 'bg-color-palette-yellow1', 4 | 'bg-color-palette-yellow2', 5 | 'bg-color-palette-green1', 6 | 'bg-color-palette-green2', 7 | 'bg-color-palette-green3', 8 | 'bg-mint-01', 9 | 'bg-color-palette-blue1', 10 | 'bg-color-palette-blue2', 11 | 'bg-color-palette-purple1', 12 | 'bg-color-palette-pink', 13 | 'bg-gray-bg-07', 14 | ] as const; 15 | 16 | export const COLOR_PALETTE_MAP = { 17 | '#FF8080': 'bg-color-palette-red', 18 | '#FFB62F': 'bg-color-palette-yellow1', 19 | '#FFF787': 'bg-color-palette-yellow2', 20 | '#B6FFA5': 'bg-color-palette-green1', 21 | '#5CE082': 'bg-color-palette-green2', 22 | '#179F62': 'bg-color-palette-green3', 23 | '#06FFD2': 'bg-mint-01', 24 | '#27C5FF': 'bg-color-palette-blue1', 25 | '#3D6DFF': 'bg-color-palette-blue2', 26 | '#7742FF': 'bg-color-palette-purple1', 27 | '#FF74F8': 'bg-color-palette-pink', 28 | '#868C93': 'bg-gray-bg-07', 29 | } as const; 30 | -------------------------------------------------------------------------------- /src/shared/constants/emailRegex.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line no-useless-escape 2 | export const emailRegex = /^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/; 3 | -------------------------------------------------------------------------------- /src/shared/constants/error.ts: -------------------------------------------------------------------------------- 1 | export const SHOULD_HANDLE_ERROR = [401, 403, 404, 429]; 2 | 3 | export const ERROR_CODES = { 4 | UNAUTHORIZED: 401, 5 | FORBIDDEN: 403, 6 | NOT_FOUND: 404, 7 | TOO_MANY_REQUESTS: 429, 8 | INTERNAL_SERVER_ERROR: 500, 9 | SERVICE_UNAVAILABLE: 503, 10 | } as const; 11 | -------------------------------------------------------------------------------- /src/shared/constants/fields.ts: -------------------------------------------------------------------------------- 1 | import BusinessIcon from '@/shared/assets/svgs/onboarding/ic_business.svg'; 2 | import DesignIcon from '@/shared/assets/svgs/onboarding/ic_design.svg'; 3 | import DevelopmentIcon from '@/shared/assets/svgs/onboarding/ic_development.svg'; 4 | import MarketingIcon from '@/shared/assets/svgs/onboarding/ic_marketing.svg'; 5 | import PlanningIcon from '@/shared/assets/svgs/onboarding/ic_planning.svg'; 6 | import StudyIcon from '@/shared/assets/svgs/onboarding/ic_studying.svg'; 7 | 8 | export const FIELDS = ['비즈니스', '디자인', '마케팅', '기획', '공부', '개발', '기타'] as const; 9 | 10 | export const FIELDS_WITH_ICONS = [ 11 | { label: '비즈니스', img: BusinessIcon }, 12 | { label: '디자인', img: DesignIcon }, 13 | { label: '마케팅', img: MarketingIcon }, 14 | { label: '기획', img: PlanningIcon }, 15 | { label: '공부', img: StudyIcon }, 16 | { label: '개발', img: DevelopmentIcon }, 17 | ] as const; 18 | 19 | export const FIELDS_MAP = { 20 | 비즈니스: 'Business Owner/Executive', 21 | 디자인: 'Designer', 22 | 마케팅: 'Marketer', 23 | 기획: 'PM/PO', 24 | 공부: 'Student', 25 | 개발: 'Dev', 26 | 기타: 'Others', 27 | }; 28 | -------------------------------------------------------------------------------- /src/shared/constants/timerPageText.ts: -------------------------------------------------------------------------------- 1 | export const TIMEZONE = 'Asia/Seoul'; 2 | export const DATE_FORMAT = 'YYYY-MM-DD'; 3 | export const DEFAULT_URL = 'chrome://newtab'; 4 | -------------------------------------------------------------------------------- /src/shared/constants/weekDays.ts: -------------------------------------------------------------------------------- 1 | export const WEEK_DAYS: string[] = ['일', '월', '화', '수', '목', '금', '토']; 2 | -------------------------------------------------------------------------------- /src/shared/hocs/withAuthProtection.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | // import { Navigate } from 'react-router-dom'; 4 | 5 | // import { getAccessTotken } from '@/utils/token'; 6 | 7 | const withAuthProtection = (WrappedComponent: React.ComponentType) => { 8 | const HOC: React.FC = (props) => { 9 | //Todo: 개발이 진행되면 실제 토큰 상태를 받아서 login page로 이동 시킴 10 | // const accessToken = getAccessTotken(); 11 | // if (!accessToken) { 12 | // alert('로그인 해주세요'); 13 | // return ; 14 | // } 15 | 16 | return ; 17 | }; 18 | 19 | return HOC; 20 | }; 21 | 22 | export default withAuthProtection; 23 | -------------------------------------------------------------------------------- /src/shared/hooks/useCarousel.ts: -------------------------------------------------------------------------------- 1 | import { RefObject } from 'react'; 2 | 3 | interface UseCarouselProps { 4 | carouselRef: RefObject; 5 | } 6 | 7 | interface UseCarouselReturn { 8 | handleNext: () => void; 9 | handlePrev: () => void; 10 | } 11 | 12 | const useCarousel = ({ carouselRef }: UseCarouselProps): UseCarouselReturn => { 13 | const handleNext = () => { 14 | if (carouselRef.current) { 15 | carouselRef.current.scrollBy({ 16 | left: carouselRef.current.offsetWidth, 17 | behavior: 'smooth', 18 | }); 19 | } 20 | }; 21 | 22 | const handlePrev = () => { 23 | if (carouselRef.current) { 24 | carouselRef.current.scrollBy({ 25 | left: -carouselRef.current.offsetWidth, 26 | behavior: 'smooth', 27 | }); 28 | } 29 | }; 30 | 31 | return { handleNext, handlePrev }; 32 | }; 33 | 34 | export default useCarousel; 35 | -------------------------------------------------------------------------------- /src/shared/hooks/useClickOutside.ts: -------------------------------------------------------------------------------- 1 | import { RefObject, useEffect } from 'react'; 2 | 3 | const useClickOutside = ( 4 | ref: RefObject, 5 | callback: (event?: MouseEvent) => void, 6 | enable: boolean = true, 7 | ) => { 8 | useEffect(() => { 9 | const handleClickOutside = (event: MouseEvent) => { 10 | if (!enable) return; 11 | if (ref.current && !ref.current.contains(event.target as Node)) { 12 | callback(event); 13 | } 14 | }; 15 | 16 | document.addEventListener('mousedown', handleClickOutside); 17 | return () => { 18 | document.removeEventListener('mousedown', handleClickOutside); 19 | }; 20 | }, [ref, callback, enable]); 21 | }; 22 | 23 | export default useClickOutside; 24 | -------------------------------------------------------------------------------- /src/shared/layout/Layout.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react'; 2 | 3 | import Sidebar from './Sidebar/Sidebar'; 4 | 5 | interface LayoutProps { 6 | children: ReactNode; 7 | } 8 | 9 | const Layout = ({ children }: LayoutProps) => { 10 | return ( 11 |
    12 | 13 |
    {children}
    14 |
    15 | ); 16 | }; 17 | 18 | export default Layout; 19 | -------------------------------------------------------------------------------- /src/shared/layout/Sidebar/ModalContentsSetting/ModalContentsSetting.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | 3 | import SettingIcon from '@/shared/assets/svgs/setting.svg?react'; 4 | import UserIcon from '@/shared/assets/svgs/user_circle.svg?react'; 5 | 6 | import { useGetProfile } from '@/shared/apisV2/setting/setting.queries'; 7 | 8 | import AccountContent from './AccountContent/AccountContent'; 9 | import Tabs from './Tabs/Tabs'; 10 | import WorkSpaceSettingContent from './WorkspaceSettingContent/WorkspaceSettingContent'; 11 | 12 | interface ModalContentsSettingProps { 13 | isModalOpen: boolean; 14 | } 15 | 16 | const ModalContentsSetting = ({ isModalOpen }: ModalContentsSettingProps) => { 17 | const [activeTab, setActiveTab] = useState('account'); 18 | 19 | const { data: userProfile } = useGetProfile(); 20 | 21 | const personalWorkspaces = ['개인 프로젝트']; 22 | const teamWorkspaces = ['팀 A 프로젝트', '팀 B 프로젝트']; 23 | 24 | const handleTabChange = (value: string) => { 25 | setActiveTab(value); 26 | }; 27 | 28 | useEffect(() => { 29 | if (!isModalOpen) { 30 | setActiveTab('account'); 31 | } 32 | }, [isModalOpen]); 33 | 34 | return ( 35 |
    36 | 37 | 38 | 39 | 40 | 내 계정 41 | 42 | 43 | 44 | 45 | 46 | 47 | 설정 {'\0 준비 중'} 48 | 49 | 50 | 51 | 52 | 53 |
    54 | {activeTab === 'account' && userProfile?.data ? ( 55 | 56 | ) : ( 57 | 58 | )} 59 |
    60 |
    61 | ); 62 | }; 63 | 64 | export default ModalContentsSetting; 65 | -------------------------------------------------------------------------------- /src/shared/layout/Sidebar/ModalContentsSetting/WorkspaceSettingContent/WorkspaceSettingContent.tsx: -------------------------------------------------------------------------------- 1 | import ButtonRadius8 from '@/shared/components/ButtonRadius8/ButtonRadius8'; 2 | 3 | interface WorkSpaceSettingContentProps { 4 | personalWorkspaces: string[]; 5 | teamWorkspaces: string[]; 6 | } 7 | 8 | const WorkSpaceSettingContent = ({ personalWorkspaces, teamWorkspaces }: WorkSpaceSettingContentProps) => ( 9 | <> 10 |

    내 워크스페이스

    11 | 12 | {personalWorkspaces.map((name, index) => ( 13 |
    14 |
    15 |
    16 | {name} 17 |
    18 |
    19 | ))} 20 | 21 |

    22 | 팀 워크스페이스 23 |

    24 | 25 | {teamWorkspaces.map((name, index) => ( 26 |
    27 |
    28 |
    29 | {name} 30 |
    31 |
    32 | ))} 33 | 34 |
    35 | 변경사항 저장 36 |
    37 | 38 | ); 39 | 40 | export default WorkSpaceSettingContent; 41 | -------------------------------------------------------------------------------- /src/shared/mocks/faviconData.ts: -------------------------------------------------------------------------------- 1 | interface FaviconDataProps { 2 | id: number; 3 | name: string; 4 | favicon: string; 5 | } 6 | 7 | export const Favicon_DATA: FaviconDataProps[] = [ 8 | { 9 | id: 1, 10 | name: 'Naver', 11 | favicon: 'https://www.naver.com/favicon.ico', 12 | }, 13 | { 14 | id: 2, 15 | name: 'Youtube', 16 | favicon: 'https://www.youtube.com/favicon.ico', 17 | }, 18 | { 19 | id: 3, 20 | name: 'Slack', 21 | favicon: 'https://www.slack.com/favicon.ico', 22 | }, 23 | ]; 24 | -------------------------------------------------------------------------------- /src/shared/mocks/homeData.ts: -------------------------------------------------------------------------------- 1 | export const todoData = [ 2 | { id: 1, title: '노션 세팅하기', date: '24-07-10', accumulatedTime: 0 }, 3 | { id: 2, title: '슬랙 세팅하기', date: '24-07-10', accumulatedTime: 3000 }, 4 | { id: 3, title: '노션 세팅하기', date: '24-07-10', accumulatedTime: 4000 }, 5 | { id: 4, title: '노션 세팅하기', date: '24-07-10', accumulatedTime: 5000 }, 6 | { id: 5, title: '노션 세팅하기', date: '24-07-10', accumulatedTime: 0 }, 7 | { id: 6, title: '슬랙 세팅하기', date: '24-07-10', accumulatedTime: 3000 }, 8 | { id: 7, title: '노션 세팅하기', date: '24-07-10', accumulatedTime: 4000 }, 9 | { id: 8, title: '노션 세팅하기', date: '24-07-10', accumulatedTime: 5000 }, 10 | ]; 11 | -------------------------------------------------------------------------------- /src/shared/mocks/userData.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/morib-in/Morib-Client/e855b115b93b9f35a569fc7dab7f7daf41101cb6/src/shared/mocks/userData.ts -------------------------------------------------------------------------------- /src/shared/types/SSEEvent.ts: -------------------------------------------------------------------------------- 1 | export type SSEEventType = 2 | | 'connect' 3 | | 'timeout' 4 | | 'completion' 5 | | 'refresh' 6 | | 'timerStart' 7 | | 'timerStopAction' 8 | | 'friendRequest' 9 | | 'friendRequestAccept'; 10 | -------------------------------------------------------------------------------- /src/shared/types/allowedService.ts: -------------------------------------------------------------------------------- 1 | import { COLOR_PALETTE_MAP } from '../constants/colorPalette'; 2 | import { 3 | GetAllowedServiceGroupDetailRes, 4 | GetAllowedServiceListRes, 5 | GetRecommendedSitesRes, 6 | } from './api/allowedService'; 7 | import { GetPopoverAllowedServiceListRes } from './api/timer'; 8 | 9 | export type ColorPaletteType = keyof typeof COLOR_PALETTE_MAP; 10 | 11 | export type AllowedServiceGroupType = GetAllowedServiceListRes['data'][number]; 12 | 13 | export type AllowedServiceGroupDetailType = GetAllowedServiceGroupDetailRes['data']; 14 | 15 | export type AllowedServiceGroupDetailSiteType = AllowedServiceGroupDetailType['allowedSites'][number]; 16 | 17 | export type RecommendSiteType = GetRecommendedSitesRes['data']['recommendSites'][number]; 18 | 19 | export type PopoverAllowedServiceGroupType = GetPopoverAllowedServiceListRes['data']; 20 | 21 | export type PopoverAllowedSitesType = GetPopoverAllowedServiceListRes['data'][number]['allowedSites']; 22 | 23 | export type PopoverAllowedSiteType = PopoverAllowedSitesType[number]; 24 | -------------------------------------------------------------------------------- /src/shared/types/allowedSites.ts: -------------------------------------------------------------------------------- 1 | import { PostInterestAreaReq } from './api/onboarding'; 2 | 3 | export type AllowedSitesType = PostInterestAreaReq['allowedSites']; 4 | 5 | export type AllowedSiteType = AllowedSitesType[number]; 6 | -------------------------------------------------------------------------------- /src/shared/types/api/allowedService.ts: -------------------------------------------------------------------------------- 1 | import type { ColorPaletteType } from '../allowedService'; 2 | 3 | export interface PostAddAllowedServiceGroupReq { 4 | name: string; 5 | colorCode: ColorPaletteType; 6 | } 7 | 8 | export interface GetAllowedServiceListReq { 9 | connectType: 'DESKTOP' | 'WEB'; 10 | } 11 | 12 | export interface GetAllowedServiceListRes { 13 | status: number; 14 | message: string; 15 | data: { 16 | id: number; 17 | name: string; 18 | colorCode: ColorPaletteType; 19 | favicons: string[]; 20 | extraCnt: 0; 21 | }[]; 22 | } 23 | 24 | export interface GetAllowedServiceGroupDetailReq { 25 | allowedGroupId: number; 26 | connectType: 'DESKTOP' | 'WEB'; 27 | } 28 | 29 | export interface GetAllowedServiceGroupDetailRes { 30 | status: number; 31 | message: string; 32 | data: { 33 | id: number; 34 | name: string; 35 | colorCode: ColorPaletteType; 36 | allowedSites: { 37 | id: number; 38 | favicon: string; 39 | pageName: string; 40 | siteName: string; 41 | siteUrl: string; 42 | }[]; 43 | }; 44 | } 45 | 46 | export interface PatchChangeAllowedServiceGroupNameReq { 47 | allowedGroupId: number; 48 | name: string; 49 | } 50 | 51 | export interface PatchChangeAllowedServiceGroupColorReq { 52 | allowedGroupId: number; 53 | colorCode: ColorPaletteType; 54 | } 55 | 56 | export interface DeleteAllowedServiceGroupReq { 57 | allowedGroupId: number; 58 | } 59 | 60 | export interface GetRecommendedSitesRes { 61 | status: number; 62 | message: string; 63 | data: { 64 | recommendSites: { 65 | siteName: string; 66 | siteUrl: string; 67 | favicon: string; 68 | }[]; 69 | }; 70 | } 71 | 72 | export interface PostAddAllowedServiceReq { 73 | allowedGroupId: number; 74 | siteUrl: string; 75 | } 76 | 77 | export interface DeleteAllowedServiceReq { 78 | allowedSiteId: string; 79 | } 80 | 81 | export interface GetRecommendedSitesReq { 82 | allowedGroupId: number; 83 | } 84 | 85 | export interface PostMergeAllowedSiteReq { 86 | allowedGroupId: number; 87 | siteUrl: string; 88 | } 89 | -------------------------------------------------------------------------------- /src/shared/types/api/auth.ts: -------------------------------------------------------------------------------- 1 | export interface reissueRes { 2 | status: number; 3 | message: string; 4 | data: { 5 | accessToken: string; 6 | }; 7 | } 8 | -------------------------------------------------------------------------------- /src/shared/types/api/common.ts: -------------------------------------------------------------------------------- 1 | export interface GetUrlInfoReq { 2 | siteUrl: string; 3 | } 4 | 5 | export interface GetUrlInfoRes { 6 | status: number; 7 | message: string; 8 | data: { 9 | favicon: string; 10 | siteName: string; 11 | pageName: string; 12 | siteUrl: string; 13 | }; 14 | } 15 | 16 | export interface PostToggleTaskStatusReq { 17 | taskId: number; 18 | } 19 | -------------------------------------------------------------------------------- /src/shared/types/api/error.ts: -------------------------------------------------------------------------------- 1 | export interface ApiErrorResponseType { 2 | response: { 3 | data: { 4 | status: number; 5 | message: string; 6 | }; 7 | }; 8 | } 9 | -------------------------------------------------------------------------------- /src/shared/types/api/friends.ts: -------------------------------------------------------------------------------- 1 | export interface GetFriendListRes { 2 | status: number; 3 | message: string; 4 | data: { 5 | id: number; 6 | name: string; 7 | email: string; 8 | imageUrl: string; 9 | isOnline: boolean; 10 | elapsedTime: number; 11 | }[]; 12 | } 13 | 14 | export interface GetFriendRequestListRes { 15 | status: number; 16 | message: string; 17 | data: { 18 | send: { 19 | id: number; 20 | name: string; 21 | email: string; 22 | imageUrl: string; 23 | }[]; 24 | receive: { 25 | id: number; 26 | name: string; 27 | email: string; 28 | imageUrl: string; 29 | }[]; 30 | }; 31 | } 32 | 33 | export interface PostSendFriendRequestReq { 34 | friendEmail: string; 35 | } 36 | 37 | export interface DeleteCancelFriendRequestReq { 38 | friendId: number; 39 | } 40 | 41 | export interface PostAcceptFriendRequestReq { 42 | friendId: number; 43 | } 44 | 45 | export interface DeleteRejectFriendRequestReq { 46 | friendId: number; 47 | } 48 | 49 | export interface DeleteFriendReq { 50 | friendId: number; 51 | } 52 | -------------------------------------------------------------------------------- /src/shared/types/api/home.ts: -------------------------------------------------------------------------------- 1 | export interface GetCategoryTaskReq { 2 | startDate: string; 3 | endDate: string; 4 | } 5 | 6 | export interface GetCategoryTaskRes { 7 | status: number; 8 | message: string; 9 | data: { 10 | date: string; 11 | categories: { 12 | category: { id: number; name: string }; 13 | tasks: { 14 | id: number; 15 | name: string; 16 | startDate: string; 17 | endDate: string | null; 18 | elapsedTime: number; 19 | isComplete: boolean; 20 | }[]; 21 | }[]; 22 | }[]; 23 | } 24 | 25 | export interface GetWorkTimeReq { 26 | targetDate: string; 27 | } 28 | 29 | export interface GetWorkTimeRes { 30 | status: number; 31 | message: string; 32 | data: { 33 | targetDate: string; 34 | sumTodayElapsedTime: number; 35 | }; 36 | } 37 | 38 | export interface postAddTodayTodosReq { 39 | targetDate: string; 40 | taskIdList: number[]; 41 | } 42 | 43 | export interface PostCreateTaskReq { 44 | categoryId: number; 45 | name: string; 46 | startDate: string; 47 | endDate: string | null; 48 | } 49 | 50 | export interface PostToggleTaskStatusReq { 51 | taskId: number; 52 | } 53 | 54 | export interface PostAddCategoryReq { 55 | name: string; 56 | } 57 | 58 | export interface DeleteCategoryReq { 59 | categoryId: number; 60 | } 61 | 62 | export interface DeleteTaskReq { 63 | taskId: number; 64 | } 65 | 66 | export interface PatchCategoryReq { 67 | categoryId: number; 68 | name: string; 69 | } 70 | 71 | export interface PatchTaskReq { 72 | taskId: number; 73 | name: string; 74 | startDate: string; 75 | endDate: string | null; 76 | } 77 | -------------------------------------------------------------------------------- /src/shared/types/api/onboarding.ts: -------------------------------------------------------------------------------- 1 | import { AllowedSiteType } from '@/shared/types/allowedSites'; 2 | 3 | import { ColorPaletteType } from '../allowedService'; 4 | import { FieldTypeMapped } from '../fileds'; 5 | 6 | export interface PostInterestAreaReq { 7 | name: string; 8 | colorCode: ColorPaletteType; 9 | interestArea: FieldTypeMapped; 10 | allowedSites: { favicon: string; siteName: string; pageName: string; siteUrl: string }[]; 11 | } 12 | 13 | export interface PostInterestAreaRes { 14 | status: number; 15 | message: string; 16 | } 17 | 18 | export interface GetSuugestedSitesRes { 19 | status: number; 20 | message: string; 21 | data: Record; 22 | } 23 | -------------------------------------------------------------------------------- /src/shared/types/api/setting.ts: -------------------------------------------------------------------------------- 1 | export interface GetProfileRes { 2 | status: number; 3 | message: string; 4 | data: { 5 | id: number; 6 | name: string; 7 | email: string; 8 | imageUrl: string; 9 | isPushEnabled: boolean; 10 | }; 11 | } 12 | 13 | export interface PutChangeProfileReq { 14 | name: string; 15 | imageUrl: string; 16 | isPushEnabled: boolean; 17 | } 18 | -------------------------------------------------------------------------------- /src/shared/types/common/index.tsx: -------------------------------------------------------------------------------- 1 | export enum Direction { 2 | LEFT = 'left', 3 | RIGHT = 'right', 4 | UP = 'up', 5 | DOWN = 'down', 6 | } 7 | 8 | export enum HomeLargeBtnVariant { 9 | MIDDLE = 'middle', 10 | LARGE = 'large', 11 | } 12 | -------------------------------------------------------------------------------- /src/shared/types/fileds.ts: -------------------------------------------------------------------------------- 1 | import { FIELDS, FIELDS_MAP } from '@/shared/constants/fields'; 2 | 3 | export type FieldType = (typeof FIELDS)[number]; 4 | 5 | export type FieldTypeMapped = (typeof FIELDS_MAP)[FieldType]; 6 | -------------------------------------------------------------------------------- /src/shared/types/friend.ts: -------------------------------------------------------------------------------- 1 | import { GetFriendListRes, GetFriendRequestListRes } from './api/friends'; 2 | 3 | export type FriendListType = GetFriendListRes['data']; 4 | export type FriendType = GetFriendListRes['data'][number]; 5 | export type FriendRequestListType = GetFriendRequestListRes['data']['send']; 6 | export type FriendRequestListItemType = FriendRequestListType[number]; 7 | -------------------------------------------------------------------------------- /src/shared/types/global.ts: -------------------------------------------------------------------------------- 1 | export enum Direction { 2 | LEFT = 'left', 3 | RIGHT = 'right', 4 | UP = 'up', 5 | DOWN = 'down', 6 | } 7 | 8 | export enum HomeLargeBtnVariant { 9 | MIDDLE = 'middle', 10 | LARGE = 'large', 11 | } 12 | -------------------------------------------------------------------------------- /src/shared/types/home/index.tsx: -------------------------------------------------------------------------------- 1 | export interface Task { 2 | id: number; 3 | name: string; 4 | startDate: string; 5 | endDate: string | null; 6 | targetTime: number; 7 | isComplete: true; 8 | } 9 | 10 | export interface Category { 11 | id: number; 12 | name: string; 13 | startDate: string; 14 | endDate: string; 15 | } 16 | export interface CategoryWithTasks { 17 | category: Category; 18 | tasks: Task[]; 19 | } 20 | 21 | export interface DailyData { 22 | date: string; 23 | categories: CategoryWithTasks[] | []; 24 | } 25 | -------------------------------------------------------------------------------- /src/shared/types/profile.ts: -------------------------------------------------------------------------------- 1 | import { GetProfileRes } from './api/setting'; 2 | 3 | export type UserProfileType = GetProfileRes['data']; 4 | -------------------------------------------------------------------------------- /src/shared/types/tasks.ts: -------------------------------------------------------------------------------- 1 | import { GetCategoryTaskRes } from './api/home'; 2 | import { GetTimerFriendsRes, GetTimerTodosRes } from './api/timer'; 3 | 4 | export type TaskType = GetCategoryTaskRes['data'][number]['categories'][number]['tasks'][number]; 5 | export type TaskListType = GetCategoryTaskRes['data'][number]['categories'][number]['tasks']; 6 | export type CategoryListType = GetCategoryTaskRes['data'][number]['categories'][number]['category']; 7 | export type CategoriesType = GetCategoryTaskRes['data'][number]['categories']; 8 | 9 | export type TimerTodoType = GetTimerTodosRes['data']['task'][number]; 10 | export type TimerFriendType = GetTimerFriendsRes['data'][number]; 11 | -------------------------------------------------------------------------------- /src/shared/types/todoData.ts: -------------------------------------------------------------------------------- 1 | export interface Todo { 2 | id: number; 3 | name: string; 4 | startDate: string; 5 | endDate: string | null; 6 | targetTime: number; 7 | isComplete: boolean; 8 | categoryName: string; 9 | } 10 | -------------------------------------------------------------------------------- /src/shared/types/userData.ts: -------------------------------------------------------------------------------- 1 | export interface TodoDataTypes { 2 | id: number; 3 | title: string; 4 | date: string; 5 | accumulatedTime: number; 6 | } 7 | -------------------------------------------------------------------------------- /src/shared/utils/auth.ts: -------------------------------------------------------------------------------- 1 | import { ROUTES_CONFIG } from '@/router/routesConfig'; 2 | 3 | export const getAccessToken = () => { 4 | const accessToken = localStorage.getItem('accessToken'); 5 | return accessToken; 6 | }; 7 | 8 | export const setAccessToken = (accessToken: string) => { 9 | localStorage.setItem('accessToken', accessToken); 10 | }; 11 | 12 | export const reloginWithoutLogout = () => { 13 | localStorage.removeItem('accessToken'); 14 | location.href = ROUTES_CONFIG.login.path; 15 | }; 16 | 17 | export const getRefreshToken = () => { 18 | const refreshToken = localStorage.getItem('refreshToken'); 19 | return refreshToken; 20 | }; 21 | 22 | export const setRefreshToken = (refreshToken: string) => { 23 | localStorage.setItem('refreshToken', refreshToken); 24 | }; 25 | -------------------------------------------------------------------------------- /src/shared/utils/calendar/index.ts: -------------------------------------------------------------------------------- 1 | import { Dayjs } from 'dayjs'; 2 | 3 | export const formatDateInfo = (date: Dayjs | null) => { 4 | if (!date) return { year: '', month: '', day: '' }; 5 | return { year: date.year().toString(), month: (date.month() + 1).toString(), day: date.date().toString() }; 6 | }; 7 | 8 | export const formatFullDateInfo = (date: Dayjs | null) => { 9 | if (!date) return { year: '', month: '', day: '' }; 10 | const { year, month, day } = formatDateInfo(date); 11 | return { year, month: month.padStart(2, '0'), day: day.padStart(2, '0') }; 12 | }; 13 | 14 | export const formatCalendarApiDate = (date: Dayjs | null): string => { 15 | if (!date) return ''; 16 | const { year, month, day } = formatFullDateInfo(date); 17 | return `${year}-${month}-${day}`; 18 | }; 19 | -------------------------------------------------------------------------------- /src/shared/utils/date/index.ts: -------------------------------------------------------------------------------- 1 | import dayjs, { Dayjs } from 'dayjs'; 2 | import isoWeek from 'dayjs/plugin/isoWeek'; 3 | 4 | dayjs.extend(isoWeek); 5 | 6 | export const getWeekDates = (selectedDate: Dayjs) => { 7 | const startOfWeek = dayjs(selectedDate).startOf('isoWeek'); 8 | const weekDays = ['월', '화', '수', '목', '금', '토', '일']; 9 | 10 | const weekDates = Array.from({ length: 7 }, (_, i) => { 11 | const date = startOfWeek.add(i, 'day'); 12 | return { 13 | date, 14 | day: weekDays[i], 15 | }; 16 | }); 17 | 18 | return weekDates; 19 | }; 20 | 21 | export const getHomeDropdownData = (currentDate: Dayjs) => { 22 | const startDate = dayjs(currentDate).subtract(1, 'year').startOf('month'); 23 | const endDate = dayjs(currentDate).add(1, 'year').startOf('month'); 24 | 25 | const diffMonths = endDate.diff(startDate, 'month') + 1; 26 | const homeDropdownDate = Array.from({ length: diffMonths }, (_, i) => startDate.add(i, 'month')); 27 | 28 | return homeDropdownDate; 29 | }; 30 | 31 | export const getThisWeekRange = (selectedDate: Dayjs) => { 32 | const startDate = dayjs(selectedDate).startOf('isoWeek').format('YYYY-MM-DD'); 33 | const endDate = dayjs(selectedDate).endOf('isoWeek').format('YYYY-MM-DD'); 34 | 35 | return { startDate, endDate }; 36 | }; 37 | -------------------------------------------------------------------------------- /src/shared/utils/path.ts: -------------------------------------------------------------------------------- 1 | import { ROUTES_CONFIG } from '@/router/routesConfig'; 2 | 3 | export const getActivePath = (path: string) => { 4 | let activePage; 5 | 6 | switch (path) { 7 | case ROUTES_CONFIG.home.path: 8 | activePage = ROUTES_CONFIG.home.path; 9 | break; 10 | case ROUTES_CONFIG.onboarding.path: 11 | activePage = ROUTES_CONFIG.onboarding.path; 12 | break; 13 | case ROUTES_CONFIG.allowedService.path: 14 | activePage = ROUTES_CONFIG.allowedService.path; 15 | break; 16 | default: 17 | activePage = ''; 18 | break; 19 | } 20 | 21 | return activePage; 22 | }; 23 | -------------------------------------------------------------------------------- /src/shared/utils/tasks.ts: -------------------------------------------------------------------------------- 1 | import { Dayjs } from 'dayjs'; 2 | 3 | import { GetCategoryTaskRes } from '@/shared/types/api/home'; 4 | import type { CategoriesType, TaskListType } from '@/shared/types/tasks'; 5 | 6 | export const getDailyCategoryTask = (selectedDate: Dayjs, data: GetCategoryTaskRes['data']) => { 7 | const formattedDate = selectedDate.format('YYYY-MM-DD'); 8 | 9 | let matchingCategories: CategoriesType = []; 10 | 11 | data.forEach(({ date, categories }) => { 12 | if (date === formattedDate) { 13 | matchingCategories = categories; 14 | } 15 | }); 16 | 17 | return matchingCategories; 18 | }; 19 | 20 | export const splitTasksByCompletion = (tasks: TaskListType) => { 21 | let completedTasks: TaskListType = []; 22 | let ongoingTasks: TaskListType = []; 23 | 24 | tasks.forEach((task) => { 25 | if (task.isComplete) { 26 | completedTasks = [...completedTasks, task]; 27 | } else { 28 | ongoingTasks = [...ongoingTasks, task]; 29 | } 30 | }); 31 | 32 | return { completedTasks, ongoingTasks }; 33 | }; 34 | 35 | export const isTaskExist = (dailyCategoryTask: CategoriesType) => { 36 | return dailyCategoryTask.some((categoryWithTasks) => categoryWithTasks.tasks.length > 0); 37 | }; 38 | -------------------------------------------------------------------------------- /src/shared/utils/time/index.ts: -------------------------------------------------------------------------------- 1 | export const formatSeconds = (seconds: number) => { 2 | if (seconds === 0) return '00:00:00'; 3 | 4 | const hours = Math.floor(seconds / 3600); 5 | const minutes = Math.floor((seconds % 3600) / 60); 6 | const secs = seconds % 60; 7 | 8 | const formattedHours = String(hours).padStart(2, '0'); 9 | const formattedMinutes = String(minutes).padStart(2, '0'); 10 | const formattedSeconds = String(secs).padStart(2, '0'); 11 | 12 | return `${formattedHours}:${formattedMinutes}:${formattedSeconds}`; 13 | }; 14 | 15 | export const convertTime = (time: number) => { 16 | const hours = Math.floor(time / 3600); 17 | const minutes = Math.floor((time % 3600) / 60); 18 | const seconds = time % 60; 19 | return { hours, minutes, seconds }; 20 | }; 21 | 22 | export const formatSecondsForFriendsList = (seconds: number) => { 23 | if (seconds === 0) return '00시간 00분 00초'; 24 | 25 | const hours = Math.floor(seconds / 3600); 26 | const minutes = Math.floor((seconds % 3600) / 60); 27 | const secs = seconds % 60; 28 | 29 | const formattedHours = String(hours).padStart(2, '0'); 30 | const formattedMinutes = String(minutes).padStart(2, '0'); 31 | const formattedSeconds = String(secs).padStart(2, '0'); 32 | 33 | return `${formattedHours}시간 ${formattedMinutes}분 ${formattedSeconds}초`; 34 | }; 35 | -------------------------------------------------------------------------------- /src/shared/utils/timer/index.ts: -------------------------------------------------------------------------------- 1 | import type { TimerTodoType } from '@/shared/types/tasks'; 2 | 3 | export const splitTasksByCompletion = (tasks: TimerTodoType[]) => { 4 | let completedTodos: TimerTodoType[] = []; 5 | let ongoingTodos: TimerTodoType[] = []; 6 | 7 | tasks.forEach((task) => { 8 | if (task.isComplete) { 9 | completedTodos = [...completedTodos, task]; 10 | } else { 11 | ongoingTodos = [...ongoingTodos, task]; 12 | } 13 | }); 14 | 15 | return { completedTodos, ongoingTodos }; 16 | }; 17 | -------------------------------------------------------------------------------- /src/shared/utils/url.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 주어진 URL에서 주요 도메인(예: "naver", "google")을 추출 3 | * @param {string} url - 추출할 URL 문자열. 4 | * @returns {string} - URL에서 추출한 주요 도메인, 유효하지 않은 URL인 경우 빈 문자열 반환. 5 | */ 6 | export const getMainDomain = (url: string) => { 7 | try { 8 | const { hostname } = new URL(url); 9 | 10 | const cleanedHostname = hostname.replace(/^www\./, ''); 11 | 12 | const domainParts = cleanedHostname.split('.'); 13 | 14 | return domainParts[0]; 15 | } catch (error) { 16 | console.error('유효하지 않은 URL입니다:', error); 17 | return ''; 18 | } 19 | }; 20 | 21 | export const getBaseUrl = (url: string): string => { 22 | try { 23 | const urlObj = new URL(url); 24 | return urlObj.origin; 25 | } catch (error) { 26 | return url; 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /src/shared/utils/url/index.ts: -------------------------------------------------------------------------------- 1 | export const getBaseUrl = (url: string): string => { 2 | try { 3 | const urlObj = new URL(url); 4 | return urlObj.origin; 5 | } catch (error) { 6 | return url; 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /src/shared/utils/validation.ts: -------------------------------------------------------------------------------- 1 | export const isUrlValid = (url: string): boolean => { 2 | const urlPattern = /^(https?:\/\/)?(localhost|\w+(\.\w+)+)(:\d{1,5})?(\/[\w#!:.?+=&%@!\-/]*)?$/; 3 | return urlPattern.test(url); 4 | }; 5 | 6 | export const isEmailValid = (email: string): boolean => { 7 | const emailRegex = /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/; 8 | return emailRegex.test(email); 9 | }; 10 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | interface ImportMetaEnv { 4 | readonly VITE_BASE_URL: string; 5 | readonly VITE_GOOGLE_URL: string; 6 | readonly VITE_SENTRY_DSN: string; 7 | } 8 | 9 | interface ImportMeta { 10 | readonly env: ImportMetaEnv; 11 | } 12 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", 5 | "target": "ES2020", 6 | "useDefineForClassFields": true, 7 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 8 | "module": "ESNext", 9 | "skipLibCheck": true, 10 | 11 | /* Bundler mode */ 12 | "moduleResolution": "bundler", 13 | "allowImportingTsExtensions": true, 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "moduleDetection": "force", 17 | "noEmit": true, 18 | "jsx": "react-jsx", 19 | 20 | /* Linting */ 21 | "strict": true, 22 | "noUnusedLocals": false, 23 | "noUnusedParameters": false, 24 | "noFallthroughCasesInSwitch": true, 25 | 26 | /* 절대경로 */ 27 | "baseUrl": ".", 28 | "paths": { 29 | "@/*": ["src/*"] 30 | }, 31 | 32 | /*svg 모듈 */ 33 | "types": ["vite-plugin-svgr/client"] 34 | }, 35 | "include": ["src"] 36 | } 37 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { 5 | "path": "./tsconfig.app.json" 6 | }, 7 | { 8 | "path": "./tsconfig.node.json" 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", 5 | "skipLibCheck": true, 6 | "module": "ESNext", 7 | "moduleResolution": "bundler", 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "noEmit": true, 11 | 12 | /* 절대경로 */ 13 | "baseUrl": ".", 14 | "paths": { 15 | "@/*": ["src/*"] 16 | }, 17 | 18 | /*svg 모듈 */ 19 | "types": ["vite-plugin-svgr/client"] 20 | }, 21 | "include": ["vite.config.ts"] 22 | } 23 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "rewrites": [{ "source": "/(.*)", "destination": "/" }] 3 | } -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import react from '@vitejs/plugin-react-swc'; 2 | import { defineConfig } from 'vite'; 3 | import svgr from 'vite-plugin-svgr'; 4 | 5 | // https://vitejs.dev/config/ 6 | export default defineConfig({ 7 | plugins: [react(), svgr()], 8 | resolve: { 9 | alias: [{ find: '@', replacement: '/src' }], 10 | }, 11 | }); 12 | --------------------------------------------------------------------------------