├── README.md ├── client ├── .eslintrc.js ├── .gitignore ├── .prettierrc ├── babel.config.js ├── package-lock.json ├── package.json ├── src │ ├── App.tsx │ ├── api │ │ └── index.ts │ ├── components │ │ ├── ChannelListBox │ │ │ ├── ChannelList │ │ │ │ ├── ChannelItem │ │ │ │ │ └── ChannelItem.tsx │ │ │ │ └── ChannelList.tsx │ │ │ ├── ChannelListBox.tsx │ │ │ └── ChannelListHeader │ │ │ │ ├── ChannelListHeader.tsx │ │ │ │ ├── CreateChannelModal │ │ │ │ ├── CreateChannelModalBody │ │ │ │ │ └── CreateChannelModalBody.tsx │ │ │ │ ├── CreateChannelModalHeader │ │ │ │ │ └── CreateChannelModalHeader.tsx │ │ │ │ └── index.ts │ │ │ │ └── FindChannelModal │ │ │ │ ├── FindChannelModalBody │ │ │ │ └── FindChannelModalBody.tsx │ │ │ │ ├── FindChannelModalHeader │ │ │ │ └── FindChannelModalHeader.tsx │ │ │ │ └── index.ts │ │ ├── CodeVerifyBox │ │ │ └── CodeVerifyBox.tsx │ │ ├── DetailBox │ │ │ ├── DeatailHeader │ │ │ │ └── DetailHeader.tsx │ │ │ └── DetailBody │ │ │ │ ├── DetailBody.tsx │ │ │ │ ├── DetailButtonBox │ │ │ │ └── DetailButtonBox.tsx │ │ │ │ └── DetailList │ │ │ │ └── DetailList.tsx │ │ ├── EmailBox │ │ │ └── EmailBox.tsx │ │ ├── LeftSideBar │ │ │ ├── LeftSideBarContent │ │ │ │ └── LeftSideBarContent.tsx │ │ │ ├── LeftSideBarHeader │ │ │ │ └── LeftSideBarHeader.tsx │ │ │ └── LeftSidebar.tsx │ │ ├── LoginBox │ │ │ └── LoginBox.tsx │ │ ├── SignupBox │ │ │ └── SignupBox.tsx │ │ ├── SubThreadListBox │ │ │ ├── ParentThread │ │ │ │ └── ParentThread.tsx │ │ │ ├── ReplyCountHorizon │ │ │ │ └── ReplyCountHorizon.tsx │ │ │ ├── SubThreadList │ │ │ │ └── SubThreadList.tsx │ │ │ ├── SubThreadListBox.tsx │ │ │ └── SubThreadListHeader │ │ │ │ └── SubThreadListHeader.tsx │ │ ├── ThreadListBox │ │ │ ├── ThreadList │ │ │ │ └── ThreadList.tsx │ │ │ ├── ThreadListBox.tsx │ │ │ └── ThreadListHeader │ │ │ │ ├── ChannelModal │ │ │ │ ├── AddTopicModal │ │ │ │ │ ├── AddTopicModalBody │ │ │ │ │ │ └── AddTopicModalBody.tsx │ │ │ │ │ ├── AddTopicModalHeader │ │ │ │ │ │ └── AddTopicModalHeader.tsx │ │ │ │ │ └── index.ts │ │ │ │ └── ShowUsersModal │ │ │ │ │ ├── ShowUsersModalBody │ │ │ │ │ └── ShowUsersModalBody.tsx │ │ │ │ │ ├── ShowUsersModalHeader │ │ │ │ │ └── ShowUsersModalHeader.tsx │ │ │ │ │ └── index.ts │ │ │ │ └── ThreadListHeader.tsx │ │ ├── common │ │ │ ├── AddUsersModal │ │ │ │ ├── AddUsersModalBody │ │ │ │ │ └── AddUsersModalBody.tsx │ │ │ │ ├── AddUsersModalHeader │ │ │ │ │ └── AddUsersModalHeader.tsx │ │ │ │ └── index.ts │ │ │ ├── CloseIconBox │ │ │ │ └── CloseIconBox.tsx │ │ │ ├── DimModal │ │ │ │ └── DimModal.tsx │ │ │ ├── EmojiListModal │ │ │ │ └── EmojiListModal.tsx │ │ │ ├── Header │ │ │ │ ├── Header.tsx │ │ │ │ └── UserProfileBox │ │ │ │ │ ├── UserProfileBody │ │ │ │ │ └── UserProfileBody.tsx │ │ │ │ │ ├── UserProfileHeader │ │ │ │ │ └── UserProfileHeader.tsx │ │ │ │ │ └── index.ts │ │ │ ├── Icon │ │ │ │ ├── AddUserIcon │ │ │ │ │ └── AddUserIcon.tsx │ │ │ │ ├── ArrowDownIcon │ │ │ │ │ └── ArrowDownIcon.tsx │ │ │ │ ├── ArrowRightIcon │ │ │ │ │ └── ArrowRightIcon.tsx │ │ │ │ ├── ChannelLockIcon │ │ │ │ │ └── ChannelLockIcon.tsx │ │ │ │ ├── ClockIcon │ │ │ │ │ └── ClockIcon.tsx │ │ │ │ ├── CommentIcon │ │ │ │ │ └── CommentIcon.tsx │ │ │ │ ├── ConversationIcon │ │ │ │ │ └── ConversationIcon.tsx │ │ │ │ ├── DotIcon │ │ │ │ │ └── DotIcon.tsx │ │ │ │ ├── GoogleLogoIcon │ │ │ │ │ └── GoogleLogoIcon.tsx │ │ │ │ ├── LightIcon │ │ │ │ │ └── LightIcon.tsx │ │ │ │ ├── LockIcon │ │ │ │ │ └── LockIcon.tsx │ │ │ │ ├── PaperPlaneIcon │ │ │ │ │ └── PaperPlaneIcon.tsx │ │ │ │ ├── PlusIcon │ │ │ │ │ └── PlusIcon.tsx │ │ │ │ ├── PoundIcon │ │ │ │ │ └── PoundIcon.tsx │ │ │ │ ├── ReactionIcon │ │ │ │ │ └── ReactionIcon.tsx │ │ │ │ ├── RightArrowLineIcon │ │ │ │ │ └── RightArrowLineIcon.tsx │ │ │ │ ├── RightIcon │ │ │ │ │ └── RightIcon.tsx │ │ │ │ ├── TrashIcon │ │ │ │ │ └── TrashIcon.tsx │ │ │ │ ├── UserStateIcon │ │ │ │ │ └── UserStateIcon.tsx │ │ │ │ ├── WarningIcon │ │ │ │ │ └── WarningIcon.tsx │ │ │ │ ├── WriteIcon │ │ │ │ │ └── WriteIcon.tsx │ │ │ │ └── index.ts │ │ │ ├── LazyImage │ │ │ │ └── LazyImage.tsx │ │ │ ├── LogoBox │ │ │ │ └── LogoBox.tsx │ │ │ ├── ModalCloseBox │ │ │ │ └── ModalCloseBox.tsx │ │ │ ├── Popover │ │ │ │ └── Popover.tsx │ │ │ ├── RightSideBar │ │ │ │ ├── RightSideBar.tsx │ │ │ │ ├── RightSideBarBody │ │ │ │ │ └── RightSideBarBody.tsx │ │ │ │ └── RightSideBarHeader │ │ │ │ │ └── RightSideBarHeader.tsx │ │ │ ├── ThreadInputBox │ │ │ │ └── ThreadInputBox.tsx │ │ │ ├── ThreadItem │ │ │ │ ├── EmojiBox │ │ │ │ │ ├── EmojiBox.tsx │ │ │ │ │ └── EmojiBoxItem │ │ │ │ │ │ ├── EmojiBoxItem.tsx │ │ │ │ │ │ └── TooltipPopup │ │ │ │ │ │ └── TooltipPopup.tsx │ │ │ │ ├── ReplyButton │ │ │ │ │ └── ReplyButton.tsx │ │ │ │ ├── ThreadItem.tsx │ │ │ │ └── ThreadPopup │ │ │ │ │ └── ThreadPopup.tsx │ │ │ └── index.ts │ │ └── index.ts │ ├── config │ │ └── index.ts │ ├── fonts │ │ ├── NotoSansKR-Bold.otf │ │ ├── NotoSansKR-Bold.woff │ │ ├── NotoSansKR-Bold.woff2 │ │ ├── NotoSansKR-Light.otf │ │ ├── NotoSansKR-Light.woff │ │ ├── NotoSansKR-Light.woff2 │ │ ├── NotoSansKR-Medium.otf │ │ ├── NotoSansKR-Medium.woff │ │ ├── NotoSansKR-Medium.woff2 │ │ ├── NotoSansKR-Regular.otf │ │ ├── NotoSansKR-Regular.woff │ │ └── NotoSansKR-Regular.woff2 │ ├── hoc │ │ ├── RestrictRoute.tsx │ │ ├── index.ts │ │ └── withAuth.tsx │ ├── hooks │ │ ├── index.ts │ │ ├── useAuthState.ts │ │ ├── useChannelState.ts │ │ ├── useDuplicatedChannelState.ts │ │ ├── useEmojiState.ts │ │ ├── useFindChannel.ts │ │ ├── useInfiniteScroll.ts │ │ ├── useOnClickOutside.ts │ │ ├── useRedirectState.ts │ │ ├── useSignupState.ts │ │ ├── useSocketState.ts │ │ ├── useSubThreadState.ts │ │ ├── useThreadState.ts │ │ ├── useUserState.ts │ │ └── useViewport.ts │ ├── index.html │ ├── index.tsx │ ├── pages │ │ ├── EmailVerifyPage.tsx │ │ ├── HomePage.tsx │ │ ├── LoadingPage.tsx │ │ ├── LoginPage.tsx │ │ ├── NotFoundPage.tsx │ │ ├── SignupPage.tsx │ │ ├── WorkSpacePage.tsx │ │ └── index.ts │ ├── public │ │ └── icon │ │ │ ├── loading-color.svg │ │ │ ├── loading.svg │ │ │ ├── slack-logo.gif │ │ │ ├── slack-logo.svg │ │ │ ├── slack-logo.webp │ │ │ └── slack.ico │ ├── services │ │ ├── auth.service.ts │ │ ├── channel.service.ts │ │ ├── emoji.service.ts │ │ ├── index.ts │ │ ├── subThread.service.ts │ │ ├── thread.service.ts │ │ └── user.service.ts │ ├── store │ │ ├── index.ts │ │ ├── modules │ │ │ ├── auth.slice.ts │ │ │ ├── channel.slice.ts │ │ │ ├── duplicatedChannel.slice.ts │ │ │ ├── emoji.slice.ts │ │ │ ├── findChannel.slice.ts │ │ │ ├── index.ts │ │ │ ├── redirect.slice.ts │ │ │ ├── signup.slice.ts │ │ │ ├── socket.slice.ts │ │ │ ├── subThread.slice.ts │ │ │ ├── thread.slice.ts │ │ │ └── user.slice.ts │ │ └── sagas │ │ │ ├── authSaga.ts │ │ │ ├── channelSaga.ts │ │ │ ├── duplicatedChannelSaga.ts │ │ │ ├── emojiSaga.ts │ │ │ ├── findChannelSaga.ts │ │ │ ├── index.ts │ │ │ ├── signupSaga.ts │ │ │ ├── socketSaga.ts │ │ │ ├── subThreadSaga.ts │ │ │ ├── threadSaga.ts │ │ │ └── userSaga.ts │ ├── styles │ │ ├── index.ts │ │ ├── mixin.ts │ │ ├── shared │ │ │ ├── button.ts │ │ │ ├── form.ts │ │ │ └── index.ts │ │ ├── styled.d.ts │ │ └── theme.ts │ ├── types │ │ ├── asset.d.ts │ │ ├── auth.ts │ │ ├── channel.ts │ │ ├── channelInfo.ts │ │ ├── index.ts │ │ ├── service.ts │ │ ├── socket.ts │ │ ├── thread.ts │ │ └── user.ts │ └── utils │ │ ├── constants.ts │ │ └── utils.ts ├── tsconfig.json └── webpack.config.js ├── deploy.sh └── server ├── .eslintrc.js ├── .gitignore ├── .prettierrc ├── package-lock.json ├── package.json ├── src ├── app.ts ├── config │ └── index.ts ├── db │ └── index.ts ├── lib │ ├── passport.ts │ └── socket.ts ├── middlewares │ └── auth.middleware.ts ├── models │ ├── channels.model.ts │ ├── emoji.model.ts │ ├── index.ts │ ├── thread.model.ts │ └── user.model.ts ├── public │ └── imgs │ │ ├── etc │ │ └── .keep │ │ └── profile │ │ └── .keep ├── routes │ ├── api │ │ ├── auth │ │ │ ├── auth.controller.ts │ │ │ ├── auth.yaml │ │ │ └── index.ts │ │ ├── channels │ │ │ ├── [channelId] │ │ │ │ ├── channelId.controller.ts │ │ │ │ ├── channelsId.yaml │ │ │ │ └── index.ts │ │ │ ├── channels.controller.ts │ │ │ ├── channels.yaml │ │ │ └── index.ts │ │ ├── emojis │ │ │ ├── emojis.controllers.ts │ │ │ └── index.ts │ │ ├── index.ts │ │ ├── oauth │ │ │ ├── index.ts │ │ │ └── oauth.controller.ts │ │ ├── threads │ │ │ ├── [threadId] │ │ │ │ ├── index.ts │ │ │ │ ├── threadId.controller.ts │ │ │ │ └── threadId.yaml │ │ │ ├── index.ts │ │ │ ├── threads.controller.ts │ │ │ └── threads.yaml │ │ └── users │ │ │ ├── [userId] │ │ │ ├── index.ts │ │ │ ├── userId.controller.ts │ │ │ └── userId.yaml │ │ │ ├── index.ts │ │ │ ├── users.controller.ts │ │ │ └── users.yaml │ └── swagger.yaml ├── services │ ├── channel.service.ts │ ├── emoji.service.ts │ ├── index.ts │ └── thread.service.ts ├── types │ └── index.ts └── utils │ ├── constants.ts │ └── utils.ts └── tsconfig.json /README.md: -------------------------------------------------------------------------------- 1 | # 팀 협업도구, 우리동네 슬랙 🚀 2 | 3 |
4 | 5 | > 배포 주소 : ~https://boost-slack.ga/~ (서버 반납) 6 | 7 |
8 | 9 | ## 🎞 데모 영상 10 | 11 | 12 | 13 | 클릭 시 영상 링크로 이동합니다! 14 | 15 |
16 | 17 | ## 🔎 프로젝트 소개 18 | 19 | 팀 협업도구로 유명한 슬랙을 구현하는 프로젝트 입니다. 20 | 기본적인 회원가입 및 로그인, 채널 분리, 실시간 대화, 이모지 등의 기능을 구현하였습니다. 21 | 22 |
23 | 24 | ## 🧱 기술 스택 25 | 26 | tech_stack 27 | 28 |
29 | 30 | ## 📔 Wiki 31 | 프로젝트와 관련된 상세한 내용은 [Wiki](https://github.com/boostcamp-2020/Project06-A-Slack/wiki) 그리고 초대된 노션에 기록되어 있습니다! 32 | 33 |
34 | 35 | ## 💁🏻‍♀️💁🏻‍♂️ 멤버 36 | 37 | | 멘토님 | J014 | J020 | J214 | 38 | |:-------:| :-----------------------------------------------:| :---------------------------------------------: | :----------------------------------------------------: | 39 | |(추가예정)| drawing|drawing|drawing| 40 | | 오혜성| 권순원[(grap3fruit)](https://github.com/grap3fruit)| 권현준[(rnjshippo)](https://github.com/rnjshippo)| 한상욱[(hansanguk0222)](https://github.com/hansanguk0222)| 41 | -------------------------------------------------------------------------------- /client/.eslintrc.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | env: { 5 | browser: true, 6 | es2021: true, 7 | node: true, 8 | }, 9 | extends: [ 10 | 'plugin:react/recommended', 11 | 'airbnb', 12 | 'plugin:@typescript-eslint/recommended', 13 | 'prettier/@typescript-eslint', 14 | 'plugin:prettier/recommended', 15 | ], 16 | parser: '@typescript-eslint/parser', 17 | parserOptions: { 18 | ecmaFeatures: { 19 | jsx: true, 20 | }, 21 | ecmaVersion: 12, 22 | sourceType: 'module', 23 | }, 24 | plugins: ['react', '@typescript-eslint', 'prettier'], 25 | rules: { 26 | 'react/jsx-filename-extension': [1, { extensions: ['.js', '.jsx', '.ts', '.tsx'] }], 27 | 'react/jsx-one-expression-per-line': 'off', 28 | 'prettier/prettier': 'error', 29 | 'no-console': 'warn', 30 | 'no-plusplus': ['error', { allowForLoopAfterthoughts: true }], 31 | 'no-unused-expressions': ['warn'], 32 | 'import/extensions': [ 33 | 'error', 34 | 'always', 35 | { 36 | js: 'never', 37 | jsx: 'never', 38 | ts: 'never', 39 | tsx: 'never', 40 | }, 41 | ], 42 | 'no-constant-condition': ['error', { checkLoops: false }], 43 | 'no-restricted-globals': 'warn', 44 | 'no-use-before-define': ['off'], // import React할 때 에러떠서 off 45 | '@typescript-eslint/no-use-before-define': ['warn'], 46 | 'import/prefer-default-export': 'off', // 한 개만 export할때는 export default를 쓰도록 하는 옵션 47 | }, 48 | settings: { 49 | 'import/resolver': { 50 | typescript: { 51 | project: path.join(__dirname, './tsconfig.json'), // tsconfig 옵션을 감지하도록 추가 52 | }, 53 | }, 54 | react: { version: 'detect' }, 55 | }, 56 | ignorePatterns: ['node_modules', 'babel.config.js', 'webpack.config.js', '.eslintrc.js'], 57 | }; 58 | -------------------------------------------------------------------------------- /client/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env* 3 | dist -------------------------------------------------------------------------------- /client/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "tabWidth": 2, 4 | "singleQuote": true, 5 | "trailingComma": "all", 6 | "bracketSpacing": true, 7 | "semi": true, 8 | "useTabs": false, 9 | "arrowParens": "always", 10 | "endOfLine": "auto" 11 | } 12 | -------------------------------------------------------------------------------- /client/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = (api) => { 2 | api.cache(true); 3 | 4 | const presets = [ 5 | [ 6 | '@babel/preset-env', 7 | { 8 | targets: '> 0.25%, not dead', 9 | useBuiltIns: 'usage', 10 | corejs: '3', 11 | modules: false, 12 | }, 13 | ], 14 | '@babel/preset-react', 15 | '@babel/preset-typescript', 16 | ]; 17 | const plugins = [ 18 | ['@babel/plugin-transform-async-to-generator'], 19 | ['@babel/plugin-transform-runtime', { corejs: 3 }], 20 | ['@babel/plugin-transform-arrow-functions'], 21 | ]; 22 | 23 | return { 24 | plugins, 25 | presets, 26 | }; 27 | }; 28 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "slace-client", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "cross-env MODE=dev webpack-dev-server --open --mode development --progress", 7 | "watch": "webpack --mode development --progress", 8 | "build": "webpack --mode production --progress" 9 | }, 10 | "dependencies": { 11 | "@reduxjs/toolkit": "^1.4.0", 12 | "axios": "^0.21.0", 13 | "core-js": "^3.7.0", 14 | "jsonwebtoken": "^8.5.1", 15 | "polished": "^4.0.5", 16 | "qs": "^6.9.4", 17 | "react": "^17.0.1", 18 | "react-dom": "^17.0.1", 19 | "react-google-login": "^5.1.25", 20 | "react-redux": "^7.2.2", 21 | "react-router-dom": "^5.2.0", 22 | "redux": "^4.0.5", 23 | "redux-saga": "^1.1.3", 24 | "socket.io-client": "^3.0.2", 25 | "styled-components": "^5.2.1", 26 | "styled-normalize": "^8.0.7", 27 | "validator": "^13.1.17" 28 | }, 29 | "devDependencies": { 30 | "@babel/cli": "^7.12.1", 31 | "@babel/core": "^7.12.3", 32 | "@babel/plugin-transform-arrow-functions": "^7.12.1", 33 | "@babel/plugin-transform-async-to-generator": "^7.12.1", 34 | "@babel/plugin-transform-runtime": "^7.12.1", 35 | "@babel/preset-env": "^7.12.1", 36 | "@babel/preset-react": "^7.12.5", 37 | "@babel/preset-typescript": "^7.12.1", 38 | "@babel/runtime": "^7.12.5", 39 | "@babel/runtime-corejs3": "^7.11.2", 40 | "@types/jsonwebtoken": "^8.5.0", 41 | "@types/node": "^14.14.8", 42 | "@types/qs": "^6.9.5", 43 | "@types/react": "^16.9.56", 44 | "@types/react-dom": "^16.9.9", 45 | "@types/react-redux": "^7.1.11", 46 | "@types/react-router-dom": "^5.1.6", 47 | "@types/socket.io-client": "^1.4.34", 48 | "@types/styled-components": "^5.1.4", 49 | "@types/validator": "^13.1.0", 50 | "@typescript-eslint/eslint-plugin": "^4.8.1", 51 | "@typescript-eslint/parser": "^4.8.1", 52 | "babel-loader": "^8.2.1", 53 | "clean-webpack-plugin": "^3.0.0", 54 | "cross-env": "^7.0.2", 55 | "css-loader": "^5.0.0", 56 | "dotenv-webpack": "^5.1.0", 57 | "eslint": "^7.13.0", 58 | "eslint-config-airbnb": "^18.2.1", 59 | "eslint-config-prettier": "^6.13.0", 60 | "eslint-config-react-app": "^6.0.0", 61 | "eslint-import-resolver-alias": "^1.1.2", 62 | "eslint-import-resolver-typescript": "^2.3.0", 63 | "eslint-plugin-import": "^2.22.1", 64 | "eslint-plugin-jsx-a11y": "^6.4.1", 65 | "eslint-plugin-prettier": "^3.1.4", 66 | "eslint-plugin-react": "^7.21.5", 67 | "eslint-plugin-react-hooks": "^4.2.0", 68 | "file-loader": "^6.1.1", 69 | "html-webpack-plugin": "^4.5.0", 70 | "prettier": "^2.1.2", 71 | "sass": "^1.27.0", 72 | "sass-loader": "^10.0.4", 73 | "style-loader": "^2.0.0", 74 | "terser-webpack-plugin": "^4.2.3", 75 | "ts-loader": "^8.0.11", 76 | "typescript": "^4.0.5", 77 | "url-loader": "^4.1.1", 78 | "webpack": "^4.44.2", 79 | "webpack-cli": "^3.3.12", 80 | "webpack-dev-server": "^3.11.0" 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /client/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { lazy, Suspense } from 'react'; 2 | import { BrowserRouter, Redirect, Route, Switch } from 'react-router-dom'; 3 | import { ThemeProvider } from 'styled-components'; 4 | import LoadingPage from '@/pages/LoadingPage'; 5 | import theme from '@/styles/theme'; 6 | import { GlobalStyle } from '@/styles'; 7 | import { withAuth } from '@/hoc'; 8 | 9 | const HomePage = lazy(() => import('@/pages/HomePage')); 10 | const LoginPage = lazy(() => import('@/pages/LoginPage')); 11 | const EmailVerifyPage = lazy(() => import('@/pages/EmailVerifyPage')); 12 | const SignupPage = lazy(() => import('@/pages/SignupPage')); 13 | const WorkSpacePage = lazy(() => import('@/pages/WorkSpacePage')); 14 | 15 | const App = () => { 16 | return ( 17 | 18 | 19 | 20 | }> 21 | 22 | 23 | 24 | 25 | 26 | 31 | 32 | 33 | 34 | 35 | 36 | ); 37 | }; 38 | 39 | export default App; 40 | -------------------------------------------------------------------------------- /client/src/api/index.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { TOKEN_TYPE } from '@/utils/constants'; 3 | import { verifyJWT } from '@/utils/utils'; 4 | 5 | const instance = axios.create({ timeout: 9000 }); 6 | 7 | instance.interceptors.request.use( 8 | async (config) => { 9 | const accessToken = localStorage.getItem('accessToken'); 10 | if (accessToken) { 11 | try { 12 | await verifyJWT(accessToken, TOKEN_TYPE.ACCESS); 13 | return { 14 | ...config, 15 | headers: { ...config.headers, Authorization: `Bearer ${accessToken}` }, 16 | }; 17 | } catch (err) { 18 | const refreshToken = localStorage.getItem('refreshToken'); 19 | if (refreshToken) { 20 | try { 21 | const { data, status } = await axios.post('/api/auth/token/refresh', { 22 | refreshToken, 23 | }); 24 | if (status === 200) { 25 | localStorage.setItem('accessToken', data.accessToken); 26 | return { 27 | ...config, 28 | headers: { ...config.headers, Authorization: `Bearer ${data.accessToken}` }, 29 | }; 30 | } 31 | } catch (error) { 32 | if (error?.response?.status === 401) { 33 | localStorage.removeItem('accessToken'); 34 | localStorage.removeItem('refreshToken'); 35 | localStorage.removeItem('userId'); 36 | window.location.href = '/login'; 37 | } 38 | } 39 | } 40 | } 41 | } 42 | return config; 43 | }, 44 | (err) => { 45 | return Promise.reject(err); 46 | }, 47 | ); 48 | 49 | instance.interceptors.response.use( 50 | (res) => { 51 | if (res.status >= 400) { 52 | console.error('api 요청 실패', res); 53 | } 54 | return res; 55 | }, 56 | (err) => { 57 | if (axios.isCancel(err)) { 58 | console.log('요청 취소', err); 59 | } else { 60 | if (err?.response?.status === 401) { 61 | const { url } = err.response.config; 62 | if (url !== '/api/auth/login' && url !== '/api/oauth/google/signup') { 63 | window.location.href = '/login'; 64 | } 65 | } 66 | console.error('api 에러', err); 67 | } 68 | return Promise.reject(err); 69 | }, 70 | ); 71 | 72 | export default instance; 73 | -------------------------------------------------------------------------------- /client/src/components/ChannelListBox/ChannelList/ChannelItem/ChannelItem.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import styled, { css } from 'styled-components'; 3 | import { Link } from 'react-router-dom'; 4 | import { useJoinChannelListState, useChannelState } from '@/hooks'; 5 | import { flex } from '@/styles/mixin'; 6 | import { LockIcon, PoundIcon } from '@/components'; 7 | import { useDispatch } from 'react-redux'; 8 | import { unsetUnreadFlag } from '@/store/modules/channel.slice'; 9 | 10 | interface ChannelProps { 11 | picked: boolean; 12 | } 13 | 14 | const Channel = styled.div` 15 | ${flex('center', 'flex-center')} 16 | height: 1.75rem; 17 | padding: 1rem 0 1rem 1.75rem; 18 | font-size: 0.95rem; 19 | color: ${(props) => 20 | props.picked ? props.theme.color.semiWhite : props.theme.color.channelItemColor}; 21 | &:hover { 22 | ${(props) => 23 | !props.picked && 24 | css` 25 | background: rgba(0, 0, 0, 0.2); 26 | `} 27 | } 28 | background: ${(props) => (props.picked ? props.theme.color.blue1 : 'transparent')}; 29 | user-select: none; 30 | `; 31 | 32 | const Icon = styled.div` 33 | margin-right: 15px; 34 | `; 35 | 36 | interface NameProps { 37 | unreadMessage: boolean; 38 | } 39 | 40 | const Name = styled.span` 41 | font-weight: 400; 42 | overflow: hidden; 43 | text-overflow: ellipsis; 44 | white-space: nowrap; 45 | font-weight: ${(props) => (props.unreadMessage ? '800' : 'normal')}; 46 | color: ${(props) => (props.unreadMessage ? 'white' : 'inherit')}; 47 | `; 48 | 49 | interface ChannelItemProps { 50 | idx: number; 51 | } 52 | 53 | const ChannelItem = ({ idx }: ChannelItemProps) => { 54 | const { id, name, isPublic } = useJoinChannelListState(idx); 55 | const { current, myChannelList } = useChannelState(); 56 | 57 | const { unreadMessage } = myChannelList[idx]; 58 | 59 | const picked = id === current?.id; 60 | const unread = !!unreadMessage && !picked; 61 | 62 | const dispatch = useDispatch(); 63 | 64 | useEffect(() => { 65 | if (picked && current) { 66 | dispatch(unsetUnreadFlag({ channelId: current.id })); 67 | } 68 | }, [current]); 69 | 70 | return ( 71 | 72 | 73 | 74 | {isPublic ? ( 75 | 76 | ) : ( 77 | 78 | )} 79 | 80 | {name} 81 | 82 | 83 | ); 84 | }; 85 | 86 | export default ChannelItem; 87 | -------------------------------------------------------------------------------- /client/src/components/ChannelListBox/ChannelList/ChannelList.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-nested-ternary */ 2 | /* eslint-disable react/jsx-props-no-spreading */ 3 | import React, { ReactElement, useEffect } from 'react'; 4 | import { useDispatch } from 'react-redux'; 5 | import { loadMyChannelsRequest, setReloadMyChannelListFlag } from '@/store/modules/channel.slice'; 6 | import { useChannelState, useAuthState } from '@/hooks'; 7 | import { Channel } from '@/types'; 8 | import ChannelItem from './ChannelItem/ChannelItem'; 9 | 10 | const ChannelList = ({ 11 | channelType, 12 | channelListVisible, 13 | }: { 14 | channelType: number; 15 | channelListVisible: boolean; 16 | }): ReactElement => { 17 | const dispatch = useDispatch(); 18 | const { myChannelList, current, reloadMyChannelList } = useChannelState(); 19 | const { userId } = useAuthState(); 20 | 21 | useEffect(() => { 22 | if (userId) { 23 | dispatch(loadMyChannelsRequest({ userId: +userId })); 24 | } 25 | }, [dispatch, userId]); 26 | 27 | useEffect(() => { 28 | if (reloadMyChannelList) { 29 | if (userId) { 30 | dispatch(loadMyChannelsRequest({ userId: +userId })); 31 | } 32 | dispatch(setReloadMyChannelListFlag({ reloadMyChannelList: false })); 33 | } 34 | }, [reloadMyChannelList]); 35 | 36 | return ( 37 | <> 38 | {myChannelList?.map( 39 | (channel: Channel, idx: number) => 40 | channelType === channel.channelType && 41 | (!channelListVisible ? ( 42 | current?.id === channel.id && 43 | ) : ( 44 | 45 | )), 46 | )} 47 | 48 | ); 49 | }; 50 | 51 | export default ChannelList; 52 | -------------------------------------------------------------------------------- /client/src/components/ChannelListBox/ChannelListBox.tsx: -------------------------------------------------------------------------------- 1 | import { CHANNEL_TYPE } from '@/utils/constants'; 2 | import React, { ReactElement, useState } from 'react'; 3 | import styled, { css } from 'styled-components'; 4 | import ChannelList from './ChannelList/ChannelList'; 5 | import ChannelListHeader from './ChannelListHeader/ChannelListHeader'; 6 | 7 | interface ContainerProps { 8 | isDM: boolean; 9 | } 10 | 11 | const Container = styled.div` 12 | padding: ${(props) => props.theme.size.s} 0; 13 | ${(props) => 14 | props.isDM && 15 | css` 16 | min-height: 200px; 17 | `} 18 | `; 19 | 20 | const ChannelListBox = ({ channelType }: { channelType: number }): ReactElement => { 21 | const [channelListVisible, setChannelListVisible] = useState(true); 22 | return ( 23 | 24 | 29 | 30 | 31 | ); 32 | }; 33 | 34 | export default ChannelListBox; 35 | -------------------------------------------------------------------------------- /client/src/components/ChannelListBox/ChannelListHeader/CreateChannelModal/CreateChannelModalHeader/CreateChannelModalHeader.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/no-unescaped-entities */ 2 | /* eslint-disable @typescript-eslint/ban-types */ 3 | import React from 'react'; 4 | import styled from 'styled-components'; 5 | 6 | const HeaderContent = styled.div` 7 | font-size: 1.8rem; 8 | font-weight: 800; 9 | color: ${(props) => props.theme.color.lightBlack}; 10 | padding: 14px 0; 11 | `; 12 | 13 | interface CreateChannelModalHeaderProps { 14 | secret: boolean; 15 | } 16 | 17 | const CreateChannelModalHeader: React.FC = ({ 18 | secret, 19 | }: CreateChannelModalHeaderProps) => { 20 | return {secret ? 'Create a private channel' : 'Create a channel'}; 21 | }; 22 | 23 | export default CreateChannelModalHeader; 24 | -------------------------------------------------------------------------------- /client/src/components/ChannelListBox/ChannelListHeader/CreateChannelModal/index.ts: -------------------------------------------------------------------------------- 1 | export { default as CreateChannelModalHeader } from './CreateChannelModalHeader/CreateChannelModalHeader'; 2 | export { default as CreateChannelModalBody } from './CreateChannelModalBody/CreateChannelModalBody'; 3 | -------------------------------------------------------------------------------- /client/src/components/ChannelListBox/ChannelListHeader/FindChannelModal/FindChannelModalHeader/FindChannelModalHeader.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import { flex } from '@/styles/mixin'; 4 | 5 | const Container = styled.div` 6 | ${flex('center', 'flex-start')} 7 | width: 100%; 8 | height: 3.2rem; 9 | flex-shrink: 0; 10 | font-size: 1.4rem; 11 | font-weight: 800; 12 | background-color: white; 13 | color: ${(props) => props.theme.color.lightBlack}; 14 | `; 15 | 16 | const FindChannelModalHeader: React.FC = () => { 17 | return Channel browser; 18 | }; 19 | 20 | export default FindChannelModalHeader; 21 | -------------------------------------------------------------------------------- /client/src/components/ChannelListBox/ChannelListHeader/FindChannelModal/index.ts: -------------------------------------------------------------------------------- 1 | export { default as FindChannelModalHeader } from './FindChannelModalHeader/FindChannelModalHeader'; 2 | export { default as FindChannelModalBody } from './FindChannelModalBody/FindChannelModalBody'; 3 | -------------------------------------------------------------------------------- /client/src/components/DetailBox/DeatailHeader/DetailHeader.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import { useChannelState } from '@/hooks'; 4 | import { flex } from '@/styles/mixin'; 5 | import { LockIcon, PoundIcon } from '@/components'; 6 | import theme from '@/styles/theme'; 7 | 8 | const Container = styled.div` 9 | ${flex('center', 'space-between')} 10 | width: 100%; 11 | height: 4.3rem; 12 | flex-shrink: 0; 13 | background-color: white; 14 | `; 15 | 16 | const LeftBox = styled.div``; 17 | 18 | const LeftTopBox = styled.div` 19 | font-weight: 800; 20 | `; 21 | 22 | const LeftBottomBox = styled.div` 23 | width: 16rem; 24 | overflow: hidden; 25 | text-overflow: ellipsis; 26 | white-space: nowrap; 27 | `; 28 | 29 | const ChannelTitle = styled.span` 30 | color: ${(props) => props.theme.color.black5}; 31 | font-size: 0.8rem; 32 | font-weight: 200; 33 | margin-left: 0.2rem; 34 | `; 35 | 36 | const DetailHeader: React.FC = () => { 37 | const { current } = useChannelState(); 38 | 39 | return ( 40 | 41 | 42 | Detail 43 | 44 | {current?.isPublic ? ( 45 | 46 | ) : ( 47 | 48 | )} 49 | {current?.name} 50 | 51 | 52 | 53 | ); 54 | }; 55 | 56 | export default DetailHeader; 57 | -------------------------------------------------------------------------------- /client/src/components/DetailBox/DetailBody/DetailBody.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import React from 'react'; 3 | import { DetailList } from './DetailList/DetailList'; 4 | import { DetailButtonBox } from './DetailButtonBox/DetailButtonBox'; 5 | 6 | const Container = styled.div``; 7 | 8 | const DetailBody: React.FC = () => { 9 | return ( 10 | 11 | 12 | 13 | 14 | ); 15 | }; 16 | 17 | export default DetailBody; 18 | -------------------------------------------------------------------------------- /client/src/components/DetailBox/DetailBody/DetailButtonBox/DetailButtonBox.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import { flex } from '@/styles/mixin'; 4 | 5 | const Container = styled.div` 6 | ${flex('center', 'space-around')} 7 | padding: 1.5rem 2rem; 8 | border-bottom: 1px solid ${(props) => props.theme.color.lightGray2}; 9 | `; 10 | 11 | const Label = styled.label` 12 | ${flex('center', 'center', 'column')} 13 | color: ${(props) => props.theme.color.black8}; 14 | font-size: 0.8rem; 15 | font-weight: 200; 16 | `; 17 | 18 | const Button = styled.button` 19 | ${flex()} 20 | border-radius: 50%; 21 | width: 40px; 22 | height: 40px; 23 | border: none; 24 | outline: none; 25 | background-color: #f3f3f3; 26 | &:hover { 27 | background-color: ${(props) => props.theme.color.lightGray2}; 28 | } 29 | &:active { 30 | background-color: ${(props) => props.theme.color.lightGray1}; 31 | } 32 | `; 33 | 34 | export const DetailButtonBox: React.FC = () => { 35 | return ( 36 | 37 | 41 | ); 42 | }; 43 | 44 | export default LeftSideBar; 45 | -------------------------------------------------------------------------------- /client/src/components/LeftSideBar/LeftSidebar.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactElement } from 'react'; 2 | import styled from 'styled-components'; 3 | import LeftSidebarHeader from './LeftSideBarHeader/LeftSideBarHeader'; 4 | import LeftSidebarContent from './LeftSideBarContent/LeftSideBarContent'; 5 | 6 | const Container = styled.div` 7 | width: 16.25rem; 8 | height: 100%; 9 | display: flex; 10 | flex-direction: column; 11 | background: ${(props) => props.theme.color.purple2}; 12 | `; 13 | 14 | const LeftSideBar = (): ReactElement => { 15 | return ( 16 | 17 | 18 | 19 | 20 | ); 21 | }; 22 | 23 | export default LeftSideBar; 24 | -------------------------------------------------------------------------------- /client/src/components/SubThreadListBox/ParentThread/ParentThread.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import { Thread } from '@/types'; 4 | import { ThreadItem } from '@/components'; 5 | 6 | const Container = styled.div``; 7 | 8 | interface parentThreadProps { 9 | parentThread: Thread; 10 | } 11 | 12 | const SubThreadList: React.FC = ({ parentThread }: parentThreadProps) => { 13 | if (parentThread.isDeleted) { 14 | if (parentThread.subCount > 0) { 15 | const deletedThread = { ...parentThread, content: 'This message was deleted.' }; 16 | return ( 17 | 18 | 19 | 20 | ); 21 | } 22 | } 23 | 24 | return ( 25 | 26 | 27 | 28 | ); 29 | }; 30 | 31 | export default SubThreadList; 32 | -------------------------------------------------------------------------------- /client/src/components/SubThreadListBox/ReplyCountHorizon/ReplyCountHorizon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import { flex } from '@/styles/mixin'; 4 | 5 | const Container = styled.div` 6 | width: 100%; 7 | ${flex()}; 8 | padding: 0.5rem 1rem; 9 | `; 10 | 11 | const Text = styled.div` 12 | color: ${(props) => props.theme.color.black8}; 13 | margin-right: 0.5rem; 14 | font-size: 0.75rem; 15 | font-weight: 400; 16 | white-space: nowrap; 17 | `; 18 | 19 | const Line = styled.div` 20 | width: 100%; 21 | height: 0.5px; 22 | background-color: ${(props) => props.theme.color.lightGray2}; 23 | `; 24 | 25 | interface ReplyCountHorizonProps { 26 | subCount: number; 27 | } 28 | 29 | const ReplyCountHorizon: React.FC = ({ 30 | subCount, 31 | }: ReplyCountHorizonProps) => { 32 | return ( 33 | 34 | {subCount === 1 ? `${subCount} reply` : `${subCount} replies`} 35 | 36 | 37 | ); 38 | }; 39 | 40 | export default ReplyCountHorizon; 41 | -------------------------------------------------------------------------------- /client/src/components/SubThreadListBox/SubThreadList/SubThreadList.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef } from 'react'; 2 | import styled from 'styled-components'; 3 | import { Thread } from '@/types'; 4 | import { ThreadItem } from '@/components'; 5 | 6 | const Container = styled.div``; 7 | 8 | const Bottom = styled.div``; 9 | interface SubThreadListProps { 10 | subThreadList: Thread[] | null; 11 | } 12 | 13 | const SubThreadList: React.FC = ({ subThreadList }: SubThreadListProps) => { 14 | const bottomRef = useRef(null); 15 | return ( 16 | 17 | {subThreadList 18 | ?.filter((thread) => !thread.isDeleted) 19 | .map((thread: Thread) => { 20 | return ; 21 | })} 22 | 23 | 24 | ); 25 | }; 26 | 27 | export default SubThreadList; 28 | -------------------------------------------------------------------------------- /client/src/components/SubThreadListBox/SubThreadListBox.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import styled from 'styled-components'; 3 | import { useSubThreadState } from '@/hooks'; 4 | import { useParams } from 'react-router-dom'; 5 | import { useDispatch } from 'react-redux'; 6 | import { getSubThreadRequest } from '@/store/modules/subThread.slice'; 7 | import { isNumberTypeValue } from '@/utils/utils'; 8 | import { ThreadInputBox } from '@/components/common'; 9 | import { INPUT_BOX_TYPE } from '@/utils/constants'; 10 | import ParentThread from './ParentThread/ParentThread'; 11 | import SubThreadList from './SubThreadList/SubThreadList'; 12 | import ReplyCountHorizon from './ReplyCountHorizon/ReplyCountHorizon'; 13 | 14 | const Container = styled.div` 15 | width: 25rem; 16 | display: flex; 17 | flex-direction: column; 18 | `; 19 | 20 | const ListContainer = styled.div` 21 | width: 100%; 22 | display: flex; 23 | flex-direction: column; 24 | flex: 1; 25 | /* overflow-y: auto; */ 26 | `; 27 | 28 | interface RightSideParams { 29 | channelId: string | undefined; 30 | rightSideType: string | undefined; 31 | threadId: string | undefined; 32 | } 33 | 34 | const SubThreadListBox: React.FC = () => { 35 | const dispatch = useDispatch(); 36 | const { threadId }: RightSideParams = useParams(); 37 | const { parentThread, subThreadList } = useSubThreadState(); 38 | 39 | useEffect(() => { 40 | if (!Number.isNaN(Number(threadId))) { 41 | dispatch(getSubThreadRequest({ parentId: Number(threadId) })); 42 | } 43 | }, [threadId]); 44 | 45 | return ( 46 | <> 47 | {isNumberTypeValue(threadId) && parentThread !== undefined && ( 48 | 49 | 50 | 51 | {parentThread.subCount > 0 && } 52 | 53 | 54 | 55 | 56 | )} 57 | 58 | ); 59 | }; 60 | 61 | export default SubThreadListBox; 62 | -------------------------------------------------------------------------------- /client/src/components/SubThreadListBox/SubThreadListHeader/SubThreadListHeader.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import { useDispatch } from 'react-redux'; 3 | import { useChannelState, useUserState } from '@/hooks'; 4 | import styled from 'styled-components'; 5 | import { useParams } from 'react-router-dom'; 6 | import { loadChannelRequest } from '@/store/modules/channel.slice'; 7 | import { LockIcon, PoundIcon } from '@/components'; 8 | import theme from '@/styles/theme'; 9 | import { flex } from '@/styles/mixin'; 10 | 11 | const Container = styled.div` 12 | ${flex('center', 'space-between')} 13 | width: 100%; 14 | height: 4.3rem; 15 | flex-shrink: 0; 16 | background-color: white; 17 | `; 18 | 19 | const LeftBox = styled.div``; 20 | 21 | const LeftTopBox = styled.div` 22 | font-weight: 800; 23 | `; 24 | const LeftBottomBox = styled.div` 25 | width: 16rem; 26 | overflow: hidden; 27 | text-overflow: ellipsis; 28 | white-space: nowrap; 29 | `; 30 | 31 | const ChannelTitle = styled.span` 32 | color: ${(props) => props.theme.color.black5}; 33 | font-size: 0.8rem; 34 | font-weight: 200; 35 | margin-left: 0.2rem; 36 | `; 37 | 38 | interface RightSideParams { 39 | channelId: string; 40 | } 41 | 42 | const SubThreadListHeader = () => { 43 | const dispatch = useDispatch(); 44 | const { current } = useChannelState(); 45 | const { userInfo } = useUserState(); 46 | const { channelId }: RightSideParams = useParams(); 47 | 48 | useEffect(() => { 49 | if (userInfo) { 50 | dispatch(loadChannelRequest({ channelId: +channelId, userId: userInfo.id })); 51 | } 52 | }, [channelId, userInfo]); 53 | 54 | return ( 55 | 56 | 57 | Thread 58 | 59 | {current?.isPublic ? ( 60 | 61 | ) : ( 62 | 63 | )} 64 | {current?.name} 65 | 66 | 67 | 68 | ); 69 | }; 70 | 71 | export default SubThreadListHeader; 72 | -------------------------------------------------------------------------------- /client/src/components/ThreadListBox/ThreadListBox.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import styled from 'styled-components'; 3 | import { useDispatch } from 'react-redux'; 4 | import { useParams } from 'react-router-dom'; 5 | import { INPUT_BOX_TYPE, SOCKET_MESSAGE_TYPE } from '@/utils/constants'; 6 | import { ThreadInputBox } from '@/components'; 7 | import { loadChannelRequest } from '@/store/modules/channel.slice'; 8 | import { 9 | enterRoomRequest, 10 | leaveRoomRequest, 11 | sendMessageRequest, 12 | } from '@/store/modules/socket.slice'; 13 | import { useChannelState, useSocketState, useUserState } from '@/hooks'; 14 | import { isNumberTypeValue } from '@/utils/utils'; 15 | import { getThreadRequest } from '@/store/modules/thread.slice'; 16 | import ThreadList from './ThreadList/ThreadList'; 17 | import ThreadListHeader from './ThreadListHeader/ThreadListHeader'; 18 | 19 | const Container = styled.div` 20 | width: 100%; 21 | flex: 1; 22 | display: flex; 23 | flex-direction: column; 24 | border-right: 1px solid ${(props) => props.theme.color.lightGray2}; 25 | `; 26 | 27 | interface RightSideParams { 28 | channelId: string; 29 | threadId: string; 30 | } 31 | 32 | const ThreadListBox = () => { 33 | const { channelId, threadId }: RightSideParams = useParams(); 34 | 35 | const dispatch = useDispatch(); 36 | 37 | const { current } = useChannelState(); 38 | const { userInfo, edit } = useUserState(); 39 | const { socket } = useSocketState(); 40 | 41 | useEffect(() => { 42 | if (isNumberTypeValue(channelId)) { 43 | if (userInfo) { 44 | dispatch(loadChannelRequest({ channelId: +channelId, userId: userInfo.id })); 45 | } 46 | } 47 | }, [channelId, userInfo]); 48 | 49 | useEffect(() => { 50 | if (Number.isInteger(+channelId)) { 51 | dispatch(getThreadRequest({ channelId: +channelId })); 52 | } 53 | }, [channelId]); 54 | 55 | useEffect(() => { 56 | if (edit.success && userInfo) { 57 | dispatch( 58 | sendMessageRequest({ 59 | type: SOCKET_MESSAGE_TYPE.USER, 60 | user: userInfo, 61 | parentThreadId: threadId, 62 | }), 63 | ); 64 | } 65 | }, [edit]); 66 | 67 | useEffect(() => { 68 | if (current && socket) { 69 | dispatch(enterRoomRequest({ room: current.name })); 70 | } 71 | return () => { 72 | if (current && socket) { 73 | dispatch(leaveRoomRequest({ room: current.name })); 74 | } 75 | }; 76 | }, [current, socket]); 77 | 78 | return ( 79 | 80 | 81 | 82 | 83 | 84 | ); 85 | }; 86 | 87 | export default ThreadListBox; 88 | -------------------------------------------------------------------------------- /client/src/components/ThreadListBox/ThreadListHeader/ChannelModal/AddTopicModal/AddTopicModalBody/AddTopicModalBody.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef, useState } from 'react'; 2 | import styled from 'styled-components'; 3 | import { flex } from '@/styles/mixin'; 4 | import { useDispatch } from 'react-redux'; 5 | import { useChannelState } from '@/hooks'; 6 | import { sendMessageRequest } from '@/store/modules/socket.slice'; 7 | import { SOCKET_MESSAGE_TYPE, CHANNEL_SUBTYPE } from '@/utils/constants'; 8 | import { FormInput, SubmitButton as SB, CancelButton as CB } from '@/styles/shared'; 9 | 10 | const ModalBody = styled.div` 11 | ${flex()}; 12 | padding: 0 1.4rem; 13 | `; 14 | 15 | const TextArea = styled(FormInput)` 16 | width: 100%; 17 | height: 90px; 18 | font-size: ${(props) => props.theme.size.m}; 19 | resize: none; 20 | `; 21 | 22 | const ModalFooter = styled.div` 23 | width: 100%; 24 | height: 100%; 25 | margin: 1.4rem 0; 26 | padding: 0 1rem; 27 | border-radius: 0 0 5px 5px; 28 | ${flex('center', 'flex-end')}; 29 | `; 30 | 31 | const CancelButton = styled(CB)` 32 | font-weight: 800; 33 | margin: 0 0.5rem; 34 | `; 35 | 36 | const SubmitButton = styled(SB)` 37 | font-weight: 800; 38 | margin: 0 0.5rem; 39 | `; 40 | 41 | interface AddTopicModalBodyProps { 42 | setAddTopicModalVisible: (fn: (state: boolean) => boolean) => void; 43 | } 44 | 45 | const AddTopicModalBody: React.FC = ({ 46 | setAddTopicModalVisible, 47 | }: AddTopicModalBodyProps) => { 48 | const { current } = useChannelState(); 49 | const [content, setContent] = useState(current?.topic ?? ''); 50 | const dispatch = useDispatch(); 51 | 52 | const changeContent = (e: React.ChangeEvent) => { 53 | const { value } = e.target; 54 | setContent(value); 55 | }; 56 | 57 | const clickCancel = () => { 58 | setAddTopicModalVisible((state: boolean) => !state); 59 | }; 60 | 61 | const clickSubmit = () => { 62 | if (!content.trim()) { 63 | alert('토픽을 입력해주세요'); 64 | return; 65 | } 66 | if (current?.id) { 67 | dispatch( 68 | sendMessageRequest({ 69 | type: SOCKET_MESSAGE_TYPE.CHANNEL, 70 | subType: CHANNEL_SUBTYPE.UPDATE_CHANNEL_TOPIC, 71 | channel: { ...current, topic: content }, 72 | room: current?.name as string, 73 | }), 74 | ); 75 | setAddTopicModalVisible((state: boolean) => !state); 76 | } 77 | }; 78 | 79 | const textareaRef = useRef(null); 80 | 81 | useEffect(() => { 82 | textareaRef.current?.focus(); 83 | }, []); 84 | 85 | return ( 86 | <> 87 | 88 |