├── .eslintrc.js ├── .gitignore ├── .prettierrc ├── README.md ├── package.json ├── public ├── android-icon-144x144.png ├── android-icon-192x192.png ├── android-icon-36x36.png ├── android-icon-48x48.png ├── android-icon-72x72.png ├── android-icon-96x96.png ├── apple-icon-114x114.png ├── apple-icon-120x120.png ├── apple-icon-144x144.png ├── apple-icon-152x152.png ├── apple-icon-180x180.png ├── apple-icon-57x57.png ├── apple-icon-60x60.png ├── apple-icon-72x72.png ├── apple-icon-76x76.png ├── apple-icon-precomposed.png ├── apple-icon.png ├── browserconfig.xml ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon-96x96.png ├── favicon.ico ├── index.html ├── manifest.json ├── ms-icon-144x144.png ├── ms-icon-150x150.png ├── ms-icon-310x310.png ├── ms-icon-70x70.png └── robots.txt ├── src ├── App.tsx ├── apis │ └── client.ts ├── assets │ ├── icons │ │ ├── accntSuccess.png │ │ ├── goalEmpty.png │ │ ├── goalSuccess.png │ │ ├── ico_Google_logo.svg │ │ ├── ico_KakaoTalk_logo.svg │ │ ├── ico_Naver_logo.png │ │ ├── prepare.png │ │ └── success.png │ └── img │ │ ├── bank │ │ ├── BNK.png │ │ ├── Citi.png │ │ ├── DGB.png │ │ ├── GJ.png │ │ ├── Hana.png │ │ ├── JB.png │ │ ├── KB.png │ │ ├── KDB.png │ │ ├── Kiup.png │ │ ├── NH.png │ │ ├── Post.png │ │ ├── SC.png │ │ ├── SH.png │ │ ├── SMG.png │ │ ├── Shinhan.png │ │ ├── Shinhyup.png │ │ └── Woori.png │ │ ├── default.png │ │ ├── goal │ │ ├── group_color.png │ │ ├── group_gray.png │ │ ├── personal_color.png │ │ └── personal_gray.png │ │ ├── guide │ │ ├── line.png │ │ └── scroll.png │ │ └── onboarding │ │ ├── onboarding1.png │ │ ├── onboarding2.png │ │ ├── onboarding3.png │ │ └── onboarding4.png ├── components │ ├── account │ │ ├── AccountInfoCard.tsx │ │ ├── AccountInfoInput.tsx │ │ ├── AccountNoInput.tsx │ │ ├── AccountNoValidate.tsx │ │ └── AccountSelectSection.tsx │ ├── badge │ │ └── MyFilteredBadges.tsx │ ├── common │ │ ├── alert │ │ │ ├── Alert.tsx │ │ │ ├── Info.tsx │ │ │ ├── InfoError.tsx │ │ │ └── InfoLoading.tsx │ │ ├── elem │ │ │ ├── BadgeBox.tsx │ │ │ ├── BankBox.tsx │ │ │ ├── BankIcons.tsx │ │ │ ├── C2TextBox.tsx │ │ │ ├── Contact.tsx │ │ │ ├── DateSelectBox.tsx │ │ │ ├── EmojiBox.tsx │ │ │ ├── ErrorMsg.tsx │ │ │ ├── Icon.tsx │ │ │ ├── InputBox.tsx │ │ │ ├── LoadingIcon.tsx │ │ │ ├── LoadingMsg.tsx │ │ │ ├── LoginButton.tsx │ │ │ ├── Logo.tsx │ │ │ ├── LogoSubTitle.tsx │ │ │ ├── LogoTitle.tsx │ │ │ ├── ModalBox.tsx │ │ │ ├── OptionSelectBox.tsx │ │ │ ├── ProfileImg.tsx │ │ │ ├── ProgressBar.tsx │ │ │ ├── RadioInput.tsx │ │ │ ├── RadioSelectBox.tsx │ │ │ ├── RangeSlider.tsx │ │ │ ├── SettingButton.tsx │ │ │ ├── TextButton.tsx │ │ │ ├── ToggleSelectBox.tsx │ │ │ ├── ValidateMsg.tsx │ │ │ ├── WelcomePic.tsx │ │ │ └── btn │ │ │ │ ├── AddGoalBtn.tsx │ │ │ │ ├── CloseIconBtn.tsx │ │ │ │ └── ImgEditBtn.tsx │ │ └── tag │ │ │ ├── DdayTag.tsx │ │ │ ├── FilterTag.tsx │ │ │ ├── HashTag.tsx │ │ │ └── StateTag.tsx │ ├── goal │ │ ├── GroupGoalCard.tsx │ │ ├── GroupGoalCardSmall.tsx │ │ ├── MyFilteredGoals.tsx │ │ ├── MyGoalCard.tsx │ │ ├── StateGoalCard.tsx │ │ ├── detail │ │ │ ├── ParticipantSection.tsx │ │ │ └── ReportModal.tsx │ │ ├── goalDetail │ │ │ ├── GoalAccountInfo.tsx │ │ │ ├── GoalBalanceCard.tsx │ │ │ ├── GoalDeleteButton.tsx │ │ │ ├── GoalDescCard.tsx │ │ │ ├── GoalInfoCard.tsx │ │ │ ├── GoalModifyButton.tsx │ │ │ ├── GoalPeriodCard.tsx │ │ │ ├── GoalTagsCard.tsx │ │ │ └── group │ │ │ │ ├── JoinButton.tsx │ │ │ │ ├── ParticipantCard.tsx │ │ │ │ ├── ParticipantList.tsx │ │ │ │ └── WithdrawButton.tsx │ │ ├── input │ │ │ ├── DateInput.tsx │ │ │ ├── EmojiInput.tsx │ │ │ ├── NumInput.tsx │ │ │ └── TextInput.tsx │ │ ├── lookup │ │ │ ├── GroupGoals.tsx │ │ │ └── ImpendingGoals.tsx │ │ ├── modify │ │ │ ├── AccntToggle.tsx │ │ │ └── GoalDataInput.tsx │ │ ├── post │ │ │ ├── BankList.tsx │ │ │ ├── GoalInfoInput.tsx │ │ │ ├── TagInputSection.tsx │ │ │ ├── TypeSelectSection.tsx │ │ │ └── goalInfo │ │ │ │ └── DateSelectSection.tsx │ │ ├── search │ │ │ └── SearchResults.tsx │ │ └── searchFilter │ │ │ ├── FiltersModal.tsx │ │ │ ├── RangeSelectBox.tsx │ │ │ ├── SortFilters.tsx │ │ │ └── StatusFilter.tsx │ ├── guide │ │ └── HomeGuide.tsx │ ├── header │ │ └── SearchBar.tsx │ ├── settings │ │ ├── LogoutButton.tsx │ │ ├── ModifyAccount.tsx │ │ ├── ResetPinNumber.tsx │ │ ├── WithdrawalService.tsx │ │ └── myAccounts │ │ │ └── MyAccountCard.tsx │ └── user │ │ ├── UserDetailProfile.tsx │ │ ├── UserDetailTabSection.tsx │ │ ├── UserProfile.tsx │ │ ├── editUserProfile │ │ └── imageCroper │ │ │ └── Crop.tsx │ │ └── signup │ │ ├── GoogleSignupButton.tsx │ │ ├── KakaoSignupButton.tsx │ │ └── NaverSignupButton.tsx ├── hooks │ ├── useAccntAuth.tsx │ ├── useAccntAuthState.tsx │ ├── useAccntAutoPost.tsx │ ├── useAccntManualPost.tsx │ ├── useAccntValidate.tsx │ ├── useAccountsData.tsx │ ├── useBadgesData.tsx │ ├── useBalanceData.tsx │ ├── useBalanceModify.tsx │ ├── useBankId.tsx │ ├── useBankSelect.tsx │ ├── useBanksData.tsx │ ├── useDateInput.tsx │ ├── useEmojiSelect.tsx │ ├── useGoalDetailData.tsx │ ├── useGoalLookupData.tsx │ ├── useGoalLookupImpendingData.tsx │ ├── useGoalModify.tsx │ ├── useGoalModifyInput.tsx │ ├── useGoalPostInput.tsx │ ├── useGoalsFilter.tsx │ ├── useHeaderState.tsx │ ├── useIsManual.tsx │ ├── useJoinGoal.tsx │ ├── useJoinGoalModal.tsx │ ├── useNavigateState.tsx │ ├── useNumInput.tsx │ ├── usePageName.tsx │ ├── usePinNumberKeypad.tsx │ ├── usePinNumberRepost.tsx │ ├── usePinNumberSignupPost.tsx │ ├── usePostGoal.tsx │ ├── useRangeBar.tsx │ ├── useRangeInput.tsx │ ├── useSearchFilterInput.tsx │ ├── useSearchFilterState.tsx │ ├── useSearchFilterTags.tsx │ ├── useSearchGoalsData.tsx │ ├── useSearchKeyword.tsx │ ├── useSignup.tsx │ ├── useTab.tsx │ ├── useTagInput.tsx │ ├── useTxtInput.tsx │ ├── useUserBadgesData.tsx │ ├── useUserGoalsData.tsx │ ├── useUserProfileData.tsx │ ├── useUserProfileModify.tsx │ └── useUserProfileModifyInput.tsx ├── index.tsx ├── interfaces │ └── interfaces.ts ├── pages │ ├── AgreementOfCollectionPersonalInfo.tsx │ ├── CreateAccntAuto.tsx │ ├── CreateAccntManual.tsx │ ├── CreateGoalData.tsx │ ├── DetailGoal.tsx │ ├── DetailUser.tsx │ ├── EditUserProfile.tsx │ ├── GoogleLogin.tsx │ ├── Home.tsx │ ├── JoinGoal.tsx │ ├── KakaoLogin.tsx │ ├── LoginPage.tsx │ ├── LookupGoals.tsx │ ├── ModifyGoal.tsx │ ├── ModifyGoalData.tsx │ ├── NaverLogin.tsx │ ├── NotFoundError.tsx │ ├── NotSupportedDevice.tsx │ ├── PinNumberPage.tsx │ ├── PostGoal.tsx │ ├── Prepare.tsx │ ├── Redirect.tsx │ ├── SearchGoals.tsx │ ├── SelectAccnt.tsx │ ├── SelectGoalType.tsx │ ├── UserSettingAccountList.tsx │ ├── UserSettings.tsx │ └── WelcomePage.tsx ├── react-app-env.d.ts ├── recoil │ ├── accntAtoms.ts │ ├── badgeAtoms.ts │ ├── goalsAtoms.ts │ └── userAtoms.ts ├── reportWebVitals.ts ├── shared │ ├── AuthLayout.tsx │ ├── DesktopLayout.tsx │ ├── Header.tsx │ ├── Navigation.tsx │ ├── PublicLayout.tsx │ ├── RefreshLayout.tsx │ ├── RouteChangeTracker.tsx │ └── Router.tsx ├── styles │ ├── colors.ts │ ├── index.css │ └── theme.ts └── utils │ ├── accountInfoChecker.ts │ ├── dDayCalculator.ts │ ├── dateTranslator.ts │ ├── goalInfoChecker.ts │ ├── imageCropper.tsx │ ├── jwtDecoder.ts │ ├── privateInfoFormatter.ts │ └── progressState.ts ├── tsconfig.json └── yarn.lock /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es2021: true, 5 | node: true, 6 | }, 7 | extends: [ 8 | 'eslint:recommended', 9 | 'plugin:react/recommended', 10 | 'plugin:@typescript-eslint/recommended', 11 | ], 12 | overrides: [], 13 | parser: '@typescript-eslint/parser', 14 | parserOptions: { 15 | ecmaVersion: 'latest', 16 | sourceType: 'module', 17 | }, 18 | plugins: ['react', '@typescript-eslint'], 19 | }; 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | .env 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "always", 3 | "bracketSpacing": true, 4 | "jsxBracketSameLine": true, 5 | "jsxSingleQuote": true, 6 | "printWidth": 120, 7 | "proseWrap": "always", 8 | "quoteProps": "as-needed", 9 | "semi": true, 10 | "singleQuote": true, 11 | "tabWidth": 2, 12 | "trailingComma": "es5", 13 | "useTabs": false 14 | } 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # :moneybag: 티끌모아 태산 :moneybag: 2 | 3 | ## 프로젝트 링크 4 | 5 | 🌐[teekle-taesan](https://teekle-taesan.com/) 6 | 7 | ## 프로젝트 개요 8 | 9 | :white_check_mark: **프로젝트 한줄 소개** 10 | 11 | 2030 재테크 병아리들을 위한 돈 모으기 습관 형성 서비스 입니다. 12 | 13 | :white_check_mark: **기획 의도** 14 | 15 | 재테크 관심도가 높아지고 있는 2030 세대들을 위해 작은 금액부터 목표를 세우고 관리하는 저축 습관 형성 서비스를 제공하고자 했습니다. 16 | 17 | :white_check_mark: **진행 기간** 18 | 19 | 2022.12.30 - 2023.2.9 20 | 21 | :white_check_mark: **구성원** 22 | :runner: [손유진](https://github.com/YujeanSohn) 23 | :runner: [박태근](https://github.com/ptg0811) 24 | 25 | ## 기술 스택 26 | 27 |
28 | 29 | 30 | 31 | 32 | 33 |
34 | 35 | ## 사용 라이브러리 36 | 37 | [`aws-sdk`](https://www.npmjs.com/package/aws-sdk) 38 | [`emoji-picker-react`](https://www.npmjs.com/package/emoji-picker-react) 39 | [`axios`](https://axios-http.com/kr/docs/intro) 40 | 41 | ## 기술적 선택 이유 && 트러블 슈팅 42 | 43 | :wrench: [TroubleShooting](https://www.notion.so/0c15396642cc4607991b275f8fe52c1a) 44 | 45 | ## API 명세 46 | 47 | :notebook: [API](https://www.notion.so/MVP-09346594381b498d94bbaf4f629193a9) 48 | 49 | ## 와이어프레임 50 | 51 | :art: [Figma](https://www.figma.com/file/XZx7V517CCYsc55go50xMZ/%ED%8B%B0%EB%81%8C%EB%AA%A8%EC%95%84%ED%83%9C%EC%82%B0?node-id=0%3A1&t=L9PpVmOEUqOAIzOP-0) 52 | 53 | ## 서비스 소개 54 | 55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@emotion/react": "^11.10.5", 7 | "@emotion/styled": "^11.10.5", 8 | "@mui/material": "^5.11.8", 9 | "@mui/styled-engine-sc": "^5.11.0", 10 | "@reduxjs/toolkit": "^1.9.1", 11 | "@testing-library/jest-dom": "^5.14.1", 12 | "@testing-library/react": "^13.0.0", 13 | "@testing-library/user-event": "^13.2.1", 14 | "@types/aws-sdk": "^2.7.0", 15 | "@types/jest": "^27.0.1", 16 | "@types/node": "^16.7.13", 17 | "@types/react": "^18.0.0", 18 | "@types/react-dom": "^18.0.0", 19 | "@types/react-slick": "^0.23.10", 20 | "@types/styled-components": "^5.1.26", 21 | "@typescript-eslint/eslint-plugin": "^5.48.0", 22 | "@typescript-eslint/parser": "^5.48.0", 23 | "aws-sdk": "^2.1302.0", 24 | "axios": "^1.2.2", 25 | "emoji-picker-react": "^4.4.7", 26 | "jwt-decode": "^3.1.2", 27 | "react": "^18.2.0", 28 | "react-dom": "^18.2.0", 29 | "react-easy-crop": "^4.6.3", 30 | "react-ga": "^3.3.1", 31 | "react-loading": "^2.0.3", 32 | "react-query": "^3.39.2", 33 | "react-redux": "^8.0.5", 34 | "react-router-dom": "^6.6.1", 35 | "react-scripts": "5.0.1", 36 | "react-slick": "^0.29.0", 37 | "recoil": "^0.7.6", 38 | "recoil-persist": "^4.2.0", 39 | "redux": "^4.2.0", 40 | "slick-carousel": "^1.8.1", 41 | "styled-components": "^5.3.6", 42 | "typescript": "^4.4.2", 43 | "web-vitals": "^2.1.0" 44 | }, 45 | "scripts": { 46 | "start": "react-scripts start", 47 | "build": "react-scripts build", 48 | "test": "react-scripts test", 49 | "eject": "react-scripts eject", 50 | "deploy": "yarn run build && aws s3 sync build/ s3://teekle-taesan" 51 | }, 52 | "eslintConfig": { 53 | "extends": [ 54 | "react-app", 55 | "react-app/jest" 56 | ] 57 | }, 58 | "browserslist": { 59 | "production": [ 60 | ">0.2%", 61 | "not dead", 62 | "not op_mini all" 63 | ], 64 | "development": [ 65 | "last 1 chrome version", 66 | "last 1 firefox version", 67 | "last 1 safari version" 68 | ] 69 | }, 70 | "devDependencies": { 71 | "eslint": "^8.31.0", 72 | "eslint-config-prettier": "^8.6.0", 73 | "eslint-plugin-prettier": "^4.2.1", 74 | "eslint-plugin-react": "^7.31.11", 75 | "prettier": "^2.8.1" 76 | }, 77 | "proxy": "https://api.hyphen.im" 78 | } 79 | -------------------------------------------------------------------------------- /public/android-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamBudgetOverflow/frontend/2dd054a68ec7c483eecc27256e760eeeb934a792/public/android-icon-144x144.png -------------------------------------------------------------------------------- /public/android-icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamBudgetOverflow/frontend/2dd054a68ec7c483eecc27256e760eeeb934a792/public/android-icon-192x192.png -------------------------------------------------------------------------------- /public/android-icon-36x36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamBudgetOverflow/frontend/2dd054a68ec7c483eecc27256e760eeeb934a792/public/android-icon-36x36.png -------------------------------------------------------------------------------- /public/android-icon-48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamBudgetOverflow/frontend/2dd054a68ec7c483eecc27256e760eeeb934a792/public/android-icon-48x48.png -------------------------------------------------------------------------------- /public/android-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamBudgetOverflow/frontend/2dd054a68ec7c483eecc27256e760eeeb934a792/public/android-icon-72x72.png -------------------------------------------------------------------------------- /public/android-icon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamBudgetOverflow/frontend/2dd054a68ec7c483eecc27256e760eeeb934a792/public/android-icon-96x96.png -------------------------------------------------------------------------------- /public/apple-icon-114x114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamBudgetOverflow/frontend/2dd054a68ec7c483eecc27256e760eeeb934a792/public/apple-icon-114x114.png -------------------------------------------------------------------------------- /public/apple-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamBudgetOverflow/frontend/2dd054a68ec7c483eecc27256e760eeeb934a792/public/apple-icon-120x120.png -------------------------------------------------------------------------------- /public/apple-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamBudgetOverflow/frontend/2dd054a68ec7c483eecc27256e760eeeb934a792/public/apple-icon-144x144.png -------------------------------------------------------------------------------- /public/apple-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamBudgetOverflow/frontend/2dd054a68ec7c483eecc27256e760eeeb934a792/public/apple-icon-152x152.png -------------------------------------------------------------------------------- /public/apple-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamBudgetOverflow/frontend/2dd054a68ec7c483eecc27256e760eeeb934a792/public/apple-icon-180x180.png -------------------------------------------------------------------------------- /public/apple-icon-57x57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamBudgetOverflow/frontend/2dd054a68ec7c483eecc27256e760eeeb934a792/public/apple-icon-57x57.png -------------------------------------------------------------------------------- /public/apple-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamBudgetOverflow/frontend/2dd054a68ec7c483eecc27256e760eeeb934a792/public/apple-icon-60x60.png -------------------------------------------------------------------------------- /public/apple-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamBudgetOverflow/frontend/2dd054a68ec7c483eecc27256e760eeeb934a792/public/apple-icon-72x72.png -------------------------------------------------------------------------------- /public/apple-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamBudgetOverflow/frontend/2dd054a68ec7c483eecc27256e760eeeb934a792/public/apple-icon-76x76.png -------------------------------------------------------------------------------- /public/apple-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamBudgetOverflow/frontend/2dd054a68ec7c483eecc27256e760eeeb934a792/public/apple-icon-precomposed.png -------------------------------------------------------------------------------- /public/apple-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamBudgetOverflow/frontend/2dd054a68ec7c483eecc27256e760eeeb934a792/public/apple-icon.png -------------------------------------------------------------------------------- /public/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | #ffffff -------------------------------------------------------------------------------- /public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamBudgetOverflow/frontend/2dd054a68ec7c483eecc27256e760eeeb934a792/public/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamBudgetOverflow/frontend/2dd054a68ec7c483eecc27256e760eeeb934a792/public/favicon-32x32.png -------------------------------------------------------------------------------- /public/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamBudgetOverflow/frontend/2dd054a68ec7c483eecc27256e760eeeb934a792/public/favicon-96x96.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamBudgetOverflow/frontend/2dd054a68ec7c483eecc27256e760eeeb934a792/public/favicon.ico -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "transparent" 15 | } 16 | -------------------------------------------------------------------------------- /public/ms-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamBudgetOverflow/frontend/2dd054a68ec7c483eecc27256e760eeeb934a792/public/ms-icon-144x144.png -------------------------------------------------------------------------------- /public/ms-icon-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamBudgetOverflow/frontend/2dd054a68ec7c483eecc27256e760eeeb934a792/public/ms-icon-150x150.png -------------------------------------------------------------------------------- /public/ms-icon-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamBudgetOverflow/frontend/2dd054a68ec7c483eecc27256e760eeeb934a792/public/ms-icon-310x310.png -------------------------------------------------------------------------------- /public/ms-icon-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamBudgetOverflow/frontend/2dd054a68ec7c483eecc27256e760eeeb934a792/public/ms-icon-70x70.png -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Router from './shared/Router'; 4 | 5 | const App = () => { 6 | return ; 7 | }; 8 | 9 | export default App; 10 | -------------------------------------------------------------------------------- /src/assets/icons/accntSuccess.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamBudgetOverflow/frontend/2dd054a68ec7c483eecc27256e760eeeb934a792/src/assets/icons/accntSuccess.png -------------------------------------------------------------------------------- /src/assets/icons/goalEmpty.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamBudgetOverflow/frontend/2dd054a68ec7c483eecc27256e760eeeb934a792/src/assets/icons/goalEmpty.png -------------------------------------------------------------------------------- /src/assets/icons/goalSuccess.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamBudgetOverflow/frontend/2dd054a68ec7c483eecc27256e760eeeb934a792/src/assets/icons/goalSuccess.png -------------------------------------------------------------------------------- /src/assets/icons/ico_Google_logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/ico_KakaoTalk_logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/ico_Naver_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamBudgetOverflow/frontend/2dd054a68ec7c483eecc27256e760eeeb934a792/src/assets/icons/ico_Naver_logo.png -------------------------------------------------------------------------------- /src/assets/icons/prepare.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamBudgetOverflow/frontend/2dd054a68ec7c483eecc27256e760eeeb934a792/src/assets/icons/prepare.png -------------------------------------------------------------------------------- /src/assets/icons/success.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamBudgetOverflow/frontend/2dd054a68ec7c483eecc27256e760eeeb934a792/src/assets/icons/success.png -------------------------------------------------------------------------------- /src/assets/img/bank/BNK.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamBudgetOverflow/frontend/2dd054a68ec7c483eecc27256e760eeeb934a792/src/assets/img/bank/BNK.png -------------------------------------------------------------------------------- /src/assets/img/bank/Citi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamBudgetOverflow/frontend/2dd054a68ec7c483eecc27256e760eeeb934a792/src/assets/img/bank/Citi.png -------------------------------------------------------------------------------- /src/assets/img/bank/DGB.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamBudgetOverflow/frontend/2dd054a68ec7c483eecc27256e760eeeb934a792/src/assets/img/bank/DGB.png -------------------------------------------------------------------------------- /src/assets/img/bank/GJ.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamBudgetOverflow/frontend/2dd054a68ec7c483eecc27256e760eeeb934a792/src/assets/img/bank/GJ.png -------------------------------------------------------------------------------- /src/assets/img/bank/Hana.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamBudgetOverflow/frontend/2dd054a68ec7c483eecc27256e760eeeb934a792/src/assets/img/bank/Hana.png -------------------------------------------------------------------------------- /src/assets/img/bank/JB.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamBudgetOverflow/frontend/2dd054a68ec7c483eecc27256e760eeeb934a792/src/assets/img/bank/JB.png -------------------------------------------------------------------------------- /src/assets/img/bank/KB.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamBudgetOverflow/frontend/2dd054a68ec7c483eecc27256e760eeeb934a792/src/assets/img/bank/KB.png -------------------------------------------------------------------------------- /src/assets/img/bank/KDB.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamBudgetOverflow/frontend/2dd054a68ec7c483eecc27256e760eeeb934a792/src/assets/img/bank/KDB.png -------------------------------------------------------------------------------- /src/assets/img/bank/Kiup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamBudgetOverflow/frontend/2dd054a68ec7c483eecc27256e760eeeb934a792/src/assets/img/bank/Kiup.png -------------------------------------------------------------------------------- /src/assets/img/bank/NH.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamBudgetOverflow/frontend/2dd054a68ec7c483eecc27256e760eeeb934a792/src/assets/img/bank/NH.png -------------------------------------------------------------------------------- /src/assets/img/bank/Post.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamBudgetOverflow/frontend/2dd054a68ec7c483eecc27256e760eeeb934a792/src/assets/img/bank/Post.png -------------------------------------------------------------------------------- /src/assets/img/bank/SC.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamBudgetOverflow/frontend/2dd054a68ec7c483eecc27256e760eeeb934a792/src/assets/img/bank/SC.png -------------------------------------------------------------------------------- /src/assets/img/bank/SH.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamBudgetOverflow/frontend/2dd054a68ec7c483eecc27256e760eeeb934a792/src/assets/img/bank/SH.png -------------------------------------------------------------------------------- /src/assets/img/bank/SMG.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamBudgetOverflow/frontend/2dd054a68ec7c483eecc27256e760eeeb934a792/src/assets/img/bank/SMG.png -------------------------------------------------------------------------------- /src/assets/img/bank/Shinhan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamBudgetOverflow/frontend/2dd054a68ec7c483eecc27256e760eeeb934a792/src/assets/img/bank/Shinhan.png -------------------------------------------------------------------------------- /src/assets/img/bank/Shinhyup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamBudgetOverflow/frontend/2dd054a68ec7c483eecc27256e760eeeb934a792/src/assets/img/bank/Shinhyup.png -------------------------------------------------------------------------------- /src/assets/img/bank/Woori.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamBudgetOverflow/frontend/2dd054a68ec7c483eecc27256e760eeeb934a792/src/assets/img/bank/Woori.png -------------------------------------------------------------------------------- /src/assets/img/default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamBudgetOverflow/frontend/2dd054a68ec7c483eecc27256e760eeeb934a792/src/assets/img/default.png -------------------------------------------------------------------------------- /src/assets/img/goal/group_color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamBudgetOverflow/frontend/2dd054a68ec7c483eecc27256e760eeeb934a792/src/assets/img/goal/group_color.png -------------------------------------------------------------------------------- /src/assets/img/goal/group_gray.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamBudgetOverflow/frontend/2dd054a68ec7c483eecc27256e760eeeb934a792/src/assets/img/goal/group_gray.png -------------------------------------------------------------------------------- /src/assets/img/goal/personal_color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamBudgetOverflow/frontend/2dd054a68ec7c483eecc27256e760eeeb934a792/src/assets/img/goal/personal_color.png -------------------------------------------------------------------------------- /src/assets/img/goal/personal_gray.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamBudgetOverflow/frontend/2dd054a68ec7c483eecc27256e760eeeb934a792/src/assets/img/goal/personal_gray.png -------------------------------------------------------------------------------- /src/assets/img/guide/line.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamBudgetOverflow/frontend/2dd054a68ec7c483eecc27256e760eeeb934a792/src/assets/img/guide/line.png -------------------------------------------------------------------------------- /src/assets/img/guide/scroll.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamBudgetOverflow/frontend/2dd054a68ec7c483eecc27256e760eeeb934a792/src/assets/img/guide/scroll.png -------------------------------------------------------------------------------- /src/assets/img/onboarding/onboarding1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamBudgetOverflow/frontend/2dd054a68ec7c483eecc27256e760eeeb934a792/src/assets/img/onboarding/onboarding1.png -------------------------------------------------------------------------------- /src/assets/img/onboarding/onboarding2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamBudgetOverflow/frontend/2dd054a68ec7c483eecc27256e760eeeb934a792/src/assets/img/onboarding/onboarding2.png -------------------------------------------------------------------------------- /src/assets/img/onboarding/onboarding3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamBudgetOverflow/frontend/2dd054a68ec7c483eecc27256e760eeeb934a792/src/assets/img/onboarding/onboarding3.png -------------------------------------------------------------------------------- /src/assets/img/onboarding/onboarding4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamBudgetOverflow/frontend/2dd054a68ec7c483eecc27256e760eeeb934a792/src/assets/img/onboarding/onboarding4.png -------------------------------------------------------------------------------- /src/components/account/AccountSelectSection.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | import AccountInfoCard from './AccountInfoCard'; 5 | 6 | import { IAccount } from '../../interfaces/interfaces'; 7 | 8 | interface AccountSelectProps { 9 | accounts: Array; 10 | accountSelectHandler: (accountId: number) => void; 11 | } 12 | 13 | const AccountSelect = ({ accounts, accountSelectHandler }: AccountSelectProps) => { 14 | const handleSelect = (accountId: number) => { 15 | accountSelectHandler(accountId); 16 | }; 17 | 18 | return ( 19 | 20 | 21 | 연결된 계좌 22 | {accounts.map((account) => ( 23 | handleSelect(account.accountId)} 27 | /> 28 | ))} 29 | 30 | 31 | ); 32 | }; 33 | 34 | const Wrapper = styled.div` 35 | display: flex; 36 | flex-direction: column; 37 | justify-content: space-between; 38 | width: 100%; 39 | `; 40 | 41 | const ContentWrapper = styled.div` 42 | display: flex; 43 | flex-direction: column; 44 | gap: 20px; 45 | `; 46 | 47 | const SubTitle = styled.div` 48 | font: ${(props) => props.theme.paragraphsP3M}; 49 | `; 50 | 51 | export default AccountSelect; 52 | -------------------------------------------------------------------------------- /src/components/common/alert/Alert.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | interface AlertProps { 5 | height?: string; 6 | showBgColor: boolean; 7 | children: React.ReactNode; 8 | } 9 | 10 | const Alert = ({ height, showBgColor, children }: AlertProps) => { 11 | return ( 12 | 13 | 14 | {children} 15 | 16 | 17 | ); 18 | }; 19 | 20 | const AlertWrapper = styled.div<{ height?: string }>` 21 | display: flex; 22 | flex-direction: column; 23 | justify-content: center; 24 | align-items: center; 25 | width: 100%; 26 | height: ${(props) => props.height}; 27 | `; 28 | 29 | const AlertBox = styled.div<{ showBgColor: boolean; height?: string }>` 30 | padding: 20px 0; 31 | display: flex; 32 | flex-direction: column; 33 | justify-content: center; 34 | align-items: center; 35 | width: 100%; 36 | height: ${(props) => props.height}; 37 | border-radius: 16px; 38 | background-color: ${(props) => (props.showBgColor ? props.theme.gray300 : 'transparent')}; 39 | `; 40 | 41 | export default Alert; 42 | -------------------------------------------------------------------------------- /src/components/common/alert/InfoError.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | import Icon from '../elem/Icon'; 5 | import Contact from '../elem/Contact'; 6 | 7 | const InfoError = () => { 8 | return ( 9 | 10 | 11 | 17 | 18 | 19 | 20 | 문제가 발생했습니다. 21 |
22 | 관리자에게 문의해주세요 23 |
24 | 25 |
26 |
27 | ); 28 | }; 29 | 30 | const Wrapper = styled.div` 31 | width: 100%; 32 | height: 100%; 33 | background-color: white; 34 | `; 35 | 36 | const IconWrapper = styled.div` 37 | position: relative; 38 | display: flex; 39 | flex-direction: column; 40 | justify-content: center; 41 | align-items: center; 42 | width: 100%; 43 | height: 100%; 44 | `; 45 | 46 | const TextWrapper = styled.div` 47 | position: absolute; 48 | top: calc(50% + 120px); 49 | display: flex; 50 | flex-direction: column; 51 | gap: 4px; 52 | width: 100%; 53 | `; 54 | 55 | const Text = styled.div` 56 | line-height: 150%; 57 | text-align: center; 58 | font: ${(props) => props.theme.headingH2}; 59 | `; 60 | 61 | export default InfoError; 62 | -------------------------------------------------------------------------------- /src/components/common/alert/InfoLoading.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | import LoadingIcon from '../elem/LoadingIcon'; 5 | 6 | function InfoLoading() { 7 | return ( 8 | 9 | 10 | 11 | ); 12 | } 13 | 14 | const Wrapper = styled.div` 15 | display: flex; 16 | flex-direction: column; 17 | justify-content: center; 18 | align-items: center; 19 | width: 100%; 20 | height: 100%; 21 | background-color: white; 22 | `; 23 | 24 | export default InfoLoading; 25 | -------------------------------------------------------------------------------- /src/components/common/elem/BadgeBox.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | const BadgeBox = ({ imgURL }: { imgURL: string }) => { 5 | return ; 6 | }; 7 | 8 | const Badge = styled.img` 9 | width: 100%; 10 | max-width: 100px; 11 | max-height: 100px; 12 | aspect-ratio: 1; 13 | `; 14 | 15 | export default BadgeBox; 16 | -------------------------------------------------------------------------------- /src/components/common/elem/BankBox.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | import BankIcons from './BankIcons'; 5 | 6 | interface BankBoxProps { 7 | id: number; 8 | name: string; 9 | selectHandler: (id: number) => void; 10 | } 11 | 12 | function BankBox({ id, name, selectHandler }: BankBoxProps) { 13 | return ( 14 | selectHandler(id)}> 15 | 16 | {name.length > 4 ? name.slice(0, 4) : name} 17 | 18 | ); 19 | } 20 | 21 | const Bank = styled.div` 22 | padding: 10px 22px; 23 | display: flex; 24 | flex-direction: column; 25 | align-items: center; 26 | gap: 8px; 27 | width: calc(100% - 44px); 28 | height: calc(100% - 20px); 29 | border-radius: 8px; 30 | background-color: ${(props) => props.theme.gray300}; 31 | `; 32 | 33 | const Img = styled.div` 34 | width: 40px; 35 | height: 40px; 36 | background-color: black; 37 | `; 38 | 39 | const Name = styled.span` 40 | font: ${(props) => props.theme.captionC2}; 41 | `; 42 | 43 | export default BankBox; 44 | -------------------------------------------------------------------------------- /src/components/common/elem/C2TextBox.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | const C2TextBox = ({ text }: { text: string }) => { 5 | return {text}; 6 | }; 7 | 8 | const TextBox = styled.div` 9 | font: ${(props) => props.theme.captionC2}; 10 | `; 11 | 12 | export default C2TextBox; 13 | -------------------------------------------------------------------------------- /src/components/common/elem/Contact.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | const Contact = () => { 5 | return sonewdim@naver.com; 6 | }; 7 | 8 | const Wrapper = styled.div` 9 | text-align: center; 10 | font: ${(props) => props.theme.paragraphsP3R}; 11 | `; 12 | 13 | export default Contact; 14 | -------------------------------------------------------------------------------- /src/components/common/elem/DateSelectBox.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | interface DateSelectBoxProps { 5 | // date input box value should be 'YYYY-MM-DD' format 6 | value: string; 7 | min: string; 8 | max: string; 9 | onChange: (e: React.FormEvent) => void; 10 | } 11 | 12 | function DateSelectBox({ value, min, max, onChange }: DateSelectBoxProps) { 13 | return ; 14 | } 15 | 16 | const DateSelect = styled.input` 17 | padding: 0 10px; 18 | width: calc(100% - 20px); 19 | height: 100%; 20 | font: ${(props) => props.theme.captionC2}; 21 | border: 1px solid black; 22 | `; 23 | 24 | export default DateSelectBox; 25 | -------------------------------------------------------------------------------- /src/components/common/elem/EmojiBox.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import { Emoji } from 'emoji-picker-react'; 4 | 5 | interface Emoji { 6 | unicode: string; 7 | boxSize: number; 8 | emojiSize: number; 9 | showBg?: boolean; 10 | } 11 | 12 | const EmojiBox = ({ unicode, boxSize, emojiSize, showBg = true }: Emoji) => { 13 | return ( 14 | 15 | 16 | 17 | ); 18 | }; 19 | 20 | const Wrapper = styled.div<{ size: string; showBg: boolean }>` 21 | display: flex; 22 | justify-content: center; 23 | align-items: center; 24 | width: ${(props) => props.size}; 25 | height: ${(props) => props.size}; 26 | border-radius: 8px; 27 | background-color: ${(props) => (props.showBg ? props.theme.gray100 : 'transparent')}; 28 | `; 29 | 30 | export default EmojiBox; 31 | -------------------------------------------------------------------------------- /src/components/common/elem/ErrorMsg.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import Icon from './Icon'; 4 | 5 | const ErrorMsg = () => { 6 | return ( 7 | 8 | 14 | 15 | 문제가 발생했습니다. 16 |
17 | 관리자에게 문의해주세요 18 |
19 | sonewdim@naver.com 20 |
21 | ); 22 | }; 23 | 24 | const Wrapper = styled.div` 25 | display: flex; 26 | flex-direction: column; 27 | align-items: center; 28 | gap: 5px; 29 | `; 30 | 31 | const Text = styled.div` 32 | line-height: 30px; 33 | text-align: center; 34 | font: ${(props) => props.theme.captionC1}; 35 | `; 36 | 37 | const Contact = styled.div` 38 | text-align: center; 39 | font: ${(props) => props.theme.captionC2}; 40 | `; 41 | 42 | export default ErrorMsg; 43 | -------------------------------------------------------------------------------- /src/components/common/elem/Icon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | interface IconProps { 5 | width: number; 6 | height: number; 7 | color: string; 8 | path: string; 9 | } 10 | 11 | const Icon = ({ width, height, color, path }: IconProps) => { 12 | return ( 13 | 14 | 15 | 16 | ); 17 | }; 18 | 19 | const SVGIcon = styled.svg<{ width: string; height: string }>` 20 | width: ${(props) => props.width}; 21 | height: ${(props) => props.height}; 22 | `; 23 | 24 | const Path = styled.path<{ color: string }>` 25 | fill: ${(props) => { 26 | switch (props.color) { 27 | case 'primary400': 28 | return props.theme.primary400; 29 | case 'gray400': 30 | return props.theme.gray400; 31 | case 'secondary400': 32 | return props.theme.secondary400; 33 | case 'black': 34 | return 'black'; 35 | case 'white': 36 | return 'white'; 37 | default: 38 | return props.color; 39 | } 40 | }}; 41 | `; 42 | 43 | export default Icon; 44 | -------------------------------------------------------------------------------- /src/components/common/elem/InputBox.tsx: -------------------------------------------------------------------------------- 1 | import React, { forwardRef, Ref } from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | interface InputBoxProps { 5 | type: 'text' | 'password'; 6 | value?: string | number; 7 | placeholder?: string; 8 | onChangeHandler?: (e: React.FormEvent) => void; 9 | onKeyPressHandler?: (e: React.KeyboardEvent) => void; 10 | onFocusHandler?: (e: React.FocusEvent) => void; 11 | onBlurHandler?: (e: React.FocusEvent) => void; 12 | isDisabled?: boolean; 13 | showBorder?: boolean; 14 | showTextCounter?: boolean; 15 | maxLen?: number; 16 | textLen?: number; 17 | } 18 | 19 | const InputBox = ( 20 | { 21 | type, 22 | value, 23 | placeholder, 24 | onChangeHandler, 25 | onKeyPressHandler, 26 | onFocusHandler, 27 | onBlurHandler, 28 | isDisabled, 29 | showBorder = true, 30 | showTextCounter = false, 31 | maxLen, 32 | textLen, 33 | }: InputBoxProps, 34 | ref?: Ref 35 | ) => { 36 | return ( 37 | 38 | 50 | {showTextCounter ? {`${textLen}/${maxLen}`} : <>} 51 | 52 | ); 53 | }; 54 | 55 | const Wrapper = styled.div` 56 | position: relative; 57 | display: flex; 58 | flex-direction: row; 59 | align-items: center; 60 | width: 100%; 61 | height: 100%; 62 | `; 63 | 64 | const Input = styled.input<{ showBorder: boolean }>` 65 | padding: 3px 0; 66 | width: 100%; 67 | height: 100%; 68 | border: none; 69 | border-bottom: ${(props) => (props.showBorder ? '1px solid black' : '')}; 70 | font: ${(props) => props.theme.paragraphsP3R}; 71 | color: black; 72 | background-color: transparent; 73 | :focus { 74 | outline: none; 75 | } 76 | `; 77 | 78 | const InputCounter = styled.span` 79 | position: absolute; 80 | padding: 9px 5px 4px; 81 | right: 0; 82 | display: flex; 83 | flex-direction: row; 84 | align-items: center; 85 | font: ${(props) => props.theme.captionC2}; 86 | color: ${(props) => props.theme.gray600}; 87 | background-color: white; 88 | `; 89 | 90 | export default forwardRef(InputBox); 91 | -------------------------------------------------------------------------------- /src/components/common/elem/LoadingIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactLoading from 'react-loading'; 3 | 4 | interface LoadingIconProps { 5 | size: number; 6 | color: string; 7 | } 8 | 9 | function LoadingIcon({ size, color }: LoadingIconProps) { 10 | return ; 11 | } 12 | 13 | export default LoadingIcon; 14 | -------------------------------------------------------------------------------- /src/components/common/elem/LoadingMsg.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import LoadingIcon from './LoadingIcon'; 4 | 5 | const LoadingMsg = () => { 6 | return ( 7 | 8 | 9 | 데이터를 불러오는 중 입니다 10 | 11 | ); 12 | }; 13 | 14 | const Wrapper = styled.div` 15 | display: flex; 16 | flex-direction: column; 17 | align-items: center; 18 | gap: 20px; 19 | `; 20 | 21 | const Text = styled.div` 22 | font: ${(props) => props.theme.captionC2}; 23 | `; 24 | 25 | export default LoadingMsg; 26 | -------------------------------------------------------------------------------- /src/components/common/elem/LoginButton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | import NaverLogo from '../../../assets/icons/ico_Naver_logo.png'; 5 | import KakaoLogo from '../../../assets/icons/ico_KakaoTalk_logo.svg'; 6 | import GoogleLogo from '../../../assets/icons/ico_Google_logo.svg'; 7 | 8 | interface LoginButtonProps { 9 | text: string; 10 | method: 'naver' | 'kakao' | 'google'; 11 | onClickHandler: () => void; 12 | } 13 | 14 | const LoginButton = ({ text, method, onClickHandler }: LoginButtonProps) => { 15 | const buttonLogo = (method: 'naver' | 'kakao' | 'google') => { 16 | switch (method) { 17 | case 'naver': 18 | return ; 19 | case 'kakao': 20 | return ; 21 | case 'google': 22 | return ; 23 | } 24 | }; 25 | 26 | return ( 27 | 31 | ); 32 | }; 33 | 34 | const loginButtonStyles = { 35 | naver: { 36 | bgColor: '#03C75A', 37 | fontColor: 'white', 38 | border: 'none', 39 | }, 40 | kakao: { 41 | bgColor: '#f9e000', 42 | fontColor: 'black', 43 | border: 'none', 44 | }, 45 | google: { 46 | bgColor: 'white', 47 | fontColor: 'black', 48 | border: '1px solid', 49 | }, 50 | }; 51 | 52 | const Button = styled.button<{ method: 'naver' | 'kakao' | 'google' }>` 53 | width: 100%; 54 | display: flex; 55 | flex-direction: row; 56 | justify-content: center; 57 | align-items: center; 58 | gap: 10px; 59 | border: ${(props) => loginButtonStyles[props.method].border}; 60 | border-radius: 8px; 61 | background-color: ${(props) => loginButtonStyles[props.method].bgColor}; 62 | :hover { 63 | cursor: pointer; 64 | } 65 | `; 66 | 67 | const LogoWrapper = styled.div` 68 | text-align: center; 69 | padding: 10px 0; 70 | `; 71 | 72 | const TextWrapper = styled.div<{ method: 'naver' | 'kakao' | 'google' }>` 73 | text-align: center; 74 | padding: 10px 0; 75 | font: ${(props) => props.theme.paragraphP2M}; 76 | color: ${(props) => loginButtonStyles[props.method].fontColor}; 77 | `; 78 | 79 | export default LoginButton; 80 | -------------------------------------------------------------------------------- /src/components/common/elem/ModalBox.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent } from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | interface ModalBoxProps { 5 | show: boolean; 6 | maxScreenHeight: number; 7 | bgColor?: string; 8 | children: React.ReactNode; 9 | } 10 | 11 | const ModalBox: FunctionComponent = ({ show, maxScreenHeight, children, bgColor }) => { 12 | return ( 13 | 14 | 15 | {children} 16 | 17 | 18 | ); 19 | }; 20 | 21 | const Wrapper = styled.div<{ show: boolean }>` 22 | position: absolute; 23 | top: 0; 24 | left: 0; 25 | z-index: 10; 26 | display: ${(props) => (props.show ? '' : 'none')}; 27 | width: 100%; 28 | height: 100%; 29 | background-color: rgba(0, 0, 0, 0.5); 30 | `; 31 | 32 | const Modal = styled.div<{ bgColor?: string; maxScreenHeight: number }>` 33 | position: absolute; 34 | bottom: 0; 35 | left: 0; 36 | padding: 22px; 37 | width: calc(100% - 44px); 38 | max-height: 616px; 39 | border-radius: 16px 16px 0 0; 40 | background-color: ${(props) => (props.bgColor ? `${props.bgColor}` : 'white')}; 41 | @media screen and (max-height: ${(props) => `${props.maxScreenHeight}px`}) { 42 | height: calc(100% - 100px); 43 | } 44 | `; 45 | 46 | const ContentWrapper = styled.div` 47 | position: relative; 48 | width: 100%; 49 | height: 100%; 50 | `; 51 | 52 | export default ModalBox; 53 | -------------------------------------------------------------------------------- /src/components/common/elem/OptionSelectBox.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | interface OptionSelectBoxProps { 5 | placeholder: string; 6 | value: string; 7 | onClickHandler: () => void; 8 | } 9 | 10 | const OptionSelectBox = ({ placeholder, value, onClickHandler }: OptionSelectBoxProps) => { 11 | return ( 12 | 13 | 17 | 18 | ); 19 | }; 20 | 21 | const Wrapper = styled.div` 22 | position: relative; 23 | display: flex; 24 | flex-direction: row; 25 | align-items: center; 26 | width: 100%; 27 | height: 100%; 28 | `; 29 | 30 | const Select = styled.div` 31 | position: relative; 32 | padding: 4px 0; 33 | width: 100%; 34 | text-align: left; 35 | font: ${(props) => props.theme.paragraphsP3R}; 36 | color: ${(props) => props.theme.gray600}; 37 | border-bottom: 1px solid black; 38 | `; 39 | 40 | const Button = styled.div` 41 | position: absolute; 42 | right: 0; 43 | top: 0; 44 | margin-bottom: 4px; 45 | width: 24px; 46 | height: 24px; 47 | border: 1px solid black; 48 | `; 49 | 50 | export default OptionSelectBox; 51 | -------------------------------------------------------------------------------- /src/components/common/elem/ProfileImg.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import LoadingIcon from './LoadingIcon'; 4 | 5 | interface ProfileImgProps { 6 | url: string; 7 | size: number; 8 | isLoading?: boolean; 9 | borderColor?: string; 10 | } 11 | 12 | const ProfileImg = ({ url, size, isLoading, borderColor }: ProfileImgProps) => { 13 | return ( 14 | <> 15 | {isLoading ? ( 16 | 17 | 18 | 19 | ) : ( 20 | 25 | )} 26 | 27 | ); 28 | }; 29 | 30 | const LoadingImg = styled.div<{ size: string }>` 31 | display: flex; 32 | flex-direction: row; 33 | justify-content: center; 34 | align-items: center; 35 | width: ${(props) => props.size}; 36 | height: ${(props) => props.size}; 37 | border-radius: 50%; 38 | background-color: ${(props) => props.theme.gray300}; 39 | `; 40 | 41 | const Img = styled.img<{ size: string; borderColor?: string }>` 42 | width: ${(props) => props.size}; 43 | height: ${(props) => props.size}; 44 | border-radius: 50%; 45 | border: ${(props) => (props.borderColor ? `1px solid ${props.borderColor}` : `1px solid ${props.theme.gray500}`)}; 46 | `; 47 | 48 | export default ProfileImg; 49 | -------------------------------------------------------------------------------- /src/components/common/elem/ProgressBar.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | interface ProgressBarProps { 5 | percentage: number; 6 | height: number; 7 | borderRadius: number; 8 | } 9 | 10 | const ProgressBar = ({ percentage, height, borderRadius }: ProgressBarProps) => { 11 | return ( 12 | 13 | 14 | 15 | ); 16 | }; 17 | 18 | const BarWrapper = styled.div<{ height: string; borderRadius: string }>` 19 | position: relative; 20 | width: 100%; 21 | height: ${(props) => props.height}; 22 | border-radius: ${(props) => props.borderRadius}; 23 | background-color: ${(props) => props.theme.primary50}; 24 | `; 25 | 26 | const Bar = styled.div<{ width: string; height: string; borderRadius: string }>` 27 | position: absolute; 28 | top: 0; 29 | left: 0; 30 | width: ${(props) => props.width}; 31 | height: ${(props) => props.height}; 32 | border-radius: ${(props) => props.borderRadius}; 33 | background-color: ${(props) => props.theme.primary500}; 34 | `; 35 | 36 | export default ProgressBar; 37 | -------------------------------------------------------------------------------- /src/components/common/elem/RadioInput.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | const RadioInput = () => { 5 | return ; 6 | }; 7 | 8 | const Radio = styled.div` 9 | width: 20px; 10 | height: 20px; 11 | border-radius: 50%; 12 | background-color: white; 13 | `; 14 | 15 | export default RadioInput; 16 | -------------------------------------------------------------------------------- /src/components/common/elem/RadioSelectBox.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | interface RadioSelectBoxProps { 5 | options: Array; 6 | selected?: string; 7 | onChangeHandler: (e: React.ChangeEvent) => void; 8 | flexDirection?: 'row' | 'column'; 9 | alignItems?: 'center' | 'flex-start'; 10 | } 11 | 12 | function RadioSelectBox({ options, selected, onChangeHandler, flexDirection, alignItems }: RadioSelectBoxProps) { 13 | return ( 14 | 15 | {options.map((option) => ( 16 | 17 | 18 | 19 | {option} 20 | 21 | 22 | ))} 23 | 24 | ); 25 | } 26 | 27 | const Wrapper = styled.div<{ flexDirection: string; alignItems: string }>` 28 | padding: 10px 0; 29 | width: 100%; 30 | display: flex; 31 | flex-direction: ${(props) => props.flexDirection}; 32 | align-items: ${(props) => props.alignItems}; 33 | gap: 20px; 34 | `; 35 | 36 | const RadioSelectWrapper = styled.div` 37 | display: flex; 38 | flex-direction: row; 39 | justify-items: auto; 40 | align-items: center; 41 | gap: 16px; 42 | `; 43 | 44 | const RadioLabel = styled.span` 45 | font: ${(props) => props.theme.paragraphsP2M}; 46 | `; 47 | 48 | export default RadioSelectBox; 49 | -------------------------------------------------------------------------------- /src/components/common/elem/SettingButton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | interface SettingButtonProps { 5 | text: string; 6 | onClickHandler: () => void; 7 | } 8 | 9 | const SettingButton = ({ text, onClickHandler }: SettingButtonProps) => { 10 | return ( 11 | 12 | 13 | 14 | ); 15 | }; 16 | 17 | const SettingButtonWrapper = styled.div` 18 | display: flex; 19 | flex-direction: row; 20 | width: 100%; 21 | `; 22 | 23 | const Button = styled.button` 24 | font: ${(props) => props.theme.paragraphsP3R}; 25 | border: none; 26 | background-color: transparent; 27 | `; 28 | 29 | export default SettingButton; 30 | -------------------------------------------------------------------------------- /src/components/common/elem/TextButton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | interface TextButtonProps { 5 | text: string; 6 | bgColor?: string; 7 | color?: string; 8 | font?: string; 9 | padding?: number; 10 | onClickHandler: () => void; 11 | isDisabled?: boolean; 12 | } 13 | 14 | const TextButton = ({ text, bgColor, color, font, padding, onClickHandler, isDisabled }: TextButtonProps) => { 15 | return ( 16 | 21 | ); 22 | }; 23 | 24 | const Button = styled.button<{ bgColor?: string; disable?: boolean }>` 25 | width: 100%; 26 | display: flex; 27 | flex-direction: row; 28 | justify-content: center; 29 | align-items: center; 30 | border: none; 31 | border-radius: 8px; 32 | background-color: ${(props) => 33 | props.disable ? props.theme.gray300 : props.bgColor ? `${props.bgColor}` : props.theme.primary400}; 34 | :hover { 35 | cursor: pointer; 36 | } 37 | `; 38 | 39 | const TextWrapper = styled.div<{ 40 | bgColor?: string; 41 | color?: string; 42 | font?: string; 43 | padding?: number; 44 | disable?: boolean; 45 | }>` 46 | padding: ${(props) => (props.padding ? `${props.padding}px 0` : '12px 0')}; 47 | font: ${(props) => (props.font ? props.font : props.theme.paragraphsP2M)}; 48 | color: ${(props) => 49 | props.disable ? 'black' : props.bgColor === 'gray' ? 'black' : props.color ? `${props.color}` : 'white'}; 50 | `; 51 | 52 | export default TextButton; 53 | -------------------------------------------------------------------------------- /src/components/common/elem/ValidateMsg.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | interface ValidateMsgProps { 5 | msg: string; 6 | type: 'error' | 'success'; 7 | } 8 | 9 | const ValidateMsg = ({ msg, type }: ValidateMsgProps) => { 10 | return {msg}; 11 | }; 12 | 13 | const Msg = styled.p<{ type: 'error' | 'success' }>` 14 | font: ${(props) => props.theme.captionC3}; 15 | color: ${(props) => (props.type === 'error' ? 'red' : 'blue')}; 16 | `; 17 | 18 | export default ValidateMsg; 19 | -------------------------------------------------------------------------------- /src/components/common/elem/btn/AddGoalBtn.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useNavigate } from 'react-router-dom'; 3 | import styled from 'styled-components'; 4 | 5 | import Icon from '../Icon'; 6 | 7 | const AddGoalBtn = () => { 8 | const navigate = useNavigate(); 9 | 10 | return ( 11 | navigate('/goals/post/type')}> 12 | 13 | 19 | 20 | 21 | ); 22 | }; 23 | 24 | const ButtonBox = styled.div` 25 | position: absolute; 26 | bottom: 12px; 27 | right: 22px; 28 | display: flex; 29 | flex-direction: column; 30 | justify-content: center; 31 | align-items: center; 32 | width: 60px; 33 | height: 60px; 34 | border-radius: 50%; 35 | background-color: ${(props) => props.theme.primary400}; 36 | box-shadow: 2px 2px 8px rgba(0, 0, 0, 0.3); 37 | :hover { 38 | cursor: pointer; 39 | } 40 | `; 41 | 42 | const IconWrapper = styled.div` 43 | display: flex; 44 | flex-direction: row; 45 | justify-content: center; 46 | align-items: center; 47 | width: 20px; 48 | height: 20px; 49 | `; 50 | 51 | export default AddGoalBtn; 52 | -------------------------------------------------------------------------------- /src/components/common/elem/btn/CloseIconBtn.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | import Icon from '../Icon'; 5 | 6 | interface CloseIconBtnProps { 7 | color?: string; 8 | closeHandler: () => void; 9 | } 10 | 11 | const CloseIconBtn = ({ color = 'primary400', closeHandler }: CloseIconBtnProps) => { 12 | return ( 13 | 14 | 22 | 23 | ); 24 | }; 25 | 26 | const ButtonBox = styled.div` 27 | display: flex; 28 | flex-direction: row; 29 | justify-content: flex-end; 30 | align-items: center; 31 | width: 100%; 32 | `; 33 | 34 | const Button = styled.div` 35 | display: flex; 36 | flex-direction: row; 37 | justify-content: center; 38 | align-items: center; 39 | width: 24px; 40 | height: 24px; 41 | `; 42 | 43 | export default CloseIconBtn; 44 | -------------------------------------------------------------------------------- /src/components/common/elem/btn/ImgEditBtn.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | import Icon from '../Icon'; 5 | 6 | interface ImgEditBtnProps { 7 | btnSize: number; 8 | clickHandler: () => void; 9 | } 10 | 11 | const ImgEditBtn = ({ btnSize, clickHandler }: ImgEditBtnProps) => { 12 | return ( 13 | 21 | ); 22 | }; 23 | 24 | const Button = styled.div<{ size: string }>` 25 | display: flex; 26 | flex-direction: row; 27 | justify-content: center; 28 | align-items: center; 29 | width: ${(props) => props.size}; 30 | height: ${(props) => props.size}; 31 | border-radius: 50%; 32 | border: none; 33 | background-color: ${(props) => props.theme.gray300}; 34 | `; 35 | 36 | export default ImgEditBtn; 37 | -------------------------------------------------------------------------------- /src/components/common/tag/DdayTag.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import styled from 'styled-components'; 3 | import { dDayCalculator } from '../../../utils/dDayCalculator'; 4 | 5 | const DdayTag = ({ targetDate }: { targetDate: Date }) => { 6 | const [days, setDays] = useState(0); 7 | useEffect(() => { 8 | setDays(dDayCalculator(targetDate)); 9 | }, [targetDate]); 10 | 11 | if (days < 0) return <>; 12 | 13 | return {`D-${days === 0 ? 'day' : days}`}; 14 | }; 15 | 16 | const Tag = styled.div` 17 | padding: 4px 12px; 18 | font: ${(props) => props.theme.captionC2}; 19 | border-radius: 15px; 20 | color: white; 21 | background-color: ${(props) => props.theme.primary400}; 22 | `; 23 | 24 | export default DdayTag; 25 | -------------------------------------------------------------------------------- /src/components/common/tag/FilterTag.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | import CloseIconBtn from '../elem/btn/CloseIconBtn'; 5 | import Icon from '../elem/Icon'; 6 | 7 | interface FilterTagProps { 8 | value: string; 9 | removeHandler?: (value: string) => void; 10 | } 11 | 12 | const FilterTag = React.memo(function FilterTag({ value, removeHandler }: FilterTagProps) { 13 | return ( 14 | 15 | {value} 16 | {removeHandler ? ( 17 | removeHandler(value)} /> 18 | ) : ( 19 | 20 | 21 | 22 | )} 23 | 24 | ); 25 | }); 26 | 27 | const Tag = styled.div` 28 | padding: 4px 12px; 29 | display: flex; 30 | flex-direction: row; 31 | align-items: center; 32 | gap: 5px; 33 | flex: 0 0 auto; 34 | font: ${(props) => props.theme.captionC1}; 35 | border-radius: 8px; 36 | border: 2px solid ${(props) => props.theme.gray300}; 37 | white-space: nowrap; 38 | `; 39 | 40 | const IconWrapper = styled.div` 41 | display: flex; 42 | flex-direction: row; 43 | justify-content: center; 44 | align-items: center; 45 | width: 24px; 46 | height: 24px; 47 | `; 48 | 49 | export default FilterTag; 50 | -------------------------------------------------------------------------------- /src/components/common/tag/HashTag.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | import Icon from '../elem/Icon'; 5 | 6 | interface HashTagProps { 7 | tag: string; 8 | removeHandler?: (tag: string) => void; 9 | } 10 | 11 | const HashTag = React.memo(function HashTag({ tag, removeHandler }: HashTagProps) { 12 | return ( 13 | 14 | {`#${tag}`} 15 | {!removeHandler ? ( 16 | <> 17 | ) : ( 18 | removeHandler(tag)}> 19 | 25 | 26 | )} 27 | 28 | ); 29 | }); 30 | 31 | const Tag = styled.div` 32 | display: flex; 33 | flex-direction: row; 34 | align-items: center; 35 | gap: 5px; 36 | flex: 0 0 auto; 37 | font: ${(props) => props.theme.captionC2}; 38 | color: ${(props) => props.theme.primary400}; 39 | border-radius: 16px; 40 | `; 41 | 42 | const DeleteButton = styled.div` 43 | display: flex; 44 | flex-direction: column; 45 | justify-content: center; 46 | align-items: center; 47 | width: 1rem; 48 | height: 1rem; 49 | background-color: transparent; 50 | :hover { 51 | cursor: pointer; 52 | } 53 | `; 54 | 55 | export default HashTag; 56 | -------------------------------------------------------------------------------- /src/components/common/tag/StateTag.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | import { GoalStatus } from '../../../interfaces/interfaces'; 5 | 6 | const stateKR = (state: GoalStatus) => { 7 | switch (state) { 8 | case GoalStatus.recruit: 9 | return '진행 예정'; 10 | case GoalStatus.proceeding: 11 | return '진행중'; 12 | case GoalStatus.done: 13 | return '종료'; 14 | default: 15 | return ''; 16 | } 17 | }; 18 | 19 | const stateColor = (state: GoalStatus) => { 20 | switch (state) { 21 | case GoalStatus.recruit: 22 | return '#f9c342'; 23 | case GoalStatus.proceeding: 24 | return '#009642'; 25 | case GoalStatus.done: 26 | return 'black'; 27 | default: 28 | return ''; 29 | } 30 | }; 31 | 32 | const StateTag = ({ state }: { state: GoalStatus }) => { 33 | return ( 34 | 35 | 36 | {stateKR(state)} 37 | 38 | ); 39 | }; 40 | 41 | const Tag = styled.div` 42 | display: flex; 43 | flex-direction: row; 44 | align-items: center; 45 | gap: 5px; 46 | `; 47 | 48 | const StateCircle = styled.div<{ color: string }>` 49 | width: 8px; 50 | height: 8px; 51 | border-radius: 50%; 52 | background-color: ${(props) => props.color}; 53 | `; 54 | 55 | const Text = styled.div<{ color: string }>` 56 | font: ${(props) => props.theme.captionC2}; 57 | color: ${(props) => props.color}; 58 | `; 59 | 60 | export default StateTag; 61 | -------------------------------------------------------------------------------- /src/components/goal/GroupGoalCardSmall.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useNavigate } from 'react-router-dom'; 3 | import styled from 'styled-components'; 4 | 5 | import EmojiBox from '../common/elem/EmojiBox'; 6 | 7 | import { ISearchGoal } from '../../interfaces/interfaces'; 8 | 9 | const GroupGoalCardSmall = ({ goal }: { goal: ISearchGoal }) => { 10 | const navigate = useNavigate(); 11 | return ( 12 | navigate(`/goals/${goal.goalId}`)}> 13 | 14 | 15 | {goal.title.length > 7 ? `${goal.title.slice(0, 7)}...` : goal.title} 16 | 17 | {goal.hashTag.slice(0, 2).map((tag) => { 18 | if (tag.length === 0) { 19 | return ; 20 | } 21 | return {`#${tag.length > 3 ? `${tag.slice(0, 3)}...` : tag}`}; 22 | })} 23 | 24 | 25 | 26 | ); 27 | }; 28 | 29 | const CardWrapper = styled.div` 30 | padding: 10px; 31 | display: flex; 32 | flex-direction: column; 33 | align-items: center; 34 | gap: 8px; 35 | flex: 0 0 auto; 36 | max-width: 120px; 37 | max-height: 160px; 38 | width: 30%; 39 | border-radius: 16px; 40 | background-color: white; 41 | box-shadow: 2px 2px 8px rgba(0, 0, 0, 0.2); 42 | `; 43 | 44 | const TextWrapper = styled.div` 45 | display: flex; 46 | flex-direction: column; 47 | align-items: flex-start; 48 | gap: 4px; 49 | width: 100%; 50 | `; 51 | 52 | const Title = styled.p` 53 | width: 100%; 54 | font: ${(props) => props.theme.paragraphsP3M}; 55 | `; 56 | 57 | const TagList = styled.div` 58 | display: flex; 59 | flex-direction: row; 60 | align-items: center; 61 | flex-wrap: wrap; 62 | width: 100%; 63 | `; 64 | 65 | const Tag = styled.span` 66 | word-break: keep-all; 67 | white-space: nowrap; 68 | font: ${(props) => props.theme.paragraphsP3R}; 69 | color: ${(props) => props.theme.primaryMain}; 70 | `; 71 | 72 | export default GroupGoalCardSmall; 73 | -------------------------------------------------------------------------------- /src/components/goal/goalDetail/GoalAccountInfo.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | import ErrorMsg from '../../common/elem/ErrorMsg'; 5 | import Alert from '../../common/alert/Alert'; 6 | import AccountInfoCard from '../../account/AccountInfoCard'; 7 | 8 | import useAccountsData from '../../../hooks/useAccountsData'; 9 | 10 | import { accountInfoFinder } from '../../../utils/accountInfoChecker'; 11 | 12 | const GoalAccountInfo = ({ accountId }: { accountId: number }) => { 13 | const { isLoading, isError, accounts } = useAccountsData(); 14 | 15 | if (isLoading || !accounts) return <>; 16 | 17 | return ( 18 | <> 19 | {!accountInfoFinder(accounts, accountId).acctNo ? ( 20 | <> 21 | ) : ( 22 | <> 23 | 연결 계좌 정보 24 | {isError ? ( 25 | 26 | 27 | 28 | ) : ( 29 | 30 | )} 31 | 32 | )} 33 | 34 | ); 35 | }; 36 | 37 | const SubTitle = styled.div` 38 | font: ${(props) => props.theme.paragraphsP3M}; 39 | `; 40 | 41 | export default GoalAccountInfo; 42 | -------------------------------------------------------------------------------- /src/components/goal/goalDetail/GoalDeleteButton.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { useNavigate } from 'react-router-dom'; 3 | import { useMutation } from 'react-query'; 4 | 5 | import TextButton from '../../common/elem/TextButton'; 6 | 7 | import { goalApi } from '../../../apis/client'; 8 | 9 | interface GoalDeleteButtonProps { 10 | goalId: number; 11 | isDeletedHandler: (result: boolean) => void; 12 | } 13 | 14 | const GoalDeleteButton = ({ goalId, isDeletedHandler }: GoalDeleteButtonProps) => { 15 | const navigate = useNavigate(); 16 | const { mutate } = useMutation('deleteGoal', () => goalApi.deleteGoal(goalId), { 17 | onSuccess: () => { 18 | isDeletedHandler(true); 19 | setTimeout(() => navigate(-1), 2000); 20 | }, 21 | }); 22 | 23 | const handleDeleteGoalButton = () => { 24 | mutate(); 25 | }; 26 | 27 | return ; 28 | }; 29 | 30 | export default GoalDeleteButton; 31 | -------------------------------------------------------------------------------- /src/components/goal/goalDetail/GoalDescCard.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | interface GoalDescCardProps { 5 | description: string; 6 | } 7 | 8 | const GoalDescCard = ({ description }: GoalDescCardProps) => { 9 | return ( 10 | 11 | 목표 12 | {description} 13 | 14 | ); 15 | }; 16 | 17 | const Wrapper = styled.div` 18 | display: flex; 19 | flex-direction: row; 20 | align-items: center; 21 | gap: 20px; 22 | padding: 10px 20px; 23 | width: calc(100% - 40px); 24 | border-radius: 16px; 25 | background-color: white; 26 | `; 27 | 28 | const SubTitle = styled.div` 29 | font: ${(props) => props.theme.captionC1}; 30 | color: ${(props) => props.theme.gray600}; 31 | `; 32 | 33 | const Description = styled.div` 34 | font: ${(props) => props.theme.paragraphsP3R}; 35 | `; 36 | 37 | export default GoalDescCard; 38 | -------------------------------------------------------------------------------- /src/components/goal/goalDetail/GoalModifyButton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useNavigate } from 'react-router-dom'; 3 | 4 | import TextButton from '../../common/elem/TextButton'; 5 | 6 | const GoalModifyButton = ({ isGroup }: { isGroup: boolean }) => { 7 | const navigate = useNavigate(); 8 | const handleModify = () => { 9 | navigate(`modify/data/${isGroup ? 'group' : 'personal'}`); 10 | }; 11 | 12 | return ; 13 | }; 14 | 15 | export default GoalModifyButton; 16 | -------------------------------------------------------------------------------- /src/components/goal/goalDetail/GoalPeriodCard.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | import DdayTag from '../../common/tag/DdayTag'; 5 | 6 | import { dateStringTranslator } from '../../../utils/dateTranslator'; 7 | 8 | interface GoalPeriodCard { 9 | startDate: Date; 10 | endDate: Date; 11 | } 12 | 13 | const GoalPeriodCard = ({ startDate, endDate }: GoalPeriodCard) => { 14 | return ( 15 | 16 | 17 | 기간 18 | {`${dateStringTranslator(new Date(startDate))} - ${dateStringTranslator(new Date(endDate))}`} 19 | 20 | 21 | 22 | ); 23 | }; 24 | 25 | const Wrapper = styled.div` 26 | display: flex; 27 | flex-direction: row; 28 | justify-content: space-between; 29 | align-items: center; 30 | padding: 10px 20px; 31 | width: calc(100% - 40px); 32 | border-radius: 16px; 33 | background-color: white; 34 | `; 35 | 36 | const GoalPeriodCardWrapper = styled.div` 37 | display: flex; 38 | flex-direction: row; 39 | align-items: center; 40 | gap: 20px; 41 | `; 42 | 43 | const SubTitle = styled.div` 44 | font: ${(props) => props.theme.captionC1}; 45 | color: ${(props) => props.theme.gray600}; 46 | `; 47 | 48 | const Period = styled.div` 49 | font: ${(props) => props.theme.paragraphsP3R}; 50 | `; 51 | export default GoalPeriodCard; 52 | -------------------------------------------------------------------------------- /src/components/goal/goalDetail/GoalTagsCard.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import HashTag from '../../common/tag/HashTag'; 4 | 5 | interface GoalTagsCardProps { 6 | hashTag: string[]; 7 | } 8 | 9 | const GoalTagsCard = ({ hashTag }: GoalTagsCardProps) => { 10 | return ( 11 | 12 | 13 | {hashTag.map((tag) => { 14 | return ; 15 | })} 16 | 17 | 18 | ); 19 | }; 20 | 21 | const Wrapper = styled.div` 22 | display: flex; 23 | flex-direction: row; 24 | align-items: center; 25 | gap: 20px; 26 | padding: 10px 20px; 27 | width: calc(100% - 40px); 28 | border-radius: 16px; 29 | background-color: white; 30 | `; 31 | 32 | const TagList = styled.div` 33 | display: flex; 34 | flex-direction: row; 35 | align-items: center; 36 | flex-wrap: wrap; 37 | width: 100%; 38 | gap: 10px; 39 | `; 40 | 41 | export default GoalTagsCard; 42 | -------------------------------------------------------------------------------- /src/components/goal/goalDetail/group/ParticipantCard.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useNavigate } from 'react-router-dom'; 3 | import styled from 'styled-components'; 4 | 5 | import ProfileImg from '../../../common/elem/ProfileImg'; 6 | 7 | import { IMemeberInfo } from '../../../../interfaces/interfaces'; 8 | 9 | interface ParticipantCardProps { 10 | type: 'creator' | 'participant'; 11 | info: IMemeberInfo; 12 | } 13 | 14 | const ParticipantCard = ({ type, info }: ParticipantCardProps) => { 15 | const navigate = useNavigate(); 16 | 17 | return ( 18 | 19 | navigate(`/users/${info.userId}`)}> 20 | 21 | {info.nickname} 22 | 23 | {`${info.attainment}%`} 24 | 25 | ); 26 | }; 27 | 28 | const Wrapper = styled.div` 29 | padding: 10px 0; 30 | display: flex; 31 | flex-direction: row; 32 | justify-content: space-between; 33 | align-items: center; 34 | flex: 0 0 auto; 35 | width: 100%; 36 | border-radius: 16px; 37 | background-color: white; 38 | `; 39 | 40 | const PaticpantInfoWrapper = styled.div` 41 | display: flex; 42 | flex-direction: row; 43 | align-items: center; 44 | gap: 10px; 45 | `; 46 | 47 | const Nickname = styled.span<{ color: string }>` 48 | font: ${(props) => props.theme.paragraphsP3M}; 49 | color: ${(props) => props.color}; 50 | `; 51 | 52 | const Attainment = styled(Nickname)``; 53 | 54 | export default ParticipantCard; 55 | -------------------------------------------------------------------------------- /src/components/goal/goalDetail/group/ParticipantList.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | import ParticipantCard from './ParticipantCard'; 5 | 6 | import { IMemeberInfo } from '../../../../interfaces/interfaces'; 7 | 8 | interface IParticipnatListProps { 9 | createdUserId: number; 10 | members: Array; 11 | } 12 | 13 | const ParticipantList = ({ createdUserId, members }: IParticipnatListProps) => { 14 | const creator = members 15 | .filter((m) => m.userId === createdUserId) 16 | .map((m) => ); 17 | 18 | const participants = members 19 | .filter((m) => m.userId !== createdUserId) 20 | .map((m) => ); 21 | 22 | return ( 23 | 24 | 목표 개설자 25 | {creator.length === 0 ? 탈퇴한 사용자가 개설한 목표입니다 : creator} 26 | 목표 참여자 27 | {participants.length === 0 ? 아직 참여자가 없습니다 : participants} 28 | 29 | ); 30 | }; 31 | 32 | const Wrapper = styled.div` 33 | padding: 8px 20px; 34 | display: flex; 35 | flex-direction: column; 36 | align-items: center; 37 | gap: 8px; 38 | flex-wrap: nowrap; 39 | width: calc(100% - 40px); 40 | height: calc(100% - 16px); 41 | border-radius: 16px; 42 | background-color: white; 43 | `; 44 | 45 | const Type = styled.div` 46 | width: 100%; 47 | font: ${(props) => props.theme.captionC1}; 48 | color: ${(props) => props.theme.gray600}; 49 | `; 50 | 51 | const CardWrapper = styled.div` 52 | display: flex; 53 | flex-direction: column; 54 | align-items: center; 55 | width: 100%; 56 | `; 57 | 58 | const ListWrapper = styled(CardWrapper)` 59 | gap: 8px; 60 | overflow-y: auto; 61 | `; 62 | 63 | const Info = styled.div` 64 | padding: 10px 0; 65 | font: ${(props) => props.theme.captionC1}; 66 | color: ${(props) => props.theme.gray400}; 67 | `; 68 | 69 | export default ParticipantList; 70 | -------------------------------------------------------------------------------- /src/components/goal/goalDetail/group/WithdrawButton.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { useMutation } from 'react-query'; 3 | import styled from 'styled-components'; 4 | 5 | import TextButton from '../../../common/elem/TextButton'; 6 | import ModalBox from '../../../common/elem/ModalBox'; 7 | import CloseIconBtn from '../../../common/elem/btn/CloseIconBtn'; 8 | 9 | import { goalApi } from '../../../../apis/client'; 10 | import { useNavigate } from 'react-router-dom'; 11 | 12 | const WithDrawButton = ({ goalId }: { goalId: number }) => { 13 | const [showInfo, setShowInfo] = useState(false); 14 | const navigate = useNavigate(); 15 | const { mutate } = useMutation('withDrawGoal', () => goalApi.withdrawGoal(goalId), { 16 | onSuccess: () => { 17 | navigate(0); 18 | }, 19 | }); 20 | const handleWithdrawGoal = () => { 21 | mutate(); 22 | }; 23 | 24 | return ( 25 | <> 26 | setShowInfo(true)} /> 27 | 28 | setShowInfo(false)} /> 29 | 30 | 목표를 그만두시겠습니까? 31 | 32 | 33 | 34 | 35 | ); 36 | }; 37 | 38 | const Content = styled.div` 39 | display: flex; 40 | flex-direction: column; 41 | gap: 20px; 42 | width: 100%; 43 | `; 44 | 45 | const Info = styled.div` 46 | word-break: keep-all; 47 | font: ${(props) => props.theme.captionC1}; 48 | `; 49 | 50 | export default WithDrawButton; 51 | -------------------------------------------------------------------------------- /src/components/goal/input/DateInput.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | import DateSelectBox from '../../common/elem/DateSelectBox'; 5 | 6 | import useDateInput from '../../../hooks/useDateInput'; 7 | 8 | import { dateStringTranslatorWithPoint } from '../../../utils/dateTranslator'; 9 | 10 | interface DateInputProps { 11 | title: string; 12 | startDate: Date; 13 | initVal: string; 14 | min: number; 15 | max: number; 16 | isDisabled: boolean; 17 | changeHandler: (val: Date) => void; 18 | } 19 | const DateInput = ({ title, startDate, initVal, min, max, isDisabled, changeHandler }: DateInputProps) => { 20 | const { minDate, maxDate, start, value, onChangeStartDate, onChangeEndDate } = useDateInput({ 21 | startDate, 22 | initVal, 23 | minDays: min, 24 | maxDays: max, 25 | }); 26 | 27 | useEffect(() => { 28 | onChangeStartDate(startDate); 29 | }, [startDate]); 30 | 31 | useEffect(() => { 32 | changeHandler(new Date(value)); 33 | }, [value]); 34 | 35 | return ( 36 | 37 | {title} 38 | 39 | {dateStringTranslatorWithPoint(start)} 40 | - 41 | {isDisabled ? ( 42 | {dateStringTranslatorWithPoint(new Date(value))} 43 | ) : ( 44 | 45 | 46 | 47 | )} 48 | 49 | 50 | ); 51 | }; 52 | 53 | const Wrapper = styled.div<{ disabled: boolean }>` 54 | width: 100%; 55 | display: flex; 56 | flex-direction: column; 57 | align-items: flex-start; 58 | gap: 10px; 59 | opacity: ${(props) => (props.disabled ? 0.5 : 1)}; 60 | `; 61 | 62 | const SubTitle = styled.div` 63 | font: ${(props) => props.theme.captionC1}; 64 | color: ${(props) => props.theme.gray600}; 65 | `; 66 | 67 | const RowContent = styled.div` 68 | display: flex; 69 | flex-direction: row; 70 | align-items: center; 71 | gap: 20px; 72 | width: 100%; 73 | `; 74 | 75 | const DateText = styled.div` 76 | font: ${(props) => props.theme.paragraphP2}; 77 | `; 78 | 79 | const InputWrapper = styled.div` 80 | height: 30px; 81 | `; 82 | 83 | export default DateInput; 84 | -------------------------------------------------------------------------------- /src/components/goal/input/EmojiInput.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | import EmojiBox from '../../common/elem/EmojiBox'; 5 | import ImgEditBtn from '../../common/elem/btn/ImgEditBtn'; 6 | import EmojiPicker from 'emoji-picker-react'; 7 | 8 | import useEmojiSelect from '../../../hooks/useEmojiSelect'; 9 | 10 | interface EmojiInputProps { 11 | initVal: string; 12 | changeHandler: (emoji: string) => void; 13 | } 14 | 15 | function EmojiInput({ initVal, changeHandler }: EmojiInputProps) { 16 | const { showEmojis, emoji, handleShowEmojis, handleEmojiSelect } = useEmojiSelect({ initVal }); 17 | useEffect(() => { 18 | changeHandler(emoji); 19 | }, [emoji]); 20 | 21 | return ( 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | handleEmojiSelect(emoji)} /> 31 | 32 | 33 | ); 34 | } 35 | 36 | const EmojiContentBox = styled.div` 37 | position: relative; 38 | display: flex; 39 | flex-direction: row; 40 | justify-content: center; 41 | align-items: flex-end; 42 | width: 100%; 43 | `; 44 | 45 | const BoxWrapper = styled.div` 46 | position: relative; 47 | width: 80px; 48 | height: 80px; 49 | `; 50 | 51 | const BtnWrapper = styled.div` 52 | position: absolute; 53 | bottom: 0; 54 | right: -20px; 55 | `; 56 | 57 | const EmojiPickerWrapper = styled.div<{ show: boolean }>` 58 | position: absolute; 59 | top: 110%; 60 | z-index: 5; 61 | display: ${(props) => (props.show ? '' : 'none')}; 62 | box-shadow: 5px 5px 20px rgba(0, 0, 0, 0.2); 63 | `; 64 | 65 | export default EmojiInput; 66 | -------------------------------------------------------------------------------- /src/components/goal/input/NumInput.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | import InputBox from '../../common/elem/InputBox'; 5 | import ValidateMsg from '../../common/elem/ValidateMsg'; 6 | 7 | import useNumInput from '../../../hooks/useNumInput'; 8 | 9 | interface NumInputProps { 10 | title?: string; 11 | type: '목표 금액' | '인원'; 12 | placeholder: string; 13 | initVal: number; 14 | min: number; 15 | max: number; 16 | isDisabled: boolean; 17 | inputWidth?: number; 18 | changeHandler: (val: number) => void; 19 | errHandler: (isErr: boolean) => void; 20 | } 21 | 22 | const NumInput = ({ 23 | title, 24 | type, 25 | placeholder, 26 | initVal, 27 | min, 28 | max, 29 | isDisabled, 30 | inputWidth, 31 | changeHandler, 32 | errHandler, 33 | }: NumInputProps) => { 34 | const { value, errMsg, onChange } = useNumInput({ initValue: initVal, min, max, type }); 35 | useEffect(() => { 36 | changeHandler(value); 37 | }, [value]); 38 | 39 | useEffect(() => { 40 | if (errMsg.length !== 0) return errHandler(true); 41 | errHandler(false); 42 | }, [value, errMsg]); 43 | 44 | return ( 45 | 46 | {title ? {title} : <>} 47 | 48 | 49 | 56 | 57 | 58 | 59 | 60 | 61 | ); 62 | }; 63 | 64 | const Wrapper = styled.div<{ disabled: boolean }>` 65 | display: flex; 66 | flex-direction: column; 67 | align-items: flex-start; 68 | gap: 8px; 69 | width: 100%; 70 | opacity: ${(props) => (props.disabled ? 0.5 : 1)}; 71 | `; 72 | 73 | const SubTitle = styled.div` 74 | font: ${(props) => props.theme.captionC1}; 75 | color: ${(props) => props.theme.gray600}; 76 | `; 77 | 78 | const InputWrapper = styled.div<{ width?: string }>` 79 | width: ${(props) => (props.width ? props.width : '100%')}; 80 | height: 30px; 81 | `; 82 | 83 | const RowContent = styled.div` 84 | display: flex; 85 | flex-direction: row; 86 | align-items: center; 87 | gap: 8px; 88 | width: 100%; 89 | `; 90 | 91 | export default NumInput; 92 | -------------------------------------------------------------------------------- /src/components/goal/input/TextInput.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | import InputBox from '../../common/elem/InputBox'; 5 | import ValidateMsg from '../../common/elem/ValidateMsg'; 6 | 7 | import useTxtInput from '../../../hooks/useTxtInput'; 8 | 9 | interface TextInputProps { 10 | title?: string; 11 | type: 12 | | '제목' 13 | | '설명' 14 | | '해시태그' 15 | | '계좌번호' 16 | | '계좌 비밀번호' 17 | | '인터넷 뱅킹 아이디' 18 | | '인터넷 뱅킹 비밀번호' 19 | | '계좌 입금자명'; 20 | placeholder: string; 21 | initVal: string; 22 | min: number; 23 | max: number; 24 | isDisabled: boolean; 25 | changeHandler: (val: string) => void; 26 | errHandler: (isErr: boolean) => void; 27 | } 28 | 29 | const TextInput = ({ 30 | title, 31 | type, 32 | placeholder, 33 | initVal, 34 | min, 35 | max, 36 | isDisabled, 37 | changeHandler, 38 | errHandler, 39 | }: TextInputProps) => { 40 | const { value, errMsg, onChange } = useTxtInput({ 41 | initValue: initVal, 42 | minLength: min, 43 | maxLength: max, 44 | type: type, 45 | }); 46 | 47 | useEffect(() => { 48 | changeHandler(value); 49 | }, [value]); 50 | 51 | useEffect(() => { 52 | if (value.length === 0 || errMsg.length !== 0) return errHandler(true); 53 | errHandler(false); 54 | }, [value, errMsg]); 55 | 56 | return ( 57 | 58 | {title ? {title} : <>} 59 | 60 | 69 | 70 | 71 | 72 | ); 73 | }; 74 | 75 | const Wrapper = styled.div<{ disabled: boolean }>` 76 | display: flex; 77 | flex-direction: column; 78 | align-items: flex-start; 79 | gap: 8px; 80 | width: 100%; 81 | opacity: ${(props) => (props.disabled ? 0.5 : 1)}; 82 | `; 83 | 84 | const SubTitle = styled.div` 85 | font: ${(props) => props.theme.captionC1}; 86 | color: ${(props) => props.theme.gray600}; 87 | `; 88 | 89 | const InputWrapper = styled.div` 90 | width: 100%; 91 | height: 30px; 92 | `; 93 | 94 | export default TextInput; 95 | -------------------------------------------------------------------------------- /src/components/goal/modify/AccntToggle.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import useAccountsData from '../../../hooks/useAccountsData'; 3 | 4 | import ToggleSelectBox from '../../common/elem/ToggleSelectBox'; 5 | import ValidateMsg from '../../common/elem/ValidateMsg'; 6 | 7 | import { isManualAccountAddable } from '../../../utils/accountInfoChecker'; 8 | 9 | interface AccntToggleProps { 10 | initVal: boolean; 11 | changeHandler: (val: boolean) => void; 12 | } 13 | 14 | const AccntToggle = ({ initVal, changeHandler }: AccntToggleProps) => { 15 | const [isManual, setisManual] = useState(initVal); 16 | const handleSelectisAuto = (isTrue: boolean) => { 17 | setisManual(isTrue); 18 | }; 19 | 20 | useEffect(() => { 21 | changeHandler(isManual); 22 | }, [isManual]); 23 | 24 | const { isLoading, isError, accounts } = useAccountsData(); 25 | 26 | return ( 27 | <> 28 | 38 | {isError ? : <>} 39 | {isManual && !isManualAccountAddable(accounts) ? ( 40 | 41 | ) : ( 42 | <> 43 | )} 44 | 45 | ); 46 | }; 47 | 48 | export default AccntToggle; 49 | -------------------------------------------------------------------------------- /src/components/goal/post/BankList.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | import Icon from '../../common/elem/Icon'; 5 | import BankBox from '../../common/elem/BankBox'; 6 | 7 | import { IBank } from '../../../interfaces/interfaces'; 8 | 9 | interface BankListProps { 10 | banks: Array; 11 | closeHandler: () => void; 12 | selectHandler: (bank: IBank) => void; 13 | } 14 | 15 | const BankList = ({ banks, closeHandler, selectHandler }: BankListProps) => { 16 | return ( 17 | 18 | 19 | 은행을 선택해주세요 20 | 28 | 29 | 30 | {banks.map((bank) => ( 31 | 32 | selectHandler(bank)} /> 33 | 34 | ))} 35 | 36 | 37 | ); 38 | }; 39 | 40 | const Wrapper = styled.div` 41 | display: flex; 42 | flex-direction: column; 43 | gap: 25px; 44 | width: 100%; 45 | `; 46 | 47 | const TopContent = styled.div` 48 | display: flex; 49 | flex-direction: row; 50 | justify-content: space-between; 51 | align-items: center; 52 | width: 100%; 53 | `; 54 | 55 | const SubTitle = styled.div` 56 | font: ${(props) => props.theme.paragraphsP1M}; 57 | `; 58 | 59 | const Button = styled.div` 60 | width: 24px; 61 | height: 24px; 62 | `; 63 | 64 | const BottomContent = styled.div` 65 | display: flex; 66 | flex-direction: row; 67 | row-gap: 12px; 68 | flex-wrap: wrap; 69 | width: 100%; 70 | `; 71 | 72 | const BankBoxWrapper = styled.div` 73 | padding: 0 5px; 74 | width: calc(25% - 10px); 75 | height: 81px; 76 | `; 77 | 78 | export default BankList; 79 | -------------------------------------------------------------------------------- /src/components/goal/searchFilter/RangeSelectBox.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import useRangeInput from '../../../hooks/useRangeInput'; 4 | 5 | import RangeSlider from '../../common/elem/RangeSlider'; 6 | 7 | interface RangeSelectBoxProps { 8 | min: number; 9 | max: number; 10 | gap: number; 11 | unit: string; 12 | isDisabled: boolean; 13 | minChangeHandler: (min: number) => void; 14 | maxChangeHandler: (max: number) => void; 15 | } 16 | 17 | const RangeSelectBox = ({ 18 | min: minInitVal, 19 | max: maxInitVal, 20 | gap, 21 | unit, 22 | isDisabled, 23 | minChangeHandler, 24 | maxChangeHandler, 25 | }: RangeSelectBoxProps) => { 26 | const { 27 | min: selectedMin, 28 | max: selectedMax, 29 | handleMinChange, 30 | handleMaxChange, 31 | } = useRangeInput({ minInitVal, maxInitVal }); 32 | 33 | const minChange = (min: number) => { 34 | handleMinChange(min); 35 | minChangeHandler(min); 36 | }; 37 | 38 | const maxChange = (max: number) => { 39 | handleMaxChange(max); 40 | maxChangeHandler(max); 41 | }; 42 | 43 | return ( 44 | 45 | {`${selectedMin.toLocaleString()} ~ ${selectedMax.toLocaleString()} ${unit}`} 46 | 54 | 55 | ); 56 | }; 57 | 58 | const Wrapper = styled.div<{ disabled: boolean }>` 59 | display: flex; 60 | flex-direction: column; 61 | justify-content: flex-start; 62 | width: 100%; 63 | gap: 8px; 64 | opacity: ${(props) => (props.disabled ? 0.3 : 1)}; 65 | `; 66 | 67 | const RangeIndicator = styled.div` 68 | font: ${(props) => props.theme.paragraphsP2M}; 69 | `; 70 | 71 | export default RangeSelectBox; 72 | -------------------------------------------------------------------------------- /src/components/goal/searchFilter/StatusFilter.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | import RadioSelectBox from '../../common/elem/RadioSelectBox'; 5 | 6 | import { StatusType, StatusTypeKR, StatusKRtoEnum } from '../../../interfaces/interfaces'; 7 | 8 | const statusList = [StatusType.total, StatusType.proceeding, StatusType.recruit]; 9 | 10 | interface StatusFilterProps { 11 | selected: StatusType; 12 | changeHandler: (status: StatusType) => void; 13 | } 14 | 15 | const StatusFilter = ({ selected, changeHandler }: StatusFilterProps) => { 16 | const handleStatusChange = (e: React.ChangeEvent) => { 17 | changeHandler(StatusKRtoEnum(e.currentTarget.value)); 18 | }; 19 | 20 | return ( 21 | 22 | StatusTypeKR(v))} 24 | selected={StatusTypeKR(selected)} 25 | onChangeHandler={handleStatusChange} 26 | flexDirection='column' 27 | alignItems='flex-start' 28 | /> 29 | 30 | ); 31 | }; 32 | 33 | const Wrapper = styled.div` 34 | display: flex; 35 | flex-direction: column; 36 | justify-content: flex-start; 37 | width: 100%; 38 | gap: 8px; 39 | `; 40 | 41 | export default StatusFilter; 42 | -------------------------------------------------------------------------------- /src/components/header/SearchBar.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | import InputBox from '../common/elem/InputBox'; 5 | 6 | interface SearchBarProps { 7 | show: boolean; 8 | value: string; 9 | changeHandler: (keyword: string) => void; 10 | keyPressHandler: (e: React.KeyboardEvent) => void; 11 | } 12 | 13 | const SearchBar = ({ show, value, changeHandler, keyPressHandler }: SearchBarProps) => { 14 | return ( 15 | 16 | 17 | changeHandler(e.currentTarget.value)} 22 | onKeyPressHandler={keyPressHandler} 23 | showBorder={false} 24 | /> 25 | 26 | 27 | ); 28 | }; 29 | 30 | const SearchBarLayout = styled.div<{ show: boolean }>` 31 | width: ${(props) => (props.show ? '100%' : '0')}; 32 | height: 60%; 33 | transition: width 0.8s; 34 | `; 35 | 36 | const SearchInputWrapper = styled.div<{ show: boolean }>` 37 | padding: ${(props) => (props.show ? '4px 17px' : '0')}; 38 | width: calc(100% - 34px); 39 | border-radius: 32px; 40 | background-color: ${(props) => props.theme.primary50}; 41 | transition: padding 0.5s; 42 | `; 43 | 44 | export default SearchBar; 45 | -------------------------------------------------------------------------------- /src/components/settings/ModifyAccount.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useNavigate, useParams } from 'react-router-dom'; 3 | 4 | import SettingButton from '../common/elem/SettingButton'; 5 | 6 | // TODO: 2차개발, 실계좌 정보 관리 7 | const ModifyAccount = () => { 8 | const navigate = useNavigate(); 9 | const { id } = useParams(); 10 | 11 | const handleModifyAccountInfo = () => { 12 | navigate(`/users/settings/accounts/${id}`); 13 | }; 14 | return ; 15 | }; 16 | 17 | export default ModifyAccount; 18 | -------------------------------------------------------------------------------- /src/components/settings/ResetPinNumber.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import SettingButton from '../common/elem/SettingButton'; 3 | 4 | const ResetPinNumber = () => { 5 | const handleResetPinNumber = () => { 6 | console.log('계핀번호 재설정'); 7 | }; 8 | return ; 9 | }; 10 | 11 | export default ResetPinNumber; 12 | -------------------------------------------------------------------------------- /src/components/settings/WithdrawalService.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | import ModalBox from '../common/elem/ModalBox'; 5 | import SettingButton from '../common/elem/SettingButton'; 6 | import TextButton from '../common/elem/TextButton'; 7 | 8 | interface WithdrawalServiceProps { 9 | warningHandler: (show: boolean) => void; 10 | } 11 | 12 | const WithdrawalService = ({ warningHandler }: WithdrawalServiceProps) => { 13 | const [showConfirm, setShowConfirm] = useState(false); 14 | const handleWithdrawalConfirmModal = () => { 15 | setShowConfirm(true); 16 | }; 17 | 18 | return ( 19 | <> 20 | 21 | 22 | 23 | 탈..퇴.. 하시겠습니까? 24 | warningHandler(true)} 30 | /> 31 | 32 | 33 | setShowConfirm(false)} 39 | /> 40 | 41 | 42 | 43 | ); 44 | }; 45 | 46 | const ConfirmButtonWrapper = styled.div` 47 | display: flex; 48 | flex-direction: column; 49 | padding: 6px 0px; 50 | margin: 8px 0px; 51 | width: 100%; 52 | gap: 10px; 53 | border-radius: 8px; 54 | background-color: white; 55 | `; 56 | 57 | const ConfirmMsg = styled.div` 58 | width: 100%; 59 | text-align: center; 60 | font: ${(props) => props.theme.captionC2}; 61 | color: ${(props) => props.theme.gray600}; 62 | border-bottom: 1px solid; 63 | border-color: ${(props) => props.theme.gray300}; 64 | padding: 5px 0px; 65 | `; 66 | 67 | const CancleButtonWrapper = styled.div` 68 | display: flex; 69 | flex-direction: column; 70 | padding: 6px 0px; 71 | width: 100%; 72 | border-radius: 8px; 73 | background-color: white; 74 | `; 75 | 76 | export default WithdrawalService; 77 | -------------------------------------------------------------------------------- /src/components/user/UserDetailTabSection.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | import MyFilteredGoals from '../goal/MyFilteredGoals'; 5 | import MyFilteredBadges from '../badge/MyFilteredBadges'; 6 | 7 | import useTab from '../../hooks/useTab'; 8 | 9 | const UserDetailTab = ({ userId }: { userId: number }) => { 10 | const { tabs, handleTabClick } = useTab(); 11 | 12 | return ( 13 | 14 | 15 | {tabs.map((tab) => ( 16 | handleTabClick(tab.title)}> 17 | {tab.title} 18 | 19 | ))} 20 | 21 | 22 | {tabs.map((tab) => { 23 | if (tab.isSelected) { 24 | switch (tab.title) { 25 | case '목표': 26 | return ; 27 | case '뱃지': 28 | return ; 29 | } 30 | } 31 | })} 32 | 33 | 34 | ); 35 | }; 36 | 37 | const Wrapper = styled.div` 38 | width: 100%; 39 | height: 100%; 40 | `; 41 | 42 | const TabList = styled.div` 43 | display: flex; 44 | flex-direction: row; 45 | width: 100%; 46 | height: 45px; 47 | `; 48 | 49 | const Tab = styled.div<{ selected: boolean }>` 50 | padding: 8px 0; 51 | width: 50%; 52 | font: ${(props) => props.theme.paragraphsP1M}; 53 | text-align: center; 54 | color: ${(props) => (props.selected ? props.theme.primary400 : props.theme.gray600)}; 55 | border-bottom: ${(props) => (props.selected ? `2px solid ${props.theme.primary400}` : '')}; 56 | `; 57 | 58 | const ContentBox = styled.div` 59 | padding: 20px 22px 0; 60 | height: calc(100% - 65px); 61 | background-color: ${(props) => props.theme.gray100}; 62 | `; 63 | 64 | export default UserDetailTab; 65 | -------------------------------------------------------------------------------- /src/components/user/UserProfile.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useRecoilValue } from 'recoil'; 3 | import styled from 'styled-components'; 4 | 5 | import ProfileImg from '../common/elem/ProfileImg'; 6 | 7 | import useUserProfileData from '../../hooks/useUserProfileData'; 8 | 9 | import { userId } from '../../recoil/userAtoms'; 10 | 11 | const UserProfile = () => { 12 | const { id } = useRecoilValue(userId); 13 | const { isLoading, isError, profile } = useUserProfileData({ 14 | getUserId: id, 15 | }); 16 | 17 | if (isError || !profile) 18 | return ( 19 | 20 | 사용자 데이터를 불러오는 데 실패했습니다 21 | 22 | ); 23 | 24 | return ( 25 | 26 | 27 | {profile.nickname} 28 | 29 | ); 30 | }; 31 | 32 | const Wrapper = styled.div` 33 | display: flex; 34 | padding: 8px 22px; 35 | flex-direction: row; 36 | align-items: center; 37 | gap: 10px; 38 | background-color: white; 39 | `; 40 | 41 | const LoadingText = styled.div` 42 | display: flex; 43 | flex-direction: row; 44 | align-items: center; 45 | gap: 10px; 46 | font: ${(props) => props.theme.paragraphsP3R}; 47 | color: ${(props) => props.theme.gray600}; 48 | `; 49 | 50 | const ErrorText = styled(LoadingText)` 51 | color: red; 52 | `; 53 | 54 | const Nickname = styled.div` 55 | font: ${(props) => props.theme.headingH4}; 56 | `; 57 | 58 | export default UserProfile; 59 | -------------------------------------------------------------------------------- /src/components/user/signup/GoogleSignupButton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import LoginButton from '../../common/elem/LoginButton'; 3 | 4 | const GoogleSignupButton = () => { 5 | const handleGoogleSignup = () => { 6 | window.location.href = `https://accounts.google.com/o/oauth2/v2/auth?client_id=${process.env.REACT_APP_GOOGLE_CLIENT_ID}&redirect_uri=${process.env.REACT_APP_GOOGLE_REDIRECT_URI}&response_type=code&scope=https://www.googleapis.com/auth/userinfo.email`; 7 | }; 8 | 9 | return ( 10 | <> 11 | 12 | 13 | ); 14 | }; 15 | 16 | export default GoogleSignupButton; 17 | -------------------------------------------------------------------------------- /src/components/user/signup/KakaoSignupButton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import LoginButton from '../../common/elem/LoginButton'; 3 | 4 | const KakaoSignupButton = () => { 5 | const handleKakaoSignup = () => { 6 | window.location.href = `https://kauth.kakao.com/oauth/authorize?response_type=code&client_id=${process.env.REACT_APP_KAKAO_CLIENT_ID}&redirect_uri=${process.env.REACT_APP_KAKAO_REDIRECT_URI}`; 7 | }; 8 | 9 | return ( 10 | <> 11 | 12 | 13 | ); 14 | }; 15 | 16 | export default KakaoSignupButton; 17 | -------------------------------------------------------------------------------- /src/components/user/signup/NaverSignupButton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import LoginButton from '../../common/elem/LoginButton'; 3 | 4 | const NaverSignupButton = () => { 5 | // TODO: state 추후 변경 필요 6 | const naverStateString = 'test'; 7 | 8 | const handleNaverSignup = () => { 9 | window.location.href = `https://nid.naver.com/oauth2.0/authorize?response_type=code&client_id=${process.env.REACT_APP_NAVER_CLIENT_ID}&state=${naverStateString}&redirect_uri=${process.env.REACT_APP_NAVER_REDIRECT_URI}`; 10 | }; 11 | 12 | return ( 13 | <> 14 | 15 | 16 | ); 17 | }; 18 | 19 | export default NaverSignupButton; 20 | -------------------------------------------------------------------------------- /src/hooks/useAccntAuth.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { useMutation } from 'react-query'; 3 | 4 | import { IBank, IReqAuthAccount, IReqAuthAccountResp } from '../interfaces/interfaces'; 5 | 6 | import { bankAPI } from '../apis/client'; 7 | 8 | interface useAccntAuthProps { 9 | accntNo: string; 10 | bank: IBank; 11 | oriSeqNoHandler: (oriSeqNo: string) => void; 12 | authReqHandler: (result: boolean) => void; 13 | } 14 | 15 | const useAccntAuth = ({ accntNo, bank, oriSeqNoHandler, authReqHandler }: useAccntAuthProps) => { 16 | const [isAuthRequested, setIsAuthRequested] = useState(false); 17 | const { isLoading, isError, mutate } = useMutation( 18 | 'reqAuthAccnt', 19 | bankAPI.reqAuthAccnt, 20 | { 21 | onSuccess: (data) => { 22 | if (data.successYn === 'N') { 23 | throw new Error(); 24 | } 25 | authReqHandler(true); 26 | setTimeout(() => setIsAuthRequested(true), 2500); 27 | oriSeqNoHandler(data.oriSeqNo); 28 | }, 29 | onError: () => { 30 | authReqHandler(false); 31 | setIsAuthRequested(true); 32 | }, 33 | } 34 | ); 35 | 36 | const handleReqAuthAccnt = async () => { 37 | mutate({ bankCode: bank.bankCode, accntNo: accntNo }); 38 | }; 39 | 40 | return { isLoading, isError, isAuthRequested, handleReqAuthAccnt }; 41 | }; 42 | 43 | export default useAccntAuth; 44 | -------------------------------------------------------------------------------- /src/hooks/useAccntAuthState.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | 3 | const useAccntAuthState = () => { 4 | const [oriSeqNo, setOriSeqNo] = useState(''); 5 | const handleSetOriSeqNo = (oriSeqNo: string) => { 6 | setOriSeqNo(oriSeqNo); 7 | }; 8 | 9 | const [authReqCnt, setAuthReqCnt] = useState(0); 10 | const [isAuthRequested, setIsAuthRequested] = useState(false); 11 | const handleIsAuthRequested = (result: boolean) => { 12 | setAuthReqCnt((prev) => prev + 1); 13 | setIsAuthRequested(result); 14 | }; 15 | 16 | const handleAccntNoEdit = () => { 17 | // TODO: prevent too many accnt auth request until 24 hours 18 | if (authReqCnt > 3) alert('최대 계좌 수정 횟수는 3회입니다.'); 19 | setIsAuthRequested(false); 20 | setOriSeqNo(''); 21 | }; 22 | 23 | const [isAuthenticated, setIsAuthenticated] = useState(false); 24 | const handleIsAuthenticated = (result: boolean) => { 25 | setIsAuthenticated(result); 26 | }; 27 | 28 | return { 29 | oriSeqNo, 30 | isAuthRequested, 31 | isAuthenticated, 32 | handleSetOriSeqNo, 33 | handleIsAuthRequested, 34 | handleIsAuthenticated, 35 | handleAccntNoEdit, 36 | }; 37 | }; 38 | 39 | export default useAccntAuthState; 40 | -------------------------------------------------------------------------------- /src/hooks/useAccntAutoPost.tsx: -------------------------------------------------------------------------------- 1 | import { useNavigate } from 'react-router-dom'; 2 | import { useRecoilValue } from 'recoil'; 3 | import { useMutation } from 'react-query'; 4 | 5 | import { userId } from '../recoil/userAtoms'; 6 | 7 | import { IPostAccount, IPostAutoAccount } from '../interfaces/interfaces'; 8 | 9 | import { accountApi } from '../apis/client'; 10 | 11 | const useAccntAutoPost = ({ acctInfo }: { acctInfo: IPostAccount }) => { 12 | const { id: loginUserId } = useRecoilValue(userId); 13 | const navigate = useNavigate(); 14 | const { 15 | isLoading, 16 | isError, 17 | data: accountId, 18 | mutate, 19 | } = useMutation('postAccount', accountApi.createAutoAccount, { 20 | onError: (e) => { 21 | if (e === 401) { 22 | navigate('/', { replace: true }); 23 | } 24 | }, 25 | }); 26 | const handlePostAccount = () => { 27 | mutate({ userId: loginUserId, acctInfo }); 28 | }; 29 | 30 | return { 31 | isLoading, 32 | isError, 33 | accountId, 34 | handlePostAccount, 35 | }; 36 | }; 37 | 38 | export default useAccntAutoPost; 39 | -------------------------------------------------------------------------------- /src/hooks/useAccntManualPost.tsx: -------------------------------------------------------------------------------- 1 | import { useRecoilValue } from 'recoil'; 2 | import { useMutation } from 'react-query'; 3 | 4 | import { userId } from '../recoil/userAtoms'; 5 | 6 | import { accountApi } from '../apis/client'; 7 | import { useNavigate } from 'react-router-dom'; 8 | 9 | interface useAccntManualPostProps { 10 | type: string; 11 | goalId: number; 12 | } 13 | 14 | const useAccntManualPost = ({ type, goalId }: useAccntManualPostProps) => { 15 | const { id: loginUserId } = useRecoilValue(userId); 16 | const navigate = useNavigate(); 17 | const { 18 | isLoading, 19 | mutate: createManualAccnt, 20 | isError, 21 | } = useMutation('createManualAccount', () => accountApi.createManualAccount(loginUserId), { 22 | onSuccess: (data) => { 23 | if (type === 'join') { 24 | setTimeout(() => navigate(`/goals/join/${goalId}/accounts/${data}`, { replace: true }), 2000); 25 | } 26 | 27 | if (type === 'post') { 28 | setTimeout(() => navigate(`/goals/post/${data}`, { replace: true }), 2000); 29 | } 30 | }, 31 | onError: (e) => { 32 | if (e === 401) { 33 | navigate('/', { replace: true }); 34 | } 35 | }, 36 | }); 37 | 38 | return { isLoading, isError, createManualAccnt }; 39 | }; 40 | 41 | export default useAccntManualPost; 42 | -------------------------------------------------------------------------------- /src/hooks/useAccntValidate.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { useRecoilValue } from 'recoil'; 3 | 4 | import { accntInfo } from '../recoil/accntAtoms'; 5 | 6 | import { IValidateAccount, IValidateAccountResp } from '../interfaces/interfaces'; 7 | 8 | import { bankAPI } from '../apis/client'; 9 | import { useMutation } from 'react-query'; 10 | 11 | const useAccntValidate = () => { 12 | const savedAccntInfo = useRecoilValue(accntInfo); 13 | const [accnt, setAccnt] = useState({ 14 | bankCode: savedAccntInfo.bankCode, 15 | bankUserId: '', 16 | bankUserPw: '', 17 | accntNo: savedAccntInfo.accntNo, 18 | accntPw: '', 19 | }); 20 | const handleBankUserIdChange = (bankUserId: string) => { 21 | setAccnt((prev) => { 22 | return { ...prev, bankUserId }; 23 | }); 24 | }; 25 | const handleBankUserPwChange = (bankUserPw: string) => { 26 | setAccnt((prev) => { 27 | return { ...prev, bankUserPw }; 28 | }); 29 | }; 30 | const handleAccntPwChange = (accntPw: string) => { 31 | setAccnt((prev) => { 32 | return { ...prev, accntPw }; 33 | }); 34 | }; 35 | 36 | const [isValidAccnt, setIsValidAccnt] = useState(false); 37 | const { mutate } = useMutation( 38 | 'validateAccnt', 39 | bankAPI.validateAccntInfo, 40 | { 41 | onSuccess: (data) => { 42 | if (data.common.errYn === 'Y') { 43 | return alert(data.common.errMsg); 44 | } 45 | 46 | setIsValidAccnt(true); 47 | }, 48 | onError: () => { 49 | setIsValidAccnt(false); 50 | }, 51 | } 52 | ); 53 | const handleValidate = () => { 54 | mutate(accnt); 55 | }; 56 | 57 | return { isValidAccnt, accnt, handleAccntPwChange, handleBankUserIdChange, handleBankUserPwChange, handleValidate }; 58 | }; 59 | 60 | export default useAccntValidate; 61 | -------------------------------------------------------------------------------- /src/hooks/useAccountsData.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { useNavigate } from 'react-router-dom'; 3 | import { useQuery } from 'react-query'; 4 | import { useRecoilValue } from 'recoil'; 5 | 6 | import { accountApi } from '../apis/client'; 7 | 8 | import { userId } from '../recoil/userAtoms'; 9 | 10 | import { IAccount } from '../interfaces/interfaces'; 11 | 12 | const useAccountsData = () => { 13 | const { id: loginUserId } = useRecoilValue(userId); 14 | const [accounts, setAccounts] = useState>([]); 15 | const navigate = useNavigate(); 16 | const { isLoading, isError } = useQuery>('getAccounts', () => accountApi.getAccounts(loginUserId), { 17 | onSuccess: (data) => { 18 | setAccounts(data); 19 | }, 20 | onError: (e) => { 21 | if (e === 401) { 22 | navigate('/', { replace: true }); 23 | } 24 | }, 25 | }); 26 | 27 | return { isLoading, isError, accounts }; 28 | }; 29 | 30 | export default useAccountsData; 31 | -------------------------------------------------------------------------------- /src/hooks/useBadgesData.tsx: -------------------------------------------------------------------------------- 1 | import { useNavigate } from 'react-router-dom'; 2 | import { useSetRecoilState } from 'recoil'; 3 | import { useQuery } from 'react-query'; 4 | 5 | import { IBadge } from '../interfaces/interfaces'; 6 | 7 | import { badgeApi } from '../apis/client'; 8 | 9 | import { badges } from '../recoil/badgeAtoms'; 10 | 11 | const useBadgesData = () => { 12 | const navigate = useNavigate(); 13 | const setBadges = useSetRecoilState(badges); 14 | useQuery>('userBadges', () => badgeApi.getBadges(), { 15 | onSuccess: (data) => { 16 | setBadges(data); 17 | }, 18 | onError: (e) => { 19 | if (e === 401) { 20 | navigate('/', { replace: true }); 21 | } 22 | }, 23 | }); 24 | }; 25 | 26 | export default useBadgesData; 27 | -------------------------------------------------------------------------------- /src/hooks/useBalanceData.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { useNavigate } from 'react-router-dom'; 3 | import { useQuery } from 'react-query'; 4 | import { useRecoilValue } from 'recoil'; 5 | 6 | import { accountApi } from '../apis/client'; 7 | 8 | import { userId } from '../recoil/userAtoms'; 9 | 10 | const useBalanceData = ({ accountId }: { accountId: number }) => { 11 | const [balance, setBalance] = useState(0); 12 | const { id: loginUserId } = useRecoilValue(userId); 13 | const navigate = useNavigate(); 14 | const { isLoading, isError } = useQuery( 15 | 'accountBalance', 16 | () => accountApi.getAccountBalance({ userId: loginUserId, accountId }), 17 | { 18 | onSuccess: (data) => { 19 | setBalance(data); 20 | }, 21 | onError: (e) => { 22 | if (e === 401) { 23 | navigate('/', { replace: true }); 24 | } 25 | }, 26 | } 27 | ); 28 | 29 | return { isLoading, isError, balance }; 30 | }; 31 | 32 | export default useBalanceData; 33 | -------------------------------------------------------------------------------- /src/hooks/useBankId.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { useRecoilValue } from 'recoil'; 3 | 4 | import { banksInfo } from '../recoil/accntAtoms'; 5 | 6 | const useBankId = ({ bankCode }: { bankCode: string }) => { 7 | const [bankId, setBankId] = useState(0); 8 | const banks = useRecoilValue(banksInfo); 9 | const getBankId = () => { 10 | const bank = banks.find((bank) => bank.bankCode === bankCode); 11 | if (!bank) return setBankId(0); 12 | return setBankId(bank.bankId); 13 | }; 14 | 15 | useEffect(() => { 16 | getBankId(); 17 | }, [bankCode]); 18 | 19 | return { bankId }; 20 | }; 21 | 22 | export default useBankId; 23 | -------------------------------------------------------------------------------- /src/hooks/useBankSelect.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | 3 | import { IBank } from '../interfaces/interfaces'; 4 | 5 | const useBankSelect = ({ initVal }: { initVal: IBank }) => { 6 | const [showBanks, setShowBanks] = useState(false); 7 | const handleShowBanks = () => { 8 | setShowBanks(!showBanks); 9 | }; 10 | const [selectedBank, setSelectedBank] = useState(initVal); 11 | const handleBankSelect = (bank: IBank) => { 12 | setSelectedBank(bank); 13 | setShowBanks(false); 14 | }; 15 | 16 | return { showBanks, selectedBank, handleShowBanks, handleBankSelect }; 17 | }; 18 | 19 | export default useBankSelect; 20 | -------------------------------------------------------------------------------- /src/hooks/useBanksData.tsx: -------------------------------------------------------------------------------- 1 | import { useNavigate } from 'react-router-dom'; 2 | import { useSetRecoilState } from 'recoil'; 3 | import { useQuery } from 'react-query'; 4 | 5 | import { IBank } from '../interfaces/interfaces'; 6 | 7 | import { goalApi } from '../apis/client'; 8 | 9 | import { banksInfo } from '../recoil/accntAtoms'; 10 | 11 | function useBanksData() { 12 | const setBanksInfo = useSetRecoilState(banksInfo); 13 | const navigate = useNavigate(); 14 | useQuery>('getBanks', () => goalApi.getBanks(), { 15 | select: (data) => data.slice(2, -1), 16 | onSuccess: (data) => { 17 | setBanksInfo(data); 18 | }, 19 | onError: (e) => { 20 | if (e === 401) { 21 | navigate('/', { replace: true }); 22 | } 23 | }, 24 | }); 25 | 26 | return; 27 | } 28 | 29 | export default useBanksData; 30 | -------------------------------------------------------------------------------- /src/hooks/useDateInput.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { dateISOStringDateTranslator } from '../utils/dateTranslator'; 3 | interface useDateInputProps { 4 | startDate: Date; 5 | initVal?: string; 6 | minDays: number; 7 | maxDays: number; 8 | } 9 | 10 | const useDateInput = ({ startDate, initVal, minDays, maxDays }: useDateInputProps) => { 11 | const getFutureDate = (startDate: Date, afterDays: number) => { 12 | const funtureDate = new Date().setDate(startDate.getDate() + afterDays); 13 | return dateISOStringDateTranslator(new Date(funtureDate)); 14 | }; 15 | 16 | const [start, setStart] = useState(startDate); 17 | const [minDate, setMinDate] = useState(getFutureDate(startDate, minDays)); 18 | const [maxDate, setMaxDate] = useState(getFutureDate(startDate, maxDays)); 19 | const [value, setValue] = useState(initVal ? initVal : minDate); 20 | 21 | const onChangeStartDate = (date: Date) => { 22 | setStart(date); 23 | }; 24 | 25 | const onChangeEndDate = (e: React.FormEvent) => { 26 | setValue(e.currentTarget.value); 27 | }; 28 | 29 | useEffect(() => { 30 | setMinDate(getFutureDate(start, minDays)); 31 | setMaxDate(getFutureDate(start, maxDays)); 32 | }, [start]); 33 | 34 | useEffect(() => { 35 | if (!initVal) setValue(minDate); 36 | }, [minDate]); 37 | 38 | return { minDate, maxDate, start, value, onChangeStartDate, onChangeEndDate }; 39 | }; 40 | 41 | export default useDateInput; 42 | -------------------------------------------------------------------------------- /src/hooks/useEmojiSelect.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { EmojiClickData } from 'emoji-picker-react'; 3 | 4 | const useEmojiSelect = ({ initVal }: { initVal: string }) => { 5 | const [showEmojis, setShowEmojis] = useState(false); 6 | const handleShowEmojis = () => { 7 | setShowEmojis(!showEmojis); 8 | }; 9 | const [emoji, setEmoji] = useState(initVal); 10 | const handleEmojiSelect = (emoji: EmojiClickData) => { 11 | setShowEmojis(false); 12 | setEmoji(emoji.unified); 13 | }; 14 | 15 | return { showEmojis, emoji, handleShowEmojis, handleEmojiSelect }; 16 | }; 17 | 18 | export default useEmojiSelect; 19 | -------------------------------------------------------------------------------- /src/hooks/useGoalDetailData.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { useNavigate } from 'react-router-dom'; 3 | import { useSetRecoilState } from 'recoil'; 4 | import { useQuery } from 'react-query'; 5 | 6 | import { GoalStatus, GoalStatusStringtoType, IGoalDetail } from '../interfaces/interfaces'; 7 | 8 | import { goalApi } from '../apis/client'; 9 | 10 | import { isGroup, isMember } from '../utils/goalInfoChecker'; 11 | import { accountIdFinder, balanceIdFinder } from '../utils/accountInfoChecker'; 12 | 13 | import { goalDetail } from '../recoil/goalsAtoms'; 14 | 15 | interface useGoalDetailProps { 16 | loginUserId: number; 17 | goalId: string; 18 | } 19 | 20 | const fetchGoalDetail = (goalId: string) => { 21 | return goalApi.getGoalDetail(Number(goalId)); 22 | }; 23 | 24 | const useGoalDetailData = ({ loginUserId, goalId }: useGoalDetailProps) => { 25 | const [isGroupVal, setIsGroup] = useState(false); 26 | const [isMemberVal, setIsMember] = useState(false); 27 | const [status, setStatus] = useState(GoalStatus.proceeding); 28 | const [accountId, setAccountId] = useState(0); 29 | const [balanceId, setBalanceId] = useState(0); 30 | const setGoalDetail = useSetRecoilState(goalDetail); 31 | const navigate = useNavigate(); 32 | const { isLoading, data, isError } = useQuery('goalDetail', () => fetchGoalDetail(goalId), { 33 | onSuccess: (data) => { 34 | setGoalDetail(data); 35 | setIsGroup(isGroup(data.headCount)); 36 | setIsMember(isMember(loginUserId, data.members)); 37 | setStatus(GoalStatusStringtoType(data.status)); 38 | setAccountId(accountIdFinder(data.members, loginUserId)); 39 | setBalanceId(balanceIdFinder(data.members, loginUserId)); 40 | }, 41 | onError: (e) => { 42 | if (e === 404) { 43 | navigate('/notfound', { replace: true }); 44 | } 45 | if (e === 401) { 46 | navigate('/', { replace: true }); 47 | } 48 | }, 49 | }); 50 | 51 | return { isLoading, isError, data, isGroupVal, isMemberVal, status, accountId, balanceId }; 52 | }; 53 | 54 | export default useGoalDetailData; 55 | -------------------------------------------------------------------------------- /src/hooks/useGoalLookupData.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { useMutation } from 'react-query'; 3 | import { useNavigate } from 'react-router-dom'; 4 | import { useRecoilValue, useSetRecoilState } from 'recoil'; 5 | 6 | import { goalApi } from '../apis/client'; 7 | 8 | import { ISearchGoal, ISearchGoalResult } from '../interfaces/interfaces'; 9 | import { groupGoals, isSearchGoalLastPage, searchGoalLastUpdate } from '../recoil/goalsAtoms'; 10 | 11 | interface useGoalLookupData { 12 | initVal: Array; 13 | } 14 | 15 | const useGoalLookupData = ({ initVal }: useGoalLookupData) => { 16 | const navigate = useNavigate(); 17 | const savedLookupGoals = useRecoilValue(groupGoals); 18 | 19 | const [goals, setGoals] = useState>(initVal); 20 | const saveLookupGoals = useSetRecoilState(groupGoals); 21 | const saveIsLastPage = useSetRecoilState(isSearchGoalLastPage); 22 | const saveLastUpdate = useSetRecoilState(searchGoalLastUpdate); 23 | 24 | const { isLoading, isError, mutate } = useMutation('getGoals', goalApi.getGoals, { 25 | onSuccess: (data, cursor) => { 26 | if (cursor === 0) { 27 | setGoals([...data.result]); 28 | saveLookupGoals([...data.result]); 29 | saveIsLastPage(data.isLastPage); 30 | saveLastUpdate(new Date()); 31 | return; 32 | } 33 | 34 | setGoals((prev) => [...prev, ...data.result]); 35 | saveLookupGoals([...savedLookupGoals, ...data.result]); 36 | saveIsLastPage(data.isLastPage); 37 | saveLastUpdate(new Date()); 38 | }, 39 | onError: (error) => { 40 | if (error === 401) { 41 | navigate('/'); 42 | } 43 | }, 44 | }); 45 | 46 | return { isLoading, isError, goals, mutate }; 47 | }; 48 | 49 | export default useGoalLookupData; 50 | -------------------------------------------------------------------------------- /src/hooks/useGoalLookupImpendingData.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { useQuery } from 'react-query'; 3 | import { useNavigate } from 'react-router-dom'; 4 | 5 | import { goalApi } from '../apis/client'; 6 | 7 | import { ISearchGoal } from '../interfaces/interfaces'; 8 | 9 | const useGoalLookupImpendingData = () => { 10 | const navigate = useNavigate(); 11 | 12 | const [impendingGoals, setImpendingGoals] = useState>([]); 13 | 14 | const { isLoading, isError } = useQuery, unknown>('getImpendingGoals', goalApi.getImpendingGoals, { 15 | onSuccess: (data) => { 16 | if (data !== undefined) { 17 | return setImpendingGoals(data); 18 | } 19 | }, 20 | onError: (error) => { 21 | if (error === 401) { 22 | navigate('/'); 23 | } 24 | }, 25 | }); 26 | 27 | return { isLoading, isError, impendingGoals }; 28 | }; 29 | 30 | export default useGoalLookupImpendingData; 31 | -------------------------------------------------------------------------------- /src/hooks/useGoalModify.tsx: -------------------------------------------------------------------------------- 1 | import { useMutation } from 'react-query'; 2 | import { useNavigate } from 'react-router-dom'; 3 | import { useSetRecoilState, useRecoilValue } from 'recoil'; 4 | 5 | import { goalApi } from '../apis/client'; 6 | 7 | import { IModifyGoal } from '../interfaces/interfaces'; 8 | 9 | import { postGoal } from '../recoil/goalsAtoms'; 10 | 11 | const useGoalModify = ({ goalId }: { goalId: number }) => { 12 | const setPostGoal = useSetRecoilState(postGoal); 13 | const navigate = useNavigate(); 14 | const { isLoading, isError, mutate } = useMutation('modifyGoal', goalApi.modifyGoal, { 15 | onSuccess: () => { 16 | setPostGoal({ 17 | emoji: '26f0-fe0f', 18 | title: '', 19 | description: '', 20 | hashTag: [''], 21 | amount: 1000, 22 | startDate: new Date(), 23 | endDate: new Date(), 24 | headCount: 1, 25 | isPrivate: false, 26 | isManual: false, 27 | accountId: 0, 28 | }); 29 | 30 | setTimeout(() => navigate(`/goals/${goalId}`, { replace: true }), 2000); 31 | }, 32 | onError: (e) => { 33 | if (e === 401) { 34 | navigate('/', { replace: true }); 35 | } 36 | }, 37 | }); 38 | 39 | const savedPostGoal = useRecoilValue(postGoal); 40 | const handleModifyGoal = () => { 41 | mutate({ goalId, goal: savedPostGoal }); 42 | }; 43 | 44 | return { isLoading, isError, handleModifyGoal }; 45 | }; 46 | 47 | export default useGoalModify; 48 | -------------------------------------------------------------------------------- /src/hooks/useGoalModifyInput.tsx: -------------------------------------------------------------------------------- 1 | import { useNavigate } from 'react-router-dom'; 2 | import { useSetRecoilState } from 'recoil'; 3 | import { IPostGoal } from '../interfaces/interfaces'; 4 | 5 | import { postGoalType } from '../recoil/goalsAtoms'; 6 | import { postGoal } from '../recoil/goalsAtoms'; 7 | 8 | const useGoalModifyInput = ({ goalId }: { goalId: number }) => { 9 | const setPostGoalType = useSetRecoilState(postGoalType); 10 | const setPostGoal = useSetRecoilState(postGoal); 11 | const navigate = useNavigate(); 12 | const handleSaveGoalInput = (inputVal: IPostGoal) => { 13 | setPostGoal({ 14 | emoji: inputVal.emoji, 15 | title: inputVal.title, 16 | description: inputVal.description, 17 | hashTag: [...inputVal.hashTag], 18 | amount: inputVal.amount, 19 | startDate: inputVal.startDate, 20 | endDate: inputVal.endDate, 21 | headCount: inputVal.headCount, 22 | isPrivate: inputVal.isPrivate, 23 | isManual: inputVal.isManual, 24 | accountId: inputVal.accountId, 25 | }); 26 | 27 | setPostGoalType({ isGroup: false }); 28 | 29 | navigate(`/goals/${goalId}/modify`, { replace: true }); 30 | }; 31 | 32 | return { handleSaveGoalInput }; 33 | }; 34 | 35 | export default useGoalModifyInput; 36 | -------------------------------------------------------------------------------- /src/hooks/useGoalPostInput.tsx: -------------------------------------------------------------------------------- 1 | import { useNavigate } from 'react-router-dom'; 2 | import { useSetRecoilState } from 'recoil'; 3 | import { IPostGoal } from '../interfaces/interfaces'; 4 | 5 | import { postGoalType } from '../recoil/goalsAtoms'; 6 | import { postGoal } from '../recoil/goalsAtoms'; 7 | 8 | interface useGoalInputProps { 9 | type: 'post' | 'modify'; 10 | inputVal: IPostGoal; 11 | } 12 | 13 | const useGoalInput = ({ type, inputVal }: useGoalInputProps) => { 14 | const setPostGoalType = useSetRecoilState(postGoalType); 15 | const setPostGoal = useSetRecoilState(postGoal); 16 | const navigate = useNavigate(); 17 | const handleSaveGoalInput = () => { 18 | setPostGoal({ 19 | emoji: inputVal.emoji, 20 | title: inputVal.title, 21 | description: inputVal.description, 22 | hashTag: [...inputVal.hashTag], 23 | amount: inputVal.amount, 24 | startDate: inputVal.startDate, 25 | endDate: inputVal.endDate, 26 | headCount: inputVal.headCount, 27 | isPrivate: inputVal.isPrivate, 28 | isManual: inputVal.isManual, 29 | accountId: 0, 30 | }); 31 | 32 | setPostGoalType({ isGroup: false }); 33 | 34 | if (type === 'post') { 35 | if (inputVal.isManual) { 36 | navigate(`/goals/post/0/accounts/manual`, { replace: true }); 37 | } else { 38 | navigate('/accounts/choose'); 39 | } 40 | } 41 | }; 42 | 43 | return { handleSaveGoalInput }; 44 | }; 45 | 46 | export default useGoalInput; 47 | -------------------------------------------------------------------------------- /src/hooks/useIsManual.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { useQuery } from 'react-query'; 3 | import { useRecoilValue } from 'recoil'; 4 | 5 | import { accountApi } from '../apis/client'; 6 | 7 | import { userId } from '../recoil/userAtoms'; 8 | 9 | import { accountInfoFinder } from '../utils/accountInfoChecker'; 10 | 11 | import { IAccount } from '../interfaces/interfaces'; 12 | 13 | const useIsManual = ({ accountId }: { accountId: number }) => { 14 | const { id: loginUserId } = useRecoilValue(userId); 15 | const [isManual, setIsManual] = useState(false); 16 | const { isLoading, isError } = useQuery>('getAccounts', () => accountApi.getAccounts(loginUserId), { 17 | onSuccess: (data) => { 18 | const accountInfo = accountInfoFinder(data, accountId); 19 | setIsManual(accountInfo.bankId === 2); 20 | }, 21 | }); 22 | 23 | return { isLoading, isError, isManual }; 24 | }; 25 | 26 | export default useIsManual; 27 | -------------------------------------------------------------------------------- /src/hooks/useJoinGoal.tsx: -------------------------------------------------------------------------------- 1 | import { useMutation } from 'react-query'; 2 | 3 | import { goalApi } from '../apis/client'; 4 | import { useNavigate } from 'react-router-dom'; 5 | 6 | const useJoinGoal = ({ goalId }: { goalId: number }) => { 7 | const navigate = useNavigate(); 8 | const { 9 | isLoading, 10 | mutate: joinGoal, 11 | isError, 12 | } = useMutation('joinGoal', goalApi.joinGoal, { 13 | onSuccess: () => { 14 | setTimeout(() => navigate(`/goals/${goalId}`, { replace: true }), 2000); 15 | }, 16 | onError: (e) => { 17 | if (e === 401) { 18 | navigate('/', { replace: true }); 19 | } 20 | }, 21 | }); 22 | 23 | const handleJoin = (accountId: number) => { 24 | joinGoal({ goalId, accountId }); 25 | }; 26 | 27 | return { 28 | isLoading, 29 | isError, 30 | handleJoin, 31 | }; 32 | }; 33 | 34 | export default useJoinGoal; 35 | -------------------------------------------------------------------------------- /src/hooks/useJoinGoalModal.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { useNavigate } from 'react-router-dom'; 3 | 4 | const useJoinGoalModal = ({ goalId }: { goalId: number }) => { 5 | const [showOption, setShowOption] = useState(false); 6 | // TODO: 실계좌 기능 오픈 7 | const [isManual, setIsManual] = useState(true); 8 | const handleSelectOption = (isTrue: boolean) => { 9 | setIsManual(isTrue); 10 | }; 11 | const handleSelectOptionDone = () => { 12 | if (isManual) { 13 | handleJoinEnd(); 14 | navigate(`/goals/join/${goalId}/accounts/manual`); 15 | return; 16 | } 17 | 18 | setShowOption(false); 19 | setShowAccounts(true); 20 | }; 21 | 22 | const [showAccounts, setShowAccounts] = useState(false); 23 | const [selectedAccntId, setSelectedAccntId] = useState(0); 24 | const handleSelectAccnt = (accountId: number) => { 25 | setSelectedAccntId(accountId); 26 | }; 27 | const navigate = useNavigate(); 28 | const handleSelectAccntDone = () => { 29 | if (selectedAccntId > 0) { 30 | handleJoinEnd(); 31 | navigate(`/goals/join/${goalId}/accounts/${selectedAccntId}`); 32 | return; 33 | } 34 | 35 | handleJoinEnd(); 36 | navigate(`/goals/join/${goalId}/accounts/auto`); 37 | }; 38 | 39 | const handleJoinStart = () => { 40 | setShowOption(true); 41 | }; 42 | 43 | const handleJoinEnd = () => { 44 | if (showOption) setShowOption(false); 45 | if (showAccounts) setShowAccounts(false); 46 | }; 47 | 48 | return { 49 | showOption, 50 | showAccounts, 51 | selectedAccntId, 52 | handleJoinStart, 53 | handleJoinEnd, 54 | handleSelectOption, 55 | handleSelectOptionDone, 56 | handleSelectAccnt, 57 | handleSelectAccntDone, 58 | }; 59 | }; 60 | 61 | export default useJoinGoalModal; 62 | -------------------------------------------------------------------------------- /src/hooks/useNavigateState.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | import { useNavigate } from 'react-router-dom'; 3 | import { useRecoilValue } from 'recoil'; 4 | 5 | import { userId } from '../recoil/userAtoms'; 6 | 7 | export enum Menu { 8 | home, 9 | lookup, 10 | search, 11 | my, 12 | none, 13 | } 14 | 15 | const pathMenuConverter = (path: string, userId: number) => { 16 | if (path.includes('/goals/lookup/search')) return Menu.search; 17 | if (path === '/goals/lookup') return Menu.lookup; 18 | if (path === '/home') return Menu.home; 19 | if (path.includes('/users') && !path.includes('/edit') && !path.includes('/settings')) return Menu.my; 20 | 21 | return Menu.none; 22 | }; 23 | 24 | interface useNavigateStateProps { 25 | pathname: string; 26 | userId: number; 27 | } 28 | 29 | const useNavigateState = ({ pathname, userId }: useNavigateStateProps) => { 30 | const navigate = useNavigate(); 31 | const [show, setShow] = useState(true); 32 | const [selectedMenu, setSelectedMenu] = useState(Menu.home); 33 | const handleMenuSelect = (menu: Menu) => { 34 | switch (menu) { 35 | case Menu.home: 36 | return setSelectedMenu(Menu.home); 37 | case Menu.lookup: 38 | return setSelectedMenu(Menu.lookup); 39 | case Menu.search: 40 | return setSelectedMenu(Menu.search); 41 | case Menu.my: 42 | return setSelectedMenu(Menu.my); 43 | } 44 | }; 45 | 46 | const handlePageNavigate = (menu: Menu) => { 47 | switch (menu) { 48 | case Menu.home: 49 | return navigate('/home'); 50 | case Menu.lookup: 51 | return navigate('/goals/lookup'); 52 | case Menu.my: 53 | return navigate(`/users/${userId}`); 54 | } 55 | }; 56 | 57 | useEffect(() => { 58 | if (pathname.includes('/goals/') && !pathname.includes('lookup')) return setShow(false); 59 | if (pathname.includes('/accounts')) return setShow(false); 60 | if (pathname.includes('/users/edit')) return setShow(false); 61 | if (pathname.includes('/users/') && pathname !== `/users/${userId}`) return setShow(false); 62 | if (pathname.includes('/chats')) return setShow(false); 63 | 64 | setShow(true); 65 | handleMenuSelect(pathMenuConverter(pathname, userId)); 66 | handlePageNavigate(pathMenuConverter(pathname, userId)); 67 | }, [pathname]); 68 | 69 | return { selectedMenu, show, handleMenuSelect, handlePageNavigate }; 70 | }; 71 | 72 | export default useNavigateState; 73 | -------------------------------------------------------------------------------- /src/hooks/useNumInput.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | 3 | interface useNumInputProps { 4 | initValue: number; 5 | min: number; 6 | max: number; 7 | type: '목표 금액' | '인원'; 8 | } 9 | 10 | const useNumInput = ({ initValue, min, max, type }: useNumInputProps) => { 11 | const [value, setValue] = useState(initValue); 12 | const [errMsg, setErrMsg] = useState(''); 13 | 14 | const [isValidated, setIsValidated] = useState(false); 15 | const validate = () => { 16 | if (value < min) { 17 | switch (type) { 18 | case '목표 금액': 19 | setErrMsg(`${type} 최소값은 ${min.toLocaleString()} 원 입니다.`); 20 | return; 21 | case '인원': 22 | setErrMsg(`${type} 최소값은 ${min} 명 입니다.`); 23 | return; 24 | } 25 | } 26 | 27 | if (value > max) { 28 | switch (type) { 29 | case '목표 금액': 30 | setErrMsg(`${type} 최대값은 ${max.toLocaleString()} 원 입니다.`); 31 | return; 32 | case '인원': 33 | setErrMsg(`${type} 최대값은 ${max} 명 입니다.`); 34 | return; 35 | } 36 | } 37 | 38 | setErrMsg(''); 39 | }; 40 | 41 | useEffect(() => { 42 | if (!isValidated) return; 43 | validate(); 44 | }, [value]); 45 | 46 | const onChange = (e: React.FormEvent) => { 47 | if (!isValidated) setIsValidated(true); 48 | if (isNaN(Number(e.currentTarget.value.replaceAll(',', '')))) return setValue(0); 49 | setValue(Number(e.currentTarget.value.replaceAll(',', ''))); 50 | }; 51 | 52 | const reset = () => setValue(initValue); 53 | 54 | return { value, errMsg, onChange, reset }; 55 | }; 56 | 57 | export default useNumInput; 58 | -------------------------------------------------------------------------------- /src/hooks/usePageName.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | import { useRecoilValue } from 'recoil'; 3 | 4 | import { userId } from '../recoil/userAtoms'; 5 | 6 | enum PageType { 7 | postGoal, 8 | selectAccnt, 9 | createAccnt, 10 | lookupGoal, 11 | my, 12 | editProfile, 13 | settings, 14 | none, 15 | } 16 | 17 | const PageKR = (type: PageType) => { 18 | switch (type) { 19 | case PageType.postGoal: 20 | return '목표 추가하기'; 21 | case PageType.selectAccnt: 22 | return '계좌 선택'; 23 | case PageType.createAccnt: 24 | return '계좌 연결'; 25 | case PageType.lookupGoal: 26 | return '목표 조회'; 27 | case PageType.my: 28 | return '마이페이지'; 29 | case PageType.editProfile: 30 | return '프로필 수정'; 31 | case PageType.settings: 32 | return '설정'; 33 | default: 34 | return ''; 35 | } 36 | }; 37 | 38 | const usePageName = ({ pathname }: { pathname: string }) => { 39 | const { id } = useRecoilValue(userId); 40 | const [pageType, setPageType] = useState(PageType.none); 41 | useEffect(() => { 42 | switch (pathname) { 43 | case '/goals/post/type': 44 | setPageType(PageType.postGoal); 45 | break; 46 | case '/goals/post/data/personal': 47 | setPageType(PageType.postGoal); 48 | break; 49 | case '/goals/post/data/group': 50 | setPageType(PageType.postGoal); 51 | break; 52 | case '/accounts/choose': 53 | setPageType(PageType.selectAccnt); 54 | break; 55 | case '/accounts/post': 56 | setPageType(PageType.createAccnt); 57 | break; 58 | case '/goals/lookup': 59 | setPageType(PageType.lookupGoal); 60 | break; 61 | default: 62 | setPageType(PageType.none); 63 | } 64 | if (pathname.includes('/edit')) { 65 | setPageType(PageType.editProfile); 66 | return; 67 | } 68 | if (pathname.includes('/settings')) { 69 | setPageType(PageType.settings); 70 | return; 71 | } 72 | if (pathname === `/users/${id}`) { 73 | setPageType(PageType.my); 74 | return; 75 | } 76 | }, [pathname]); 77 | 78 | const [pageName, setPageName] = useState(''); 79 | useEffect(() => { 80 | setPageName(PageKR(pageType)); 81 | }, [pageType]); 82 | 83 | return { pageName }; 84 | }; 85 | 86 | export default usePageName; 87 | -------------------------------------------------------------------------------- /src/hooks/usePinNumberRepost.tsx: -------------------------------------------------------------------------------- 1 | import { useNavigate } from 'react-router-dom'; 2 | import { useSetRecoilState } from 'recoil'; 3 | import { userAPI } from '../apis/client'; 4 | import { userId } from '../recoil/userAtoms'; 5 | 6 | import jwtDecoder from 'jwt-decode'; 7 | 8 | import { MyToken } from '../interfaces/interfaces'; 9 | 10 | const usePinNumberRepost = (loginPinNumber: string) => { 11 | const setUserId = useSetRecoilState(userId); 12 | const navigate = useNavigate(); 13 | const getAccessToken = async () => { 14 | try { 15 | const data = await userAPI.postAccessTokenByPinCode(loginPinNumber); 16 | localStorage.setItem('accessToken', data.accessToken); 17 | setUserId({ id: jwtDecoder(data.accessToken).userId }); 18 | 19 | navigate('/home'); 20 | } catch (e) { 21 | localStorage.removeItem('accessToken'); 22 | setUserId({ id: 0 }); 23 | if (e === 401) { 24 | navigate('/', { replace: true }); 25 | } 26 | } 27 | }; 28 | 29 | return { getAccessToken }; 30 | }; 31 | 32 | export default usePinNumberRepost; 33 | -------------------------------------------------------------------------------- /src/hooks/usePinNumberSignupPost.tsx: -------------------------------------------------------------------------------- 1 | import { useQuery } from 'react-query'; 2 | import { useNavigate } from 'react-router-dom'; 3 | import { userAPI } from '../apis/client'; 4 | 5 | interface PinNumberSignupPostProps { 6 | id: number; 7 | pinNumber2: string; 8 | } 9 | 10 | const usePinNumberSignupPost = ({ id, pinNumber2 }: PinNumberSignupPostProps) => { 11 | const navigate = useNavigate(); 12 | 13 | const { refetch } = useQuery( 14 | 'postPinCode', 15 | () => { 16 | userAPI.postPinCode(id, pinNumber2); 17 | }, 18 | { 19 | enabled: false, 20 | onSuccess: () => { 21 | localStorage.removeItem('isPincodeRegistered'); 22 | navigate('/welcome'); 23 | }, 24 | onError: (e) => { 25 | if (e === 401) { 26 | navigate('/', { replace: true }); 27 | } 28 | }, 29 | } 30 | ); 31 | 32 | return { refetch }; 33 | }; 34 | 35 | export default usePinNumberSignupPost; 36 | -------------------------------------------------------------------------------- /src/hooks/usePostGoal.tsx: -------------------------------------------------------------------------------- 1 | import { useMutation } from 'react-query'; 2 | import { useNavigate } from 'react-router-dom'; 3 | import { useRecoilValue } from 'recoil'; 4 | 5 | import { goalApi } from '../apis/client'; 6 | 7 | import { postGoal } from '../recoil/goalsAtoms'; 8 | 9 | const usePostGoal = ({ accountId }: { accountId: number }) => { 10 | const savedPostGoal = useRecoilValue(postGoal); 11 | const navigate = useNavigate(); 12 | const { isLoading, isError, mutate } = useMutation( 13 | 'postGoal', 14 | () => goalApi.postGoal({ ...savedPostGoal, accountId }), 15 | { 16 | onSuccess: () => { 17 | setTimeout(() => navigate(`/home`, { replace: true }), 2000); 18 | }, 19 | onError: (e) => { 20 | if (e === 401) { 21 | navigate('/', { replace: true }); 22 | } 23 | }, 24 | } 25 | ); 26 | const handlePostGoal = () => { 27 | mutate(); 28 | }; 29 | 30 | return { isLoading, isError, handlePostGoal }; 31 | }; 32 | 33 | export default usePostGoal; 34 | -------------------------------------------------------------------------------- /src/hooks/useRangeBar.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | 3 | interface useRangeBarProps { 4 | min: number; 5 | max: number; 6 | } 7 | 8 | const useRangeBar = ({ min, max }: useRangeBarProps) => { 9 | const [fixedMin] = useState(min); 10 | const [fixedMax] = useState(max); 11 | const [minVal, setMinVal] = useState(min); 12 | const [maxVal, setMaxVal] = useState(max); 13 | const [minPercent, setMinPercent] = useState(0); 14 | const [maxPercent, setMaxPercent] = useState(0); 15 | 16 | const setMin = (e: React.ChangeEvent) => { 17 | if (Number(e.target.value) >= maxVal) return; 18 | setMinVal(Number(e.target.value)); 19 | }; 20 | 21 | const setMax = (e: React.ChangeEvent) => { 22 | if (Number(e.target.value) <= minVal) return; 23 | setMaxVal(Number(e.target.value)); 24 | }; 25 | 26 | const handleRageChange = () => { 27 | setMinPercent(Math.trunc(((minVal - fixedMin) / (fixedMax - fixedMin)) * 100)); 28 | setMaxPercent(Math.trunc(((maxVal - minVal) / (fixedMax - fixedMin)) * 100)); 29 | }; 30 | 31 | useEffect(() => { 32 | handleRageChange(); 33 | }, [minVal, maxVal]); 34 | 35 | const reset = () => { 36 | setMinVal(fixedMin); 37 | setMaxVal(fixedMax); 38 | }; 39 | 40 | return { minVal, maxVal, minPercent, maxPercent, setMin, setMax, reset }; 41 | }; 42 | 43 | export default useRangeBar; 44 | -------------------------------------------------------------------------------- /src/hooks/useRangeInput.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | 3 | interface useRangeInputProps { 4 | minInitVal: number; 5 | maxInitVal: number; 6 | } 7 | 8 | const useRangeInput = ({ minInitVal, maxInitVal }: useRangeInputProps) => { 9 | const [min, setMin] = useState(minInitVal); 10 | const handleMinChange = (min: number) => { 11 | setMin(min); 12 | }; 13 | const [max, setMax] = useState(maxInitVal); 14 | const handleMaxChange = (max: number) => { 15 | setMax(max); 16 | }; 17 | const handleInitialize = () => { 18 | setMin(minInitVal); 19 | setMax(maxInitVal); 20 | }; 21 | 22 | return { 23 | min, 24 | max, 25 | handleMinChange, 26 | handleMaxChange, 27 | handleInitialize, 28 | }; 29 | }; 30 | 31 | export default useRangeInput; 32 | -------------------------------------------------------------------------------- /src/hooks/useSearchFilterInput.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | 3 | import { StatusType, SortType } from '../interfaces/interfaces'; 4 | 5 | interface useSearchFilterInputProps { 6 | initStatus: StatusType; 7 | initSort: SortType; 8 | initMin: number; 9 | initMax: number; 10 | } 11 | 12 | const useSearchFilterInput = ({ initStatus, initSort, initMin, initMax }: useSearchFilterInputProps) => { 13 | const [status, setStatus] = useState(initStatus); 14 | const handleStatusChange = (type: StatusType) => { 15 | setStatus(type); 16 | }; 17 | const [sort, setSort] = useState(initSort); 18 | const handleSortChange = (type: SortType) => { 19 | setSort(type); 20 | }; 21 | const [min, setMin] = useState(initMin); 22 | const handleMinChange = (min: number) => { 23 | setMin(min); 24 | }; 25 | const [max, setMax] = useState(initMax); 26 | const handleMaxChange = (max: number) => { 27 | setMax(max); 28 | }; 29 | 30 | return { 31 | status, 32 | sort, 33 | min, 34 | max, 35 | handleStatusChange, 36 | handleSortChange, 37 | handleMinChange, 38 | handleMaxChange, 39 | }; 40 | }; 41 | 42 | export default useSearchFilterInput; 43 | -------------------------------------------------------------------------------- /src/hooks/useSearchFilterState.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { 3 | StatusType, 4 | OrderType, 5 | SortType, 6 | ISearchFilter, 7 | StatusTypetoString, 8 | SortTypetoString, 9 | OrderTypetoString, 10 | } from '../interfaces/interfaces'; 11 | 12 | const useSearchFilterState = ({ initVal }: { initVal: ISearchFilter }) => { 13 | const [filter, setFilter] = useState(initVal); 14 | 15 | const handleFilterChange = (status: StatusType, sort: SortType, min: number, max: number) => { 16 | setFilter((prev) => { 17 | return { 18 | ...prev, 19 | status: StatusTypetoString(status), 20 | ordered: 'DESC', 21 | sorted: SortTypetoString(sort), 22 | min: min, 23 | max: max, 24 | cursor: 0, 25 | goalId: 0, 26 | }; 27 | }); 28 | }; 29 | 30 | const handleKeywordChange = (keyword: string) => { 31 | setFilter((prev) => { 32 | return { ...prev, keyword }; 33 | }); 34 | }; 35 | const handleStatusChange = (type: StatusType) => { 36 | setFilter((prev) => { 37 | return { ...prev, status: StatusTypetoString(type) }; 38 | }); 39 | }; 40 | const handleSortChange = (type: SortType) => { 41 | setFilter((prev) => { 42 | return { ...prev, sorted: SortTypetoString(type) }; 43 | }); 44 | }; 45 | const handleRangeChange = (min: number, max: number) => { 46 | setFilter((prev) => { 47 | return { ...prev, min, max }; 48 | }); 49 | }; 50 | const handleOrderTypeChange = (type: OrderType) => { 51 | setFilter((prev) => { 52 | return { ...prev, ordered: OrderTypetoString(type) }; 53 | }); 54 | }; 55 | 56 | return { 57 | filter, 58 | handleKeywordChange, 59 | handleFilterChange, 60 | handleStatusChange, 61 | handleSortChange, 62 | handleRangeChange, 63 | handleOrderTypeChange, 64 | }; 65 | }; 66 | 67 | export default useSearchFilterState; 68 | -------------------------------------------------------------------------------- /src/hooks/useSearchFilterTags.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | 3 | import { SearchFilterType, filters } from '../components/goal/searchFilter/FiltersModal'; 4 | import { StatusType, SortType } from '../interfaces/interfaces'; 5 | 6 | interface FilterTag { 7 | type: SearchFilterType; 8 | value: string; 9 | } 10 | 11 | interface useSearchFIlterTagsProps { 12 | statusChangeHandler: (type: StatusType) => void; 13 | sortChangeHandler: (type: SortType) => void; 14 | rangeChangeHandler: (min: number, max: number) => void; 15 | } 16 | 17 | const useSearchFilterTags = ({ 18 | statusChangeHandler, 19 | sortChangeHandler, 20 | rangeChangeHandler, 21 | }: useSearchFIlterTagsProps) => { 22 | const [filterTags, setFilterTags] = useState>( 23 | filters.map((v) => { 24 | return { type: v, value: '' }; 25 | }) 26 | ); 27 | 28 | const handleFilterAdd = (filter: SearchFilterType, value: string) => { 29 | setFilterTags((prev) => { 30 | const added = [...prev]; 31 | added.map((v) => { 32 | if (v.type === filter) { 33 | return (v.value = value); 34 | } 35 | }); 36 | 37 | return added; 38 | }); 39 | }; 40 | 41 | const handleFilterRemove = (filter: SearchFilterType) => { 42 | setFilterTags((prev) => { 43 | const removed = [...prev]; 44 | removed.map((v) => { 45 | if (v.type === filter) { 46 | return (v.value = ''); 47 | } 48 | }); 49 | 50 | return removed; 51 | }); 52 | 53 | if (filter === SearchFilterType.status) { 54 | statusChangeHandler(StatusType.total); 55 | return; 56 | } 57 | 58 | sortChangeHandler(SortType.none); 59 | rangeChangeHandler(0, 0); 60 | }; 61 | 62 | const handleFiltersReset = () => { 63 | setFilterTags( 64 | filters.map((v) => { 65 | return { type: v, value: '' }; 66 | }) 67 | ); 68 | }; 69 | 70 | return { filterTags, handleFilterAdd, handleFilterRemove, handleFiltersReset }; 71 | }; 72 | 73 | export default useSearchFilterTags; 74 | -------------------------------------------------------------------------------- /src/hooks/useSearchGoalsData.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { useMutation } from 'react-query'; 3 | import { useNavigate } from 'react-router-dom'; 4 | import { useRecoilValue, useSetRecoilState } from 'recoil'; 5 | 6 | import { goalApi } from '../apis/client'; 7 | 8 | import { ISearchFilter, ISearchGoal, ISearchGoalResult } from '../interfaces/interfaces'; 9 | 10 | import { groupGoals, isSearchGoalLastPage, searchFilters, searchGoalLastUpdate } from '../recoil/goalsAtoms'; 11 | 12 | const useSearchGoalsData = ({ initVal }: { initVal: Array }) => { 13 | const navigate = useNavigate(); 14 | const savedSearchGoals = useRecoilValue(groupGoals); 15 | 16 | const [searchResult, setSearchResult] = useState>(initVal); 17 | const resetIsLastPage = () => { 18 | saveIsLastPage(false); 19 | }; 20 | const [totalCnt, setTotalCnt] = useState(0); 21 | const saveSearchFilters = useSetRecoilState(searchFilters); 22 | const saveSearchGoals = useSetRecoilState(groupGoals); 23 | const saveIsLastPage = useSetRecoilState(isSearchGoalLastPage); 24 | const saveLastUpdate = useSetRecoilState(searchGoalLastUpdate); 25 | 26 | const { isLoading, isError, mutate } = useMutation( 27 | 'getSearchResult', 28 | goalApi.getGoalsByWord, 29 | { 30 | onSuccess: (data, params) => { 31 | saveSearchFilters(params); 32 | if (params.cursor === 0 && params.goalId === 0) { 33 | setSearchResult(data.result); 34 | saveSearchGoals(data.result); 35 | saveIsLastPage(data.isLastPage); 36 | saveLastUpdate(new Date()); 37 | setTotalCnt(Number(data.count)); 38 | return; 39 | } 40 | 41 | setSearchResult((prev) => [...prev, ...data.result]); 42 | saveSearchGoals([...savedSearchGoals, ...data.result]); 43 | saveIsLastPage(data.isLastPage); 44 | saveLastUpdate(new Date()); 45 | }, 46 | onError: (e) => { 47 | if (e === 401) { 48 | navigate('/', { replace: true }); 49 | } 50 | }, 51 | } 52 | ); 53 | 54 | return { isLoading, isError, searchResult, totalCnt, mutate, resetIsLastPage }; 55 | }; 56 | 57 | export default useSearchGoalsData; 58 | -------------------------------------------------------------------------------- /src/hooks/useSearchKeyword.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { useNavigate } from 'react-router-dom'; 3 | 4 | const useSearchKeyword = () => { 5 | const navigate = useNavigate(); 6 | const [onFocus, setOnFocus] = useState(false); 7 | const [searchKeyword, setSearchKeyword] = useState(''); 8 | 9 | const handleSearchButton = (searchKeyword: string) => { 10 | setSearchKeyword(''); 11 | navigate(`/goals/lookup/search?search=${searchKeyword}`); 12 | }; 13 | 14 | const handleOnKeyPress = (event: React.KeyboardEvent) => { 15 | if (event.code === 'Enter' && searchKeyword) { 16 | handleSearchButton(searchKeyword); 17 | } 18 | }; 19 | 20 | useEffect(() => { 21 | if (onFocus === true) { 22 | navigate('goals/lookup/search'); 23 | } 24 | }, [onFocus]); 25 | 26 | return { onFocus, searchKeyword, setOnFocus, setSearchKeyword, handleOnKeyPress }; 27 | }; 28 | 29 | export default useSearchKeyword; 30 | -------------------------------------------------------------------------------- /src/hooks/useSignup.tsx: -------------------------------------------------------------------------------- 1 | import { useNavigate } from 'react-router-dom'; 2 | import { useMutation } from 'react-query'; 3 | import { useSetRecoilState } from 'recoil'; 4 | import jwtDecoder from 'jwt-decode'; 5 | 6 | import { userId } from '../recoil/userAtoms'; 7 | 8 | import { userAPI } from '../apis/client'; 9 | 10 | import { ISignupResponse, MyToken } from '../interfaces/interfaces'; 11 | 12 | interface useSignupProps { 13 | type: 'naver' | 'kakao' | 'google'; 14 | } 15 | 16 | const useSignup = ({ type }: useSignupProps) => { 17 | const setAPI = () => { 18 | switch (type) { 19 | case 'naver': 20 | return userAPI.getNaverSignup; 21 | case 'kakao': 22 | return userAPI.getKakaoSignup; 23 | case 'google': 24 | return userAPI.getGoogleSignup; 25 | } 26 | }; 27 | 28 | const setUserId = useSetRecoilState(userId); 29 | const navigate = useNavigate(); 30 | const { mutate } = useMutation('naverSignup', setAPI(), { 31 | onSuccess: (data) => { 32 | localStorage.setItem('accessToken', data.accessToken); 33 | localStorage.setItem('refreshToken', data.refreshToken); 34 | localStorage.setItem('isNewComer', data.newComer ? 'true' : 'false'); 35 | localStorage.setItem('isPincodeRegistered', data.isExistPinCode ? 'true' : 'false'); 36 | localStorage.setItem('name', data.name); 37 | setUserId({ id: jwtDecoder(data.accessToken).userId }); 38 | 39 | if (data.newComer === true || !data.isExistPinCode) { 40 | return navigate('/pinnumber', { replace: true }); 41 | } else { 42 | return navigate('/home'); 43 | } 44 | }, 45 | onError: (e) => { 46 | alert(`${type} 로그인에 실패했습니다.\n${e}\n관리자에게 문의해주세요.\nsonewdim@naver.com`); 47 | localStorage.clear(); 48 | }, 49 | }); 50 | 51 | return { mutate }; 52 | }; 53 | 54 | export default useSignup; 55 | -------------------------------------------------------------------------------- /src/hooks/useTab.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | 3 | interface ITab { 4 | title: string; 5 | isSelected: boolean; 6 | } 7 | 8 | const useTab = () => { 9 | const [tabs, setTabs] = useState>([ 10 | { title: '목표', isSelected: true }, 11 | { title: '뱃지', isSelected: false }, 12 | ]); 13 | 14 | const handleTabClick = (selectedTab: string) => { 15 | setTabs((prev) => { 16 | const tabList = [...prev]; 17 | tabList.map((v) => { 18 | if (v.title === selectedTab) { 19 | v.isSelected = true; 20 | return; 21 | } 22 | v.isSelected = false; 23 | }); 24 | 25 | return tabList; 26 | }); 27 | }; 28 | 29 | return { tabs, handleTabClick }; 30 | }; 31 | 32 | export default useTab; 33 | -------------------------------------------------------------------------------- /src/hooks/useTagInput.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | 3 | const useTagInput = ({ initVal }: { initVal: Array }) => { 4 | const [tagList, setTagList] = useState>(initVal); 5 | 6 | const handleTagListChange = (tagList: Array) => { 7 | setTagList(tagList); 8 | }; 9 | 10 | return { tagList, handleTagListChange }; 11 | }; 12 | 13 | export default useTagInput; 14 | -------------------------------------------------------------------------------- /src/hooks/useTxtInput.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | 3 | interface useInputProps { 4 | initValue: string; 5 | minLength: number; 6 | maxLength: number; 7 | type: 8 | | '제목' 9 | | '설명' 10 | | '해시태그' 11 | | '계좌번호' 12 | | '계좌 비밀번호' 13 | | '인터넷 뱅킹 아이디' 14 | | '인터넷 뱅킹 비밀번호' 15 | | '계좌 입금자명'; 16 | regExp?: RegExp; 17 | } 18 | 19 | const useTxtInput = ({ initValue, minLength, maxLength, type, regExp }: useInputProps) => { 20 | const [value, setValue] = useState(initValue); 21 | const [errMsg, setErrMsg] = useState(''); 22 | 23 | const [isValidated, setIsValidated] = useState(false); 24 | const maxValidate = () => { 25 | if (value.length > maxLength) { 26 | setErrMsg(`${type} 길이는 최대 ${maxLength}자 입니다.`); 27 | setValue((prev) => prev.slice(0, maxLength + 1)); 28 | return false; 29 | } 30 | 31 | return true; 32 | }; 33 | 34 | const minValidate = () => { 35 | if (minLength === 0) return true; 36 | if (value.length < minLength) { 37 | setErrMsg(`${type} 길이는 최소 ${minLength}자 입니다.`); 38 | return false; 39 | } 40 | 41 | return true; 42 | }; 43 | 44 | const regExpValidate = () => { 45 | if (!regExp) return true; 46 | const isValid = regExp.test(value); 47 | if (!isValid) { 48 | setErrMsg(`${type}의 형식에 맞지 않는 값 입니다.`); 49 | return false; 50 | } 51 | 52 | return true; 53 | }; 54 | 55 | const validate = () => { 56 | const isMinValid = minValidate(); 57 | const isMaxValid = maxValidate(); 58 | const isRegExpValid = regExpValidate(); 59 | 60 | if (isMinValid && isMaxValid && isRegExpValid) { 61 | setErrMsg(''); 62 | } 63 | }; 64 | 65 | useEffect(() => { 66 | if (!isValidated) return; 67 | validate(); 68 | }, [value]); 69 | 70 | const onChange = (e: React.FormEvent) => { 71 | if (!isValidated) setIsValidated(true); 72 | setValue(e.currentTarget.value); 73 | }; 74 | 75 | const reset = () => setValue(initValue); 76 | 77 | return { value, errMsg, onChange, reset }; 78 | }; 79 | 80 | export default useTxtInput; 81 | -------------------------------------------------------------------------------- /src/hooks/useUserBadgesData.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { useNavigate } from 'react-router-dom'; 3 | import { useRecoilValue } from 'recoil'; 4 | import { useMutation } from 'react-query'; 5 | 6 | import { IBadge, IUserBadge } from '../interfaces/interfaces'; 7 | 8 | import { userAPI } from '../apis/client'; 9 | 10 | import { badges } from '../recoil/badgeAtoms'; 11 | 12 | const useUserBadgesData = ({ getUserId }: { getUserId: number }) => { 13 | const savedBadges = useRecoilValue(badges); 14 | const [userBadges, setUserBadges] = useState>([]); 15 | 16 | const navigate = useNavigate(); 17 | const { isLoading, isError, mutate } = useMutation, unknown, number>( 18 | 'userBadges', 19 | userAPI.getUserBadges, 20 | { 21 | onSuccess: (data) => { 22 | setUserBadges(() => { 23 | const modified = [...savedBadges]; 24 | return modified.map((v) => { 25 | for (const b of data) { 26 | if (v.badgeId === b.badgeId) { 27 | v = { ...v, image: v.image.split('.png')[0] + '_color.png' }; 28 | } 29 | } 30 | 31 | return v; 32 | }); 33 | }); 34 | }, 35 | onError: (e) => { 36 | if (e === 401) { 37 | navigate('/', { replace: true }); 38 | } 39 | }, 40 | } 41 | ); 42 | 43 | useEffect(() => { 44 | mutate(getUserId); 45 | }, []); 46 | 47 | return { isLoading, isError, userBadges }; 48 | }; 49 | 50 | export default useUserBadgesData; 51 | -------------------------------------------------------------------------------- /src/hooks/useUserGoalsData.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { useNavigate } from 'react-router-dom'; 3 | import { useRecoilValue } from 'recoil'; 4 | import { useQuery } from 'react-query'; 5 | 6 | import { userId } from '../recoil/userAtoms'; 7 | 8 | import { GoalStatus, GoalStatusStringtoType, IGoal } from '../interfaces/interfaces'; 9 | 10 | import { userAPI } from '../apis/client'; 11 | 12 | const getSuccessCnt = (goals: Array) => { 13 | return goals.filter((goal) => GoalStatusStringtoType(goal.status) === GoalStatus.done && goal.attainment === 100) 14 | .length; 15 | }; 16 | 17 | const getWorkingCnt = (goals: Array) => { 18 | return goals.filter((goal) => GoalStatusStringtoType(goal.status) === GoalStatus.proceeding).length; 19 | }; 20 | 21 | const getTotalCnt = (goals: Array) => { 22 | return goals.length; 23 | }; 24 | 25 | const useUserGoalsData = ({ getUserId }: { getUserId: number }) => { 26 | const loginUserId = useRecoilValue(userId); 27 | const isLoginUser = loginUserId === getUserId; 28 | const [goals, setGoals] = useState>([]); 29 | const [totalCnt, setTotalCnt] = useState(0); 30 | const [successCnt, setSuccessCnt] = useState(0); 31 | const [workingCnt, setWorkingCnt] = useState(0); 32 | const navigate = useNavigate(); 33 | const { isLoading, isError } = useQuery>('userGoals', () => userAPI.getUserGoals(getUserId), { 34 | onSuccess: (data) => { 35 | if (!isLoginUser) { 36 | const filtered = data.filter((goal) => !goal.isPrivate); 37 | setSuccessCnt(getSuccessCnt(filtered)); 38 | setWorkingCnt(getWorkingCnt(filtered)); 39 | setTotalCnt(getTotalCnt(filtered)); 40 | setGoals(data); 41 | return; 42 | } 43 | 44 | setSuccessCnt(getSuccessCnt(data)); 45 | setWorkingCnt(getWorkingCnt(data)); 46 | setTotalCnt(getTotalCnt(data)); 47 | setGoals(data); 48 | }, 49 | onError: (e) => { 50 | if (e === 404) { 51 | navigate('/notfound', { replace: true }); 52 | } 53 | if (e === 401) { 54 | navigate('/', { replace: true }); 55 | } 56 | }, 57 | }); 58 | 59 | return { isLoading, isError, totalCnt, successCnt, workingCnt, goals }; 60 | }; 61 | 62 | export default useUserGoalsData; 63 | -------------------------------------------------------------------------------- /src/hooks/useUserProfileData.tsx: -------------------------------------------------------------------------------- 1 | import { useNavigate } from 'react-router-dom'; 2 | import { useQuery } from 'react-query'; 3 | import { useRecoilValue, useSetRecoilState } from 'recoil'; 4 | 5 | import { IUserProfile } from '../interfaces/interfaces'; 6 | 7 | import { userAPI } from '../apis/client'; 8 | 9 | import { userId, userProfile } from '../recoil/userAtoms'; 10 | 11 | const useUserProfileData = ({ getUserId }: { getUserId: number }) => { 12 | const { id: loginUserId } = useRecoilValue(userId); 13 | const isLoginUser = loginUserId === getUserId; 14 | const setUserProfile = useSetRecoilState(userProfile); 15 | const navigate = useNavigate(); 16 | const { 17 | isLoading, 18 | isError, 19 | data: profile, 20 | } = useQuery('userProfile', () => userAPI.getUserProfile(getUserId), { 21 | onSuccess: (data) => { 22 | if (!isLoginUser) return; 23 | setUserProfile(data); 24 | }, 25 | onError: (e) => { 26 | if (e === 404) { 27 | navigate('/notfound', { replace: true }); 28 | } 29 | if (e === 401) { 30 | navigate('/', { replace: true }); 31 | } 32 | }, 33 | }); 34 | 35 | return { isLoading, isError, profile }; 36 | }; 37 | 38 | export default useUserProfileData; 39 | -------------------------------------------------------------------------------- /src/hooks/useUserProfileModify.tsx: -------------------------------------------------------------------------------- 1 | import { useMutation } from 'react-query'; 2 | import { useNavigate } from 'react-router-dom'; 3 | import AWS from 'aws-sdk'; 4 | 5 | import { IUpdateUserProfile } from '../interfaces/interfaces'; 6 | 7 | import { userAPI } from '../apis/client'; 8 | 9 | const useUserProfileModify = ({ userId, userProfile }: IUpdateUserProfile) => { 10 | const navigate = useNavigate(); 11 | const { mutate: profileUpdate } = useMutation( 12 | 'postEditUserProfile', 13 | userAPI.patchEditUserProfile, 14 | { 15 | onSuccess: () => { 16 | setTimeout(() => navigate(`/users/${userId}`, { replace: true }), 500); 17 | }, 18 | onError: (e) => { 19 | alert('프로필 수정에 실패하였습니다.'); 20 | if (e === 401) { 21 | navigate('/', { replace: true }); 22 | } 23 | }, 24 | } 25 | ); 26 | 27 | const handleProfileModify = (uploadFile?: File) => { 28 | if (uploadFile) { 29 | AWS.config.update({ 30 | region: process.env.REACT_APP_S3_REGION, 31 | accessKeyId: process.env.REACT_APP_S3_ACCESS_KEY, 32 | secretAccessKey: process.env.REACT_APP_S3_SECRET_KEY, 33 | }); 34 | 35 | const upload = new AWS.S3.ManagedUpload({ 36 | params: { 37 | Bucket: process.env.REACT_APP_S3_BUCKET_NAME as string, 38 | Body: uploadFile, 39 | ContentType: uploadFile.type, 40 | Key: 'data/profile/' + userId + '.' + uploadFile.name.split('.').pop(), 41 | }, 42 | }); 43 | 44 | upload 45 | .promise() 46 | .then((data) => { 47 | profileUpdate({ 48 | userId, 49 | userProfile: { ...userProfile, image: uploadFile ? data.Location : userProfile.image }, 50 | }); 51 | return; 52 | }) 53 | .catch(() => { 54 | alert('프로필 사진이 업로드되지 않았습니다.'); 55 | return; 56 | }); 57 | 58 | return; 59 | } 60 | 61 | profileUpdate({ userId, userProfile }); 62 | }; 63 | 64 | return { handleProfileModify }; 65 | }; 66 | 67 | export default useUserProfileModify; 68 | -------------------------------------------------------------------------------- /src/hooks/useUserProfileModifyInput.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useState, SetStateAction, Dispatch } from 'react'; 2 | import { SetterOrUpdater } from 'recoil'; 3 | 4 | import { IUserProfile } from '../interfaces/interfaces'; 5 | 6 | import { readFile } from '../utils/imageCropper'; 7 | 8 | interface UseUserProfileModifyInputParams { 9 | profile: IUserProfile; 10 | setShowCropper: Dispatch>; 11 | setCroppedImageData: SetterOrUpdater<{ cropImage: string }>; 12 | } 13 | 14 | const useUserProfileModifyInput = ({ 15 | profile, 16 | setShowCropper, 17 | setCroppedImageData, 18 | }: UseUserProfileModifyInputParams) => { 19 | const ref = useRef(null); 20 | 21 | const [nickname, setNickname] = useState(profile.nickname); 22 | const [description, setDescription] = useState(profile.description); 23 | 24 | const handleUploadedImageChange = async (e: React.ChangeEvent) => { 25 | if (!e.target.files) return; 26 | 27 | const uploadedFile = e.target.files[0]; 28 | 29 | if (uploadedFile?.type.indexOf('image/') === -1) { 30 | alert('이미지를 첨부해주세요!'); 31 | return; 32 | } 33 | if (uploadedFile.size / (1024 * 1024) >= 4) { 34 | alert('이미지가 너무 큽니다.'); 35 | return; 36 | } 37 | 38 | const imageDataUrl = await readFile(uploadedFile); 39 | 40 | setShowCropper(true); 41 | setCroppedImageData({ cropImage: imageDataUrl as string }); 42 | }; 43 | 44 | const handleEditProfileImage = () => { 45 | if (!ref.current) return; 46 | ref.current.click(); 47 | }; 48 | 49 | const handleNicknameChange = (e: React.FormEvent) => { 50 | setNickname(e.currentTarget.value); 51 | }; 52 | 53 | const handleDescriptionChange = (e: React.FormEvent) => { 54 | setDescription(e.currentTarget.value); 55 | }; 56 | 57 | return { 58 | ref, 59 | nickname, 60 | description, 61 | handleUploadedImageChange, 62 | handleEditProfileImage, 63 | handleNicknameChange, 64 | handleDescriptionChange, 65 | }; 66 | }; 67 | 68 | export default useUserProfileModifyInput; 69 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { Suspense } from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import { RecoilRoot } from 'recoil'; 4 | import { QueryClient, QueryClientProvider } from 'react-query'; 5 | import { ThemeProvider } from 'styled-components'; 6 | import ReactGA from 'react-ga'; 7 | 8 | import App from './App'; 9 | import reportWebVitals from './reportWebVitals'; 10 | 11 | import './styles/index.css'; 12 | import defaultTheme from './styles/theme'; 13 | 14 | const queryClient = new QueryClient(); 15 | const trackingId = process.env.REACT_APP_GOOGLE_ANALYTICS_TRACKING_ID; 16 | 17 | ReactGA.initialize(trackingId as string); 18 | 19 | const rootElement = document.getElementById('root'); 20 | if (!rootElement) throw new Error('Failed to find the root element'); 21 | const root = ReactDOM.createRoot(rootElement); 22 | root.render( 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | ); 35 | 36 | reportWebVitals(); 37 | -------------------------------------------------------------------------------- /src/pages/AgreementOfCollectionPersonalInfo.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | // TODO: media query 설정 5 | // TODO: redirect uri, client id env 파일 설정 6 | const AgreementOfCollectionPersonalInfo = () => { 7 | return 개인정보 수집활용 동의서; 8 | }; 9 | 10 | const Wrapper = styled.div` 11 | display: flex; 12 | flex-direction: column; 13 | justify-content: center; 14 | align-items: center; 15 | `; 16 | 17 | export default AgreementOfCollectionPersonalInfo; 18 | -------------------------------------------------------------------------------- /src/pages/CreateAccntAuto.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { useLocation, useNavigate, useParams } from 'react-router-dom'; 3 | import styled from 'styled-components'; 4 | 5 | import AccountNoInput from '../components/account/AccountNoInput'; 6 | import AccountNoValidate from '../components/account/AccountNoValidate'; 7 | import AccountInfoInput from '../components/account/AccountInfoInput'; 8 | 9 | import useAccntAuthState from '../hooks/useAccntAuthState'; 10 | 11 | const CreateAccntAuto = () => { 12 | const { goalId } = useParams(); 13 | const [joinGoalId, setJoinGoalId] = useState(0); 14 | useEffect(() => { 15 | if (goalId) setJoinGoalId(Number(goalId)); 16 | }, [goalId]); 17 | 18 | const { pathname } = useLocation(); 19 | const navigate = useNavigate(); 20 | const handleAccountId = (accountId: number) => { 21 | pathname.includes('join') 22 | ? navigate(`/goals/join/${joinGoalId}/accounts/${accountId}`) 23 | : navigate(`/goals/post/${accountId}`); 24 | }; 25 | 26 | const { 27 | oriSeqNo, 28 | isAuthRequested, 29 | isAuthenticated, 30 | handleSetOriSeqNo, 31 | handleIsAuthRequested, 32 | handleIsAuthenticated, 33 | handleAccntNoEdit, 34 | } = useAccntAuthState(); 35 | 36 | if (!isAuthRequested) 37 | return ( 38 | 39 | 40 | 41 | ); 42 | if (isAuthRequested && !isAuthenticated) 43 | return ( 44 | 45 | 50 | 51 | ); 52 | if (isAuthenticated) 53 | return ( 54 | 55 | 56 | 57 | ); 58 | return ; 59 | }; 60 | 61 | const Wrapper = styled.div` 62 | padding: 20px 22px; 63 | display: flex; 64 | flex-direction: column; 65 | width: calc(100% - 44px); 66 | height: calc(100% - 40px); 67 | overflow-y: auto; 68 | background-color: white; 69 | `; 70 | 71 | export default CreateAccntAuto; 72 | -------------------------------------------------------------------------------- /src/pages/CreateAccntManual.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import { useParams } from 'react-router-dom'; 3 | import styled from 'styled-components'; 4 | 5 | import Info from '../components/common/alert/Info'; 6 | 7 | import useAccntManualPost from '../hooks/useAccntManualPost'; 8 | 9 | const CreateAccntManual = () => { 10 | const { type, goalId } = useParams(); 11 | if (!type || (!goalId && type === 'join')) return <>잘못된 요청 값입니다; 12 | 13 | const { isLoading, isError, createManualAccnt } = useAccntManualPost({ type, goalId: Number(goalId) }); 14 | useEffect(() => { 15 | createManualAccnt(); 16 | }, []); 17 | 18 | if (isLoading) 19 | return ( 20 | 21 | 직접 입력 목표 정보 확인 중 입니다. 22 | 23 | ); 24 | 25 | if (isError) 26 | return ( 27 | 28 | 29 | 직접 입력 계좌 생성이 실패했습니다. 30 |
31 | 다시 시도해주세요. 32 |
33 |
34 | ); 35 | 36 | return ( 37 | 38 | 직접 입력 계좌가 연결되었습니다. 39 | 40 | ); 41 | }; 42 | 43 | const Wrapper = styled.div` 44 | padding: 20px 22px; 45 | display: flex; 46 | flex-direction: column; 47 | justify-content: space-between; 48 | width: calc(100% - 44px); 49 | height: calc(100% - 40px); 50 | `; 51 | 52 | export default CreateAccntManual; 53 | -------------------------------------------------------------------------------- /src/pages/CreateGoalData.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useParams } from 'react-router-dom'; 3 | import { useRecoilValue } from 'recoil'; 4 | import styled from 'styled-components'; 5 | 6 | import GoalInfoInput from '../components/goal/post/GoalInfoInput'; 7 | import Info from '../components/common/alert/Info'; 8 | 9 | import useAccountsData from '../hooks/useAccountsData'; 10 | 11 | import { postGoal } from '../recoil/goalsAtoms'; 12 | 13 | import { isManualAccountAddable } from '../utils/accountInfoChecker'; 14 | 15 | import RouteChangeTracker from '../shared/RouteChangeTracker'; 16 | 17 | const CreateGoalData = () => { 18 | RouteChangeTracker(); 19 | const { type } = useParams(); 20 | const { accounts } = useAccountsData(); 21 | const savedPostGoal = useRecoilValue(postGoal); 22 | 23 | return ( 24 | 25 | {isManualAccountAddable(accounts) ? ( 26 | 27 | ) : ( 28 | 29 | 최대 목표 개수만큼 진행 중입니다. 30 | 31 | 목표는 최대 10개까지 동시 진행할 수 있습니다. 32 |
33 | 현재 진행 중인 목표가 완료된 이후,
34 | 목표 생성 및 참여가 가능합니다. 35 |
36 |
37 | )} 38 |
39 | ); 40 | }; 41 | 42 | const Wrapper = styled.div` 43 | position: relative; 44 | padding: 28px 22px 20px; 45 | display: flex; 46 | flex-direction: column; 47 | width: calc(100% - 44px); 48 | height: calc(100% - 48px); 49 | background-color: white; 50 | `; 51 | 52 | const SubInfo = styled.div` 53 | font: ${(props) => props.theme.paragraphsP3R}; 54 | `; 55 | 56 | export default CreateGoalData; 57 | -------------------------------------------------------------------------------- /src/pages/GoogleLogin.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | import Info from '../components/common/alert/Info'; 5 | import Contact from '../components/common/elem/Contact'; 6 | 7 | import useSignup from '../hooks/useSignup'; 8 | 9 | import RouteChangeTracker from '../shared/RouteChangeTracker'; 10 | 11 | const GoogleLogin = () => { 12 | RouteChangeTracker(); 13 | const code = new URL(window.location.href).searchParams.get('code'); 14 | if (!code) 15 | return ( 16 | 17 | 18 | 잘못된 코드를 받았습니다. 19 |
20 | 관리자에게 문의해주세요 21 |
22 | 23 |
24 |
25 | ); 26 | 27 | const { mutate } = useSignup({ type: 'google' }); 28 | 29 | useEffect(() => { 30 | mutate(code); 31 | }, []); 32 | 33 | return ( 34 | 35 | 구글로 로그인 중입니다. 36 | 37 | ); 38 | }; 39 | 40 | const Wrapper = styled.div` 41 | display: flex; 42 | flex-direction: column; 43 | justify-content: center; 44 | align-items: center; 45 | height: 100%; 46 | width: 100%; 47 | `; 48 | 49 | export default GoogleLogin; 50 | -------------------------------------------------------------------------------- /src/pages/JoinGoal.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import { useParams } from 'react-router-dom'; 3 | import styled from 'styled-components'; 4 | 5 | import Info from '../components/common/alert/Info'; 6 | import TextButton from '../components/common/elem/TextButton'; 7 | 8 | import useJoinGoal from '../hooks/useJoinGoal'; 9 | 10 | import RouteChangeTracker from '../shared/RouteChangeTracker'; 11 | 12 | const JoinGoal = () => { 13 | RouteChangeTracker(); 14 | const { goalId, accountId } = useParams(); 15 | if (!goalId || !accountId) return <>잘못된 요청 값입니다.; 16 | const { isLoading, isError, handleJoin } = useJoinGoal({ goalId: Number(goalId) }); 17 | useEffect(() => { 18 | handleJoin(Number(accountId)); 19 | }, []); 20 | 21 | if (isLoading) 22 | return ( 23 | 24 | 목표에 참여 요청 중 입니다. 25 | 26 | ); 27 | 28 | if (isError) 29 | return ( 30 | 31 | 32 | 목표 참여가 실패했습니다. 33 |
34 | 다시 시도해주세요. 35 |
36 | handleJoin(Number(accountId))} /> 37 |
38 | ); 39 | 40 | return ( 41 | 42 | 목표 참여가 완료되었습니다. 43 | 44 | ); 45 | }; 46 | 47 | const Wrapper = styled.div` 48 | padding: 20px 22px; 49 | display: flex; 50 | flex-direction: column; 51 | justify-content: space-between; 52 | width: calc(100% - 44px); 53 | height: calc(100% - 40px); 54 | `; 55 | 56 | export default JoinGoal; 57 | -------------------------------------------------------------------------------- /src/pages/KakaoLogin.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | import Info from '../components/common/alert/Info'; 5 | import Contact from '../components/common/elem/Contact'; 6 | 7 | import useSignup from '../hooks/useSignup'; 8 | 9 | import RouteChangeTracker from '../shared/RouteChangeTracker'; 10 | 11 | const KakaoLogin = () => { 12 | RouteChangeTracker(); 13 | const code = new URL(window.location.href).searchParams.get('code'); 14 | if (!code) 15 | return ( 16 | 17 | 18 | 잘못된 코드를 받았습니다. 19 |
20 | 관리자에게 문의해주세요 21 |
22 | 23 |
24 |
25 | ); 26 | 27 | const { mutate } = useSignup({ type: 'kakao' }); 28 | 29 | useEffect(() => { 30 | mutate(code); 31 | }, []); 32 | 33 | return ( 34 | 35 | 카카오로 로그인 중입니다. 36 | 37 | ); 38 | }; 39 | 40 | const Wrapper = styled.div` 41 | display: flex; 42 | flex-direction: column; 43 | justify-content: center; 44 | align-items: center; 45 | height: 100%; 46 | width: 100%; 47 | `; 48 | 49 | export default KakaoLogin; 50 | -------------------------------------------------------------------------------- /src/pages/LookupGoals.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef } from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | import ImpendingGoals from '../components/goal/lookup/ImpendingGoals'; 5 | import GroupGoals from '../components/goal/lookup/GroupGoals'; 6 | import AddGoalBtn from '../components/common/elem/btn/AddGoalBtn'; 7 | 8 | import RouteChangeTracker from '../shared/RouteChangeTracker'; 9 | 10 | const LookupGoals = () => { 11 | RouteChangeTracker(); 12 | const ref = useRef(null); 13 | 14 | return ( 15 | 16 |
17 | 18 |
19 | 20 | 21 |
22 | ); 23 | }; 24 | 25 | const Wrapper = styled.div` 26 | position: relative; 27 | padding-top: 20px; 28 | display: flex; 29 | flex-direction: column; 30 | width: 100%; 31 | height: calc(100% - 20px); 32 | overflow: hidden; 33 | `; 34 | 35 | export default LookupGoals; 36 | -------------------------------------------------------------------------------- /src/pages/ModifyGoal.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import { useParams } from 'react-router-dom'; 3 | import styled from 'styled-components'; 4 | 5 | import Info from '../components/common/alert/Info'; 6 | import InfoLoading from '../components/common/alert/InfoLoading'; 7 | import TextButton from '../components/common/elem/TextButton'; 8 | 9 | import useGoalModify from '../hooks/useGoalModify'; 10 | import RouteChangeTracker from '../shared/RouteChangeTracker'; 11 | 12 | const ModifyGoal = () => { 13 | RouteChangeTracker(); 14 | const { id } = useParams(); 15 | if (!id) 16 | return ( 17 | 18 | 잘못된 요청값 입니다. 19 | 20 | ); 21 | 22 | const { isLoading, isError, handleModifyGoal } = useGoalModify({ goalId: Number(id) }); 23 | 24 | useEffect(() => { 25 | handleModifyGoal(); 26 | }, []); 27 | 28 | if (isLoading) return ; 29 | 30 | if (isError) 31 | return ( 32 | 33 | 34 | 목표 수정이 실패했습니다. 35 |
36 | 다시 시도해주세요. 37 |
38 | 39 |
40 | ); 41 | return
; 42 | }; 43 | 44 | const Wrapper = styled.div` 45 | padding: 20px 22px; 46 | display: flex; 47 | flex-direction: column; 48 | justify-content: space-between; 49 | width: calc(100% - 44px); 50 | height: calc(100% - 40px); 51 | `; 52 | 53 | export default ModifyGoal; 54 | -------------------------------------------------------------------------------- /src/pages/NaverLogin.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | import Info from '../components/common/alert/Info'; 5 | import Contact from '../components/common/elem/Contact'; 6 | 7 | import useSignup from '../hooks/useSignup'; 8 | 9 | import RouteChangeTracker from '../shared/RouteChangeTracker'; 10 | 11 | const NaverLogin = () => { 12 | RouteChangeTracker(); 13 | const code = new URL(window.location.href).searchParams.get('code'); 14 | if (!code) 15 | return ( 16 | 17 | 18 | 잘못된 코드를 받았습니다. 19 |
20 | 관리자에게 문의해주세요 21 |
22 | 23 |
24 |
25 | ); 26 | 27 | const { mutate } = useSignup({ type: 'naver' }); 28 | 29 | useEffect(() => { 30 | mutate(code); 31 | }, []); 32 | 33 | return ( 34 | 35 | 네이버로 로그인 중입니다. 36 | 37 | ); 38 | }; 39 | 40 | const Wrapper = styled.div` 41 | display: flex; 42 | flex-direction: column; 43 | justify-content: center; 44 | align-items: center; 45 | height: 100%; 46 | width: 100%; 47 | `; 48 | 49 | export default NaverLogin; 50 | -------------------------------------------------------------------------------- /src/pages/NotFoundError.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import { useNavigate } from 'react-router-dom'; 3 | import styled from 'styled-components'; 4 | 5 | import Info from '../components/common/alert/Info'; 6 | 7 | const NotFoundError = () => { 8 | const navigate = useNavigate(); 9 | 10 | useEffect(() => { 11 | setTimeout(() => navigate('/'), 3000); 12 | }, []); 13 | 14 | return ( 15 | 16 | 17 | 존재하지 않는 페이지입니다.
허용된 경로로 접근해 주세요. 18 |
19 |
20 | ); 21 | }; 22 | 23 | const Wrapper = styled.div` 24 | width: 100%; 25 | height: 100%; 26 | `; 27 | export default NotFoundError; 28 | -------------------------------------------------------------------------------- /src/pages/NotSupportedDevice.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | const NotSuppoertedDevice = () => { 5 | return ( 6 | 7 | 8 | 화면을 지원하지 않는 기기 입니다.
다른 기기에서 접속해 주세요.{' '} 9 |
10 |
11 | ); 12 | }; 13 | 14 | const Wrapper = styled.div` 15 | padding-top: 200px; 16 | display: flex; 17 | flex-direction: column; 18 | align-items: center; 19 | gap: 10px; 20 | width: 100%; 21 | height: calc(100% - 200px); 22 | background-color: white; 23 | overflow: hidden; 24 | `; 25 | 26 | const Text = styled.div` 27 | font: ${(props) => props.theme.headingH2}; 28 | text-align: center; 29 | `; 30 | 31 | export default NotSuppoertedDevice; 32 | -------------------------------------------------------------------------------- /src/pages/PostGoal.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import { useParams } from 'react-router-dom'; 3 | import styled from 'styled-components'; 4 | 5 | import Info from '../components/common/alert/Info'; 6 | import TextButton from '../components/common/elem/TextButton'; 7 | 8 | import usePostGoal from '../hooks/usePostGoal'; 9 | 10 | import RouteChangeTracker from '../shared/RouteChangeTracker'; 11 | 12 | const PostGoal = () => { 13 | RouteChangeTracker(); 14 | const { accountId } = useParams(); 15 | if (!accountId) return <>잘못된 요청 값입니다.; 16 | const { isLoading, isError, handlePostGoal } = usePostGoal({ accountId: Number(accountId) }); 17 | useEffect(() => { 18 | handlePostGoal(); 19 | }, []); 20 | 21 | if (isLoading) 22 | return ( 23 | 24 | 목표 생성 중 입니다. 25 | 26 | ); 27 | 28 | if (isError) 29 | return ( 30 | 31 | 32 | 목표 생성이 실패했습니다. 33 |
34 | 다시 시도해주세요. 35 |
36 | 37 |
38 | ); 39 | 40 | return ( 41 | 42 | 목표 생성이 완료되었습니다. 43 | 44 | ); 45 | }; 46 | 47 | const Wrapper = styled.div` 48 | padding: 20px 22px; 49 | display: flex; 50 | flex-direction: column; 51 | justify-content: space-between; 52 | width: calc(100% - 44px); 53 | height: calc(100% - 40px); 54 | `; 55 | 56 | export default PostGoal; 57 | -------------------------------------------------------------------------------- /src/pages/Prepare.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | import Info from '../components/common/alert/Info'; 5 | import RouteChangeTracker from '../shared/RouteChangeTracker'; 6 | 7 | const Prepare = () => { 8 | RouteChangeTracker(); 9 | return ( 10 | 11 | 서비스 준비 중입니다. 12 | 13 | ); 14 | }; 15 | 16 | const Wrapper = styled.div` 17 | width: 100%; 18 | height: 100%; 19 | `; 20 | 21 | export default Prepare; 22 | -------------------------------------------------------------------------------- /src/pages/Redirect.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import { useNavigate } from 'react-router'; 3 | import styled from 'styled-components'; 4 | 5 | import DesktopLayout from '../shared/DesktopLayout'; 6 | import Info from '../components/common/alert/Info'; 7 | import LogoTitle from '../components/common/elem/LogoTitle'; 8 | 9 | const Redirect = () => { 10 | const accessToken = localStorage.getItem('accessToken'); 11 | const refreshToken = localStorage.getItem('refreshToken'); 12 | const isRefreshExpire = localStorage.getItem('isRefreshExpire'); 13 | const isPincodeRegistered = localStorage.getItem('isPincodeRegistered') === 'true' ? true : false; 14 | const navigate = useNavigate(); 15 | useEffect(() => { 16 | if (accessToken && refreshToken) { 17 | navigate('/home'); 18 | return; 19 | } else if ((!isPincodeRegistered && accessToken && refreshToken) || (!accessToken && refreshToken)) { 20 | setTimeout(() => navigate('/pinnumber'), 3000); 21 | return; 22 | } else if (!accessToken && !refreshToken) { 23 | setTimeout(() => navigate('/login'), 3000); 24 | return; 25 | } 26 | }, [accessToken, refreshToken]); 27 | 28 | return ( 29 | 30 | 31 | {isRefreshExpire ? ( 32 | <> 33 | {!refreshToken ? ( 34 | 35 | 로그인 정보가 만료되었습니다. 36 |
37 | 로그인 화면으로 이동합니다. 38 |
39 | ) : ( 40 | 41 | 로그인이 만료되었습니다. 42 |
43 | 핀번호를 다시 입력해주세요. 44 |
45 | )} 46 | 47 | ) : ( 48 | 49 | 50 | 51 | )} 52 |
53 |
54 | ); 55 | }; 56 | 57 | const Wrapper = styled.div` 58 | width: 100%; 59 | height: 100%; 60 | background-color: white; 61 | overflow: hidden; 62 | `; 63 | 64 | export default Redirect; 65 | -------------------------------------------------------------------------------- /src/pages/SelectGoalType.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import { useSetRecoilState } from 'recoil'; 3 | import styled from 'styled-components'; 4 | 5 | import TypeSelect from '../components/goal/post/TypeSelectSection'; 6 | 7 | import { detailGoalId } from '../recoil/goalsAtoms'; 8 | 9 | export enum GoalType { 10 | group, 11 | personal, 12 | none, 13 | } 14 | 15 | const SelectGoalType = () => { 16 | const setGoalId = useSetRecoilState(detailGoalId); 17 | useEffect(() => { 18 | setGoalId(0); 19 | }, []); 20 | 21 | return ( 22 | 23 | 24 | 25 | ); 26 | }; 27 | 28 | const Wrapper = styled.div` 29 | padding: 28px 22px 20px; 30 | display: flex; 31 | flex-direction: column; 32 | width: calc(100% - 44px); 33 | height: calc(100% - 48px); 34 | overflow-y: auto; 35 | background-color: white; 36 | `; 37 | 38 | export default SelectGoalType; 39 | -------------------------------------------------------------------------------- /src/pages/WelcomePage.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef } from 'react'; 2 | import { useNavigate } from 'react-router-dom'; 3 | import styled from 'styled-components'; 4 | import Slider from 'react-slick'; 5 | 6 | import 'slick-carousel/slick/slick.css'; 7 | import 'slick-carousel/slick/slick-theme.css'; 8 | 9 | import DesktopLayout from '../shared/DesktopLayout'; 10 | import RouteChangeTracker from '../shared/RouteChangeTracker'; 11 | import WelcomePic from '../components/common/elem/WelcomePic'; 12 | 13 | const WelcomePage = () => { 14 | RouteChangeTracker(); 15 | const navigate = useNavigate(); 16 | const name = localStorage.getItem('name'); 17 | 18 | useEffect(() => { 19 | setTimeout(() => { 20 | localStorage.removeItem('name'); 21 | navigate('/home'); 22 | }, 13000); 23 | }, []); 24 | 25 | const settings = { 26 | dots: false, 27 | arrows: true, 28 | infinite: false, 29 | fade: true, 30 | autoplay: true, 31 | autoplaySpeed: 2500, 32 | slidesToshow: 1, 33 | slidesToScroll: 1, 34 | focusOnSelect: true, 35 | // afterChange: () => navigate('/home'), 36 | }; 37 | 38 | return ( 39 | 40 | 41 | 42 |
43 | 44 | 45 | {name} 님
46 | 가입을 환영합니다! 47 |
48 | 49 |
50 |
51 | 52 | 53 | 54 | 55 |
56 |
57 |
58 | ); 59 | }; 60 | 61 | const Wrapper = styled.div` 62 | width: 100%; 63 | height: 100%; 64 | `; 65 | 66 | const WelcomeWrapper = styled.div` 67 | padding-top: 200px; 68 | gap: 10px; 69 | display: flex; 70 | flex-direction: column; 71 | align-items: center; 72 | width: 100%; 73 | height: calc(100%-200px); 74 | overflow: hidden; 75 | `; 76 | 77 | const Text = styled.div` 78 | font: ${(props) => props.theme.headingH2}; 79 | text-align: center; 80 | `; 81 | 82 | const Img = styled.img` 83 | width: 100%; 84 | height: 100vh; 85 | `; 86 | 87 | export default WelcomePage; 88 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/recoil/accntAtoms.ts: -------------------------------------------------------------------------------- 1 | import { atom } from 'recoil'; 2 | import { recoilPersist } from 'recoil-persist'; 3 | 4 | import { IAccount, IBank, IReqAuthAccount } from '../interfaces/interfaces'; 5 | 6 | const { persistAtom } = recoilPersist(); 7 | 8 | export const banksInfo = atom>({ 9 | key: 'banksInfo', 10 | default: [ 11 | { 12 | bankId: 0, 13 | bankCode: '', 14 | bankName: '', 15 | }, 16 | ], 17 | effects_UNSTABLE: [persistAtom], 18 | }); 19 | 20 | export const accntInfo = atom({ 21 | key: 'accntInfo', 22 | default: { 23 | bankCode: '', 24 | accntNo: '', 25 | }, 26 | }); 27 | 28 | export const selectedBankInfo = atom({ 29 | key: 'selectedBankInfo', 30 | default: { 31 | bankId: 0, 32 | bankCode: '', 33 | bankName: '', 34 | }, 35 | }); 36 | -------------------------------------------------------------------------------- /src/recoil/badgeAtoms.ts: -------------------------------------------------------------------------------- 1 | import { atom } from 'recoil'; 2 | import { recoilPersist } from 'recoil-persist'; 3 | import { IBadge } from '../interfaces/interfaces'; 4 | 5 | const { persistAtom } = recoilPersist(); 6 | 7 | export const badges = atom>({ 8 | key: 'badges', 9 | default: [ 10 | { 11 | badgeId: 0, 12 | title: '', 13 | description: '', 14 | image: '', 15 | }, 16 | ], 17 | effects_UNSTABLE: [persistAtom], 18 | }); 19 | -------------------------------------------------------------------------------- /src/recoil/userAtoms.ts: -------------------------------------------------------------------------------- 1 | import { atom } from 'recoil'; 2 | import { recoilPersist } from 'recoil-persist'; 3 | 4 | import { IGoal } from '../interfaces/interfaces'; 5 | 6 | const { persistAtom } = recoilPersist(); 7 | 8 | export const userId = atom({ 9 | key: 'userId', 10 | default: { 11 | id: 0, 12 | }, 13 | effects_UNSTABLE: [persistAtom], 14 | }); 15 | 16 | export const userProfile = atom({ 17 | key: 'userProfile', 18 | default: { 19 | image: '', 20 | nickname: '', 21 | description: '', 22 | }, 23 | effects_UNSTABLE: [persistAtom], 24 | }); 25 | 26 | export const isGuideDone = atom({ 27 | key: 'isGuideDone', 28 | default: { 29 | home: false, 30 | }, 31 | effects_UNSTABLE: [persistAtom], 32 | }); 33 | 34 | export const userGoals = atom>({ 35 | key: 'userGoals', 36 | default: [ 37 | { 38 | userId: 0, 39 | goalId: 0, 40 | nickname: '', 41 | amount: 0, 42 | attainment: 0, 43 | curCount: 0, 44 | headCount: 0, 45 | startDate: new Date(), 46 | endDate: new Date(), 47 | status: 'proceeding', 48 | title: '', 49 | hashtag: [''], 50 | emoji: '', 51 | description: '', 52 | createdAt: new Date(), 53 | updatedAt: new Date(), 54 | isPrivate: false, 55 | }, 56 | ], 57 | }); 58 | 59 | export const userProfileCropImage = atom({ 60 | key: 'userProfileCropImage', 61 | default: { 62 | cropImage: '', 63 | }, 64 | }); 65 | -------------------------------------------------------------------------------- /src/reportWebVitals.ts: -------------------------------------------------------------------------------- 1 | import { ReportHandler } from 'web-vitals'; 2 | 3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => { 4 | if (onPerfEntry && onPerfEntry instanceof Function) { 5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 6 | getCLS(onPerfEntry); 7 | getFID(onPerfEntry); 8 | getFCP(onPerfEntry); 9 | getLCP(onPerfEntry); 10 | getTTFB(onPerfEntry); 11 | }); 12 | } 13 | }; 14 | 15 | export default reportWebVitals; 16 | -------------------------------------------------------------------------------- /src/shared/AuthLayout.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useState, useEffect } from 'react'; 2 | import { useNavigate, useLocation, Outlet } from 'react-router-dom'; 3 | import { useRecoilValue } from 'recoil'; 4 | import styled from 'styled-components'; 5 | 6 | import Header from './Header'; 7 | import Navigation from './Navigation'; 8 | 9 | import { userId } from '../recoil/userAtoms'; 10 | import DesktopLayout from './DesktopLayout'; 11 | 12 | const AuthLayout = () => { 13 | const accessToken = localStorage.getItem('accessToken'); 14 | const refreshToken = localStorage.getItem('refreshToken'); 15 | const navigate = useNavigate(); 16 | useEffect(() => { 17 | if (!accessToken && refreshToken) { 18 | navigate('/pinnumber'); 19 | return; 20 | } 21 | if (!accessToken && !refreshToken) { 22 | navigate('/login'); 23 | return; 24 | } 25 | }, [accessToken, refreshToken]); 26 | 27 | const { pathname } = useLocation(); 28 | const headerRef = useRef(null); 29 | const [headerNavHeight, setHeaderNavHeight] = useState(0); 30 | 31 | const { id } = useRecoilValue(userId); 32 | useEffect(() => { 33 | if (!headerRef.current) return; 34 | if ( 35 | (pathname.includes('/goals/') && !pathname.includes('lookup')) || 36 | pathname.includes('/accounts') || 37 | pathname.includes('/chats') || 38 | pathname.includes('/users/edit') || 39 | (pathname.includes('/users/') && pathname !== `/users/${id}`) 40 | ) { 41 | return setHeaderNavHeight(headerRef.current.clientHeight); 42 | } 43 | setHeaderNavHeight(headerRef.current.clientHeight + 88); 44 | }, [headerRef.current?.clientHeight, pathname]); 45 | 46 | return ( 47 | 48 | 49 |
50 | 51 | 52 | 53 | 54 | 55 | 56 | ); 57 | }; 58 | 59 | const Wrapper = styled.div` 60 | position: relative; 61 | width: 100%; 62 | height: 100%; 63 | overflow: hidden; 64 | `; 65 | 66 | const Body = styled.div<{ height: string }>` 67 | width: 100%; 68 | height: ${(props) => `calc(100% - ${props.height})`}; 69 | background-color: white; 70 | `; 71 | 72 | export default AuthLayout; 73 | -------------------------------------------------------------------------------- /src/shared/PublicLayout.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import { useNavigate } from 'react-router-dom'; 3 | import { Outlet } from 'react-router'; 4 | import styled from 'styled-components'; 5 | 6 | import DesktopLayout from './DesktopLayout'; 7 | 8 | const PublicLayout = () => { 9 | const navigate = useNavigate(); 10 | const accessToken = localStorage.getItem('accessToken'); 11 | const refreshToken = localStorage.getItem('refreshToken'); 12 | const isPincodeRegistered = localStorage.getItem('isPincodeRegistered') === 'true' ? true : false; 13 | 14 | useEffect(() => { 15 | if (isPincodeRegistered && accessToken && refreshToken) { 16 | navigate('/home'); 17 | return; 18 | } 19 | 20 | if (!isPincodeRegistered && accessToken && refreshToken) { 21 | navigate('/pinnumber'); 22 | return; 23 | } 24 | }, [accessToken, refreshToken]); 25 | 26 | return ( 27 | 28 | 29 | 30 | 31 | 32 | ); 33 | }; 34 | 35 | const Wrapper = styled.div` 36 | width: 100%; 37 | height: 100%; 38 | background-color: white; 39 | overflow: hidden; 40 | `; 41 | 42 | export default PublicLayout; 43 | -------------------------------------------------------------------------------- /src/shared/RefreshLayout.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import { useNavigate } from 'react-router-dom'; 3 | import { Outlet } from 'react-router'; 4 | import styled from 'styled-components'; 5 | 6 | import DesktopLayout from './DesktopLayout'; 7 | 8 | const RefreshLayout = () => { 9 | const navigate = useNavigate(); 10 | const accessToken = localStorage.getItem('accessToken'); 11 | const refreshToken = localStorage.getItem('refreshToken'); 12 | const isPincodeRegistered = localStorage.getItem('isPincodeRegistered') === 'true' ? true : false; 13 | 14 | useEffect(() => { 15 | if (isPincodeRegistered && accessToken && refreshToken) { 16 | navigate('/home'); 17 | return; 18 | } 19 | 20 | if (!accessToken && !refreshToken) { 21 | navigate('/login'); 22 | return; 23 | } 24 | }, [accessToken, refreshToken]); 25 | 26 | return ( 27 | 28 | 29 | 30 | 31 | 32 | ); 33 | }; 34 | 35 | const Wrapper = styled.div` 36 | width: 100%; 37 | height: 100%; 38 | `; 39 | 40 | export default RefreshLayout; 41 | -------------------------------------------------------------------------------- /src/shared/RouteChangeTracker.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import ReactGA from 'react-ga'; 3 | 4 | const trackingId = process.env.REACT_APP_GOOGLE_ANALYTICS_TRACKING_ID; 5 | 6 | const RouteChangeTracker = () => { 7 | const pathName = window.location.pathname; 8 | useEffect(() => { 9 | getGA(); 10 | }, [pathName]); 11 | 12 | const getGA = () => { 13 | ReactGA.initialize(trackingId as string); 14 | ReactGA.set({ page: pathName }); 15 | ReactGA.pageview(pathName); 16 | }; 17 | }; 18 | 19 | export default RouteChangeTracker; 20 | -------------------------------------------------------------------------------- /src/styles/colors.ts: -------------------------------------------------------------------------------- 1 | export const Colors = Object.freeze({ 2 | primary: '#5FCF89', 3 | sub: '#AFFF383', 4 | kakao: '#ffe812', 5 | naver: '#03C75A', 6 | white: '#FFFFFF', 7 | black: '#000000', 8 | }); 9 | 10 | // TODO: 삭제 예정 11 | -------------------------------------------------------------------------------- /src/styles/index.css: -------------------------------------------------------------------------------- 1 | /* http://meyerweb.com/eric/tools/css/reset/ 2 | v2.0 | 20110126 3 | License: none (public domain) 4 | */ 5 | 6 | /* prettier-ignore */ 7 | html, body, div, span, applet, object, iframe, 8 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 9 | a, abbr, acronym, address, big, cite, code, 10 | del, dfn, em, img, ins, kbd, q, s, samp, 11 | small, strike, strong, sub, sup, tt, var, 12 | b, u, i, center, 13 | dl, dt, dd, ol, ul, li, 14 | fieldset, form, label, legend, 15 | table, caption, tbody, tfoot, thead, tr, th, td, 16 | article, aside, canvas, details, embed, 17 | figure, figcaption, footer, header, hgroup, 18 | menu, nav, output, ruby, section, summary, 19 | time, mark, audio, video { 20 | margin: 0; 21 | padding: 0; 22 | border: 0; 23 | vertical-align: baseline; 24 | } 25 | 26 | @media (orientation: landscape) { 27 | @media screen and (max-width: 700px) { 28 | html { 29 | transform: rotate(-90deg); 30 | } 31 | } 32 | } 33 | 34 | /* HTML5 display-role reset for older browsers */ 35 | article, 36 | aside, 37 | details, 38 | figcaption, 39 | figure, 40 | footer, 41 | header, 42 | hgroup, 43 | menu, 44 | nav, 45 | section { 46 | display: block; 47 | } 48 | 49 | body { 50 | line-height: 1; 51 | } 52 | 53 | ol, 54 | ul { 55 | list-style: none; 56 | } 57 | 58 | blockquote, 59 | q { 60 | quotes: none; 61 | } 62 | 63 | blockquote:before, 64 | blockquote:after, 65 | q:before, 66 | q:after { 67 | content: ''; 68 | content: none; 69 | } 70 | 71 | table { 72 | border-collapse: collapse; 73 | border-spacing: 0; 74 | } 75 | 76 | a { 77 | color: white !important; 78 | text-decoration: none !important; 79 | } 80 | -------------------------------------------------------------------------------- /src/styles/theme.ts: -------------------------------------------------------------------------------- 1 | const defaultTheme = { 2 | // typography 3 | headingH1: 'bold 32px "SUIT"', 4 | headingH2: 'bold 24px "SUIT"', 5 | headingH3: 'bold 20px "SUIT"', 6 | headingH4: 'bold 18px "SUIT"', 7 | paragraphsP1M: '600 20px "SUIT"', 8 | paragraphsP2M: '600 18px "SUIT"', 9 | paragraphsP3M: '600 16px "SUIT"', 10 | paragraphsP1R: '400 20px "SUIT"', 11 | paragraphsP2R: '400 18px "SUIT"', 12 | paragraphsP3R: '400 16px "SUIT"', 13 | captionC1: '400 14px "SUIT"', 14 | captionC2: '400 12px "SUIT"', 15 | captionC3: '400 10px "SUIT"', 16 | // color 17 | primary50: '#e4f7ea', 18 | primary100: '#beeacc', 19 | primary200: '#92dcac', 20 | primaryMain: '#5fcf8a', 21 | primary400: '#2bc470', 22 | primary500: '#00b858', 23 | primary600: '#00a94e', 24 | primary700: '#009642', 25 | primary800: '#008537', 26 | primary900: '#006523', 27 | secondary50: '#fffde9', 28 | secondary100: '#fef9c8', 29 | secondary200: '#fef5a4', 30 | secondaryMain: '#fff383', 31 | secondary400: '#fdee68', 32 | secondary500: '#fae94c', 33 | secondary600: '#fcdb4b', 34 | secondary700: '#f9c342', 35 | secondary800: '#f6ac39', 36 | secondary900: '#f18529', 37 | gray50: '#fbfbfb', 38 | gray100: '#f7f7f7', 39 | gray200: '#f1f1f1', 40 | gray300: '#e4e4e4', 41 | gray400: '#c2c2c2', 42 | gray500: '#a3a3a3', 43 | gray600: '#7a7a7a', 44 | gray700: '#666666', 45 | gray800: '#464646', 46 | gray900: '#252525', 47 | }; 48 | 49 | export default defaultTheme; 50 | -------------------------------------------------------------------------------- /src/utils/accountInfoChecker.ts: -------------------------------------------------------------------------------- 1 | import { IAccount, IMemeberInfo } from '../interfaces/interfaces'; 2 | 3 | export const accountIdFinder = (members: Array, userId: number) => { 4 | const found = members.find((member) => member.userId === userId); 5 | const accountId = !found ? 0 : found.accountId; 6 | 7 | return accountId; 8 | }; 9 | 10 | export const balanceIdFinder = (members: Array, userId: number) => { 11 | const found = members.find((member) => member.userId === userId); 12 | const balanceId = !found ? 0 : found.balanceId; 13 | 14 | return balanceId; 15 | }; 16 | 17 | export const accountInfoFinder = (accounts: Array, accountId: number): IAccount => { 18 | const found = accounts.find((accnt) => accnt.accountId === accountId); 19 | const account: IAccount = !found ? { accountId: 0, bankId: 0, acctNo: '', connected: false } : found; 20 | 21 | return account; 22 | }; 23 | 24 | export const availAutoAccountFinder = (accounts: Array): Array => { 25 | return accounts.filter((accnt) => accnt.bankId !== 2 && !accnt.connected); 26 | }; 27 | 28 | export const isAutoAccountAddable = (accounts: Array): boolean => { 29 | return accounts.filter((accnt) => accnt.bankId !== 2).length < 1; 30 | }; 31 | 32 | export const availManualAccountFinder = (accounts: Array): Array => { 33 | return accounts.filter((accnt) => accnt.bankId === 2 && !accnt.connected); 34 | }; 35 | 36 | export const isManualAccountAddable = (accounts: Array): boolean => { 37 | return accounts.filter((accnt) => accnt.bankId === 2).length < 10 || availManualAccountFinder(accounts).length > 0; 38 | }; 39 | -------------------------------------------------------------------------------- /src/utils/dDayCalculator.ts: -------------------------------------------------------------------------------- 1 | export const dDayCalculator = (targetDate: Date): number => { 2 | const today = new Date().getTime(); 3 | const endDay = new Date(new Date(targetDate.setHours(24)).setMinutes(0)).setSeconds(0); 4 | const dDay = Math.trunc((endDay - today) / (60000 * 60 * 24)); 5 | 6 | return dDay; 7 | }; 8 | -------------------------------------------------------------------------------- /src/utils/dateTranslator.ts: -------------------------------------------------------------------------------- 1 | // dateStringTranslator returns date to formatted string timestamp 2 | // ex.'YYYY/MM/DD(Day)' 3 | export const dateStringTranslator = (targetDate: Date) => { 4 | const dayIndex = (day: number) => { 5 | switch (day) { 6 | case 0: 7 | return '일'; 8 | case 1: 9 | return '월'; 10 | case 2: 11 | return '화'; 12 | case 3: 13 | return '수'; 14 | case 4: 15 | return '목'; 16 | case 5: 17 | return '금'; 18 | case 6: 19 | return '토'; 20 | } 21 | }; 22 | 23 | const endDateIndicator = 24 | targetDate.getFullYear() + 25 | '/' + 26 | (targetDate.getMonth() + 1) + 27 | '/' + 28 | targetDate.getDate() + 29 | '(' + 30 | dayIndex(targetDate.getDay()) + 31 | ')'; 32 | 33 | return endDateIndicator; 34 | }; 35 | 36 | export const dateStringTranslatorWithPoint = (date: Date) => { 37 | const endDateIndicator = date.getFullYear() + '.' + (date.getMonth() + 1) + '.' + date.getDate(); 38 | 39 | return endDateIndicator; 40 | }; 41 | 42 | // dateISOStringDateTranslator returns date to local ISO formatted string timestamp 43 | // ex. 'YYYY-MM-DD' 44 | export const dateISOStringDateTranslator = (date: Date) => { 45 | const localTimeOffset = new Date().getTimezoneOffset() * 60000; 46 | const localDate = date.getTime() - localTimeOffset; 47 | return new Date(localDate).toISOString().split('T')[0]; 48 | }; 49 | 50 | export const dateCalculator = (startDate: Date, endDate: Date) => { 51 | const start = startDate.getTime() / (1000 * 60 * 60 * 24); 52 | const end = endDate.getTime() / (1000 * 60 * 60 * 24); 53 | return end - start; 54 | }; 55 | -------------------------------------------------------------------------------- /src/utils/goalInfoChecker.ts: -------------------------------------------------------------------------------- 1 | import { IMemeberInfo } from '../interfaces/interfaces'; 2 | 3 | export const participantFinder = (members: Array, userId: number) => { 4 | const found = members.find((member) => member.userId === userId); 5 | const participant = !found ? { userId: 0, accountId: 0, nickname: '', image: '', attainment: 0 } : found; 6 | 7 | return participant; 8 | }; 9 | 10 | export const isGroup = (headCount: number) => { 11 | return headCount !== 1; 12 | }; 13 | 14 | export const isMember = (userId: number, members: Array) => { 15 | return members.findIndex((member) => member.userId === userId) !== -1; 16 | }; 17 | -------------------------------------------------------------------------------- /src/utils/imageCropper.tsx: -------------------------------------------------------------------------------- 1 | import { Area } from 'react-easy-crop/types'; 2 | 3 | export const createImage = (url: string) => 4 | new Promise((resolve, reject) => { 5 | const image = new Image(); 6 | image.addEventListener('load', () => resolve(image)); 7 | image.addEventListener('error', (error) => reject(error)); 8 | image.setAttribute('crossOrigin', 'anonymous'); 9 | image.src = url; 10 | }); 11 | 12 | export default async function getCroppedImg( 13 | imageSrc: string, 14 | pixelCrop: Area, 15 | flip = { horizontal: false, vertical: false } 16 | ) { 17 | const image: any = await createImage(imageSrc); 18 | const canvas = document.createElement('canvas'); 19 | const ctx = canvas.getContext('2d'); 20 | 21 | if (!ctx) { 22 | return null; 23 | } 24 | 25 | canvas.width = image.width; 26 | canvas.height = image.height; 27 | 28 | ctx.translate(image.width / 2, image.height / 2); 29 | ctx.scale(flip.horizontal ? -1 : 1, flip.vertical ? -1 : 1); 30 | ctx.translate(-image.width / 2, -image.height / 2); 31 | ctx.drawImage(image, 0, 0); 32 | 33 | const data = ctx.getImageData(pixelCrop.x, pixelCrop.y, pixelCrop.width, pixelCrop.height); 34 | 35 | canvas.width = pixelCrop.width; 36 | canvas.height = pixelCrop.height; 37 | 38 | ctx.putImageData(data, 0, 0); 39 | 40 | return canvas.toDataURL('image/jpeg'); 41 | } 42 | 43 | export function readFile(file: File) { 44 | return new Promise((resolve) => { 45 | const reader = new FileReader(); 46 | reader.addEventListener('load', () => resolve(reader.result), false); 47 | reader.readAsDataURL(file); 48 | }); 49 | } 50 | -------------------------------------------------------------------------------- /src/utils/jwtDecoder.ts: -------------------------------------------------------------------------------- 1 | import jwtDecode from 'jwt-decode'; 2 | import { MyToken } from '../interfaces/interfaces'; 3 | 4 | export const getUserIdFromAccessToken = (token: string) => { 5 | return jwtDecode(token).userId; 6 | }; 7 | -------------------------------------------------------------------------------- /src/utils/privateInfoFormatter.ts: -------------------------------------------------------------------------------- 1 | interface privateInfoFormatterParams { 2 | data: string | number; 3 | showLen: number; 4 | showDir: 'head' | 'tail'; 5 | } 6 | 7 | export const privateInfoFormatter = ({ data, showLen, showDir }: privateInfoFormatterParams): string => { 8 | const strData = String(data); 9 | 10 | if (showDir === 'head') { 11 | return strData.slice(0, showLen) + '*'.repeat(strData.length - showLen); 12 | } 13 | 14 | return '*'.repeat(strData.length - showLen) + strData.slice(-showLen); 15 | }; 16 | -------------------------------------------------------------------------------- /src/utils/progressState.ts: -------------------------------------------------------------------------------- 1 | export const setProgressState = (attainment: number): string => { 2 | if (attainment < 10) return '💝 목표를 이루기 위해 다짐한 멋진 마음! 응원할게요'; 3 | if (attainment < 20) return '⛰ 차근차근 모은 티끌이 조만간 태산이 될거에요'; 4 | if (attainment < 30) return '😎 시작했을 때의 다짐을 떠올려 봐요'; 5 | if (attainment < 40) return '🥳 어느새 3분의 1 넘게 모으고 있어요! 금새 절반에 다다를 거에요'; 6 | if (attainment < 50) return '👍 절반에 가까워지고 있어요! 최고에요'; 7 | if (attainment < 60) return '🤩 벌써 절반을 넘었어요! 잘하고 있어요!'; 8 | if (attainment < 70) return '🤸 목표 달성 했을 때의 즐거움을 떠올려봐요'; 9 | if (attainment < 80) return '🏃 목표 달성까지 얼마 남지 않았어요! 조금만 더 힘내봐요'; 10 | if (attainment < 90) return '🤟 목표 달성이 코앞이에요!'; 11 | if (attainment < 100) return '🌄 정상에 오르기 일보 직전!'; 12 | if (attainment === 100) return '🎉 목표를 달성했어요! 정말 멋져요'; 13 | 14 | return 'undefined goal attainment value'; 15 | }; 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "types": ["react/next", "react-dom/next"], 6 | "allowJs": true, 7 | "skipLibCheck": true, 8 | "esModuleInterop": true, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "noFallthroughCasesInSwitch": true, 13 | "module": "esnext", 14 | "moduleResolution": "node", 15 | "resolveJsonModule": true, 16 | "isolatedModules": true, 17 | "noEmit": true, 18 | "jsx": "react-jsx" 19 | }, 20 | "include": ["src"] 21 | } 22 | --------------------------------------------------------------------------------