├── .github ├── ISSUE_TEMPLATE │ ├── bug_request.md │ └── feature_request.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── README.md ├── client ├── .babelrc.json ├── .eslintrc.json ├── .prettierrc.json ├── .storybook │ ├── main.js │ └── preview.js ├── README.md ├── env │ └── .env.sample ├── package-lock.json ├── package.json ├── public │ ├── favicon.ico │ ├── imgs │ │ ├── channel-icon.png │ │ ├── close-filled-icon.png │ │ ├── close-icon.png │ │ ├── detail-icon.png │ │ ├── emoji-icon.png │ │ ├── filter-icon.png │ │ ├── index.d.ts │ │ ├── lock-icon.png │ │ ├── logo-text.png │ │ ├── logo.png │ │ ├── option-icon.png │ │ ├── plus-icon.png │ │ ├── search-icon.png │ │ ├── send-message-icon.png │ │ ├── sort-icon.png │ │ ├── star-blue.png │ │ ├── star.png │ │ ├── thread-icon.png │ │ └── user-icon.png │ └── index.html ├── src │ ├── common │ │ ├── constants │ │ │ ├── chat-type.ts │ │ │ ├── chatroom-type.ts │ │ │ ├── default-section-name.ts │ │ │ ├── http-status-code.ts │ │ │ ├── index.ts │ │ │ ├── key-code.ts │ │ │ ├── scroll-event-type.ts │ │ │ ├── size.ts │ │ │ └── sort-method.ts │ │ ├── socket │ │ │ ├── emits │ │ │ │ ├── chatroom.ts │ │ │ │ ├── message.ts │ │ │ │ ├── reaction.ts │ │ │ │ └── thread.ts │ │ │ ├── socketIO.ts │ │ │ └── types │ │ │ │ ├── chatroom-types.ts │ │ │ │ ├── message-types.ts │ │ │ │ ├── reaction-types.ts │ │ │ │ └── thread-types.ts │ │ ├── store │ │ │ ├── actions │ │ │ │ ├── channel-action.ts │ │ │ │ ├── chatroom-action.ts │ │ │ │ ├── modal-action.ts │ │ │ │ ├── thread-action.ts │ │ │ │ └── user-action.ts │ │ │ ├── index.ts │ │ │ ├── reducers │ │ │ │ ├── channel-reducer.ts │ │ │ │ ├── chatroom-reducer.ts │ │ │ │ ├── index.ts │ │ │ │ ├── modal-reducer.ts │ │ │ │ ├── thread-reducer.ts │ │ │ │ └── user-reducer.ts │ │ │ ├── sagas │ │ │ │ ├── channel-saga.ts │ │ │ │ ├── chatroom-saga.ts │ │ │ │ ├── index.ts │ │ │ │ ├── thread-saga.ts │ │ │ │ └── user-saga.ts │ │ │ └── types │ │ │ │ ├── channel-types.ts │ │ │ │ ├── chatroom-types.ts │ │ │ │ ├── message-types.ts │ │ │ │ ├── modal-types.ts │ │ │ │ ├── reactions-type.ts │ │ │ │ ├── thread-types.ts │ │ │ │ └── user-types.ts │ │ ├── theme │ │ │ ├── color.ts │ │ │ └── index.ts │ │ └── utils │ │ │ ├── api.ts │ │ │ ├── auth.ts │ │ │ ├── blockPage.ts │ │ │ ├── index.ts │ │ │ ├── logout.ts │ │ │ ├── modal.ts │ │ │ ├── registerToken.ts │ │ │ ├── scroll.ts │ │ │ ├── time.ts │ │ │ └── uriParser.ts │ ├── components │ │ ├── atoms │ │ │ ├── ActiveLight │ │ │ │ ├── ActiveLight.stories.tsx │ │ │ │ └── ActiveLight.tsx │ │ │ ├── Button │ │ │ │ ├── Button.stories.tsx │ │ │ │ └── Button.tsx │ │ │ ├── DropMenuBox │ │ │ │ ├── DropMenuBox.stories.tsx │ │ │ │ └── DropMenuBox.tsx │ │ │ ├── DropMenuItem │ │ │ │ ├── DropMenuItem.stories.tsx │ │ │ │ └── DropMenuItem.tsx │ │ │ ├── Emoji │ │ │ │ ├── Emoji.stories.tsx │ │ │ │ └── Emoji.tsx │ │ │ ├── HoverInput │ │ │ │ ├── HoverInput.stories.tsx │ │ │ │ └── HoverInput.tsx │ │ │ ├── Icon │ │ │ │ ├── Icon.stories.tsx │ │ │ │ └── Icon.tsx │ │ │ ├── Input │ │ │ │ ├── Input.stories.tsx │ │ │ │ └── Input.tsx │ │ │ ├── LogoImg │ │ │ │ ├── LogoImg.stories.tsx │ │ │ │ └── LogoImg.tsx │ │ │ ├── ModalBox │ │ │ │ ├── ModalBox.stories.tsx │ │ │ │ └── ModalBox.tsx │ │ │ ├── ProfileImg │ │ │ │ ├── ProfileImg.stories.tsx │ │ │ │ └── ProfileImg.tsx │ │ │ ├── ProfileModalBody │ │ │ │ ├── ProfileModalBody.stories.tsx │ │ │ │ └── ProfileModalBody.tsx │ │ │ ├── ProfileModalImg │ │ │ │ ├── ProfileModalImg.stories.tsx │ │ │ │ └── ProfileModalImg.tsx │ │ │ ├── Text │ │ │ │ ├── Text.stories.tsx │ │ │ │ └── Text.tsx │ │ │ └── index.ts │ │ ├── molecules │ │ │ ├── Actionbar │ │ │ │ ├── Actionbar.stories.tsx │ │ │ │ └── Actionbar.tsx │ │ │ ├── ActiveProfileImg │ │ │ │ ├── ActiveProfileImg.stories.tsx │ │ │ │ └── ActiveProfileImg.tsx │ │ │ ├── AddChannelButton │ │ │ │ ├── AddChannelButton.stories.tsx │ │ │ │ └── AddChannelButton.tsx │ │ │ ├── BrowsePageChannelBody │ │ │ │ ├── BrowsePageChannelBody.stories.tsx │ │ │ │ └── BrowsePageChannelBody.tsx │ │ │ ├── BrowsePageChannelButton │ │ │ │ ├── BrowsePageChannelButton.stories.tsx │ │ │ │ └── BrowsePageChannelButton.tsx │ │ │ ├── BrowsePageChannelHeader │ │ │ │ ├── BrowsePageChannelHeader.stories.tsx │ │ │ │ └── BrowsePageChannelHeader.tsx │ │ │ ├── BrowsePageControls │ │ │ │ ├── BrowsePageControls.stories.tsx │ │ │ │ └── BrowsePageControls.tsx │ │ │ ├── BrowsePageSearchBar │ │ │ │ ├── BrowsePageSearchBar.stories.tsx │ │ │ │ └── BrowsePageSearchBar.tsx │ │ │ ├── Channel │ │ │ │ ├── Channel.stories.tsx │ │ │ │ └── Channel.tsx │ │ │ ├── ChannelModal │ │ │ │ ├── ChannelModal.stories.tsx │ │ │ │ └── ChannelModal.tsx │ │ │ ├── DM │ │ │ │ ├── DM.stories.tsx │ │ │ │ └── DM.tsx │ │ │ ├── EmojiBox │ │ │ │ ├── EmojiBox.stories.tsx │ │ │ │ └── EmojiBox.tsx │ │ │ ├── EmojiPicker │ │ │ │ ├── EmojiPicker.stories.tsx │ │ │ │ └── EmojiPicker.tsx │ │ │ ├── GithubLoginButton │ │ │ │ ├── GithubLoginButton.stories.tsx │ │ │ │ └── GithubLoginButton.tsx │ │ │ ├── HoverIcon │ │ │ │ ├── HoverIcon.stories.tsx │ │ │ │ └── HoverIcon.tsx │ │ │ ├── InputMessage │ │ │ │ ├── InputMessage.stories.tsx │ │ │ │ └── InputMessage.tsx │ │ │ ├── InputReply │ │ │ │ ├── InputReply.stories.tsx │ │ │ │ └── InputReply.tsx │ │ │ ├── Message │ │ │ │ ├── Message.stories.tsx │ │ │ │ └── Message.tsx │ │ │ ├── MessageReplyBar │ │ │ │ ├── MessageReplyBar.stories.tsx │ │ │ │ └── MessageReplyBar.tsx │ │ │ ├── ProfileModal │ │ │ │ ├── ProfileModal.stories.tsx │ │ │ │ └── ProfileModal.tsx │ │ │ ├── Reply │ │ │ │ └── Reply.tsx │ │ │ ├── Section │ │ │ │ ├── Section.stories.tsx │ │ │ │ └── Section.tsx │ │ │ ├── SendMessageButton │ │ │ │ ├── SendMessageButton.stories.tsx │ │ │ │ └── SendMessageButton.tsx │ │ │ ├── UserBox │ │ │ │ ├── UserBox.stories.tsx │ │ │ │ └── UserBox.tsx │ │ │ ├── UserBoxModalSearchBar │ │ │ │ ├── UserBoxModalSearchBar.stories.tsx │ │ │ │ └── UserBoxModalSearchBar.tsx │ │ │ ├── UserBoxModalUserItem │ │ │ │ ├── UserBoxModalUserItem.stories.tsx │ │ │ │ └── UserBoxModalUserItem.tsx │ │ │ ├── WhiteButtonWithIcon │ │ │ │ ├── WhiteButtonWithIcon.stories.tsx │ │ │ │ └── WhiteButtonWithIcon.tsx │ │ │ └── index.ts │ │ ├── organisms │ │ │ ├── BrowsePageChannel │ │ │ │ ├── BrowsePageChannel.stories.tsx │ │ │ │ └── BrowsePageChannel.tsx │ │ │ ├── BrowsePageChannelList │ │ │ │ ├── BrowsePageChannelList.stories.tsx │ │ │ │ └── BrowsePageChannelList.tsx │ │ │ ├── BrowsePageHeader │ │ │ │ ├── BrowsePageHeader.stories.tsx │ │ │ │ └── BrowsePageHeader.tsx │ │ │ ├── ChatroomBody │ │ │ │ ├── ChatroomBody.stories.tsx │ │ │ │ └── ChatroomBody.tsx │ │ │ ├── ChatroomHeader │ │ │ │ ├── ChatroomHeader.stories.tsx │ │ │ │ └── ChatroomHeader.tsx │ │ │ ├── CreateChannelModal │ │ │ │ ├── CreateChannelModal.stories.tsx │ │ │ │ └── CreateChannelModal.tsx │ │ │ ├── Header │ │ │ │ ├── Header.stories.tsx │ │ │ │ └── Header.tsx │ │ │ ├── LoginForm │ │ │ │ ├── LoginForm.stories.tsx │ │ │ │ └── LoginForm.tsx │ │ │ ├── Sidebar │ │ │ │ ├── Sidebar.stories.tsx │ │ │ │ └── Sidebar.tsx │ │ │ ├── ThreadBody │ │ │ │ ├── ThreadBody.tsx │ │ │ │ └── ThreadReplies.tsx │ │ │ ├── ThreadHeader │ │ │ │ └── ThreadHeader.tsx │ │ │ ├── UserBoxModal │ │ │ │ ├── UserBoxModal.stories.tsx │ │ │ │ └── UserBoxModal.tsx │ │ │ └── index.ts │ │ └── templates │ │ │ ├── Body.tsx │ │ │ ├── FlexContainer.tsx │ │ │ ├── Main.tsx │ │ │ ├── MainBox.tsx │ │ │ └── index.ts │ ├── index.tsx │ ├── pages │ │ ├── ChannelBrowser │ │ │ └── ChannelBrowser.tsx │ │ ├── Chatroom │ │ │ ├── Chatroom.tsx │ │ │ ├── ChatroomThread.tsx │ │ │ └── Thread.tsx │ │ ├── Login.tsx │ │ ├── LoginLoading.tsx │ │ └── index.ts │ └── shared │ │ └── App.tsx ├── tsconfig.json └── webpack.config.js └── server ├── .env.sample ├── .eslintrc.js ├── .prettierrc ├── README.md ├── docker-compose.yml ├── ormconfig.js ├── package-lock.json ├── package.json ├── src ├── application.ts ├── common │ ├── config │ │ └── passport.ts │ ├── constants │ │ ├── chat-type.ts │ │ ├── default-section-name.ts │ │ ├── event-name.ts │ │ └── http-status-code.ts │ ├── error │ │ ├── bad-request-error.ts │ │ ├── conflict-error.ts │ │ ├── forbidden-error.ts │ │ ├── not-found-error.ts │ │ └── unauthorized-error.ts │ ├── middleware │ │ ├── error-handler.ts │ │ └── redis.ts │ └── utils │ │ ├── index.ts │ │ └── validator.ts ├── controller │ ├── auth-controller.ts │ ├── chatroom-controller.ts │ ├── message-controller.ts │ ├── message-reaction-controller.ts │ ├── oauth-controller.ts │ ├── reaction-controller.ts │ ├── reply-controller.ts │ ├── user-chatroom-controller.ts │ └── user-controller.ts ├── model │ ├── chatroom.ts │ ├── message-reaction.ts │ ├── message.ts │ ├── reaction.ts │ ├── reply-reaction.ts │ ├── reply.ts │ ├── section.ts │ ├── socket.ts │ ├── user-chatroom.ts │ └── user.ts ├── repository │ ├── chatroom-repository.ts │ ├── message-reaction-repository.ts │ ├── message-repository.ts │ ├── reacion-repository.ts │ ├── reply-reaction-repository.ts │ ├── reply-repository.ts │ ├── section-repository.ts │ ├── socket-repository.ts │ ├── user-chatroom-repository.ts │ └── user-repository.ts ├── router │ ├── api.ts │ ├── auth-router.ts │ ├── chatroom-router.ts │ ├── index.ts │ ├── message-reaction-router.ts │ ├── message-router.ts │ ├── oauth.ts │ ├── reaction-router.ts │ ├── reply-router.ts │ ├── user-chatroom-router.ts │ └── user-router.ts ├── seeds │ └── chatroom.ts ├── service │ ├── chatroom-service.ts │ ├── message-reaction-service.ts │ ├── message-service.ts │ ├── reaction-service.ts │ ├── reply-reaction-service.ts │ ├── reply-service.ts │ ├── socket-service.ts │ ├── user-chatroom-service.ts │ └── user-service.ts └── socket │ ├── event │ ├── chatroom-event.ts │ ├── connection-event.ts │ ├── disconnect-event.ts │ ├── message-event.ts │ ├── message-reaction-event.ts │ ├── reply-event.ts │ └── reply-rection-event.ts │ ├── handler │ ├── chatroom-handler.ts │ ├── message-handler.ts │ ├── message-reaction-handler.ts │ ├── reply-handler.ts │ ├── reply-reaction-handler.ts │ └── socket-handler.ts │ ├── index.ts │ ├── init-socket.ts │ ├── middleware │ └── jwt.ts │ └── socket.ts └── tsconfig.json /.github/ISSUE_TEMPLATE/bug_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug request 3 | about: Report a bug 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | 9 | --- 10 | 11 | ## :warning: 버그 설명 12 | 13 | 버그 설명 14 | 15 | <선택> 이미지 첨부 16 | 17 | 18 | 19 | ## 📑 체크리스트 20 | 21 | > 해결해야하는 버그 체크리스트 22 | 23 | - [ ] 체크 사항 1 24 | - [ ] 체크 사항 2 25 | - [ ] 체크 사항 3 26 | - [ ] 체크 사항 4 27 | 28 | 29 | 30 | ## 🚧 주의 사항 31 | 32 | > 버그를 해결할 때 유의깊게 살펴볼 사항 33 | 34 | - 주의 사항 1 35 | - 주의 사항 2 -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## 💁 설명 11 | 12 | 이슈 설명 13 | 14 | <선택> 이미지 첨부 15 | 16 | 17 | 18 | ## 📑 체크리스트 19 | 20 | > 구현해야하는 이슈 체크리스트 21 | 22 | - [ ] 체크 사항 1 23 | - [ ] 체크 사항 2 24 | - [ ] 체크 사항 3 25 | - [ ] 체크 사항 4 26 | 27 | 28 | 29 | ## 🚧 주의 사항 30 | 31 | > 이슈를 구현할 때 유의깊게 살펴볼 사항 32 | 33 | - 주의 사항 1 34 | - 주의 사항 2 -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## :bookmark_tabs: 제목 2 | 3 | <필수> PR 제목 4 | 5 | <선택> 이미지 첨부 6 | 7 | 8 | ## :speech_balloon: 작업 내용 9 | 10 | > 구현 내용 및 작업 했던 내역 11 | 12 | - [x] 작업 내역 1 13 | - [x] 작업 내역 2 14 | - [x] 작업 내역 3 15 | - [x] 작업 내역 4 16 | 17 | 18 | ## :construction: PR 특이 사항 19 | 20 | > PR을 볼 때 주의깊게 봐야하거나 말하고 싶은 점 21 | 22 | - 특이 사항 1 23 | - 특이 사항 2 24 | 25 | -------------------------------------------------------------------------------- /client/.babelrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "corejs": "2", 7 | "useBuiltIns": "entry" 8 | } 9 | ] 10 | ] 11 | } -------------------------------------------------------------------------------- /client/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "node": true, 5 | "es2021": true 6 | }, 7 | "ignorePatterns": ["node_modules/"], 8 | "extends": [ 9 | "eslint:recommended", 10 | "plugin:@typescript-eslint/eslint-recommended", 11 | "airbnb-base", 12 | "prettier/@typescript-eslint", 13 | "plugin:prettier/recommended" 14 | ], 15 | "globals": { 16 | "Atomics": "readonly", 17 | "SharedArrayBuffer": "readonly" 18 | }, 19 | "parser": "@typescript-eslint/parser", 20 | "parserOptions": { 21 | "ecmaFeatures": { 22 | "jsx": true 23 | }, 24 | "ecmaVersion": 12, 25 | "sourceType": "module" 26 | }, 27 | "plugins": ["react", "@typescript-eslint", "prettier"], 28 | "settings": { 29 | "import/resolver": { 30 | "node": { 31 | "extensions": [".js", ".jsx", ".ts", ".tsx"] 32 | } 33 | } 34 | }, 35 | "rules": { 36 | "prettier/prettier": [ 37 | "error", 38 | { 39 | "endOfLine": "auto" 40 | } 41 | ], 42 | "linebreak-style": 0, 43 | "no-case-declarations": 0, 44 | "no-use-before-define": "off", 45 | "import/prefer-default-export": "off", 46 | "import/no-unresolved": "off", 47 | "@typescript-eslint/no-use-before-define": ["error"], 48 | "import/extensions": [ 49 | "error", 50 | "ignorePackages", 51 | { 52 | "js": "never", 53 | "jsx": "never", 54 | "ts": "never", 55 | "tsx": "never" 56 | } 57 | ], 58 | "no-alert": "off" 59 | } 60 | } -------------------------------------------------------------------------------- /client/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 150, 3 | "tabWidth": 2, 4 | "singleQuote": true, 5 | "trailingComma": "none", 6 | "bracketSpacing": true, 7 | "semi": true, 8 | "useTabs": false, 9 | "arrowParens": "always", 10 | "endOfLine": "lf", 11 | "jsxBracketSameLine": true, 12 | "jsxSingleQuote": false 13 | } -------------------------------------------------------------------------------- /client/.storybook/main.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | "stories": [ 5 | "../src/**/*.stories.mdx", 6 | "../src/**/*.stories.@(js|jsx|ts|tsx)" 7 | ], 8 | "addons": [ 9 | '@storybook/addon-links', 10 | '@storybook/addon-essentials', 11 | '@storybook/addon-actions', 12 | '@storybook/addon-knobs', 13 | ], 14 | webpackFinal: async(config, { configType }) => { 15 | config.resolve.alias = { 16 | ...config.resolve.alias, 17 | '@components': path.resolve(__dirname, '../src/components'), 18 | '@pages': path.resolve(__dirname, '../src/pages'), 19 | '@theme': path.resolve(__dirname, '../src/common/theme'), 20 | '@utils': path.resolve(__dirname, '../src/common/utils'), 21 | '@store': path.resolve(__dirname, '../src/common/store'), 22 | '@imgs': path.resolve(__dirname, '../public/imgs'), 23 | '@socket': path.resolve(__dirname, '../src/common/socket'), 24 | '@constants': path.resolve(__dirname, '../src/common/constants') 25 | } 26 | return config; 27 | } 28 | } -------------------------------------------------------------------------------- /client/.storybook/preview.js: -------------------------------------------------------------------------------- 1 | 2 | export const parameters = { 3 | actions: { argTypesRegex: "^on[A-Z].*" }, 4 | } -------------------------------------------------------------------------------- /client/README.md: -------------------------------------------------------------------------------- 1 | # How to Start 2 | 3 | ## client 4 | 5 | ### env 6 | > /env/production.env 7 | ``` 8 | API_URL= 9 | NODE_ENV= 10 | ``` 11 | 12 | ### Start 13 | > Install 14 | ``` 15 | npm install 16 | ``` 17 | > Start 18 | ``` 19 | npm run start 20 | ``` 21 | 22 | > Storybook 23 | 24 | ``` 25 | npm run storybook 26 | ``` 27 | 28 | > Build 29 | 30 | ``` 31 | npm run build 32 | ``` 33 | 34 | -------------------------------------------------------------------------------- /client/env/.env.sample: -------------------------------------------------------------------------------- 1 | SERVER_ADDR=localhost:3000 -------------------------------------------------------------------------------- /client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project12-B-Slack-Web/f54c18ab64ce57c0aaf097ff3b7c0991359f5e52/client/public/favicon.ico -------------------------------------------------------------------------------- /client/public/imgs/channel-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project12-B-Slack-Web/f54c18ab64ce57c0aaf097ff3b7c0991359f5e52/client/public/imgs/channel-icon.png -------------------------------------------------------------------------------- /client/public/imgs/close-filled-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project12-B-Slack-Web/f54c18ab64ce57c0aaf097ff3b7c0991359f5e52/client/public/imgs/close-filled-icon.png -------------------------------------------------------------------------------- /client/public/imgs/close-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project12-B-Slack-Web/f54c18ab64ce57c0aaf097ff3b7c0991359f5e52/client/public/imgs/close-icon.png -------------------------------------------------------------------------------- /client/public/imgs/detail-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project12-B-Slack-Web/f54c18ab64ce57c0aaf097ff3b7c0991359f5e52/client/public/imgs/detail-icon.png -------------------------------------------------------------------------------- /client/public/imgs/emoji-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project12-B-Slack-Web/f54c18ab64ce57c0aaf097ff3b7c0991359f5e52/client/public/imgs/emoji-icon.png -------------------------------------------------------------------------------- /client/public/imgs/filter-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project12-B-Slack-Web/f54c18ab64ce57c0aaf097ff3b7c0991359f5e52/client/public/imgs/filter-icon.png -------------------------------------------------------------------------------- /client/public/imgs/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.png' { 2 | const value: any; 3 | export = value; 4 | } 5 | -------------------------------------------------------------------------------- /client/public/imgs/lock-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project12-B-Slack-Web/f54c18ab64ce57c0aaf097ff3b7c0991359f5e52/client/public/imgs/lock-icon.png -------------------------------------------------------------------------------- /client/public/imgs/logo-text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project12-B-Slack-Web/f54c18ab64ce57c0aaf097ff3b7c0991359f5e52/client/public/imgs/logo-text.png -------------------------------------------------------------------------------- /client/public/imgs/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project12-B-Slack-Web/f54c18ab64ce57c0aaf097ff3b7c0991359f5e52/client/public/imgs/logo.png -------------------------------------------------------------------------------- /client/public/imgs/option-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project12-B-Slack-Web/f54c18ab64ce57c0aaf097ff3b7c0991359f5e52/client/public/imgs/option-icon.png -------------------------------------------------------------------------------- /client/public/imgs/plus-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project12-B-Slack-Web/f54c18ab64ce57c0aaf097ff3b7c0991359f5e52/client/public/imgs/plus-icon.png -------------------------------------------------------------------------------- /client/public/imgs/search-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project12-B-Slack-Web/f54c18ab64ce57c0aaf097ff3b7c0991359f5e52/client/public/imgs/search-icon.png -------------------------------------------------------------------------------- /client/public/imgs/send-message-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project12-B-Slack-Web/f54c18ab64ce57c0aaf097ff3b7c0991359f5e52/client/public/imgs/send-message-icon.png -------------------------------------------------------------------------------- /client/public/imgs/sort-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project12-B-Slack-Web/f54c18ab64ce57c0aaf097ff3b7c0991359f5e52/client/public/imgs/sort-icon.png -------------------------------------------------------------------------------- /client/public/imgs/star-blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project12-B-Slack-Web/f54c18ab64ce57c0aaf097ff3b7c0991359f5e52/client/public/imgs/star-blue.png -------------------------------------------------------------------------------- /client/public/imgs/star.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project12-B-Slack-Web/f54c18ab64ce57c0aaf097ff3b7c0991359f5e52/client/public/imgs/star.png -------------------------------------------------------------------------------- /client/public/imgs/thread-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project12-B-Slack-Web/f54c18ab64ce57c0aaf097ff3b7c0991359f5e52/client/public/imgs/thread-icon.png -------------------------------------------------------------------------------- /client/public/imgs/user-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project12-B-Slack-Web/f54c18ab64ce57c0aaf097ff3b7c0991359f5e52/client/public/imgs/user-icon.png -------------------------------------------------------------------------------- /client/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Black 8 | 16 | 17 | 18 |
19 | 20 | -------------------------------------------------------------------------------- /client/src/common/constants/chat-type.ts: -------------------------------------------------------------------------------- 1 | export const ChatType = { 2 | Message: 'Message', 3 | ReplyTitle: 'ReplyTitle', 4 | Reply: 'Reply' 5 | }; 6 | 7 | export type ChatTypes = typeof ChatType[keyof typeof ChatType]; 8 | -------------------------------------------------------------------------------- /client/src/common/constants/chatroom-type.ts: -------------------------------------------------------------------------------- 1 | export const ChatroomType = { 2 | DM: 'DM', 3 | Channel: 'Channel' 4 | }; 5 | 6 | export type ChatroomTypes = typeof ChatroomType[keyof typeof ChatroomType]; 7 | -------------------------------------------------------------------------------- /client/src/common/constants/default-section-name.ts: -------------------------------------------------------------------------------- 1 | export const DefaultSectionName = { 2 | CHANNELS: 'Channels', 3 | DIRECT_MESSAGES: 'Direct Messages', 4 | STARRED: 'Starred' 5 | }; 6 | 7 | export type DefaultSectionNames = typeof DefaultSectionName[keyof typeof DefaultSectionName]; 8 | -------------------------------------------------------------------------------- /client/src/common/constants/http-status-code.ts: -------------------------------------------------------------------------------- 1 | export const HttpStatusCode = { 2 | OK: 200, 3 | CREATED: 201, 4 | NO_CONTENT: 204, 5 | BAD_REQUEST: 400, 6 | UNAUTHORIZED: 401, 7 | FORBIDDEN: 403, 8 | NOT_FOUND: 404, 9 | CONFLICT: 409, 10 | INTERNAL_SERVER_ERROR: 500 11 | }; 12 | 13 | export type HttpStatusCodes = typeof HttpStatusCode[keyof typeof HttpStatusCode]; 14 | -------------------------------------------------------------------------------- /client/src/common/constants/index.ts: -------------------------------------------------------------------------------- 1 | import { ChatType, ChatTypes } from './chat-type'; 2 | import { ChatroomType, ChatroomTypes } from './chatroom-type'; 3 | import { DefaultSectionName, DefaultSectionNames } from './default-section-name'; 4 | import { HttpStatusCode, HttpStatusCodes } from './http-status-code'; 5 | import { KeyCode, KeyCodes } from './key-code'; 6 | import { ScrollEventType, ScrollEventTypes } from './scroll-event-type'; 7 | import { Size, Sizes } from './size'; 8 | import { SortMethod, SortMethods } from './sort-method'; 9 | 10 | export { 11 | ChatType, 12 | ChatTypes, 13 | ChatroomType, 14 | ChatroomTypes, 15 | DefaultSectionName, 16 | DefaultSectionNames, 17 | HttpStatusCode, 18 | HttpStatusCodes, 19 | KeyCode, 20 | KeyCodes, 21 | ScrollEventType, 22 | ScrollEventTypes, 23 | Size, 24 | Sizes, 25 | SortMethod, 26 | SortMethods 27 | }; 28 | -------------------------------------------------------------------------------- /client/src/common/constants/key-code.ts: -------------------------------------------------------------------------------- 1 | export const KeyCode = { 2 | ENTER: 13 3 | }; 4 | 5 | export type KeyCodes = typeof KeyCode[keyof typeof KeyCode]; 6 | -------------------------------------------------------------------------------- /client/src/common/constants/scroll-event-type.ts: -------------------------------------------------------------------------------- 1 | export const ScrollEventType = { 2 | COMMON: 'Common', 3 | LOADING: 'Loading', 4 | COMPLETELOADING: 'Complete loading', 5 | INPUTTEXT: 'Input Text' 6 | }; 7 | 8 | export const THROTTLETIME = 50; 9 | 10 | export type ScrollEventTypes = typeof ScrollEventType[keyof typeof ScrollEventType]; 11 | -------------------------------------------------------------------------------- /client/src/common/constants/size.ts: -------------------------------------------------------------------------------- 1 | export const Size = { 2 | BIG: 'big', 3 | LARGE: 'large', 4 | MEDIUM: 'medium', 5 | SMALL: 'small', 6 | SUPER_SMALL: 'superSmall' 7 | }; 8 | 9 | export type Sizes = typeof Size[keyof typeof Size]; 10 | -------------------------------------------------------------------------------- /client/src/common/constants/sort-method.ts: -------------------------------------------------------------------------------- 1 | export const SortMethod = { 2 | NEWEST_CHANNEL: 'Newest channel', 3 | OLDEST_CHANNEL: 'Oldest channel', 4 | MOST_MEMBERS: 'Most members', 5 | A_TO_Z: 'A to Z', 6 | Z_TO_A: 'Z to A' 7 | }; 8 | export type SortMethods = typeof SortMethod[keyof typeof SortMethod]; 9 | -------------------------------------------------------------------------------- /client/src/common/socket/emits/chatroom.ts: -------------------------------------------------------------------------------- 1 | import { JOIN_CHATROOM, JOIN_DM, LEAVE_CHANNEL } from '@socket/types/chatroom-types'; 2 | import socket from '../socketIO'; 3 | 4 | export const joinChatroom = (chatroomId: number) => { 5 | socket.emit(JOIN_CHATROOM, { chatroomId }); 6 | }; 7 | 8 | export const joinDM = (userId: number, chatroomId: number) => { 9 | socket.emit(JOIN_DM, { userId, chatroomId }); 10 | }; 11 | 12 | export const leaveChannel = (chatroomId: number) => { 13 | socket.emit(LEAVE_CHANNEL, { chatroomId }); 14 | }; 15 | -------------------------------------------------------------------------------- /client/src/common/socket/emits/message.ts: -------------------------------------------------------------------------------- 1 | import { CREATE_MESSAGE, createMessageState } from '@socket/types/message-types'; 2 | import socket from '../socketIO'; 3 | 4 | export const createMessage = (message: createMessageState) => { 5 | socket.emit(CREATE_MESSAGE, message); 6 | }; 7 | -------------------------------------------------------------------------------- /client/src/common/socket/emits/reaction.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CREATE_MESSAGE_REACTION, 3 | DELETE_MESSAGE_REACTION, 4 | CREATE_REPLY_REACTION, 5 | DELETE_REPLY_REACTION, 6 | createMessageReactionState, 7 | deleteMessageReactionState, 8 | createReplyReactionState, 9 | deleteReplyReactionState 10 | } from '@socket/types/reaction-types'; 11 | import socket from '../socketIO'; 12 | 13 | export const createMessageReaction = (reaction: createMessageReactionState) => { 14 | socket.emit(CREATE_MESSAGE_REACTION, reaction); 15 | }; 16 | 17 | export const deleteMessageReaction = (reaction: deleteMessageReactionState) => { 18 | socket.emit(DELETE_MESSAGE_REACTION, reaction); 19 | }; 20 | 21 | export const createReplyReaction = (reaction: createReplyReactionState) => { 22 | socket.emit(CREATE_REPLY_REACTION, reaction); 23 | }; 24 | 25 | export const deleteReplyReaction = (reaction: deleteReplyReactionState) => { 26 | socket.emit(DELETE_REPLY_REACTION, reaction); 27 | }; 28 | -------------------------------------------------------------------------------- /client/src/common/socket/emits/thread.ts: -------------------------------------------------------------------------------- 1 | import { CREATE_REPLY, createThreadState } from '@socket/types/thread-types'; 2 | import socket from '../socketIO'; 3 | 4 | export const createReply = (reply: createThreadState) => { 5 | socket.emit(CREATE_REPLY, reply); 6 | }; 7 | -------------------------------------------------------------------------------- /client/src/common/socket/socketIO.ts: -------------------------------------------------------------------------------- 1 | import { io } from 'socket.io-client'; 2 | 3 | const socketURL: any = process.env.API_URL; 4 | 5 | const socket = io(socketURL, { 6 | query: { token: window.localStorage.getItem('token') }, 7 | transports: ['websocket', 'polling'] 8 | }); 9 | 10 | export default socket; 11 | -------------------------------------------------------------------------------- /client/src/common/socket/types/chatroom-types.ts: -------------------------------------------------------------------------------- 1 | export const JOIN_CHATROOM = 'join chatroom'; 2 | export const JOIN_DM = 'join DM'; 3 | export const LEAVE_CHANNEL = 'leave channel'; 4 | -------------------------------------------------------------------------------- /client/src/common/socket/types/message-types.ts: -------------------------------------------------------------------------------- 1 | export const CREATE_MESSAGE = 'create message'; 2 | 3 | export interface createMessageState { 4 | content: string; 5 | chatroomId: number | null; 6 | } 7 | -------------------------------------------------------------------------------- /client/src/common/socket/types/reaction-types.ts: -------------------------------------------------------------------------------- 1 | export const CREATE_MESSAGE_REACTION = 'create reaction'; 2 | export const DELETE_MESSAGE_REACTION = 'delete reaction'; 3 | export const CREATE_REPLY_REACTION = 'create reply reaction'; 4 | export const DELETE_REPLY_REACTION = 'delete reply reaction'; 5 | 6 | interface userInfo { 7 | displayName: string; 8 | userId: number; 9 | } 10 | 11 | export interface createMessageReactionState { 12 | messageId: number; 13 | title: string; 14 | emoji: string; 15 | } 16 | 17 | export interface deleteMessageReactionState { 18 | messageId: number; 19 | reactionId: number; 20 | } 21 | 22 | export interface createReplyReactionState { 23 | replyId: number; 24 | title: string; 25 | emoji: string; 26 | } 27 | 28 | export interface deleteReplyReactionState { 29 | replyId: number; 30 | reactionId: number; 31 | } 32 | 33 | export interface socketMessageReactionState { 34 | authors: Array; 35 | chatroomId: number; 36 | emoji: string; 37 | messageId: number; 38 | reactionId: number; 39 | title: string; 40 | } 41 | 42 | export interface socketReplyReactionState { 43 | reactionId: number; 44 | title: string; 45 | emoji: string; 46 | replyId: number; 47 | authors: Array; 48 | messageId: number; 49 | } 50 | -------------------------------------------------------------------------------- /client/src/common/socket/types/thread-types.ts: -------------------------------------------------------------------------------- 1 | export const CREATE_REPLY = 'create reply'; 2 | 3 | export interface createThreadState { 4 | content: string; 5 | messageId: number | null; 6 | } 7 | -------------------------------------------------------------------------------- /client/src/common/store/actions/channel-action.ts: -------------------------------------------------------------------------------- 1 | import { 2 | INIT_CHANNELS_ASYNC, 3 | LOAD_NEXT_CHANNELS_ASYNC, 4 | JOIN_CHANNEL_ASYNC, 5 | LEAVE_CHANNEL, 6 | LoadNextChannelState, 7 | JoinChannelState, 8 | LeaveChannelState 9 | } from '../types/channel-types'; 10 | 11 | export const initChannels = () => ({ type: INIT_CHANNELS_ASYNC }); 12 | export const loadNextChannels = (payload: LoadNextChannelState) => ({ type: LOAD_NEXT_CHANNELS_ASYNC, payload }); 13 | export const joinChannel = (payload: JoinChannelState) => ({ type: JOIN_CHANNEL_ASYNC, payload }); 14 | export const leaveChannel = (payload: LeaveChannelState) => ({ type: LEAVE_CHANNEL, payload }); 15 | -------------------------------------------------------------------------------- /client/src/common/store/actions/modal-action.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CREATE_MODAL_OPEN, 3 | CREATE_MODAL_CLOSE, 4 | CHANNEL_MODAL_OPEN, 5 | CHANNEL_MODAL_CLOSE, 6 | USERBOX_MODAL_OPEN, 7 | USERBOX_MODAL_CLOSE, 8 | PROFILE_MODAL_OPEN, 9 | PROFILE_MODAL_CLOSE, 10 | EMOJI_PICKER_OPEN, 11 | EMOJI_PICKER_CLOSE, 12 | ChannelModalState, 13 | ProfileModalState, 14 | EmojiPickerState 15 | } from '@store/types/modal-types'; 16 | 17 | export const createModalOpen = () => ({ type: CREATE_MODAL_OPEN }); 18 | export const createModalClose = () => ({ type: CREATE_MODAL_CLOSE }); 19 | export const channelModalOpen = (payload: ChannelModalState) => ({ type: CHANNEL_MODAL_OPEN, payload }); 20 | export const channelModalClose = () => ({ type: CHANNEL_MODAL_CLOSE }); 21 | export const userboxModalOpen = () => ({ type: USERBOX_MODAL_OPEN }); 22 | export const userboxModalClose = () => ({ type: USERBOX_MODAL_CLOSE }); 23 | export const profileModalOpen = (payload: ProfileModalState) => ({ type: PROFILE_MODAL_OPEN, payload }); 24 | export const profileModalClose = () => ({ type: PROFILE_MODAL_CLOSE }); 25 | export const emojiPickerOpen = (payload: EmojiPickerState) => ({ type: EMOJI_PICKER_OPEN, payload }); 26 | export const emojiPickerClose = () => ({ type: EMOJI_PICKER_CLOSE }); 27 | -------------------------------------------------------------------------------- /client/src/common/store/actions/thread-action.ts: -------------------------------------------------------------------------------- 1 | import { socketReplyReactionState } from '@socket/types/reaction-types'; 2 | import { 3 | LOAD_THREAD_ASYNC, 4 | INSERT_REPLY, 5 | LOAD_NEXT_REPLIES_ASYNC, 6 | ADD_REPLY_REACTION, 7 | DELETE_REPLY_REACTION, 8 | ReplyState, 9 | AsyncloadNextReplysState, 10 | AsyncLoadThreadState 11 | } from '@store/types/thread-types'; 12 | 13 | export const loadThread = (payload: AsyncLoadThreadState) => ({ type: LOAD_THREAD_ASYNC, payload }); 14 | export const InsertReply = (payload: ReplyState) => ({ type: INSERT_REPLY, payload }); 15 | export const loadNextReplies = (payload: AsyncloadNextReplysState) => ({ type: LOAD_NEXT_REPLIES_ASYNC, payload }); 16 | export const createReplyReaction = (payload: socketReplyReactionState) => ({ type: ADD_REPLY_REACTION, payload }); 17 | export const deleteReplyReaction = (payload: socketReplyReactionState) => ({ type: DELETE_REPLY_REACTION, payload }); 18 | -------------------------------------------------------------------------------- /client/src/common/store/actions/user-action.ts: -------------------------------------------------------------------------------- 1 | import { LOGIN_ASYNC, LOGOUT } from '../types/user-types'; 2 | 3 | export const loginAsync = () => ({ type: LOGIN_ASYNC }); 4 | export const logout = () => ({ type: LOGOUT }); 5 | -------------------------------------------------------------------------------- /client/src/common/store/index.ts: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware } from 'redux'; 2 | import createSagaMiddleware from 'redux-saga'; 3 | import { rootReducer } from './reducers'; 4 | import { rootSaga } from './sagas'; 5 | 6 | const sagaMiddleware = createSagaMiddleware(); 7 | 8 | export default createStore(rootReducer, applyMiddleware(sagaMiddleware)); 9 | 10 | sagaMiddleware.run(rootSaga); 11 | -------------------------------------------------------------------------------- /client/src/common/store/reducers/channel-reducer.ts: -------------------------------------------------------------------------------- 1 | import { ChannelState, ChannelsState, ChannelTypes, INIT_CHANNELS, LOAD_NEXT_CHANNELS, JOIN_CHANNEL, LEAVE_CHANNEL } from '../types/channel-types'; 2 | 3 | const initialState: ChannelsState = { 4 | channelCount: 0, 5 | channels: [] 6 | }; 7 | 8 | export default function channelReducer(state = initialState, action: ChannelTypes) { 9 | switch (action.type) { 10 | case INIT_CHANNELS: { 11 | return { 12 | channelCount: action.payload.channelCount, 13 | channels: action.payload.channels 14 | }; 15 | } 16 | case LOAD_NEXT_CHANNELS: { 17 | return { 18 | ...state, 19 | channels: [...state.channels, ...action.payload.channels] 20 | }; 21 | } 22 | case JOIN_CHANNEL: { 23 | const { chatroomId } = action.payload; 24 | const channels = state.channels.map((channel: ChannelState) => { 25 | if (channel.chatroomId === chatroomId) return { ...channel, isJoined: true, members: channel.members + 1 }; 26 | return channel; 27 | }); 28 | return { ...state, channels }; 29 | } 30 | case LEAVE_CHANNEL: { 31 | const { chatroomId } = action.payload; 32 | const channels = state.channels.map((channel: ChannelState) => { 33 | if (channel.chatroomId === chatroomId) return { ...channel, isJoined: false, members: channel.members - 1 }; 34 | return channel; 35 | }); 36 | 37 | return { ...state, channels }; 38 | } 39 | default: { 40 | return state; 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /client/src/common/store/reducers/index.ts: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import userReducer from './user-reducer'; 3 | import chatroomReducer from './chatroom-reducer'; 4 | import modalReducer from './modal-reducer'; 5 | import channelReducer from './channel-reducer'; 6 | import threadReducer from './thread-reducer'; 7 | 8 | export const rootReducer = combineReducers({ 9 | user: userReducer, 10 | chatroom: chatroomReducer, 11 | modal: modalReducer, 12 | channel: channelReducer, 13 | thread: threadReducer 14 | }); 15 | 16 | export type RootState = ReturnType; 17 | -------------------------------------------------------------------------------- /client/src/common/store/reducers/user-reducer.ts: -------------------------------------------------------------------------------- 1 | import { UserState, UserTypes } from '@store/types/user-types'; 2 | 3 | const initialState: UserState = { 4 | userId: null, 5 | profileUri: '', 6 | displayName: '' 7 | }; 8 | 9 | const UserReducer = (state = initialState, action: UserTypes) => { 10 | switch (action.type) { 11 | case 'LOGIN': 12 | return { ...state, ...action.payload }; 13 | case 'LOGOUT': 14 | return { ...state, ...initialState }; 15 | default: 16 | return state; 17 | } 18 | }; 19 | 20 | export default UserReducer; 21 | -------------------------------------------------------------------------------- /client/src/common/store/sagas/channel-saga.ts: -------------------------------------------------------------------------------- 1 | import { call, put, takeEvery } from 'redux-saga/effects'; 2 | import API from '@utils/api'; 3 | import { 4 | AsyncLoadNextChannels, 5 | AsyncJoinChannel, 6 | INIT_CHANNELS, 7 | INIT_CHANNELS_ASYNC, 8 | JOIN_CHANNEL, 9 | JOIN_CHANNEL_ASYNC, 10 | LOAD_NEXT_CHANNELS, 11 | LOAD_NEXT_CHANNELS_ASYNC 12 | } from '../types/channel-types'; 13 | import { ADD_CHANNEL, PICK_CHANNEL_ASYNC } from '../types/chatroom-types'; 14 | 15 | function* initChannelsSaga() { 16 | try { 17 | const { channels, channelCount } = yield call(API.getChannels); 18 | yield put({ type: INIT_CHANNELS, payload: { channelCount, channels } }); 19 | } catch (e) { 20 | console.log(e); 21 | } 22 | } 23 | 24 | function* loadNextChannels(action: AsyncLoadNextChannels) { 25 | try { 26 | const { title } = action.payload; 27 | const nextChannels = yield call(API.getNextChannels, title); 28 | yield put({ type: LOAD_NEXT_CHANNELS, payload: { channels: nextChannels } }); 29 | } catch (e) { 30 | console.log(e); 31 | } 32 | } 33 | 34 | function* joinChannel(action: AsyncJoinChannel) { 35 | try { 36 | const { chatroomId } = action.payload; 37 | yield call(API.joinChannel, chatroomId); 38 | yield put({ type: JOIN_CHANNEL, payload: { chatroomId } }); 39 | const chatroom = yield call(API.getChatroom, chatroomId); 40 | const { chatType, isPrivate, title } = chatroom; 41 | const payload = { chatroomId, chatType, isPrivate, title }; 42 | yield put({ type: ADD_CHANNEL, payload }); 43 | yield put({ type: PICK_CHANNEL_ASYNC, payload: { selectedChatroomId: chatroomId } }); 44 | } catch (e) { 45 | console.log(e); 46 | } 47 | } 48 | 49 | export function* channelSaga() { 50 | yield takeEvery(INIT_CHANNELS_ASYNC, initChannelsSaga); 51 | yield takeEvery(LOAD_NEXT_CHANNELS_ASYNC, loadNextChannels); 52 | yield takeEvery(JOIN_CHANNEL_ASYNC, joinChannel); 53 | } 54 | -------------------------------------------------------------------------------- /client/src/common/store/sagas/index.ts: -------------------------------------------------------------------------------- 1 | import { all } from 'redux-saga/effects'; 2 | import { chatroomSaga } from './chatroom-saga'; 3 | import { userSaga } from './user-saga'; 4 | import { channelSaga } from './channel-saga'; 5 | import { threadSaga } from './thread-saga'; 6 | 7 | export function* rootSaga() { 8 | yield all([chatroomSaga(), userSaga(), channelSaga(), threadSaga()]); 9 | } 10 | -------------------------------------------------------------------------------- /client/src/common/store/sagas/thread-saga.ts: -------------------------------------------------------------------------------- 1 | import { call, put, takeEvery } from 'redux-saga/effects'; 2 | import API from '@utils/api'; 3 | import { 4 | LOAD_THREAD, 5 | LOAD_THREAD_ASYNC, 6 | LOAD_NEXT_REPLIES, 7 | LOAD_NEXT_REPLIES_ASYNC, 8 | AsyncLoadThread, 9 | AsyncLoadNextThreadReplys 10 | } from '@store/types/thread-types'; 11 | 12 | function* loadThreadSaga(action: AsyncLoadThread) { 13 | try { 14 | const { messageId } = action.payload; 15 | const payload = yield call(API.getThread, messageId); 16 | const { title } = yield call(API.getChatroom, payload.message.chatroom.chatroomId); 17 | yield put({ type: LOAD_THREAD, payload: { ...payload, title } }); 18 | } catch (e) { 19 | console.log(e); 20 | } 21 | } 22 | 23 | function* loadNextReplies(action: AsyncLoadNextThreadReplys) { 24 | try { 25 | const { offsetReply, messageId } = action.payload; 26 | const offsetId = offsetReply.replyId; 27 | const { replies } = yield call(API.getNextReplies, messageId, offsetId); 28 | yield put({ type: LOAD_NEXT_REPLIES, payload: { replies } }); 29 | } catch (e) { 30 | console.log(e); 31 | } 32 | } 33 | 34 | export function* threadSaga() { 35 | yield takeEvery(LOAD_THREAD_ASYNC, loadThreadSaga); 36 | yield takeEvery(LOAD_NEXT_REPLIES_ASYNC, loadNextReplies); 37 | } 38 | -------------------------------------------------------------------------------- /client/src/common/store/sagas/user-saga.ts: -------------------------------------------------------------------------------- 1 | import { call, put, takeEvery } from 'redux-saga/effects'; 2 | import API from '@utils/api'; 3 | import { LOGIN, LOGIN_ASYNC } from '../types/user-types'; 4 | 5 | function* userLoginSaga() { 6 | try { 7 | const payload = yield call(API.getUserInfo); 8 | yield put({ type: LOGIN, payload }); 9 | } catch (e) { 10 | console.log(e); 11 | } 12 | } 13 | 14 | export function* userSaga() { 15 | yield takeEvery(LOGIN_ASYNC, userLoginSaga); 16 | } 17 | -------------------------------------------------------------------------------- /client/src/common/store/types/channel-types.ts: -------------------------------------------------------------------------------- 1 | export const INIT_CHANNELS = 'INIT_CHANNELS'; 2 | export const INIT_CHANNELS_ASYNC = 'INIT_CHANNELS_ASYNC'; 3 | export const LOAD_NEXT_CHANNELS = 'LOAD_NEXT_CHANNELS'; 4 | export const LOAD_NEXT_CHANNELS_ASYNC = 'LOAD_NEXT_CHANNELS_ASYNC'; 5 | export const JOIN_CHANNEL = 'JOIN_CHANNEL'; 6 | export const JOIN_CHANNEL_ASYNC = 'JOIN_CHANNEL_ASYNC'; 7 | export const LEAVE_CHANNEL = 'LEAVE_CHANNEL'; 8 | 9 | export interface ChannelState { 10 | chatroomId: number; 11 | title: string; 12 | description?: string; 13 | isPrivate: boolean; 14 | members: number; 15 | isJoined: boolean; 16 | } 17 | 18 | export interface ChannelsState { 19 | channelCount: number; 20 | channels: Array; 21 | } 22 | 23 | export interface JoinChannelState { 24 | chatroomId: number; 25 | } 26 | 27 | export interface LeaveChannelState { 28 | chatroomId: number; 29 | } 30 | 31 | export interface LoadNextChannelState { 32 | title: string; 33 | } 34 | 35 | export interface AsyncLoadNextChannels { 36 | type: typeof LOAD_NEXT_CHANNELS_ASYNC; 37 | payload: LoadNextChannelState; 38 | } 39 | 40 | export interface AsyncJoinChannel { 41 | type: typeof JOIN_CHANNEL_ASYNC; 42 | payload: JoinChannelState; 43 | } 44 | 45 | interface InitChannelsAction { 46 | type: typeof INIT_CHANNELS; 47 | payload: ChannelsState; 48 | } 49 | 50 | interface LoadNextChannelsAction { 51 | type: typeof LOAD_NEXT_CHANNELS; 52 | payload: ChannelsState; 53 | } 54 | 55 | interface JoinChannelAction { 56 | type: typeof JOIN_CHANNEL; 57 | payload: JoinChannelState; 58 | } 59 | 60 | interface LeaveChannelAction { 61 | type: typeof LEAVE_CHANNEL; 62 | payload: LeaveChannelState; 63 | } 64 | 65 | export type ChannelTypes = InitChannelsAction | LoadNextChannelsAction | JoinChannelAction | LeaveChannelAction; 66 | -------------------------------------------------------------------------------- /client/src/common/store/types/message-types.ts: -------------------------------------------------------------------------------- 1 | import { UserState } from './user-types'; 2 | import { ReactionsState } from './reactions-type'; 3 | import { ChatroomThreadState } from './chatroom-types'; 4 | 5 | export interface MessageState { 6 | messageId: number; 7 | createdAt: Date; 8 | updatedAt: Date; 9 | user: UserState; 10 | messageReactions: Array; 11 | thread: ChatroomThreadState; 12 | } 13 | 14 | export interface MessagesState { 15 | messages: Array; 16 | } 17 | -------------------------------------------------------------------------------- /client/src/common/store/types/reactions-type.ts: -------------------------------------------------------------------------------- 1 | export interface ReactionsState { 2 | reactionId: number; 3 | title: string; 4 | reactionCount: number; 5 | reactionDisplayNames: Array; 6 | emoji: string; 7 | } 8 | 9 | export interface ReplyReactionsState { 10 | reactionId: number; 11 | title: string; 12 | reactionCount: number; 13 | replyDisplayNames: Array; 14 | emoji: string; 15 | } 16 | -------------------------------------------------------------------------------- /client/src/common/store/types/user-types.ts: -------------------------------------------------------------------------------- 1 | export const LOGIN = 'LOGIN'; 2 | export const LOGIN_ASYNC = 'LOGIN_ASYNC'; 3 | export const LOGOUT = 'LOGOUT'; 4 | 5 | export interface UserState { 6 | userId: number | null; 7 | profileUri: string; 8 | displayName: string; 9 | } 10 | 11 | interface UserLoginAction { 12 | type: typeof LOGIN; 13 | payload: UserState; 14 | } 15 | 16 | interface UserLogoutAction { 17 | type: typeof LOGOUT; 18 | } 19 | 20 | export type UserTypes = UserLoginAction | UserLogoutAction; 21 | -------------------------------------------------------------------------------- /client/src/common/theme/color.ts: -------------------------------------------------------------------------------- 1 | const color = { 2 | primary: 'black', 3 | secondary: '#1a1d21', 4 | tertiary: 'white', 5 | quaternary: 'rgb(246, 246, 246)', 6 | text_primary: 'rgb(198, 199, 200)', 7 | text_secondary: 'white', 8 | text_tertiary: 'rgba(147,147,147,1)', 9 | text_quaternary: 'rgba(83,83,83,1)', 10 | text_quinary: 'rgb(160, 158, 169)', 11 | text_senary: 'rgb(98, 100, 136)', 12 | text_septenary: '#186b8e', 13 | border_primary: '#e2e2e2', 14 | border_secondary: '#c6c6c6', 15 | border_tertiary: '#1d9bd1', 16 | light_primary: '#33e600', 17 | box_shadow_primary: 'rgba(27, 31, 35, 0.075)', 18 | box_shadow_secondary: 'rgba(3, 102, 214, 0.3)', 19 | box_shadow_tertiary: 'rgba(255, 255, 255, 0.1)', 20 | modal_bg_outer_primary: 'rgba(0, 0, 0, 0.5)', 21 | modal_bg_inner_primary: 'white', 22 | modal_bg_inner_secondary: 'rgba(248, 248, 248, 1)', 23 | selected_chatroom: '#0576b9', 24 | hover_primary: 'rgb(248, 248, 248)', 25 | hover_secondary: 'rgb(18, 100, 163)', 26 | button_secondary: '#017a5a', 27 | button_tertiary: 'rgba(221,221,221,1)', 28 | sidebar_bg: '#1a1e22', 29 | sidebar_border: '#313537', 30 | emoji_bg: 'rgb(232, 245, 250)' 31 | }; 32 | 33 | export default color; 34 | -------------------------------------------------------------------------------- /client/src/common/theme/index.ts: -------------------------------------------------------------------------------- 1 | import color from './color'; 2 | 3 | export { color }; 4 | -------------------------------------------------------------------------------- /client/src/common/utils/auth.ts: -------------------------------------------------------------------------------- 1 | export const checkAuthToToken = () => { 2 | return !!localStorage.getItem('token'); 3 | }; 4 | -------------------------------------------------------------------------------- /client/src/common/utils/blockPage.ts: -------------------------------------------------------------------------------- 1 | import { auth } from '@utils/index'; 2 | 3 | const blockPage = () => { 4 | if (auth.checkAuthToToken() && window.location.pathname === '/login') { 5 | window.location.href = '/'; 6 | } 7 | if (!auth.checkAuthToToken() && window.location.pathname !== '/login') { 8 | window.location.href = '/login'; 9 | } 10 | }; 11 | 12 | export { blockPage }; 13 | -------------------------------------------------------------------------------- /client/src/common/utils/index.ts: -------------------------------------------------------------------------------- 1 | import API from './api'; 2 | import * as uriParser from './uriParser'; 3 | import { blockPage } from './blockPage'; 4 | import registerToken from './registerToken'; 5 | import { logout } from './logout'; 6 | import * as auth from './auth'; 7 | 8 | export { API, uriParser, blockPage, registerToken, logout, auth }; 9 | -------------------------------------------------------------------------------- /client/src/common/utils/logout.ts: -------------------------------------------------------------------------------- 1 | export const logout = () => { 2 | window.localStorage.removeItem('token'); 3 | window.location.href = '/login'; 4 | }; 5 | -------------------------------------------------------------------------------- /client/src/common/utils/modal.ts: -------------------------------------------------------------------------------- 1 | import { useDispatch } from 'react-redux'; 2 | import { profileModalOpen } from '@store/actions/modal-action'; 3 | 4 | interface User { 5 | userId: number; 6 | profileUri: string; 7 | displayName: string; 8 | } 9 | 10 | export const openProfileModal = (user: User) => { 11 | const dispatch = useDispatch(); 12 | return (e: any) => { 13 | const x = window.pageXOffset + e.target.getBoundingClientRect().left; 14 | const y = window.pageYOffset + e.target.getBoundingClientRect().top; 15 | const { userId, profileUri, displayName } = user; 16 | dispatch(profileModalOpen({ x, y, userId, profileUri, displayName })); 17 | }; 18 | }; 19 | -------------------------------------------------------------------------------- /client/src/common/utils/registerToken.ts: -------------------------------------------------------------------------------- 1 | import { API, uriParser, auth } from '@utils/index'; 2 | 3 | const registerToken = async () => { 4 | if (uriParser.isExistParseCodeUrl()) { 5 | const code = uriParser.getCode(); 6 | const token = await API.getToken(code); 7 | if (token) { 8 | localStorage.setItem('token', token); 9 | window.location.href = '/client/1'; 10 | } 11 | } else if (auth.checkAuthToToken()) window.location.href = '/client/1'; 12 | else window.location.href = '/login'; 13 | }; 14 | 15 | export default registerToken; 16 | -------------------------------------------------------------------------------- /client/src/common/utils/scroll.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-param-reassign */ 2 | export const moveScrollToTheBottom = (El: any) => { 3 | const { scrollHeight, clientHeight } = El.current; 4 | const maxScrollTop = scrollHeight - clientHeight; 5 | El.current.scrollTop = maxScrollTop > 0 ? maxScrollTop : 0; 6 | }; 7 | -------------------------------------------------------------------------------- /client/src/common/utils/time.ts: -------------------------------------------------------------------------------- 1 | const untisOfTime = { 2 | seconds: 1, 3 | minutes: 60, 4 | hours: 60 * 60, 5 | days: 24 * 60 * 60, 6 | months: ((365 + 365 + 365 + 365 + 366) / 5 / 12) * 24 * 60 * 60, 7 | years: ((365 + 365 + 365 + 365 + 366) / 5) * 24 * 60 * 60 8 | }; 9 | 10 | const timeAgo = (date: Date): string => { 11 | const timeDiff = (new Date().getTime() - date.getTime()) / 1000; 12 | 13 | const minutes = timeDiff / untisOfTime.minutes; 14 | const hours = timeDiff / untisOfTime.hours; 15 | const days = timeDiff / untisOfTime.days; 16 | const months = timeDiff / untisOfTime.months; 17 | const years = timeDiff / untisOfTime.years; 18 | 19 | if (minutes < 1) return `1 minute ago`; 20 | if (minutes < 60) return `${Math.round(minutes)} minute ago`; 21 | if (hours < 24) return `${Math.round(hours)} hours ago`; 22 | if (days < 31) return `${Math.round(days)} days ago`; 23 | if (months < 12) return `${Math.round(months)} months ago`; 24 | return `${Math.round(years)} years ago`; 25 | }; 26 | 27 | const getTimeConversionValue = (date: Date): string => { 28 | const time = new Date(date); 29 | return time.toLocaleString('en-US', { hour: 'numeric', minute: 'numeric', hour12: true }); 30 | }; 31 | 32 | export { timeAgo, getTimeConversionValue }; 33 | -------------------------------------------------------------------------------- /client/src/common/utils/uriParser.ts: -------------------------------------------------------------------------------- 1 | export const isExistParseCodeUrl = () => { 2 | const pattern = new RegExp(/^\?code=\w+$/); 3 | return pattern.test(window.location.search); 4 | }; 5 | 6 | export const getCode = () => { 7 | const pattern = new RegExp(/[^(?code=)]\w+/); 8 | const code = pattern.exec(window.location.search); 9 | return code ? code[0] : null; 10 | }; 11 | 12 | export const getChatroomId = () => { 13 | const chatroomUrlpattern = new RegExp(/^\/client\/[0-9]+(\/thread\/[0-9]+)*$/); 14 | if (chatroomUrlpattern.test(window.location.pathname)) { 15 | const pattern = new RegExp(/[0-9]+/g); 16 | const code = pattern.exec(window.location.pathname); 17 | return code ? Number(code[0]) : null; 18 | } 19 | return null; 20 | }; 21 | 22 | export const getThreadId = () => { 23 | const threadUrlpattern = new RegExp(/^\/client\/[0-9]+(\/thread\/[0-9]+)$/); 24 | if (threadUrlpattern.test(window.location.pathname)) { 25 | const pattern = new RegExp(/[0-9]+$/g); 26 | const code = pattern.exec(window.location.pathname); 27 | return code ? Number(code[0]) : null; 28 | } 29 | return null; 30 | }; 31 | -------------------------------------------------------------------------------- /client/src/components/atoms/ActiveLight/ActiveLight.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Story, Meta } from '@storybook/react/types-6-0'; 3 | import { Size } from '@constants/index'; 4 | import { ActiveLight, ActiveLightProps } from './ActiveLight'; 5 | 6 | export default { 7 | title: 'atom/ActiveLight', 8 | component: ActiveLight 9 | } as Meta; 10 | 11 | const Template: Story = (args) => ; 12 | 13 | export const LargeActiveLight = Template.bind({}); 14 | LargeActiveLight.args = { 15 | size: Size.LARGE 16 | }; 17 | 18 | export const MediumActiveLight = Template.bind({}); 19 | MediumActiveLight.args = { 20 | size: Size.MEDIUM 21 | }; 22 | 23 | export const SmallActiveLight = Template.bind({}); 24 | SmallActiveLight.args = { 25 | size: Size.SMALL 26 | }; 27 | -------------------------------------------------------------------------------- /client/src/components/atoms/ActiveLight/ActiveLight.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import { color } from '@theme/index'; 4 | import { Size, Sizes } from '@constants/index'; 5 | 6 | interface ActiveLightProps { 7 | size?: Sizes; 8 | isActive?: boolean; 9 | } 10 | 11 | const ActiveLightContainter = styled.div` 12 | width: ${({ size }) => { 13 | if (size === Size.LARGE) return '0.5rem'; 14 | if (size === Size.MEDIUM) return '0.4rem'; 15 | return '0.3rem'; 16 | }}; 17 | height: ${({ size }) => { 18 | if (size === Size.LARGE) return '0.5rem'; 19 | if (size === Size.MEDIUM) return '0.4rem'; 20 | return '0.3rem'; 21 | }}; 22 | border-width: ${({ size }) => { 23 | if (size === Size.LARGE) return '0.17rem'; 24 | if (size === Size.MEDIUM) return '0.13rem'; 25 | return '0.1rem'; 26 | }}; 27 | border-color: ${({ isActive }) => (isActive ? color.light_primary : color.border_secondary)}; 28 | border-style: solid; 29 | border-radius: 1rem; 30 | background-color: ${({ isActive }) => (isActive ? color.light_primary : color.primary)}; 31 | `; 32 | 33 | const ActiveLight: React.FC = ({ size = Size.MEDIUM, isActive = true, ...props }) => { 34 | return ; 35 | }; 36 | 37 | export { ActiveLight, ActiveLightProps }; 38 | -------------------------------------------------------------------------------- /client/src/components/atoms/Button/Button.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Story, Meta } from '@storybook/react/types-6-0'; 3 | import { Button, ButtonProps } from './Button'; 4 | 5 | export default { 6 | title: 'atom/Button', 7 | component: Button, 8 | argTypes: { 9 | backgroundColor: { control: 'color' }, 10 | borderColor: { control: 'color' }, 11 | fontColor: { control: 'color' } 12 | } 13 | } as Meta; 14 | 15 | const Template: Story = (args) => 24 | ) : ( 25 | 34 | )} 35 | 36 | ); 37 | }; 38 | 39 | export { BrowsePageChannelButton, BrowsePageChannelButtonProps }; 40 | -------------------------------------------------------------------------------- /client/src/components/molecules/BrowsePageChannelHeader/BrowsePageChannelHeader.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Story, Meta } from '@storybook/react/types-6-0'; 3 | import { BrowsePageChannelHeader, BrowsePageChannelHeaderProps } from './BrowsePageChannelHeader'; 4 | 5 | export default { 6 | title: 'molecules/BrowsePageChannelHeader', 7 | component: BrowsePageChannelHeader 8 | } as Meta; 9 | 10 | const Template: Story = (args) => ; 11 | 12 | export const BlackBrowsePageChannelHeader = Template.bind({}); 13 | BlackBrowsePageChannelHeader.args = { 14 | title: 'notice', 15 | isPrivate: true 16 | }; 17 | -------------------------------------------------------------------------------- /client/src/components/molecules/BrowsePageChannelHeader/BrowsePageChannelHeader.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import { Icon, Text } from '@components/atoms'; 4 | import ChannelIcon from '@imgs/channel-icon.png'; 5 | import LockIcon from '@imgs/lock-icon.png'; 6 | import { color } from '@theme/index'; 7 | import { Size } from '@constants/index'; 8 | 9 | interface BrowsePageChannelHeaderProps { 10 | title: string; 11 | isPrivate?: boolean; 12 | } 13 | 14 | const BrowsePageChannelHeaderWrap = styled.div` 15 | display: flex; 16 | p { 17 | margin-left: 0.3rem; 18 | } 19 | `; 20 | 21 | const BrowsePageChannelHeader: React.FC = ({ title, isPrivate, ...props }) => { 22 | return ( 23 | 24 | 25 | 26 | {title} 27 | 28 | 29 | ); 30 | }; 31 | 32 | export { BrowsePageChannelHeader, BrowsePageChannelHeaderProps }; 33 | -------------------------------------------------------------------------------- /client/src/components/molecules/BrowsePageControls/BrowsePageControls.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Story, Meta } from '@storybook/react/types-6-0'; 3 | import { BrowsePageControls, BrowsePageControlsProps } from './BrowsePageControls'; 4 | 5 | export default { 6 | title: 'molecules/BrowsePageControls', 7 | component: BrowsePageControls 8 | } as Meta; 9 | 10 | const Template: Story = (args) => ; 11 | 12 | export const BlackBrowsePageControls = Template.bind({}); 13 | BlackBrowsePageControls.args = { 14 | channelCount: 193 15 | }; 16 | -------------------------------------------------------------------------------- /client/src/components/molecules/BrowsePageControls/BrowsePageControls.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import styled from 'styled-components'; 3 | import { Text } from '@components/atoms'; 4 | import { color } from '@theme/index'; 5 | import { WhiteButtonWithIcon } from '@components/molecules'; 6 | import SortIcon from '@imgs/sort-icon.png'; 7 | import FilterIcon from '@imgs/filter-icon.png'; 8 | import { Size, SortMethod, SortMethods } from '@constants/index'; 9 | 10 | interface BrowsePageControlsProps { 11 | channelCount: number; 12 | } 13 | 14 | const BrowsePageControlsWrap = styled.div` 15 | display: flex; 16 | justify-content: space-between; 17 | align-items: center; 18 | padding-top: 0.4rem; 19 | padding-bottom: 1rem; 20 | margin-left: 1.5rem; 21 | margin-right: 2.5rem; 22 | border-bottom: 0.5px solid ${color.border_secondary}; 23 | `; 24 | 25 | const BrowsePageControlsButtonWrap = styled.div` 26 | display: flex; 27 | `; 28 | 29 | const BrowsePageControls: React.FC = ({ channelCount, ...props }) => { 30 | const [sortMethod, setSortMethod] = useState(SortMethod.A_TO_Z); 31 | 32 | const handlingSortButton = () => {}; 33 | const handlingFilterButton = () => {}; 34 | 35 | return ( 36 | 37 | 38 | {`${channelCount} channels`} 39 | 40 | 41 | 42 | {`Sort: ${sortMethod}`} 43 | 44 | 45 | Filter 46 | 47 | 48 | 49 | ); 50 | }; 51 | 52 | export { BrowsePageControls, BrowsePageControlsProps }; 53 | -------------------------------------------------------------------------------- /client/src/components/molecules/BrowsePageSearchBar/BrowsePageSearchBar.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Story, Meta } from '@storybook/react/types-6-0'; 3 | import { BrowsePageSearchBar, BrowsePageSearchBarProps } from './BrowsePageSearchBar'; 4 | 5 | export default { 6 | title: 'molecules/BrowsePageSearchBar', 7 | component: BrowsePageSearchBar 8 | } as Meta; 9 | 10 | const Template: Story = (args) => ; 11 | 12 | export const BlackBrowsePageSearchBar = Template.bind({}); 13 | BlackBrowsePageSearchBar.args = {}; 14 | -------------------------------------------------------------------------------- /client/src/components/molecules/Channel/Channel.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Story, Meta } from '@storybook/react/types-6-0'; 3 | import { Channel, ChannelProps } from './Channel'; 4 | 5 | export default { 6 | title: 'molecules/Channel', 7 | component: Channel 8 | } as Meta; 9 | 10 | const Template: Story = (args) => ; 11 | 12 | export const BlackChannel = Template.bind({}); 13 | BlackChannel.args = { 14 | children: '5주-그룹-프로젝트-슬랙' 15 | }; 16 | -------------------------------------------------------------------------------- /client/src/components/molecules/ChannelModal/ChannelModal.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Story, Meta } from '@storybook/react/types-6-0'; 3 | import { ChannelModal, ChannelModalProps } from './ChannelModal'; 4 | 5 | export default { 6 | title: 'molecules/ChannelModal', 7 | component: ChannelModal 8 | } as Meta; 9 | 10 | const Template: Story = (args) => ; 11 | 12 | export const BlackChannelModal = Template.bind({}); 13 | BlackChannelModal.args = {}; 14 | -------------------------------------------------------------------------------- /client/src/components/molecules/ChannelModal/ChannelModal.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { DropMenuBox, DropMenuItem } from '@components/atoms'; 3 | import { useDispatch, useSelector } from 'react-redux'; 4 | import { useHistory } from 'react-router-dom'; 5 | import { createModalOpen, channelModalClose } from '@store/actions/modal-action'; 6 | import { resetSelectedChannel } from '@store/actions/chatroom-action'; 7 | import styled from 'styled-components'; 8 | 9 | interface ChannelModalProps {} 10 | 11 | const DropMenuItemContainer = styled.div` 12 | padding: 1rem 0rem; 13 | `; 14 | 15 | const ChannelModal: React.FC = ({ ...props }) => { 16 | const history = useHistory(); 17 | const dispatch = useDispatch(); 18 | const { x, y } = useSelector((store: any) => store.modal.channelModal); 19 | 20 | const isOpen = useSelector((store: any) => store.modal.channelModal.isOpen); 21 | 22 | const handlingCloseModal = () => { 23 | dispatch(channelModalClose()); 24 | }; 25 | const handlingBrowseChannelsClick = () => { 26 | if (window.location.pathname !== `/channel-browser`) history.push(`/channel-browser`); 27 | dispatch(resetSelectedChannel()); 28 | 29 | dispatch(channelModalClose()); 30 | }; 31 | const handlingCreateChannelClick = () => { 32 | dispatch(createModalOpen()); 33 | }; 34 | return ( 35 | <> 36 | {isOpen ? ( 37 | handlingCloseModal()} {...props}> 38 | 39 | Browse channels 40 | Create a channel 41 | 42 | 43 | ) : null} 44 | 45 | ); 46 | }; 47 | 48 | export { ChannelModal, ChannelModalProps }; 49 | -------------------------------------------------------------------------------- /client/src/components/molecules/DM/DM.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Story, Meta } from '@storybook/react/types-6-0'; 3 | import { DM, DMProps } from './DM'; 4 | 5 | export default { 6 | title: 'molecules/DM', 7 | component: DM 8 | } as Meta; 9 | 10 | const Template: Story = (args) => ; 11 | 12 | export const BlackDM = Template.bind({}); 13 | BlackDM.args = { 14 | children: 'J030_김도호' 15 | }; 16 | -------------------------------------------------------------------------------- /client/src/components/molecules/DM/DM.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import { Text } from '@components/atoms'; 4 | import { ActiveProfileImg } from '@components/molecules'; 5 | import { useHistory } from 'react-router-dom'; 6 | import { color } from '@theme/index'; 7 | import { useDispatch } from 'react-redux'; 8 | import { pickChannel } from '@store/actions/chatroom-action'; 9 | import { getThreadId } from '@utils/uriParser'; 10 | import { Size } from '@constants/index'; 11 | 12 | interface DMProps { 13 | children: React.ReactChild; 14 | isSelect?: boolean; 15 | src?: string; 16 | chatroomId: number; 17 | } 18 | 19 | const DMContainter = styled.div<{ isSelect: boolean }>` 20 | display: flex; 21 | align-items: center; 22 | padding: 0.4rem 1rem; 23 | cursor: pointer; 24 | ${(props) => (props.isSelect ? `background-color: ${color.selected_chatroom}` : `&:hover { background-color: ${color.primary};}`)} 25 | `; 26 | 27 | const TextWrap = styled.div` 28 | margin-left: 1rem; 29 | width: -webkit-fill-available; 30 | `; 31 | 32 | const DM: React.FC = ({ children, src, chatroomId, isSelect = false, ...props }) => { 33 | const history = useHistory(); 34 | const dispatch = useDispatch(); 35 | 36 | const handlingClick = () => { 37 | const threadId = getThreadId(); 38 | const pathname = threadId ? `/client/${chatroomId}/thread/${threadId}` : `/client/${chatroomId}`; 39 | if (window.location.pathname !== pathname) history.push(pathname); 40 | dispatch(pickChannel({ selectedChatroomId: chatroomId })); 41 | }; 42 | 43 | return ( 44 | 45 | 46 | 47 | 48 | 49 | 50 | ); 51 | }; 52 | 53 | export { DM, DMProps }; 54 | -------------------------------------------------------------------------------- /client/src/components/molecules/EmojiBox/EmojiBox.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Story, Meta } from '@storybook/react/types-6-0'; 3 | import { EmojiBox, EmojiBoxProps } from './EmojiBox'; 4 | 5 | export default { 6 | title: 'molecules/EmojiBox', 7 | component: EmojiBox 8 | } as Meta; 9 | 10 | const Template: Story = (args) => ; 11 | 12 | export const BlackEmojiBox = Template.bind({}); 13 | BlackEmojiBox.args = { 14 | emoji: '✔', 15 | number: 1 16 | }; 17 | 18 | export const HeartEmojiBox = Template.bind({}); 19 | HeartEmojiBox.args = { 20 | emoji: '❤', 21 | number: 1 22 | }; 23 | -------------------------------------------------------------------------------- /client/src/components/molecules/EmojiBox/EmojiBox.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-expressions */ 2 | import React, { useState } from 'react'; 3 | import styled from 'styled-components'; 4 | import { Emoji } from '@components/atoms'; 5 | import { color } from '@theme/index'; 6 | 7 | interface EmojiBoxProps { 8 | emoji: string; 9 | active?: boolean; 10 | number: number; 11 | reactionId: number; 12 | title: string; 13 | createReaction: any; 14 | deleteReaction: any; 15 | } 16 | 17 | const EmojiBoxContainer = styled.div<{ isActive: boolean }>` 18 | display: flex; 19 | justify-content: space-evenly; 20 | align-items: center; 21 | padding-bottom: 0.2rem; 22 | width: 3rem; 23 | background-color: ${(props) => (props.isActive ? color.emoji_bg : color.quaternary)}; 24 | border-radius: 1rem; 25 | cursor: pointer; 26 | margin-right: 0.3rem; 27 | box-shadow: 0 0 0 1px inset ${(props) => (props.isActive ? color.border_tertiary : color.quaternary)}; 28 | 29 | ${(props) => 30 | props.isActive 31 | ? null 32 | : `&:hover { 33 | background-color: ${color.tertiary}; 34 | box-shadow: 0 0 0 1px inset;`}} 35 | `; 36 | 37 | const EmojiBoxText = styled.p` 38 | font-size: 0.9rem; 39 | margin: 0; 40 | `; 41 | 42 | const EmojiBox: React.FC = ({ reactionId, title, createReaction, deleteReaction, active = false, number, emoji, ...props }) => { 43 | const [isActive, setActive] = useState(active); 44 | 45 | const handlingClick = () => { 46 | isActive ? deleteReaction(reactionId) : createReaction(title, emoji); 47 | setActive(!isActive); 48 | }; 49 | 50 | return ( 51 | 52 | 53 | {number} 54 | 55 | ); 56 | }; 57 | 58 | export { EmojiBox, EmojiBoxProps }; 59 | -------------------------------------------------------------------------------- /client/src/components/molecules/EmojiPicker/EmojiPicker.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Story, Meta } from '@storybook/react/types-6-0'; 3 | import { EmojiPicker, EmojiPickerProps } from './EmojiPicker'; 4 | 5 | export default { 6 | title: 'atom/EmojiPicker', 7 | component: EmojiPicker 8 | } as Meta; 9 | 10 | const Template: Story = (args) => ; 11 | 12 | export const BlackEmojiPicker = Template.bind({}); 13 | BlackEmojiPicker.args = { 14 | onEmojiClick: () => {} 15 | }; 16 | -------------------------------------------------------------------------------- /client/src/components/molecules/EmojiPicker/EmojiPicker.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Picker from 'emoji-picker-react'; 3 | import { DropMenuBox } from '@components/atoms'; 4 | import { useDispatch, useSelector } from 'react-redux'; 5 | import { emojiPickerClose } from '@store/actions/modal-action'; 6 | import { createMessageReaction, createReplyReaction } from '@socket/emits/reaction'; 7 | import { ChatType } from '@constants/index'; 8 | 9 | interface EmojiPickerProps {} 10 | 11 | const EmojiPicker: React.FC = () => { 12 | const dispatch = useDispatch(); 13 | const { x, y, chatId, isOpen, type } = useSelector((store: any) => store.modal.emojiPicker); 14 | 15 | const onEmojiClick = (e: any, emojiObject: any) => { 16 | const { names, emoji } = emojiObject; 17 | if (type === ChatType.Reply) createReplyReaction({ replyId: chatId, title: names[0], emoji }); 18 | else createMessageReaction({ messageId: chatId, title: names[0], emoji }); 19 | dispatch(emojiPickerClose()); 20 | }; 21 | 22 | const handlingCloseModal = () => { 23 | dispatch(emojiPickerClose()); 24 | }; 25 | 26 | return ( 27 | <> 28 | {isOpen && ( 29 | 30 | 31 | 32 | )} 33 | 34 | ); 35 | }; 36 | 37 | export { EmojiPicker, EmojiPickerProps }; 38 | -------------------------------------------------------------------------------- /client/src/components/molecules/GithubLoginButton/GithubLoginButton.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Story, Meta } from '@storybook/react/types-6-0'; 3 | import { GithubLoginButton, GithubLoginButtonProps } from './GithubLoginButton'; 4 | 5 | export default { 6 | title: 'molecules/GithubLoginButton', 7 | component: GithubLoginButton 8 | } as Meta; 9 | 10 | const Template: Story = (args) => ; 11 | 12 | export const BlackGithubLoginButton = Template.bind({}); 13 | BlackGithubLoginButton.args = {}; 14 | -------------------------------------------------------------------------------- /client/src/components/molecules/GithubLoginButton/GithubLoginButton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Button, Icon, Text } from '@components/atoms'; 3 | import styled from 'styled-components'; 4 | import { API } from '@utils/index'; 5 | import { color } from '@theme/index'; 6 | 7 | interface GithubLoginButtonProps {} 8 | 9 | const TextWrap = styled.div` 10 | margin-left: 1rem; 11 | `; 12 | 13 | const handlingGithubLoginButton = async () => { 14 | await API.oauthLogin(); 15 | }; 16 | 17 | const GithubLoginButton: React.FC = ({ ...props }) => { 18 | return ( 19 | 25 | ); 26 | }; 27 | 28 | export { GithubLoginButton, GithubLoginButtonProps }; 29 | -------------------------------------------------------------------------------- /client/src/components/molecules/HoverIcon/HoverIcon.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Story, Meta } from '@storybook/react/types-6-0'; 3 | import Plus from '@imgs/plus-icon.png'; 4 | import { Size } from '@constants/index'; 5 | import { HoverIcon, HoverIconProps } from './HoverIcon'; 6 | 7 | export default { 8 | title: 'molecules/HoverIcon', 9 | component: HoverIcon 10 | } as Meta; 11 | 12 | const Template: Story = (args) => ; 13 | 14 | export const MediumHoverIcon = Template.bind({}); 15 | MediumHoverIcon.args = { 16 | src: Plus, 17 | size: Size.MEDIUM 18 | }; 19 | 20 | export const LargeHoverIcon = Template.bind({}); 21 | LargeHoverIcon.args = { 22 | src: Plus, 23 | size: Size.LARGE 24 | }; 25 | -------------------------------------------------------------------------------- /client/src/components/molecules/HoverIcon/HoverIcon.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | import React from 'react'; 3 | import styled from 'styled-components'; 4 | import { Icon } from '@components/atoms'; 5 | import { color } from '@theme/index'; 6 | import { Sizes, Size } from '@constants/index'; 7 | 8 | interface HoverIconProps { 9 | size: Sizes; 10 | src?: string; 11 | onClick?: any; 12 | } 13 | 14 | const StyledHoverIcon = styled.div` 15 | display: flex; 16 | justify-content: center; 17 | align-items: center; 18 | width: ${({ size }) => { 19 | if (size === Size.LARGE) return '2.4rem'; 20 | if (size === Size.MEDIUM) return '2.0rem'; 21 | return '1.8rem'; 22 | }}; 23 | height: ${({ size }) => { 24 | if (size === Size.LARGE) return '2.4rem'; 25 | if (size === Size.MEDIUM) return '2.0rem'; 26 | return '1.8rem'; 27 | }}; 28 | border-radius: 0.4rem; 29 | cursor: pointer; 30 | &:hover { 31 | background-color: ${color.hover_primary}; 32 | } 33 | `; 34 | 35 | const HoverIcon: React.FC = ({ size = Size.MEDIUM, src, ...props }) => { 36 | return ( 37 | 38 | 39 | 40 | ); 41 | }; 42 | 43 | export { HoverIcon, HoverIconProps }; 44 | -------------------------------------------------------------------------------- /client/src/components/molecules/InputMessage/InputMessage.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Story, Meta } from '@storybook/react/types-6-0'; 3 | import { InputMessage, InputMessageProps } from './InputMessage'; 4 | 5 | export default { 6 | title: 'molecules/InputMessage', 7 | component: InputMessage 8 | } as Meta; 9 | 10 | const Template: Story = (args) => ; 11 | 12 | export const MessageInputMessage = Template.bind({}); 13 | MessageInputMessage.args = { 14 | isThread: false, 15 | title: '5주-그룹-프로젝트-슬랙b' 16 | }; 17 | 18 | export const ThreadInputMessage = Template.bind({}); 19 | ThreadInputMessage.args = { 20 | isThread: true 21 | }; 22 | -------------------------------------------------------------------------------- /client/src/components/molecules/InputMessage/InputMessage.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import styled from 'styled-components'; 3 | import { Input } from '@components/atoms'; 4 | import { SendMessageButton } from '@components/molecules'; 5 | import { color } from '@theme/index'; 6 | import { createMessage } from '@socket/emits/message'; 7 | import { ChatType, ScrollEventType } from '@constants/index'; 8 | 9 | interface InputMessageProps { 10 | isThread?: boolean; 11 | title: string; 12 | setEventType: any; 13 | chatroomId: number; 14 | } 15 | 16 | const InputMessageContainer = styled.div` 17 | display: flex; 18 | justify-content: space-between; 19 | align-items: center; 20 | border: 1px solid ${color.primary}; 21 | padding: 0.5rem 0rem; 22 | width: 100%; 23 | max-height: 70%; 24 | border-radius: 0.3rem; 25 | `; 26 | 27 | const InputWrap = styled.div` 28 | margin-left: 1rem; 29 | width: 70%; 30 | `; 31 | 32 | const ButtonWrap = styled.div` 33 | margin-right: 1rem; 34 | `; 35 | 36 | const InputMessage: React.FC = ({ children, title, isThread, chatroomId, setEventType, ...props }) => { 37 | const [content, setContent] = useState(''); 38 | 39 | const sendMessage = () => { 40 | if (content === '') return; 41 | setEventType(ScrollEventType.INPUTTEXT); 42 | createMessage({ content, chatroomId }); 43 | setContent(''); 44 | }; 45 | 46 | return ( 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | ); 56 | }; 57 | 58 | export { InputMessage, InputMessageProps }; 59 | -------------------------------------------------------------------------------- /client/src/components/molecules/InputReply/InputReply.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Story, Meta } from '@storybook/react/types-6-0'; 3 | import { InputReply, InputReplyProps } from './InputReply'; 4 | 5 | export default { 6 | title: 'molecules/InputReply', 7 | component: InputReply 8 | } as Meta; 9 | 10 | const Template: Story = (args) => ; 11 | 12 | export const ThreadInputReply = Template.bind({}); 13 | ThreadInputReply.args = { 14 | isThread: true 15 | }; 16 | -------------------------------------------------------------------------------- /client/src/components/molecules/InputReply/InputReply.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import styled from 'styled-components'; 3 | import { Input } from '@components/atoms'; 4 | import { color } from '@theme/index'; 5 | import { SendMessageButton } from '@components/molecules'; 6 | import { createReply } from '@socket/emits/thread'; 7 | import { ScrollEventType, ChatType } from '@constants/index'; 8 | 9 | interface InputReplyProps { 10 | isThread?: boolean; 11 | messageId: number | null; 12 | setEventType: any; 13 | } 14 | 15 | const InputReplyContainer = styled.div` 16 | display: flex; 17 | justify-content: space-between; 18 | align-items: center; 19 | border: 1px solid ${color.primary}; 20 | padding: 0.5rem 0rem; 21 | width: 100%; 22 | max-height: 70%; 23 | border-radius: 0.3rem; 24 | `; 25 | 26 | const InputWrap = styled.div` 27 | margin-left: 1rem; 28 | width: 70%; 29 | `; 30 | 31 | const ButtonWrap = styled.div` 32 | margin-right: 1rem; 33 | `; 34 | 35 | const InputReply: React.FC = ({ children, messageId, isThread, setEventType, ...props }) => { 36 | const [content, setContent] = useState(''); 37 | const sendReply = () => { 38 | if (content === '') return; 39 | setEventType(ScrollEventType.INPUTTEXT); 40 | createReply({ content, messageId }); 41 | setContent(''); 42 | }; 43 | 44 | return ( 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | ); 54 | }; 55 | 56 | export { InputReply, InputReplyProps }; 57 | -------------------------------------------------------------------------------- /client/src/components/molecules/Message/Message.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Story, Meta } from '@storybook/react/types-6-0'; 3 | import { Message, MessageProps } from './Message'; 4 | 5 | export default { 6 | title: 'molecules/Message', 7 | component: Message 8 | } as Meta; 9 | 10 | const Template: Story = (args) => ; 11 | 12 | export const BlackMessage = Template.bind({}); 13 | BlackMessage.args = { 14 | src: 'https://avatars2.githubusercontent.com/u/33643752?s=460&u=a9a75e7c6922a23eb365b258a60499bbb9a9c655&v=4', 15 | author: 'J030_김도호', 16 | content: 17 | '안녕하세요. 김도호입니다. 안녕하세요. 김도호입니다. 안녕하세요. 김도호입니다. 안녕하세요. 김도호입니다. 안녕하세요. 김도호입니다. 안녕하세요. 김도호입니다. 안녕하세요. 김도호입니다. 안녕하세요. 김도호입니다. 안녕하세요. 김도호입니다. ' 18 | }; 19 | -------------------------------------------------------------------------------- /client/src/components/molecules/MessageReplyBar/MessageReplyBar.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Story, Meta } from '@storybook/react/types-6-0'; 3 | import { MessageReplyBar, MessageReplyBarProps } from './MessageReplyBar'; 4 | 5 | export default { 6 | title: 'molecules/MessageReplyBar', 7 | component: MessageReplyBar 8 | } as Meta; 9 | 10 | const Template: Story = (args) => ; 11 | 12 | const handlingMessageReplyBarClick = () => {}; 13 | 14 | export const OneMessageReplyBar = Template.bind({}); 15 | OneMessageReplyBar.args = { 16 | profileImgs: ['https://avatars1.githubusercontent.com/u/59037261?s=64&v=4'], 17 | replyCount: 1, 18 | lastRepliedTime: new Date('2020-12-07 23:48:52.547160'), 19 | onClick: handlingMessageReplyBarClick 20 | }; 21 | 22 | export const ThreeMessageReplyBar = Template.bind({}); 23 | ThreeMessageReplyBar.args = { 24 | profileImgs: [ 25 | 'https://avatars3.githubusercontent.com/u/33643752?s=64&v=4', 26 | 'https://avatars1.githubusercontent.com/u/59037261?s=64&v=4', 27 | 'https://avatars0.githubusercontent.com/u/37091190?s=64&v=4' 28 | ], 29 | replyCount: 3, 30 | lastRepliedTime: new Date('2020-12-08 08:32:51.536510'), 31 | onClick: handlingMessageReplyBarClick 32 | }; 33 | -------------------------------------------------------------------------------- /client/src/components/molecules/MessageReplyBar/MessageReplyBar.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import { ProfileImg, Text } from '@components/atoms'; 4 | import { color } from '@theme/index'; 5 | import { timeAgo } from '@utils/time'; 6 | import { Size } from '@constants/index'; 7 | 8 | interface MessageReplyBarProps { 9 | profileImgs: Array; 10 | replyCount: number; 11 | lastRepliedTime: Date; 12 | onClick?: () => void; 13 | } 14 | 15 | const MessageReplyBarWrap = styled.div` 16 | display: flex; 17 | margin-top: 0.2rem; 18 | padding: 0.2rem; 19 | border-radius: 0.3rem; 20 | cursor: pointer; 21 | &:hover { 22 | background-color: ${color.tertiary}; 23 | } 24 | `; 25 | 26 | const ProfileImgWrap = styled.div` 27 | margin: 0rem 0.05rem; 28 | `; 29 | 30 | const ReplyCountWrap = styled.div` 31 | display: flex; 32 | margin: 0rem 0.5rem; 33 | p:hover { 34 | text-decoration: underline; 35 | } 36 | `; 37 | 38 | const MessageReplyBar: React.FC = ({ profileImgs, replyCount, lastRepliedTime, onClick, ...props }) => { 39 | const profileNum = profileImgs.length >= 5 ? 5 : profileImgs.length; 40 | const removedOverlapProfileImgs = Array.from(new Set(profileImgs)); 41 | const createProfileImg = removedOverlapProfileImgs.slice(0, profileNum).map((profileImg) => ( 42 | 43 | 44 | 45 | )); 46 | 47 | return ( 48 | 49 | {createProfileImg} 50 | 51 | 52 | {`${replyCount} ${replyCount === 1 ? 'reply' : 'replies'}`} 53 | 54 | 55 | 56 | {timeAgo(lastRepliedTime)} 57 | 58 | 59 | ); 60 | }; 61 | 62 | export { MessageReplyBar, MessageReplyBarProps }; 63 | -------------------------------------------------------------------------------- /client/src/components/molecules/ProfileModal/ProfileModal.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Story, Meta } from '@storybook/react/types-6-0'; 3 | import { ProfileModal, ProfileModalProps } from './ProfileModal'; 4 | 5 | export default { 6 | title: 'molecules/ProfileModal', 7 | component: ProfileModal 8 | } as Meta; 9 | 10 | const Template: Story = (args) => ; 11 | 12 | export const BlackProfileModal = Template.bind({}); 13 | BlackProfileModal.args = { 14 | userId: 1, 15 | profileUri: 'https://avatars2.githubusercontent.com/u/59037261?s=460&u=7b7a0a2f151c1f49c5bc8068d4d6a5bf50c94c7b&v=4', 16 | displayName: 'profornnan' 17 | }; 18 | -------------------------------------------------------------------------------- /client/src/components/molecules/Section/Section.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Story, Meta } from '@storybook/react/types-6-0'; 3 | import { Channel, DM } from '@components/molecules'; 4 | import { Section, SectionProps } from './Section'; 5 | 6 | export default { 7 | title: 'molecules/Section', 8 | component: Section 9 | } as Meta; 10 | 11 | const mockChannelChildren = ['5주-그룹-프로젝트-슬랙', 'boost-ajae', '어몽어스'].map((item) => { 12 | return {item}; 13 | }); 14 | 15 | const mockDMChildren = ['J003_강동훈', 'J030_김도호', 'J211_탁성건'].map((item) => { 16 | return {item}; 17 | }); 18 | 19 | const Template: Story = (args) =>
; 20 | 21 | export const ChannelSection = Template.bind({}); 22 | ChannelSection.args = { 23 | sectionName: 'Channels', 24 | children: mockChannelChildren 25 | }; 26 | 27 | export const DMSection = Template.bind({}); 28 | DMSection.args = { 29 | sectionName: 'Direct Messages', 30 | children: mockDMChildren 31 | }; 32 | -------------------------------------------------------------------------------- /client/src/components/molecules/Section/Section.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import styled from 'styled-components'; 3 | import { DefaultSectionName } from '@constants/index'; 4 | import { AddChannelButton } from '../AddChannelButton/AddChannelButton'; 5 | 6 | interface SectionProps { 7 | children: React.ReactNode; 8 | sectionName: string; 9 | isSelect?: boolean; 10 | } 11 | 12 | const SectionWrap = styled.div` 13 | position: relative; 14 | `; 15 | 16 | const AddChannelButtonWrap = styled.div` 17 | position: absolute; 18 | top: 0; 19 | right: 0.5rem; 20 | `; 21 | 22 | const StyledSection = styled.details` 23 | color: rgb(198, 199, 200); 24 | font-size: 1rem; 25 | margin-bottom: 1rem; 26 | `; 27 | 28 | const Summary = styled.summary` 29 | outline: none; 30 | cursor: pointer; 31 | margin-bottom: 0.3rem; 32 | `; 33 | 34 | const Section: React.FC = ({ children, sectionName = 'Section', isSelect = false, ...props }) => { 35 | const [isHover, setHover] = useState(false); 36 | 37 | const onMouseEnter = () => { 38 | setHover(true); 39 | }; 40 | 41 | const onMouseLeave = () => { 42 | setHover(false); 43 | }; 44 | 45 | return ( 46 | 47 | {isHover && DefaultSectionName.CHANNELS === sectionName && ( 48 | 49 | 50 | 51 | )} 52 | 53 | 54 | {sectionName} 55 | 56 | {children} 57 | 58 | 59 | ); 60 | }; 61 | 62 | export { Section, SectionProps }; 63 | -------------------------------------------------------------------------------- /client/src/components/molecules/SendMessageButton/SendMessageButton.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Story, Meta } from '@storybook/react/types-6-0'; 3 | import { SendMessageButton, SendMessageButtonProps } from './SendMessageButton'; 4 | 5 | export default { 6 | title: 'molecules/SendMessageButton', 7 | component: SendMessageButton 8 | } as Meta; 9 | 10 | const Template: Story = (args) => ; 11 | 12 | export const ActiveMessageButton = Template.bind({}); 13 | ActiveMessageButton.args = { 14 | isActive: true 15 | }; 16 | 17 | export const InactiveMessageButton = Template.bind({}); 18 | InactiveMessageButton.args = { 19 | isActive: false 20 | }; 21 | -------------------------------------------------------------------------------- /client/src/components/molecules/SendMessageButton/SendMessageButton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import { Icon } from '@components/atoms'; 4 | import SendMessageIcon from '@imgs/send-message-icon.png'; 5 | import { color } from '@theme/index'; 6 | import { Size } from '@constants/index'; 7 | 8 | interface SendMessageButtonProps { 9 | isActive: boolean; 10 | sendMessage: () => void; 11 | } 12 | 13 | const SendMessageButtonContainer = styled.div<{ isActive: boolean }>` 14 | display: flex; 15 | justify-content: center; 16 | align-items: baseline; 17 | width: 1.3rem; 18 | padding: 0.3rem; 19 | border-radius: 0.3rem; 20 | cursor: pointer; 21 | ${(props) => (props.isActive ? `background-color: ${color.button_secondary}` : '')} 22 | `; 23 | 24 | const SendMessageButton: React.FC = ({ isActive, sendMessage, ...props }) => { 25 | const handlingClick = () => { 26 | sendMessage(); 27 | }; 28 | return ( 29 | 30 | 31 | 32 | ); 33 | }; 34 | 35 | export { SendMessageButton, SendMessageButtonProps }; 36 | -------------------------------------------------------------------------------- /client/src/components/molecules/UserBox/UserBox.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Story, Meta } from '@storybook/react/types-6-0'; 3 | import { UserBox, UserBoxProps } from './UserBox'; 4 | 5 | export default { 6 | title: 'molecules/UserBox', 7 | component: UserBox 8 | } as Meta; 9 | 10 | const Template: Story = (args) => ; 11 | 12 | export const OneUserBox = Template.bind({}); 13 | OneUserBox.args = { 14 | member: [{ name: '김도호', profileUri: 'https://avatars2.githubusercontent.com/u/33643752?s=460&u=a9a75e7c6922a23eb365b258a60499bbb9a9c655&v=4' }] 15 | }; 16 | 17 | export const TwoUserBox = Template.bind({}); 18 | TwoUserBox.args = { 19 | member: [ 20 | { name: '강동훈', profileUri: 'https://avatars0.githubusercontent.com/u/37091190?s=400&u=d358f361db0c43c0fccdcbd31de5ded89efe0169&v=4' }, 21 | { name: '김도호', profileUri: 'https://avatars2.githubusercontent.com/u/33643752?s=460&u=a9a75e7c6922a23eb365b258a60499bbb9a9c655&v=4' } 22 | ] 23 | }; 24 | 25 | export const ThreeUserBox = Template.bind({}); 26 | ThreeUserBox.args = { 27 | member: [ 28 | { name: '강동훈', profileUri: 'https://avatars0.githubusercontent.com/u/37091190?s=400&u=d358f361db0c43c0fccdcbd31de5ded89efe0169&v=4' }, 29 | { name: '김도호', profileUri: 'https://avatars2.githubusercontent.com/u/33643752?s=460&u=a9a75e7c6922a23eb365b258a60499bbb9a9c655&v=4' }, 30 | { name: '탁성건', profileUri: 'https://avatars2.githubusercontent.com/u/59037261?s=460&u=7b7a0a2f151c1f49c5bc8068d4d6a5bf50c94c7b&v=4' } 31 | ] 32 | }; 33 | -------------------------------------------------------------------------------- /client/src/components/molecules/UserBox/UserBox.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import { ProfileImg } from '@components/atoms'; 4 | import { color } from '@theme/index'; 5 | import { userboxModalOpen } from '@store/actions/modal-action'; 6 | import { useDispatch } from 'react-redux'; 7 | 8 | interface UserBoxProps { 9 | member: Array; 10 | } 11 | 12 | const UserBoxWrap = styled.div` 13 | display: flex; 14 | align-items: center; 15 | padding-right: 1rem; 16 | border-radius: 0.2rem; 17 | &:hover { 18 | background-color: ${color.hover_primary}; 19 | } 20 | cursor: pointer; 21 | `; 22 | 23 | const ProfileImgWrap = styled.div` 24 | border: 2px solid white; 25 | border-radius: 0.5rem; 26 | margin-left: -0.4rem; 27 | `; 28 | 29 | const Text = styled.div` 30 | margin-left: 0.7rem; 31 | color: ${color.text_senary}; 32 | font-weight: 600; 33 | `; 34 | 35 | const UserBox: React.FC = ({ member, ...props }) => { 36 | const dispatch = useDispatch(); 37 | const memberNum = member.length; 38 | const displayMembers: object[] = new Array(3); 39 | const loopValue = memberNum > 3 ? 3 : memberNum; 40 | 41 | for (let i = 0; i < loopValue; i += 1) { 42 | displayMembers.push({ id: member[i].userId, profileUri: member[i].profileUri }); 43 | } 44 | 45 | const createProfileImg = displayMembers.map((Member: any) => ( 46 | 47 | 48 | 49 | )); 50 | 51 | return ( 52 | dispatch(userboxModalOpen())} {...props}> 53 | {createProfileImg} 54 | {memberNum} 55 | 56 | ); 57 | }; 58 | 59 | export { UserBox, UserBoxProps }; 60 | -------------------------------------------------------------------------------- /client/src/components/molecules/UserBoxModalSearchBar/UserBoxModalSearchBar.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Story, Meta } from '@storybook/react/types-6-0'; 3 | import { UserBoxModalSearchBar, UserBoxModalSearchBarProps } from './UserBoxModalSearchBar'; 4 | 5 | export default { 6 | title: 'molecules/UserBoxModalSearchBar', 7 | component: UserBoxModalSearchBar 8 | } as Meta; 9 | 10 | const Template: Story = (args) => ; 11 | 12 | export const BlackUserBoxModalSearchBar = Template.bind({}); 13 | BlackUserBoxModalSearchBar.args = {}; 14 | -------------------------------------------------------------------------------- /client/src/components/molecules/UserBoxModalUserItem/UserBoxModalUserItem.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Story, Meta } from '@storybook/react/types-6-0'; 3 | import { UserBoxModalUserItem, UserBoxModalUserItemProps } from './UserBoxModalUserItem'; 4 | 5 | export default { 6 | title: 'molecules/UserBoxModalUserItem', 7 | component: UserBoxModalUserItem 8 | } as Meta; 9 | 10 | const Template: Story = (args) => ; 11 | 12 | export const BlackUserBoxModalUserItem = Template.bind({}); 13 | BlackUserBoxModalUserItem.args = { 14 | user: { 15 | displayName: 'J030_김도호', 16 | profileUri: 'https://avatars2.githubusercontent.com/u/33643752?s=460&u=a9a75e7c6922a23eb365b258a60499bbb9a9c655&v=4', 17 | userId: 1 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /client/src/components/molecules/UserBoxModalUserItem/UserBoxModalUserItem.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import { ProfileImg, Text } from '@components/atoms'; 4 | import { color } from '@theme/index'; 5 | import { openProfileModal } from '@utils/modal'; 6 | import { Size } from '@constants/index'; 7 | 8 | interface User { 9 | displayName: string; 10 | profileUri: string; 11 | userId: number; 12 | } 13 | 14 | interface UserBoxModalUserItemProps { 15 | user: User; 16 | } 17 | 18 | const UserBoxModalUserItemContainer = styled.div` 19 | display: flex; 20 | align-items: center; 21 | padding: 0.5rem 0; 22 | &:hover { 23 | background-color: ${color.hover_primary}; 24 | } 25 | `; 26 | 27 | const ProfileImgWrap = styled.div` 28 | cursor: pointer; 29 | `; 30 | 31 | const TextWrap = styled.div` 32 | margin-left: 0.5rem; 33 | cursor: pointer; 34 | `; 35 | 36 | const UserBoxModalUserItem: React.FC = ({ user, ...props }) => { 37 | return ( 38 | 39 | 40 | 41 | 42 | 43 | 44 | {user.displayName} 45 | 46 | 47 | 48 | ); 49 | }; 50 | 51 | export { UserBoxModalUserItem, UserBoxModalUserItemProps }; 52 | -------------------------------------------------------------------------------- /client/src/components/molecules/WhiteButtonWithIcon/WhiteButtonWithIcon.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Story, Meta } from '@storybook/react/types-6-0'; 3 | import SortIcon from '@imgs/sort-icon.png'; 4 | import { WhiteButtonWithIcon, WhiteButtonWithIconProps } from './WhiteButtonWithIcon'; 5 | 6 | export default { 7 | title: 'molecules/WhiteButtonWithIcon', 8 | component: WhiteButtonWithIcon 9 | } as Meta; 10 | 11 | const onClick = () => {}; 12 | 13 | const Template: Story = (args) => ; 14 | 15 | export const BlackWhiteButtonWithIcon = Template.bind({}); 16 | BlackWhiteButtonWithIcon.args = { 17 | children: 'Sort', 18 | iconSrc: SortIcon, 19 | onClick 20 | }; 21 | -------------------------------------------------------------------------------- /client/src/components/molecules/WhiteButtonWithIcon/WhiteButtonWithIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import { Button, Text, Icon } from '@components/atoms'; 4 | import { color } from '@theme/index'; 5 | import { Size } from '@constants/index'; 6 | 7 | interface WhiteButtonWithIconProps { 8 | children: React.ReactChild; 9 | iconSrc: string; 10 | onClick: () => void; 11 | } 12 | 13 | const WhiteButtonWithIconWrap = styled.div` 14 | display: flex; 15 | width: fit-content; 16 | p { 17 | margin-left: 0.3rem; 18 | } 19 | `; 20 | 21 | const WhiteButtonWithIcon: React.FC = ({ children, iconSrc, onClick, ...props }) => { 22 | return ( 23 | 24 | 36 | 37 | ); 38 | }; 39 | 40 | export { WhiteButtonWithIcon, WhiteButtonWithIconProps }; 41 | -------------------------------------------------------------------------------- /client/src/components/organisms/BrowsePageChannel/BrowsePageChannel.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Story, Meta } from '@storybook/react/types-6-0'; 3 | import { BrowsePageChannel, BrowsePageChannelProps } from './BrowsePageChannel'; 4 | 5 | export default { 6 | title: 'organisms/BrowsePageChannel', 7 | component: BrowsePageChannel 8 | } as Meta; 9 | 10 | const Template: Story = (args) => ; 11 | 12 | export const BlackBrowsePageChannel = Template.bind({}); 13 | BlackBrowsePageChannel.args = { 14 | chatroomId: 1, 15 | title: 'notice', 16 | isJoined: true, 17 | members: 4, 18 | description: '공지사항을 안내하는 채널', 19 | isPrivate: true 20 | }; 21 | -------------------------------------------------------------------------------- /client/src/components/organisms/BrowsePageChannelList/BrowsePageChannelList.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Story, Meta } from '@storybook/react/types-6-0'; 3 | import { BrowsePageChannelList, BrowsePageChannelListProps } from './BrowsePageChannelList'; 4 | 5 | export default { 6 | title: 'organisms/BrowsePageChannelList', 7 | component: BrowsePageChannelList 8 | } as Meta; 9 | 10 | const Template: Story = (args) => ; 11 | 12 | export const BlackBrowsePageChannelList = Template.bind({}); 13 | BlackBrowsePageChannelList.args = { 14 | channels: [ 15 | { 16 | chatroomId: 1, 17 | title: 'notice', 18 | description: '공지사항을 안내하는 채널', 19 | isPrivate: false, 20 | members: 110, 21 | isJoined: true 22 | }, 23 | { 24 | chatroomId: 2, 25 | title: '질의응답', 26 | isPrivate: false, 27 | members: 10, 28 | isJoined: false 29 | }, 30 | { 31 | chatroomId: 3, 32 | title: 'black', 33 | description: 'black', 34 | isPrivate: true, 35 | members: 3, 36 | isJoined: true 37 | } 38 | ] 39 | }; 40 | -------------------------------------------------------------------------------- /client/src/components/organisms/BrowsePageChannelList/BrowsePageChannelList.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import styled from 'styled-components'; 3 | import { BrowsePageChannel } from '@components/organisms'; 4 | import { useDispatch } from 'react-redux'; 5 | import { ChannelState } from '@store/types/channel-types'; 6 | import { loadNextChannels } from '@store/actions/channel-action'; 7 | 8 | interface BrowsePageChannelListProps { 9 | channels: Array; 10 | } 11 | 12 | const BrowsePageChannelListContainter = styled.div` 13 | display: flex; 14 | flex-direction: column; 15 | overflow-y: scroll; 16 | padding: 0rem 1.5rem; 17 | height: 71%; 18 | `; 19 | 20 | const BrowsePageChannelList: React.FC = ({ channels, ...props }) => { 21 | const dispatch = useDispatch(); 22 | const [lastRequestChannelTitle, setLastRequestChannelTitle] = useState(''); 23 | const onScrollHandler = (e: any) => { 24 | const title: string | null = channels[channels.length - 1]?.title; 25 | if (e.target.scrollTop >= ((e.target.scrollHeight - e.target.clientHeight) * 2) / 3) { 26 | if (title === lastRequestChannelTitle) return; 27 | dispatch(loadNextChannels({ title })); 28 | setLastRequestChannelTitle(title); 29 | } 30 | }; 31 | 32 | const createMessages = () => { 33 | return channels.map((channel: any) => ( 34 | 43 | )); 44 | }; 45 | 46 | return ( 47 | 48 | {createMessages()} 49 | 50 | ); 51 | }; 52 | 53 | export { BrowsePageChannelList, BrowsePageChannelListProps }; 54 | -------------------------------------------------------------------------------- /client/src/components/organisms/BrowsePageHeader/BrowsePageHeader.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Story, Meta } from '@storybook/react/types-6-0'; 3 | import { BrowsePageHeader, BrowsePageHeaderProps } from './BrowsePageHeader'; 4 | 5 | export default { 6 | title: 'organisms/BrowsePageHeader', 7 | component: BrowsePageHeader 8 | } as Meta; 9 | 10 | const onClick = () => {}; 11 | 12 | const Template: Story = (args) => ; 13 | 14 | export const BlackBrowsePageHeader = Template.bind({}); 15 | BlackBrowsePageHeader.args = { 16 | onClick 17 | }; 18 | -------------------------------------------------------------------------------- /client/src/components/organisms/BrowsePageHeader/BrowsePageHeader.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import { Text, Button } from '@components/atoms'; 4 | import { color } from '@theme/index'; 5 | import { Size } from '@constants/index'; 6 | 7 | interface BrowsePageHeaderProps { 8 | onClick?: () => void; 9 | } 10 | 11 | const BrowsePageHeaderWrap = styled.div` 12 | display: flex; 13 | justify-content: space-between; 14 | align-items: center; 15 | height: 10%; 16 | width: 100%; 17 | box-shadow: 0 2px 2px -2px ${color.border_primary}; 18 | background-color: ${color.tertiary}; 19 | z-index: 2; 20 | `; 21 | 22 | const ContentWrap = styled.div` 23 | display: flex; 24 | align-items: center; 25 | padding: 0rem 1.6rem; 26 | `; 27 | 28 | const BrowsePageHeader: React.FC = ({ onClick, ...props }) => { 29 | return ( 30 | 31 | 32 | 33 | Channel browser 34 | 35 | 36 | 37 | 40 | 41 | 42 | ); 43 | }; 44 | 45 | export { BrowsePageHeader, BrowsePageHeaderProps }; 46 | -------------------------------------------------------------------------------- /client/src/components/organisms/ChatroomBody/ChatroomBody.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Story, Meta } from '@storybook/react/types-6-0'; 3 | import { ChatroomBody, ChatroomBodyProps } from './ChatroomBody'; 4 | 5 | export default { 6 | title: 'organisms/ChatroomBody', 7 | component: ChatroomBody 8 | } as Meta; 9 | 10 | const Template: Story = (args) => ; 11 | 12 | export const BlackChatroomBody = Template.bind({}); 13 | BlackChatroomBody.args = { 14 | title: '5주-그룹-프로젝트-슬랙b' 15 | }; 16 | -------------------------------------------------------------------------------- /client/src/components/organisms/ChatroomHeader/ChatroomHeader.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Story, Meta } from '@storybook/react/types-6-0'; 3 | import { ChatroomHeader, ChatroomHeaderProps } from './ChatroomHeader'; 4 | 5 | export default { 6 | title: 'organisms/ChatroomHeader', 7 | component: ChatroomHeader 8 | } as Meta; 9 | 10 | const Template: Story = (args) => ; 11 | 12 | export const BlackChatroomHeader = Template.bind({}); 13 | BlackChatroomHeader.args = { 14 | title: '5주-그룹-프로젝트-슬랙b' 15 | }; 16 | -------------------------------------------------------------------------------- /client/src/components/organisms/CreateChannelModal/CreateChannelModal.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Story, Meta } from '@storybook/react/types-6-0'; 3 | import { CreateChannelModal, CreateChannelModalProps } from './CreateChannelModal'; 4 | 5 | export default { 6 | title: 'organisms/CreateChannelModal', 7 | component: CreateChannelModal 8 | } as Meta; 9 | 10 | const Template: Story = (args) => ; 11 | 12 | export const BlackCreateChannelModal = Template.bind({}); 13 | BlackCreateChannelModal.args = {}; 14 | -------------------------------------------------------------------------------- /client/src/components/organisms/Header/Header.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Story, Meta } from '@storybook/react/types-6-0'; 3 | import { Header, HeaderProps } from './Header'; 4 | 5 | export default { 6 | title: 'organisms/Header', 7 | component: Header 8 | } as Meta; 9 | 10 | const Template: Story = (args) =>
; 11 | 12 | export const BlackHeader = Template.bind({}); 13 | BlackHeader.args = {}; 14 | -------------------------------------------------------------------------------- /client/src/components/organisms/Header/Header.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import styled from 'styled-components'; 3 | import { LogoImg } from '@components/atoms'; 4 | import { ActiveProfileImg } from '@components/molecules'; 5 | import { color } from '@theme/index'; 6 | import { useDispatch, useSelector } from 'react-redux'; 7 | import { RootState } from '@store/reducers'; 8 | import { loginAsync } from '@store/actions/user-action'; 9 | 10 | interface HeaderProps {} 11 | 12 | const HeaderContainter = styled.div` 13 | display: flex; 14 | justify-content: space-between; 15 | align-items: center; 16 | padding: 0rem 1rem; 17 | min-height: 6%; 18 | background-color: ${color.primary}; 19 | box-shadow: 0 1px 0 0 ${color.box_shadow_tertiary}; 20 | `; 21 | 22 | const Header: React.FC = ({ ...props }) => { 23 | const { profileUri } = useSelector((state: RootState) => state.user); 24 | const dispatch = useDispatch(); 25 | useEffect(() => { 26 | dispatch(loginAsync()); 27 | }, []); 28 | 29 | return ( 30 | 31 | 32 | 33 | 34 | ); 35 | }; 36 | 37 | export { Header, HeaderProps }; 38 | -------------------------------------------------------------------------------- /client/src/components/organisms/LoginForm/LoginForm.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Story, Meta } from '@storybook/react/types-6-0'; 3 | import { LoginForm, LoginFormProps } from './LoginForm'; 4 | 5 | export default { 6 | title: 'organisms/LoginForm', 7 | component: LoginForm 8 | } as Meta; 9 | 10 | const Template: Story = (args) => ; 11 | 12 | export const BlackLoginForm = Template.bind({}); 13 | BlackLoginForm.args = {}; 14 | -------------------------------------------------------------------------------- /client/src/components/organisms/LoginForm/LoginForm.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Text, LogoImg } from '@components/atoms'; 3 | import { GithubLoginButton } from '@components/molecules'; 4 | import styled from 'styled-components'; 5 | import { color } from '@theme/index'; 6 | import { Size } from '@constants/index'; 7 | 8 | interface LoginFormProps {} 9 | 10 | const StyledLoginForm = styled.div` 11 | display: flex; 12 | flex-direction: column; 13 | justify-content: space-between; 14 | align-items: center; 15 | height: 17rem; 16 | `; 17 | 18 | const LoginForm: React.FC = () => { 19 | return ( 20 | 21 | 22 | 23 | Black에 로그인 24 | 25 | 26 | 로그인하려면 사용하는 Github 계정으로 계속해 주세요. 27 | 28 | 29 | 30 | ); 31 | }; 32 | 33 | export { LoginForm, LoginFormProps }; 34 | -------------------------------------------------------------------------------- /client/src/components/organisms/Sidebar/Sidebar.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Story, Meta } from '@storybook/react/types-6-0'; 3 | import { Channel, DM, Section } from '@components/molecules'; 4 | import { Sidebar, SidebarProps } from './Sidebar'; 5 | 6 | export default { 7 | title: 'organisms/Sidebar', 8 | component: Sidebar 9 | } as Meta; 10 | 11 | const mockChannelChildren = ['5주-그룹-프로젝트-슬랙', 'boost-ajae', '어몽어스'].map((item) => { 12 | return {item}; 13 | }); 14 | 15 | const mockDMChildren = ['J003_강동훈', 'J030_김도호', 'J211_탁성건'].map((item) => { 16 | return {item}; 17 | }); 18 | 19 | const mockChildren = [ 20 |
{mockChannelChildren}
, 21 |
{mockDMChildren}
22 | ]; 23 | 24 | const Template: Story = (args) => ; 25 | 26 | export const BlackSidebar = Template.bind({}); 27 | BlackSidebar.args = { 28 | children: mockChildren 29 | }; 30 | -------------------------------------------------------------------------------- /client/src/components/organisms/ThreadBody/ThreadReplies.tsx: -------------------------------------------------------------------------------- 1 | import { Reply } from '@components/molecules'; 2 | import { RootState } from '@store/reducers'; 3 | import { color } from '@theme/index'; 4 | import React from 'react'; 5 | import { useSelector } from 'react-redux'; 6 | import styled from 'styled-components'; 7 | 8 | interface ThreadRepliesProps { 9 | messageId: number | null; 10 | } 11 | 12 | const ReplyBarContainer = styled.div` 13 | display: flex; 14 | justify-content: center; 15 | align-items: center; 16 | `; 17 | 18 | const ReplyBarText = styled.p` 19 | margin: 0; 20 | color: ${color.text_primary}; 21 | `; 22 | 23 | const ReplyBarLine = styled.div` 24 | height: 2px; 25 | width: 80%; 26 | background-color: ${color.quaternary}; 27 | margin: 0rem 0.3rem; 28 | `; 29 | 30 | const ThreadReplies: React.FC = () => { 31 | const { replies } = useSelector((store: RootState) => store.thread); 32 | 33 | const createReplies = () => { 34 | return replies.map((reply: any) => ); 35 | }; 36 | const replyLength = replies.length; 37 | return ( 38 | <> 39 | {replyLength !== 0 ? ( 40 | 41 | {replyLength} reply 42 | 43 | 44 | ) : null} 45 | {replyLength !== 0 ? createReplies() : null} 46 | 47 | ); 48 | }; 49 | 50 | export { ThreadReplies, ThreadRepliesProps }; 51 | -------------------------------------------------------------------------------- /client/src/components/organisms/ThreadHeader/ThreadHeader.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import { color } from '@theme/index'; 4 | import { Text } from '@components/atoms'; 5 | import { HoverIcon } from '@components/molecules'; 6 | import CloseIcon from '@imgs/close-icon.png'; 7 | import { useHistory } from 'react-router-dom'; 8 | import { uriParser } from '@utils/index'; 9 | import { useSelector } from 'react-redux'; 10 | import { Size } from '@constants/index'; 11 | 12 | interface ThreadHeaderProps {} 13 | 14 | const ThreadHeaderContainter = styled.div` 15 | display: flex; 16 | justify-content: space-between; 17 | align-items: center; 18 | padding: 0rem 1rem; 19 | height: 10%; 20 | box-shadow: 0 3px 2px -2px ${color.border_primary}; 21 | `; 22 | 23 | const TitleContainer = styled.div``; 24 | 25 | const TextWrap = styled.div` 26 | display: grid; 27 | `; 28 | 29 | const ThreadHeader: React.FC = ({ ...props }) => { 30 | const history = useHistory(); 31 | const { title } = useSelector((store: any) => store.thread); 32 | const handlingCLoseButton = () => { 33 | history.push(`/client/${uriParser.getChatroomId()}`); 34 | }; 35 | 36 | return ( 37 | 38 | 39 | 40 | Thread 41 | 42 | 43 | {`#${title}`} 44 | 45 | 46 | 47 | 48 | ); 49 | }; 50 | 51 | export { ThreadHeader, ThreadHeaderProps }; 52 | -------------------------------------------------------------------------------- /client/src/components/organisms/index.ts: -------------------------------------------------------------------------------- 1 | import { Header } from './Header/Header'; 2 | import { Sidebar } from './Sidebar/Sidebar'; 3 | import { ChatroomHeader } from './ChatroomHeader/ChatroomHeader'; 4 | import { ChatroomBody } from './ChatroomBody/ChatroomBody'; 5 | import { LoginForm } from './LoginForm/LoginForm'; 6 | import { BrowsePageChannel } from './BrowsePageChannel/BrowsePageChannel'; 7 | import { CreateChannelModal } from './CreateChannelModal/CreateChannelModal'; 8 | import { BrowsePageHeader } from './BrowsePageHeader/BrowsePageHeader'; 9 | import { UserBoxModal } from './UserBoxModal/UserBoxModal'; 10 | import { ThreadHeader } from './ThreadHeader/ThreadHeader'; 11 | import { BrowsePageChannelList } from './BrowsePageChannelList/BrowsePageChannelList'; 12 | import { ThreadBody } from './ThreadBody/ThreadBody'; 13 | 14 | export { 15 | Header, 16 | Sidebar, 17 | ChatroomHeader, 18 | ChatroomBody, 19 | LoginForm, 20 | BrowsePageChannel, 21 | CreateChannelModal, 22 | BrowsePageHeader, 23 | UserBoxModal, 24 | ThreadBody, 25 | ThreadHeader, 26 | BrowsePageChannelList 27 | }; 28 | -------------------------------------------------------------------------------- /client/src/components/templates/FlexContainer.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | const StyledFlexContainer = styled.div` 5 | display: flex; 6 | flex-direction: column; 7 | justify-content: center; 8 | align-items: center; 9 | height: 100%; 10 | width: 100%; 11 | `; 12 | 13 | const FlexContainer: React.FC = ({ children }) => { 14 | return {children}; 15 | }; 16 | 17 | export default FlexContainer; 18 | -------------------------------------------------------------------------------- /client/src/components/templates/Main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | const StyledMain = styled.div` 5 | display: flex; 6 | width: 100%; 7 | height: 94%; 8 | overflow-x: auto; 9 | `; 10 | 11 | const Main: React.FC = ({ children }) => { 12 | return {children}; 13 | }; 14 | 15 | export default Main; 16 | -------------------------------------------------------------------------------- /client/src/components/templates/MainBox.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | const StyledMainBox = styled.main` 5 | width: -webkit-fill-available; 6 | height: 100%; 7 | `; 8 | 9 | const MainBox: React.FC = ({ children }) => { 10 | return {children}; 11 | }; 12 | 13 | export default MainBox; 14 | -------------------------------------------------------------------------------- /client/src/components/templates/index.ts: -------------------------------------------------------------------------------- 1 | import FlexContainer from './FlexContainer'; 2 | import Main from './Main'; 3 | import MainBox from './MainBox'; 4 | import Body from './Body'; 5 | 6 | export { FlexContainer, Main, MainBox, Body }; 7 | -------------------------------------------------------------------------------- /client/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { Provider } from 'react-redux'; 4 | import store from '@store/index'; 5 | import App from './shared/App'; 6 | 7 | ReactDOM.render( 8 | 9 | 10 | , 11 | document.getElementById('root') 12 | ); 13 | -------------------------------------------------------------------------------- /client/src/pages/ChannelBrowser/ChannelBrowser.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import styled from 'styled-components'; 3 | import { BrowsePageChannelList, BrowsePageHeader } from '@components/organisms'; 4 | import { BrowsePageControls, BrowsePageSearchBar } from '@components/molecules'; 5 | import { useHistory } from 'react-router-dom'; 6 | import { useDispatch, useSelector } from 'react-redux'; 7 | import { RootState } from '@store/reducers/index'; 8 | import { initChannels } from '@store/actions/channel-action'; 9 | import { createModalOpen } from '@store/actions/modal-action'; 10 | 11 | interface ChannelBrowserProps { 12 | children: React.ReactNode; 13 | } 14 | 15 | const ChannelBrowserContainer = styled.div` 16 | display: flex; 17 | flex-direction: column; 18 | height: 100%; 19 | `; 20 | 21 | const SearchBarWrap = styled.div` 22 | padding: 1rem 1.5rem; 23 | `; 24 | 25 | const ChannelBrowser: React.FC = ({ children, ...props }) => { 26 | const history = useHistory(); 27 | 28 | const dispatch = useDispatch(); 29 | const { channelCount, channels } = useSelector((store: RootState) => store.channel); 30 | const { selectedChatroomId } = useSelector((store: RootState) => store.chatroom); 31 | 32 | const handlingCreateButtonClick = () => { 33 | dispatch(createModalOpen()); 34 | }; 35 | 36 | useEffect(() => { 37 | dispatch(initChannels()); 38 | }, []); 39 | 40 | useEffect(() => { 41 | if (selectedChatroomId) history.push(`/client/${selectedChatroomId}`); 42 | }, [selectedChatroomId]); 43 | 44 | return ( 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | ); 54 | }; 55 | 56 | export { ChannelBrowser }; 57 | -------------------------------------------------------------------------------- /client/src/pages/Chatroom/Chatroom.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import styled from 'styled-components'; 3 | import { ChatroomHeader, ChatroomBody } from '@components/organisms'; 4 | import { useDispatch, useSelector } from 'react-redux'; 5 | import { RootState } from '@store/reducers/index'; 6 | import { loadAsync } from '@store/actions/chatroom-action'; 7 | import { RouteComponentProps } from 'react-router-dom'; 8 | 9 | interface ChatroomProps { 10 | width?: string; 11 | } 12 | 13 | const ChatroomContainer = styled.div` 14 | height: 100%; 15 | width: ${(props) => props.width || '100%'}; 16 | min-width: 35rem; 17 | `; 18 | 19 | const Chatroom: React.FC = ({ width }) => { 20 | const dispatch = useDispatch(); 21 | const { selectedChatroomId, selectedChatroom, messages } = useSelector((store: RootState) => store.chatroom); 22 | const { title, users } = selectedChatroom; 23 | useEffect(() => { 24 | if (selectedChatroomId !== null) dispatch(loadAsync({ selectedChatroomId })); 25 | }, [selectedChatroomId]); 26 | 27 | return ( 28 | 29 | 30 | 31 | 32 | ); 33 | }; 34 | 35 | export { Chatroom }; 36 | -------------------------------------------------------------------------------- /client/src/pages/Chatroom/ChatroomThread.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import { Chatroom, Thread } from '@pages/index'; 4 | import { RouteComponentProps } from 'react-router-dom'; 5 | 6 | interface ChatroomThreadProps {} 7 | 8 | const ChatroomThreadContainer = styled.div` 9 | display: flex; 10 | height: 100%; 11 | `; 12 | 13 | const ChatroomThread: React.FC = ({ ...props }) => { 14 | return ( 15 | 16 | 17 | 18 | 19 | ); 20 | }; 21 | 22 | export { ChatroomThread }; 23 | -------------------------------------------------------------------------------- /client/src/pages/Chatroom/Thread.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import styled from 'styled-components'; 3 | import { color } from '@theme/index'; 4 | import { ThreadHeader, ThreadBody } from '@components/organisms'; 5 | import { useDispatch } from 'react-redux'; 6 | import { loadThread } from '@store/actions/thread-action'; 7 | import { getThreadId } from '@utils/uriParser'; 8 | 9 | interface ThreadProps {} 10 | 11 | const ThreadContainer = styled.div` 12 | width: 35%; 13 | min-width: 25rem; 14 | height: 100%; 15 | border-left: 1px solid ${color.border_primary}; 16 | `; 17 | 18 | const Thread: React.FC = () => { 19 | const dispatch = useDispatch(); 20 | const threadId = getThreadId(); 21 | 22 | useEffect(() => { 23 | if (threadId) dispatch(loadThread({ messageId: threadId })); 24 | }, []); 25 | 26 | return ( 27 | 28 | 29 | {threadId && } 30 | 31 | ); 32 | }; 33 | 34 | export { Thread }; 35 | -------------------------------------------------------------------------------- /client/src/pages/Login.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { FlexContainer } from '@components/templates'; 3 | import { LoginForm } from '@components/organisms'; 4 | 5 | interface LoginProps { 6 | children: React.ReactNode; 7 | } 8 | 9 | const Login: React.FC = ({ children, ...props }) => { 10 | return ( 11 | 12 | 13 | 14 | ); 15 | }; 16 | 17 | export { Login }; 18 | -------------------------------------------------------------------------------- /client/src/pages/LoginLoading.tsx: -------------------------------------------------------------------------------- 1 | import { registerToken } from '@utils/index'; 2 | import React, { useEffect } from 'react'; 3 | 4 | interface LoginProps {} 5 | 6 | const LoginLoading: React.FC = () => { 7 | useEffect(() => { 8 | registerToken(); 9 | }, []); 10 | return <>; 11 | }; 12 | 13 | export { LoginLoading }; 14 | -------------------------------------------------------------------------------- /client/src/pages/index.ts: -------------------------------------------------------------------------------- 1 | import { Chatroom } from './Chatroom/Chatroom'; 2 | import { Login } from './Login'; 3 | import { ChatroomThread } from './Chatroom/ChatroomThread'; 4 | import { Thread } from './Chatroom/Thread'; 5 | import { LoginLoading } from './LoginLoading'; 6 | import { ChannelBrowser } from './ChannelBrowser/ChannelBrowser'; 7 | 8 | export { Chatroom, Login, LoginLoading, ChannelBrowser, Thread, ChatroomThread }; 9 | -------------------------------------------------------------------------------- /client/src/shared/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment, useEffect } from 'react'; 2 | import { BrowserRouter, Switch, Route, withRouter } from 'react-router-dom'; 3 | import { Chatroom, Login, LoginLoading, ChannelBrowser, ChatroomThread } from '@pages/index'; 4 | import { Header, Sidebar, CreateChannelModal, UserBoxModal } from '@components/organisms'; 5 | import { blockPage, uriParser } from '@utils/index'; 6 | import { Main, MainBox, Body } from '@components/templates'; 7 | import { ChannelModal, ProfileModal, EmojiPicker } from '@components/molecules'; 8 | 9 | const App = () => { 10 | useEffect(() => { 11 | if (!uriParser.isExistParseCodeUrl()) blockPage(); 12 | }, []); 13 | 14 | return ( 15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 |
23 | 24 | 25 | 26 | 27 | 28 | 29 |
30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | ); 40 | }; 41 | 42 | export default App; 43 | -------------------------------------------------------------------------------- /client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "lib": ["es5", "es2015", "es2016", "es2017", "es2018", "es2019", "es2020", "DOM"], 6 | "jsx": "react", 7 | "outDir": "./dist", 8 | "strict": true, 9 | "allowJs": true, 10 | "esModuleInterop": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "allowSyntheticDefaultImports": true, 13 | "moduleResolution": "node", 14 | "baseUrl": ".", 15 | "types": ["node"], 16 | "paths": { 17 | "@components/*": ["src/components/*"], 18 | "@pages/*": ["src/pages/*"], 19 | "@theme/*": ["src/common/theme/*"], 20 | "@utils/*": ["src/common/utils/*"], 21 | "@store/*": ["src/common/store/*"], 22 | "@imgs/*": ["public/imgs/*"], 23 | "@socket/*": ["src/common/socket/*"], 24 | "@constants/*": ["src/common/constants/*"] 25 | } 26 | }, 27 | "exclude": ["node_modules"], 28 | "include": ["src/**/*.ts", "src/**/*.tsx", "public"] 29 | } -------------------------------------------------------------------------------- /server/.env.sample: -------------------------------------------------------------------------------- 1 | # PORT 2 | PORT=port 3 | 4 | # DB 5 | DB_HOST=db_host 6 | DB_PORT=db_port 7 | DB_USERNAME=db_username 8 | DB_PASSWORD=db_password 9 | DB_DATABASE=db_database 10 | 11 | # REDIS 12 | REDIS_PORT=redis_port -------------------------------------------------------------------------------- /server/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | env: { 4 | node: true, 5 | es2021: true, 6 | jest: true 7 | }, 8 | extends: ['plugin:@typescript-eslint/eslint-recommended', 'airbnb-base', 'prettier/@typescript-eslint', 'plugin:prettier/recommended'], 9 | plugins: ['@typescript-eslint', 'prettier'], 10 | ignorePatterns: ['dist/', 'node_modules/'], 11 | rules: { 12 | 'prettier/prettier': [ 13 | 'error', 14 | { 15 | endOfLine: 'auto' 16 | } 17 | ], 18 | '@typescript-eslint/no-explicit-any': 0, 19 | 'prefer-const': 0, 20 | 'import/no-unresolved': 0, 21 | 'import/extensions': 0, 22 | 'import/prefer-default-export': 0, 23 | 'class-methods-use-this': 0 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /server/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 150, 3 | "tabWidth": 2, 4 | "singleQuote": true, 5 | "trailingComma": "none", 6 | "bracketSpacing": true, 7 | "semi": true, 8 | "useTabs": false, 9 | "arrowParens": "always", 10 | "endOfLine": "lf" 11 | } -------------------------------------------------------------------------------- /server/README.md: -------------------------------------------------------------------------------- 1 | # How to Start 2 | ## env 3 | > /ormconfig.env 4 | ``` 5 | TYPEORM_SEEDING_FACTORIES=src/factories/**/*{.ts,.js} 6 | TYPEORM_SEEDING_SEEDS=src/seeds/**/*{.ts,.js} 7 | TYPEORM_ENTITIES = src/model/**/*.ts 8 | TYPEORM_CONNECTION = 9 | TYPEORM_HOST = 10 | TYPEORM_USERNAME = 11 | TYPEORM_PASSWORD = 12 | TYPEORM_DATABASE = 13 | TYPEORM_PORT = 14 | TYPEORM_SYNCHRONIZE = true 15 | TYPEORM_LOGGING = false 16 | ``` 17 | > /production.env 18 | ``` 19 | # PORT 20 | PORT = 21 | # DB 22 | DB_HOST = 23 | DB_PORT = 24 | DB_USERNAME = 25 | DB_PASSWORD = 26 | DB_DATABASE = 27 | # REDIS 28 | REDIS_PORT = 29 | REDIS_HOST = 30 | # PASSPORT 31 | CLIENT_ID = 32 | CLIENT_SECRET = 33 | CALLBACK_URL = 34 | # JWT 35 | JWT_SECRET = 36 | # SESSION 37 | SESSION_SECRET = 38 | # CLIENT 39 | CLIENT_ADDRESS = 40 | ``` 41 | ### DB 42 | ``` 43 | docker-compose --env-file development.env up -d 44 | ``` 45 | ### seed 46 | ``` 47 | npm run seed 48 | ``` 49 | ### start 50 | > install 51 | ``` 52 | npm install 53 | ``` 54 | > start 55 | ``` 56 | npm run start 57 | ``` 58 | -------------------------------------------------------------------------------- /server/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | services: 3 | db: 4 | image: 'mysql' 5 | volumes: 6 | - dbdata:/var/lib/mysql 7 | environment: 8 | - MYSQL_ROOT_PASSWORD=${DB_PASSWORD} 9 | - MYSQL_DATABASE=${DB_DATABASE} 10 | - MYSQL_USER=${DB_USER} 11 | - MYSQL_PASSWORD=${DB_PASSWORD} 12 | ports: 13 | - '${DB_PORT}:3306' 14 | 15 | redis: 16 | image: 'redis' 17 | volumes: 18 | - redisdata:/data 19 | ports: 20 | - '${REDIS_PORT}:6379' 21 | 22 | volumes: 23 | dbdata: 24 | driver: local 25 | redisdata: 26 | driver: local 27 | -------------------------------------------------------------------------------- /server/ormconfig.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | type: 'mysql', 3 | host: process.env.DB_HOST, 4 | port: process.env.DB_PORT, 5 | username: process.env.DB_USERNAME, 6 | password: process.env.DB_PASSWORD, 7 | database: process.env.DB_DATABASE, 8 | synchronize: true, 9 | logging: false, 10 | entities: ['src/model/**/*.ts'] 11 | }; 12 | -------------------------------------------------------------------------------- /server/src/common/config/passport.ts: -------------------------------------------------------------------------------- 1 | import passport from 'passport'; 2 | import UserService from '@service/user-service'; 3 | import { Strategy as JwtStrategy, ExtractJwt } from 'passport-jwt'; 4 | import { Strategy as GitHubStrategy } from 'passport-github'; 5 | 6 | function passportConfig() { 7 | passport.serializeUser((user, done) => { 8 | done(null, user); 9 | }); 10 | 11 | passport.deserializeUser((user, done) => { 12 | done(null, user); 13 | }); 14 | 15 | passport.use( 16 | new GitHubStrategy( 17 | { 18 | clientID: process.env.CLIENT_ID, 19 | clientSecret: process.env.CLIENT_SECRET, 20 | callbackURL: process.env.CALLBACK_URL 21 | }, 22 | async (accessToken, refreshToken, profile, cb) => { 23 | return cb(null, profile); 24 | } 25 | ) 26 | ); 27 | const opts = { 28 | jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), 29 | secretOrKey: process.env.JWT_SECRET 30 | }; 31 | 32 | passport.use( 33 | new JwtStrategy(opts, async function (jwtPayload, done) { 34 | try { 35 | const user = await UserService.getInstance().getUserById(String(jwtPayload.userId)); 36 | return done(null, user); 37 | } catch (err) { 38 | return done(null, false); 39 | } 40 | }) 41 | ); 42 | } 43 | 44 | export default passportConfig; 45 | -------------------------------------------------------------------------------- /server/src/common/constants/chat-type.ts: -------------------------------------------------------------------------------- 1 | const enum ChatType { 2 | DM = 'DM', 3 | Channel = 'Channel' 4 | } 5 | export default ChatType; 6 | -------------------------------------------------------------------------------- /server/src/common/constants/default-section-name.ts: -------------------------------------------------------------------------------- 1 | const enum DefaultSectionName { 2 | Channels = 'Channels', 3 | DirectMessages = 'Direct Messages', 4 | Starred = 'Starred' 5 | } 6 | 7 | export default DefaultSectionName; 8 | -------------------------------------------------------------------------------- /server/src/common/constants/event-name.ts: -------------------------------------------------------------------------------- 1 | const enum eventName { 2 | CREATE_MESSAGE = 'create message', 3 | UPDATE_MESSAGE = 'update message', 4 | DELETE_MESSAGE = 'delete message', 5 | CREATE_REPLY = 'create reply', 6 | UPDATE_REPLY = 'update reply', 7 | DELETE_REPLY = 'delete reply', 8 | JOIN_CHATROOM = 'join chatroom', 9 | CREATE_CHATROOM = 'create chatroom', 10 | CREATE_REACTION = 'create reaction', 11 | DELETE_REACTION = 'delete reaction', 12 | DISCONNECT = 'disconnect', 13 | JOIN_DM = 'join DM', 14 | LEAVE_CHANNEL = 'leave channel', 15 | CREATE_REPLY_REACTION = 'create reply reaction', 16 | DELETE_REPLY_REACTION = 'delete reply reaction' 17 | } 18 | export default eventName; 19 | -------------------------------------------------------------------------------- /server/src/common/constants/http-status-code.ts: -------------------------------------------------------------------------------- 1 | const enum HttpStatusCode { 2 | OK = 200, 3 | CREATED = 201, 4 | NO_CONTENT = 204, 5 | BAD_REQUEST = 400, 6 | UNAUTHORIZED = 401, 7 | FORBIDDEN = 403, 8 | NOT_FOUND = 404, 9 | CONFLICT = 409, 10 | INTERNAL_SERVER_ERROR = 500 11 | } 12 | 13 | export default HttpStatusCode; 14 | -------------------------------------------------------------------------------- /server/src/common/error/bad-request-error.ts: -------------------------------------------------------------------------------- 1 | import HttpStatusCode from '@constants/http-status-code'; 2 | 3 | class BadRequestError extends Error { 4 | status: number; 5 | 6 | constructor() { 7 | super('Bad Request'); 8 | this.status = HttpStatusCode.BAD_REQUEST; 9 | } 10 | } 11 | 12 | export default BadRequestError; 13 | -------------------------------------------------------------------------------- /server/src/common/error/conflict-error.ts: -------------------------------------------------------------------------------- 1 | import HttpStatusCode from '@constants/http-status-code'; 2 | 3 | class ConflictError extends Error { 4 | status: number; 5 | 6 | constructor() { 7 | super('Conflict'); 8 | this.status = HttpStatusCode.CONFLICT; 9 | } 10 | } 11 | 12 | export default ConflictError; 13 | -------------------------------------------------------------------------------- /server/src/common/error/forbidden-error.ts: -------------------------------------------------------------------------------- 1 | import HttpStatusCode from '@constants/http-status-code'; 2 | 3 | class ForbiddenError extends Error { 4 | status: number; 5 | 6 | constructor() { 7 | super('Forbidden'); 8 | this.status = HttpStatusCode.FORBIDDEN; 9 | } 10 | } 11 | 12 | export default ForbiddenError; 13 | -------------------------------------------------------------------------------- /server/src/common/error/not-found-error.ts: -------------------------------------------------------------------------------- 1 | import HttpStatusCode from '@constants/http-status-code'; 2 | 3 | class NotFoundError extends Error { 4 | status: number; 5 | 6 | constructor() { 7 | super('Not Found'); 8 | this.status = HttpStatusCode.NOT_FOUND; 9 | } 10 | } 11 | 12 | export default NotFoundError; 13 | -------------------------------------------------------------------------------- /server/src/common/error/unauthorized-error.ts: -------------------------------------------------------------------------------- 1 | import HttpStatusCode from '@constants/http-status-code'; 2 | 3 | class UnauthorizedError extends Error { 4 | status: number; 5 | 6 | constructor() { 7 | super('Unauthorized'); 8 | this.status = HttpStatusCode.UNAUTHORIZED; 9 | } 10 | } 11 | 12 | export default UnauthorizedError; 13 | -------------------------------------------------------------------------------- /server/src/common/middleware/error-handler.ts: -------------------------------------------------------------------------------- 1 | import HttpStatusCode from '@constants/http-status-code'; 2 | import { Request, Response, NextFunction } from 'express'; 3 | 4 | const errorHandler = (err: any, req: Request, res: Response, next: NextFunction) => { 5 | res.status(err.status || HttpStatusCode.INTERNAL_SERVER_ERROR).send(); 6 | }; 7 | 8 | export { errorHandler }; 9 | -------------------------------------------------------------------------------- /server/src/common/middleware/redis.ts: -------------------------------------------------------------------------------- 1 | import { Response, NextFunction } from 'express'; 2 | import redis from 'redis'; 3 | import dotenv from 'dotenv'; 4 | 5 | const initEnv = () => { 6 | const envFile: string = `${process.env.NODE_ENV || 'development'}.env`; 7 | dotenv.config({ path: envFile }); 8 | }; 9 | 10 | class Redis { 11 | client: any; 12 | 13 | constructor() { 14 | initEnv(); 15 | this.client = redis.createClient({ 16 | host: process.env.REDIS_HOST, 17 | port: Number(process.env.REDIS_PORT) 18 | }); 19 | } 20 | 21 | createCode = async (code, token) => { 22 | this.client.set(code, token); 23 | }; 24 | 25 | deleteCode = (code) => { 26 | this.client.del(code); 27 | }; 28 | } 29 | 30 | const middleware = (req: any, res: Response, next: NextFunction) => { 31 | req.Redis = new Redis(); 32 | next(); 33 | }; 34 | 35 | export default middleware; 36 | -------------------------------------------------------------------------------- /server/src/common/utils/index.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project12-B-Slack-Web/f54c18ab64ce57c0aaf097ff3b7c0991359f5e52/server/src/common/utils/index.ts -------------------------------------------------------------------------------- /server/src/common/utils/validator.ts: -------------------------------------------------------------------------------- 1 | import { validate } from 'class-validator'; 2 | import BadRequestError from '@error/bad-request-error'; 3 | 4 | const validator = async (reqType: object) => { 5 | const errors = await validate(reqType); 6 | if (errors.length > 0) { 7 | throw new BadRequestError(); 8 | } 9 | }; 10 | 11 | export default validator; 12 | -------------------------------------------------------------------------------- /server/src/controller/auth-controller.ts: -------------------------------------------------------------------------------- 1 | import HttpStatusCode from '@constants/http-status-code'; 2 | import { NextFunction, Request, Response } from 'express'; 3 | 4 | const AuthController = { 5 | async getUserInfo(req: Request, res: Response, next: NextFunction) { 6 | const { userId, profileUri, displayName } = req.user; 7 | try { 8 | res.status(HttpStatusCode.OK).json({ userId, profileUri, displayName }); 9 | } catch (err) { 10 | next(err); 11 | } 12 | } 13 | }; 14 | 15 | export default AuthController; 16 | -------------------------------------------------------------------------------- /server/src/controller/message-controller.ts: -------------------------------------------------------------------------------- 1 | import HttpStatusCode from '@constants/http-status-code'; 2 | import MessageService from '@service/message-service'; 3 | import { NextFunction, Request, Response } from 'express'; 4 | 5 | const messageController = { 6 | async getMessages(req: Request, res: Response, next: NextFunction) { 7 | try { 8 | const { chatRoomId } = req.params; 9 | const { offsetId } = req.query; 10 | const messages = await MessageService.getInstance().getMessages(Number(chatRoomId), Number(offsetId)); 11 | res.status(HttpStatusCode.OK).json(messages); 12 | } catch (err) { 13 | next(err); 14 | } 15 | }, 16 | async createMessage(req: Request, res: Response, next: NextFunction) { 17 | const { userId } = req.user; 18 | const { chatRoomId, content } = req.body; 19 | try { 20 | const messageId = await MessageService.getInstance().createMessage(Number(userId), Number(chatRoomId), String(content)); 21 | res.status(HttpStatusCode.CREATED).json(messageId); 22 | } catch (err) { 23 | next(err); 24 | } 25 | }, 26 | async updateMessage(req: Request, res: Response, next: NextFunction) { 27 | const { content } = req.body; 28 | const { messageId } = req.params; 29 | try { 30 | await MessageService.getInstance().updateMessage(Number(messageId), String(content)); 31 | res.status(HttpStatusCode.CREATED).json(messageId); 32 | } catch (err) { 33 | next(err); 34 | } 35 | }, 36 | async deleteMessage(req: Request, res: Response, next: NextFunction) { 37 | const { messageId } = req.params; 38 | try { 39 | await MessageService.getInstance().deleteMessage(Number(messageId)); 40 | res.status(HttpStatusCode.NO_CONTENT).send(); 41 | } catch (err) { 42 | next(err); 43 | } 44 | } 45 | }; 46 | 47 | export default messageController; 48 | -------------------------------------------------------------------------------- /server/src/controller/message-reaction-controller.ts: -------------------------------------------------------------------------------- 1 | import HttpStatusCode from '@constants/http-status-code'; 2 | import { NextFunction, Request, Response } from 'express'; 3 | import MessageReactionService from '@service/message-reaction-service'; 4 | 5 | const MessageReactionController = { 6 | async createMessageReaction(req: Request, res: Response, next: NextFunction) { 7 | const { userId } = req.user; 8 | const { messageId, title, emoji } = req.body; 9 | try { 10 | const messageReaction = await MessageReactionService.getInstance().createMessageReaction(Number(userId), Number(messageId), title, emoji); 11 | res.status(HttpStatusCode.CREATED).json({ messageReaction }); 12 | } catch (err) { 13 | next(err); 14 | } 15 | }, 16 | async deleteMessageReaction(req: Request, res: Response, next: NextFunction) { 17 | const { userId } = req.user; 18 | const { messageId, reactionId } = req.params; 19 | try { 20 | await MessageReactionService.getInstance().deleteMessageReaction(Number(userId), Number(messageId), Number(reactionId)); 21 | res.status(HttpStatusCode.NO_CONTENT).send(); 22 | } catch (err) { 23 | next(err); 24 | } 25 | } 26 | }; 27 | 28 | export default MessageReactionController; 29 | -------------------------------------------------------------------------------- /server/src/controller/oauth-controller.ts: -------------------------------------------------------------------------------- 1 | import UserService from '@service/user-service'; 2 | import { Request, Response, NextFunction } from 'express'; 3 | import jwt from 'jsonwebtoken'; 4 | import randomstring from 'randomstring'; 5 | 6 | interface user { 7 | userId: number; 8 | username: string; 9 | profileUri: string; 10 | displayName: string; 11 | } 12 | 13 | declare module 'express' { 14 | interface Request { 15 | user?: user; 16 | } 17 | } 18 | 19 | const OauthController = { 20 | async OauthCallback(req: any, res: Response) { 21 | const { username, photos } = req.user; 22 | const profileUri = photos && photos[0].value; 23 | try { 24 | await UserService.getInstance().getUserById(username); 25 | } catch { 26 | await UserService.getInstance().createUser(username, profileUri); 27 | } 28 | const token = jwt.sign( 29 | { 30 | userId: req.user.username 31 | }, 32 | process.env.JWT_SECRET, 33 | { expiresIn: '1h' } 34 | ); 35 | const randomValue = randomstring.generate(7); 36 | req.Redis.createCode(randomValue, token); 37 | 38 | res.redirect(`${process.env.CLIENT_ADDRESS}?code=${randomValue}`); 39 | }, 40 | 41 | async getToken(req: any, res: Response, next: NextFunction) { 42 | try { 43 | const { code } = req.params; 44 | req.Redis.client.get(code, (err, Token) => { 45 | res.setHeader('Access-Control-Expose-Headers', 'Authorization'); 46 | res.setHeader('Authorization', `Bearer ${Token}`); 47 | res.end(); 48 | }); 49 | } catch (err) { 50 | next(err); 51 | } 52 | } 53 | }; 54 | 55 | export default OauthController; 56 | -------------------------------------------------------------------------------- /server/src/controller/reaction-controller.ts: -------------------------------------------------------------------------------- 1 | import HttpStatusCode from '@constants/http-status-code'; 2 | import { NextFunction, Request, Response } from 'express'; 3 | import ReactionService from '@service/reaction-service'; 4 | 5 | const ReactionController = { 6 | async createReaction(req: Request, res: Response, next: NextFunction) { 7 | const { title, emoji } = req.body; 8 | try { 9 | const reactionId = await ReactionService.getInstance().createReaction(title, emoji); 10 | res.status(HttpStatusCode.CREATED).json({ reactionId }); 11 | } catch (err) { 12 | next(err); 13 | } 14 | }, 15 | async getReactions(req: Request, res: Response, next: NextFunction) { 16 | try { 17 | const reactions = await ReactionService.getInstance().getReactions(); 18 | res.status(HttpStatusCode.OK).json(reactions); 19 | } catch (err) { 20 | next(err); 21 | } 22 | } 23 | }; 24 | 25 | export default ReactionController; 26 | -------------------------------------------------------------------------------- /server/src/controller/reply-controller.ts: -------------------------------------------------------------------------------- 1 | import HttpStatusCode from '@constants/http-status-code'; 2 | import { NextFunction, Request, Response } from 'express'; 3 | import ReplyService from '@service/reply-service'; 4 | import messageService from '@service/message-service'; 5 | 6 | const ReplyController = { 7 | async createReply(req: Request, res: Response, next: NextFunction) { 8 | const { userId } = req.user; 9 | const { messageId, content } = req.body; 10 | try { 11 | const replyId = await ReplyService.getInstance().createReply(Number(userId), Number(messageId), content); 12 | res.status(HttpStatusCode.CREATED).json({ replyId }); 13 | } catch (err) { 14 | next(err); 15 | } 16 | }, 17 | async getReply(req: Request, res: Response, next: NextFunction) { 18 | const { replyId } = req.params; 19 | try { 20 | const reply = await ReplyService.getInstance().getReply(Number(replyId)); 21 | res.status(HttpStatusCode.OK).json(reply); 22 | } catch (err) { 23 | next(err); 24 | } 25 | }, 26 | async getReplies(req: Request, res: Response, next: NextFunction) { 27 | const { offsetId } = req.query; 28 | const { messageId } = req.params; 29 | try { 30 | const message = await messageService.getInstance().getMessage(Number(messageId)); 31 | const replies = await ReplyService.getInstance().getReplies(Number(messageId), Number(offsetId)); 32 | const replyCount = await ReplyService.getInstance().getReplyCount(Number(messageId)); 33 | res.header('Access-Control-Expose-Headers', 'x-total-count'); 34 | res.setHeader('x-total-count', replyCount); 35 | res.status(HttpStatusCode.OK).json({ message, replies }); 36 | } catch (err) { 37 | next(err); 38 | } 39 | } 40 | }; 41 | 42 | export default ReplyController; 43 | -------------------------------------------------------------------------------- /server/src/controller/user-chatroom-controller.ts: -------------------------------------------------------------------------------- 1 | import HttpStatusCode from '@constants/http-status-code'; 2 | import { NextFunction, Request, Response } from 'express'; 3 | import UserChatroomService from '@service/user-chatroom-service'; 4 | 5 | const UserChatroomController = { 6 | async joinChatroom(req: Request, res: Response, next: NextFunction) { 7 | const { userId } = req.user; 8 | const { chatroomId } = req.body; 9 | try { 10 | await UserChatroomService.getInstance().joinChatroom(Number(userId), Number(chatroomId)); 11 | res.status(HttpStatusCode.CREATED).send(); 12 | } catch (err) { 13 | next(err); 14 | } 15 | }, 16 | async inviteChatroom(req: Request, res: Response, next: NextFunction) { 17 | const { users, chatroomId } = req.body; 18 | try { 19 | await UserChatroomService.getInstance().inviteChatroom(users, Number(chatroomId)); 20 | res.status(HttpStatusCode.CREATED).send(); 21 | } catch (err) { 22 | next(err); 23 | } 24 | }, 25 | async getUserChatrooms(req: Request, res: Response, next: NextFunction) { 26 | const { userId } = req.user; 27 | try { 28 | const users = await UserChatroomService.getInstance().getUserChatrooms(userId); 29 | res.status(HttpStatusCode.OK).json(users); 30 | } catch (err) { 31 | next(err); 32 | } 33 | } 34 | }; 35 | 36 | export default UserChatroomController; 37 | -------------------------------------------------------------------------------- /server/src/controller/user-controller.ts: -------------------------------------------------------------------------------- 1 | import HttpStatusCode from '@constants/http-status-code'; 2 | import UserService from '@service/user-service'; 3 | import { NextFunction, Request, Response } from 'express'; 4 | 5 | const UserController = { 6 | async getUsers(req: Request, res: Response, next: NextFunction) { 7 | try { 8 | const users = await UserService.getInstance().getUsers(); 9 | res.status(HttpStatusCode.OK).json(users); 10 | } catch (err) { 11 | next(err); 12 | } 13 | }, 14 | async getUser(req: Request, res: Response, next: NextFunction) { 15 | const { userId } = req.params; 16 | try { 17 | const user = await UserService.getInstance().getUser(Number(userId)); 18 | res.status(HttpStatusCode.OK).json(user); 19 | } catch (err) { 20 | next(err); 21 | } 22 | } 23 | }; 24 | 25 | export default UserController; 26 | -------------------------------------------------------------------------------- /server/src/model/chatroom.ts: -------------------------------------------------------------------------------- 1 | import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn, OneToMany, DeleteDateColumn } from 'typeorm'; 2 | import { IsBoolean, IsIn, IsString } from 'class-validator'; 3 | import UserChatroom from '@model/user-chatroom'; 4 | import Message from '@model/message'; 5 | import ChatType from '@constants/chat-type'; 6 | 7 | @Entity({ name: 'chatroom' }) 8 | export default class Chatroom { 9 | @PrimaryGeneratedColumn() 10 | chatroomId: number; 11 | 12 | @Column({ unique: true, nullable: true }) 13 | @IsString() 14 | title: string; 15 | 16 | @Column({ nullable: true }) 17 | description: string; 18 | 19 | @Column() 20 | @IsBoolean() 21 | isPrivate: boolean; 22 | 23 | @Column() 24 | @IsIn([ChatType.DM, ChatType.Channel]) 25 | chatType: string; 26 | 27 | @Column({ nullable: true }) 28 | topic: string; 29 | 30 | @CreateDateColumn() 31 | createdAt: Date; 32 | 33 | @UpdateDateColumn() 34 | updatedAt: Date; 35 | 36 | @DeleteDateColumn() 37 | deletedAt: Date; 38 | 39 | @OneToMany(() => UserChatroom, (userChatroom) => userChatroom.chatroom) 40 | userChatrooms: UserChatroom[]; 41 | 42 | @OneToMany(() => Message, (message) => message.chatroom) 43 | messages: Message[]; 44 | } 45 | -------------------------------------------------------------------------------- /server/src/model/message-reaction.ts: -------------------------------------------------------------------------------- 1 | import { Entity, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn, ManyToOne, JoinColumn, DeleteDateColumn } from 'typeorm'; 2 | import Reaction from '@model/reaction'; 3 | import User from '@model/user'; 4 | import Message from '@model/message'; 5 | 6 | @Entity({ name: 'message_reaction' }) 7 | export default class MessageReaction { 8 | @PrimaryGeneratedColumn() 9 | messageReactionId: number; 10 | 11 | @CreateDateColumn() 12 | createdAt: Date; 13 | 14 | @UpdateDateColumn() 15 | updatedAt: Date; 16 | 17 | @DeleteDateColumn() 18 | deletedAt: Date; 19 | 20 | @ManyToOne(() => Reaction, (reaction) => reaction.reactionId) 21 | @JoinColumn({ name: 'reactionId' }) 22 | reaction: Reaction; 23 | 24 | @ManyToOne(() => User, (user) => user.userId) 25 | @JoinColumn({ name: 'userId' }) 26 | user: User; 27 | 28 | @ManyToOne(() => Message, (message) => message.messageId) 29 | @JoinColumn({ name: 'messageId' }) 30 | message: Message; 31 | } 32 | -------------------------------------------------------------------------------- /server/src/model/message.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Entity, 3 | Column, 4 | PrimaryGeneratedColumn, 5 | CreateDateColumn, 6 | UpdateDateColumn, 7 | ManyToOne, 8 | OneToMany, 9 | JoinColumn, 10 | DeleteDateColumn 11 | } from 'typeorm'; 12 | import User from '@model/user'; 13 | import Chatroom from '@model/chatroom'; 14 | import MessageReaction from '@model/message-reaction'; 15 | import Reply from '@model/reply'; 16 | 17 | @Entity({ name: 'message' }) 18 | export default class Message { 19 | @PrimaryGeneratedColumn() 20 | messageId: number; 21 | 22 | @Column() 23 | content: string; 24 | 25 | @CreateDateColumn() 26 | createdAt: Date; 27 | 28 | @UpdateDateColumn() 29 | updatedAt: Date; 30 | 31 | @DeleteDateColumn() 32 | deletedAt: Date; 33 | 34 | @ManyToOne(() => User, (user) => user.userId) 35 | @JoinColumn({ name: 'userId' }) 36 | user: User; 37 | 38 | @ManyToOne(() => Chatroom, (chatroom) => chatroom.chatroomId) 39 | @JoinColumn({ name: 'chatroomId' }) 40 | chatroom: Chatroom; 41 | 42 | @OneToMany(() => MessageReaction, (messageReaction) => messageReaction.message) 43 | messageReactions: MessageReaction[]; 44 | 45 | @OneToMany(() => Reply, (reply) => reply.message) 46 | replies: Reply[]; 47 | } 48 | -------------------------------------------------------------------------------- /server/src/model/reaction.ts: -------------------------------------------------------------------------------- 1 | import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn, OneToMany, DeleteDateColumn } from 'typeorm'; 2 | import MessageReaction from '@model/message-reaction'; 3 | import ReplyReaction from '@model/reply-reaction'; 4 | import { IsString } from 'class-validator'; 5 | 6 | @Entity({ name: 'reaction' }) 7 | export default class Reaction { 8 | @PrimaryGeneratedColumn() 9 | reactionId: number; 10 | 11 | @Column({ length: 100, unique: true }) 12 | @IsString() 13 | title: string; 14 | 15 | @Column({ length: 10 }) 16 | @IsString() 17 | emoji: string; 18 | 19 | @CreateDateColumn() 20 | createdAt: Date; 21 | 22 | @UpdateDateColumn() 23 | updatedAt: Date; 24 | 25 | @DeleteDateColumn() 26 | deletedAt: Date; 27 | 28 | @OneToMany(() => MessageReaction, (messageReaction) => messageReaction.reaction) 29 | messageReactions: MessageReaction[]; 30 | 31 | @OneToMany(() => ReplyReaction, (replyReaction) => replyReaction.reaction) 32 | replyReactions: ReplyReaction[]; 33 | } 34 | -------------------------------------------------------------------------------- /server/src/model/reply-reaction.ts: -------------------------------------------------------------------------------- 1 | import { Entity, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn, ManyToOne, JoinColumn, DeleteDateColumn } from 'typeorm'; 2 | import Reaction from '@model/reaction'; 3 | import User from '@model/user'; 4 | import Reply from '@model/reply'; 5 | 6 | @Entity({ name: 'reply_reaction' }) 7 | export default class ReplyReaction { 8 | @PrimaryGeneratedColumn() 9 | replyReactionId: number; 10 | 11 | @CreateDateColumn() 12 | createdAt: Date; 13 | 14 | @UpdateDateColumn() 15 | updatedAt: Date; 16 | 17 | @DeleteDateColumn() 18 | deletedAt: Date; 19 | 20 | @ManyToOne(() => Reaction, (reaction) => reaction.reactionId) 21 | @JoinColumn({ name: 'reactionId' }) 22 | reaction: Reaction; 23 | 24 | @ManyToOne(() => User, (user) => user.userId) 25 | @JoinColumn({ name: 'userId' }) 26 | user: User; 27 | 28 | @ManyToOne(() => Reply, (reply) => reply.replyId) 29 | @JoinColumn({ name: 'replyId' }) 30 | reply: Reply; 31 | } 32 | -------------------------------------------------------------------------------- /server/src/model/reply.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Entity, 3 | Column, 4 | PrimaryGeneratedColumn, 5 | CreateDateColumn, 6 | UpdateDateColumn, 7 | ManyToOne, 8 | JoinColumn, 9 | DeleteDateColumn, 10 | OneToMany 11 | } from 'typeorm'; 12 | import User from '@model/user'; 13 | import Message from '@model/message'; 14 | import ReplyReaction from '@model/reply-reaction'; 15 | import { IsString } from 'class-validator'; 16 | 17 | @Entity({ name: 'reply' }) 18 | export default class Reply { 19 | @PrimaryGeneratedColumn() 20 | replyId: number; 21 | 22 | @Column() 23 | @IsString() 24 | content: string; 25 | 26 | @CreateDateColumn() 27 | createdAt: Date; 28 | 29 | @UpdateDateColumn() 30 | updatedAt: Date; 31 | 32 | @DeleteDateColumn() 33 | deletedAt: Date; 34 | 35 | @ManyToOne(() => User, (user) => user.userId) 36 | @JoinColumn({ name: 'userId' }) 37 | user: User; 38 | 39 | @ManyToOne(() => Message, (message) => message.messageId) 40 | @JoinColumn({ name: 'messageId' }) 41 | message: Message; 42 | 43 | @OneToMany(() => ReplyReaction, (replyReaction) => replyReaction.reply) 44 | replyReactions: ReplyReaction[]; 45 | } 46 | -------------------------------------------------------------------------------- /server/src/model/section.ts: -------------------------------------------------------------------------------- 1 | import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn, ManyToOne, JoinColumn, DeleteDateColumn } from 'typeorm'; 2 | import User from '@model/user'; 3 | 4 | @Entity({ name: 'section' }) 5 | export default class Section { 6 | @PrimaryGeneratedColumn() 7 | sectionId: number; 8 | 9 | @Column() 10 | sectionName: string; 11 | 12 | @CreateDateColumn() 13 | createdAt: Date; 14 | 15 | @UpdateDateColumn() 16 | updatedAt: Date; 17 | 18 | @DeleteDateColumn() 19 | deletedAt: Date; 20 | 21 | @ManyToOne(() => User, (user) => user.userId) 22 | @JoinColumn({ name: 'userId' }) 23 | user: User; 24 | } 25 | -------------------------------------------------------------------------------- /server/src/model/socket.ts: -------------------------------------------------------------------------------- 1 | import { Entity, Column, PrimaryGeneratedColumn, ManyToOne, JoinColumn } from 'typeorm'; 2 | import User from '@model/user'; 3 | 4 | @Entity({ name: 'socket' }) 5 | export default class Socket { 6 | @PrimaryGeneratedColumn() 7 | id: number; 8 | 9 | @ManyToOne(() => User, (user) => user.userId) 10 | @JoinColumn({ name: 'userId' }) 11 | user: User; 12 | 13 | @Column() 14 | socketId: string; 15 | } 16 | -------------------------------------------------------------------------------- /server/src/model/user-chatroom.ts: -------------------------------------------------------------------------------- 1 | import { Entity, Column, CreateDateColumn, UpdateDateColumn, ManyToOne, JoinColumn, PrimaryGeneratedColumn, DeleteDateColumn } from 'typeorm'; 2 | import User from '@model/user'; 3 | import Chatroom from '@model/chatroom'; 4 | 5 | @Entity({ name: 'user_chatroom' }) 6 | export default class UserChatroom { 7 | @PrimaryGeneratedColumn() 8 | userChatroomId: number; 9 | 10 | @Column() 11 | sectionName: string; 12 | 13 | @CreateDateColumn() 14 | createdAt: Date; 15 | 16 | @UpdateDateColumn() 17 | updatedAt: Date; 18 | 19 | @DeleteDateColumn() 20 | deletedAt: Date; 21 | 22 | @ManyToOne(() => User, (user) => user.userId) 23 | @JoinColumn({ name: 'userId' }) 24 | user: User; 25 | 26 | @ManyToOne(() => Chatroom, (chatroom) => chatroom.chatroomId) 27 | @JoinColumn({ name: 'chatroomId' }) 28 | chatroom: Chatroom; 29 | } 30 | -------------------------------------------------------------------------------- /server/src/model/user.ts: -------------------------------------------------------------------------------- 1 | import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn, OneToMany, DeleteDateColumn } from 'typeorm'; 2 | import { IsUrl } from 'class-validator'; 3 | import Section from '@model/section'; 4 | import UserChatroom from '@model/user-chatroom'; 5 | import Message from '@model/message'; 6 | import MessageReaction from '@model/message-reaction'; 7 | import Reply from '@model/reply'; 8 | import ReplyReaction from '@model/reply-reaction'; 9 | import Socket from '@model/socket'; 10 | 11 | @Entity({ name: 'user' }) 12 | export default class User { 13 | @PrimaryGeneratedColumn() 14 | userId: number; 15 | 16 | @Column({ length: 30, unique: true }) 17 | id: string; 18 | 19 | @Column({ length: 40, nullable: true }) 20 | password: string; 21 | 22 | @Column({ nullable: true }) 23 | @IsUrl() 24 | profileUri: string; 25 | 26 | @Column({ length: 20 }) 27 | fullName: string; 28 | 29 | @Column({ length: 20 }) 30 | displayName: string; 31 | 32 | @Column({ nullable: true }) 33 | whatIDo: string; 34 | 35 | @Column({ length: 20, nullable: true }) 36 | phoneNumber: string; 37 | 38 | @Column() 39 | isSocial: boolean; 40 | 41 | @CreateDateColumn() 42 | createdAt: Date; 43 | 44 | @UpdateDateColumn() 45 | updatedAt: Date; 46 | 47 | @DeleteDateColumn() 48 | deletedAt: Date; 49 | 50 | @OneToMany(() => Section, (section) => section.user) 51 | sections: Section[]; 52 | 53 | @OneToMany(() => UserChatroom, (userChatroom) => userChatroom.user) 54 | userChatrooms: UserChatroom[]; 55 | 56 | @OneToMany(() => Message, (message) => message.user) 57 | messages: Message[]; 58 | 59 | @OneToMany(() => MessageReaction, (messageReaction) => messageReaction.user) 60 | messageReactions: MessageReaction[]; 61 | 62 | @OneToMany(() => Reply, (reply) => reply.user) 63 | replies: Reply[]; 64 | 65 | @OneToMany(() => ReplyReaction, (replyReaction) => replyReaction.user) 66 | replyReactions: ReplyReaction[]; 67 | 68 | @OneToMany(() => Socket, (socket) => socket.user) 69 | sockets: Socket[]; 70 | } 71 | -------------------------------------------------------------------------------- /server/src/repository/chatroom-repository.ts: -------------------------------------------------------------------------------- 1 | import { EntityRepository, Repository } from 'typeorm'; 2 | import Chatroom from '@model/chatroom'; 3 | 4 | @EntityRepository(Chatroom) 5 | export default class ChatroomRepository extends Repository { 6 | findByTitle(title: string) { 7 | return this.findOne({ title }); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /server/src/repository/message-reaction-repository.ts: -------------------------------------------------------------------------------- 1 | import { EntityRepository, Repository } from 'typeorm'; 2 | import MessageReaction from '@model/message-reaction'; 3 | 4 | @EntityRepository(MessageReaction) 5 | export default class MessageReactionRepository extends Repository {} 6 | -------------------------------------------------------------------------------- /server/src/repository/message-repository.ts: -------------------------------------------------------------------------------- 1 | import { EntityRepository, Repository } from 'typeorm'; 2 | import Message from '@model/message'; 3 | 4 | @EntityRepository(Message) 5 | export default class MessageRepository extends Repository {} 6 | -------------------------------------------------------------------------------- /server/src/repository/reacion-repository.ts: -------------------------------------------------------------------------------- 1 | import { EntityRepository, Repository } from 'typeorm'; 2 | import Reaction from '@model/reaction'; 3 | 4 | @EntityRepository(Reaction) 5 | export default class ReactionRepository extends Repository {} 6 | -------------------------------------------------------------------------------- /server/src/repository/reply-reaction-repository.ts: -------------------------------------------------------------------------------- 1 | import { EntityRepository, Repository } from 'typeorm'; 2 | import ReplyReaction from '@model/reply-reaction'; 3 | 4 | @EntityRepository(ReplyReaction) 5 | export default class ReplyReactionRepository extends Repository {} 6 | -------------------------------------------------------------------------------- /server/src/repository/reply-repository.ts: -------------------------------------------------------------------------------- 1 | import { EntityRepository, Repository } from 'typeorm'; 2 | import Reply from '@model/reply'; 3 | 4 | @EntityRepository(Reply) 5 | export default class ReplyRepository extends Repository {} 6 | -------------------------------------------------------------------------------- /server/src/repository/section-repository.ts: -------------------------------------------------------------------------------- 1 | import { EntityRepository, Repository } from 'typeorm'; 2 | import Section from '@model/section'; 3 | 4 | @EntityRepository(Section) 5 | export default class SectionRepository extends Repository
{} 6 | -------------------------------------------------------------------------------- /server/src/repository/socket-repository.ts: -------------------------------------------------------------------------------- 1 | import { EntityRepository, Repository } from 'typeorm'; 2 | import Socket from '@model/socket'; 3 | 4 | @EntityRepository(Socket) 5 | export default class SocketRepository extends Repository {} 6 | -------------------------------------------------------------------------------- /server/src/repository/user-chatroom-repository.ts: -------------------------------------------------------------------------------- 1 | import { EntityRepository, Repository } from 'typeorm'; 2 | import UserChatroom from '@model/user-chatroom'; 3 | 4 | @EntityRepository(UserChatroom) 5 | export default class UserChatroomRepository extends Repository {} 6 | -------------------------------------------------------------------------------- /server/src/repository/user-repository.ts: -------------------------------------------------------------------------------- 1 | import { EntityRepository, Repository } from 'typeorm'; 2 | import User from '@model/user'; 3 | 4 | @EntityRepository(User) 5 | export default class UserRepository extends Repository {} 6 | -------------------------------------------------------------------------------- /server/src/router/api.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import userRouter from '@router/user-router'; 3 | import messageRouter from '@router/message-router'; 4 | import chatroomRouter from '@router/chatroom-router'; 5 | import userChatroomRouter from '@router/user-chatroom-router'; 6 | import replyRouter from '@router/reply-router'; 7 | import reactionRouter from '@router/reaction-router'; 8 | import authRouter from '@router/auth-router'; 9 | import messageReactionRouter from '@router/message-reaction-router'; 10 | 11 | const router = express.Router(); 12 | 13 | router.use('/messages', messageRouter); 14 | router.use('/users', userRouter); 15 | router.use('/chatrooms', chatroomRouter); 16 | router.use('/user-chatrooms', userChatroomRouter); 17 | router.use('/replies', replyRouter); 18 | router.use('/reactions', reactionRouter); 19 | router.use('/auth', authRouter); 20 | router.use('/message-reactions', messageReactionRouter); 21 | 22 | export default router; 23 | -------------------------------------------------------------------------------- /server/src/router/auth-router.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import AuthController from '@controller/auth-controller'; 3 | 4 | const router = express.Router(); 5 | 6 | router.get('/', AuthController.getUserInfo); 7 | 8 | export default router; 9 | -------------------------------------------------------------------------------- /server/src/router/chatroom-router.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import ChatroomController from '@controller/chatroom-controller'; 3 | 4 | const router = express.Router(); 5 | 6 | router.get('/', ChatroomController.getChatrooms); 7 | router.post('/channel', ChatroomController.createChannel); 8 | router.post('/dm', ChatroomController.createDM); 9 | router.get('/:chatroomId', ChatroomController.getChatroomInfo); 10 | router.patch('/:chatroomId', ChatroomController.updateChatroom); 11 | router.delete('/:chatroomId', ChatroomController.deleteChatroom); 12 | 13 | export default router; 14 | -------------------------------------------------------------------------------- /server/src/router/index.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import passport from 'passport'; 3 | import oauthRouter from '@router/oauth'; 4 | import apiRouter from '@router/api'; 5 | 6 | const router = express.Router(); 7 | 8 | router.use('/oauth', oauthRouter); 9 | router.use('/api', passport.authenticate('jwt', { session: false }), apiRouter); 10 | 11 | export default router; 12 | -------------------------------------------------------------------------------- /server/src/router/message-reaction-router.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import MessageReactionController from '@controller/message-reaction-controller'; 3 | 4 | const router = express.Router(); 5 | 6 | router.post('/', MessageReactionController.createMessageReaction); 7 | router.delete('/:messageId/:reactionId', MessageReactionController.deleteMessageReaction); 8 | 9 | export default router; 10 | -------------------------------------------------------------------------------- /server/src/router/message-router.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import messageController from '@controller/message-controller'; 3 | 4 | const router = express.Router(); 5 | 6 | router.get('/:chatRoomId', messageController.getMessages); 7 | router.post('/', messageController.createMessage); 8 | router.patch('/:messageId', messageController.updateMessage); 9 | router.delete('/:messageId', messageController.deleteMessage); 10 | 11 | export default router; 12 | -------------------------------------------------------------------------------- /server/src/router/oauth.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import OauthController from '@controller/oauth-controller'; 3 | import passport from 'passport'; 4 | 5 | const router = express.Router(); 6 | 7 | router.get('/github/login', passport.authenticate('github')); 8 | router.get('/github/callback', passport.authenticate('github', { failureRedirect: '/' }), OauthController.OauthCallback); 9 | router.get('/github/:code', OauthController.getToken); 10 | 11 | export default router; 12 | -------------------------------------------------------------------------------- /server/src/router/reaction-router.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import ReactionController from '@controller/reaction-controller'; 3 | 4 | const router = express.Router(); 5 | 6 | router.post('/', ReactionController.createReaction); 7 | router.get('/', ReactionController.getReactions); 8 | 9 | export default router; 10 | -------------------------------------------------------------------------------- /server/src/router/reply-router.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import ReplyController from '@controller/reply-controller'; 3 | 4 | const router = express.Router(); 5 | 6 | router.post('/', ReplyController.createReply); 7 | router.get('/', ReplyController.getReplies); 8 | router.get('/:messageId', ReplyController.getReplies); 9 | 10 | export default router; 11 | -------------------------------------------------------------------------------- /server/src/router/user-chatroom-router.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import UserChatroomController from '@controller/user-chatroom-controller'; 3 | 4 | const router = express.Router(); 5 | 6 | router.post('/', UserChatroomController.joinChatroom); 7 | router.post('/others', UserChatroomController.inviteChatroom); 8 | router.get('/', UserChatroomController.getUserChatrooms); 9 | 10 | export default router; 11 | -------------------------------------------------------------------------------- /server/src/router/user-router.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import UserController from '@controller/user-controller'; 3 | 4 | const router = express.Router(); 5 | 6 | router.get('/', UserController.getUsers); 7 | router.get('/:userId', UserController.getUser); 8 | 9 | export default router; 10 | -------------------------------------------------------------------------------- /server/src/seeds/chatroom.ts: -------------------------------------------------------------------------------- 1 | import { Factory, Seeder } from 'typeorm-seeding'; 2 | import { Connection } from 'typeorm'; 3 | import ChatType from '@constants/chat-type'; 4 | import Chatroom from '@model/chatroom'; 5 | 6 | export default class CreateChatrooms implements Seeder { 7 | public async run(factory: Factory, connection: Connection): Promise { 8 | const chatroomData = [ 9 | { 10 | title: 'random', 11 | isPrivate: false, 12 | chatType: ChatType.Channel 13 | } 14 | ]; 15 | const res = await connection.getRepository(Chatroom).findOne(chatroomData[0]); 16 | if (!res) await connection.getRepository(Chatroom).save(chatroomData); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /server/src/service/reaction-service.ts: -------------------------------------------------------------------------------- 1 | import { getCustomRepository } from 'typeorm'; 2 | import ReactionRepository from '@repository/reacion-repository'; 3 | import validator from '@utils/validator'; 4 | import BadRequestError from '@error/bad-request-error'; 5 | 6 | class ReactionService { 7 | static instance: ReactionService; 8 | 9 | reactionRepository: ReactionRepository; 10 | 11 | constructor() { 12 | this.reactionRepository = getCustomRepository(ReactionRepository); 13 | } 14 | 15 | static getInstance(): ReactionService { 16 | if (!ReactionService.instance) { 17 | ReactionService.instance = new ReactionService(); 18 | } 19 | return ReactionService.instance; 20 | } 21 | 22 | async createReaction(title: string, emoji: string) { 23 | const reaction = await this.reactionRepository.findOne({ title }); 24 | if (reaction) { 25 | throw new BadRequestError(); 26 | } 27 | 28 | const newReaction = this.reactionRepository.create({ title, emoji }); 29 | await validator(newReaction); 30 | const createdReaction = await this.reactionRepository.save(newReaction); 31 | return createdReaction.reactionId; 32 | } 33 | 34 | async getReactions() { 35 | const reactions = await this.reactionRepository 36 | .createQueryBuilder('reaction') 37 | .select(['reaction.reactionId', 'reaction.title', 'reaction.emoji']) 38 | .getMany(); 39 | 40 | return reactions; 41 | } 42 | } 43 | 44 | export default ReactionService; 45 | -------------------------------------------------------------------------------- /server/src/service/socket-service.ts: -------------------------------------------------------------------------------- 1 | import { getCustomRepository } from 'typeorm'; 2 | import SocketRepository from '@repository/socket-repository'; 3 | import UserRepository from '@repository/user-repository'; 4 | import validator from '@utils/validator'; 5 | 6 | class SocketService { 7 | static instance: SocketService; 8 | 9 | socketRepository: SocketRepository; 10 | 11 | userRepository: UserRepository; 12 | 13 | constructor() { 14 | this.socketRepository = getCustomRepository(SocketRepository); 15 | this.userRepository = getCustomRepository(UserRepository); 16 | } 17 | 18 | static getInstance(): SocketService { 19 | if (!SocketService.instance) { 20 | SocketService.instance = new SocketService(); 21 | } 22 | return SocketService.instance; 23 | } 24 | 25 | async createSocket(userId, socketId) { 26 | const user = await this.userRepository.findOne({ userId }); 27 | const newSocket = this.socketRepository.create({ user, socketId }); 28 | await this.socketRepository.save(newSocket); 29 | } 30 | 31 | async deleteSocket(socketId) { 32 | await this.socketRepository.delete({ socketId }); 33 | } 34 | 35 | async getSocketId(userId) { 36 | const user = await this.userRepository.findOne({ userId }); 37 | const socketInfos = await this.socketRepository.find({ user }); 38 | return socketInfos; 39 | } 40 | } 41 | 42 | export default SocketService; 43 | -------------------------------------------------------------------------------- /server/src/socket/event/chatroom-event.ts: -------------------------------------------------------------------------------- 1 | import chatroomHandler from '@socket/handler/chatroom-handler'; 2 | import eventName from '@constants/event-name'; 3 | 4 | const chatroomEvent = (io, socket) => { 5 | socket.on(eventName.JOIN_CHATROOM, (chatroom) => chatroomHandler.joinChatroom(io, socket, chatroom)); 6 | socket.on(eventName.JOIN_DM, (DirectMessage) => chatroomHandler.joinDM(io, socket, DirectMessage)); 7 | socket.on(eventName.LEAVE_CHANNEL, (chatroom) => chatroomHandler.leaveChatroom(io, socket, chatroom)); 8 | }; 9 | 10 | export default chatroomEvent; 11 | -------------------------------------------------------------------------------- /server/src/socket/event/connection-event.ts: -------------------------------------------------------------------------------- 1 | import chatroomHandler from '@socket/handler/chatroom-handler'; 2 | import socketHandler from '@socket/handler/socket-handler'; 3 | 4 | const connectionEvent = (io, socket) => { 5 | chatroomHandler.initJoinChatroom(io, socket); 6 | socketHandler.saveSocketId(io, socket); 7 | }; 8 | 9 | export default connectionEvent; 10 | -------------------------------------------------------------------------------- /server/src/socket/event/disconnect-event.ts: -------------------------------------------------------------------------------- 1 | import socketHandler from '@socket/handler/socket-handler'; 2 | import eventName from '@constants/event-name'; 3 | 4 | const messageEvent = (io, socket) => { 5 | socket.on(eventName.DISCONNECT, () => socketHandler.deleteSocketId(io, socket)); 6 | }; 7 | 8 | export default messageEvent; 9 | -------------------------------------------------------------------------------- /server/src/socket/event/message-event.ts: -------------------------------------------------------------------------------- 1 | import messageHandler from '@socket/handler/message-handler'; 2 | import eventName from '@constants/event-name'; 3 | 4 | const messageEvent = (io, socket) => { 5 | socket.on(eventName.CREATE_MESSAGE, (message) => messageHandler.createMessage(io, socket, message)); 6 | socket.on(eventName.UPDATE_MESSAGE, (message) => messageHandler.updateMessage(io, socket, message)); 7 | socket.on(eventName.DELETE_MESSAGE, (message) => messageHandler.deleteMessage(io, socket, message)); 8 | }; 9 | 10 | export default messageEvent; 11 | -------------------------------------------------------------------------------- /server/src/socket/event/message-reaction-event.ts: -------------------------------------------------------------------------------- 1 | import messageReactionHandler from '@socket/handler/message-reaction-handler'; 2 | import eventName from '@constants/event-name'; 3 | 4 | const messageReactionEvent = (io, socket) => { 5 | socket.on(eventName.CREATE_REACTION, (messageReaction) => messageReactionHandler.createMessageReaction(io, socket, messageReaction)); 6 | socket.on(eventName.DELETE_REACTION, (messageReaction) => messageReactionHandler.deleteMessageReaction(io, socket, messageReaction)); 7 | }; 8 | 9 | export default messageReactionEvent; 10 | -------------------------------------------------------------------------------- /server/src/socket/event/reply-event.ts: -------------------------------------------------------------------------------- 1 | import replyHandler from '@socket/handler/reply-handler'; 2 | import eventName from '@constants/event-name'; 3 | 4 | const messageEvent = (io, socket) => { 5 | socket.on(eventName.CREATE_REPLY, (reply) => replyHandler.createReply(io, socket, reply)); 6 | socket.on(eventName.UPDATE_REPLY, (reply) => replyHandler.updateReply(io, socket, reply)); 7 | socket.on(eventName.DELETE_REPLY, (reply) => replyHandler.deleteReply(io, socket, reply)); 8 | }; 9 | 10 | export default messageEvent; 11 | -------------------------------------------------------------------------------- /server/src/socket/event/reply-rection-event.ts: -------------------------------------------------------------------------------- 1 | import replyReactionHandler from '@socket/handler/reply-reaction-handler'; 2 | import eventName from '@constants/event-name'; 3 | 4 | const messageReactionEvent = (io, socket) => { 5 | socket.on(eventName.CREATE_REPLY_REACTION, (replyReaction) => replyReactionHandler.createReplyReaction(io, socket, replyReaction)); 6 | socket.on(eventName.DELETE_REPLY_REACTION, (replyReaction) => replyReactionHandler.deleteReplytReaction(io, socket, replyReaction)); 7 | }; 8 | 9 | export default messageReactionEvent; 10 | -------------------------------------------------------------------------------- /server/src/socket/handler/message-handler.ts: -------------------------------------------------------------------------------- 1 | import MessageService from '@service/message-service'; 2 | import eventName from '@constants/event-name'; 3 | 4 | const messageHandler = { 5 | async createMessage(io, socket, message) { 6 | const req = socket.request; 7 | const { chatroomId, content } = message; 8 | const { userId } = req.user; 9 | const messageId = await MessageService.getInstance().createMessage(userId, chatroomId, content); 10 | const newMessage = await MessageService.getInstance().getMessage(messageId); 11 | io.to(String(chatroomId)).emit(eventName.CREATE_MESSAGE, { ...newMessage, chatroomId }); 12 | }, 13 | async updateMessage(io, socket, message) { 14 | const { messageId, content } = message; 15 | const messageInfo = await MessageService.getInstance().getMessage(messageId); 16 | const { chatroomId } = messageInfo.chatroom; 17 | const updateMessage = await MessageService.getInstance().updateMessage(messageId, content); 18 | io.to(String(chatroomId)).emit(eventName.UPDATE_MESSAGE, updateMessage); 19 | }, 20 | async deleteMessage(io, socket, message) { 21 | const { messageId } = message; 22 | const messageInfo = await MessageService.getInstance().getMessage(messageId); 23 | const { chatroomId } = messageInfo.chatroom; 24 | await MessageService.getInstance().deleteMessage(messageId); 25 | io.to(String(chatroomId)).emit(eventName.DELETE_MESSAGE, { messageId }); 26 | } 27 | }; 28 | 29 | export default messageHandler; 30 | -------------------------------------------------------------------------------- /server/src/socket/handler/message-reaction-handler.ts: -------------------------------------------------------------------------------- 1 | import MessageReactionService from '@service/message-reaction-service'; 2 | import MessageServie from '@service/message-service'; 3 | import eventName from '@constants/event-name'; 4 | 5 | const messageHandler = { 6 | async createMessageReaction(io, socket, messageReaction) { 7 | const req = socket.request; 8 | const { userId } = req.user; 9 | const { messageId, title, emoji } = messageReaction; 10 | const newMessageReaction = await MessageReactionService.getInstance().createMessageReaction(Number(userId), Number(messageId), title, emoji); 11 | const { chatroomId } = (await MessageServie.getInstance().getMessage(messageId)).chatroom; 12 | io.to(String(chatroomId)).emit(eventName.CREATE_REACTION, { ...newMessageReaction, chatroomId }); 13 | }, 14 | 15 | async deleteMessageReaction(io, socket, messageReaction) { 16 | const req = socket.request; 17 | const { userId } = req.user; 18 | const { messageId, reactionId } = messageReaction; 19 | await MessageReactionService.getInstance().deleteMessageReaction(Number(userId), Number(messageId), Number(reactionId)); 20 | const newMessageReaction = await MessageReactionService.getInstance().getMessageReaction(Number(messageId), Number(reactionId)); 21 | const { chatroomId } = (await MessageServie.getInstance().getMessage(messageId)).chatroom; 22 | io.to(String(chatroomId)).emit(eventName.DELETE_REACTION, { ...newMessageReaction, chatroomId }); 23 | } 24 | }; 25 | 26 | export default messageHandler; 27 | -------------------------------------------------------------------------------- /server/src/socket/handler/reply-handler.ts: -------------------------------------------------------------------------------- 1 | import ReplyService from '@service/reply-service'; 2 | import eventName from '@constants/event-name'; 3 | 4 | const replyHandler = { 5 | async createReply(io, socket, reply) { 6 | const req = socket.request; 7 | const { userId } = req.user; 8 | const { messageId, content } = reply; 9 | const replyId = await ReplyService.getInstance().createReply(userId, messageId, content); 10 | const newReply = await ReplyService.getInstance().getReply(replyId); 11 | const replyInfo = await ReplyService.getInstance().getReplyInfo(replyId); 12 | const { chatroomId } = replyInfo.message.chatroom; 13 | io.to(String(chatroomId)).emit(eventName.CREATE_REPLY, { ...newReply, chatroomId, messageId }); 14 | }, 15 | 16 | async updateReply(io, socket, reply) { 17 | const { replyId, content, messageId } = reply; 18 | await ReplyService.getInstance().updateReply(replyId, content); 19 | const replyInfo = await ReplyService.getInstance().getReplyInfo(replyId); 20 | const { chatroomId } = replyInfo.message.chatroom; 21 | io.to(String(chatroomId)).emit(eventName.UPDATE_REPLY, { replyId, content, chatroomId, messageId }); 22 | }, 23 | 24 | async deleteReply(io, socket, reply) { 25 | const { replyId } = reply; 26 | const replyInfo = await ReplyService.getInstance().getReplyInfo(replyId); 27 | const { messageId } = replyInfo.message; 28 | const { chatroomId } = replyInfo.message.chatroom; 29 | await ReplyService.getInstance().deleteReply(replyId); 30 | io.to(String(chatroomId)).emit(eventName.DELETE_REPLY, { replyId, messageId, chatroomId }); 31 | } 32 | }; 33 | 34 | export default replyHandler; 35 | -------------------------------------------------------------------------------- /server/src/socket/handler/reply-reaction-handler.ts: -------------------------------------------------------------------------------- 1 | import ReplyReactionService from '@service/reply-reaction-service'; 2 | import MessageService from '@service/message-service'; 3 | import ReplyService from '@service/reply-service'; 4 | import eventName from '@constants/event-name'; 5 | 6 | const replyHandler = { 7 | async createReplyReaction(io, socket, messageReaction) { 8 | const req = socket.request; 9 | const { userId } = req.user; 10 | const { replyId, title, emoji } = messageReaction; 11 | const newReplyReaction = await ReplyReactionService.getInstance().createReplyReaction(Number(userId), Number(replyId), title, emoji); 12 | const { messageId } = (await ReplyService.getInstance().getReplyInfo(Number(replyId))).message; 13 | const { chatroomId } = (await MessageService.getInstance().getMessage(messageId)).chatroom; 14 | io.to(String(chatroomId)).emit(eventName.CREATE_REPLY_REACTION, { ...newReplyReaction, messageId }); 15 | }, 16 | 17 | async deleteReplytReaction(io, socket, replyReaction) { 18 | const req = socket.request; 19 | const { userId } = req.user; 20 | const { replyId, reactionId } = replyReaction; 21 | await ReplyReactionService.getInstance().deleteReplyReaction(Number(userId), Number(replyId), Number(reactionId)); 22 | const newReplyReaction = await ReplyReactionService.getInstance().getReplyReaction(Number(replyId), Number(reactionId)); 23 | const { messageId } = (await ReplyService.getInstance().getReplyInfo(Number(replyId))).message; 24 | const { chatroomId } = (await MessageService.getInstance().getMessage(messageId)).chatroom; 25 | io.to(String(chatroomId)).emit(eventName.DELETE_REPLY_REACTION, { ...newReplyReaction, messageId }); 26 | } 27 | }; 28 | 29 | export default replyHandler; 30 | -------------------------------------------------------------------------------- /server/src/socket/handler/socket-handler.ts: -------------------------------------------------------------------------------- 1 | import SocketService from '@service/socket-service'; 2 | 3 | const socketHandler = { 4 | async saveSocketId(io, socket) { 5 | const req = socket.request; 6 | const { userId } = req.user; 7 | const socketId = socket.id; 8 | SocketService.getInstance().createSocket(userId, socketId); 9 | }, 10 | 11 | async deleteSocketId(io, socket) { 12 | const socketId = socket.id; 13 | SocketService.getInstance().deleteSocket(socketId); 14 | } 15 | }; 16 | 17 | export default socketHandler; 18 | -------------------------------------------------------------------------------- /server/src/socket/index.ts: -------------------------------------------------------------------------------- 1 | import connectionEvent from '@socket/event/connection-event'; 2 | import messageEvent from '@socket/event/message-event'; 3 | import replyEvent from '@socket/event/reply-event'; 4 | import chatroomEvent from '@socket/event/chatroom-event'; 5 | import messageReactionEvent from '@socket/event/message-reaction-event'; 6 | import disconnectEvent from '@socket/event/disconnect-event'; 7 | import replyReactionEvent from '@socket/event/reply-rection-event'; 8 | 9 | function socketIndex(io, socket) { 10 | connectionEvent(io, socket); 11 | disconnectEvent(io, socket); 12 | messageEvent(io, socket); 13 | replyEvent(io, socket); 14 | chatroomEvent(io, socket); 15 | messageReactionEvent(io, socket); 16 | replyReactionEvent(io, socket); 17 | } 18 | 19 | export default socketIndex; 20 | -------------------------------------------------------------------------------- /server/src/socket/init-socket.ts: -------------------------------------------------------------------------------- 1 | import jwtMiddleware from '@socket/middleware/jwt'; 2 | import socketIndex from '@socket/index'; 3 | 4 | const initSocketIo = (io) => { 5 | jwtMiddleware(io); 6 | io.on('connection', (socket) => socketIndex(io, socket)); 7 | }; 8 | 9 | export default initSocketIo; 10 | -------------------------------------------------------------------------------- /server/src/socket/middleware/jwt.ts: -------------------------------------------------------------------------------- 1 | import passport from 'passport'; 2 | 3 | const jwtMiddleware = (io) => { 4 | const wrap = (middleware) => (socket, next) => { 5 | const { request } = socket; 6 | const { token } = socket.handshake.query; 7 | request.headers.authorization = token; 8 | middleware(request, {}, next); 9 | }; 10 | io.use(wrap(passport.authenticate('jwt', { session: false }))); 11 | }; 12 | 13 | export default jwtMiddleware; 14 | -------------------------------------------------------------------------------- /server/src/socket/socket.ts: -------------------------------------------------------------------------------- 1 | import { Server } from 'socket.io'; 2 | 3 | const Socket = (server) => { 4 | const io = new Server(server, { 5 | cors: { 6 | origin: '*', 7 | methods: ['GET', 'POST'] 8 | } 9 | }); 10 | return io; 11 | }; 12 | 13 | export default Socket; 14 | -------------------------------------------------------------------------------- /server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["es2015", "es2016", "es2017", "es2018", "es2019", "es2020"], 4 | "outDir": "./dist", 5 | "target": "es5", 6 | "sourceMap": true, 7 | 8 | "module": "commonjs", 9 | "moduleResolution": "node", 10 | "esModuleInterop": true, 11 | "emitDecoratorMetadata": true, 12 | "experimentalDecorators": true, 13 | 14 | "baseUrl": ".", 15 | "paths": { 16 | "@constants/*": ["src/common/constants/*"], 17 | "@error/*": ["src/common/error/*"], 18 | "@middleware/*": ["src/common/middleware/*"], 19 | "@utils/*": ["src/common/utils/*"], 20 | "@controller/*": ["src/controller/*"], 21 | "@model/*": ["src/model/*"], 22 | "@repository/*": ["src/repository/*"], 23 | "@router/*": ["src/router/*"], 24 | "@service/*": ["src/service/*"], 25 | "@config/*":["src/common/config/*"], 26 | "@socket/*":["src/socket/*"] 27 | } 28 | }, 29 | } --------------------------------------------------------------------------------