├── .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