├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── CI.yml │ ├── ReleaseNCP.yml │ ├── releaseRepo.yml │ ├── slack-notice.yml │ └── testNCP.yml ├── .gitignore ├── README.md ├── backend ├── .eslintrc.json ├── .gitignore ├── .prettierrc.json ├── build.ts ├── package.json ├── src │ ├── @types │ │ └── express │ │ │ └── index.d.ts │ ├── Server.ts │ ├── config │ │ ├── GithubPassport.ts │ │ ├── LocalPassport.ts │ │ └── Upload.ts │ ├── enum │ │ └── index.ts │ ├── index.ts │ ├── model │ │ ├── Channel.ts │ │ ├── Dm.ts │ │ ├── File.ts │ │ ├── Reaction.ts │ │ ├── Reply.ts │ │ ├── Thread.ts │ │ ├── User.ts │ │ ├── UserHasWorkspace.ts │ │ └── Workspace.ts │ ├── ormconfig.ts │ ├── pre-start │ │ └── index.ts │ ├── public │ │ ├── scripts │ │ │ └── index.js │ │ └── stylesheets │ │ │ └── style.css │ ├── repository │ │ ├── ChannelRepository.ts │ │ ├── FileRepository.ts │ │ ├── ReactionRepository.ts │ │ ├── ReplyRepository.ts │ │ ├── ThreadRepository.ts │ │ ├── UserHasWorkspaceRepository.ts │ │ ├── UserRepository.ts │ │ └── WorkspaceRepository.ts │ ├── routes │ │ ├── ChannelController.ts │ │ ├── FilesController.ts │ │ ├── LoginController.ts │ │ ├── ReactionController.ts │ │ ├── ReplyController.ts │ │ ├── ThreadController.ts │ │ ├── UserController.ts │ │ ├── UserHasWorkspaceController.ts │ │ ├── WorkspaceController.ts │ │ └── index.ts │ ├── sample │ │ ├── InsertChannelSample.ts │ │ ├── InsertFileSample.ts │ │ ├── InsertReactionSample.ts │ │ ├── InsertReplySample.ts │ │ ├── InsertThreadSample.ts │ │ ├── InsertUserHasWorkspace.ts │ │ ├── InsertUserSample.ts │ │ ├── InsertWorkspaceSample.ts │ │ ├── index.ts │ │ └── value │ │ │ ├── ChannelSampleValue.ts │ │ │ ├── EmotionSampleValue.ts │ │ │ ├── FileSampleValue.ts │ │ │ ├── ReactionSampleValue.ts │ │ │ ├── ReplySampleValue.ts │ │ │ ├── ThreadSampleValue.ts │ │ │ ├── UserHasWorkspaceSampleValue.ts │ │ │ ├── UserSampleValue.ts │ │ │ └── WorkspaceSampleValue.ts │ ├── service │ │ ├── ChannelService.ts │ │ ├── FilesService.ts │ │ ├── ReactionService.ts │ │ ├── ReplyService.ts │ │ ├── ThreadService.ts │ │ ├── UserHasWorkspaceService.ts │ │ ├── UserService.ts │ │ └── WorkspaceService.ts │ ├── shared │ │ ├── Logger.ts │ │ ├── constants.ts │ │ ├── functions.ts │ │ └── simpleuuid.ts │ ├── socket.ts │ ├── util │ │ └── getNowDate.ts │ └── views │ │ └── index.html ├── tsconfig.json ├── tsconfig.prod.json └── yarn.lock └── frontend ├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── .prettierrc.json ├── jest.config.js ├── package.json ├── public ├── favicon.ico └── index.html ├── src ├── App.tsx ├── components │ ├── atoms │ │ ├── Autocomplete │ │ │ └── index.tsx │ │ ├── DivLists │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ ├── ErrorBoundary │ │ │ └── index.tsx │ │ ├── IconButton │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ ├── ImageBox │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ ├── ImageButton │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ ├── Input │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ ├── Label │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ ├── LabeledButton │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ ├── LabeledDefaultButton │ │ │ └── index.tsx │ │ ├── Modal │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ ├── Popup │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ ├── RadioButton │ │ │ └── index.tsx │ │ ├── Spinner │ │ │ ├── index.tsx │ │ │ └── style.ts │ │ ├── ToggleButton │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ └── ViewPortInput │ │ │ ├── index.tsx │ │ │ └── styles.ts │ ├── molecules │ │ ├── AsyncBranch │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ ├── BrowseChannelHeader │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ ├── ChannelList │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ ├── ChatHeader │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ ├── CodeModal │ │ │ ├── index.tsx │ │ │ └── style.ts │ │ ├── EmojiPopup │ │ │ ├── EmojiPopupTemplate │ │ │ │ ├── index.tsx │ │ │ │ └── styles.ts │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ ├── LabeledInput │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ ├── MentionPopup │ │ │ ├── MentionPopupTemplate │ │ │ │ ├── index.tsx │ │ │ │ └── styles.ts │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ ├── MessageContent │ │ │ ├── MessageActions │ │ │ │ ├── index.tsx │ │ │ │ └── styles.ts │ │ │ ├── MessageFileStatusBar │ │ │ │ ├── index.tsx │ │ │ │ └── styles.ts │ │ │ ├── MessageFileStatusElement │ │ │ │ ├── index.tsx │ │ │ │ └── styles.ts │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ ├── NoOverlayModal │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ ├── QuestionForm │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ ├── SearchBar │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ ├── SelectWorkspace │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ ├── SelectbrowseChannelPage │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ ├── SidebarAddElement │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ ├── SidebarChannelElement │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ ├── SidebarDivision │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ ├── SortedOptionMordal │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ ├── ThreadContent │ │ │ ├── ThreadActions │ │ │ │ ├── index.tsx │ │ │ │ └── styles.ts │ │ │ ├── ThreadFileStatusBar │ │ │ │ ├── index.tsx │ │ │ │ └── styles.ts │ │ │ ├── ThreadFileStatusElement │ │ │ │ ├── index.tsx │ │ │ │ └── styles.ts │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ └── WysiwygEditor │ │ │ ├── index.tsx │ │ │ └── styles.ts │ ├── organisms │ │ ├── BrowseChannelList │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ ├── BrowseContent │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ ├── BrowseMordalContainer │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ ├── ChangePasswordContent │ │ │ ├── index.tsx │ │ │ └── style.ts │ │ ├── ChannelAbout │ │ │ ├── AboutElement │ │ │ │ ├── index.tsx │ │ │ │ └── styles.ts │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ ├── ChannelDescriptionModal │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ ├── ChannelInfoModal │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ ├── ChannelJoinFooter │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ ├── ChannelMembers │ │ │ ├── MemberTemplate │ │ │ │ ├── index.tsx │ │ │ │ └── styles.ts │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ ├── ChannelTopicModal │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ ├── ChatContent │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ ├── ChatInputBar │ │ │ ├── FileStatusBar │ │ │ │ ├── index.tsx │ │ │ │ └── styles.ts │ │ │ ├── FileStatusElement │ │ │ │ ├── index.tsx │ │ │ │ └── styles.ts │ │ │ ├── Toolbar │ │ │ │ ├── index.tsx │ │ │ │ └── styles.ts │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ ├── ChatInputBarForUpdate │ │ │ ├── index.tsx │ │ │ ├── styles.ts │ │ │ └── toolbar │ │ │ │ ├── index.tsx │ │ │ │ └── styles.ts │ │ ├── CreateChannelModal │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ ├── CreateLoginModal │ │ │ ├── index.tsx │ │ │ └── style.ts │ │ ├── EmojiModal │ │ │ ├── EmojiPopupTemplate │ │ │ │ ├── index.tsx │ │ │ │ └── styles.ts │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ ├── LoginContent │ │ │ ├── index.tsx │ │ │ └── style.ts │ │ ├── MemberElement │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ ├── PreferenceModal │ │ │ ├── AboutUs │ │ │ │ └── AboutUs.tsx │ │ │ ├── PreferenceMenuContent │ │ │ │ ├── index.tsx │ │ │ │ └── styles.ts │ │ │ ├── ThemeSelect │ │ │ │ ├── index.tsx │ │ │ │ └── styles.ts │ │ │ ├── WorkspaceOut │ │ │ │ ├── index.tsx │ │ │ │ └── styles.ts │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ ├── ReactionBar │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ ├── ReplyBar │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ ├── ReplyContent │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ ├── SetupTeamQuestions │ │ │ ├── index.tsx │ │ │ └── style.ts │ │ ├── SidebarChannelInfoModal │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ ├── SignupContent │ │ │ ├── index.tsx │ │ │ └── style.ts │ │ ├── SubmitCodeForm │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ ├── UserProfileModal │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ ├── WorkspaceContent │ │ │ ├── index.tsx │ │ │ └── style.ts │ │ ├── WorkspaceHeader │ │ │ ├── SearchResultTemplate │ │ │ │ ├── ChannelElement │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── styles.ts │ │ │ │ ├── SearchResultModal │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── styles.ts │ │ │ │ ├── index.tsx │ │ │ │ └── styles.ts │ │ │ ├── WorkspaceHeaderMenuList │ │ │ │ ├── index.tsx │ │ │ │ └── styles.ts │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ ├── WorkspaceListContent │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ └── WorkspaceSidebar │ │ │ ├── index.tsx │ │ │ └── style.ts │ ├── pages │ │ ├── BrowseChannel │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ ├── Changepassword │ │ │ ├── index.tsx │ │ │ └── style.ts │ │ ├── GeneratedCode │ │ │ ├── index.tsx │ │ │ └── style.ts │ │ ├── GetError │ │ │ ├── index.tsx │ │ │ └── style.ts │ │ ├── InvitedCode │ │ │ ├── index.tsx │ │ │ └── style.ts │ │ ├── Loading │ │ │ ├── index.tsx │ │ │ └── style.ts │ │ ├── Login │ │ │ ├── index.tsx │ │ │ └── style.ts │ │ ├── NotFound │ │ │ ├── index.tsx │ │ │ └── style.ts │ │ ├── NotLogin │ │ │ ├── index.tsx │ │ │ └── style.ts │ │ ├── NotLogout │ │ │ ├── index.tsx │ │ │ └── style.ts │ │ ├── SetupTeam │ │ │ ├── index.tsx │ │ │ └── style.ts │ │ ├── Signup │ │ │ ├── index.tsx │ │ │ └── style.ts │ │ ├── Workspace │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ └── WorkspaceList │ │ │ ├── index.tsx │ │ │ └── styles.ts │ └── templates │ │ ├── Code │ │ ├── index.tsx │ │ └── styles.ts │ │ ├── EmptyWorkspace │ │ ├── index.tsx │ │ └── styles.ts │ │ ├── Workspace │ │ ├── index.tsx │ │ └── styles.ts │ │ └── WorkspaceList │ │ ├── index.tsx │ │ └── styles.ts ├── enum │ └── index.ts ├── global │ ├── api │ │ ├── channel │ │ │ └── index.ts │ │ ├── index.ts │ │ ├── login │ │ │ └── index.ts │ │ ├── reaction │ │ │ └── index.ts │ │ ├── reply │ │ │ └── index.ts │ │ ├── thread │ │ │ └── index.ts │ │ └── workspace │ │ │ └── index.tsx │ ├── image │ │ ├── black.png │ │ ├── blue.png │ │ ├── default_account.png │ │ ├── title_booslack.png │ │ ├── violet.png │ │ ├── wavingHandFromSlack.gif │ │ ├── workspace_default_image.png │ │ ├── xMark.svg │ │ └── yellow.png │ ├── module.d.ts │ ├── options │ │ └── index.ts │ ├── style │ │ ├── index.ts │ │ ├── mixin │ │ │ └── index.ts │ │ └── theme │ │ │ └── index.ts │ ├── type │ │ └── index.ts │ └── util │ │ ├── auth.ts │ │ ├── file.ts │ │ ├── index.ts │ │ ├── inputEventHandlers.ts │ │ ├── message.ts │ │ ├── reactQueryUtil.ts │ │ ├── reaction.ts │ │ ├── reply.ts │ │ ├── transfromDate.ts │ │ └── validatePassword.ts ├── hooks │ ├── useAbstract.ts │ ├── useAsync.ts │ ├── useChannels.ts │ ├── useInfinityPage.ts │ ├── useInputs.ts │ ├── useKeyboardNavigator.ts │ ├── usePagination.ts │ ├── useRefLocate.ts │ ├── useReplys.ts │ ├── useSocket.ts │ ├── useThreads.ts │ ├── useUsers.ts │ └── useWorkspace.ts ├── index.tsx ├── routes │ ├── PrivateRoute.tsx │ └── PublicRoute.tsx ├── state │ ├── Channel │ │ └── index.ts │ ├── modal │ │ └── index.ts │ ├── theme │ │ └── index.ts │ ├── thread │ │ └── index.ts │ ├── user │ │ └── index.ts │ └── workspace │ │ └── index.ts └── test │ ├── DefaultEnvironment.tsx │ ├── page.test.tsx │ ├── router.test.tsx │ └── workspacelist.test.tsx ├── tsconfig.json ├── webpack.config.js ├── webpack.prod.config.js └── yarn.lock /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: 버그 발생 부분을 적는 bug report입니다. 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | # 📃 버그 설명 11 | 12 | 자세하게 설명하기 13 | 14 | # 📃 버그 발생 부분 설명 15 | 16 | Ex) 17 | 1. Go to '...' 18 | 2. Click on '....' 19 | 3. Scroll down to '....' 20 | 4. See error 21 | 22 | # 📃 버그 발생 부분 화면 23 | 24 | # 📃 추가 메모 사항 25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: feature 탬플릿입니다. 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | # 📃 feature 설명 11 | 12 | 자세하게 설명하기 13 | 14 | # 📃 feature 구현 목록 15 | 16 | Ex) 17 | 1. Go to '...' 18 | 2. Click on '....' 19 | 3. Scroll down to '....' 20 | 21 | # 📃 feature 구현시 고려할 사항 22 | 23 | # 📃 추가 메모 사항 24 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## 📃 PR 제목 2 | 3 | ## 📃 작업 목록 4 | 5 | Ex) 6 | - [ ] Go to '...' 7 | - [ ] Click on '....' 8 | - [ ] Scroll down to '....' 9 | 10 | ## 📃 주의 사항 11 | 12 | ## 📃 추가 메모 사항 13 | -------------------------------------------------------------------------------- /.github/workflows/CI.yml: -------------------------------------------------------------------------------- 1 | name: CI test 2 | 3 | on: 4 | push: 5 | branches-ignore: 6 | - master 7 | - dev 8 | - release 9 | - old-release 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | 15 | strategy: 16 | matrix: 17 | node-version: [14.x] 18 | 19 | steps: 20 | - uses: actions/checkout@v2 21 | 22 | - name: Use Node.js ${{ matrix.node-version }} 23 | uses: actions/setup-node@v1 24 | with: 25 | node-version: ${{ matrix.node-version }} 26 | - name: CI test for front 27 | run: | 28 | cd ./frontend/ 29 | yarn install 30 | yarn jest 31 | env: 32 | CI: '' 33 | -------------------------------------------------------------------------------- /.github/workflows/ReleaseNCP.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Deployment for main release 2 | on: 3 | push: 4 | branches: [main] 5 | jobs: 6 | deploy: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Deploy to Server 10 | uses: appleboy/ssh-action@master 11 | with: 12 | password: ${{ secrets.MAINPASSWORD }} 13 | host: ${{ secrets.MAINHOST }} 14 | username: ${{ secrets.MAINUSERNAME }} 15 | port: ${{ secrets.MAINPORT }} 16 | script: | 17 | sh ~/autoCD.sh 2>&1 18 | -------------------------------------------------------------------------------- /.github/workflows/releaseRepo.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: CD - release repo automatically 5 | 6 | on: 7 | pull_request_target: 8 | types: [closed] 9 | branches: [dev] 10 | 11 | jobs: 12 | backup: 13 | name: backup 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v2 17 | with: 18 | token: ${{ secrets.web06TOKEN}} 19 | ref: 'release' 20 | - name: backup - release 21 | run: | 22 | git checkout -b old-release 23 | git push -f origin old-release 24 | release: 25 | name: release 26 | needs: backup 27 | runs-on: ubuntu-latest 28 | steps: 29 | - uses: actions/checkout@v2 30 | with: 31 | token: ${{ secrets.web06TOKEN}} 32 | ref: 'dev' 33 | - name: update release 34 | run: | 35 | git checkout -b release 36 | git push -f origin release 37 | -------------------------------------------------------------------------------- /.github/workflows/slack-notice.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: slack-notice CI 5 | 6 | on: 7 | push: 8 | branches: [main, dev] 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: 8398a7/action-slack@v3 16 | with: 17 | status: ${{ job.status }} 18 | fields: repo,message,commit,author,action,eventName,ref,workflow,job,took # selectable (default: repo,message) 19 | env: 20 | GITHUB_TOKEN: ${{ secrets.web06TOKEN }} # required 21 | SLACK_WEBHOOK_URL: ${{ secrets.SLACKURL }} # required 22 | if: always() # Pick up events even if the job fails or is canceled. 23 | -------------------------------------------------------------------------------- /.github/workflows/testNCP.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Deployment for Test release 2 | on: 3 | push: 4 | branches: [release] 5 | jobs: 6 | deploy: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Deploy to Server 10 | uses: appleboy/ssh-action@master 11 | with: 12 | password: ${{ secrets.PASSWORD }} 13 | host: ${{ secrets.HOST }} 14 | username: ${{ secrets.USERNAME }} 15 | port: ${{ secrets.PORT }} 16 | script: | 17 | sh /git/autoCD.sh 2>&1 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🖥 booslack 2 | 3 | > 🔗 배포 링크 : http://118.67.128.103:3001/ (중지) 4 | 5 | 샘플 아이디 : ycpycpycp | 비밀번호 : Ycpycp123! 6 | 7 | > ✨ 데모 링크 : https://youtu.be/DyyKPh8H8-U 8 | 9 | booslack은 팀원들과 원활한 소통을 할 수 있게 채팅 및 화상회의를 지원해주는 협업 도구입니다. 10 | 11 |

12 | 13 |

14 | 15 | # 👨‍👧‍👦 Member 16 | 17 | | 스크린샷_2021-11-29_오후_9 36 46 | 스크린샷_2021-11-29_오후_9 36 46 | 스크린샷_2021-11-29_오후_9 36 46 | 스크린샷_2021-11-29_오후_9 36 46 | 18 | |------|------|------|------| 19 | | 🥐 설민욱 | 🍛이충헌 | 🍣박주원 | 🍜조진성 | 20 | | [@blogSoul](https://github.com/blogSoul)
백엔드 담당 | [@lodado](https://github.com/lodado)
프론트엔드 담당 | [@laz](https://github.com/laz)
프론트엔드 담당 | [@loin3](https://github.com/loin3)
백엔드 담당 | 21 | 22 | ## ⚙ 기술 스택 23 | 24 | 스크린샷 2021-11-29 오후 9 36 46 (1) 25 | 26 | # 📕 Detail 27 | 28 | ### `booslack` 프로젝트를 더 보고 싶다면 [아래 링크](https://github.com/boostcampwm-2021/web06-booslack/wiki)를 클릭해주세요! 29 | 30 | (2022-01-07 기준으로 배포 서버 변경했습니다) 31 | -------------------------------------------------------------------------------- /backend/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es2021": true, 4 | "node": true 5 | }, 6 | "extends": [ 7 | "eslint:recommended", 8 | "plugin:@typescript-eslint/recommended", 9 | "plugin:@typescript-eslint/recommended-requiring-type-checking", 10 | "airbnb-base" 11 | ], 12 | "parser": "@typescript-eslint/parser", 13 | "parserOptions": { 14 | "ecmaVersion": 12, 15 | "sourceType": "module", 16 | "project": "./tsconfig.json" 17 | }, 18 | "plugins": ["@typescript-eslint"], 19 | "rules": { 20 | "max-len": [ 21 | "error", 22 | { 23 | "code": 100 24 | } 25 | ], 26 | "no-console": 1, 27 | "no-extra-boolean-cast": 0, 28 | "@typescript-eslint/restrict-plus-operands": 0, 29 | "@typescript-eslint/explicit-module-boundary-types": 0, 30 | "@typescript-eslint/no-explicit-any": 0, 31 | "@typescript-eslint/no-floating-promises": 0, 32 | "@typescript-eslint/no-unsafe-member-access": 0, 33 | "@typescript-eslint/no-unsafe-assignment": 0, 34 | "import/no-unresolved": "off", 35 | "import/extensions": "off", 36 | "object-curly-newline": "off", 37 | "@typescript-eslint/no-misused-promises": [ 38 | "error", 39 | { 40 | "checksVoidReturn": false, 41 | "checksConditionals": false 42 | } 43 | ] 44 | }, 45 | "settings": { 46 | "import/resolver": { 47 | "node": { 48 | "extensions": [".js", ".jsx", ".ts", ".tsx"] 49 | } 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /backend/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": true, 4 | "tabWidth": 2, 5 | "trailingComma": "all", 6 | "useTabs": false, 7 | "printWidth": 100, 8 | "arrowParens": "always", 9 | "bracketSpacing": true, 10 | "endOfLine": "lf" 11 | } 12 | -------------------------------------------------------------------------------- /backend/src/@types/express/index.d.ts: -------------------------------------------------------------------------------- 1 | import { IChannel } from 'src/model/Channel'; 2 | import { IUser } from 'src/model/User'; 3 | import { IWorkspace } from 'src/model/Workspace'; 4 | 5 | declare module 'express' { 6 | export interface Request { 7 | body: { 8 | user: IUser; 9 | nickname: string; 10 | email: string; 11 | type: string; 12 | workspace: IWorkspace; 13 | name: string; 14 | password: string; 15 | profile: string; 16 | channel: IChannel; 17 | description: string; 18 | message: string; 19 | time: Date; 20 | code: string; 21 | theme: string; 22 | fileId: number; 23 | topic: string; 24 | emoji: string; 25 | files: any; 26 | 27 | userId: string; 28 | channelId: string; 29 | workspaceId: string; 30 | userHasWorkspaceId: string; 31 | threadId: string; 32 | replyId: string; 33 | reactionId: string; 34 | }; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /backend/src/config/Upload.ts: -------------------------------------------------------------------------------- 1 | import multer from 'multer'; 2 | import multerS3 from 'multer-s3'; 3 | import AWS from 'aws-sdk'; 4 | import { FILE_LIMIT_SIZE } from '../enum'; 5 | 6 | const s3 = new AWS.S3({ 7 | endpoint: process.env.NCLOUD_S3_ENDPOINT, 8 | accessKeyId: process.env.NCLOUD_ACCESS_KEY, 9 | secretAccessKey: process.env.NCLOUD_SECRET_KEY, 10 | region: process.env.NCLOUD_REGION, 11 | }); 12 | 13 | const storage = multerS3({ 14 | s3, 15 | bucket: process.env.NCLOUD_BUCKET || 'booslack', 16 | // eslint-disable-next-line @typescript-eslint/unbound-method 17 | contentType: multerS3.AUTO_CONTENT_TYPE, 18 | acl: 'public-read', 19 | key: (req, file, callback) => { 20 | callback(null, file.originalname); 21 | }, 22 | metadata: (req, file, callback) => { 23 | callback(null, { fieldName: file.fieldname }); 24 | }, 25 | }); 26 | 27 | const Upload = multer({ 28 | storage, 29 | limits: { fileSize: FILE_LIMIT_SIZE }, 30 | }); 31 | 32 | export default Upload; 33 | -------------------------------------------------------------------------------- /backend/src/enum/index.ts: -------------------------------------------------------------------------------- 1 | export const OFFSET_START = 0; 2 | 3 | export const PAGE_LIMIT_COUNT = 10; 4 | 5 | export const LOCALTYPE_GITHUB = 0; 6 | 7 | export const LOCALTYPE_LOCAL = 1; 8 | 9 | export const FILE_LIMIT_SIZE = 5 * 1024 * 1024; 10 | 11 | export const WORKSPACELIST_PAGE_LIMIT_COUNT = 7; 12 | -------------------------------------------------------------------------------- /backend/src/index.ts: -------------------------------------------------------------------------------- 1 | import './pre-start'; // Must be the first import 2 | import { createConnection } from 'typeorm'; 3 | import { createServer } from 'http'; 4 | import app from './Server'; 5 | import logger from './shared/Logger'; 6 | import 'reflect-metadata'; 7 | import connectionOptions from './ormconfig'; 8 | import addSampleData from './sample'; 9 | import initializeSocket from './socket'; 10 | 11 | createConnection(connectionOptions) 12 | .then(() => { 13 | // Start the server 14 | const port = Number(process.env.PORT || 3000); 15 | const httpServer = createServer(app); 16 | initializeSocket(httpServer); 17 | httpServer.listen(port, () => { 18 | logger.info(`Express server started on port: ${port}`); 19 | }); 20 | }) 21 | .then(() => { 22 | if (process.env.DB_AUTO_ADD_DATA === 'true') addSampleData(); 23 | }) 24 | .catch((error) => console.log(error)); 25 | -------------------------------------------------------------------------------- /backend/src/model/Channel.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Column, 3 | ManyToOne, 4 | ManyToMany, 5 | JoinTable, 6 | OneToMany, 7 | Entity, 8 | PrimaryGeneratedColumn, 9 | CreateDateColumn, 10 | } from 'typeorm'; 11 | import Thread from './Thread'; 12 | import UserHasWorkspace from './UserHasWorkspace'; 13 | import Workspace from './Workspace'; 14 | 15 | @Entity() 16 | class Channel { 17 | @PrimaryGeneratedColumn() 18 | id!: number; 19 | 20 | @Column() 21 | name!: string; 22 | 23 | @Column({ nullable: true }) 24 | description!: string; 25 | 26 | @Column({ nullable: true }) 27 | topic!: string; 28 | 29 | @Column({ type: 'tinyint' }) 30 | private!: number; 31 | 32 | @Column({ nullable: true }) 33 | workspaceId!: number; 34 | 35 | @CreateDateColumn({ type: 'timestamp' }) 36 | createdAt!: Date 37 | 38 | @OneToMany(() => Thread, (thread) => thread.channel) 39 | threads!: Thread[]; 40 | 41 | @ManyToOne(() => Workspace, (workspace) => workspace.channels) 42 | workspace!: Workspace; 43 | 44 | @ManyToMany(() => UserHasWorkspace) 45 | @JoinTable({ 46 | name: 'user_has_workspace_channel', 47 | }) 48 | userHasWorkspaces!: UserHasWorkspace[]; 49 | } 50 | 51 | export default Channel; 52 | -------------------------------------------------------------------------------- /backend/src/model/Dm.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Entity, Column, 3 | ManyToOne, OneToMany, 4 | PrimaryGeneratedColumn, CreateDateColumn, 5 | } from 'typeorm'; 6 | import Thread from './Thread'; 7 | import Workspace from './Workspace'; 8 | 9 | @Entity() 10 | class Dm { 11 | @PrimaryGeneratedColumn() 12 | id!: number 13 | 14 | @Column() 15 | name!: string; 16 | 17 | @Column({ nullable: true }) 18 | description!: string; 19 | 20 | @Column({ nullable: true }) 21 | topic!: string; 22 | 23 | @Column({ nullable: true }) 24 | workspaceId!: number; 25 | 26 | @CreateDateColumn({ type: 'timestamp' }) 27 | createdAt!: Date 28 | 29 | @OneToMany(() => Thread, (thread) => thread.dm) 30 | threads!: Thread[]; 31 | 32 | @ManyToOne(() => Workspace, (workspace) => workspace.dms) 33 | workspace!: Workspace; 34 | } 35 | 36 | export default Dm; 37 | -------------------------------------------------------------------------------- /backend/src/model/File.ts: -------------------------------------------------------------------------------- 1 | import { Entity, Column, CreateDateColumn, ManyToOne, PrimaryGeneratedColumn } from 'typeorm'; 2 | import Thread from './Thread'; 3 | import Reply from './Reply'; 4 | 5 | @Entity() 6 | class File { 7 | @PrimaryGeneratedColumn() 8 | id!: number; 9 | 10 | @Column() 11 | name!: string; 12 | 13 | @Column({ nullable: true }) 14 | url!: string; 15 | 16 | @Column() 17 | extension!: string; 18 | 19 | @Column({ nullable: true }) 20 | threadId!: number; 21 | 22 | @Column({ nullable: true }) 23 | replyId!: number; 24 | 25 | @CreateDateColumn({ type: 'timestamp' }) 26 | createdAt!: Date; 27 | 28 | @ManyToOne(() => Thread, (thread) => thread.files, { onUpdate: 'CASCADE' }) 29 | thread!: Thread; 30 | 31 | @ManyToOne(() => Reply, (reply) => reply.files, { onUpdate: 'CASCADE' }) 32 | reply!: Reply; 33 | } 34 | 35 | export default File; 36 | -------------------------------------------------------------------------------- /backend/src/model/Reaction.ts: -------------------------------------------------------------------------------- 1 | import { Column, ManyToOne, Entity, PrimaryGeneratedColumn, CreateDateColumn } from 'typeorm'; 2 | import Thread from './Thread'; 3 | import Reply from './Reply'; 4 | import UserHasWorkspace from './UserHasWorkspace'; 5 | 6 | @Entity() 7 | class Reaction { 8 | @PrimaryGeneratedColumn() 9 | id!: number; 10 | 11 | @Column({ nullable: true }) 12 | threadId!: number; 13 | 14 | @Column({ nullable: true }) 15 | emoji!: string; 16 | 17 | @Column({ nullable: true }) 18 | userHasWorkspaceId!: number; 19 | 20 | @Column({ nullable: true }) 21 | replyId!: number; 22 | 23 | @CreateDateColumn({ type: 'timestamp' }) 24 | createdAt!: Date; 25 | 26 | @ManyToOne(() => Thread, (thread) => thread.reactions, { onDelete: 'CASCADE' }) 27 | thread!: Thread; 28 | 29 | @ManyToOne(() => UserHasWorkspace, (userHasWorkspace) => userHasWorkspace.reactions, { 30 | onDelete: 'SET NULL', 31 | }) 32 | userHasWorkspace!: UserHasWorkspace; 33 | 34 | @ManyToOne(() => Reply, (reply) => reply.reactions, { onDelete: 'CASCADE' }) 35 | reply!: Reply; 36 | } 37 | 38 | export default Reaction; 39 | -------------------------------------------------------------------------------- /backend/src/model/Reply.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Column, 3 | ManyToOne, 4 | Entity, 5 | UpdateDateColumn, 6 | OneToMany, 7 | PrimaryGeneratedColumn, 8 | CreateDateColumn, 9 | } from 'typeorm'; 10 | import UserHasWorkspace from './UserHasWorkspace'; 11 | import Thread from './Thread'; 12 | import Reaction from './Reaction'; 13 | import File from './File'; 14 | 15 | @Entity() 16 | class Reply { 17 | @PrimaryGeneratedColumn() 18 | id!: number; 19 | 20 | @Column() 21 | message!: string; 22 | 23 | @Column({ nullable: true }) 24 | threadId!: number; 25 | 26 | @Column({ nullable: true }) 27 | userHasWorkspaceId!: number; 28 | 29 | @CreateDateColumn({ type: 'timestamp' }) 30 | createdAt!: Date; 31 | 32 | @UpdateDateColumn({ type: 'timestamp', nullable: true }) 33 | updatedAt!: Date; 34 | 35 | @OneToMany(() => Reaction, (reaction) => reaction.reply) 36 | reactions!: Reaction[]; 37 | 38 | @OneToMany(() => File, (file) => file.reply) 39 | files!: File[]; 40 | 41 | @ManyToOne(() => Thread, (thread) => thread.replys, { onDelete: 'CASCADE' }) 42 | thread!: Thread; 43 | 44 | @ManyToOne(() => UserHasWorkspace, (userHasWorkspace) => userHasWorkspace.replys, { 45 | onDelete: 'SET NULL', 46 | }) 47 | userHasWorkspace!: UserHasWorkspace; 48 | } 49 | 50 | export default Reply; 51 | -------------------------------------------------------------------------------- /backend/src/model/Thread.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Column, 3 | ManyToOne, 4 | OneToMany, 5 | UpdateDateColumn, 6 | Entity, 7 | PrimaryGeneratedColumn, 8 | CreateDateColumn, 9 | } from 'typeorm'; 10 | import Dm from './Dm'; 11 | import Channel from './Channel'; 12 | import UserHasWorkspace from './UserHasWorkspace'; 13 | import Reply from './Reply'; 14 | import File from './File'; 15 | import Reaction from './Reaction'; 16 | 17 | @Entity() 18 | class Thread { 19 | @PrimaryGeneratedColumn() 20 | id!: number; 21 | 22 | @Column() 23 | message!: string; 24 | 25 | @Column({ nullable: true }) 26 | channelId!: number; 27 | 28 | @Column({ nullable: true }) 29 | dmId!: number; 30 | 31 | @Column({ nullable: true }) 32 | userHasWorkspaceId!: number; 33 | 34 | @CreateDateColumn({ type: 'timestamp' }) 35 | createdAt!: Date; 36 | 37 | @UpdateDateColumn({ type: 'timestamp', nullable: true }) 38 | updatedAt!: Date; 39 | 40 | @OneToMany(() => Reply, (reply) => reply.thread) 41 | replys!: Reply[]; 42 | 43 | @OneToMany(() => File, (file) => file.thread) 44 | files!: File[]; 45 | 46 | @OneToMany(() => Reaction, (reaction) => reaction.thread) 47 | reactions!: Reaction[]; 48 | 49 | @ManyToOne(() => Channel, (channel) => channel.threads) 50 | channel!: Channel; 51 | 52 | @ManyToOne(() => Dm, (dm) => dm.threads) 53 | dm!: Dm; 54 | 55 | @ManyToOne(() => UserHasWorkspace, (userHasWorkspace) => userHasWorkspace.threads, { 56 | onDelete: 'SET NULL', 57 | }) 58 | userHasWorkspace!: UserHasWorkspace; 59 | } 60 | 61 | export default Thread; 62 | -------------------------------------------------------------------------------- /backend/src/model/User.ts: -------------------------------------------------------------------------------- 1 | import { Column, OneToMany, Entity, PrimaryGeneratedColumn, CreateDateColumn } from 'typeorm'; 2 | import UserHasWorkspace from './UserHasWorkspace'; 3 | 4 | @Entity() 5 | class User { 6 | @PrimaryGeneratedColumn() 7 | id!: number; 8 | 9 | @Column() 10 | account!: string; 11 | 12 | @Column({ nullable: true }) 13 | email!: string; 14 | 15 | @Column({ nullable: true }) 16 | password!: string; 17 | 18 | @Column({ type: 'tinyint' }) 19 | local!: number; 20 | 21 | @Column('int', { default: 1 }) 22 | theme!: number; 23 | 24 | @CreateDateColumn({ type: 'timestamp' }) 25 | createdAt!: Date; 26 | 27 | @OneToMany(() => UserHasWorkspace, (userHasWorkspace) => userHasWorkspace.user) 28 | userHasWorkspaces!: UserHasWorkspace[]; 29 | } 30 | 31 | export default User; 32 | -------------------------------------------------------------------------------- /backend/src/model/UserHasWorkspace.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Column, 3 | ManyToOne, 4 | OneToMany, 5 | Entity, 6 | PrimaryGeneratedColumn, 7 | CreateDateColumn, 8 | } from 'typeorm'; 9 | import User from './User'; 10 | import Workspace from './Workspace'; 11 | import Thread from './Thread'; 12 | import Reply from './Reply'; 13 | import Reaction from './Reaction'; 14 | 15 | @Entity() 16 | class UserHasWorkspace { 17 | @PrimaryGeneratedColumn() 18 | id!: number; 19 | 20 | @Column({ nullable: true }) 21 | nickname!: string; 22 | 23 | @Column({ nullable: true }) 24 | description!: string; 25 | 26 | @Column('int', { default: 1 }) 27 | theme!: number; 28 | 29 | @Column({ nullable: true }) 30 | workspaceId!: number; 31 | 32 | @Column({ nullable: true }) 33 | userId!: number; 34 | 35 | @Column({ nullable: true }) 36 | fileId!: number; 37 | 38 | @Column({ nullable: true }) 39 | fileUrl!: string; 40 | 41 | @CreateDateColumn({ type: 'timestamp' }) 42 | createdAt!: Date; 43 | 44 | @OneToMany(() => Reply, (reply) => reply.userHasWorkspace) 45 | replys!: Reply[]; 46 | 47 | @OneToMany(() => Thread, (thread) => thread.userHasWorkspace) 48 | threads!: Thread[]; 49 | 50 | @OneToMany(() => Reaction, (reaction) => reaction.userHasWorkspace) 51 | reactions!: Reaction[]; 52 | 53 | @ManyToOne(() => Workspace, (workspace) => workspace.userHasWorkspaces) 54 | workspace!: Workspace; 55 | 56 | @ManyToOne(() => User, (user) => user.userHasWorkspaces) 57 | user!: User; 58 | } 59 | 60 | export default UserHasWorkspace; 61 | -------------------------------------------------------------------------------- /backend/src/model/Workspace.ts: -------------------------------------------------------------------------------- 1 | import { Column, Entity, OneToMany, PrimaryGeneratedColumn, CreateDateColumn } from 'typeorm'; 2 | import Channel from './Channel'; 3 | import UserHasWorkspace from './UserHasWorkspace'; 4 | import Dm from './Dm'; 5 | 6 | @Entity() 7 | class Workspace { 8 | @PrimaryGeneratedColumn() 9 | id!: number 10 | 11 | @Column() 12 | name!: string; 13 | 14 | @Column({ nullable: true }) 15 | code!: string; 16 | 17 | @Column({ nullable: true }) 18 | fileId!: number; 19 | 20 | @CreateDateColumn({ type: 'timestamp' }) 21 | createdAt!: Date 22 | 23 | @OneToMany(() => UserHasWorkspace, (userHasWorkspace) => userHasWorkspace.workspace) 24 | userHasWorkspaces!: UserHasWorkspace[]; 25 | 26 | @OneToMany(() => Channel, (channel) => channel.workspace) 27 | channels!: Channel[]; 28 | 29 | @OneToMany(() => Dm, (dm) => dm.workspace) 30 | dms!: Dm[]; 31 | } 32 | 33 | export default Workspace; 34 | -------------------------------------------------------------------------------- /backend/src/ormconfig.ts: -------------------------------------------------------------------------------- 1 | import dotenv from 'dotenv'; 2 | import { ConnectionOptions } from 'typeorm'; 3 | 4 | dotenv.config(); 5 | 6 | const entitiyPath: string = process.env.DB_ENTITY_PATH || 'src/model/*.ts'; 7 | 8 | const ormconfig: ConnectionOptions = { 9 | type: 'mysql', 10 | host: process.env.DB_HOST, 11 | port: Number(process.env.DB_PORT), 12 | username: process.env.DB_USERNAME, 13 | password: process.env.DB_PASSWORD, 14 | database: process.env.DB_DATABASE, 15 | synchronize: true, 16 | logging: false, 17 | entities: [entitiyPath], 18 | extra: { charset: 'utf8mb4_unicode_ci' }, 19 | }; 20 | 21 | export default ormconfig; 22 | -------------------------------------------------------------------------------- /backend/src/pre-start/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Pre-start is where we want to place things that must run BEFORE the express server is started. 3 | * This is useful for environment variables, command-line arguments, and cron-jobs. 4 | */ 5 | 6 | import path from 'path'; 7 | import dotenv from 'dotenv'; 8 | import commandLineArgs from 'command-line-args'; 9 | 10 | (() => { 11 | // Setup command line options 12 | const options = commandLineArgs([ 13 | { 14 | name: 'env', 15 | alias: 'e', 16 | defaultValue: 'development', 17 | type: String, 18 | }, 19 | ]); 20 | // Set the env file 21 | const result2 = dotenv.config({ 22 | path: path.join(__dirname, `env/${options.env}.env`), 23 | // path: path.join(__dirname, "env/", options.env, ".env"), 24 | }); 25 | if (result2.error) { 26 | throw result2.error; 27 | } 28 | })(); 29 | -------------------------------------------------------------------------------- /backend/src/public/stylesheets/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 100px; 3 | font: 14px 'Lucida Grande', Helvetica, Arial, sans-serif; 4 | } 5 | 6 | body .users-column { 7 | display: inline-block; 8 | margin-right: 2em; 9 | vertical-align: top; 10 | } 11 | 12 | body .users-column .column-header { 13 | padding-bottom: 5px; 14 | font-weight: 700; 15 | font-size: 1.2em; 16 | } 17 | 18 | body .add-user-col input { 19 | margin-bottom: 10px; 20 | } 21 | 22 | body .users-column .user-display-ele { 23 | padding-bottom: 10px; 24 | } 25 | 26 | body .users-column .user-display-ele button { 27 | margin-top: 2px; 28 | margin-bottom: 10px; 29 | } 30 | 31 | body .users-column .user-display-ele .edit-view { 32 | display: none; 33 | } 34 | -------------------------------------------------------------------------------- /backend/src/repository/FileRepository.ts: -------------------------------------------------------------------------------- 1 | import { EntityRepository, Repository } from 'typeorm'; 2 | import File from '../model/File'; 3 | 4 | @EntityRepository(File) 5 | export default class FileRepository extends Repository {} 6 | -------------------------------------------------------------------------------- /backend/src/repository/ReactionRepository.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 | -------------------------------------------------------------------------------- /backend/src/repository/ReplyRepository.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 | -------------------------------------------------------------------------------- /backend/src/repository/ThreadRepository.ts: -------------------------------------------------------------------------------- 1 | import { EntityRepository, Repository } from 'typeorm'; 2 | import Thread from '../model/Thread'; 3 | 4 | @EntityRepository(Thread) 5 | export default class ThreadRepository extends Repository {} 6 | -------------------------------------------------------------------------------- /backend/src/repository/WorkspaceRepository.ts: -------------------------------------------------------------------------------- 1 | import { EntityRepository, Repository } from 'typeorm'; 2 | import Workspace from '../model/Workspace'; 3 | 4 | @EntityRepository(Workspace) 5 | export default class WorkspaceRepository extends Repository {} 6 | -------------------------------------------------------------------------------- /backend/src/routes/ChannelController.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import { 3 | getAllChannels, 4 | getOneChannel, 5 | addOneChannel, 6 | updateOneChannel, 7 | deleteOneChannel, 8 | addUserToChannel, 9 | deleteUserFromChannel, 10 | getChannelsThatUserIn, 11 | getChannels, 12 | } from '../service/ChannelService'; 13 | 14 | const channelRouter = Router(); 15 | 16 | channelRouter.post('/userToChannel', addUserToChannel); 17 | channelRouter.delete('/userFromChannel', deleteUserFromChannel); 18 | channelRouter.get('/channelsThatUserIn', getChannelsThatUserIn); 19 | 20 | channelRouter.get('/all', getChannels); 21 | channelRouter.get('/', getAllChannels); 22 | channelRouter.get('/:id', getOneChannel); 23 | channelRouter.post('/', addOneChannel); 24 | channelRouter.put('/:id', updateOneChannel); 25 | channelRouter.delete('/:id', deleteOneChannel); 26 | 27 | export default channelRouter; 28 | -------------------------------------------------------------------------------- /backend/src/routes/FilesController.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import Upload from '../config/Upload'; 3 | import { 4 | getAllFiles, 5 | getOneFile, 6 | addOneFile, 7 | updateOneFile, 8 | deleteOneFile, 9 | uploadFile, 10 | uploadFiles, 11 | getOneFileByUserHasWorkspaceId, 12 | } from '../service/FilesService'; 13 | 14 | const FileRouter = Router(); 15 | 16 | FileRouter.post('/upload', Upload.single('file'), uploadFile); 17 | FileRouter.post('/uploads', Upload.array('file'), uploadFiles); 18 | 19 | FileRouter.get('/userhasworkspace/:id', getOneFileByUserHasWorkspaceId); 20 | 21 | FileRouter.get('/', getAllFiles); 22 | FileRouter.get('/:id', getOneFile); 23 | FileRouter.post('/', addOneFile); 24 | FileRouter.put('/:id', updateOneFile); 25 | FileRouter.delete('/:id', deleteOneFile); 26 | 27 | export default FileRouter; 28 | -------------------------------------------------------------------------------- /backend/src/routes/ReactionController.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import { 3 | addReaction, 4 | addReplyReaction, 5 | deleteReactionById, 6 | deleteReactionByInfo, 7 | deleteReplyReaction, 8 | } from '../service/ReactionService'; 9 | 10 | const channelRouter = Router(); 11 | 12 | channelRouter.post('/', addReaction); 13 | channelRouter.post('/reply', addReplyReaction); 14 | channelRouter.delete('/reply', deleteReplyReaction); 15 | channelRouter.delete('/:id', deleteReactionById); 16 | channelRouter.delete('/', deleteReactionByInfo); 17 | 18 | export default channelRouter; 19 | -------------------------------------------------------------------------------- /backend/src/routes/ReplyController.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import { 3 | getAllReplys, 4 | getReply, 5 | addReply, 6 | updateReply, 7 | deleteReply, 8 | getPartialReplysByThreadId, 9 | updateReplyAndFiles, 10 | } from '../service/ReplyService'; 11 | 12 | const replyRouter = Router(); 13 | 14 | replyRouter.get('/partial', getPartialReplysByThreadId); 15 | 16 | replyRouter.put('/files/:id', updateReplyAndFiles); 17 | 18 | replyRouter.get('/', getAllReplys); 19 | replyRouter.get('/:id', getReply); 20 | replyRouter.post('/', addReply); 21 | replyRouter.put('/:id', updateReply); 22 | replyRouter.delete('/:id', deleteReply); 23 | 24 | export default replyRouter; 25 | -------------------------------------------------------------------------------- /backend/src/routes/ThreadController.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import { 3 | addThread, 4 | deleteThread, 5 | getPartialThreadsByChannelId, 6 | getThread, 7 | updateThread, 8 | updateThreadAndFiles, 9 | } from '../service/ThreadService'; 10 | 11 | const threadRouter = Router(); 12 | 13 | threadRouter.put('/files/:id', updateThreadAndFiles); 14 | 15 | threadRouter.get('/', getPartialThreadsByChannelId); 16 | threadRouter.get('/:id', getThread); 17 | threadRouter.post('/', addThread); 18 | threadRouter.put('/:id', updateThread); 19 | threadRouter.delete('/:id', deleteThread); 20 | 21 | export default threadRouter; 22 | -------------------------------------------------------------------------------- /backend/src/routes/UserController.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import { 3 | getAllUsers, 4 | getOneUser, 5 | addOneUser, 6 | findOneUser, 7 | updateOneUser, 8 | deleteOneUser, 9 | addUserToWorkspace, 10 | getAllUsersWithChannelInfo, 11 | deleteUserFromChannel, 12 | } from '../service/UserService'; 13 | 14 | const userRouter = Router(); 15 | 16 | userRouter.post('/userToWorkspace', addUserToWorkspace); 17 | userRouter.get('/workspaces', getAllUsersWithChannelInfo); 18 | 19 | userRouter.get('/', getAllUsers); 20 | userRouter.get('/find', findOneUser); 21 | userRouter.get('/:id', getOneUser); 22 | userRouter.post('/', addOneUser); 23 | userRouter.put('/:id', updateOneUser); 24 | userRouter.delete('/:id', deleteOneUser); 25 | // eslint-disable-next-line @typescript-eslint/no-unsafe-argument 26 | userRouter.delete('/:workspaceId/:channelId', deleteUserFromChannel); 27 | export default userRouter; 28 | -------------------------------------------------------------------------------- /backend/src/routes/UserHasWorkspaceController.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import { 3 | getUserHasWorkspace, 4 | updateUserHasWorkspace, 5 | deleteUserHasWorkspace, 6 | getUserHasWorkspacesByWorkspaceId, 7 | } from '@service/UserHasWorkspaceService'; 8 | 9 | const userHasWorkspaceRouter = Router(); 10 | 11 | userHasWorkspaceRouter.get('/all', getUserHasWorkspacesByWorkspaceId); 12 | userHasWorkspaceRouter.get('/', getUserHasWorkspace); 13 | userHasWorkspaceRouter.put('/:workspaceId', updateUserHasWorkspace); 14 | userHasWorkspaceRouter.delete('/:id', deleteUserHasWorkspace); 15 | 16 | export default userHasWorkspaceRouter; 17 | -------------------------------------------------------------------------------- /backend/src/routes/WorkspaceController.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import { 3 | getAllWorkspaces, 4 | getOneWorkspace, 5 | addOneWorkspace, 6 | updateOneWorkspace, 7 | deleteOneWorkspace, 8 | getAllUserWorkspaces, 9 | addUserToWorkspace, 10 | } from '../service/WorkspaceService'; 11 | 12 | const workspaceRouter = Router(); 13 | workspaceRouter.post('/code', addUserToWorkspace); 14 | workspaceRouter.get('/user', getAllUserWorkspaces); 15 | workspaceRouter.get('/', getAllWorkspaces); 16 | workspaceRouter.get('/:id', getOneWorkspace); 17 | workspaceRouter.post('/', addOneWorkspace); 18 | workspaceRouter.put('/:id', updateOneWorkspace); 19 | workspaceRouter.delete('/:id', deleteOneWorkspace); 20 | 21 | export default workspaceRouter; 22 | -------------------------------------------------------------------------------- /backend/src/routes/index.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import channelRouter from './ChannelController'; 3 | import userRouter from './UserController'; 4 | import workspaceRouter from './WorkspaceController'; 5 | import loginRouter from './LoginController'; 6 | import threadRouter from './ThreadController'; 7 | import userHasWorkspaceRouter from './UserHasWorkspaceController'; 8 | import FileRouter from './FilesController'; 9 | import reactionRouter from './ReactionController'; 10 | import replyRouter from './ReplyController'; 11 | 12 | const baseRouter = Router(); 13 | baseRouter.use('/users', userRouter); 14 | baseRouter.use('/userHasWorkspaces', userHasWorkspaceRouter); 15 | baseRouter.use('/workspaces', workspaceRouter); 16 | baseRouter.use('/channels', channelRouter); 17 | baseRouter.use('/threads', threadRouter); 18 | baseRouter.use('/login', loginRouter); 19 | baseRouter.use('/files', FileRouter); 20 | baseRouter.use('/reactions', reactionRouter); 21 | baseRouter.use('/replys', replyRouter); 22 | 23 | export default baseRouter; 24 | -------------------------------------------------------------------------------- /backend/src/sample/InsertChannelSample.ts: -------------------------------------------------------------------------------- 1 | import { getConnection, getRepository } from 'typeorm'; 2 | import Channel from '../model/Channel'; 3 | import ChannelSampleValue from './value/ChannelSampleValue'; 4 | 5 | const insertChannelSample = async () => { 6 | const ChannelCount = await getRepository(Channel).count(); 7 | if (ChannelCount > 0) return; 8 | await getConnection() 9 | .createQueryBuilder() 10 | .insert() 11 | .into(Channel) 12 | .values(ChannelSampleValue) 13 | .execute(); 14 | }; 15 | 16 | export default insertChannelSample; 17 | -------------------------------------------------------------------------------- /backend/src/sample/InsertFileSample.ts: -------------------------------------------------------------------------------- 1 | import { getConnection, getRepository } from 'typeorm'; 2 | import File from '../model/File'; 3 | import FileSampleValue from './value/FileSampleValue'; 4 | 5 | const insertFileSample = async () => { 6 | const FileCount = await getRepository(File).count(); 7 | if (FileCount > 0) return; 8 | await getConnection() 9 | .createQueryBuilder() 10 | .insert() 11 | .into(File) 12 | .values(FileSampleValue) 13 | .execute(); 14 | }; 15 | 16 | export default insertFileSample; 17 | -------------------------------------------------------------------------------- /backend/src/sample/InsertReactionSample.ts: -------------------------------------------------------------------------------- 1 | import { getConnection, getRepository } from 'typeorm'; 2 | import Reaction from '../model/Reaction'; 3 | import ReactionSampleValue from './value/ReactionSampleValue'; 4 | 5 | const insertReactionSample = async () => { 6 | const ReactionCount = await getRepository(Reaction).count(); 7 | if (ReactionCount > 0) return; 8 | await getConnection() 9 | .createQueryBuilder() 10 | .insert() 11 | .into(Reaction) 12 | .values(ReactionSampleValue) 13 | .execute(); 14 | }; 15 | 16 | export default insertReactionSample; 17 | -------------------------------------------------------------------------------- /backend/src/sample/InsertReplySample.ts: -------------------------------------------------------------------------------- 1 | import { getConnection, getRepository } from 'typeorm'; 2 | import Reply from '../model/Reply'; 3 | import ReplySampleValue from './value/ReplySampleValue'; 4 | 5 | const insertReplySample = async () => { 6 | const ReplyCount = await getRepository(Reply).count(); 7 | if (ReplyCount > 0) return; 8 | await getConnection() 9 | .createQueryBuilder() 10 | .insert() 11 | .into(Reply) 12 | .values(ReplySampleValue) 13 | .execute(); 14 | }; 15 | 16 | export default insertReplySample; 17 | -------------------------------------------------------------------------------- /backend/src/sample/InsertThreadSample.ts: -------------------------------------------------------------------------------- 1 | import { getConnection, getRepository } from 'typeorm'; 2 | import Thread from '../model/Thread'; 3 | import ThreadSampleValue from './value/ThreadSampleValue'; 4 | 5 | const insertThreadSample = async () => { 6 | const ThreadCount = await getRepository(Thread).count(); 7 | if (ThreadCount > 0) return; 8 | await getConnection() 9 | .createQueryBuilder() 10 | .insert() 11 | .into(Thread) 12 | .values(ThreadSampleValue) 13 | .execute(); 14 | }; 15 | 16 | export default insertThreadSample; 17 | -------------------------------------------------------------------------------- /backend/src/sample/InsertUserHasWorkspace.ts: -------------------------------------------------------------------------------- 1 | import { getConnection, getRepository } from 'typeorm'; 2 | import UserHasWorkspace from '../model/UserHasWorkspace'; 3 | import UserHasWorkspaceSampleValue from './value/UserHasWorkspaceSampleValue'; 4 | 5 | const insertUserHasWorkspaceSample = async () => { 6 | const UserHasWorkspaceCount = await getRepository(UserHasWorkspace).count(); 7 | if (UserHasWorkspaceCount > 0) return; 8 | await getConnection() 9 | .createQueryBuilder() 10 | .insert() 11 | .into(UserHasWorkspace) 12 | .values(UserHasWorkspaceSampleValue) 13 | .execute(); 14 | }; 15 | 16 | export default insertUserHasWorkspaceSample; 17 | -------------------------------------------------------------------------------- /backend/src/sample/InsertUserSample.ts: -------------------------------------------------------------------------------- 1 | import { getConnection, getRepository } from 'typeorm'; 2 | import User from '../model/User'; 3 | import UserSampleValue from './value/UserSampleValue'; 4 | 5 | const insertUserSample = async () => { 6 | const UserCount = await getRepository(User).count(); 7 | if (UserCount > 0) return; 8 | await getConnection() 9 | .createQueryBuilder() 10 | .insert() 11 | .into(User) 12 | .values(UserSampleValue) 13 | .execute(); 14 | }; 15 | 16 | export default insertUserSample; 17 | -------------------------------------------------------------------------------- /backend/src/sample/InsertWorkspaceSample.ts: -------------------------------------------------------------------------------- 1 | import { getConnection, getRepository } from 'typeorm'; 2 | import Workspace from '../model/Workspace'; 3 | import WorkspaceSampleValue from './value/WorkspaceSampleValue'; 4 | 5 | const insertWorkspaceSample = async () => { 6 | const WorkspaceCount = await getRepository(Workspace).count(); 7 | if (WorkspaceCount > 0) return; 8 | await getConnection() 9 | .createQueryBuilder() 10 | .insert() 11 | .into(Workspace) 12 | .values(WorkspaceSampleValue) 13 | .execute(); 14 | }; 15 | 16 | export default insertWorkspaceSample; 17 | -------------------------------------------------------------------------------- /backend/src/sample/index.ts: -------------------------------------------------------------------------------- 1 | import insertUserSample from './InsertUserSample'; 2 | import insertWorkspaceSample from './InsertWorkspaceSample'; 3 | import insertUserHasWorkspaceSample from './InsertUserHasWorkspace'; 4 | import insertChannelSample from './InsertChannelSample'; 5 | import insertThreadSample from './InsertThreadSample'; 6 | import insertFileSample from './InsertFileSample'; 7 | import insertReactionSample from './InsertReactionSample'; 8 | import insertReplySample from './InsertReplySample'; 9 | 10 | const addSampleData = async () => { 11 | // await insertUserSample(); 12 | // await insertWorkspaceSample(); 13 | // await insertUserHasWorkspaceSample(); 14 | // await insertChannelSample(); 15 | // await insertThreadSample(); 16 | // await insertFileSample(); 17 | // await insertReactionSample(); 18 | // await insertReplySample(); 19 | }; 20 | 21 | export default addSampleData; 22 | -------------------------------------------------------------------------------- /backend/src/sample/value/EmotionSampleValue.ts: -------------------------------------------------------------------------------- 1 | const EmotionSampleValue = [ 2 | { name: 'smile1' }, 3 | { name: 'smile2' }, 4 | { name: 'smile3' }, 5 | { name: 'smile4' }, 6 | { name: 'smile5' }, 7 | ]; 8 | 9 | export default EmotionSampleValue; 10 | -------------------------------------------------------------------------------- /backend/src/sample/value/FileSampleValue.ts: -------------------------------------------------------------------------------- 1 | const FileSampleValue = [ 2 | { name: 'undefined1', extension: 'txt' }, 3 | { name: 'undefined2', extension: 'txt' }, 4 | { name: 'undefined3', extension: 'txt' }, 5 | { name: 'undefined4', extension: 'txt' }, 6 | { name: 'undefined5', extension: 'txt' }, 7 | ]; 8 | 9 | export default FileSampleValue; 10 | -------------------------------------------------------------------------------- /backend/src/sample/value/ReactionSampleValue.ts: -------------------------------------------------------------------------------- 1 | const ReactionSampleValue = [ 2 | { threadId: 1, userHasWorkspaceId: 1, emoji: '💖' }, 3 | { threadId: 1, userHasWorkspaceId: 40, emoji: '💖' }, 4 | { threadId: 10, userHasWorkspaceId: 1, emoji: '🥳' }, 5 | { threadId: 10, userHasWorkspaceId: 40, emoji: '🥳' }, 6 | ]; 7 | 8 | export default ReactionSampleValue; 9 | -------------------------------------------------------------------------------- /backend/src/sample/value/ReplySampleValue.ts: -------------------------------------------------------------------------------- 1 | const ReplySampleValue = [ 2 | { message: '화이팅!!', threadId: 1, userHasWorkspaceId: 1 }, 3 | { message: '화이팅!!!', threadId: 1, userHasWorkspaceId: 1 }, 4 | { message: '화이팅!!!!', threadId: 1, userHasWorkspaceId: 1 }, 5 | { message: '화이팅!!!!!', threadId: 1, userHasWorkspaceId: 1 }, 6 | { message: '화이팅!!!!!!', threadId: 1, userHasWorkspaceId: 1 }, 7 | { message: 'booslack 화이팅!!', threadId: 1, userHasWorkspaceId: 1 }, 8 | ]; 9 | 10 | export default ReplySampleValue; 11 | -------------------------------------------------------------------------------- /backend/src/sample/value/WorkspaceSampleValue.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | const WorkspaceSampleValue = [ 3 | { name: 'random' }, 4 | { name: 'Web' }, 5 | { name: '안드로이드' }, 6 | { name: 'ios' }, 7 | { name: 'soccer' }, 8 | { name: 'basketball' }, 9 | { name: 'piano' }, 10 | { name: '헬스' }, 11 | { name: '복싱' }, 12 | { name: '부동산' }, 13 | { name: '부스트캠프' }, 14 | { name: '부커톤' }, 15 | { name: 'saymyname' }, 16 | { name: 'hobby' }, 17 | { name: '위로받기' }, 18 | { name: 'justtalk' }, 19 | { name: 'feelMess' }, 20 | { name: '수학문제풀이' }, 21 | { name: '알고리즘' }, 22 | { name: '개발자취업' }, 23 | { name: 'CS스터디' }, 24 | { name: '인프라' }, 25 | { name: '축구교실' }, 26 | { name: '농구교실' }, 27 | { name: '야구교실' }, 28 | { name: '스우파' }, 29 | { name: '고민을털어봐!' }, 30 | { name: '잡담' }, 31 | { name: '주식' }, 32 | { name: '비트코인' }, 33 | { name: 'MBTI' }, 34 | { name: '노래공유' }, 35 | { name: '보컬' }, 36 | { name: '기타' }, 37 | { name: '휘문고모여라!' }, 38 | { name: '군대' }, 39 | { name: '화장품' }, 40 | { name: '그림' }, 41 | { name: 'Web' }, 42 | ]; 43 | export default WorkspaceSampleValue; 44 | -------------------------------------------------------------------------------- /backend/src/shared/Logger.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Setup the jet-logger. 3 | * 4 | * Documentation: https://github.com/seanpmaxwell/jet-logger 5 | */ 6 | 7 | import Logger from 'jet-logger'; 8 | 9 | const logger = new Logger(); 10 | 11 | export default logger; 12 | -------------------------------------------------------------------------------- /backend/src/shared/constants.ts: -------------------------------------------------------------------------------- 1 | // Put shared constants here 2 | 3 | const paramMissingError = 'One or more of the required parameters was missing.'; 4 | 5 | export default paramMissingError; 6 | -------------------------------------------------------------------------------- /backend/src/shared/functions.ts: -------------------------------------------------------------------------------- 1 | import logger from './Logger'; 2 | 3 | export const pErr = (err: Error) => { 4 | if (err) { 5 | logger.err(err); 6 | } 7 | }; 8 | 9 | export const getRandomInt = () => Math.floor(Math.random() * 1_000_000_000_000); 10 | -------------------------------------------------------------------------------- /backend/src/shared/simpleuuid.ts: -------------------------------------------------------------------------------- 1 | const allString = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; 2 | 3 | /* eslint-disable arrow-body-style */ 4 | const generateUniqSerial = (): string => { 5 | return 'xxxxxx'.replace(/[x]/g, () => { 6 | const number = allString[Math.floor(Math.random() * (allString.length - 1))]; 7 | return number.toUpperCase(); 8 | }); 9 | }; 10 | 11 | export default generateUniqSerial; 12 | -------------------------------------------------------------------------------- /backend/src/socket.ts: -------------------------------------------------------------------------------- 1 | import { Server } from 'socket.io'; 2 | import * as http from 'http'; 3 | 4 | const initializeSocket = (httpServer: http.Server) => { 5 | const io = new Server(httpServer, { 6 | cors: { 7 | origin: [ 8 | 'http://localhost:3001', 9 | 'http://118.67.142.116:3001/', 10 | 'http://118.67.142.116:8081/', 11 | ], 12 | }, 13 | }); 14 | 15 | io.of(/^\/workspace:\d+$/).on('connection', (socket) => { 16 | const namespace = socket.nsp; 17 | 18 | socket.on('threads', (channelId, threadId) => { 19 | namespace.emit('threads', channelId, threadId); 20 | }); 21 | 22 | socket.on('channels', (workspaceId) => { 23 | namespace.emit('channels', workspaceId); 24 | }); 25 | 26 | socket.on('channel', (channelId) => { 27 | namespace.emit('channel', channelId); 28 | }); 29 | }); 30 | }; 31 | 32 | export default initializeSocket; 33 | -------------------------------------------------------------------------------- /backend/src/util/getNowDate.ts: -------------------------------------------------------------------------------- 1 | export const getNowDate = () => { 2 | const today: Date = new Date(); 3 | return `${today.getFullYear()}-${today.getMonth()}-${today.getDay()}`; 4 | }; 5 | 6 | export const getNowDateAndTime = () => { 7 | const today: Date = new Date(); 8 | const Dates = `${today.getFullYear()}-${today.getMonth()}-${today.getDay()}`; 9 | const hour = (`0${today.getHours()}`).slice(-2); 10 | const minute = (`0${today.getMinutes()}`).slice(-2); 11 | const second = (`0${today.getSeconds()}`).slice(-2); 12 | return `${Dates} ${hour}-${minute}-${second}`; 13 | }; 14 | -------------------------------------------------------------------------------- /backend/src/views/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ExpressGeneratorTypeScriptApp 6 | 7 | 8 | 9 | This page does not exist. 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /backend/tsconfig.prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "sourceMap": false 5 | }, 6 | "exclude": ["spec", "src/**/*.mock.ts", "src/public/"] 7 | } 8 | -------------------------------------------------------------------------------- /frontend/.eslintignore: -------------------------------------------------------------------------------- 1 | *.config.* -------------------------------------------------------------------------------- /frontend/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true, 5 | "jest/globals": true 6 | }, 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:react/recommended", 10 | "plugin:@typescript-eslint/recommended", 11 | "airbnb", 12 | "airbnb-typescript", 13 | "prettier/prettier", 14 | "eslint:recommended", 15 | "plugin:jest/recommended" 16 | ], 17 | "parser": "@typescript-eslint/parser", 18 | "parserOptions": { 19 | "ecmaFeatures": { 20 | "jsx": true 21 | }, 22 | "ecmaVersion": 12, 23 | "sourceType": "module", 24 | "project": "./tsconfig.json" 25 | }, 26 | "plugins": ["react", "@typescript-eslint", "prettier", "jest"], 27 | "rules": { 28 | "prettier/prettier": [ 29 | "error", 30 | { 31 | "endOfLine": "auto" 32 | } 33 | ], 34 | "object-curly-newline": "off", 35 | "react/jsx-filename-extension": "off", 36 | "no-use-before-define": "off", 37 | "@typescript-eslint/no-use-before-define": ["error"], 38 | "import/extensions": "off", 39 | "import/prefer-default-export": "off" 40 | }, 41 | "globals": { 42 | "JSX": true 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /frontend/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": true, 4 | "tabWidth": 2, 5 | "trailingComma": "all", 6 | "useTabs": false, 7 | "printWidth": 80, 8 | "arrowParens": "always", 9 | "bracketSpacing": true, 10 | "endOfLine": "auto" 11 | } 12 | -------------------------------------------------------------------------------- /frontend/jest.config.js: -------------------------------------------------------------------------------- 1 | const tsconfig = require('./tsconfig.json'); 2 | const moduleNameMapper = require('tsconfig-paths-jest')(tsconfig); 3 | 4 | console.log(moduleNameMapper); 5 | 6 | module.exports = { 7 | transform: { 8 | '^.+\\.tsx?$': 'ts-jest', 9 | '^.+\\.js$': 'babel-jest', 10 | '.+\\.(css|styl|less|sass|scss|png|jpg|gif|ttf|woff|woff2)$': 11 | 'jest-transform-stub', 12 | }, 13 | testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$', 14 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], 15 | moduleNameMapper, 16 | testEnvironment: 'jsdom', 17 | globals: { 18 | 'ts-jest': { 19 | diagnostics: false, 20 | }, 21 | }, 22 | }; 23 | -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/web06-booslack/df85700401e6635c2a7ddd35e75552a3f314fe7e/frontend/public/favicon.ico -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 13 | 14 | 15 | 21 | 22 | 26 | booslack 27 | 28 | 29 |
30 |
31 | 32 | 33 | -------------------------------------------------------------------------------- /frontend/src/components/atoms/DivLists/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Container from './styles'; 3 | 4 | interface Props { 5 | text: string; 6 | onClick: () => void; 7 | className?: string; 8 | } 9 | 10 | const DivLists = ({ text, onClick, className }: Props): JSX.Element => { 11 | return ( 12 | 13 | {text} 14 | 15 | ); 16 | }; 17 | 18 | DivLists.defaultProps = { 19 | className: '', 20 | }; 21 | 22 | export default DivLists; 23 | -------------------------------------------------------------------------------- /frontend/src/components/atoms/DivLists/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | const Container = styled.div` 4 | width: inherit; 5 | text-overflow: ellipsis; 6 | text-align: center; 7 | overflow: hidden; 8 | border-radius: 4px; 9 | margin: 5px 0 10px 0; 10 | padding: 3px; 11 | 12 | &:hover { 13 | background-color: #1164a3; 14 | cursor: pointer; 15 | color: #fff; 16 | } 17 | `; 18 | 19 | export default Container; 20 | -------------------------------------------------------------------------------- /frontend/src/components/atoms/ErrorBoundary/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | interface Props { 4 | fallback: JSX.Element; 5 | } 6 | 7 | class ErrorBoundary extends React.Component { 8 | constructor(props) { 9 | super(props); 10 | this.state = { hasError: false }; 11 | } 12 | 13 | static getDerivedStateFromError() { 14 | return { hasError: true }; 15 | } 16 | 17 | componentDidCatch(error, errorInfo) { 18 | // console.log(error, errorInfo); 19 | } 20 | 21 | render() { 22 | const { hasError } = this.state; 23 | const { children } = this.props; 24 | 25 | if (hasError) { 26 | return this.props.fallback; 27 | } 28 | return children; 29 | } 30 | } 31 | 32 | export default ErrorBoundary; 33 | -------------------------------------------------------------------------------- /frontend/src/components/atoms/IconButton/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { RefObject } from 'react'; 2 | import { IconType } from 'react-icons'; 3 | import Container from './styles'; 4 | 5 | interface Props { 6 | onClick: () => void; 7 | icon: IconType; 8 | className?: T; 9 | customRef?: undefined | RefObject; 10 | fontSize?: number; 11 | width?: number; 12 | height?: number; 13 | color?: string; 14 | } 15 | 16 | const IconButton = ({ 17 | onClick, 18 | icon, 19 | className, 20 | customRef, 21 | fontSize, 22 | width, 23 | height, 24 | color, 25 | children, 26 | }: Props): JSX.Element => { 27 | const Icon = icon; 28 | return ( 29 | 36 | 37 | {children} 38 | 39 | ); 40 | }; 41 | 42 | IconButton.defaultProps = { 43 | className: {}, 44 | customRef: undefined, 45 | fontSize: 18, 46 | width: 30, 47 | height: 30, 48 | }; 49 | 50 | export default IconButton; 51 | -------------------------------------------------------------------------------- /frontend/src/components/atoms/IconButton/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | const Container = styled.button` 4 | background: transparent; 5 | border: 0px solid; 6 | `; 7 | 8 | export default Container; 9 | -------------------------------------------------------------------------------- /frontend/src/components/atoms/ImageBox/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Container from './styles'; 3 | 4 | interface Props { 5 | image: string; 6 | className?: T; 7 | } 8 | 9 | const ImageBox = ({ 10 | image, 11 | className, 12 | }: Props): JSX.Element => { 13 | return ; 14 | }; 15 | 16 | ImageBox.defaultProps = { 17 | className: {}, 18 | }; 19 | 20 | export default ImageBox; 21 | -------------------------------------------------------------------------------- /frontend/src/components/atoms/ImageBox/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | const Container = styled.img` 4 | width: inherit; 5 | height: inherit; 6 | `; 7 | 8 | export default Container; 9 | -------------------------------------------------------------------------------- /frontend/src/components/atoms/ImageButton/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { RefObject } from 'react'; 2 | import { SetterOrUpdater } from 'recoil'; 3 | import Container from './styles'; 4 | 5 | interface Props { 6 | onClick: (e: React.MouseEvent) => void | SetterOrUpdater; 7 | width?: number; 8 | height?: number; 9 | image: string; 10 | className?: string; 11 | customRef?: undefined | RefObject; 12 | } 13 | 14 | const ImageButton = ({ 15 | onClick, 16 | width, 17 | height, 18 | image, 19 | className, 20 | customRef, 21 | }: Props): JSX.Element => { 22 | return ( 23 | 31 | ); 32 | }; 33 | 34 | ImageButton.defaultProps = { 35 | className: '', 36 | width: 30, 37 | height: 30, 38 | customRef: undefined, 39 | }; 40 | 41 | export default ImageButton; 42 | -------------------------------------------------------------------------------- /frontend/src/components/atoms/ImageButton/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | interface Props { 4 | width: number; 5 | height: number; 6 | image: string; 7 | } 8 | 9 | const Container = styled.button` 10 | width: ${({ width }) => width}px; 11 | height: ${({ height }) => height}px; 12 | border: 0; 13 | background-color: inherit; 14 | background-image: url(${({ image }) => image}); 15 | background-refeat: no-repeat; 16 | background-size: cover; 17 | `; 18 | 19 | export default Container; 20 | -------------------------------------------------------------------------------- /frontend/src/components/atoms/Input/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Container from './styles'; 3 | 4 | interface Props { 5 | onChange: (e) => void; 6 | name: string; 7 | value: string; 8 | onClick?: () => void; 9 | width?: number; 10 | height?: number; 11 | type?: string; 12 | placeholder: string; 13 | className?: T; 14 | onInput?: (e) => void; 15 | } 16 | 17 | const Input = ({ 18 | onChange, 19 | name, 20 | value, 21 | onClick, 22 | type, 23 | checked, 24 | width, 25 | height, 26 | placeholder, 27 | className, 28 | onInput, 29 | }: Props): JSX.Element => { 30 | return ( 31 | 43 | ); 44 | }; 45 | 46 | export default Input; 47 | -------------------------------------------------------------------------------- /frontend/src/components/atoms/Input/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | interface Props { 4 | width: number; 5 | height: number; 6 | } 7 | 8 | const Container = styled.input` 9 | width: ${({ width }) => width}px; 10 | height: ${({ height }) => height}px; 11 | padding: 8px 12px; 12 | `; 13 | 14 | export default Container; 15 | -------------------------------------------------------------------------------- /frontend/src/components/atoms/Label/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Container from './styles'; 3 | 4 | interface Props { 5 | text: string; 6 | width?: number; 7 | height?: number; 8 | color?: string; 9 | backgroundColor?: string; 10 | className?: string; 11 | } 12 | 13 | const Label = ({ 14 | text, 15 | width, 16 | height, 17 | color, 18 | backgroundColor, 19 | className, 20 | }: Props): JSX.Element => { 21 | return ( 22 | 29 | {text} 30 | 31 | ); 32 | }; 33 | 34 | Label.defaultProps = { 35 | className: '', 36 | }; 37 | 38 | export default Label; 39 | -------------------------------------------------------------------------------- /frontend/src/components/atoms/Label/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | interface Props { 4 | width: number; 5 | height: number; 6 | color: string; 7 | backgroundColor: string; 8 | } 9 | 10 | const Container = styled.span` 11 | width: ${({ width }) => width}px; 12 | height: ${({ height }) => height}px; 13 | color: ${({ color }) => color}; 14 | background-color: ${({ backgroundColor }) => backgroundColor}; 15 | `; 16 | 17 | export default Container; 18 | -------------------------------------------------------------------------------- /frontend/src/components/atoms/LabeledButton/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { RefObject } from 'react'; 2 | import Container from './styles'; 3 | 4 | interface Props { 5 | onClick: (e) => void | React.FormEventHandler; 6 | text: string; 7 | width?: number; 8 | height?: number; 9 | color?: string; 10 | type?: string | undefined; 11 | backgroundColor?: string; 12 | className?: T; 13 | customRef?: undefined | RefObject; 14 | disabled?: boolean; 15 | } 16 | 17 | const LabeledButton = ({ 18 | onClick, 19 | text, 20 | width, 21 | height, 22 | color, 23 | type, 24 | backgroundColor = 'transparent', 25 | className, 26 | customRef, 27 | disabled, 28 | }: Props): JSX.Element => { 29 | return ( 30 | 41 | {text} 42 | 43 | ); 44 | }; 45 | 46 | LabeledButton.defaultProps = { 47 | className: {}, 48 | width: 30, 49 | height: 30, 50 | color: undefined, 51 | backgroundColor: undefined, 52 | customRef: undefined, 53 | disabled: false, 54 | type: 'button', 55 | }; 56 | 57 | export default LabeledButton; 58 | -------------------------------------------------------------------------------- /frontend/src/components/atoms/LabeledButton/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | interface Props { 4 | color: string; 5 | backgroundColor: string; 6 | disabled?: unknown; 7 | } 8 | 9 | const Container = styled.button` 10 | color: ${({ color }) => color}; 11 | background-color: ${({ backgroundColor }) => backgroundColor}; 12 | border: 0px; 13 | border-radius: 4px; 14 | cursor: pointer; 15 | `; 16 | 17 | export default Container; 18 | -------------------------------------------------------------------------------- /frontend/src/components/atoms/LabeledDefaultButton/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { RefObject } from 'react'; 2 | import LabeledButton from '@atoms/LabeledButton'; 3 | import { BUTTON_SIZE } from '@enum/index'; 4 | 5 | interface Props { 6 | onClick?: (e) => void; 7 | width?: number; 8 | height?: number; 9 | text: string; 10 | color?: string; 11 | backgroundColor?: string; 12 | className?: T; 13 | customRef?: RefObject; 14 | disabled?: boolean; 15 | } 16 | 17 | const { 18 | width: ButtonWidth, 19 | height: ButtonHeight, 20 | color: ButtonColor, 21 | backgroundColor: ButtonBackground, 22 | } = BUTTON_SIZE; 23 | 24 | const LabeledDefaultButton = ({ 25 | onClick, 26 | width, 27 | height, 28 | text, 29 | color, 30 | backgroundColor, 31 | className, 32 | customRef, 33 | disabled, 34 | }: Props): JSX.Element => { 35 | return ( 36 | 47 | ); 48 | }; 49 | 50 | LabeledDefaultButton.defaultProps = { 51 | onClick: (e) => {}, 52 | width: ButtonWidth as number, 53 | height: ButtonHeight as number, 54 | color: ButtonColor, 55 | backgroundColor: ButtonBackground, 56 | className: {}, 57 | customRef: undefined, 58 | disabled: false, 59 | }; 60 | 61 | export default LabeledDefaultButton; 62 | -------------------------------------------------------------------------------- /frontend/src/components/atoms/Modal/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode } from 'react'; 2 | import { createPortal } from 'react-dom'; 3 | import { Container, Content, Overlay } from './styles'; 4 | 5 | const root = document.getElementById('portal'); 6 | 7 | interface Props { 8 | isOpen: boolean; 9 | onClose: () => void; 10 | zIndex: number; 11 | children: ReactNode; 12 | className?: string; 13 | } 14 | 15 | const Modal = ({ 16 | isOpen, 17 | onClose, 18 | zIndex = 100, 19 | children, 20 | className, 21 | }: Props): JSX.Element => { 22 | return createPortal( 23 | 24 | 25 | 26 | {children} 27 | 28 | , 29 | root, 30 | ); 31 | }; 32 | 33 | export default Modal; 34 | -------------------------------------------------------------------------------- /frontend/src/components/atoms/Modal/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const Container = styled.div<{ visible: boolean }>` 4 | display: ${({ visible }) => (visible ? 'block' : 'none')}; 5 | `; 6 | 7 | export const Content = styled.div<{ zIndex: number }>` 8 | position: fixed; 9 | top: 50%; 10 | left: 50%; 11 | transform: translate(-50%, -50%); 12 | width: 100%; 13 | z-index: ${({ zIndex }) => zIndex}; 14 | `; 15 | 16 | export const Overlay = styled.div<{ zIndex: number }>` 17 | position: fixed; 18 | top: 0; 19 | left: 0; 20 | bottom: 0; 21 | right: 0; 22 | background-color: rgba(0, 0, 0, 0.6); 23 | z-index: ${({ zIndex }) => zIndex - 1}; 24 | `; 25 | -------------------------------------------------------------------------------- /frontend/src/components/atoms/Popup/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode } from 'react'; 2 | import { createPortal } from 'react-dom'; 3 | import { Container, Content, Overlay } from './styles'; 4 | 5 | const root = document.getElementById('portal'); 6 | 7 | interface Props { 8 | row?: number; 9 | isOpen: boolean; 10 | onClose: () => void; 11 | zIndex: number; 12 | children: ReactNode; 13 | className?: string; 14 | } 15 | 16 | const Popup = ({ 17 | isOpen, 18 | onClose, 19 | zIndex = 110, 20 | children, 21 | className, 22 | }: Props): JSX.Element => { 23 | return createPortal( 24 | 25 | 26 | 27 | {children} 28 | 29 | , 30 | root, 31 | ); 32 | }; 33 | 34 | export default Popup; 35 | -------------------------------------------------------------------------------- /frontend/src/components/atoms/Popup/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const Container = styled.div<{ visible: boolean }>` 4 | display: ${({ visible }) => (visible ? 'block' : 'none')}; 5 | `; 6 | 7 | export const Content = styled.div<{ zIndex: number }>` 8 | position: absolute; 9 | z-index: ${({ zIndex }) => zIndex}; 10 | `; 11 | 12 | export const Overlay = styled.div<{ zIndex: number }>` 13 | position: fixed; 14 | top: 0; 15 | left: 0; 16 | bottom: 0; 17 | right: 0; 18 | z-index: ${({ zIndex }) => zIndex - 1}; 19 | `; 20 | -------------------------------------------------------------------------------- /frontend/src/components/atoms/RadioButton/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { ChangeEvent } from 'react'; 2 | 3 | interface Props { 4 | name: string; 5 | value: number; 6 | isChecked?: number; 7 | onChange?: (e: ChangeEvent) => void; 8 | className?: string; 9 | } 10 | 11 | const RadioButton = ({ 12 | name, 13 | isChecked, 14 | onChange, 15 | value, 16 | className, 17 | }: Props): JSX.Element => { 18 | return ( 19 | <> 20 | 28 | {name} 29 | 30 | ); 31 | }; 32 | 33 | RadioButton.defaultProps = { 34 | className: '', 35 | isChecked: false, 36 | onChange: () => {}, 37 | }; 38 | 39 | export default RadioButton; 40 | -------------------------------------------------------------------------------- /frontend/src/components/atoms/Spinner/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { DotLoader, PacmanLoader } from 'react-spinners'; 3 | import { Center, ErrorContainer, PacmanContainer } from './style'; 4 | 5 | export const Spinner = ({ 6 | size, 7 | color, 8 | }: { 9 | size: number; 10 | color: string; 11 | }): JSX.Element => { 12 | return ( 13 |
14 | 15 |
16 | ); 17 | }; 18 | 19 | export const ErrorSpinner = ({ 20 | size, 21 | color, 22 | }: { 23 | size: number; 24 | color: string; 25 | }): JSX.Element => { 26 | return ( 27 |
28 | 29 | 30 | 31 | 32 |

오류가 발생했습니다. 다시 시도해주세요.

33 |
34 |
35 | ); 36 | }; 37 | -------------------------------------------------------------------------------- /frontend/src/components/atoms/Spinner/style.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const Center = styled.div` 4 | display: flex; 5 | justify-content: center; 6 | width: 100%; 7 | margin: 12px; 8 | padding: 12px; 9 | `; 10 | 11 | export const PacmanContainer = styled.div<{ size: number }>` 12 | display: flex; 13 | justify-content: center; 14 | width: 100%; 15 | height: ${({ size }) => `${size * 2}px`}; 16 | margin: 12px; 17 | padding: 12px; 18 | `; 19 | 20 | export const ErrorContainer = styled.div` 21 | display: flex; 22 | justify-content: center; 23 | width: 100%; 24 | margin: 12px; 25 | padding: 12px; 26 | border: none; 27 | `; 28 | -------------------------------------------------------------------------------- /frontend/src/components/atoms/ToggleButton/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Container, ToggleOff, ToggleOn } from './styles'; 3 | 4 | interface Props { 5 | isOn: boolean; 6 | setIsOn: React.Dispatch>; 7 | } 8 | 9 | const ToggleButton = ({ isOn, setIsOn, className }: Props): JSX.Element => { 10 | return ( 11 | setIsOn((prevState) => !prevState)}> 12 | {isOn ? ( 13 | 14 | ) : ( 15 | 16 | )} 17 | 18 | ); 19 | }; 20 | 21 | export default ToggleButton; 22 | -------------------------------------------------------------------------------- /frontend/src/components/atoms/ToggleButton/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { BsToggleOn, BsToggleOff } from 'react-icons/bs'; 3 | 4 | export const Container = styled.button` 5 | background: transparent; 6 | border: 0px solid; 7 | `; 8 | 9 | export const ToggleOn = styled(BsToggleOn)` 10 | color: #34785c; 11 | `; 12 | 13 | export const ToggleOff = styled(BsToggleOff)` 14 | color: #8e8d8e; 15 | `; 16 | -------------------------------------------------------------------------------- /frontend/src/components/atoms/ViewPortInput/index.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/ban-ts-comment */ 2 | import React from 'react'; 3 | import Container, { Form } from './styles'; 4 | 5 | interface Props { 6 | inputName?: string; 7 | onSubmit?: React.FormEventHandler; 8 | onChange?: React.FormEventHandler; 9 | placeholder: string; 10 | type?: string; 11 | customRef?: React.RefObject; 12 | className?: string; 13 | } 14 | 15 | const ViewportInput = ({ 16 | inputName, 17 | onSubmit, 18 | onChange, 19 | type, 20 | placeholder, 21 | customRef, 22 | className, 23 | }: Props): JSX.Element => { 24 | return ( 25 |
26 | 35 | 36 | ); 37 | }; 38 | 39 | ViewportInput.defaultProps = { 40 | inputName: '', 41 | onSubmit: () => {}, 42 | onChange: () => {}, 43 | type: 'text', 44 | customRef: undefined, 45 | className: '', 46 | }; 47 | 48 | export default ViewportInput; 49 | -------------------------------------------------------------------------------- /frontend/src/components/atoms/ViewPortInput/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | const Container = styled.input` 4 | width: 100%; 5 | height: inherit; 6 | `; 7 | 8 | export const Form = styled.form` 9 | width: 100%; 10 | `; 11 | 12 | export default Container; 13 | -------------------------------------------------------------------------------- /frontend/src/components/molecules/AsyncBranch/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { Suspense, useContext } from 'react'; 2 | import { ThemeContext } from 'styled-components'; 3 | import ErrorBoundary from '@atoms/ErrorBoundary'; 4 | import { ErrorSpinner, Spinner } from '@atoms/Spinner'; 5 | 6 | interface Props { 7 | size: number; 8 | children: JSX.Element | JSX.Element[]; 9 | } 10 | 11 | const AsyncBranch = ({ size, children }: Props): JSX.Element => { 12 | const themeContext = useContext(ThemeContext); 13 | const color = themeContext.bigHeaderColor; 14 | 15 | return ( 16 | }> 17 | }> 18 | {children} 19 | 20 | 21 | ); 22 | }; 23 | 24 | AsyncBranch.defaultProps = {}; 25 | 26 | export default AsyncBranch; 27 | -------------------------------------------------------------------------------- /frontend/src/components/molecules/AsyncBranch/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { theme } from 'styled-tools'; 3 | import { PacmanLoader } from 'react-spinners'; 4 | import { flexAlignCenter } from '@global/style/mixin'; 5 | import { defaultTheme } from '@global/style/theme'; 6 | 7 | const Container = styled.div` 8 | width: inherit; 9 | height: 600px; 10 | background: transparent; 11 | border: 0px solid; 12 | ${flexAlignCenter} 13 | overflow : hidden; 14 | `; 15 | 16 | export const SpinnerContainer = styled.div` 17 | width: 55vw; 18 | min-width: 300px; 19 | height: 60px; 20 | `; 21 | 22 | export const MarginDiv = styled.div` 23 | margin-top: 50px; 24 | `; 25 | 26 | export const LoadingSpinner = styled(PacmanLoader)` 27 | margin-top: 50px; 28 | background-color: ${theme('backgroundColor', defaultTheme.backgroundColor)}; 29 | `; 30 | 31 | export default Container; 32 | -------------------------------------------------------------------------------- /frontend/src/components/molecules/BrowseChannelHeader/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Container } from './styles'; 3 | 4 | interface Props { 5 | width?: number; 6 | title: JSX.Element; 7 | content?: null | JSX.Element; 8 | rightButton: JSX.Element; 9 | className?: T; 10 | } 11 | 12 | const BrowseChannelHeader = ({ 13 | width = null, 14 | title, 15 | content, 16 | rightButton, 17 | className, 18 | }: Props): JSX.Element => { 19 | return ( 20 | 21 | {title} 22 | {content} 23 | {rightButton} 24 | 25 | ); 26 | }; 27 | 28 | BrowseChannelHeader.defaultProps = { 29 | width: null, 30 | content: <>, 31 | className: {}, 32 | }; 33 | 34 | export default BrowseChannelHeader; 35 | -------------------------------------------------------------------------------- /frontend/src/components/molecules/BrowseChannelHeader/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | interface Props { 4 | width?: number; 5 | } 6 | 7 | export const Container = styled.div` 8 | display: flex; 9 | min-height: 48.99px; 10 | width: ${({ width }) => { 11 | if (width) return `${width}vw`; 12 | return 'inherit'; 13 | }}; 14 | justify-content: space-between; 15 | align-items: center; 16 | } 17 | background-color: #fff; 18 | & > * { 19 | margin: 0 1vw 0 1vw; 20 | 21 | `; 22 | 23 | export default Container; 24 | -------------------------------------------------------------------------------- /frontend/src/components/molecules/ChatHeader/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useSetRecoilState } from 'recoil'; 3 | import { useParams } from 'react-router-dom'; 4 | import { MdPeople } from 'react-icons/md'; 5 | import { channelInfoModalState } from '@state/modal'; 6 | import { useChannelQuery } from '@hook/useChannels'; 7 | import { 8 | Container, 9 | StyledLabeledButton, 10 | StyledIconButton, 11 | HeaderContainer, 12 | StyledLabel, 13 | } from './styles'; 14 | 15 | const ChatHeader = (): JSX.Element => { 16 | const { channelId }: { channelId: string } = useParams(); 17 | 18 | const setIsOpen = useSetRecoilState(channelInfoModalState); 19 | const { isLoading, isError, data } = useChannelQuery(channelId); 20 | 21 | if (isLoading) return
Loading
; 22 | if (isError) return
Error
; 23 | 24 | return ( 25 | 26 | 27 | setIsOpen({ isOpen: true, isAboutTab: true })} 30 | /> 31 | {data.topic && } 32 | 33 | setIsOpen({ isOpen: true, isAboutTab: false })} 36 | /> 37 | 38 | ); 39 | }; 40 | 41 | export default ChatHeader; 42 | -------------------------------------------------------------------------------- /frontend/src/components/molecules/ChatHeader/styles.ts: -------------------------------------------------------------------------------- 1 | import IconButton from '@atoms/IconButton'; 2 | import Label from '@atoms/Label'; 3 | import LabeledButton from '@atoms/LabeledButton'; 4 | import styled from 'styled-components'; 5 | 6 | export const Container = styled.div` 7 | display: flex; 8 | justify-content: space-between; 9 | min-height: 48.99px; 10 | align-items: center; 11 | } 12 | background-color: #fff; 13 | 14 | & > * { 15 | margin: 0 1vw 0 1vw; 16 | } 17 | 18 | --saf-0: rgba(var(--sk_foreground_low, 29, 28, 29), 0.13); 19 | box-shadow: 0 0 0 1px var(--saf-0), 0 4px 12px 0 rgba(0, 0, 0, 0.12); 20 | `; 21 | 22 | export const HeaderContainer = styled.div` 23 | display: flex; 24 | align-items: baseline; 25 | `; 26 | 27 | export const StyledLabel = styled(Label)` 28 | margin-left: 8px; 29 | font-size: 13px; 30 | color: #1d1c1db3; 31 | `; 32 | 33 | export const StyledLabeledButton = styled(LabeledButton)` 34 | font-size: 18px; 35 | font-weight: bold; 36 | &: hover { 37 | cursor: pointer; 38 | background-color: #f6f6f6; 39 | } 40 | `; 41 | 42 | export const StyledIconButton = styled(IconButton)` 43 | &: hover { 44 | cursor: pointer; 45 | background-color: #f6f6f6; 46 | } 47 | `; 48 | -------------------------------------------------------------------------------- /frontend/src/components/molecules/CodeModal/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useRecoilState } from 'recoil'; 3 | import { codeModalState } from '@state/modal'; 4 | import { StyledModal, Container, ModalButton, ModalMessage } from './style'; 5 | 6 | interface Props { 7 | Content: string; 8 | } 9 | 10 | const CodeModal = ({ Content }: Props): JSX.Element => { 11 | const [{ status, text }, setObject] = useRecoilState(codeModalState); 12 | return ( 13 | setObject({ status: false, text: undefined })} 16 | > 17 | 18 | {text || Content} 19 | setObject({ status: false, text: undefined })} 23 | /> 24 | 25 | 26 | ); 27 | }; 28 | 29 | export default CodeModal; 30 | -------------------------------------------------------------------------------- /frontend/src/components/molecules/CodeModal/style.ts: -------------------------------------------------------------------------------- 1 | import Modal from '@atoms/Modal'; 2 | import styled from 'styled-components'; 3 | import LabeledButton from '@atoms/LabeledButton'; 4 | 5 | export const StyledModal = styled(Modal)` 6 | max-width: 580px; 7 | width: 380px; 8 | height: 250px; 9 | background-color: white; 10 | border-radius: 15px; 11 | display: flex; 12 | justify-content: center; 13 | align-items: center; 14 | `; 15 | 16 | export const Container = styled.div` 17 | display: flex; 18 | justify-content: space-between; 19 | flex-direction: column; 20 | align-items: center; 21 | height: 170px; 22 | padding: 5px; 23 | `; 24 | 25 | export const ModalButton = styled(LabeledButton)` 26 | background-color: #c8c7ef; 27 | width: 240px; 28 | height: 60px; 29 | `; 30 | 31 | export const ModalMessage = styled.span` 32 | font-size: 26px; 33 | box-sizing: content-box; 34 | font-weight: 500; 35 | width: 330px; 36 | `; 37 | -------------------------------------------------------------------------------- /frontend/src/components/molecules/EmojiPopup/EmojiPopupTemplate/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import useKeyboardNavigator from '@hook/useKeyboardNavigator'; 3 | import { 4 | Container, 5 | PrimaryContent, 6 | SecondaryContent, 7 | StyledBoldLabel, 8 | UserContainer, 9 | UserElement, 10 | } from './styles'; 11 | 12 | interface Props { 13 | matches: []; 14 | setValue: React.Dispatch; 15 | } 16 | 17 | const EmojiPopupTemplate = ({ matches, setValue }: Props): JSX.Element => { 18 | const index = useKeyboardNavigator(matches, setValue); 19 | 20 | return ( 21 | 22 | {matches.map((emoji, idx) => ( 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | ))} 34 | 35 | ); 36 | }; 37 | 38 | export default EmojiPopupTemplate; 39 | -------------------------------------------------------------------------------- /frontend/src/components/molecules/EmojiPopup/EmojiPopupTemplate/styles.ts: -------------------------------------------------------------------------------- 1 | import Label from '@atoms/Label'; 2 | import styled from 'styled-components'; 3 | import { RoundScrollBar } from '@global/style/mixin'; 4 | import { ifProp } from 'styled-tools'; 5 | 6 | export const Container = styled.div` 7 | ${RoundScrollBar}; 8 | overflow-y: scroll; 9 | overflow-x: hidden; 10 | width: 100%; 11 | max-height: 300px; 12 | `; 13 | 14 | interface Props { 15 | selected: boolean; 16 | } 17 | 18 | export const UserContainer = styled.div` 19 | padding: 0 1rem; 20 | background-color: ${ifProp({ selected: true }, '#2C639E', 'transparent')}; 21 | `; 22 | 23 | export const UserElement = styled.div` 24 | height: 32px; 25 | display: flex; 26 | justify-content: space-between; 27 | align-items: center; 28 | `; 29 | 30 | export const PrimaryContent = styled.div``; 31 | 32 | export const SecondaryContent = styled.div``; 33 | 34 | export const StyledBoldLabel = styled(Label)` 35 | font-weight: bold; 36 | `; 37 | -------------------------------------------------------------------------------- /frontend/src/components/molecules/EmojiPopup/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { Dispatch, RefObject, useEffect } from 'react'; 2 | import * as unicodeEmoji from 'unicode-emoji'; 3 | import EmojiPopupTemplate from './EmojiPopupTemplate'; 4 | import { StyledPopup } from './styles'; 5 | 6 | interface Props { 7 | input: string; 8 | isOpen: boolean; 9 | value: any; 10 | setValue: Dispatch; 11 | close: () => void; 12 | customRef: RefObject; 13 | xWidth: number; 14 | yHeight: number; 15 | } 16 | 17 | const emojis = unicodeEmoji.getEmojis(); 18 | 19 | const EmojiPopup = ({ 20 | input, 21 | isOpen, 22 | value, 23 | setValue, 24 | close, 25 | customRef, 26 | xWidth, 27 | yHeight, 28 | }: Props): JSX.Element => { 29 | useEffect(() => { 30 | if (value) { 31 | close(); 32 | } 33 | }, [value]); 34 | 35 | return ( 36 | 43 | emoji.description.includes(input))} 45 | setValue={setValue} 46 | /> 47 | 48 | ); 49 | }; 50 | 51 | export default EmojiPopup; 52 | -------------------------------------------------------------------------------- /frontend/src/components/molecules/EmojiPopup/styles.ts: -------------------------------------------------------------------------------- 1 | import NoOverlayModal from '@molecules/NoOverlayModal'; 2 | import styled from 'styled-components'; 3 | 4 | export const StyledPopup = styled(NoOverlayModal)` 5 | --saf-0: rgba(var(--sk_foreground_low, 29, 28, 29), 0.13); 6 | box-shadow: 0 0 0 1px var(--saf-0), 0 4px 12px 0 rgba(0, 0, 0, 0.08); 7 | line-height: 1rem; 8 | position: fixed; 9 | background-color: white; 10 | 11 | width: 300px; 12 | height: 300px; 13 | `; 14 | 15 | export default StyledPopup; 16 | -------------------------------------------------------------------------------- /frontend/src/components/molecules/LabeledInput/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Input from '@atoms/Input'; 3 | import Container, { StyledLabel } from './styles'; 4 | 5 | interface Props { 6 | onChange: (e) => void; 7 | name: string; 8 | value: string; 9 | label: string; 10 | placeholder?: string; 11 | } 12 | 13 | const LabeledInput = ({ 14 | onChange, 15 | name, 16 | value, 17 | label, 18 | placeholder, 19 | className, 20 | }: Props): JSX.Element => { 21 | return ( 22 | 23 | 24 | 30 | 31 | ); 32 | }; 33 | 34 | export default LabeledInput; 35 | -------------------------------------------------------------------------------- /frontend/src/components/molecules/LabeledInput/styles.ts: -------------------------------------------------------------------------------- 1 | import Label from '@atoms/Label'; 2 | import styled from 'styled-components'; 3 | 4 | const Container = styled.div` 5 | display: flex; 6 | flex-direction: column; 7 | `; 8 | 9 | export const StyledLabel = styled(Label)` 10 | font-weight: bold; 11 | `; 12 | 13 | export default Container; 14 | -------------------------------------------------------------------------------- /frontend/src/components/molecules/MentionPopup/MentionPopupTemplate/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Label from '@atoms/Label'; 3 | import useKeyboardNavigator from '@hook/useKeyboardNavigator'; 4 | import defaultImage from '@global/image/default_account.png'; 5 | import { 6 | Container, 7 | PrimaryContent, 8 | SecondaryContent, 9 | StyledImageBox, 10 | UserContainer, 11 | UserElement, 12 | } from './styles'; 13 | 14 | interface Props { 15 | matches: []; 16 | setValue: React.Dispatch; 17 | } 18 | 19 | const MentionPopupTemplate = ({ matches, setValue }: Props): JSX.Element => { 20 | const index = useKeyboardNavigator(matches, setValue); 21 | return ( 22 | 23 | {matches.map((user, idx) => ( 24 | 25 | 26 | 27 | 34 | 36 | 37 | {user.inChannel === '0' && 39 | 40 | 41 | ))} 42 | 43 | ); 44 | }; 45 | 46 | export default MentionPopupTemplate; 47 | -------------------------------------------------------------------------------- /frontend/src/components/molecules/MentionPopup/MentionPopupTemplate/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { ifProp } from 'styled-tools'; 3 | import ImageBox from '@atoms/ImageBox'; 4 | 5 | export const Container = styled.div` 6 | width: 100%; 7 | `; 8 | 9 | interface Props { 10 | selected: boolean; 11 | } 12 | 13 | export const UserContainer = styled.div` 14 | padding: 0 1rem; 15 | background-color: ${ifProp({ selected: true }, '#2C639E', 'transparent')}; 16 | `; 17 | 18 | export const UserElement = styled.div` 19 | height: 32px; 20 | display: flex; 21 | justify-content: space-between; 22 | align-items: center; 23 | `; 24 | 25 | export const PrimaryContent = styled.div` 26 | padding: 3px; 27 | display: flex; 28 | justify-content: start; 29 | flex-direction: row; 30 | align-items: center; 31 | `; 32 | 33 | export const SecondaryContent = styled.div``; 34 | 35 | export const StyledImageBox = styled(ImageBox)` 36 | width: 30px; 37 | height: 30px; 38 | margin-right: 5px; 39 | border-radius: 5px; 40 | `; 41 | -------------------------------------------------------------------------------- /frontend/src/components/molecules/MentionPopup/styles.ts: -------------------------------------------------------------------------------- 1 | import NoOverlayModal from '@molecules/NoOverlayModal'; 2 | import styled from 'styled-components'; 3 | 4 | export const StyledPopup = styled(NoOverlayModal)` 5 | --saf-0: rgba(var(--sk_foreground_low, 29, 28, 29), 0.13); 6 | box-shadow: 0 0 0 1px var(--saf-0), 0 4px 12px 0 rgba(0, 0, 0, 0.08); 7 | line-height: 1rem; 8 | position: fixed; 9 | background-color: white; 10 | 11 | width: 300px; 12 | height: 300px; 13 | `; 14 | 15 | export default StyledPopup; 16 | -------------------------------------------------------------------------------- /frontend/src/components/molecules/MessageContent/MessageFileStatusBar/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | ThreadFileStatusBarLayout, 4 | ThreadFileStatusBarContainer, 5 | } from './styles'; 6 | import MessageFileStatusElement from '../MessageFileStatusElement'; 7 | 8 | interface Props { 9 | files: File[]; 10 | } 11 | 12 | const MessageFileStatusBar = ({ files }: Props): JSX.Element => { 13 | return ( 14 | <> 15 | 16 | 17 | {files.map((file, index) => ( 18 | 19 | ))} 20 | 21 | 22 | 23 | ); 24 | }; 25 | 26 | export default MessageFileStatusBar; 27 | -------------------------------------------------------------------------------- /frontend/src/components/molecules/MessageContent/MessageFileStatusBar/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const ThreadFileStatusBarContainer = styled.div` 4 | max-width: 100%; 5 | width: 100%; 6 | display: grid; 7 | grid-template-columns: 1fr 1fr; 8 | grid-auto-rows: 1fr; 9 | gap: 10px 20px; 10 | `; 11 | 12 | export const ThreadFileStatusBarLayout = styled.div` 13 | max-width: 100%; 14 | width: 730px; 15 | margin-bottom: 10px; 16 | `; 17 | -------------------------------------------------------------------------------- /frontend/src/components/molecules/MessageContent/MessageFileStatusElement/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import ImageBox from '@atoms/ImageBox'; 3 | import { MdTextSnippet, MdInsertDriveFile } from 'react-icons/md'; 4 | 5 | export const MessageFileStatusLayOut = styled.div` 6 | position: relative; 7 | min-width: 100px; 8 | min-height: 100px; 9 | width: 100%; 10 | height: 100%; 11 | max-width: 300px; 12 | max-height: 300px; 13 | border: 1px solid #989898; 14 | border-radius: 5px; 15 | margin-right: 10px; 16 | display: flex; 17 | justify-content: center; 18 | align-items: center; 19 | padding: 5px; 20 | `; 21 | 22 | export const MessageFileStatusElementImage = styled(ImageBox)` 23 | object-fit: contain; 24 | box-sizing: content-box; 25 | width: 95%; 26 | border-radius: 5px; 27 | `; 28 | 29 | export const MessageMdTextSnippet = styled(MdTextSnippet)` 30 | width: 100%; 31 | height: 100%; 32 | border-radius: 5px; 33 | `; 34 | 35 | export const DownloadContainer = styled.a` 36 | width: 100%; 37 | height: 100%; 38 | `; 39 | 40 | export const DownloadCover = styled.div` 41 | position: absolute; 42 | top: 2px; 43 | right: 2px; 44 | display: flex; 45 | justify-content: center; 46 | align-items: center; 47 | font-size: 12px; 48 | color: blueviolet; 49 | box-sizing: content-box; 50 | z-index: -5; 51 | `; 52 | 53 | export const StyleMdInsertDriveFile = styled(MdInsertDriveFile)` 54 | width: 100%; 55 | height: 100%; 56 | border-radius: 5px; 57 | color: black; 58 | opacity: 0.9; 59 | `; 60 | -------------------------------------------------------------------------------- /frontend/src/components/molecules/NoOverlayModal/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { RefObject } from 'react'; 2 | import Container from './styles'; 3 | 4 | interface Props { 5 | xWidth: number; 6 | yHeight: number; 7 | isOpened: boolean; 8 | children: JSX.Element; 9 | onClose: () => void; 10 | zIndex?: number; 11 | customRef: RefObject; 12 | className?: string; 13 | } 14 | 15 | const NoOverlayModal = ({ 16 | xWidth, 17 | yHeight, 18 | isOpened, 19 | onClose, 20 | customRef, 21 | children, 22 | className, 23 | zIndex, 24 | }: Props): JSX.Element => { 25 | return ( 26 | 35 | {children} 36 | 37 | ); 38 | }; 39 | 40 | NoOverlayModal.defaultProps = { 41 | className: '', 42 | zIndex: 80, 43 | }; 44 | 45 | export default NoOverlayModal; 46 | -------------------------------------------------------------------------------- /frontend/src/components/molecules/NoOverlayModal/styles.ts: -------------------------------------------------------------------------------- 1 | import Popup from '@atoms/Popup'; 2 | import { RefObject } from 'react'; 3 | import styled from 'styled-components'; 4 | 5 | interface Props { 6 | x: undefined | number; 7 | y: undefined | number; 8 | customRef: RefObject; 9 | } 10 | 11 | const Container = styled(Popup)` 12 | position: absolute; 13 | 14 | ${({ x, y }) => { 15 | return `top : ${y}px; left: ${x}px;`; 16 | }} 17 | background-color: #F8F8F8; 18 | 19 | border: 1px solid black; 20 | box-shadow: 0 0 0 1px rgb(29 28 29 / 13%), 0 4px 12px 0 rgb(0 0 0 / 12%); 21 | border-radius: 6px; 22 | overflow: hidden; 23 | `; 24 | 25 | export default Container; 26 | -------------------------------------------------------------------------------- /frontend/src/components/molecules/QuestionForm/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef } from 'react'; 2 | import ViewportInput from '@atoms/ViewPortInput'; 3 | import { Container, StyledLabel, StyledLabeledDefaultButton } from './styles'; 4 | 5 | interface Props { 6 | count: string; 7 | title: string; 8 | content: string; 9 | type?: string; 10 | placeholder?: string; 11 | onSubmit?: React.FormEventHandler; 12 | onChange?: React.FormEventHandler; 13 | onSet: (arg0: { value: string } | { files: File }) => void; 14 | } 15 | 16 | const QuestionForm = ({ 17 | count, 18 | type, 19 | title, 20 | content, 21 | placeholder, 22 | onSubmit, 23 | onChange, 24 | onSet, 25 | }: Props): JSX.Element => { 26 | const inputRef = useRef(); 27 | 28 | return ( 29 | 30 | 31 | 32 | 33 | 40 | { 43 | onSet(inputRef?.current); 44 | }} 45 | /> 46 | 47 | ); 48 | }; 49 | 50 | QuestionForm.defaultProps = { 51 | placeholder: '', 52 | type: 'text', 53 | onSubmit: () => {}, 54 | onChange: () => {}, 55 | }; 56 | 57 | export default QuestionForm; 58 | -------------------------------------------------------------------------------- /frontend/src/components/molecules/QuestionForm/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import Label from '@atoms/Label'; 3 | import LabeledDefaultButton from '@atoms/LabeledDefaultButton'; 4 | import { ThemeButton } from '@global/style/mixin'; 5 | 6 | export const Container = styled.div` 7 | position: relative; 8 | 9 | border: 0; 10 | 11 | margin: 5vh 1vw 0 1vw; 12 | overflow: hidden; 13 | 14 | & > span:first-child { 15 | font-size: large; 16 | color: #808080; 17 | margin-bottom: 3vh; 18 | } 19 | 20 | & > span:nth-child(3) { 21 | font-size: xx-large; 22 | font-weight: bold; 23 | } 24 | `; 25 | 26 | export const StyledLabel = styled(Label)` 27 | display: block; 28 | font-weight: bold; 29 | margin-bottom: 2vh; 30 | `; 31 | 32 | export const StyledLabeledDefaultButton = styled(LabeledDefaultButton)` 33 | margin-top: 5vh; 34 | width: 8vw; 35 | height: 6vh; 36 | ${ThemeButton} 37 | `; 38 | 39 | export default Container; 40 | -------------------------------------------------------------------------------- /frontend/src/components/molecules/SearchBar/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Container, StyledViewportInput } from './styles'; 3 | 4 | interface Props { 5 | placeholder: undefined | string; 6 | onSubmit: (e: React.FormEvent) => void; 7 | } 8 | 9 | const SearchBar = ({ placeholder, onSubmit }: Props): JSX.Element => { 10 | return ( 11 | 12 | 17 | 18 | ); 19 | }; 20 | 21 | export default SearchBar; 22 | -------------------------------------------------------------------------------- /frontend/src/components/molecules/SearchBar/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import ViewportInput from '@atoms/ViewPortInput'; 3 | import { BROWSER_CHANNEL_LIST_SIZE } from '@enum/index'; 4 | 5 | interface Props { 6 | width?: number; 7 | height?: number; 8 | } 9 | 10 | const { height: ListHeight } = BROWSER_CHANNEL_LIST_SIZE; 11 | 12 | export const Container = styled.div` 13 | display: flex; 14 | width: ${({ width }) => { 15 | if (width) return `${width}vw`; 16 | return '100%'; 17 | }}; 18 | `; 19 | 20 | export const StyledViewportInput = styled(ViewportInput)` 21 | display: flex; 22 | width: 100%; 23 | height: ${ListHeight}vh; 24 | background-color: rgba(var(--sk_primary_background, 255, 255, 255), 1); 25 | --saf-0: rgba(var(--sk_primary_foreground, 29, 28, 29), 0.3); 26 | border: 1px solid var(--saf-0); 27 | border-radius: 4px; 28 | color: rgba(var(--sk_foreground_max_solid, 97, 96, 97), 1); 29 | display: flex; 30 | padding: 0 8px; 31 | transition: border 80ms ease-out, box-shadow 80ms ease-out; 32 | font-size: 15px; 33 | line-height: 1.46668; 34 | font-weight: 400; 35 | `; 36 | 37 | export default Container; 38 | -------------------------------------------------------------------------------- /frontend/src/components/molecules/SelectWorkspace/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import ImageBox from '@atoms/ImageBox'; 3 | 4 | interface Props { 5 | width?: string; 6 | } 7 | 8 | export const Container = styled.div` 9 | display: flex; 10 | 11 | width: ${(props) => props.width ?? 'inherit'}; 12 | flex-direction: row; 13 | align-items: center; 14 | height: inherit; 15 | 16 | margin-left: 10px; 17 | overflow: visible; 18 | `; 19 | 20 | export const TextSet = styled.div` 21 | flex-direction: column; 22 | margin: 10px 10px 10px 10px; 23 | 24 | &>: first-child { 25 | font-weight: bold; 26 | } 27 | &>: last-child { 28 | color: grey; 29 | } 30 | `; 31 | 32 | export const StyledImageColumn = styled.div` 33 | min-width: 70px; 34 | min-height: 70px; 35 | width: 70px; 36 | height: 70px; 37 | `; 38 | 39 | export const StyledImageBox = styled(ImageBox)` 40 | border-radius: 5px; 41 | `; 42 | -------------------------------------------------------------------------------- /frontend/src/components/molecules/SelectbrowseChannelPage/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | const Container = styled.div` 4 | display: flex; 5 | justify-content: center; 6 | align-items: center; 7 | background: transparent; 8 | width: inherit; 9 | border: 0px solid; 10 | 11 | margin-top: 5vh; 12 | `; 13 | 14 | export const StyledButton = styled.button<{ isCursor?: boolean }>` 15 | background: transparent; 16 | border: 0; 17 | font-size: 20px; 18 | margin: 0 1vw 0 1vw; 19 | width: 30px; 20 | height: 30px; 21 | 22 | color: ${({ isCursor }) => (isCursor ? 'red' : '')}; 23 | 24 | &:hover { 25 | cursor: pointer; 26 | } 27 | `; 28 | 29 | export default Container; 30 | -------------------------------------------------------------------------------- /frontend/src/components/molecules/SidebarAddElement/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Label from '@atoms/Label'; 3 | import { Container, StyledLabel } from './styles'; 4 | 5 | interface Props { 6 | onClick: () => void; 7 | label: string; 8 | } 9 | 10 | const SidebarAddElement = ({ onClick, label }: Props): JSX.Element => { 11 | return ( 12 | 13 | 14 | 16 | 18 | ); 19 | }; 20 | 21 | export default SidebarAddElement; 22 | -------------------------------------------------------------------------------- /frontend/src/components/molecules/SidebarAddElement/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { theme } from 'styled-tools'; 3 | import { defaultTheme } from '@global/style/theme'; 4 | 5 | interface Props { 6 | width: number; 7 | height: number; 8 | } 9 | 10 | export const Container = styled.div` 11 | display: flex; 12 | flex-direction: row; 13 | align-items: center; 14 | height: 36px; 15 | width: 100%; 16 | 17 | color: ${theme('smallText', defaultTheme.smallText)}; 18 | &: hover { 19 | cursor: pointer; 20 | background-color: ${theme('focusedMenu', defaultTheme.focusedMenu)}; 21 | } 22 | `; 23 | 24 | export const StyledLabel = styled.span` 25 | margin: 1rem; 26 | `; 27 | -------------------------------------------------------------------------------- /frontend/src/components/molecules/SidebarChannelElement/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Label from '@atoms/Label'; 3 | import { Container, StyledLabel } from './styles'; 4 | 5 | interface Props { 6 | onClick: () => void; 7 | label: string; 8 | isPrivate: boolean; 9 | } 10 | 11 | const SidebarChannelElement = ({ 12 | onClick, 13 | label, 14 | isPrivate, 15 | }: Props): JSX.Element => { 16 | return ( 17 | 18 | 19 | 21 | 23 | ); 24 | }; 25 | 26 | export default SidebarChannelElement; 27 | -------------------------------------------------------------------------------- /frontend/src/components/molecules/SidebarChannelElement/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { theme } from 'styled-tools'; 3 | import { defaultTheme } from '@global/style/theme'; 4 | 5 | interface Props { 6 | width: number; 7 | height: number; 8 | } 9 | 10 | export const Container = styled.div` 11 | display: flex; 12 | flex-direction: row; 13 | align-items: center; 14 | height: 36px; 15 | min-width: 250px; 16 | &: hover { 17 | cursor: pointer; 18 | background-color: ${theme('focusedMenu', defaultTheme.focusedMenu)}; 19 | } 20 | `; 21 | 22 | export const StyledLabel = styled.span` 23 | margin: 1rem; 24 | `; 25 | -------------------------------------------------------------------------------- /frontend/src/components/molecules/SidebarDivision/index.tsx: -------------------------------------------------------------------------------- 1 | import IconButton from '@atoms/IconButton'; 2 | import Label from '@atoms/Label'; 3 | import React, { useContext } from 'react'; 4 | import { Container, StyledLabel, StyledIconButton } from './styles'; 5 | import { MdAdd } from 'react-icons/md'; 6 | import { useRecoilState } from 'recoil'; 7 | import { channelCreateModalState } from '@state/modal'; 8 | import { ThemeContext } from 'styled-components'; 9 | 10 | type SidebarDivisionTypes = 'Starred' | 'Channels' | 'Direct Messages'; 11 | 12 | interface Props { 13 | label: string; 14 | options?: boolean; 15 | type: SidebarDivisionTypes; 16 | } 17 | 18 | const SidebarDivision = ({ 19 | label, 20 | options = true, 21 | type, 22 | }: Props): JSX.Element => { 23 | const [isOpen, setIsOpen] = useRecoilState(channelCreateModalState); 24 | 25 | const themeContext = useContext(ThemeContext); 26 | const { smallText } = themeContext; 27 | 28 | return ( 29 | 30 | 31 | 33 | 34 | setIsOpen(true)} 37 | color={smallText} 38 | /> 39 | 40 | 41 | ); 42 | }; 43 | 44 | export default SidebarDivision; 45 | -------------------------------------------------------------------------------- /frontend/src/components/molecules/SidebarDivision/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | interface Props { 4 | width: number; 5 | height: number; 6 | color?: string; 7 | } 8 | 9 | export const Container = styled.div` 10 | display: flex; 11 | flex-direction: row; 12 | align-items: center; 13 | height: 36px; 14 | width: 100%; 15 | &: hover * { 16 | cursor: pointer; 17 | } ; 18 | `; 19 | 20 | export const StyledLabel = styled.span` 21 | flex-grow: 1; 22 | padding: 1rem; 23 | text-decoration: none; 24 | color: ${({ color }) => color}; 25 | `; 26 | 27 | export const StyledIconButton = styled.div` 28 | border-radius: 1rem; 29 | right: 1rem; 30 | color: ${({ color }) => color}; 31 | `; 32 | -------------------------------------------------------------------------------- /frontend/src/components/molecules/SortedOptionMordal/styles.ts: -------------------------------------------------------------------------------- 1 | import { RefObject } from 'react'; 2 | import Popup from '@atoms/Popup'; 3 | import { BUTTON_SIZE } from '@enum/index'; 4 | import { flexAlignCenter } from '@global/style/mixin'; 5 | import styled from 'styled-components'; 6 | 7 | const { height: ButtonHeight } = BUTTON_SIZE; 8 | 9 | interface Props { 10 | x: undefined | number; 11 | y: undefined | number; 12 | customRef: RefObject; 13 | } 14 | 15 | const Container = styled(Popup)` 16 | position: absolute; 17 | 18 | ${({ x, y }) => { 19 | return `top : ${ButtonHeight + y + 36}px; left: ${x - 61}px;`; 20 | }} 21 | background-color: white; 22 | width: 200px; 23 | height: 130px; 24 | 25 | --saf-0: rgba(var(--sk_foreground_low, 29, 28, 29), 0.13); 26 | box-shadow: 0 0 0 1px var(--saf-0), 0 4px 12px 0 rgba(0, 0, 0, 0.12); 27 | background-color: rgba(var(--sk_foreground_min_solid, 248, 248, 248), 1); 28 | border-radius: 6px; 29 | padding: 0.1px 0; 30 | `; 31 | 32 | export const StyledDiv = styled.div` 33 | display: flx; 34 | ${flexAlignCenter} 35 | flex-direction : row; 36 | width: 100%; 37 | height: 20px; 38 | margin: 5px 0 10px 0; 39 | 40 | & > * { 41 | margin-right: 5px; 42 | } 43 | `; 44 | export default Container; 45 | -------------------------------------------------------------------------------- /frontend/src/components/molecules/ThreadContent/ThreadFileStatusBar/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | ThreadFileStatusBarLayout, 4 | ThreadFileStatusBarContainer, 5 | } from './styles'; 6 | import ThreadFileStatusElement from '../ThreadFileStatusElement'; 7 | 8 | interface Props { 9 | files: File[]; 10 | } 11 | 12 | const ThreadFileStatusBar = ({ files }: Props): JSX.Element => { 13 | return ( 14 | <> 15 | 16 | 17 | {files.map((file, index) => ( 18 | 19 | ))} 20 | 21 | 22 | 23 | ); 24 | }; 25 | 26 | export default ThreadFileStatusBar; 27 | -------------------------------------------------------------------------------- /frontend/src/components/molecules/ThreadContent/ThreadFileStatusBar/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const ThreadFileStatusBarContainer = styled.div` 4 | max-width: 100%; 5 | width: 100%; 6 | display: grid; 7 | grid-template-columns: 1fr 1fr; 8 | grid-auto-rows: 1fr; 9 | gap: 10px 20px; 10 | `; 11 | 12 | export const ThreadFileStatusBarLayout = styled.div` 13 | max-width: 100%; 14 | width: 730px; 15 | margin-bottom: 10px; 16 | `; 17 | -------------------------------------------------------------------------------- /frontend/src/components/molecules/ThreadContent/ThreadFileStatusElement/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import ImageBox from '@atoms/ImageBox'; 3 | import { MdTextSnippet, MdInsertDriveFile } from 'react-icons/md'; 4 | 5 | export const ThreadFileStatusLayOut = styled.div` 6 | position: relative; 7 | max-width: calc(100% - 5px); 8 | width: 100%; 9 | height: auto; 10 | border: 1px solid #989898; 11 | border-radius: 5px; 12 | margin-right: 10px; 13 | display: flex; 14 | justify-content: center; 15 | align-items: center; 16 | padding: 5px; 17 | `; 18 | 19 | export const ThreadFileStatusElementImage = styled(ImageBox)` 20 | object-fit: contain; 21 | box-sizing: content-box; 22 | width: 95%; 23 | border-radius: 5px; 24 | `; 25 | 26 | export const ThreadMdTextSnippet = styled(MdTextSnippet)` 27 | width: 100%; 28 | height: 100%; 29 | border-radius: 5px; 30 | `; 31 | 32 | export const DownloadContainer = styled.a` 33 | width: 100%; 34 | height: 100%; 35 | `; 36 | 37 | export const DownloadCover = styled.div` 38 | position: absolute; 39 | top: 2px; 40 | right: 2px; 41 | display: flex; 42 | justify-content: center; 43 | align-items: center; 44 | font-size: 12px; 45 | color: blueviolet; 46 | box-sizing: content-box; 47 | z-index: -5; 48 | `; 49 | 50 | export const StyleMdInsertDriveFile = styled(MdInsertDriveFile)` 51 | width: 100%; 52 | height: 100%; 53 | border-radius: 5px; 54 | color: black; 55 | opacity: 0.9; 56 | `; 57 | -------------------------------------------------------------------------------- /frontend/src/components/organisms/BrowseContent/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useRecoilValue, useSetRecoilState } from 'recoil'; 3 | import Label from '@atoms/Label'; 4 | import BrowseChannelList from '@organisms/BrowseChannelList'; 5 | import { channelCreateModalState } from '@state/modal'; 6 | import { mainWorkspaceSizeState } from '@state/workspace'; 7 | import { 8 | Container, 9 | StyledBrowseChannelHeader, 10 | StyledLabeledButton, 11 | } from './styles'; 12 | 13 | const BrowseContent = (): JSX.Element => { 14 | const setIsOpen = useSetRecoilState(channelCreateModalState); 15 | const WIDTHSIZE = useRecoilValue(mainWorkspaceSizeState); 16 | 17 | const Title: JSX.Element =