├── .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 |
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 |
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 |
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 |
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 |
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 |
15 |
--------------------------------------------------------------------------------
/src/shared/assets/svgs/add_btn.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/shared/assets/svgs/arrow_circle_up_right.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/shared/assets/svgs/arrow_right.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/shared/assets/svgs/bell.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/shared/assets/svgs/btn_add.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/src/shared/assets/svgs/btn_arrow.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/src/shared/assets/svgs/btn_arrow_bgNone.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/shared/assets/svgs/btn_cal.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/src/shared/assets/svgs/btn_cal_black.svg:
--------------------------------------------------------------------------------
1 |
10 |
--------------------------------------------------------------------------------
/src/shared/assets/svgs/btn_hamburger.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/shared/assets/svgs/btn_home.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/shared/assets/svgs/btn_list.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/shared/assets/svgs/btn_moribset_active.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/shared/assets/svgs/btn_moribset_default.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/shared/assets/svgs/btn_today.svg:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/src/shared/assets/svgs/button_inputSuccess.svg:
--------------------------------------------------------------------------------
1 |
11 |
--------------------------------------------------------------------------------
/src/shared/assets/svgs/check_box_blank.svg:
--------------------------------------------------------------------------------
1 |
11 |
--------------------------------------------------------------------------------
/src/shared/assets/svgs/check_box_fill.svg:
--------------------------------------------------------------------------------
1 |
11 |
--------------------------------------------------------------------------------
/src/shared/assets/svgs/common/ic_logo.svg:
--------------------------------------------------------------------------------
1 |
10 |
--------------------------------------------------------------------------------
/src/shared/assets/svgs/common/ic_meatball_default.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/src/shared/assets/svgs/connection_icon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/shared/assets/svgs/default_profile.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/src/shared/assets/svgs/default_url_favicon.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/src/shared/assets/svgs/defaultpause.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/src/shared/assets/svgs/defaultplay.svg:
--------------------------------------------------------------------------------
1 |
12 |
--------------------------------------------------------------------------------
/src/shared/assets/svgs/description.svg:
--------------------------------------------------------------------------------
1 |
9 |
--------------------------------------------------------------------------------
/src/shared/assets/svgs/disabled_dropdown.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/src/shared/assets/svgs/dropIcon.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/src/shared/assets/svgs/elipse.svg:
--------------------------------------------------------------------------------
1 |
15 |
--------------------------------------------------------------------------------
/src/shared/assets/svgs/error_input.svg:
--------------------------------------------------------------------------------
1 |
11 |
--------------------------------------------------------------------------------
/src/shared/assets/svgs/friend_delBtn.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/src/shared/assets/svgs/gradient_circle.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/shared/assets/svgs/header_delBtn.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/src/shared/assets/svgs/home/ic_plus.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/shared/assets/svgs/home_default_icon.svg:
--------------------------------------------------------------------------------
1 |
18 |
--------------------------------------------------------------------------------
/src/shared/assets/svgs/hoverpause.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/src/shared/assets/svgs/hoverplay.svg:
--------------------------------------------------------------------------------
1 |
12 |
--------------------------------------------------------------------------------
/src/shared/assets/svgs/ic_back_btn.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/src/shared/assets/svgs/ic_delete_alert.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/src/shared/assets/svgs/ic_description.svg:
--------------------------------------------------------------------------------
1 |
11 |
--------------------------------------------------------------------------------
/src/shared/assets/svgs/ic_folder.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/shared/assets/svgs/ic_line.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/shared/assets/svgs/ic_logo.svg:
--------------------------------------------------------------------------------
1 |
11 |
--------------------------------------------------------------------------------
/src/shared/assets/svgs/ic_minus.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/shared/assets/svgs/ic_pencil.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/shared/assets/svgs/icon_clock.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/shared/assets/svgs/large_plus.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/src/shared/assets/svgs/logo_icon.svg:
--------------------------------------------------------------------------------
1 |
11 |
--------------------------------------------------------------------------------
/src/shared/assets/svgs/mail.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/src/shared/assets/svgs/mingcute_time-fill.svg:
--------------------------------------------------------------------------------
1 |
16 |
--------------------------------------------------------------------------------
/src/shared/assets/svgs/mingcute_time-line.svg:
--------------------------------------------------------------------------------
1 |
11 |
--------------------------------------------------------------------------------
/src/shared/assets/svgs/minus_btn.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/shared/assets/svgs/more_friend.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/shared/assets/svgs/moribSet.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/shared/assets/svgs/onboarding/ic_business.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/shared/assets/svgs/onboarding/ic_design.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/shared/assets/svgs/onboarding/ic_development.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/shared/assets/svgs/onboarding/ic_marketing.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/shared/assets/svgs/onboarding/ic_planning.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/shared/assets/svgs/onboarding/ic_studying.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/shared/assets/svgs/plus.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/shared/assets/svgs/popover_add_category.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/src/shared/assets/svgs/selected_number_icon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/shared/assets/svgs/timer/ic_check_box_active.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/shared/assets/svgs/timer/ic_check_box_inactive.svg:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/src/shared/assets/svgs/timer/ic_deactivated_clock.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/shared/assets/svgs/timer/ic_online.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/shared/assets/svgs/timer/ic_timer_inner_circle.svg:
--------------------------------------------------------------------------------
1 |
15 |
--------------------------------------------------------------------------------
/src/shared/assets/svgs/todo_meatball_press.svg:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/src/shared/assets/svgs/todo_toggle.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/shared/assets/svgs/tooltip_triangle.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/shared/assets/svgs/triangle.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/shared/assets/svgs/upIcon.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/src/shared/assets/svgs/user_circle.svg:
--------------------------------------------------------------------------------
1 |
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 |
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 ;
9 | };
10 |
11 | export default CircleColorIcon;
12 |
--------------------------------------------------------------------------------
/src/shared/components/ColorPallete/ColorPallete.tsx:
--------------------------------------------------------------------------------
1 | import { ButtonHTMLAttributes, ReactNode, forwardRef } from 'react';
2 |
3 | import { COLOR_PALETTE_MAP } from '@/shared/constants/colorPalette';
4 |
5 | interface ColorPaletteRootProps {
6 | isOpen: boolean;
7 | className?: string;
8 | children: ReactNode;
9 | }
10 |
11 | const ColorPaletteRoot = forwardRef(function ColorPaletteRoot(
12 | { isOpen, className, children },
13 | ref,
14 | ) {
15 | if (!isOpen) return null;
16 |
17 | return (
18 |
22 | {children}
23 |
24 | );
25 | });
26 |
27 | interface ColorPaletteColorButtonProps extends ButtonHTMLAttributes {
28 | hashColor: keyof typeof COLOR_PALETTE_MAP;
29 | isSelected?: boolean;
30 | }
31 |
32 | const ColorPaletteColorButton = ({ hashColor, isSelected, ...props }: ColorPaletteColorButtonProps) => {
33 | return (
34 |
38 | );
39 | };
40 |
41 | const ColorPalette = Object.assign(ColorPaletteRoot, { ColorButton: ColorPaletteColorButton });
42 |
43 | export default ColorPalette;
44 |
--------------------------------------------------------------------------------
/src/shared/components/FallbackApiError/FallbackApiError.tsx:
--------------------------------------------------------------------------------
1 | import HomeLargeBtn from '@/shared/components/ButtonHomeLarge/ButtonHomeLarge';
2 |
3 | import { HomeLargeBtnVariant } from '@/shared/types/global';
4 |
5 | import ErrorIcon from '@/shared/assets/svgs/error.svg?react';
6 |
7 | interface ErrorProps {
8 | resetError: () => void;
9 | }
10 |
11 | const FallbackApiError = ({ resetError }: ErrorProps) => {
12 | return (
13 |
14 |
15 |
16 |
일시적인 오류가 발생했습니다.
17 |
잠시 후 다시 이용해 주세요.
18 |
19 |
20 |
21 | 다시 시도하기
22 |
23 |
24 |
25 |
26 | );
27 | };
28 |
29 | export default FallbackApiError;
30 |
--------------------------------------------------------------------------------
/src/shared/components/FaviconImage/FaviconImage.tsx:
--------------------------------------------------------------------------------
1 | import { ImgHTMLAttributes } from 'react';
2 |
3 | import DefaultFaviconImage from '@/shared/assets/svgs/default_url_favicon.svg';
4 |
5 | export const FaviconImage = ({ src, className, ...rest }: ImgHTMLAttributes) => {
6 | return (
7 |
{
12 | e.currentTarget.src = DefaultFaviconImage;
13 | e.currentTarget.alt = '모립 로고 아이콘';
14 | }}
15 | />
16 | );
17 | };
18 |
19 | export default FaviconImage;
20 |
--------------------------------------------------------------------------------
/src/shared/components/HeartBeatBoundary/HeartBeatBoundary.tsx:
--------------------------------------------------------------------------------
1 | import { Outlet } from 'react-router-dom';
2 |
3 | import { useGetHeartBeat } from '@/shared/apisV2/common/common.queries';
4 |
5 | const HeartBeatBoundary = () => {
6 | useGetHeartBeat();
7 |
8 | return ;
9 | };
10 |
11 | export default HeartBeatBoundary;
12 |
--------------------------------------------------------------------------------
/src/shared/components/LoadingOverlay/LoadingOverlay.tsx:
--------------------------------------------------------------------------------
1 | import { useRef } from 'react';
2 | import Lottie from 'react-lottie';
3 |
4 | import LoadingLottie from '@/shared/assets/lotties/loading.json';
5 |
6 | import Portal from '../Portal/Portal';
7 |
8 | const defaultOptions = {
9 | autoplay: true,
10 | animationData: LoadingLottie,
11 | };
12 |
13 | interface LoadingOverlayProps {
14 | isLoading: boolean;
15 | dim?: boolean;
16 | }
17 |
18 | const LoadingOverlay = ({ isLoading, dim = true }: LoadingOverlayProps) => {
19 | const lottieRef = useRef(null);
20 |
21 | const bgStyle = dim ? 'bg-dim' : 'bg-gray-bg-01';
22 |
23 | return (
24 | <>
25 | {isLoading && (
26 |
27 |
28 |
29 |
30 | Loading...
31 |
32 |
33 |
34 | )}
35 | >
36 | );
37 | };
38 |
39 | export default LoadingOverlay;
40 |
--------------------------------------------------------------------------------
/src/shared/components/ModalContentsFriends/FriendRequest/ButtonRequestAction/ButtonRequestAction.tsx:
--------------------------------------------------------------------------------
1 | import { ButtonHTMLAttributes, ReactNode } from 'react';
2 |
3 | interface CategoryBtnProps extends ButtonHTMLAttributes {
4 | variant?: 'positive' | 'negative';
5 | children: ReactNode;
6 | }
7 |
8 | const ButtonRequestAction = ({ variant, children, ...props }: CategoryBtnProps) => {
9 | const btnVariant = {
10 | positive: 'text-gray-bg-01 bg-mint-02 hover:bg-mint-02-hover active:bg-mint-02-press',
11 | negative: 'text-white bg-gray-bg-06 hover:bg-gray-bg-04 active:bg-gray-bg-05',
12 | };
13 |
14 | const commonStyle = ' px-[2.2rem] py-[1rem] rounded-[5px] subhead-semibold-18 ';
15 |
16 | const styledBtn = variant ? btnVariant[variant] : '';
17 |
18 | return (
19 |
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 |
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 | ,
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 |
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 |
--------------------------------------------------------------------------------