├── .eslintrc.js ├── .github ├── CODEOWNERS └── pull_request_template.md ├── .gitignore ├── .husky └── prepare-commit-msg ├── .prettierignore ├── .prettierrc.json ├── .vscode └── settings.json ├── README.md ├── craco.config.js ├── package-lock.json ├── package.json ├── postcss.config.js ├── public ├── _redirects ├── favicon.ico ├── favicon1.png ├── index.html └── robots.txt ├── src ├── App.tsx ├── apis │ ├── auth.ts │ ├── instance.ts │ ├── myRecord.ts │ ├── record.ts │ ├── reply.ts │ ├── share.ts │ └── user.ts ├── assets │ ├── 404.svg │ ├── 6x12RightArrow.svg │ ├── Expand_right.svg │ ├── ImageContainer │ │ ├── left_Arrow.svg │ │ └── right_Arrow.svg │ ├── back.svg │ ├── camera.svg │ ├── check.svg │ ├── chip_icon │ │ ├── cake.svg │ │ ├── celebrate.svg │ │ ├── consolate.svg │ │ ├── depress.svg │ │ ├── happy.svg │ │ ├── index.ts │ │ ├── love.svg │ │ ├── mySide.svg │ │ └── sympathy.svg │ ├── close_icon.svg │ ├── collect_page_icon │ │ ├── collapse.svg │ │ ├── reset.svg │ │ ├── reset_disabled.svg │ │ └── scrollTop.svg │ ├── constant │ │ ├── RecordColors.ts │ │ ├── RecordIcons.ts │ │ ├── collect.ts │ │ ├── constant.ts │ │ ├── others.ts │ │ └── ranking.ts │ ├── deleteIcon.svg │ ├── detail_page_icon │ │ ├── Close.svg │ │ ├── arrow_down.svg │ │ └── arrow_up.svg │ ├── firecracker.svg │ ├── front.svg │ ├── front_white.svg │ ├── google.svg │ ├── heart_large.svg │ ├── home_img.svg │ ├── icon_closed.svg │ ├── kakao.svg │ ├── moon_big.svg │ ├── moon_large.svg │ ├── more.svg │ ├── myRecordIcon │ │ ├── arrow_down.svg │ │ ├── arrow_up.svg │ │ ├── calendar.svg │ │ ├── close.svg │ │ ├── comment_plus.svg │ │ └── search.svg │ ├── nav_icons │ │ ├── collect_icon.svg │ │ ├── home_icon.svg │ │ ├── myrecord_icon.svg │ │ ├── record_icon.svg │ │ └── setting_icon.svg │ ├── pin.svg │ ├── plus.svg │ ├── present_box.svg │ ├── ranking_btn_arrow.svg │ ├── ranking_down_arrow.svg │ ├── record_icons │ │ ├── crown.svg │ │ ├── gift.svg │ │ ├── heart.svg │ │ ├── index.ts │ │ ├── like.svg │ │ ├── lock.svg │ │ ├── medal.svg │ │ ├── moon.svg │ │ ├── music.svg │ │ ├── rocket.svg │ │ ├── speechbubble.svg │ │ ├── trashcan.svg │ │ ├── umbrella.svg │ │ └── wine.svg │ ├── right_purple_arrow.svg │ ├── settings_icon │ │ ├── check_box.svg │ │ └── reply_check.svg │ ├── sharing_img.svg │ ├── spinner.svg │ ├── teamIntroductionImage.svg │ └── umbrella_big.svg ├── components │ ├── Alert.tsx │ ├── BackButton.tsx │ ├── Button.tsx │ ├── Category.tsx │ ├── Chip.tsx │ ├── Input.tsx │ ├── Layout.tsx │ ├── Loading.tsx │ ├── Modal.tsx │ ├── MoreButton.tsx │ ├── Navbar.tsx │ ├── NavbarItem.tsx │ ├── ParrentCategoryTab.tsx │ ├── RankingItem.tsx │ ├── RankingItemNoData.tsx │ ├── RecordCard.tsx │ ├── ScrollTop.tsx │ ├── SmallToast.tsx │ ├── Spinner.tsx │ └── Toast.tsx ├── hooks │ ├── useCheckMobile.ts │ ├── useClickOutside.ts │ ├── useDebounce.ts │ ├── useImageSwipe.ts │ ├── useIntersectionObserver.ts │ ├── useSwipe.ts │ ├── useThrottle.ts │ └── useTimeoutFunc.ts ├── index.css ├── index.tsx ├── pages │ ├── AddRecord │ │ ├── AddRecord.tsx │ │ ├── AddRecordCategory.tsx │ │ ├── AddRecordColor.tsx │ │ ├── AddRecordFile.tsx │ │ ├── AddRecordIcon.tsx │ │ ├── AddRecordInput.tsx │ │ ├── AddRecordTextArea.tsx │ │ └── AddRecordTitle.tsx │ ├── Collect │ │ ├── Collect.tsx │ │ ├── CollectRanking.tsx │ │ ├── PeriodModal.tsx │ │ ├── RecentRecord.tsx │ │ └── Timer.tsx │ ├── DetailRecord │ │ ├── DetailRecord.tsx │ │ ├── EditModal.tsx │ │ ├── ImageContainer.tsx │ │ ├── InputAddImage.tsx │ │ ├── InputSnackBar.tsx │ │ ├── InputTextarea.tsx │ │ ├── NestedReplyItem.tsx │ │ ├── NestedReplyList.tsx │ │ ├── ReplyInput.tsx │ │ ├── ReplyItem.tsx │ │ ├── ReplyList.tsx │ │ ├── ShareModal.tsx │ │ ├── getChipIconName.ts │ │ └── getCreatedDate.ts │ ├── Login │ │ ├── GoogleButton.tsx │ │ ├── KakaoButton.tsx │ │ ├── Login.tsx │ │ └── [type].tsx │ ├── Main │ │ ├── Main.tsx │ │ ├── MixRecord.tsx │ │ ├── Ranking.tsx │ │ ├── RankingList.tsx │ │ ├── Together.tsx │ │ └── TogetherSlider.tsx │ ├── MyRecord │ │ ├── Calendar │ │ │ ├── Calendar.tsx │ │ │ ├── CalendarMonthYear.tsx │ │ │ ├── CalendarRecord.tsx │ │ │ ├── DateBox.tsx │ │ │ └── getCalendarDetail.ts │ │ ├── Common │ │ │ ├── MemoryRecordCard.tsx │ │ │ ├── MyRecordCard.tsx │ │ │ └── SearchInput.tsx │ │ ├── MemoryRecord.tsx │ │ ├── MyRecord.tsx │ │ ├── Search │ │ │ └── SearchRecord.tsx │ │ └── TodayRecord.tsx │ ├── NotFound │ │ └── NotFound.tsx │ ├── Setting │ │ ├── FeedbackMail │ │ │ └── FeedbackMail.tsx │ │ ├── ManageComment │ │ │ ├── CommentSection.tsx │ │ │ └── ManageComment.tsx │ │ ├── ModifyInfo │ │ │ └── ModifyInfo.tsx │ │ ├── Setting.tsx │ │ ├── SettingSection.tsx │ │ ├── TeamIntroduction │ │ │ └── TeamIntroduction.tsx │ │ └── Withdraw │ │ │ ├── CheckedNicknameBeforeWithDraw.tsx │ │ │ ├── CompletedWithdraw.tsx │ │ │ └── Withdraw.tsx │ └── SignUp │ │ ├── SignUp.tsx │ │ └── WithdrawSignUp.tsx ├── react-app-env.d.ts ├── react-query │ ├── hooks │ │ ├── useAuth.ts │ │ ├── useGetCategory.ts │ │ ├── useGetReply.ts │ │ ├── useMemoryRecord.ts │ │ ├── useMyRecordByDate.ts │ │ ├── useMyRecordByKeyword.ts │ │ ├── useMyRecordByMonthYear.ts │ │ ├── usePreviousUrlWithStorage.ts │ │ ├── useRecentRecord.ts │ │ └── useUser.ts │ └── queryKeys.ts ├── routes │ ├── protectedRoute.tsx │ └── router.tsx ├── store │ ├── atom.ts │ ├── collectPageAtom.ts │ ├── detailPageAtom.ts │ ├── mainPageAtom.ts │ └── myRecordAtom.ts ├── types │ ├── auth.ts │ ├── category.ts │ ├── myRecord.ts │ ├── recordData.ts │ ├── replyData.ts │ ├── request.ts │ └── response.ts └── utils │ ├── fileSize.ts │ ├── getCurrentTime.ts │ ├── getFormattedDate.ts │ ├── getIsValidateNickname.ts │ ├── getTimeGap.ts │ ├── localStorage.ts │ └── sessionStorage.ts ├── tailwind.config.js ├── tsconfig.extend.json ├── tsconfig.json └── webpack.config.js /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | node: true, 4 | browser: true, 5 | es2021: true, 6 | }, 7 | extends: [ 8 | 'prettier', 9 | 'prettier/prettier', 10 | 'plugin:prettier/recommended', 11 | 'eslint:recommended', 12 | 'plugin:react/recommended', 13 | 'plugin:@typescript-eslint/recommended', 14 | 'plugin:tailwindcss/recommended', 15 | ], 16 | parser: '@typescript-eslint/parser', 17 | parserOptions: { 18 | ecmaFeatures: { 19 | jsx: true, 20 | }, 21 | ecmaVersion: 'latest', 22 | }, 23 | plugins: ['react', '@typescript-eslint', 'tailwindcss'], 24 | rules: { 25 | indent: 'off', 26 | '@typescript-eslint/no-var-requires': 0, 27 | 'react/self-closing-comp': 'warn', // 셀프 클로징 태그 가능하면 적용 28 | 'no-console': 'warn', 29 | }, 30 | } 31 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @Seongtaek-H @endmoseung @sukyeongh 2 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## 작업 내용 2 | 3 | ## 참고 이미지(선택) 4 | 5 | ## 어떤 점을 리뷰 받고 싶으신가요? -------------------------------------------------------------------------------- /.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 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /.husky/prepare-commit-msg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx jira-prepare-commit-msg $1 5 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 2, 4 | "semi": false, 5 | "singleQuote": true 6 | } 7 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.defaultFormatter": "esbenp.prettier-vscode", 3 | "editor.formatOnSave": true, 4 | "eslint.alwaysShowStatus": true, 5 | "editor.codeActionsOnSave": { 6 | "source.fixAll.eslint": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### 🎁 서비스 소개 2 | 3 | ![image](https://user-images.githubusercontent.com/64088250/212938313-789d2c14-99fa-460d-86af-34858c5d3f0f.png) 4 |
5 |
6 | ![image](https://user-images.githubusercontent.com/64088250/212941454-e4241096-8d67-42e5-8703-acebb5257895.png) 7 | 8 |
9 | 10 | ### 💡레코딧의 KEY Point 11 | 12 | ``` 13 | 1️⃣ 내 특별한 일상을 모두와 나눌 수 있어요. 14 | ``` 15 | 16 | ``` 17 | 2️⃣ 나의 모든 일상들을 나만의 공간에 기록하고 오래 보관하고 싶어요. 18 | ``` 19 | 20 | ``` 21 | 3️⃣ 익명으로 모르는 사람들에게도 축하 또는 위로를 받아요. 22 | ``` 23 | 24 |
25 | 26 | ### ✨누가 필요해요? 27 | 28 | ![image](https://user-images.githubusercontent.com/64088250/212941594-3c774cfb-dbfd-4e90-be46-e696d34fff4a.png) 29 | 30 |
31 | 32 | ### ❓레코딧은요! 33 | 34 | ![image](https://user-images.githubusercontent.com/64088250/213171662-58687464-3607-4917-bad1-7c2ff0dfd402.png) 35 | 36 |
37 | 38 | ![image](https://user-images.githubusercontent.com/64088250/213171746-df754669-e41c-44f4-95c0-8beb6498d4aa.png) 39 | 40 |
41 | 42 | ![image](https://user-images.githubusercontent.com/64088250/213171819-8b5e80cf-4d57-4d0b-ba5b-71ff8cfbebab.png) 43 | 44 |
45 | 46 | ![image](https://user-images.githubusercontent.com/64088250/213171846-9f5b0914-2b70-4c58-b8bb-a03c1a9b9b75.png) 47 | 48 |
49 | 50 | ### 프론트엔드 팀원 51 | 52 | 53 | 54 | 59 | 64 | 69 | 70 | 71 | 72 | 73 | 74 | 75 |
55 | 56 | 57 | 58 | 60 | 61 | 62 | 63 | 65 | 66 | 67 | 68 |
김승모황성택황수경
76 | 77 | ### 백엔드 78 | 79 | [Go Repository](https://github.com/ItRecode/recordit-server) 80 | 81 | ### 기술 스택 82 | 83 | 84 | 85 | 86 |
87 | 88 | ### 🏠 텐져스 문화 89 | 90 | #### 1. 호칭은 닉네임으로 91 | 92 | - 모든 호칭은 닉네임으로 하며 ‘님’자는 생략해요. 93 | 94 | #### 2. 이해하기 쉬운 단어 선택 95 | 96 | - 비개발직군도 이해할 수 있게 쉬운 단어로 소통해요. 97 | 98 | #### 3. 정리하고 공유하는 습관 99 | 100 | - 담당자가 부재 시 다른 팀원들에게 쉬운 handoff를 위해 하루가 끝나기 전에 공유해요. 101 | - 모르는 게 있다면 팀원들에게 자유롭게 질문해요. 102 | - 습득한 지식이나 트러블 슈팅 내용을 기술 블로그에 공유해요. 103 | 104 | #### 4. 개발은 혼자 하지 않아요 105 | 106 | - PR은 꼭 1명 이상 approve를 받고 merge 해요. 107 | - 코드 리뷰를 할 때는 항상 상대방의 기분을 먼저 생각하면서 자신의 의견을 제시해요. 108 | 109 | #### 5. 나의 테스크가 아니라고 외면하지 않기 110 | 111 | - 다른 팀원의 테스크라도 팀원이 어려워한다면 먼저 적극적으로 소통하려 하고, 협력해서 문제를 해결해요. 112 | 113 | #### 6. 팀워크는 생명 114 | 115 | - 회식, 온라인 모각코, 마니또, 마피아 게임 등 다양한 활동을 하며 서로를 알아가요. 116 | 117 | #### 7. 애자일한 팀 프로젝트 118 | 119 | - JIRA를 활용해 스프린트의 이슈와 일정을 정리해요. 120 | - 매일 오전 9시에 데일리 스크럼을 진행해요. 121 | 122 | #### 8. QA 진행 123 | 124 | - 매주 금요일 QA를 진행하여 더욱 퀄리티 있는 서비스를 만들어요. 125 | 126 | #### 9. 스프린트의 마지막은 KPT 회고로 127 | 128 | - 매주 월요일 팀원들이 모두 모여 솔직하게 아쉬운 점, 좋았던 점을 얘기하며 팀을 발전해 나가요. 129 | 130 | #### 10. 개인 일정 공유하기 131 | 132 | - 스프린트 시작 전 개인 일정을 미리 공유해요. 133 | -------------------------------------------------------------------------------- /craco.config.js: -------------------------------------------------------------------------------- 1 | const CracoAlias = require('craco-alias') 2 | 3 | module.exports = { 4 | plugins: [ 5 | { 6 | plugin: CracoAlias, 7 | options: { 8 | source: 'tsconfig', 9 | baseUrl: './src', 10 | tsConfigPath: 'tsconfig.extend.json', 11 | }, 12 | }, 13 | ], 14 | } 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "recodeit-client", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@craco/craco": "^7.0.0", 7 | "@emailjs/browser": "^3.10.0", 8 | "@tanstack/react-query": "^4.20.4", 9 | "@tanstack/react-query-devtools": "^4.20.4", 10 | "@testing-library/jest-dom": "^5.16.5", 11 | "@testing-library/react": "^13.4.0", 12 | "@testing-library/user-event": "^13.5.0", 13 | "@types/jest": "^27.5.2", 14 | "@types/node": "^16.18.10", 15 | "@types/react": "^18.0.26", 16 | "@types/react-dom": "^18.0.9", 17 | "axios": "^1.2.1", 18 | "history": "^5.3.0", 19 | "react": "^18.2.0", 20 | "react-dom": "^18.2.0", 21 | "react-router-dom": "^6.5.0", 22 | "react-scripts": "5.0.1", 23 | "react-slick": "^0.29.0", 24 | "recoil": "^0.7.6", 25 | "slick-carousel": "^1.8.1", 26 | "typescript": "^4.9.4", 27 | "web-vitals": "^2.1.4" 28 | }, 29 | "scripts": { 30 | "start": "craco start", 31 | "build": "craco build", 32 | "test": "craco test", 33 | "eject": "react-scripts eject" 34 | }, 35 | "browserslist": { 36 | "production": [ 37 | ">0.2%", 38 | "not dead", 39 | "not op_mini all" 40 | ], 41 | "development": [ 42 | "last 1 chrome version", 43 | "last 1 firefox version", 44 | "last 1 safari version" 45 | ] 46 | }, 47 | "devDependencies": { 48 | "@types/react-slick": "^0.23.10", 49 | "@types/recoil": "^0.0.9", 50 | "@typescript-eslint/eslint-plugin": "^5.46.1", 51 | "@typescript-eslint/parser": "^5.46.1", 52 | "autoprefixer": "^10.4.13", 53 | "craco-alias": "^3.0.1", 54 | "eslint": "^8.30.0", 55 | "eslint-config-prettier": "^8.5.0", 56 | "eslint-plugin-prettier": "^4.2.1", 57 | "eslint-plugin-react": "^7.31.11", 58 | "eslint-plugin-tailwindcss": "^3.7.1", 59 | "husky": "^8.0.2", 60 | "jira-prepare-commit-msg": "^1.7.1", 61 | "postcss": "^8.4.20", 62 | "postcss-loader": "^7.0.2", 63 | "prettier": "^2.8.1", 64 | "prettier-plugin-tailwindcss": "^0.2.1", 65 | "tailwindcss": "^3.2.4" 66 | }, 67 | "jira-prepare-commit-msg": { 68 | "messagePattern": "[$J] $M", 69 | "jiraTicketPattern": "([A-Z]+-\\d+)", 70 | "commentChar": "#", 71 | "isConventionalCommit": false, 72 | "allowEmptyCommitMessage": false, 73 | "gitRoot": "" 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/_redirects: -------------------------------------------------------------------------------- 1 | /* /index.html 200 -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItRecode/recodeIt-client/434456a5f668211aa2348992eac0550a473be636/public/favicon.ico -------------------------------------------------------------------------------- /public/favicon1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItRecode/recodeIt-client/434456a5f668211aa2348992eac0550a473be636/public/favicon1.png -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 13 | 17 | 21 | 25 | RecordIt 26 | 31 | 35 | 44 | 45 | 46 | 47 |
48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import { QueryClient, QueryClientProvider } from '@tanstack/react-query' 2 | import { ReactQueryDevtools } from '@tanstack/react-query-devtools' 3 | import { RecoilRoot } from 'recoil' 4 | 5 | import React from 'react' 6 | import Layout from '@components/Layout' 7 | import { RouterProvider } from 'react-router-dom' 8 | import router from '@routes/router' 9 | 10 | const queryClient = new QueryClient() 11 | 12 | function App() { 13 | return ( 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | ) 23 | } 24 | 25 | export default App 26 | -------------------------------------------------------------------------------- /src/apis/auth.ts: -------------------------------------------------------------------------------- 1 | import { IAuth, ISignUp } from 'types/auth' 2 | import { baseInstance } from './instance' 3 | 4 | export const login = ({ type, token }: IAuth) => { 5 | return baseInstance.post(`/member/oauth/login/${type}`, { 6 | oauthToken: token, 7 | }) 8 | } 9 | 10 | export const signUp = ({ type, tempId, nickname }: ISignUp) => { 11 | return baseInstance.post(`/member/oauth/register/${type}`, { 12 | nickname: nickname, 13 | registerSession: tempId, 14 | }) 15 | } 16 | 17 | export const getIsDuplicatedNickname = (nickname: string) => { 18 | return baseInstance.get(`/member/nickname`, { 19 | params: { nickname }, 20 | }) 21 | } 22 | 23 | export const logout = () => { 24 | return baseInstance.post(`/member/logout`) 25 | } 26 | 27 | export const withdrawUser = () => { 28 | return baseInstance.delete(`/member/delete`) 29 | } 30 | -------------------------------------------------------------------------------- /src/apis/instance.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | 3 | const { REACT_APP_DEV_API_END_POINT } = process.env 4 | 5 | const baseInstance = axios.create({ 6 | baseURL: REACT_APP_DEV_API_END_POINT, 7 | withCredentials: true, 8 | timeout: 5000, 9 | }) 10 | 11 | export { baseInstance } 12 | -------------------------------------------------------------------------------- /src/apis/myRecord.ts: -------------------------------------------------------------------------------- 1 | import { AxiosResponse } from 'axios' 2 | import { baseInstance } from './instance' 3 | import { 4 | IMemoryRecordList, 5 | IMyRecord, 6 | IMyRecordByKeywordList, 7 | IRecordWithMonthYear, 8 | } from 'types/myRecord' 9 | 10 | const MEMORY_RECORD_SIZE = 7 11 | const MEMORY_COMMENT_SIZE = 5 12 | const MY_RECORD_KEYWORD_SIZE = 10 13 | 14 | export const getMemoryRecord = ( 15 | pageParam: number, 16 | date: string 17 | ): Promise> => { 18 | return baseInstance.get(`/record/memory`, { 19 | params: { 20 | date, 21 | memoryRecordPage: pageParam, 22 | memoryRecordSize: MEMORY_RECORD_SIZE, 23 | sizeOfCommentPerRecord: MEMORY_COMMENT_SIZE, 24 | }, 25 | }) 26 | } 27 | 28 | export const getRecordOnToday = (): Promise> => { 29 | return baseInstance.get(`/record/today`) 30 | } 31 | 32 | export const getRecordByKeyword = ( 33 | pageParam: number, 34 | keyword: string 35 | ): Promise> => { 36 | return baseInstance.get(`/record/search`, { 37 | params: { 38 | searchKeyword: keyword, 39 | page: pageParam, 40 | size: MY_RECORD_KEYWORD_SIZE, 41 | }, 42 | }) 43 | } 44 | 45 | export const getRecordByMonthYear = ( 46 | yearMonth: string 47 | ): Promise> => { 48 | return baseInstance.get(`/record/days`, { 49 | params: { 50 | yearMonth, 51 | }, 52 | }) 53 | } 54 | -------------------------------------------------------------------------------- /src/apis/record.ts: -------------------------------------------------------------------------------- 1 | import { parentCategoryID } from './../types/category' 2 | import { baseInstance } from './instance' 3 | 4 | export const getCategory = async (categoryId?: parentCategoryID) => { 5 | return await baseInstance.get('/record/category', { 6 | params: { parentRecordCategoryId: categoryId }, 7 | }) 8 | } 9 | 10 | export const enrollRecord = async (data: FormData) => { 11 | return baseInstance.post('/record', data, { 12 | headers: { 'Content-Type': 'multipart/form-data' }, 13 | }) 14 | } 15 | 16 | export const getRecord = async (recordId: string | undefined) => { 17 | if (recordId) { 18 | const res = await baseInstance.get(`/record/${recordId}`) 19 | return res.data 20 | } 21 | } 22 | 23 | export const deleteRecord = async (recordId: string | undefined) => { 24 | if (recordId) { 25 | const res = await baseInstance.delete(`/record/${recordId}`) 26 | return res.data 27 | } 28 | } 29 | 30 | export const modifyRecord = async ( 31 | recordId: string | undefined, 32 | data: FormData 33 | ) => { 34 | return baseInstance.put(`/record/${recordId}`, data, { 35 | headers: { 'Content-Type': 'multipart/form-data' }, 36 | }) 37 | } 38 | 39 | export const getRandomRecordData = async (recordCategoryId: 1 | 2) => { 40 | return await baseInstance.get('/record/random', { 41 | params: { recordCategoryId, size: 5 }, 42 | }) 43 | } 44 | 45 | export const getMixRecordData = async () => { 46 | return await baseInstance.get('/record/mix') 47 | } 48 | 49 | export const getRecentRecordData = async (page: number, dateTime: string) => { 50 | const MAX_RECORD_NUMBER = 10 51 | return await baseInstance.get('/record/recent', { 52 | params: { page, size: MAX_RECORD_NUMBER, dateTime }, 53 | }) 54 | } 55 | 56 | export const getRanking = async ( 57 | recordCategoryId: number, 58 | rankingPeriod = 'WEEK' 59 | ) => { 60 | return await baseInstance.get('/record/ranking', { 61 | params: { 62 | rankingPeriod, 63 | recordCategoryId, 64 | }, 65 | }) 66 | } 67 | 68 | export const getTotalRecordCount = async () => { 69 | return await baseInstance.get('/record/count') 70 | } 71 | -------------------------------------------------------------------------------- /src/apis/reply.ts: -------------------------------------------------------------------------------- 1 | import { baseInstance } from './instance' 2 | 3 | export const createReply = async (data: FormData) => { 4 | return await baseInstance.post('/comment', data, { 5 | headers: { 'Content-Type': 'multipart/form-data' }, 6 | }) 7 | } 8 | 9 | export const getReply = async ( 10 | recordId?: T, 11 | pageParam?: number, 12 | parentId?: number, 13 | size?: number 14 | ) => { 15 | return await baseInstance.get('/comment', { 16 | params: { 17 | page: pageParam ? pageParam : 0, 18 | parentId: parentId ? parentId : '', 19 | recordId: recordId, 20 | size: size ? size : 10, 21 | }, 22 | }) 23 | } 24 | 25 | export const deleteReply = async ( 26 | commentId: number, 27 | recordId: string | undefined 28 | ) => { 29 | return await baseInstance.delete(`/comment/${commentId}?recordId=${recordId}`) 30 | } 31 | 32 | export const updateComment = async ({ 33 | data, 34 | commentId, 35 | }: { 36 | data: FormData 37 | commentId: number 38 | }) => { 39 | return await baseInstance.put(`/comment/${commentId}`, data, { 40 | headers: { 'Content-Type': 'multipart/form-data' }, 41 | }) 42 | } 43 | 44 | export const getMyReply = async (pageParam?: number, size?: number) => { 45 | return await baseInstance.get('/comment/my', { 46 | params: { 47 | page: pageParam ? pageParam : 0, 48 | size: size ? size : 10, 49 | }, 50 | }) 51 | } 52 | -------------------------------------------------------------------------------- /src/apis/share.ts: -------------------------------------------------------------------------------- 1 | interface IShareDataType { 2 | recordId: number 3 | title: string 4 | description: string 5 | imageUrl?: string 6 | } 7 | export const ShareKakao = ({ 8 | recordId, 9 | title, 10 | description, 11 | imageUrl, 12 | }: IShareDataType) => { 13 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 14 | const { Kakao }: any = window 15 | if (!Kakao.isInitialized()) { 16 | Kakao.init(process.env.REACT_APP_KAKAO_JAVASCRIPT_KEY) 17 | } 18 | 19 | Kakao.Share.sendDefault({ 20 | objectType: 'feed', 21 | content: { 22 | title: title, 23 | description: description.replaceAll(/(
||
)/g, '\r\n'), 24 | imageUrl: imageUrl 25 | ? imageUrl 26 | : 'https://record-it.s3.ap-northeast-2.amazonaws.com/imagefile-dev/sharing+png', 27 | 28 | link: { 29 | webUrl: process.env.REACT_APP_WEB_URL, 30 | mobileWebUrl: process.env.REACT_APP_WEB_URL, 31 | }, 32 | }, 33 | 34 | buttons: [ 35 | { 36 | title: '레코드 보러가기', 37 | link: { 38 | mobileWebUrl: `${process.env.REACT_APP_WEB_URL}/record/${recordId}`, 39 | webUrl: `${process.env.REACT_APP_WEB_URL}/record/${recordId}`, 40 | }, 41 | }, 42 | ], 43 | }) 44 | } 45 | 46 | export const copyLink = async (recordId: number) => { 47 | try { 48 | await navigator.clipboard.writeText( 49 | `${window.location.origin}/record/${recordId}` 50 | ) 51 | alert('링크를 복사했습니다.') 52 | } catch (error) { 53 | alert('링크 복사에 실패했어요.') 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/apis/user.ts: -------------------------------------------------------------------------------- 1 | import { baseInstance } from './instance' 2 | 3 | export const getUserInfo = () => { 4 | return baseInstance.get(`/member/auth`) 5 | } 6 | 7 | export const updateUserInfo = (nickname: string) => { 8 | return baseInstance.put(`/member`, { nickName: nickname }) 9 | } 10 | -------------------------------------------------------------------------------- /src/assets/6x12RightArrow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/Expand_right.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/ImageContainer/left_Arrow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/assets/ImageContainer/right_Arrow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/assets/back.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/camera.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/check.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/chip_icon/index.ts: -------------------------------------------------------------------------------- 1 | import Celebrate from '@assets/chip_icon/celebrate.svg' 2 | import Happy from '@assets/chip_icon/happy.svg' 3 | import Cake from '@assets/chip_icon/cake.svg' 4 | import Love from '@assets/chip_icon/love.svg' 5 | import Consolate from '@assets/chip_icon/consolate.svg' 6 | import Sympathy from '@assets/chip_icon/sympathy.svg' 7 | import MySide from '@assets/chip_icon/mySide.svg' 8 | import Depress from '@assets/chip_icon/depress.svg' 9 | 10 | export { Celebrate, Happy, Cake, Love, Consolate, Sympathy, MySide, Depress } 11 | -------------------------------------------------------------------------------- /src/assets/close_icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/collect_page_icon/collapse.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/assets/collect_page_icon/reset.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/assets/collect_page_icon/reset_disabled.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/assets/collect_page_icon/scrollTop.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/assets/constant/RecordColors.ts: -------------------------------------------------------------------------------- 1 | export type colorSourceType = { 2 | src: string 3 | choosed: boolean 4 | id: number 5 | } 6 | 7 | export const ADD_RECORD_COLORS = [ 8 | { src: 'bg-icon-purple', choosed: true, id: 0 }, 9 | { src: 'bg-icon-yellow', choosed: false, id: 1 }, 10 | { src: 'bg-icon-pink', choosed: false, id: 2 }, 11 | { src: 'bg-icon-blue', choosed: false, id: 3 }, 12 | { src: 'bg-icon-green', choosed: false, id: 4 }, 13 | ] 14 | -------------------------------------------------------------------------------- /src/assets/constant/RecordIcons.ts: -------------------------------------------------------------------------------- 1 | import heart from '@assets/record_icons/heart.svg' 2 | import gift from '@assets/record_icons/gift.svg' 3 | import music from '@assets/record_icons/music.svg' 4 | import rocket from '@assets/record_icons/rocket.svg' 5 | import like from '@assets/record_icons/like.svg' 6 | import crown from '@assets/record_icons/crown.svg' 7 | import medal from '@assets/record_icons/medal.svg' 8 | import moon from '@assets/record_icons/moon.svg' 9 | import speechbubble from '@assets/record_icons/speechbubble.svg' 10 | import wine from '@assets/record_icons/wine.svg' 11 | import umbrella from '@assets/record_icons/umbrella.svg' 12 | import trashcan from '@assets/record_icons/trashcan.svg' 13 | import lock from '@assets/record_icons/lock.svg' 14 | 15 | export const ADD_RECORD_ICONS = Object.freeze({ 16 | celebration: [ 17 | { src: gift, id: 0 }, 18 | { src: heart, id: 1 }, 19 | { src: music, id: 2 }, 20 | { src: rocket, id: 3 }, 21 | { src: like, id: 4 }, 22 | { src: crown, id: 5 }, 23 | { src: medal, id: 6 }, 24 | ], 25 | consolation: [ 26 | { src: moon, id: 0 }, 27 | { src: speechbubble, id: 1 }, 28 | { src: wine, id: 2 }, 29 | { src: umbrella, id: 3 }, 30 | { src: trashcan, id: 4 }, 31 | { src: lock, id: 5 }, 32 | ], 33 | }) 34 | -------------------------------------------------------------------------------- /src/assets/constant/collect.ts: -------------------------------------------------------------------------------- 1 | export const RESET_TIME = 180 2 | -------------------------------------------------------------------------------- /src/assets/constant/constant.ts: -------------------------------------------------------------------------------- 1 | export const INPUT_DETAILS = Object.freeze({ 2 | MAX_INPUT_TYPING: 12, 3 | MAX_TEXTAREA_TYPING: 200, 4 | MIN_TYPING: 0, 5 | }) 6 | 7 | export const RECORD_TITLE_MAX_LENGTH = 12 8 | 9 | export const TEXT_DETAILS = Object.freeze({ 10 | CELEBRATION: 'celebration', 11 | CONSOLATION: 'consolation', 12 | }) 13 | 14 | export const CELEBRATION_ID = 1 15 | export const CONSOLATION_ID = 2 16 | 17 | export const INITIAL_RECORD_DATA = { 18 | recordId: 0, 19 | categoryId: 0, 20 | categoryName: '', 21 | title: '', 22 | content: '', 23 | writer: '', 24 | colorName: '', 25 | iconName: '', 26 | createdAt: '', 27 | imageUrls: [], 28 | } 29 | 30 | export const UNAUTHORIZED_CODE = 401 31 | 32 | export const RECORD_DETAIL_INITIAL_INPUT_HEIGHT = 89 33 | export const RECORD_DETAIL_INPUT_IMAGE_HEIGHT = 74 34 | export const RECORD_DETAIL_INPUT_HEIGHT_WITHOUT_TEXTAREA = 64 35 | export const RECORD_DETAIL_INPUT_TEXTAREAT_INITIAL_HEIGHT = 25 36 | 37 | export const INPUT_MODE = Object.freeze({ 38 | REPLY: 'reply', 39 | NESTEDREPLY: 'nestedReply', 40 | }) 41 | 42 | export const NICKNAME_MIN_LENGTH = 2 43 | export const NICKNAME_MAX_LENGTH = 8 44 | -------------------------------------------------------------------------------- /src/assets/constant/others.ts: -------------------------------------------------------------------------------- 1 | export const PREVIOUS_URL = 'previousUrl' 2 | -------------------------------------------------------------------------------- /src/assets/constant/ranking.ts: -------------------------------------------------------------------------------- 1 | export type keyOfRankingPeriod = 'TOTAL' | 'DAY' | 'WEEK' | 'MONTH' 2 | 3 | export type rankingPeriodType = { 4 | [key in keyOfRankingPeriod]: '하루' | '일주일' | '한 달' | '누적' 5 | } 6 | export const RANKINGPERIOD: rankingPeriodType = { 7 | DAY: '하루', 8 | WEEK: '일주일', 9 | MONTH: '한 달', 10 | TOTAL: '누적', 11 | } 12 | -------------------------------------------------------------------------------- /src/assets/deleteIcon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/assets/detail_page_icon/Close.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/detail_page_icon/arrow_down.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/detail_page_icon/arrow_up.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/front.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/front_white.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/google.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/assets/icon_closed.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/assets/kakao.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/more.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/assets/myRecordIcon/arrow_down.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/myRecordIcon/arrow_up.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/myRecordIcon/calendar.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/myRecordIcon/close.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/myRecordIcon/comment_plus.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/assets/myRecordIcon/search.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/nav_icons/collect_icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/nav_icons/home_icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/nav_icons/myrecord_icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/nav_icons/record_icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/assets/nav_icons/setting_icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/pin.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/plus.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/assets/ranking_btn_arrow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/ranking_down_arrow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/record_icons/index.ts: -------------------------------------------------------------------------------- 1 | import { FunctionComponent } from 'react' 2 | import { ReactComponent as crown } from './crown.svg' 3 | import { ReactComponent as gift } from './gift.svg' 4 | import { ReactComponent as heart } from './heart.svg' 5 | import { ReactComponent as like } from './like.svg' 6 | import { ReactComponent as lock } from './lock.svg' 7 | import { ReactComponent as medal } from './medal.svg' 8 | import { ReactComponent as moon } from './moon.svg' 9 | import { ReactComponent as music } from './music.svg' 10 | import { ReactComponent as rocket } from './rocket.svg' 11 | import { ReactComponent as speechbubble } from './speechbubble.svg' 12 | import { ReactComponent as trashcan } from './trashcan.svg' 13 | import { ReactComponent as umbrella } from './umbrella.svg' 14 | import { ReactComponent as wine } from './wine.svg' 15 | 16 | type iconType = { 17 | [index: string]: FunctionComponent> 18 | } 19 | 20 | const icons: iconType = { 21 | crown, 22 | gift, 23 | heart, 24 | like, 25 | lock, 26 | medal, 27 | moon, 28 | music, 29 | rocket, 30 | speechbubble, 31 | trashcan, 32 | umbrella, 33 | wine, 34 | } 35 | 36 | export default icons 37 | -------------------------------------------------------------------------------- /src/assets/right_purple_arrow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/settings_icon/check_box.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/assets/settings_icon/reply_check.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/assets/spinner.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/components/Alert.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode } from 'react' 2 | import Modal from './Modal' 3 | interface IAlertProps { 4 | visible: boolean 5 | mainMessage: ReactNode 6 | subMessage?: ReactNode 7 | cancelMessage?: string 8 | confirmMessage: string 9 | onClose: () => void 10 | onCancel?: () => void 11 | onConfirm: () => void 12 | danger?: boolean 13 | } 14 | 15 | export default function Alert({ 16 | visible, 17 | mainMessage, 18 | subMessage, 19 | cancelMessage, 20 | confirmMessage, 21 | onClose, 22 | onCancel, 23 | onConfirm, 24 | danger, 25 | }: IAlertProps) { 26 | const buttonClassName = 27 | 'h-full w-1/2 cursor-pointer bg-transparent py-4 text-base font-semibold' 28 | 29 | return ( 30 | 31 |
32 |
33 |
34 | {mainMessage} 35 |
36 | {subMessage && ( 37 |
38 | {subMessage} 39 |
40 | )} 41 |
42 |
43 | {onCancel && ( 44 | 51 | )} 52 | 53 | 62 |
63 |
64 |
65 | ) 66 | } 67 | -------------------------------------------------------------------------------- /src/components/BackButton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { ReactComponent as Back } from '@assets/back.svg' 3 | import { useNavigate } from 'react-router-dom' 4 | 5 | const BackButton = React.memo(function BackBtn({ 6 | onClick, 7 | }: { 8 | onClick?: () => void 9 | }) { 10 | const navigate = useNavigate() 11 | 12 | const handleLocateBack = () => { 13 | if (navigate(-1) === undefined) { 14 | navigate('/') 15 | } else { 16 | navigate(-1) 17 | 18 | if (onClick) { 19 | return onClick() 20 | } 21 | } 22 | } 23 | 24 | return 25 | }) 26 | 27 | export default BackButton 28 | -------------------------------------------------------------------------------- /src/components/Button.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Spinner from './Spinner' 3 | 4 | interface ButtonPropsType 5 | extends React.ButtonHTMLAttributes { 6 | property?: 'solid' | 'primary' | 'default' | 'danger' 7 | small?: boolean 8 | active?: boolean 9 | normal?: boolean 10 | disabled?: boolean 11 | loading?: boolean 12 | type?: 'button' | 'submit' | 'reset' | undefined 13 | children?: React.ReactElement | string 14 | } 15 | 16 | export default function Button({ 17 | property = 'default', 18 | small = false, 19 | active = true, 20 | normal = false, 21 | disabled = false, 22 | loading = false, 23 | type = 'button', 24 | children, 25 | ...props 26 | }: ButtonPropsType) { 27 | const setClassNameByProperty = (property: string) => { 28 | switch (property) { 29 | case 'solid': 30 | return active 31 | ? 'bg-primary-2 text-grey-1 hover:bg-primary-1' 32 | : 'bg-inactive text-grey-1 ' 33 | case 'primary': 34 | return active 35 | ? 'bg-primary-10 text-primary-2 hover:bg-primary-8 hover:text-primary-1' 36 | : 'bg-primary-10 text-primary-8' 37 | case 'default': 38 | return active 39 | ? 'border border-solid border-primary-3 bg-grey-1 text-primary-3 hover:border-primary-1 hover:text-primary-1' 40 | : 'border border-solid border-inactive text-inactive bg-grey-1' 41 | case 'danger': 42 | return active 43 | ? `border border-solid bg-grey-1 ${ 44 | normal 45 | ? 'border-grey-6 text-grey-6 hover:border-grey-7' 46 | : 'border-danger text-danger hover:border-danger' 47 | }` 48 | : 'border border-solid border-inactive text-inactive' 49 | default: 50 | return '' 51 | } 52 | } 53 | 54 | return ( 55 | 66 | ) 67 | } 68 | -------------------------------------------------------------------------------- /src/components/Category.tsx: -------------------------------------------------------------------------------- 1 | import { useGetCategory } from '@react-query/hooks/useGetCategory' 2 | import React, { Dispatch, SetStateAction, useEffect, useRef } from 'react' 3 | import { parentCategoryID } from 'types/category' 4 | import { getChipIconName } from '@pages/DetailRecord/getChipIconName' 5 | import Chip from './Chip' 6 | import useSwipe from '@hooks/useSwipe' 7 | import { CELEBRATION_ID, CONSOLATION_ID } from '@assets/constant/constant' 8 | import { useRecoilValue } from 'recoil' 9 | import { checkFromDetailPage } from '@store/detailPageAtom' 10 | 11 | export default function Category({ 12 | slider, 13 | parentCategoryId, 14 | choosedCategoryId, 15 | setChoosedCategoryId, 16 | isModify = false, 17 | }: { 18 | slider: boolean 19 | parentCategoryId: parentCategoryID 20 | choosedCategoryId: number 21 | setChoosedCategoryId: Dispatch> 22 | isModify?: boolean 23 | }) { 24 | const { categoryData } = useGetCategory(parentCategoryId) 25 | 26 | const dragRef = useRef( 27 | null 28 | ) as React.MutableRefObject 29 | const { handleMouseDown, isDragging, setIsDragging } = useSwipe(dragRef) 30 | 31 | const isFromDetailPage = useRecoilValue(checkFromDetailPage) 32 | 33 | useEffect(() => { 34 | if (slider) { 35 | if ( 36 | choosedCategoryId !== CELEBRATION_ID && 37 | choosedCategoryId !== CONSOLATION_ID && 38 | !isFromDetailPage 39 | ) { 40 | if (parentCategoryId === CELEBRATION_ID) 41 | setChoosedCategoryId(CELEBRATION_ID) 42 | if (parentCategoryId === CONSOLATION_ID) 43 | setChoosedCategoryId(CONSOLATION_ID) 44 | } 45 | dragRef.current.scrollLeft = 0 46 | } 47 | if (!slider) { 48 | if (parentCategoryId === CELEBRATION_ID) setChoosedCategoryId(3) 49 | if (parentCategoryId === CONSOLATION_ID) setChoosedCategoryId(7) 50 | } 51 | }, [parentCategoryId]) 52 | 53 | const handleClickChip = (id?: number) => { 54 | if (slider && isDragging) { 55 | setIsDragging(false) 56 | return 57 | } 58 | if (id !== undefined) { 59 | setChoosedCategoryId(id) 60 | } else { 61 | setChoosedCategoryId(parentCategoryId) 62 | } 63 | } 64 | 65 | return ( 66 |
{ 72 | handleMouseDown(e) 73 | }} 74 | > 75 | {slider && ( 76 | handleClickChip()} 85 | /> 86 | )} 87 | {categoryData && 88 | categoryData.map((item) => ( 89 | handleClickChip(item.id)} 96 | isModify={isModify} 97 | /> 98 | ))} 99 |
100 | ) 101 | } 102 | -------------------------------------------------------------------------------- /src/components/Chip.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef } from 'react' 2 | 3 | interface chipProps extends React.ButtonHTMLAttributes { 4 | type?: 'button' | 'submit' | 'reset' | undefined 5 | active: boolean 6 | icon: string | null 7 | message: string 8 | pointer?: boolean 9 | property?: 'default' | 'small' 10 | isModify?: boolean 11 | } 12 | 13 | function Chip({ 14 | active, 15 | icon = null, 16 | message, 17 | type, 18 | pointer = true, 19 | property = 'default', 20 | isModify, 21 | ...props 22 | }: chipProps) { 23 | const scrollRef = useRef(null) 24 | 25 | useEffect(() => { 26 | if (active) { 27 | scrollRef.current?.scrollIntoView({ 28 | behavior: 'auto', 29 | block: 'nearest', 30 | inline: 'center', 31 | }) 32 | } 33 | }, []) 34 | 35 | return ( 36 | 61 | ) 62 | } 63 | 64 | export default Chip 65 | -------------------------------------------------------------------------------- /src/components/Input.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { ReactComponent as CloseIcon } from '@assets/icon_closed.svg' 3 | 4 | interface InputPropsType extends React.InputHTMLAttributes { 5 | property?: 'default' | 'success' | 'error' 6 | name: string 7 | label?: string 8 | placeholder?: string 9 | value?: number | string 10 | message?: string 11 | focus?: boolean 12 | autoFocus?: boolean 13 | maxLength?: number 14 | onRemove?: (isRemove: boolean) => void 15 | } 16 | 17 | export default function Input({ 18 | property = 'default', 19 | name, 20 | label, 21 | placeholder, 22 | value, 23 | message, 24 | autoFocus = true, 25 | maxLength, 26 | onRemove, 27 | ...props 28 | }: InputPropsType) { 29 | const setClassNameByProperty = (property: string) => { 30 | if (property === 'error') return 'border-b-sub-1' 31 | if (property === 'success') return 'border-b-primary-1' 32 | 33 | return 'border-b-grey-4' 34 | } 35 | 36 | const handleRemove = () => { 37 | if (onRemove) { 38 | onRemove(true) 39 | } 40 | } 41 | 42 | return ( 43 |
44 | {label &&

{label}

} 45 |
46 | 58 | 62 |
63 | {property !== 'default' && ( 64 |

69 | {message} 70 |

71 | )} 72 |
73 | ) 74 | } 75 | -------------------------------------------------------------------------------- /src/components/Layout.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default function Layout({ children }: { children: React.ReactNode }) { 4 | return ( 5 |
6 |
7 | {children} 8 |
9 |
10 | ) 11 | } 12 | -------------------------------------------------------------------------------- /src/components/Loading.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Spinner from './Spinner' 3 | 4 | const Loading = React.memo(function LoadingComponent() { 5 | return ( 6 |
7 | 8 |

로딩 중 이에요

9 |

잠시만 기다려 주세요

10 |
11 | ) 12 | }) 13 | 14 | export default Loading 15 | -------------------------------------------------------------------------------- /src/components/Modal.tsx: -------------------------------------------------------------------------------- 1 | import useClickOutside from '@hooks/useClickOutside' 2 | import React, { ReactNode, useEffect, useMemo } from 'react' 3 | import { createPortal } from 'react-dom' 4 | 5 | interface IModalProps { 6 | visible: boolean 7 | children: ReactNode 8 | onClose?: () => void 9 | } 10 | 11 | export default function Modal({ 12 | visible = false, 13 | children, 14 | onClose, 15 | ...props 16 | }: IModalProps) { 17 | const modalRef = useClickOutside(() => { 18 | onClose && onClose() 19 | }) 20 | 21 | const modalElement = useMemo(() => document.createElement('modal'), []) 22 | 23 | useEffect(() => { 24 | document.body.appendChild(modalElement) 25 | return () => { 26 | document.body.removeChild(modalElement) 27 | } 28 | }) 29 | 30 | return createPortal( 31 |
36 |
37 |
42 | {children} 43 |
44 |
, 45 | modalElement 46 | ) 47 | } 48 | -------------------------------------------------------------------------------- /src/components/MoreButton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { ReactComponent as More } from '@assets/more.svg' 3 | 4 | export default function MoreButton() { 5 | return MoreButton 6 | } 7 | -------------------------------------------------------------------------------- /src/components/Navbar.tsx: -------------------------------------------------------------------------------- 1 | import React, { Suspense } from 'react' 2 | import { ReactComponent as Record_icon } from '@assets/nav_icons/record_icon.svg' 3 | import { Outlet, useNavigate } from 'react-router-dom' 4 | import NavbarItem from './NavbarItem' 5 | import Loading from './Loading' 6 | 7 | function NavBar() { 8 | const navigate = useNavigate() 9 | 10 | return ( 11 | <> 12 | }> 13 | 14 | 15 | 33 | 34 | ) 35 | } 36 | 37 | export const MemoizedNavbar = React.memo(NavBar) 38 | -------------------------------------------------------------------------------- /src/components/NavbarItem.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Link, useLocation } from 'react-router-dom' 3 | import { ReactComponent as Home_icon } from '@assets/nav_icons/home_icon.svg' 4 | import { ReactComponent as Rank_icon } from '@assets/nav_icons/collect_icon.svg' 5 | import { ReactComponent as MyRecord_icon } from '@assets/nav_icons/myrecord_icon.svg' 6 | import { ReactComponent as Setting_icon } from '@assets/nav_icons/setting_icon.svg' 7 | 8 | interface NavbarItemPropsType { 9 | pageName: string 10 | linkSrc: string 11 | className?: string 12 | } 13 | 14 | export default function NavbarItem({ 15 | pageName, 16 | linkSrc, 17 | className, 18 | }: NavbarItemPropsType) { 19 | const containerFormat = 20 | 'group flex h-full w-[54px] flex-col items-center justify-items-center hover:cursor-pointer' 21 | const iconFormat = 'group-hover:fill-primary-2' 22 | const textFormat = 'text-xs group-hover:text-primary-2' 23 | 24 | const { pathname } = useLocation() 25 | 26 | const checkPathWithText = (linkSrc: string) => { 27 | if (pathname === `${linkSrc}`) { 28 | return 'text-primary-2' 29 | } 30 | return 'text-grey-3' 31 | } 32 | 33 | const checkPathWithIconImg = (linkSrc: string) => { 34 | if (pathname === linkSrc) { 35 | return 'fill-primary-2' 36 | } 37 | return 'fill-grey-3' 38 | } 39 | 40 | const navbarIcon = (linkSrc: string) => { 41 | switch (linkSrc) { 42 | case '/': 43 | return ( 44 | 48 | ) 49 | case '/collect': 50 | return ( 51 | 55 | ) 56 | case '/myrecord': 57 | return ( 58 | 62 | ) 63 | case '/setting': 64 | return ( 65 | 69 | ) 70 | } 71 | } 72 | 73 | return ( 74 | 75 | {navbarIcon(linkSrc)} 76 |

81 | {pageName} 82 |

83 | 84 | ) 85 | } 86 | -------------------------------------------------------------------------------- /src/components/ParrentCategoryTab.tsx: -------------------------------------------------------------------------------- 1 | import { CELEBRATION_ID, CONSOLATION_ID } from '@assets/constant/constant' 2 | import React, { Dispatch, SetStateAction } from 'react' 3 | import { parentCategoryID } from 'types/category' 4 | 5 | const MemoizedParentCategoryTab = React.memo(function ParentCategoryTab({ 6 | parentCategoryId, 7 | setParentCategoryId, 8 | isModify, 9 | }: { 10 | parentCategoryId: number 11 | setParentCategoryId: Dispatch> 12 | isModify?: boolean 13 | }) { 14 | return ( 15 | <> 16 |
17 | 38 | 59 |
60 |
61 | 62 | ) 63 | }) 64 | 65 | export default MemoizedParentCategoryTab 66 | -------------------------------------------------------------------------------- /src/components/RankingItem.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useNavigate } from 'react-router-dom' 3 | import { parentCategoryID } from 'types/category' 4 | import { IRankingRecordData } from 'types/recordData' 5 | import recordIcons from '@assets/record_icons' 6 | import { CELEBRATION_ID } from '@assets/constant/constant' 7 | import { ReactComponent as Arrow } from '@assets/ranking_btn_arrow.svg' 8 | 9 | interface RankingItemType extends IRankingRecordData { 10 | index: number 11 | parentCategoryId: parentCategoryID 12 | } 13 | 14 | export default function RankingItem({ 15 | index, 16 | parentCategoryId, 17 | recordId, 18 | colorName, 19 | title, 20 | writer, 21 | numOfComment, 22 | iconName, 23 | }: RankingItemType) { 24 | const navigate = useNavigate() 25 | const RecordIcon = recordIcons[`${iconName}`] 26 | 27 | const screenAvailWidth = window.screen.availWidth 28 | 29 | const titleRelativeWidth = 30 | screenAvailWidth > 370 31 | ? 'max-w-[45%]' 32 | : screenAvailWidth > 350 33 | ? 'max-w-[40%]' 34 | : 'max-w-[35%]' 35 | 36 | return ( 37 |
navigate(`/record/${recordId}`)} 40 | > 41 |
42 |

{index}

43 |
46 | 47 |
48 |
49 |

{title}

50 |
51 |

{writer}

52 |

+{numOfComment}

53 |
54 |
55 |
56 | 57 | 63 |
64 | ) 65 | } 66 | -------------------------------------------------------------------------------- /src/components/RankingItemNoData.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { ReactComponent as Gift } from '@assets/record_icons/gift.svg' 3 | 4 | export default function RankingItemNoData() { 5 | return ( 6 |
7 |
8 | 9 |
10 |
11 |

아직 랭킹이 없어요

12 |

13 | 조금만 기다리면 확인할 수 있어요 14 |

15 |
16 |
17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /src/components/RecordCard.tsx: -------------------------------------------------------------------------------- 1 | import React, { Dispatch, SetStateAction } from 'react' 2 | import recordIcons from '@assets/record_icons' 3 | import { useLocation, useNavigate } from 'react-router-dom' 4 | 5 | interface CardProps { 6 | title: string 7 | recordId: number 8 | colorName: string 9 | type: 'recentRecord' | 'mainRecord' 10 | iconName: string 11 | commentCount: number 12 | isDragging?: boolean 13 | setIsDragging?: Dispatch> 14 | } 15 | 16 | function RecordCard({ 17 | recordId, 18 | title, 19 | type, 20 | colorName, 21 | iconName, 22 | commentCount, 23 | isDragging, 24 | setIsDragging, 25 | }: CardProps) { 26 | const ColorName = `bg-${colorName}` 27 | const RecordIcon = recordIcons[`${iconName}`] 28 | const navigate = useNavigate() 29 | const { pathname } = useLocation() 30 | 31 | const handleClickRecord = (recordId: number) => { 32 | if (isDragging && setIsDragging) { 33 | setIsDragging(false) 34 | return 35 | } 36 | navigate(`/record/${recordId}`, { state: { previousUrl: pathname } }) 37 | } 38 | return ( 39 |
handleClickRecord(recordId)} 45 | > 46 |
47 | 48 |
49 |
50 |

{title}

51 |
52 |

댓글 {commentCount}개

53 |
54 | ) 55 | } 56 | 57 | export default RecordCard 58 | -------------------------------------------------------------------------------- /src/components/ScrollTop.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react' 2 | import { useLocation } from 'react-router-dom' 3 | 4 | export default function ScrollTop({ children }: { children: React.ReactNode }) { 5 | const { pathname } = useLocation() 6 | 7 | useEffect(() => { 8 | window.scrollTo(0, 0) 9 | }, [pathname]) 10 | 11 | return <>{children} 12 | } 13 | -------------------------------------------------------------------------------- /src/components/SmallToast.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactElement, useEffect, useRef, useState } from 'react' 2 | 3 | interface SmallToastProps { 4 | children: ReactElement 5 | timeLimit?: number 6 | onClose: () => void 7 | } 8 | 9 | function SmallToast({ children, timeLimit = 2, onClose }: SmallToastProps) { 10 | const [times, setTimes] = useState(timeLimit) 11 | const interval: { current: NodeJS.Timeout | undefined } = useRef() 12 | useEffect(() => { 13 | if (times === 0) { 14 | onClose() 15 | } else { 16 | interval.current = setInterval(() => { 17 | setTimes(times - 1) 18 | }, 1000) 19 | return () => clearInterval(interval.current) 20 | } 21 | }, [times]) 22 | 23 | return
{children}
24 | } 25 | 26 | export default SmallToast 27 | -------------------------------------------------------------------------------- /src/components/Spinner.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { ReactComponent as SpinnerIcon } from '@assets/spinner.svg' 3 | 4 | interface ISpinnerProps { 5 | size?: 'small' | 'large' | 'button' 6 | } 7 | 8 | const Spinner = React.memo(function SpinnerComponent({ 9 | size = 'small', 10 | }: ISpinnerProps) { 11 | const setSpinnerSize = (size: string) => { 12 | switch (size) { 13 | case 'small': 14 | return 'w-10 h-10' 15 | case 'large': 16 | return 'w-[85px] h-[85px]' 17 | case 'button': 18 | return 'w-[30px] h-[30px]' 19 | } 20 | } 21 | 22 | return 23 | }) 24 | 25 | export default Spinner 26 | -------------------------------------------------------------------------------- /src/components/Toast.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode, useEffect, useRef, useState } from 'react' 2 | import Modal from './Modal' 3 | 4 | interface IToastProps { 5 | visible: boolean 6 | message: ReactNode 7 | timeLimit?: number 8 | onClose: () => void 9 | hasSecondMessage?: boolean 10 | size?: 'big' | 'basic' 11 | } 12 | 13 | function Toast({ 14 | visible, 15 | message, 16 | timeLimit = 2, 17 | onClose, 18 | hasSecondMessage = true, 19 | size = 'basic', 20 | }: IToastProps) { 21 | const [times, setTimes] = useState(timeLimit) 22 | const interval: { current: NodeJS.Timeout | undefined } = useRef() 23 | useEffect(() => { 24 | if (times === 0) { 25 | onClose() 26 | } else { 27 | interval.current = setInterval(() => { 28 | setTimes(times - 1) 29 | }, 1000) 30 | return () => clearInterval(interval.current) 31 | } 32 | }, [times]) 33 | 34 | return ( 35 | 36 |
41 |
42 | {message} 43 |
44 | {hasSecondMessage && ( 45 |

46 | {times}초 뒤에 사라집니다. 47 |

48 | )} 49 |
50 |
51 | ) 52 | } 53 | 54 | export default Toast 55 | -------------------------------------------------------------------------------- /src/hooks/useCheckMobile.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | 3 | export const useCheckMobile = () => { 4 | const [isMobile, setIsMobile] = useState(false) 5 | 6 | useEffect(() => { 7 | function detectMobileDevice(agent: string): boolean { 8 | const mobileRegex = [ 9 | /Android/i, 10 | /iPhone/i, 11 | /iPad/i, 12 | /iPod/i, 13 | /BlackBerry/i, 14 | /Windows Phone/i, 15 | ] 16 | 17 | return mobileRegex.some((mobile) => agent.match(mobile)) 18 | } 19 | 20 | setIsMobile(detectMobileDevice(window.navigator.userAgent)) 21 | }, []) 22 | 23 | return { isMobile } 24 | } 25 | -------------------------------------------------------------------------------- /src/hooks/useClickOutside.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react' 2 | 3 | const events = ['mousedown', 'touchstart'] 4 | 5 | const useClickOutside = ( 6 | handler: (e: Event) => void 7 | ) => { 8 | // 파라미터로 받는 것 : 바깥 부분을 클릭했을 때 실행 되는 이벤트 9 | const ref = useRef(null) 10 | 11 | useEffect(() => { 12 | const element = ref.current 13 | if (!element) return 14 | 15 | const handleEvent = (e: Event) => { 16 | // 이벤틑 타켓이 해당 엘리먼트에 포함되어 있는지 17 | // 포함되어 있지 않으면 이벤트를 실행 18 | !element.contains(e.target as Node) && handler(e) 19 | } 20 | 21 | for (const eventName of events) { 22 | document.addEventListener(eventName, handleEvent) 23 | } 24 | 25 | return () => { 26 | for (const eventName of events) { 27 | document.removeEventListener(eventName, handleEvent) 28 | } 29 | } 30 | }, [ref, handler]) 31 | 32 | return ref 33 | } 34 | 35 | export default useClickOutside 36 | -------------------------------------------------------------------------------- /src/hooks/useDebounce.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react' 2 | import useTimeoutFunc from './useTimeoutFunc' 3 | 4 | const useDebounce = (func: () => void, delay: number, deps: Array) => { 5 | const [run, clear] = useTimeoutFunc(func, delay) 6 | 7 | useEffect(run, deps) 8 | 9 | return clear 10 | } 11 | 12 | export default useDebounce 13 | -------------------------------------------------------------------------------- /src/hooks/useImageSwipe.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | 3 | export const useImageSwipe = (imageUrl: string[]) => { 4 | const [haveNext, setHaveNext] = useState(false) 5 | const [havePrev, setHavePrev] = useState(false) 6 | const [imageState, setImageState] = useState(0) 7 | 8 | const next = () => { 9 | setImageState((prev) => prev + 1) 10 | } 11 | const prev = () => { 12 | setImageState((prev) => prev - 1) 13 | } 14 | 15 | useEffect(() => { 16 | if (imageUrl[0]) { 17 | if (imageState === 0) { 18 | setHaveNext(true) 19 | setHavePrev(false) 20 | } 21 | if (imageState === imageUrl.length) { 22 | setHaveNext(false) 23 | setHavePrev(true) 24 | } 25 | if (imageState !== 0) setHavePrev(true) 26 | if (imageState !== imageUrl.length) setHaveNext(true) 27 | } 28 | }, [imageUrl, imageState]) 29 | 30 | return { haveNext, havePrev, next, prev, imageState } 31 | } 32 | -------------------------------------------------------------------------------- /src/hooks/useIntersectionObserver.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useRef } from 'react' 2 | 3 | type IntersectHandler = ( 4 | entry: IntersectionObserverEntry, 5 | observer: IntersectionObserver 6 | ) => void 7 | 8 | export const useIntersect = ( 9 | onIntersect: IntersectHandler, 10 | options?: IntersectionObserverInit 11 | ) => { 12 | const ref = useRef(null) 13 | const callback = useCallback( 14 | (entries: IntersectionObserverEntry[], observer: IntersectionObserver) => { 15 | entries.forEach((entry) => { 16 | if (entry.isIntersecting) onIntersect(entry, observer) 17 | }) 18 | }, 19 | [onIntersect] 20 | ) 21 | 22 | useEffect(() => { 23 | if (!ref.current) return 24 | const observer = new IntersectionObserver(callback, options) 25 | observer.observe(ref.current) 26 | return () => observer.disconnect() 27 | }, [ref, options, callback]) 28 | 29 | return ref 30 | } 31 | -------------------------------------------------------------------------------- /src/hooks/useSwipe.ts: -------------------------------------------------------------------------------- 1 | import { MutableRefObject, useState } from 'react' 2 | 3 | const useSwipe = (ref: MutableRefObject) => { 4 | const [isDragging, setIsDragging] = useState(false) 5 | let pos = { top: 0, left: 0, x: 0, y: 0 } 6 | 7 | const handleMouseDown = (e: React.MouseEvent) => { 8 | e.preventDefault() 9 | pos = { 10 | left: ref.current.scrollLeft, 11 | top: ref.current.scrollTop, 12 | x: e.clientX, 13 | y: e.clientY, 14 | } 15 | 16 | document.addEventListener('mousemove', handleMouseMove) 17 | document.addEventListener('mouseup', handleMouseUp) 18 | } 19 | 20 | const handleMouseMove = (e: MouseEvent) => { 21 | if (!isDragging) { 22 | setIsDragging(true) 23 | } 24 | const dx = e.clientX - pos.x 25 | const dy = e.clientY - pos.y 26 | 27 | ref.current.scrollTop = pos.top - dy 28 | ref.current.scrollLeft = pos.left - dx 29 | 30 | ref.current.style.cursor = 'grabbing' 31 | ref.current.style.userSelect = 'none' 32 | } 33 | 34 | const handleMouseUp = (e: MouseEvent) => { 35 | if (isDragging) { 36 | e.stopPropagation() 37 | setIsDragging(false) 38 | } 39 | document.removeEventListener('mousemove', handleMouseMove) 40 | document.removeEventListener('mouseup', handleMouseUp) 41 | 42 | ref.current.style.cursor = 'pointer' 43 | ref.current.style.removeProperty('user-select') 44 | } 45 | 46 | return { handleMouseDown, isDragging, setIsDragging } 47 | } 48 | 49 | export default useSwipe 50 | -------------------------------------------------------------------------------- /src/hooks/useThrottle.ts: -------------------------------------------------------------------------------- 1 | import { useRef } from 'react' 2 | 3 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 4 | export const useThrottle = ( 5 | callback: (...params: T) => void, 6 | delay: number 7 | ) => { 8 | const timer = useRef | null>(null) 9 | 10 | return (...params: T) => { 11 | if (!timer.current) { 12 | callback(...params) 13 | timer.current = setTimeout(() => { 14 | timer.current = null 15 | }, delay) 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/hooks/useTimeoutFunc.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useRef } from 'react' 2 | 3 | type TimeoutFuncType = ( 4 | func: () => void, 5 | delay: number 6 | ) => [run: () => void, clear: () => void] 7 | 8 | const useTimeoutFn: TimeoutFuncType = (func, delay) => { 9 | const timeoutId = useRef() 10 | const callback = useRef(func) 11 | 12 | useEffect(() => { 13 | callback.current = func 14 | }, [func]) 15 | 16 | const run = useCallback(() => { 17 | timeoutId.current && clearTimeout(timeoutId.current) 18 | 19 | timeoutId.current = setTimeout(() => { 20 | callback.current() 21 | }, delay) 22 | }, [delay]) 23 | 24 | const clear = useCallback(() => { 25 | timeoutId.current && clearTimeout(timeoutId.current) 26 | }, []) 27 | 28 | useEffect(() => clear, [clear]) 29 | 30 | return [run, clear] 31 | } 32 | 33 | export default useTimeoutFn 34 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | * { 6 | box-sizing: border-box; 7 | -ms-overflow-style: none; /* Hide scrollbar for IE and Edge */ 8 | scrollbar-width: none; /* Hide scrollbar for Firefox */ 9 | font-family: San Francisco; 10 | } 11 | /* Hide scrollbar for Chrome, Safari and Opera */ 12 | ::-webkit-scrollbar { 13 | display: none; 14 | } 15 | 16 | html, 17 | body, 18 | div, 19 | span, 20 | applet, 21 | object, 22 | iframe, 23 | h1, 24 | h2, 25 | h3, 26 | h4, 27 | h5, 28 | h6, 29 | p, 30 | blockquote, 31 | pre, 32 | a, 33 | abbr, 34 | acronym, 35 | address, 36 | big, 37 | cite, 38 | code, 39 | del, 40 | dfn, 41 | em, 42 | img, 43 | ins, 44 | kbd, 45 | q, 46 | s, 47 | samp, 48 | small, 49 | strike, 50 | strong, 51 | sub, 52 | sup, 53 | tt, 54 | var, 55 | b, 56 | u, 57 | i, 58 | center, 59 | dl, 60 | dt, 61 | dd, 62 | ol, 63 | ul, 64 | li, 65 | fieldset, 66 | form, 67 | label, 68 | legend, 69 | table, 70 | caption, 71 | tbody, 72 | tfoot, 73 | thead, 74 | tr, 75 | th, 76 | td, 77 | article, 78 | aside, 79 | canvas, 80 | details, 81 | embed, 82 | figure, 83 | figcaption, 84 | footer, 85 | header, 86 | hgroup, 87 | menu, 88 | nav, 89 | output, 90 | ruby, 91 | section, 92 | summary, 93 | time, 94 | mark, 95 | audio, 96 | video { 97 | margin: 0; 98 | padding: 0; 99 | border: 0; 100 | font-size: 100%; 101 | vertical-align: baseline; 102 | text-decoration: none; 103 | } 104 | 105 | /* HTML5 display-role reset for older browsers */ 106 | article, 107 | aside, 108 | details, 109 | figcaption, 110 | figure, 111 | footer, 112 | header, 113 | hgroup, 114 | menu, 115 | nav, 116 | section { 117 | display: block; 118 | } 119 | 120 | body { 121 | line-height: 1; 122 | } 123 | 124 | ol, 125 | ul { 126 | list-style: none; 127 | } 128 | 129 | blockquote, 130 | q { 131 | quotes: none; 132 | } 133 | 134 | blockquote:before, 135 | blockquote:after, 136 | q:before, 137 | q:after { 138 | content: ''; 139 | content: none; 140 | } 141 | 142 | table { 143 | border-collapse: collapse; 144 | border-spacing: 0; 145 | } 146 | 147 | div { 148 | border-style: solid; 149 | border-width: 0; 150 | } 151 | 152 | button, 153 | input, 154 | textarea { 155 | border: inherit; 156 | } 157 | 158 | .slick-slide { 159 | display: flex !important; 160 | justify-content: center !important; 161 | } 162 | 163 | .slick-current .slick-select { 164 | width: 186px !important; 165 | background-color: #efe9fb; 166 | } 167 | 168 | .slick-current .slick-select span { 169 | color: #703cde; 170 | } 171 | 172 | .line-clamp { 173 | display: -webkit-box; 174 | -webkit-line-clamp: 3; 175 | -webkit-box-orient: vertical; 176 | } 177 | 178 | body { 179 | min-height: -webkit-fill-available; 180 | } 181 | 182 | .year-slider { 183 | display: block; 184 | } 185 | 186 | .month-slider { 187 | display: block; 188 | } 189 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom/client' 3 | import App from './App' 4 | import './index.css' 5 | 6 | const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement) 7 | root.render( 8 | 9 | 10 | 11 | ) 12 | -------------------------------------------------------------------------------- /src/pages/AddRecord/AddRecordCategory.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react' 2 | import { useRecoilState } from 'recoil' 3 | import { formDataAtom } from '@store/atom' 4 | import Category from '@components/Category' 5 | import { parentCategoryID } from 'types/category' 6 | 7 | function AddRecordCategory({ 8 | parentCategoryId, 9 | recordCategory, 10 | isModify, 11 | }: { 12 | parentCategoryId: parentCategoryID 13 | recordCategory: number 14 | isModify: boolean 15 | }) { 16 | const [formData, setFormData] = useRecoilState(formDataAtom) 17 | const [choosedCategoryId, setChoosedCategoryId] = useState(3) 18 | 19 | useEffect(() => { 20 | setFormData({ 21 | ...formData, 22 | selectedCategory: choosedCategoryId, 23 | }) 24 | }, [choosedCategoryId]) 25 | 26 | useEffect(() => { 27 | if (isModify) { 28 | setChoosedCategoryId(recordCategory) 29 | } 30 | }, []) 31 | return ( 32 |
37 | 44 |
45 | ) 46 | } 47 | 48 | export default AddRecordCategory 49 | -------------------------------------------------------------------------------- /src/pages/AddRecord/AddRecordColor.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react' 2 | import { ReactComponent as Check } from '@assets/check.svg' 3 | import { useRecoilState } from 'recoil' 4 | import { formDataAtom } from '@store/atom' 5 | import { 6 | ADD_RECORD_COLORS, 7 | colorSourceType, 8 | } from '@assets/constant/RecordColors' 9 | import { parentCategoryID } from 'types/category' 10 | 11 | interface Props { 12 | recordColor: string 13 | parentCategoryId: parentCategoryID 14 | } 15 | 16 | function AddRecordColor({ recordColor, parentCategoryId }: Props) { 17 | const [colors, setColors] = useState(ADD_RECORD_COLORS) 18 | const [formData, setFormData] = useRecoilState(formDataAtom) 19 | 20 | useEffect(() => { 21 | setColors( 22 | colors.map((color: colorSourceType, index: number) => { 23 | if (index === 0) { 24 | return { ...color, choosed: true } 25 | } 26 | return { ...color, choosed: false } 27 | }) 28 | ) 29 | 30 | if (recordColor) { 31 | return setColors( 32 | ADD_RECORD_COLORS.map((color) => { 33 | return { ...color, choosed: color.src.indexOf(recordColor) !== -1 } 34 | }) 35 | ) 36 | } 37 | }, [parentCategoryId]) 38 | 39 | const handleChooseCurrentColor = (index: number): void => { 40 | const changeCurrent = colors.map((color) => ({ 41 | ...color, 42 | choosed: color.id === index, 43 | })) 44 | const colorSrc = colors[index].src 45 | setFormData({ 46 | ...formData, 47 | selectedColor: makeColorSrcToColor(colorSrc), 48 | }) 49 | setColors(changeCurrent) 50 | } 51 | 52 | const makeColorSrcToColor = (colorSrc: string) => { 53 | return colorSrc.slice(colorSrc.indexOf('-') + 1) 54 | } 55 | 56 | return ( 57 |
58 | {colors.map((color, index) => { 59 | return ( 60 |
61 |
handleChooseCurrentColor(index)} 64 | /> 65 | {color.choosed && ( 66 | 70 | )} 71 |
72 | ) 73 | })} 74 |
75 | ) 76 | } 77 | 78 | export default AddRecordColor 79 | -------------------------------------------------------------------------------- /src/pages/AddRecord/AddRecordInput.tsx: -------------------------------------------------------------------------------- 1 | import React, { Dispatch, SetStateAction, useEffect, useState } from 'react' 2 | import { 3 | CELEBRATION_ID, 4 | INPUT_DETAILS, 5 | RECORD_TITLE_MAX_LENGTH, 6 | } from '@assets/constant/constant' 7 | import { IsInputsHasValueType } from './AddRecord' 8 | import { parentCategoryID } from 'types/category' 9 | 10 | interface Props { 11 | setIsInputsHasValue: Dispatch> 12 | isInputsHasValue: IsInputsHasValueType 13 | setIsInputFocus: Dispatch> 14 | recordTitle: string 15 | setRecordTitle: Dispatch> 16 | parentCategoryId: parentCategoryID 17 | isModify?: boolean 18 | modifyTitle: string 19 | } 20 | 21 | function AddRecordInput({ 22 | setIsInputsHasValue, 23 | isInputsHasValue, 24 | setIsInputFocus, 25 | recordTitle, 26 | setRecordTitle, 27 | parentCategoryId, 28 | isModify, 29 | modifyTitle, 30 | }: Props) { 31 | const [isFocus, setIsFocus] = useState(false) 32 | const PLACEHOLDER_MESSAGE = 33 | parentCategoryId === CELEBRATION_ID 34 | ? 'ex) 5월 5일 내 생일' 35 | : 'ex) 오늘 우울해요' 36 | 37 | useEffect(() => { 38 | setRecordTitle('') 39 | }, [parentCategoryId]) 40 | 41 | const handleChange = (e: React.ChangeEvent) => { 42 | const inputValueLength = e.target.value.length 43 | if (inputValueLength > INPUT_DETAILS.MAX_INPUT_TYPING) { 44 | return 45 | } 46 | if (inputValueLength > INPUT_DETAILS.MIN_TYPING) { 47 | setIsInputsHasValue({ ...isInputsHasValue, input: true }) 48 | } 49 | if (inputValueLength === INPUT_DETAILS.MIN_TYPING) { 50 | setIsInputsHasValue({ ...isInputsHasValue, input: false }) 51 | } 52 | setRecordTitle(e.target.value) 53 | } 54 | 55 | const handleFocus = () => { 56 | setIsInputFocus(true) 57 | setIsFocus(true) 58 | } 59 | const handleBlur = () => { 60 | setIsInputFocus(false) 61 | setIsFocus(false) 62 | } 63 | 64 | return ( 65 |
70 | handleChange(e)} 77 | type="text" 78 | value={modifyTitle ? modifyTitle : recordTitle} 79 | maxLength={RECORD_TITLE_MAX_LENGTH} 80 | /> 81 | {`${recordTitle.length}/${INPUT_DETAILS.MAX_INPUT_TYPING}`} 82 |
83 | ) 84 | } 85 | 86 | export default AddRecordInput 87 | -------------------------------------------------------------------------------- /src/pages/AddRecord/AddRecordTextArea.tsx: -------------------------------------------------------------------------------- 1 | import React, { Dispatch, SetStateAction, useEffect } from 'react' 2 | import { CELEBRATION_ID, INPUT_DETAILS } from '@assets/constant/constant' 3 | import { IsInputsHasValueType } from './AddRecord' 4 | import { parentCategoryID } from 'types/category' 5 | 6 | type userProps = { 7 | recordContent: string 8 | setRecordContent: Dispatch> 9 | currentRecordType: parentCategoryID 10 | setIsInputsHasValue: Dispatch> 11 | isInputsHasValue: IsInputsHasValueType 12 | setIsInputFocus: Dispatch> 13 | modifyTitle: string 14 | } 15 | 16 | function AddRecordTextArea({ 17 | setIsInputsHasValue, 18 | isInputsHasValue, 19 | currentRecordType, 20 | setIsInputFocus, 21 | recordContent, 22 | setRecordContent, 23 | modifyTitle, 24 | }: userProps) { 25 | const PLACEHOLDER_MESSAGE = { 26 | celebration: 'ex) 오늘은 나의 생일이에요! 모두 축하해주세요!', 27 | consolation: 'ex) 오늘은 기분이 우울하네요. 저를 위로해주세요', 28 | } 29 | 30 | useEffect(() => { 31 | setRecordContent( 32 | modifyTitle 33 | ? recordContent.replaceAll(/(
||
)/g, '\r\n') 34 | : '' 35 | ) 36 | }, [currentRecordType]) 37 | 38 | const handleChangeTextArea = ( 39 | e: React.ChangeEvent 40 | ): void => { 41 | const inputValueLength = e.target.value.length 42 | if (inputValueLength > INPUT_DETAILS.MAX_TEXTAREA_TYPING) { 43 | return 44 | } 45 | if (inputValueLength > 0) { 46 | setIsInputsHasValue({ ...isInputsHasValue, textArea: true }) 47 | } 48 | if (inputValueLength === 0) { 49 | setIsInputsHasValue({ ...isInputsHasValue, textArea: false }) 50 | } 51 | setRecordContent(e.target.value) 52 | } 53 | 54 | return ( 55 |
58 |