From 2ce1c27ac36793c11cedc847952d7e7db3735074 Mon Sep 17 00:00:00 2001 From: rockpell Date: Thu, 10 Dec 2020 03:17:21 +0900 Subject: [PATCH 01/50] Chore: aws-sdk, multer package --- backend/package-lock.json | 210 +++++++++++++++++++++++++++++++++++++- backend/package.json | 2 + 2 files changed, 209 insertions(+), 3 deletions(-) diff --git a/backend/package-lock.json b/backend/package-lock.json index b50f6b46..f9453e06 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -1307,6 +1307,11 @@ "picomatch": "^2.0.4" } }, + "append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha1-HjRA6RXwsSA9I3SOeO3XubW0PlY=" + }, "argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -1348,6 +1353,22 @@ "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", "dev": true }, + "aws-sdk": { + "version": "2.348.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.348.0.tgz", + "integrity": "sha512-TfguapuOAwk7EG8zhYJPjkCaF4tyGjfgcXLkYbWbuS4O6E8pn0x2K5Yt1KXwLiWxG0fzKCLiiaNA5H7bKAP4YQ==", + "requires": { + "buffer": "4.9.1", + "events": "1.1.1", + "ieee754": "1.1.8", + "jmespath": "0.15.0", + "querystring": "0.2.0", + "sax": "1.2.1", + "url": "0.10.3", + "uuid": "3.1.0", + "xml2js": "0.4.19" + } + }, "babel-plugin-dynamic-import-node": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz", @@ -1362,6 +1383,11 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" }, + "base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" + }, "base64id": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", @@ -1544,6 +1570,16 @@ "resolved": "https://registry.npmjs.org/bson/-/bson-1.1.5.tgz", "integrity": "sha512-kDuEzldR21lHciPQAIulLs1LZlCXdLziXI6Mb/TDkwXhb//UORJNPXgcRs2CuO4H0DcMkpfT3/ySsP3unoZjBg==" }, + "buffer": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", + "integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=", + "requires": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" + } + }, "buffer-equal-constant-time": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", @@ -1552,8 +1588,39 @@ "buffer-from": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", - "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", - "dev": true + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" + }, + "busboy": { + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-0.2.14.tgz", + "integrity": "sha1-bCpiLvz0fFe7vh4qnDetNseSVFM=", + "requires": { + "dicer": "0.2.5", + "readable-stream": "1.1.x" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + }, + "readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + } + } }, "bytes": { "version": "3.0.0", @@ -1720,6 +1787,17 @@ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" }, + "concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "requires": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, "concurrently": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-5.3.0.tgz", @@ -1917,6 +1995,38 @@ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" }, + "dicer": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/dicer/-/dicer-0.2.5.tgz", + "integrity": "sha1-WZbAhrszIYyBLAkL3cCc0S+stw8=", + "requires": { + "readable-stream": "1.1.x", + "streamsearch": "0.1.2" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + }, + "readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + } + } + }, "doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -2513,6 +2623,11 @@ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" }, + "events": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", + "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=" + }, "express": { "version": "4.16.4", "resolved": "https://registry.npmjs.org/express/-/express-4.16.4.tgz", @@ -2676,6 +2791,11 @@ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" }, + "fs": { + "version": "0.0.1-security", + "resolved": "https://registry.npmjs.org/fs/-/fs-0.0.1-security.tgz", + "integrity": "sha1-invTcYa23d84E/I4WLV+yq9eQdQ=" + }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -2856,6 +2976,11 @@ "safer-buffer": ">= 2.1.2 < 3" } }, + "ieee754": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.8.tgz", + "integrity": "sha1-vjPUCsEO8ZJnAfbwii2G+/0a0+Q=" + }, "ignore": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", @@ -3051,6 +3176,11 @@ "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", "dev": true }, + "jmespath": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.15.0.tgz", + "integrity": "sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc=" + }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -3337,7 +3467,6 @@ "version": "0.5.5", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", - "dev": true, "requires": { "minimist": "^1.2.5" } @@ -3434,6 +3563,21 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" }, + "multer": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.2.tgz", + "integrity": "sha512-xY8pX7V+ybyUpbYMxtjM9KAiD9ixtg5/JkeKUTD6xilfDv0vzzOFcCp4Ljb1UU3tSOM3VTZtKo63OmzOrGi3Cg==", + "requires": { + "append-field": "^1.0.0", + "busboy": "^0.2.11", + "concat-stream": "^1.5.2", + "mkdirp": "^0.5.1", + "object-assign": "^4.1.1", + "on-finished": "^2.3.0", + "type-is": "^1.6.4", + "xtend": "^4.0.0" + } + }, "natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -3899,6 +4043,11 @@ "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" }, + "querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=" + }, "range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -4203,6 +4352,11 @@ "sparse-bitfield": "^3.0.3" } }, + "sax": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", + "integrity": "sha1-e45lYZCyKOgaZq6nSEgNgozS03o=" + }, "semver": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", @@ -4443,6 +4597,11 @@ "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==" }, + "streamsearch": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz", + "integrity": "sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo=" + }, "string-width": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", @@ -4658,6 +4817,11 @@ "mime-types": "~2.1.24" } }, + "typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" + }, "typedarray-to-buffer": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", @@ -4794,6 +4958,22 @@ "punycode": "^2.1.0" } }, + "url": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz", + "integrity": "sha1-Ah5NnHcF8hu/N9A861h2dAJ3TGQ=", + "requires": { + "punycode": "1.3.2", + "querystring": "0.2.0" + }, + "dependencies": { + "punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=" + } + } + }, "url-parse-lax": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz", @@ -4812,6 +4992,11 @@ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" }, + "uuid": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.1.0.tgz", + "integrity": "sha512-DIWtzUkw04M4k3bf1IcpS2tngXEL26YUD2M0tMDUpnUrz2hgzUBlD55a4FjdLGPvfHxS6uluGWvaVEqgBcVa+g==" + }, "v8-compile-cache": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.2.0.tgz", @@ -4949,6 +5134,25 @@ "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz", "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==" }, + "xml2js": { + "version": "0.4.19", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz", + "integrity": "sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==", + "requires": { + "sax": ">=0.6.0", + "xmlbuilder": "~9.0.1" + } + }, + "xmlbuilder": { + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", + "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=" + }, + "xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" + }, "y18n": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", diff --git a/backend/package.json b/backend/package.json index 358b8c08..5759dbf0 100644 --- a/backend/package.json +++ b/backend/package.json @@ -18,6 +18,7 @@ }, "homepage": "https://github.com/boostcamp-2020/Project12-C-Slack-Web", "dependencies": { + "aws-sdk": "^2.348.0", "concurrently": "^5.3.0", "cookie-parser": "^1.4.5", "core-js": "^3.6.5", @@ -29,6 +30,7 @@ "jsonwebtoken": "^8.5.1", "mongoose": "^5.10.15", "morgan": "~1.9.1", + "multer": "^1.4.2", "nodemon": "^2.0.4", "passport": "^0.4.1", "passport-github": "^1.1.0", From 878b88c030adcec6a61657ad47c3f088bc00f9a1 Mon Sep 17 00:00:00 2001 From: rockpell Date: Thu, 10 Dec 2020 03:18:08 +0900 Subject: [PATCH 02/50] Feat: s3 config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit s3 설정 파일 추가 --- backend/config/s3.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 backend/config/s3.js diff --git a/backend/config/s3.js b/backend/config/s3.js new file mode 100644 index 00000000..19f04b04 --- /dev/null +++ b/backend/config/s3.js @@ -0,0 +1,15 @@ +const AWS = require('aws-sdk') +require('dotenv').config() + +const S3 = new AWS.S3({ + endpoint: process.env.S3_ENDPOINT, + region: process.env.S3_REGION, + credentials: { + accessKeyId: process.env.S3_ACCESSKEY, + secretAccessKey: process.env.S3_SECRETKEY, + }, +}) + +const BUCKETNAME = process.env.S3_BUCKETNAME + +module.exports = { S3, BUCKETNAME } From 8a555b70f8fa3bffa8e41196cd4de8ca586d675a Mon Sep 17 00:00:00 2001 From: rockpell Date: Thu, 10 Dec 2020 03:19:19 +0900 Subject: [PATCH 03/50] Feat: file upload api MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit file upload api 추가 --- backend/controller/file/file.js | 26 ++++++++++++++++++++++ backend/controller/file/index.js | 14 ++++++++++++ backend/controller/index.js | 3 ++- backend/service/file.js | 37 ++++++++++++++++++++++++++++++++ 4 files changed, 79 insertions(+), 1 deletion(-) create mode 100644 backend/controller/file/file.js create mode 100644 backend/controller/file/index.js create mode 100644 backend/service/file.js diff --git a/backend/controller/file/file.js b/backend/controller/file/file.js new file mode 100644 index 00000000..08969f3a --- /dev/null +++ b/backend/controller/file/file.js @@ -0,0 +1,26 @@ +import { asyncWrapper } from '../../util' +import service from '../../service/file' + +exports.getFile = asyncWrapper(async (req, res) => { + const { code, success, data } = await service.getFile({ + ...req.body, + creator: req.user.id, + }) + return res.status(code).json({ success, data }) +}) + +exports.uploadFile = asyncWrapper(async (req, res) => { + const { code, success, data } = await service.uploadFile({ + file: req.file, + userId: req.user.id, + }) + return res.status(code).json({ success, data }) +}) + +exports.deleteFile = asyncWrapper(async (req, res) => { + const { code, success, data } = service.deleteFile({ + ...req.body, + userId: req.user.id, + }) + return res.status(code).json({ success, data }) +}) diff --git a/backend/controller/file/index.js b/backend/controller/file/index.js new file mode 100644 index 00000000..1b16c509 --- /dev/null +++ b/backend/controller/file/index.js @@ -0,0 +1,14 @@ +import express from 'express' +const { Auth } = require('../../middleware/auth') +const router = express.Router() +const controller = require('./file') + +const multer = require('multer') +const storage = multer.memoryStorage() +const uploader = multer({ storage: storage }) + +router.get('/', Auth, controller.getFile) +router.post('/', Auth, uploader.single('file'), controller.uploadFile) +router.delete('/', Auth, controller.deleteFile) + +module.exports = router diff --git a/backend/controller/index.js b/backend/controller/index.js index d65bf5fe..c4f8cbe7 100644 --- a/backend/controller/index.js +++ b/backend/controller/index.js @@ -1,10 +1,10 @@ import express from 'express' - import channelCotroller from './channel' import searchCotroller from './search' import userController from './user' import workspaceController from './workspace' import chatController from './chat' +import fileController from './file' const router = express.Router() router.use('/channel', channelCotroller) @@ -12,5 +12,6 @@ router.use('/search', searchCotroller) router.use('/user', userController) router.use('/workspace', workspaceController) router.use('/chat', chatController) +router.use('/file', fileController) module.exports = router diff --git a/backend/service/file.js b/backend/service/file.js new file mode 100644 index 00000000..8bf99773 --- /dev/null +++ b/backend/service/file.js @@ -0,0 +1,37 @@ +import { verifyRequiredParams, dbErrorHandler } from '../util/' +import statusCode from '../util/statusCode' +import { S3, BUCKETNAME } from '../config/s3' +import { File } from '../model/File' + +const getFile = async () => {} + +const uploadFile = async ({ file, userId }) => { + verifyRequiredParams(file, userId) + const fileName = `${file.fieldname}-${Date.now()}-${file.originalname}` + console.log('fileName: ', fileName) + + await S3.putObject({ + Bucket: BUCKETNAME, + Key: fileName, + ACL: 'public-read', + Body: file.buffer, + }).promise() + + const data = await dbErrorHandler(() => + File.create({ + name: fileName, + path: '/', + fileType: file.mimetype, + creator: userId, + }), + ) + return { code: statusCode.OK, data: data, success: true } +} + +const deleteFile = async () => {} + +module.exports = { + getFile, + uploadFile, + deleteFile, +} From 301531700807c878505764034cc5b3a67d274092 Mon Sep 17 00:00:00 2001 From: rockpell Date: Thu, 10 Dec 2020 03:19:45 +0900 Subject: [PATCH 04/50] Feat: FileUploader component MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit FileUploader component 추가 현재 미완성 --- .../src/organism/FileUploader/FileUploader.js | 31 +++++++++++++++++++ .../FileUploader/FileUploader.stories.js | 10 ++++++ frontend/src/organism/FileUploader/index.js | 1 + 3 files changed, 42 insertions(+) create mode 100644 frontend/src/organism/FileUploader/FileUploader.js create mode 100644 frontend/src/organism/FileUploader/FileUploader.stories.js create mode 100644 frontend/src/organism/FileUploader/index.js diff --git a/frontend/src/organism/FileUploader/FileUploader.js b/frontend/src/organism/FileUploader/FileUploader.js new file mode 100644 index 00000000..bd46cc8b --- /dev/null +++ b/frontend/src/organism/FileUploader/FileUploader.js @@ -0,0 +1,31 @@ +import React, { useState } from 'react' +import request from '../../util/request' + +const fileContentType = 'multipart/form-data' + +function FileUploader() { + const [selectedFile, setSelectedFile] = useState() + + const handleFileInput = e => { + setSelectedFile(e.target.files[0]) + } + const handlePost = async () => { + const formData = new FormData() + formData.append('file', selectedFile) + await request.POST('/api/file', formData, fileContentType) + console.log('formData: ', formData) + } + + return ( + <> + handleFileInput(e)} + > + + + ) +} + +export default FileUploader diff --git a/frontend/src/organism/FileUploader/FileUploader.stories.js b/frontend/src/organism/FileUploader/FileUploader.stories.js new file mode 100644 index 00000000..b477fa6c --- /dev/null +++ b/frontend/src/organism/FileUploader/FileUploader.stories.js @@ -0,0 +1,10 @@ +import React from 'react' +import { storiesOf } from '@storybook/react' +import FileUploader from './FileUploader' + +const stories = storiesOf('Organism', module) + +const TestComponent = () => { + return +} +stories.add('FileUploader', () => ) diff --git a/frontend/src/organism/FileUploader/index.js b/frontend/src/organism/FileUploader/index.js new file mode 100644 index 00000000..75f372b2 --- /dev/null +++ b/frontend/src/organism/FileUploader/index.js @@ -0,0 +1 @@ +export { default } from './FileUploader.js' From a3442e7db555f46c2de336b599d42b1855e1421c Mon Sep 17 00:00:00 2001 From: rockpell Date: Thu, 10 Dec 2020 15:02:51 +0900 Subject: [PATCH 05/50] Feat: file upload, delete api MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 파일 업로드, 삭제 api 추가 --- backend/controller/file/file.js | 5 ++-- backend/model/File.js | 6 +++++ backend/service/file.js | 44 +++++++++++++++++++++++++++++---- 3 files changed, 47 insertions(+), 8 deletions(-) diff --git a/backend/controller/file/file.js b/backend/controller/file/file.js index 08969f3a..92fd5a66 100644 --- a/backend/controller/file/file.js +++ b/backend/controller/file/file.js @@ -18,9 +18,8 @@ exports.uploadFile = asyncWrapper(async (req, res) => { }) exports.deleteFile = asyncWrapper(async (req, res) => { - const { code, success, data } = service.deleteFile({ + const { code, success } = await service.deleteFile({ ...req.body, - userId: req.user.id, }) - return res.status(code).json({ success, data }) + return res.status(code).json({ success }) }) diff --git a/backend/model/File.js b/backend/model/File.js index 6edd541e..bb4bb871 100644 --- a/backend/model/File.js +++ b/backend/model/File.js @@ -12,10 +12,16 @@ const fileSchema = mongoose.Schema( name: { type: String, }, + originalName: { + type: String, + }, creator: { type: Schema.Types.ObjectId, ref: 'User', }, + etag: { + type: String, + }, }, { timestamps: true }, ) diff --git a/backend/service/file.js b/backend/service/file.js index 8bf99773..90b1e5fa 100644 --- a/backend/service/file.js +++ b/backend/service/file.js @@ -2,15 +2,18 @@ import { verifyRequiredParams, dbErrorHandler } from '../util/' import statusCode from '../util/statusCode' import { S3, BUCKETNAME } from '../config/s3' import { File } from '../model/File' +const mongoose = require('mongoose') +const ObjectId = mongoose.Types.ObjectId -const getFile = async () => {} +const getFile = async ({ fileId }) => { + verifyRequiredParams(fileId) +} const uploadFile = async ({ file, userId }) => { verifyRequiredParams(file, userId) const fileName = `${file.fieldname}-${Date.now()}-${file.originalname}` - console.log('fileName: ', fileName) - await S3.putObject({ + const result = await S3.putObject({ Bucket: BUCKETNAME, Key: fileName, ACL: 'public-read', @@ -20,15 +23,46 @@ const uploadFile = async ({ file, userId }) => { const data = await dbErrorHandler(() => File.create({ name: fileName, + originalName: file.originalname, path: '/', fileType: file.mimetype, creator: userId, + etag: result.ETag, }), ) - return { code: statusCode.OK, data: data, success: true } + return { + code: statusCode.OK, + data: { + fileId: data._id, + fileName: data.originalName, + fileType: data.fileType, + creator: data.creator, + }, + success: true, + } } -const deleteFile = async () => {} +const deleteFile = async ({ fileId }) => { + verifyRequiredParams(fileId) + const fileData = await dbErrorHandler(() => + File.findOne({ + _id: ObjectId(fileId), + }), + ) + if (!fileData) { + return { code: statusCode.BAD_REQUEST, success: false } + } + await dbErrorHandler(() => + File.deleteOne({ + _id: ObjectId(fileId), + }), + ) + await S3.deleteObject({ + Bucket: BUCKETNAME, + Key: fileData.name, + }).promise() + return { code: statusCode.OK, success: true } +} module.exports = { getFile, From 8249768cf1bddd947ed7d4a0b01aae545f009ca2 Mon Sep 17 00:00:00 2001 From: rockpell Date: Thu, 10 Dec 2020 16:39:35 +0900 Subject: [PATCH 06/50] Feat: getFileURL api MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit getFile api를 getFileURL로 변경 --- backend/controller/file/file.js | 11 +++++++++-- backend/controller/file/index.js | 3 ++- backend/service/file.js | 20 ++++++++++++++++++-- 3 files changed, 29 insertions(+), 5 deletions(-) diff --git a/backend/controller/file/file.js b/backend/controller/file/file.js index 92fd5a66..04f6a615 100644 --- a/backend/controller/file/file.js +++ b/backend/controller/file/file.js @@ -1,8 +1,15 @@ import { asyncWrapper } from '../../util' import service from '../../service/file' -exports.getFile = asyncWrapper(async (req, res) => { - const { code, success, data } = await service.getFile({ +exports.getFileURL = asyncWrapper(async (req, res) => { + const { code, success, data } = await service.getFileURL({ + ...req.query, + }) + return res.status(code).json({ success, data }) +}) + +exports.downloadFile = asyncWrapper(async (req, res) => { + const { code, success, data } = await service.downloadFile({ ...req.body, creator: req.user.id, }) diff --git a/backend/controller/file/index.js b/backend/controller/file/index.js index 1b16c509..61513ab2 100644 --- a/backend/controller/file/index.js +++ b/backend/controller/file/index.js @@ -7,7 +7,8 @@ const multer = require('multer') const storage = multer.memoryStorage() const uploader = multer({ storage: storage }) -router.get('/', Auth, controller.getFile) +router.get('/', Auth, controller.getFileURL) +router.get('/download', Auth, controller.downloadFile) router.post('/', Auth, uploader.single('file'), controller.uploadFile) router.delete('/', Auth, controller.deleteFile) diff --git a/backend/service/file.js b/backend/service/file.js index 90b1e5fa..a0cd8dce 100644 --- a/backend/service/file.js +++ b/backend/service/file.js @@ -5,10 +5,24 @@ import { File } from '../model/File' const mongoose = require('mongoose') const ObjectId = mongoose.Types.ObjectId -const getFile = async ({ fileId }) => { +const getFileURL = async ({ fileId }) => { verifyRequiredParams(fileId) + + const fileData = await dbErrorHandler(() => + File.findOne({ + _id: ObjectId(fileId), + }), + ) + + return { + code: statusCode.OK, + data: `${process.env.S3_ENDPOINT}/${process.env.S3_BUCKETNAME}/${fileData.originalName}`, + success: true, + } } +const downloadFile = async ({ fileId }) => {} + const uploadFile = async ({ file, userId }) => { verifyRequiredParams(file, userId) const fileName = `${file.fieldname}-${Date.now()}-${file.originalname}` @@ -37,6 +51,7 @@ const uploadFile = async ({ file, userId }) => { fileName: data.originalName, fileType: data.fileType, creator: data.creator, + etag: data.etag, }, success: true, } @@ -65,7 +80,8 @@ const deleteFile = async ({ fileId }) => { } module.exports = { - getFile, uploadFile, + getFileURL, + downloadFile, deleteFile, } From 758fbe51e0303207c3e9883dc71021f405c878d1 Mon Sep 17 00:00:00 2001 From: rockpell Date: Fri, 11 Dec 2020 00:53:46 +0900 Subject: [PATCH 07/50] Fix: delete console.log --- frontend/src/organism/FileUploader/FileUploader.js | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/src/organism/FileUploader/FileUploader.js b/frontend/src/organism/FileUploader/FileUploader.js index bd46cc8b..1a330e28 100644 --- a/frontend/src/organism/FileUploader/FileUploader.js +++ b/frontend/src/organism/FileUploader/FileUploader.js @@ -13,7 +13,6 @@ function FileUploader() { const formData = new FormData() formData.append('file', selectedFile) await request.POST('/api/file', formData, fileContentType) - console.log('formData: ', formData) } return ( From 23cc75656dac53e46d07564db928726607a355e0 Mon Sep 17 00:00:00 2001 From: rockpell Date: Fri, 11 Dec 2020 00:54:11 +0900 Subject: [PATCH 08/50] Fix: getFileURL API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit getFileURL 실행시 반환하는 url이 원본 파일 이름이 포함되어 제대로 동작 안되는 문제 수정 --- backend/service/file.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/backend/service/file.js b/backend/service/file.js index a0cd8dce..bd7450f4 100644 --- a/backend/service/file.js +++ b/backend/service/file.js @@ -16,7 +16,10 @@ const getFileURL = async ({ fileId }) => { return { code: statusCode.OK, - data: `${process.env.S3_ENDPOINT}/${process.env.S3_BUCKETNAME}/${fileData.originalName}`, + data: { + url: `${process.env.S3_ENDPOINT}/${process.env.S3_BUCKETNAME}/${fileData.name}`, + originalName: fileData.originalName, + }, success: true, } } From a2d2564d5fcc61a3b95a6747c96c5043536ed826 Mon Sep 17 00:00:00 2001 From: rockpell Date: Fri, 11 Dec 2020 00:58:34 +0900 Subject: [PATCH 09/50] Feat: ImgPreview Component MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ImgPreview 컴포넌트 추가 fileId를 값으로 파일을 요청하여 이미지로 보여주는 기능 이미지 파일이 맞는지 확인하는 로직 필요할듯 --- frontend/src/atom/ImgPreview/ImgPreview.js | 29 +++++++++++++++++++ .../src/atom/ImgPreview/ImgPreview.stories.js | 15 ++++++++++ frontend/src/atom/ImgPreview/index.js | 1 + 3 files changed, 45 insertions(+) create mode 100644 frontend/src/atom/ImgPreview/ImgPreview.js create mode 100644 frontend/src/atom/ImgPreview/ImgPreview.stories.js create mode 100644 frontend/src/atom/ImgPreview/index.js diff --git a/frontend/src/atom/ImgPreview/ImgPreview.js b/frontend/src/atom/ImgPreview/ImgPreview.js new file mode 100644 index 00000000..4deae48a --- /dev/null +++ b/frontend/src/atom/ImgPreview/ImgPreview.js @@ -0,0 +1,29 @@ +import React, { useState, useEffect } from 'react' +import styled from 'styled-components' +import request from '../../util/request' + +function ImgPreview({ type, fileId, maxSize }) { + const [fileData, setFileData] = useState({}) + + useEffect(() => { + ;(async () => { + const { data } = await request.GET('/api/file', { fileId }) + setFileData(data.data) + })() + }, [fileId]) + + return ( + + ) +} + +const StyledImg = styled.img` + max-width: ${({ maxSize }) => maxSize}; + height: auto; + border-radius: 2%; +` +export default ImgPreview diff --git a/frontend/src/atom/ImgPreview/ImgPreview.stories.js b/frontend/src/atom/ImgPreview/ImgPreview.stories.js new file mode 100644 index 00000000..280132f6 --- /dev/null +++ b/frontend/src/atom/ImgPreview/ImgPreview.stories.js @@ -0,0 +1,15 @@ +import React from 'react' +import ImgPreview from './ImgPreview' + +export default { + title: 'Atom/ImgPreview', + component: ImgPreview, +} + +const Template = args => + +export const defaultImgPreview = Template.bind({}) +defaultImgPreview.args = { + fileId: '5fd1bbbadc9599329916ef90', + maxSize: '300px', +} diff --git a/frontend/src/atom/ImgPreview/index.js b/frontend/src/atom/ImgPreview/index.js new file mode 100644 index 00000000..11607d6e --- /dev/null +++ b/frontend/src/atom/ImgPreview/index.js @@ -0,0 +1 @@ +export { default } from './ImgPreview' From ebcac89df1950265ea4d2683f7c25bcbca9a594d Mon Sep 17 00:00:00 2001 From: rockpell Date: Sat, 12 Dec 2020 21:12:17 +0900 Subject: [PATCH 10/50] Feat: ImgPreview type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ImgPreview type에 따라 다른 버튼이 나타나는 기능 추가 --- frontend/src/atom/ImgPreview/ImgPreview.js | 29 ----- .../src/atom/ImgPreview/ImgPreview.stories.js | 15 --- .../src/organism/ImgPreview/ImgPreview.js | 111 ++++++++++++++++++ .../organism/ImgPreview/ImgPreview.stories.js | 23 ++++ .../{atom => organism}/ImgPreview/index.js | 0 5 files changed, 134 insertions(+), 44 deletions(-) delete mode 100644 frontend/src/atom/ImgPreview/ImgPreview.js delete mode 100644 frontend/src/atom/ImgPreview/ImgPreview.stories.js create mode 100644 frontend/src/organism/ImgPreview/ImgPreview.js create mode 100644 frontend/src/organism/ImgPreview/ImgPreview.stories.js rename frontend/src/{atom => organism}/ImgPreview/index.js (100%) diff --git a/frontend/src/atom/ImgPreview/ImgPreview.js b/frontend/src/atom/ImgPreview/ImgPreview.js deleted file mode 100644 index 4deae48a..00000000 --- a/frontend/src/atom/ImgPreview/ImgPreview.js +++ /dev/null @@ -1,29 +0,0 @@ -import React, { useState, useEffect } from 'react' -import styled from 'styled-components' -import request from '../../util/request' - -function ImgPreview({ type, fileId, maxSize }) { - const [fileData, setFileData] = useState({}) - - useEffect(() => { - ;(async () => { - const { data } = await request.GET('/api/file', { fileId }) - setFileData(data.data) - })() - }, [fileId]) - - return ( - - ) -} - -const StyledImg = styled.img` - max-width: ${({ maxSize }) => maxSize}; - height: auto; - border-radius: 2%; -` -export default ImgPreview diff --git a/frontend/src/atom/ImgPreview/ImgPreview.stories.js b/frontend/src/atom/ImgPreview/ImgPreview.stories.js deleted file mode 100644 index 280132f6..00000000 --- a/frontend/src/atom/ImgPreview/ImgPreview.stories.js +++ /dev/null @@ -1,15 +0,0 @@ -import React from 'react' -import ImgPreview from './ImgPreview' - -export default { - title: 'Atom/ImgPreview', - component: ImgPreview, -} - -const Template = args => - -export const defaultImgPreview = Template.bind({}) -defaultImgPreview.args = { - fileId: '5fd1bbbadc9599329916ef90', - maxSize: '300px', -} diff --git a/frontend/src/organism/ImgPreview/ImgPreview.js b/frontend/src/organism/ImgPreview/ImgPreview.js new file mode 100644 index 00000000..8b90fd7b --- /dev/null +++ b/frontend/src/organism/ImgPreview/ImgPreview.js @@ -0,0 +1,111 @@ +import React, { useState, useEffect } from 'react' +import styled from 'styled-components' +import request from '../../util/request' +import Button from '../../atom/Button' +import Icon from '../../atom/Icon' +import { CLOSE } from '../../constant/icon' +import { COLOR } from '../../constant/style' + +function ImgPreview({ type, fileId, maxSize }) { + const [fileData, setFileData] = useState({}) + const [isHover, setIsHover] = useState(false) + + useEffect(() => { + ;(async () => { + const { data } = await request.GET('/api/file', { fileId }) + setFileData(data.data) + })() + }, [fileId]) + + const enterMouseHandle = () => { + setIsHover(true) + } + + const leaveMouseHandle = () => { + setIsHover(false) + } + + const handleClose = () => { + console.log('handleClose') + } + + const deleteButton = () => { + return ( + + + + ) + } + + const downloadButton = () => { + return ( + { + console.log('onClick!!!') + }} + onMouseEnter={enterMouseHandle} + onMouseLeave={leaveMouseHandle} + > + Click to Download + + ) + } + + return ( + + + {isHover && + (type === 'input' + ? deleteButton() + : type === 'message' + ? downloadButton() + : null)} + + ) +} + +const StyledDiv = styled.div` + display: inline-block; + position: relative; +` + +const StyledImg = styled.img` + max-width: ${({ maxSize }) => maxSize}; + height: auto; + border-radius: 2%; + background: white; +` + +const ButtonDiv = styled.div` + position: absolute; + right: 5px; + top: 0; + background: ${COLOR.LIGHT_GRAY}; +` + +const DownloadDiv = styled.div` + position: absolute; + top: 0; + width: 100%; + height: 100%; +` + +const ClickToDownloadSpan = styled.span` + position: absolute; + bottom: 5px; + left: 10px; + color: ${COLOR.GRAY}; +` + +export default ImgPreview diff --git a/frontend/src/organism/ImgPreview/ImgPreview.stories.js b/frontend/src/organism/ImgPreview/ImgPreview.stories.js new file mode 100644 index 00000000..9c81570c --- /dev/null +++ b/frontend/src/organism/ImgPreview/ImgPreview.stories.js @@ -0,0 +1,23 @@ +import React from 'react' +import ImgPreview from './ImgPreview' + +export default { + title: 'Organism/ImgPreview', + component: ImgPreview, +} + +const Template = args => + +export const inputImgPreview = Template.bind({}) +inputImgPreview.args = { + type: 'input', // input, message + fileId: '5fd1ed979f167053f728e7ce', + maxSize: '300px', +} + +export const messageImgPreview = Template.bind({}) +messageImgPreview.args = { + type: 'message', // input, message + fileId: '5fd1ed979f167053f728e7ce', + maxSize: '300px', +} diff --git a/frontend/src/atom/ImgPreview/index.js b/frontend/src/organism/ImgPreview/index.js similarity index 100% rename from frontend/src/atom/ImgPreview/index.js rename to frontend/src/organism/ImgPreview/index.js From 85677c0ce664652de8c91b0991d2f0b92ce64ce2 Mon Sep 17 00:00:00 2001 From: jongwon Date: Sun, 13 Dec 2020 12:00:20 +0900 Subject: [PATCH 11/50] Refactor: add dynamic namespace MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 워크스페이스에 따라 socket 연결을 하기 위해 namespace routing 기능 추가 현재 접속중인 사용자를 특정할 수 있도록 workspaceUserInfoId로 room 생성 --- backend/chatServer.js | 9 +++++---- frontend/src/organism/ChatRoom/ChatRoom.js | 8 +++++++- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/backend/chatServer.js b/backend/chatServer.js index e717aa59..51dacdb2 100644 --- a/backend/chatServer.js +++ b/backend/chatServer.js @@ -10,19 +10,20 @@ const io = createChatServer(server, { cors: { origin: process.env.FRONTEND_HOST, credentials: true }, }) -const namespace = io.of('chat') +const namespace = io.of(/^\/chat\/\w+$/) namespace.use((socket, next) => { // TODO jwt 검증 로직 필요 next() }) namespace.on('connection', socket => { + const { workspaceUserInfoId } = socket.handshake.query + socket.join(workspaceUserInfoId) socket.on('new message', async data => { - // TODO 특정 채널로 전송하도록 변경, db에 저장 필요 (현재는 자신 제외 전체 전송) - const { channelId, creator } = socket.handshake.query + const { channelId } = socket.handshake.query const { contents } = data const { data: result } = await createChatMessage({ - creator, + creator: workspaceUserInfoId, channelId, contents, }) diff --git a/frontend/src/organism/ChatRoom/ChatRoom.js b/frontend/src/organism/ChatRoom/ChatRoom.js index 5b3c159c..5882d641 100644 --- a/frontend/src/organism/ChatRoom/ChatRoom.js +++ b/frontend/src/organism/ChatRoom/ChatRoom.js @@ -62,7 +62,13 @@ const ChatRoom = () => { useEffect(() => { if (workspaceUserInfo === null) return false setSocket( - io(baseURL, { query: { channelId, creator: workspaceUserInfo._id } }), + io(`${baseURL}/${workspaceId}`, { + query: { + workspaceId, + channelId, + workspaceUserInfoId: workspaceUserInfo._id, + }, + }), ) }, [workspaceId, channelId, workspaceUserInfo]) From 74b39ada4013a814aa5b9a251f9879c7d3c09b88 Mon Sep 17 00:00:00 2001 From: jongwon Date: Sun, 13 Dec 2020 14:41:30 +0900 Subject: [PATCH 12/50] Refactor: change to manage socket in global state --- backend/chatServer.js | 7 +++- frontend/src/hooks/useSocket.js | 33 ++++++++++++++++ frontend/src/organism/ChatRoom/ChatRoom.js | 39 +++++-------------- .../src/page/WorkspacePage/WorkspacePage.js | 6 ++- frontend/src/store.js | 10 ++++- 5 files changed, 59 insertions(+), 36 deletions(-) create mode 100644 frontend/src/hooks/useSocket.js diff --git a/backend/chatServer.js b/backend/chatServer.js index 51dacdb2..699f9e88 100644 --- a/backend/chatServer.js +++ b/backend/chatServer.js @@ -20,8 +20,7 @@ namespace.on('connection', socket => { const { workspaceUserInfoId } = socket.handshake.query socket.join(workspaceUserInfoId) socket.on('new message', async data => { - const { channelId } = socket.handshake.query - const { contents } = data + const { contents, channelId } = data const { data: result } = await createChatMessage({ creator: workspaceUserInfoId, channelId, @@ -35,6 +34,10 @@ namespace.on('connection', socket => { socket.join(roomId) console.log('joined', roomId) }) + socket.on('leave-room', roomId => { + socket.leave(roomId) + console.log('leaved', roomId) + }) }) server.listen(process.env.CHAT_PORT, () => { diff --git a/frontend/src/hooks/useSocket.js b/frontend/src/hooks/useSocket.js new file mode 100644 index 00000000..1f0b6a3f --- /dev/null +++ b/frontend/src/hooks/useSocket.js @@ -0,0 +1,33 @@ +import { useEffect } from 'react' +import { socketRecoil, workspaceRecoil } from '../store' +import { useParams } from 'react-router-dom' +import { useRecoilState, useRecoilValue } from 'recoil' +import io from 'socket.io-client' + +const baseURL = + process.env.NODE_ENV === 'development' + ? process.env.REACT_APP_DEV_CHAT_HOST + : process.env.REACT_APP_CHAT_HOST + +const useSocket = () => { + const { workspaceId } = useParams() + const workspaceUserInfo = useRecoilValue(workspaceRecoil) + const [socket, setSocket] = useRecoilState(socketRecoil) + + useEffect(() => { + if (workspaceId && workspaceUserInfo) { + setSocket( + io(`${baseURL}/${workspaceId}`, { + query: { + workspaceId, + workspaceUserInfoId: workspaceUserInfo._id, + }, + }), + ) + } + }, [workspaceId, workspaceUserInfo]) + + return [socket] +} + +export default useSocket diff --git a/frontend/src/organism/ChatRoom/ChatRoom.js b/frontend/src/organism/ChatRoom/ChatRoom.js index 5882d641..b6141e70 100644 --- a/frontend/src/organism/ChatRoom/ChatRoom.js +++ b/frontend/src/organism/ChatRoom/ChatRoom.js @@ -1,20 +1,14 @@ import React, { useEffect, useState, useRef, useCallback } from 'react' import styled from 'styled-components' import { useParams } from 'react-router-dom' -import io from 'socket.io-client' import { useRecoilValue } from 'recoil' import ChatMessage from '../ChatMessage' import { COLOR } from '../../constant/style' import { getChatMessage } from '../../api/chat' import MessageEditor from '../messageEditor/MessageEditor' -import { workspaceRecoil } from '../../store' +import { workspaceRecoil, socketRecoil } from '../../store' import ChannelHeader from '../ChannelHeader' -const baseURL = - process.env.NODE_ENV === 'development' - ? process.env.REACT_APP_DEV_CHAT_HOST - : process.env.REACT_APP_CHAT_HOST - const ChatRoom = () => { const viewport = useRef(null) const target = useRef(null) @@ -22,7 +16,7 @@ const ChatRoom = () => { const [targetState, setTargetState] = useState() const workspaceUserInfo = useRecoilValue(workspaceRecoil) const { workspaceId, channelId } = useParams() - const [socket, setSocket] = useState(null) + const socket = useRecoilValue(socketRecoil) const [messages, setMessages] = useState([]) const load = useRef(false) @@ -50,6 +44,7 @@ const ChatRoom = () => { const sendMessage = message => { const chat = { contents: message, + channelId, userInfo: { _id: workspaceUserInfo._id, displayName: workspaceUserInfo.displayName, @@ -60,26 +55,14 @@ const ChatRoom = () => { } useEffect(() => { - if (workspaceUserInfo === null) return false - setSocket( - io(`${baseURL}/${workspaceId}`, { - query: { - workspaceId, - channelId, - workspaceUserInfoId: workspaceUserInfo._id, - }, - }), - ) - }, [workspaceId, channelId, workspaceUserInfo]) + if (socket && channelId) socket.emit('join-room', channelId) + return () => { + socket && socket.emit('leave-room', channelId) + } + }, [channelId, socket]) useEffect(() => { if (socket) { - socket.on('connect', () => { - console.log('connected') - }) - socket.on('disconnect', () => { - console.log('disconnected') - }) socket.emit('join-room', channelId) socket.on('new message', ({ message }) => { setMessages(messages => [...messages, message]) @@ -87,11 +70,7 @@ const ChatRoom = () => { }) } return () => { - if (socket) { - socket.off('connect') - socket.off('disconnect') - socket.off('new message') - } + socket && socket.off('new message') } }, [socket]) diff --git a/frontend/src/page/WorkspacePage/WorkspacePage.js b/frontend/src/page/WorkspacePage/WorkspacePage.js index 9c74928f..a77f8e0d 100644 --- a/frontend/src/page/WorkspacePage/WorkspacePage.js +++ b/frontend/src/page/WorkspacePage/WorkspacePage.js @@ -12,12 +12,14 @@ import { COLOR } from '../../constant/style' import Icon from '../../atom/Icon' import { TOOLS } from '../../constant/icon' import useWorkspace from '../../hooks/useWorkspace' +import useSocket from '../../hooks/useSocket' function WorkspacePage() { const { channelId } = useParams() const [lineWidth, setLineWidth] = useState(20) - const Modal = useRecoilValue(modalRecoil) + const modal = useRecoilValue(modalRecoil) useWorkspace() + useSocket() const moveLine = e => { if (e.pageX === 0) return false let mouse = e.pageX @@ -50,7 +52,7 @@ function WorkspacePage() { return ( - {Modal} + {modal} 글로벌 헤더 위치 diff --git a/frontend/src/store.js b/frontend/src/store.js index 69ec4b25..2b110764 100644 --- a/frontend/src/store.js +++ b/frontend/src/store.js @@ -11,11 +11,17 @@ export const currentChannelInfoRecoil = atom({ }) export const channelsRecoil = atom({ - key: 'Channels', + key: 'channels', default: {}, }) export const modalRecoil = atom({ - key: 'Modal', + key: 'modal', default: null, }) + +export const socketRecoil = atom({ + key: 'socket', + default: null, + dangerouslyAllowMutability: true, +}) From 2f1c5d21054d4f209fcb97a4f4c4c86b9bccd248 Mon Sep 17 00:00:00 2001 From: jongwon Date: Sun, 13 Dec 2020 15:22:55 +0900 Subject: [PATCH 13/50] Feat: updating channel list of invited users MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 초대받은 사용자의 채널 목록을 최신화시킬 수 있도록 기능 추가 --- backend/chatServer.js | 3 +++ frontend/src/organism/ChannelList/ChannelList.js | 13 +++++++++---- .../InviteUserToChannelModal.js | 10 +++++++--- 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/backend/chatServer.js b/backend/chatServer.js index 699f9e88..ff4182b2 100644 --- a/backend/chatServer.js +++ b/backend/chatServer.js @@ -19,6 +19,9 @@ namespace.use((socket, next) => { namespace.on('connection', socket => { const { workspaceUserInfoId } = socket.handshake.query socket.join(workspaceUserInfoId) + socket.on('invite channel', invitedMember => { + namespace.in(invitedMember).emit('invited channel') + }) socket.on('new message', async data => { const { contents, channelId } = data const { data: result } = await createChatMessage({ diff --git a/frontend/src/organism/ChannelList/ChannelList.js b/frontend/src/organism/ChannelList/ChannelList.js index b902e822..b01a37cf 100644 --- a/frontend/src/organism/ChannelList/ChannelList.js +++ b/frontend/src/organism/ChannelList/ChannelList.js @@ -4,20 +4,25 @@ import styled from 'styled-components' import { toast } from 'react-toastify' import SectionLabel from '../SectionLabel' import SideMenuList from '../SideMenuList' -import { useRecoilState } from 'recoil' +import { useRecoilState, useRecoilValue } from 'recoil' -import { workspaceRecoil } from '../../store' +import { workspaceRecoil, socketRecoil } from '../../store' import useChannelList from '../../hooks/useChannelList' function ChannelList(props) { const [list, setList] = useState([]) const [Channels, setChannels] = useChannelList() const [userInfo, setUserInfo] = useRecoilState(workspaceRecoil) - + const socket = useRecoilValue(socketRecoil) const history = useHistory() let sectionMap = new Map() - + useEffect(() => { + if (socket) + socket.on('invited channel', () => { + setChannels() + }) + }, [socket]) useEffect(() => { if (Channels === undefined) return if (Object.keys(Channels).length !== 0) { diff --git a/frontend/src/organism/InviteUserToChannelModal/InviteUserToChannelModal.js b/frontend/src/organism/InviteUserToChannelModal/InviteUserToChannelModal.js index b34293da..f7072a91 100644 --- a/frontend/src/organism/InviteUserToChannelModal/InviteUserToChannelModal.js +++ b/frontend/src/organism/InviteUserToChannelModal/InviteUserToChannelModal.js @@ -1,7 +1,7 @@ import React, { useState, useRef } from 'react' import styled from 'styled-components' -import { useSetRecoilState } from 'recoil' -import { modalRecoil } from '../../store' +import { useSetRecoilState, useRecoilValue } from 'recoil' +import { modalRecoil, socketRecoil } from '../../store' import { useParams } from 'react-router-dom' import Button from '../../atom/Button' import Icon from '../../atom/Icon' @@ -16,7 +16,7 @@ import useChannelInfo from '../../hooks/useChannelInfo' function InviteUserToChannelModal({ handleClose }) { const [channelInfo, updateChannelInfo] = useChannelInfo() const setModal = useSetRecoilState(modalRecoil) - + const socket = useRecoilValue(socketRecoil) const [searchResult, setSearchResult] = useState(null) const [inviteUserList, setInviteUserList] = useState([]) const { workspaceId } = useParams() @@ -40,6 +40,10 @@ function InviteUserToChannelModal({ handleClose }) { if (data.success) { updateChannelInfo(channelInfo.channelId._id) + socket.emit( + 'invite channel', + inviteUserList.map(user => user._id), + ) setModal(null) } } From 9c9d029bf452a11f23f71897c22fb72740a0869f Mon Sep 17 00:00:00 2001 From: jongwon Date: Sun, 13 Dec 2020 16:46:45 +0900 Subject: [PATCH 14/50] Refactor: change all channels join when enter workspace --- backend/chatServer.js | 6 +++--- frontend/src/hooks/useSocket.js | 18 +++++++++++++++++- frontend/src/organism/ChatRoom/ChatRoom.js | 19 +++++++++---------- frontend/src/store.js | 2 +- 4 files changed, 30 insertions(+), 15 deletions(-) diff --git a/backend/chatServer.js b/backend/chatServer.js index ff4182b2..e74910f1 100644 --- a/backend/chatServer.js +++ b/backend/chatServer.js @@ -33,9 +33,9 @@ namespace.on('connection', socket => { message: { ...data, _id: result._id, createdAt: result.createdAt }, }) }) - socket.on('join-room', roomId => { - socket.join(roomId) - console.log('joined', roomId) + socket.on('join-room', (channelList = []) => { + socket.join(channelList) + console.log('joined', channelList) }) socket.on('leave-room', roomId => { socket.leave(roomId) diff --git a/frontend/src/hooks/useSocket.js b/frontend/src/hooks/useSocket.js index 1f0b6a3f..4c3565f6 100644 --- a/frontend/src/hooks/useSocket.js +++ b/frontend/src/hooks/useSocket.js @@ -1,7 +1,8 @@ import { useEffect } from 'react' -import { socketRecoil, workspaceRecoil } from '../store' +import { socketRecoil, workspaceRecoil, channelsRecoil } from '../store' import { useParams } from 'react-router-dom' import { useRecoilState, useRecoilValue } from 'recoil' +import { isEmpty } from '../util' import io from 'socket.io-client' const baseURL = @@ -12,6 +13,7 @@ const baseURL = const useSocket = () => { const { workspaceId } = useParams() const workspaceUserInfo = useRecoilValue(workspaceRecoil) + const channelList = useRecoilValue(channelsRecoil) const [socket, setSocket] = useRecoilState(socketRecoil) useEffect(() => { @@ -27,6 +29,20 @@ const useSocket = () => { } }, [workspaceId, workspaceUserInfo]) + useEffect(() => { + if (socket && !isEmpty(channelList)) + socket.emit( + 'join-room', + channelList.map(channel => channel.channelId._id), + ) + return () => { + if (socket) + socket.emit( + 'leave-room', + channelList.map(channel => channel.channelId._id), + ) + } + }, [socket, channelList]) return [socket] } diff --git a/frontend/src/organism/ChatRoom/ChatRoom.js b/frontend/src/organism/ChatRoom/ChatRoom.js index b6141e70..c2f73bb0 100644 --- a/frontend/src/organism/ChatRoom/ChatRoom.js +++ b/frontend/src/organism/ChatRoom/ChatRoom.js @@ -6,7 +6,11 @@ import ChatMessage from '../ChatMessage' import { COLOR } from '../../constant/style' import { getChatMessage } from '../../api/chat' import MessageEditor from '../messageEditor/MessageEditor' -import { workspaceRecoil, socketRecoil } from '../../store' +import { + workspaceRecoil, + socketRecoil, + currentChannelInfoRecoil, +} from '../../store' import ChannelHeader from '../ChannelHeader' const ChatRoom = () => { @@ -15,6 +19,7 @@ const ChatRoom = () => { const messageEndRef = useRef(null) const [targetState, setTargetState] = useState() const workspaceUserInfo = useRecoilValue(workspaceRecoil) + const channelInfo = useRecoilValue(currentChannelInfoRecoil) const { workspaceId, channelId } = useParams() const socket = useRecoilValue(socketRecoil) const [messages, setMessages] = useState([]) @@ -45,6 +50,7 @@ const ChatRoom = () => { const chat = { contents: message, channelId, + member: channelInfo.member, userInfo: { _id: workspaceUserInfo._id, displayName: workspaceUserInfo.displayName, @@ -54,18 +60,11 @@ const ChatRoom = () => { socket.emit('new message', chat) } - useEffect(() => { - if (socket && channelId) socket.emit('join-room', channelId) - return () => { - socket && socket.emit('leave-room', channelId) - } - }, [channelId, socket]) - useEffect(() => { if (socket) { - socket.emit('join-room', channelId) socket.on('new message', ({ message }) => { - setMessages(messages => [...messages, message]) + if (message.channelId === channelId) + setMessages(messages => [...messages, message]) if (message.userInfo._id === workspaceUserInfo._id) scrollTo() }) } diff --git a/frontend/src/store.js b/frontend/src/store.js index 2b110764..4d2c3aca 100644 --- a/frontend/src/store.js +++ b/frontend/src/store.js @@ -12,7 +12,7 @@ export const currentChannelInfoRecoil = atom({ export const channelsRecoil = atom({ key: 'channels', - default: {}, + default: [], }) export const modalRecoil = atom({ From 5e95697fbd619cc5520390d394bbd0aaa67684ba Mon Sep 17 00:00:00 2001 From: jongwon Date: Sun, 13 Dec 2020 17:20:04 +0900 Subject: [PATCH 15/50] Fix: messages were sent to previous channels --- frontend/src/organism/ChatRoom/ChatRoom.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/organism/ChatRoom/ChatRoom.js b/frontend/src/organism/ChatRoom/ChatRoom.js index c2f73bb0..5d01b169 100644 --- a/frontend/src/organism/ChatRoom/ChatRoom.js +++ b/frontend/src/organism/ChatRoom/ChatRoom.js @@ -71,7 +71,7 @@ const ChatRoom = () => { return () => { socket && socket.off('new message') } - }, [socket]) + }, [socket, channelId]) useEffect(() => { const option = { From a3ebb30b356afaa953b645f987b5bb69e6dfa4b0 Mon Sep 17 00:00:00 2001 From: rockpell Date: Sun, 13 Dec 2020 18:20:34 +0900 Subject: [PATCH 16/50] Fix: deleteFile api MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit req.body를 req.query로 변경 --- backend/controller/file/file.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/controller/file/file.js b/backend/controller/file/file.js index 04f6a615..0d66ab9f 100644 --- a/backend/controller/file/file.js +++ b/backend/controller/file/file.js @@ -26,7 +26,7 @@ exports.uploadFile = asyncWrapper(async (req, res) => { exports.deleteFile = asyncWrapper(async (req, res) => { const { code, success } = await service.deleteFile({ - ...req.body, + ...req.query, }) return res.status(code).json({ success }) }) From f772424416f80dcf3e72e9fa81aa20261291c7fe Mon Sep 17 00:00:00 2001 From: rockpell Date: Sun, 13 Dec 2020 18:21:15 +0900 Subject: [PATCH 17/50] Feat: SmallButton --- .../atom/Button/SmallButton/SmallButton.js | 61 +++++++++++++++++++ .../Button/SmallButton/SmallButton.stories.js | 23 +++++++ frontend/src/atom/Button/SmallButton/index.js | 1 + 3 files changed, 85 insertions(+) create mode 100644 frontend/src/atom/Button/SmallButton/SmallButton.js create mode 100644 frontend/src/atom/Button/SmallButton/SmallButton.stories.js create mode 100644 frontend/src/atom/Button/SmallButton/index.js diff --git a/frontend/src/atom/Button/SmallButton/SmallButton.js b/frontend/src/atom/Button/SmallButton/SmallButton.js new file mode 100644 index 00000000..34edf1bb --- /dev/null +++ b/frontend/src/atom/Button/SmallButton/SmallButton.js @@ -0,0 +1,61 @@ +import React from 'react' +import styled from 'styled-components' +import { COLOR } from '../../../constant/style' + +const SmallButton = ({ + handleClick, + children, + type = 'default', + disabled = false, +}) => { + return ( + + {children} + + ) +} + +const StyledButton = styled.button` + font-size: 8px; + font-weight: 900; + border-style: none; + border-radius: 4px; + outline: none; + color: ${({ type, disabled }) => { + if (disabled) return COLOR.GRAY + if (type === 'transparent') return COLOR.GRAY + return COLOR.WHITE + }}; + cursor: ${({ disabled }) => (disabled ? 'default' : 'pointer')}; + background-color: ${({ type, disabled }) => { + if (disabled) return COLOR.LIGHT_GRAY + if (type === 'transparent') return 'transparent' + if (type === 'icon') return 'transparent' + return COLOR.GREEN + }}; + border: ${({ type }) => { + if (type === 'transparent') return `1px solid ${COLOR.TRANSPARENT_GRAY}` + if (type === 'icon') return 'transparent' + }}; + &:hover { + ${({ type, disabled }) => { + if (disabled) return + if (type === 'transparent') + return `background: ${COLOR.HOVER_GRAY}; box-shadow: 0 1px 4px rgba(0,0,0,0.3);` + if (type === 'default') + return `background: ${COLOR.HOVER_GREEN}; box-shadow: 0 1px 4px rgba(0,0,0,0.3);` + if (type === 'icon') + return `background: ${COLOR.HOVER_GRAY}; + box-shadow:0 1px 3px 0 rgba(0,0,0, 0.08);` + }} + } + ${({ type }) => { + if (type === 'icon') + return ` + &:hover i svg { + color: ${COLOR.ICON_HOVER}; + } + ` + }} +` +export default SmallButton diff --git a/frontend/src/atom/Button/SmallButton/SmallButton.stories.js b/frontend/src/atom/Button/SmallButton/SmallButton.stories.js new file mode 100644 index 00000000..f365966a --- /dev/null +++ b/frontend/src/atom/Button/SmallButton/SmallButton.stories.js @@ -0,0 +1,23 @@ +import React from 'react' +import SmallButton from './SmallButton' +import Icon from '../../Icon' +import { CLOSE } from '../../../constant/icon' +import { COLOR } from '../../../constant/style' + +export default { + title: 'Atom/SmallButton', + component: SmallButton, +} + +const Template = args => + +export const Default = Template.bind({}) +Default.args = { + children: 'Default', +} + +export const IconButton = Template.bind({}) +IconButton.args = { + children: , + type: 'icon', +} diff --git a/frontend/src/atom/Button/SmallButton/index.js b/frontend/src/atom/Button/SmallButton/index.js new file mode 100644 index 00000000..5f4d6c68 --- /dev/null +++ b/frontend/src/atom/Button/SmallButton/index.js @@ -0,0 +1 @@ +export { default } from './SmallButton' From addff823738d5f72494a3ff3f8e228fae0da01a0 Mon Sep 17 00:00:00 2001 From: rockpell Date: Sun, 13 Dec 2020 18:22:00 +0900 Subject: [PATCH 18/50] Feat: add Delete to ImgPreview MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 이미지 제거 기능 추가 --- .../src/organism/ImgPreview/ImgPreview.js | 51 ++++++++++--------- .../organism/ImgPreview/ImgPreview.stories.js | 13 ++--- 2 files changed, 33 insertions(+), 31 deletions(-) diff --git a/frontend/src/organism/ImgPreview/ImgPreview.js b/frontend/src/organism/ImgPreview/ImgPreview.js index 8b90fd7b..b9a56c93 100644 --- a/frontend/src/organism/ImgPreview/ImgPreview.js +++ b/frontend/src/organism/ImgPreview/ImgPreview.js @@ -1,19 +1,19 @@ import React, { useState, useEffect } from 'react' import styled from 'styled-components' import request from '../../util/request' -import Button from '../../atom/Button' import Icon from '../../atom/Icon' import { CLOSE } from '../../constant/icon' import { COLOR } from '../../constant/style' +import SmallButton from '../../atom/Button/SmallButton' -function ImgPreview({ type, fileId, maxSize }) { +function ImgPreview({ type, fileId, setIsRender }) { const [fileData, setFileData] = useState({}) const [isHover, setIsHover] = useState(false) useEffect(() => { ;(async () => { - const { data } = await request.GET('/api/file', { fileId }) - setFileData(data.data) + const { data } = (await request.GET('/api/file', { fileId })) || {} + setFileData(data?.data) })() }, [fileId]) @@ -25,19 +25,17 @@ function ImgPreview({ type, fileId, maxSize }) { setIsHover(false) } - const handleClose = () => { - console.log('handleClose') + const handleDelete = async () => { + setIsRender(false) + await request.DELETE('/api/file', { fileId }) } const deleteButton = () => { return ( - - + + + + ) } @@ -46,10 +44,8 @@ function ImgPreview({ type, fileId, maxSize }) { return ( { - console.log('onClick!!!') + if (fileData) window.open(fileData.url, '_blank') }} - onMouseEnter={enterMouseHandle} - onMouseLeave={leaveMouseHandle} > Click to Download @@ -57,13 +53,11 @@ function ImgPreview({ type, fileId, maxSize }) { } return ( - + {isHover && (type === 'input' @@ -81,17 +75,23 @@ const StyledDiv = styled.div` ` const StyledImg = styled.img` - max-width: ${({ maxSize }) => maxSize}; + max-width: ${({ type }) => { + return type === 'input' ? '50px' : '300px' + }}; height: auto; border-radius: 2%; background: white; ` const ButtonDiv = styled.div` + display: inline-block; position: absolute; - right: 5px; + right: 0; top: 0; - background: ${COLOR.LIGHT_GRAY}; + background: white; + width: -webkit-fit-content; + height: -webkit-fit-content; + box-sizing: border-box; ` const DownloadDiv = styled.div` @@ -99,6 +99,7 @@ const DownloadDiv = styled.div` top: 0; width: 100%; height: 100%; + cursor: pointer; ` const ClickToDownloadSpan = styled.span` diff --git a/frontend/src/organism/ImgPreview/ImgPreview.stories.js b/frontend/src/organism/ImgPreview/ImgPreview.stories.js index 9c81570c..9e5ea405 100644 --- a/frontend/src/organism/ImgPreview/ImgPreview.stories.js +++ b/frontend/src/organism/ImgPreview/ImgPreview.stories.js @@ -5,19 +5,20 @@ export default { title: 'Organism/ImgPreview', component: ImgPreview, } - -const Template = args => +let isRender = true +const Template = args => <>{isRender && } export const inputImgPreview = Template.bind({}) inputImgPreview.args = { type: 'input', // input, message - fileId: '5fd1ed979f167053f728e7ce', - maxSize: '300px', + fileId: '5fd5dc7d8c8a82245fa0ab38', + setIsRender: () => { + isRender = false + }, } export const messageImgPreview = Template.bind({}) messageImgPreview.args = { type: 'message', // input, message - fileId: '5fd1ed979f167053f728e7ce', - maxSize: '300px', + fileId: '5fd5dc7d8c8a82245fa0ab38', } From b20f026fe098bbc195aa9148d3aa0f84d4625c04 Mon Sep 17 00:00:00 2001 From: rockpell Date: Sun, 13 Dec 2020 18:22:49 +0900 Subject: [PATCH 19/50] Fix: Button MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 중복 css 제거 --- frontend/src/atom/Button/Button.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/frontend/src/atom/Button/Button.js b/frontend/src/atom/Button/Button.js index beb629b3..9d493591 100644 --- a/frontend/src/atom/Button/Button.js +++ b/frontend/src/atom/Button/Button.js @@ -16,6 +16,7 @@ const Button = ({ const StyledButton = styled.button` font-size: 15px; + font-weight: 900; height: 36px; padding: 0 12px 1px; border-style: none; @@ -37,8 +38,6 @@ const StyledButton = styled.button` if (type === 'transparent') return `1px solid ${COLOR.TRANSPARENT_GRAY}` if (type === 'icon') return 'transparent' }}; - font-size: 15px; - font-weight: 900; &:hover { ${({ type, disabled }) => { if (disabled) return From e562c149aa6b24cac4222b2767b2c738ff97651f Mon Sep 17 00:00:00 2001 From: jongwon Date: Sun, 13 Dec 2020 19:16:34 +0900 Subject: [PATCH 20/50] Feat: add real time update channel info --- backend/chatServer.js | 6 ++++-- .../organism/ChannelHeader/ChannelHeader.js | 21 ++++++++++++++----- frontend/src/organism/ChatRoom/ChatRoom.js | 1 - .../InviteUserToChannelModal.js | 12 ++++++----- 4 files changed, 27 insertions(+), 13 deletions(-) diff --git a/backend/chatServer.js b/backend/chatServer.js index e74910f1..eedd8997 100644 --- a/backend/chatServer.js +++ b/backend/chatServer.js @@ -19,8 +19,10 @@ namespace.use((socket, next) => { namespace.on('connection', socket => { const { workspaceUserInfoId } = socket.handshake.query socket.join(workspaceUserInfoId) - socket.on('invite channel', invitedMember => { - namespace.in(invitedMember).emit('invited channel') + socket.on('invite channel', ({ channelId, members }) => { + members.forEach(member => + namespace.in(member).emit('invited channel', channelId), + ) }) socket.on('new message', async data => { const { contents, channelId } = data diff --git a/frontend/src/organism/ChannelHeader/ChannelHeader.js b/frontend/src/organism/ChannelHeader/ChannelHeader.js index 0ffdbd5b..6279ed26 100644 --- a/frontend/src/organism/ChannelHeader/ChannelHeader.js +++ b/frontend/src/organism/ChannelHeader/ChannelHeader.js @@ -1,6 +1,6 @@ -import React from 'react' +import React, { useEffect } from 'react' import styled from 'styled-components' -import { useSetRecoilState } from 'recoil' +import { useSetRecoilState, useRecoilValue } from 'recoil' import Icon from '../../atom/Icon' import { ADDUSER, INFOCIRCLE } from '../../constant/icon' @@ -9,7 +9,7 @@ import ChannelStarBtn from '../../atom/ChannelStarBtn' import ChannelPinBtn from '../../atom/ChannelPinBtn' import ChannelTopicBtn from '../../atom/ChannelTopicBtn' import ChannelMemberThumbnail from '../../atom/ChannelMemberThumbnail' -import { modalRecoil } from '../../store' +import { modalRecoil, socketRecoil } from '../../store' import InviteUserToChannelModal from '../InviteUserToChannelModal' import { COLOR } from '../../constant/style' import useChannelInfo from '../../hooks/useChannelInfo' @@ -17,11 +17,22 @@ import { isEmpty } from '../../util' function ChannelHeader() { const setModal = useSetRecoilState(modalRecoil) - const [channelInfo] = useChannelInfo() + const [channelInfo, updateChannelInfo] = useChannelInfo() + const socket = useRecoilValue(socketRecoil) const openAddUserModal = () => { setModal( setModal(null)} />) } - + useEffect(() => { + if (socket) { + socket.on('invited channel', channelId => { + if (channelId === channelInfo.channelId._id) + updateChannelInfo(channelId) + }) + } + return () => { + socket && socket.off('invited channel') + } + }, [socket, channelInfo]) return isEmpty(channelInfo) ? null : ( diff --git a/frontend/src/organism/ChatRoom/ChatRoom.js b/frontend/src/organism/ChatRoom/ChatRoom.js index 5d01b169..0327e853 100644 --- a/frontend/src/organism/ChatRoom/ChatRoom.js +++ b/frontend/src/organism/ChatRoom/ChatRoom.js @@ -50,7 +50,6 @@ const ChatRoom = () => { const chat = { contents: message, channelId, - member: channelInfo.member, userInfo: { _id: workspaceUserInfo._id, displayName: workspaceUserInfo.displayName, diff --git a/frontend/src/organism/InviteUserToChannelModal/InviteUserToChannelModal.js b/frontend/src/organism/InviteUserToChannelModal/InviteUserToChannelModal.js index f7072a91..e4af8270 100644 --- a/frontend/src/organism/InviteUserToChannelModal/InviteUserToChannelModal.js +++ b/frontend/src/organism/InviteUserToChannelModal/InviteUserToChannelModal.js @@ -39,11 +39,13 @@ function InviteUserToChannelModal({ handleClose }) { }) if (data.success) { - updateChannelInfo(channelInfo.channelId._id) - socket.emit( - 'invite channel', - inviteUserList.map(user => user._id), - ) + socket.emit('invite channel', { + channelId: channelInfo.channelId._id, + members: [ + ...channelInfo.member.map(user => user._id), + ...inviteUserList.map(user => user._id), + ], + }) setModal(null) } } From 67eb50a1d5f7e6be3e188e67e473ab5ab329a1fa Mon Sep 17 00:00:00 2001 From: rockpell Date: Sun, 13 Dec 2020 19:18:15 +0900 Subject: [PATCH 21/50] Chore: file icon --- frontend/src/constant/icon.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frontend/src/constant/icon.js b/frontend/src/constant/icon.js index 8c31df19..f09ade96 100644 --- a/frontend/src/constant/icon.js +++ b/frontend/src/constant/icon.js @@ -15,6 +15,7 @@ import { faCheck, faPlusSquare, faShare, + faFile, } from '@fortawesome/free-solid-svg-icons' import { faStar, @@ -49,3 +50,4 @@ export const CHECK = faCheck export const PLUSSQURE = faPlusSquare export const SMILE = faSmile export const SHARE = faShare +export const FILE = faFile From a5fbb9c00ee0efc05995556aa3b3227341df660d Mon Sep 17 00:00:00 2001 From: rockpell Date: Sun, 13 Dec 2020 19:18:49 +0900 Subject: [PATCH 22/50] Feat: FilePreview --- .../src/organism/FilePreview/FilePreview.js | 116 ++++++++++++++++++ .../FilePreview/FilePreview.stories.js | 24 ++++ frontend/src/organism/FilePreview/index.js | 1 + 3 files changed, 141 insertions(+) create mode 100644 frontend/src/organism/FilePreview/FilePreview.js create mode 100644 frontend/src/organism/FilePreview/FilePreview.stories.js create mode 100644 frontend/src/organism/FilePreview/index.js diff --git a/frontend/src/organism/FilePreview/FilePreview.js b/frontend/src/organism/FilePreview/FilePreview.js new file mode 100644 index 00000000..8c5b84bd --- /dev/null +++ b/frontend/src/organism/FilePreview/FilePreview.js @@ -0,0 +1,116 @@ +import React, { useState, useEffect } from 'react' +import styled from 'styled-components' +import request from '../../util/request' +import Icon from '../../atom/Icon' +import { CLOSE, FILE } from '../../constant/icon' +import { COLOR } from '../../constant/style' +import SmallButton from '../../atom/Button/SmallButton' + +function FilePreview({ type, fileId, setIsRender }) { + const [fileData, setFileData] = useState({}) + const [isHover, setIsHover] = useState(false) + + useEffect(() => { + ;(async () => { + const { data } = (await request.GET('/api/file', { fileId })) || {} + setFileData(data?.data) + })() + }, [fileId]) + + const enterMouseHandle = () => { + setIsHover(true) + } + + const leaveMouseHandle = () => { + setIsHover(false) + } + + const handleDelete = async () => { + setIsRender(false) + await request.DELETE('/api/file', { fileId }) + } + + const deleteButton = () => { + return ( + + + + + + ) + } + + const downloadButton = () => { + return ( + { + if (fileData) window.open(fileData.url, '_blank') + }} + > + Click to Download + + ) + } + return ( + + + + + {fileData?.originalName} + + {isHover && + (type === 'input' + ? deleteButton() + : type === 'message' + ? downloadButton() + : null)} + + + ) +} + +const StyledDiv = styled.div` + display: inline-block; + position: relative; + border: 1px solid ${COLOR.LIGHT_GRAY}; + border-radius: 4px; + min-width: 200px; +` + +const FlexDiv = styled.div` + display: flex; + justify-content: left; + padding: 5px 5px 5px 10px; +` + +const ButtonDiv = styled.div` + display: inline-block; + position: absolute; + right: 0; + top: 0; + background: white; + width: -webkit-fit-content; + height: -webkit-fit-content; + box-sizing: border-box; +` + +const DownloadDiv = styled.div` + position: absolute; + top: 0; + width: 100%; + height: 100%; + cursor: pointer; +` + +const ClickToDownloadSpan = styled.span` + position: absolute; + bottom: 0px; + font-size: 12px; + color: ${COLOR.GRAY}; +` + +const DescriptionDiv = styled.div` + padding: 10px 15px 10px 20px; +` + +export default FilePreview diff --git a/frontend/src/organism/FilePreview/FilePreview.stories.js b/frontend/src/organism/FilePreview/FilePreview.stories.js new file mode 100644 index 00000000..40138ef6 --- /dev/null +++ b/frontend/src/organism/FilePreview/FilePreview.stories.js @@ -0,0 +1,24 @@ +import React from 'react' +import FilePreview from './FilePreview' + +export default { + title: 'Organism/FilePreview', + component: FilePreview, +} +let isRender = true +const Template = args => <>{isRender && } + +export const inputFilePreview = Template.bind({}) +inputFilePreview.args = { + type: 'input', // input, message + fileId: '5fd5e4018c8a82245fa0ab39', + setIsRender: () => { + isRender = false + }, +} + +export const messageFilePreview = Template.bind({}) +messageFilePreview.args = { + type: 'message', // input, message + fileId: '5fd5e4018c8a82245fa0ab39', +} diff --git a/frontend/src/organism/FilePreview/index.js b/frontend/src/organism/FilePreview/index.js new file mode 100644 index 00000000..008b6f38 --- /dev/null +++ b/frontend/src/organism/FilePreview/index.js @@ -0,0 +1 @@ +export { default } from './FilePreview' From 5603658390eb67f70e2ed38c6318bd13c086d28a Mon Sep 17 00:00:00 2001 From: jongwon Date: Sun, 13 Dec 2020 20:07:40 +0900 Subject: [PATCH 23/50] Refactor: change to custom hook --- backend/chatServer.js | 12 ++--- frontend/src/hooks/useSocket.js | 30 ++++++++++-- .../organism/ChannelHeader/ChannelHeader.js | 17 ++----- .../src/organism/ChannelList/ChannelList.js | 47 +++++++------------ .../InviteUserToChannelModal.js | 6 +-- 5 files changed, 56 insertions(+), 56 deletions(-) diff --git a/backend/chatServer.js b/backend/chatServer.js index eedd8997..7274100c 100644 --- a/backend/chatServer.js +++ b/backend/chatServer.js @@ -19,10 +19,12 @@ namespace.use((socket, next) => { namespace.on('connection', socket => { const { workspaceUserInfoId } = socket.handshake.query socket.join(workspaceUserInfoId) - socket.on('invite channel', ({ channelId, members }) => { - members.forEach(member => - namespace.in(member).emit('invited channel', channelId), - ) + socket.on('invite channel', ({ channelId, origin, newMember }) => { + origin + .concat(newMember) + .forEach(member => + namespace.in(member).emit('invited channel', { channelId, newMember }), + ) }) socket.on('new message', async data => { const { contents, channelId } = data @@ -37,11 +39,9 @@ namespace.on('connection', socket => { }) socket.on('join-room', (channelList = []) => { socket.join(channelList) - console.log('joined', channelList) }) socket.on('leave-room', roomId => { socket.leave(roomId) - console.log('leaved', roomId) }) }) diff --git a/frontend/src/hooks/useSocket.js b/frontend/src/hooks/useSocket.js index 4c3565f6..e59c2b77 100644 --- a/frontend/src/hooks/useSocket.js +++ b/frontend/src/hooks/useSocket.js @@ -1,9 +1,11 @@ import { useEffect } from 'react' -import { socketRecoil, workspaceRecoil, channelsRecoil } from '../store' +import { socketRecoil, workspaceRecoil } from '../store' import { useParams } from 'react-router-dom' import { useRecoilState, useRecoilValue } from 'recoil' import { isEmpty } from '../util' import io from 'socket.io-client' +import useChannelInfo from './useChannelInfo' +import useChannelList from './useChannelList' const baseURL = process.env.NODE_ENV === 'development' @@ -13,8 +15,9 @@ const baseURL = const useSocket = () => { const { workspaceId } = useParams() const workspaceUserInfo = useRecoilValue(workspaceRecoil) - const channelList = useRecoilValue(channelsRecoil) const [socket, setSocket] = useRecoilState(socketRecoil) + const [channelList, setChannels] = useChannelList() + const [channelInfo, updateChannelInfo] = useChannelInfo() useEffect(() => { if (workspaceId && workspaceUserInfo) { @@ -30,11 +33,18 @@ const useSocket = () => { }, [workspaceId, workspaceUserInfo]) useEffect(() => { - if (socket && !isEmpty(channelList)) + if (socket && !isEmpty(channelList)) { socket.emit( 'join-room', channelList.map(channel => channel.channelId._id), ) + socket.on('invited channel', ({ channelId, newMember }) => { + console.log('invited') + if (channelId === channelInfo.channelId._id) + updateChannelInfo(channelId) + if (newMember === workspaceUserInfo._id) setChannels() + }) + } return () => { if (socket) socket.emit( @@ -43,6 +53,20 @@ const useSocket = () => { ) } }, [socket, channelList]) + + useEffect(() => { + if (socket && !isEmpty(channelInfo) && !isEmpty(workspaceUserInfo)) { + socket.on('invited channel', ({ channelId, newMember }) => { + if (channelId === channelInfo.channelId._id) + updateChannelInfo(channelId) + if (newMember.includes(workspaceUserInfo._id)) setChannels() + }) + } + return () => { + if (socket) socket.off('invited channel') + } + }, [socket, channelInfo, workspaceUserInfo]) + return [socket] } diff --git a/frontend/src/organism/ChannelHeader/ChannelHeader.js b/frontend/src/organism/ChannelHeader/ChannelHeader.js index 6279ed26..7e0bfeef 100644 --- a/frontend/src/organism/ChannelHeader/ChannelHeader.js +++ b/frontend/src/organism/ChannelHeader/ChannelHeader.js @@ -9,7 +9,7 @@ import ChannelStarBtn from '../../atom/ChannelStarBtn' import ChannelPinBtn from '../../atom/ChannelPinBtn' import ChannelTopicBtn from '../../atom/ChannelTopicBtn' import ChannelMemberThumbnail from '../../atom/ChannelMemberThumbnail' -import { modalRecoil, socketRecoil } from '../../store' +import { modalRecoil } from '../../store' import InviteUserToChannelModal from '../InviteUserToChannelModal' import { COLOR } from '../../constant/style' import useChannelInfo from '../../hooks/useChannelInfo' @@ -17,22 +17,11 @@ import { isEmpty } from '../../util' function ChannelHeader() { const setModal = useSetRecoilState(modalRecoil) - const [channelInfo, updateChannelInfo] = useChannelInfo() - const socket = useRecoilValue(socketRecoil) + const [channelInfo] = useChannelInfo() const openAddUserModal = () => { setModal( setModal(null)} />) } - useEffect(() => { - if (socket) { - socket.on('invited channel', channelId => { - if (channelId === channelInfo.channelId._id) - updateChannelInfo(channelId) - }) - } - return () => { - socket && socket.off('invited channel') - } - }, [socket, channelInfo]) + return isEmpty(channelInfo) ? null : ( diff --git a/frontend/src/organism/ChannelList/ChannelList.js b/frontend/src/organism/ChannelList/ChannelList.js index b01a37cf..f0f2aa34 100644 --- a/frontend/src/organism/ChannelList/ChannelList.js +++ b/frontend/src/organism/ChannelList/ChannelList.js @@ -4,39 +4,33 @@ import styled from 'styled-components' import { toast } from 'react-toastify' import SectionLabel from '../SectionLabel' import SideMenuList from '../SideMenuList' -import { useRecoilState, useRecoilValue } from 'recoil' - -import { workspaceRecoil, socketRecoil } from '../../store' +import { useRecoilValue } from 'recoil' +import { isEmpty } from '../../util' +import { workspaceRecoil } from '../../store' import useChannelList from '../../hooks/useChannelList' -function ChannelList(props) { +function ChannelList() { const [list, setList] = useState([]) - const [Channels, setChannels] = useChannelList() - const [userInfo, setUserInfo] = useRecoilState(workspaceRecoil) - const socket = useRecoilValue(socketRecoil) + const [channels] = useChannelList() + const userInfo = useRecoilValue(workspaceRecoil) const history = useHistory() - let sectionMap = new Map() - useEffect(() => { - if (socket) - socket.on('invited channel', () => { - setChannels() - }) - }, [socket]) + const sectionMap = new Map() + useEffect(() => { - if (Channels === undefined) return - if (Object.keys(Channels).length !== 0) { + if (channels === undefined) return + if (!isEmpty(channels)) { if (userInfo.sections) - userInfo.sections.map((sectionName, idx) => { + userInfo.sections.map(sectionName => { sectionMap.set(sectionName, []) }) SectionOrganizing() } - }, [Channels]) + }, [channels]) const SectionOrganizing = () => { try { - Channels.map((channel, index) => { + channels.forEach(channel => { if (channel.sectionName == null) { if (channel.channelId.channelType === 2) { checkHasKeyAndSetKeyInMap(sectionMap, 'Direct messages', channel) @@ -56,12 +50,12 @@ function ChannelList(props) { } } - const renderChannelSectionList = list.map((section, index) => { + const renderChannelSectionList = list.map(([sectionName, lists], index) => { return ( ) }) @@ -75,13 +69,8 @@ function ChannelList(props) { } const checkHasKeyAndSetKeyInMap = (map, key, data) => { - if (map.has(key)) { - let value = map.get(key) - value.push(data) - map.set(key, value) - } else { - map.set(key, [data]) - } + if (map.has(key)) map.set(key, [...map.get(key)].concat(data)) + else map.set(key, [data]) } const checkHasDefaultChannel = map => { diff --git a/frontend/src/organism/InviteUserToChannelModal/InviteUserToChannelModal.js b/frontend/src/organism/InviteUserToChannelModal/InviteUserToChannelModal.js index e4af8270..7e91ddd2 100644 --- a/frontend/src/organism/InviteUserToChannelModal/InviteUserToChannelModal.js +++ b/frontend/src/organism/InviteUserToChannelModal/InviteUserToChannelModal.js @@ -41,10 +41,8 @@ function InviteUserToChannelModal({ handleClose }) { if (data.success) { socket.emit('invite channel', { channelId: channelInfo.channelId._id, - members: [ - ...channelInfo.member.map(user => user._id), - ...inviteUserList.map(user => user._id), - ], + origin: channelInfo.member.map(user => user._id), + newMember: inviteUserList.map(user => user._id), }) setModal(null) } From de6959e9c829eefb2cd495a15cc9ad236d73975b Mon Sep 17 00:00:00 2001 From: Ryou_Changyu Date: Sun, 13 Dec 2020 20:25:51 +0900 Subject: [PATCH 24/50] Refactor: update reaction update service MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit db reaction 상태를 update하는 service의 로직을 refactoring --- backend/service/reaction.js | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/backend/service/reaction.js b/backend/service/reaction.js index 503bce3c..67602d39 100644 --- a/backend/service/reaction.js +++ b/backend/service/reaction.js @@ -1,20 +1,21 @@ import { Reaction } from '../model/Reaction' import { verifyRequiredParams, dbErrorHandler } from '../util' -const addReaction = async ({ chatId, workspaceUserInfoId, emoticon }) => { +const updateReaction = async ({ chatId, workspaceUserInfoId, emoticon }) => { verifyRequiredParams(chatId, workspaceUserInfoId, emoticon) - const result = await dbErrorHandler(() => - Reaction.create({ chatId, workspaceUserInfoId, emoticon }), + const isExist = await dbErrorHandler(() => + Reaction.find({ chatId, workspaceUserInfoId, emoticon }), ) - return result + if (isExist.length === 0) { + await dbErrorHandler(() => + Reaction.create({ chatId, workspaceUserInfoId, emoticon }), + ) + } else { + await dbErrorHandler(() => + Reaction.deleteOne({ chatId, workspaceUserInfoId, emoticon }), + ) + } + return isExist.length === 0 } -const removeReaction = async ({ chatId, workspaceUserInfoId, emoticon }) => { - verifyRequiredParams(chatId, workspaceUserInfoId, emoticon) - const result = await dbErrorHandler(() => - Reaction.deleteOne({ chatId, workspaceUserInfoId, emoticon }), - ) - return result -} - -module.exports = { addReaction, removeReaction } +module.exports = { updateReaction } From acf3da16395a8d84034c2da2c4e05b3ddbbdbabd Mon Sep 17 00:00:00 2001 From: Ryou_Changyu Date: Sun, 13 Dec 2020 20:27:35 +0900 Subject: [PATCH 25/50] Feat: add Backend socket reaction update MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 백엔드에서 소켓으로 전달받은 데이터를 이용해 리액션 상태 업데이트 기능 추가 --- backend/chatServer.js | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/backend/chatServer.js b/backend/chatServer.js index e74910f1..9807df16 100644 --- a/backend/chatServer.js +++ b/backend/chatServer.js @@ -3,6 +3,7 @@ import express from 'express' import { createServer } from 'http' import createChatServer from 'socket.io' import { createChatMessage } from './service/chat' +import { updateReaction } from './service/reaction' dotenv() const server = createServer(express()) @@ -33,6 +34,26 @@ namespace.on('connection', socket => { message: { ...data, _id: result._id, createdAt: result.createdAt }, }) }) + socket.on('update reaction', async data => { + const { emoji, chatId, userInfo, channelId } = data + //1 = add, 0 = remove + const result = await updateReaction({ + workspaceUserInfoId, + chatId, + emoticon: emoji, + }) + + namespace.in(channelId).emit('update reaction', { + reaction: { + chatId: chatId, + emoji: emoji, + workspaceUserInfoId: userInfo._id, + displayName: userInfo.displayName, + type: result ? 1 : 0, + }, + }) + }) + socket.on('join-room', (channelList = []) => { socket.join(channelList) console.log('joined', channelList) From 2f5e0d6554bd9ab01036f60b767a55f8425a7640 Mon Sep 17 00:00:00 2001 From: Ryou_Changyu Date: Sun, 13 Dec 2020 20:27:56 +0900 Subject: [PATCH 26/50] Refactor: remove dead code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 불필요한 코드 제거 --- backend/service/chat.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/backend/service/chat.js b/backend/service/chat.js index 7e3403d5..67e8791f 100644 --- a/backend/service/chat.js +++ b/backend/service/chat.js @@ -22,12 +22,10 @@ const createChatMessage = async ({ channelId, creator, contents }) => { } const getReplyMessage = async ({ channelId, parentId }) => { - console.log('channelId, parentId', channelId, parentId) verifyRequiredParams(channelId, parentId) const result = await dbErrorHandler(() => Chat.getReplyMessages({ channelId, parentId }), ) - console.log('result', result) return { code: statusCode.OK, data: result, success: true } } From 8dd313e9e98e324d13c22df58872a65793bebf3b Mon Sep 17 00:00:00 2001 From: Ryou_Changyu Date: Sun, 13 Dec 2020 20:29:00 +0900 Subject: [PATCH 27/50] Feat: add updateReaction util MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 프론트에서 소켓에 리액션 업데이트 정보를 담아 전달하는 유틸 구현 --- frontend/src/util/updateReaction.js | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 frontend/src/util/updateReaction.js diff --git a/frontend/src/util/updateReaction.js b/frontend/src/util/updateReaction.js new file mode 100644 index 00000000..8b1f16ed --- /dev/null +++ b/frontend/src/util/updateReaction.js @@ -0,0 +1,20 @@ +function updateReaction({ + workspaceUserInfo, + socket, + emoji, + chatId, + channelId, +}) { + const reaction = { + emoji: emoji, + chatId: chatId, + channelId: channelId, + userInfo: { + _id: workspaceUserInfo._id, + displayName: workspaceUserInfo.displayName, + }, + } + socket.emit('update reaction', reaction) +} + +export default updateReaction From 694178b236e3290f7cd5957b4ba2cb78759a9ba6 Mon Sep 17 00:00:00 2001 From: Ryou_Changyu Date: Sun, 13 Dec 2020 20:30:47 +0900 Subject: [PATCH 28/50] Refactor: update props data MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit props로 전달해주는 데이터에 chatId 추가 --- frontend/src/organism/ChatMessage/ChatMessage.js | 2 +- .../src/organism/ThreadReactionList/ThreadReactionList.js | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/src/organism/ChatMessage/ChatMessage.js b/frontend/src/organism/ChatMessage/ChatMessage.js index de058017..5299d778 100644 --- a/frontend/src/organism/ChatMessage/ChatMessage.js +++ b/frontend/src/organism/ChatMessage/ChatMessage.js @@ -30,7 +30,7 @@ const ChatMessage = forwardRef( {/* TODO thread Reaction 구현 */} {reactions && reactions.length !== 0 && ( - + )} {/* TODO view thread reply 구현 */} diff --git a/frontend/src/organism/ThreadReactionList/ThreadReactionList.js b/frontend/src/organism/ThreadReactionList/ThreadReactionList.js index 03aa6bf5..ab5bd40c 100644 --- a/frontend/src/organism/ThreadReactionList/ThreadReactionList.js +++ b/frontend/src/organism/ThreadReactionList/ThreadReactionList.js @@ -3,15 +3,15 @@ import styled, { css } from 'styled-components' import ThreadReactionCard from '../../atom/ThreadReactionCard' import AddReactionButton from '../../atom/AddReactionButton' -function ThreadReactionList({ reactions }) { +function ThreadReactionList({ reactions, chatId }) { const renderReactionCard = reactions.map((reaction, idx) => { - return + return }) return ( {renderReactionCard} - + ) } From 9342e06a7c9f36884a67ab9092dd97921038b8b3 Mon Sep 17 00:00:00 2001 From: Ryou_Changyu Date: Sun, 13 Dec 2020 20:33:56 +0900 Subject: [PATCH 29/50] Feat: add Frontend socket update reaction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 프론트에서 소켓을 통해 리액션 업데이트 정보를 받아 message의 state를 변경하는 기능 구현 --- frontend/src/organism/ChatRoom/ChatRoom.js | 47 +++++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/frontend/src/organism/ChatRoom/ChatRoom.js b/frontend/src/organism/ChatRoom/ChatRoom.js index 5d01b169..7bc20687 100644 --- a/frontend/src/organism/ChatRoom/ChatRoom.js +++ b/frontend/src/organism/ChatRoom/ChatRoom.js @@ -60,6 +60,48 @@ const ChatRoom = () => { socket.emit('new message', chat) } + const chageReactionState = (messages, reaction) => { + let done = false + const arr = messages.map((message, idx) => { + if (message._id === reaction.chatId) { + message.reactions && + message.reactions.map((item, idx) => { + if (item.emoji === reaction.emoji) { + if (reaction.type) { + const userInfo = [ + { + _id: reaction.workspaceUserInfoId, + displayName: reaction.displayName, + }, + ] + item.users = [...item.users, ...userInfo] + } else { + item.users.map((user, idx) => { + if (user._id === reaction.workspaceUserInfoId) { + item.users.splice(idx, 1) + } + }) + } + done = true + } + }) + if (!done && reaction.type === 1) { + message.reactions.push({ + emoji: reaction.emoji, + users: [ + { + _id: reaction.workspaceUserInfoId, + displayName: reaction.displayName, + }, + ], + }) + } + } + return message + }) + return [...arr] + } + useEffect(() => { if (socket) { socket.on('new message', ({ message }) => { @@ -67,9 +109,12 @@ const ChatRoom = () => { setMessages(messages => [...messages, message]) if (message.userInfo._id === workspaceUserInfo._id) scrollTo() }) + socket.on('update reaction', ({ reaction }) => { + setMessages(messages => chageReactionState(messages, reaction)) + }) } return () => { - socket && socket.off('new message') + socket && socket.off('new message') && socket.off('update reaction') } }, [socket, channelId]) From 7365559ad232f1ff9c65d14712074da9dc88e79c Mon Sep 17 00:00:00 2001 From: Ryou_Changyu Date: Sun, 13 Dec 2020 20:34:52 +0900 Subject: [PATCH 30/50] Feat: add Reaction update event on ActionBar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ActionBar에서 리액션을 업데이트하는 이벤트 추가 --- frontend/src/organism/ActionBar/ActionBar.js | 29 ++++++++++++++++---- 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/frontend/src/organism/ActionBar/ActionBar.js b/frontend/src/organism/ActionBar/ActionBar.js index 1132bd1c..cad7b54a 100644 --- a/frontend/src/organism/ActionBar/ActionBar.js +++ b/frontend/src/organism/ActionBar/ActionBar.js @@ -1,5 +1,6 @@ import React, { useState } from 'react' import styled, { css } from 'styled-components' +import { useParams } from 'react-router-dom' import EmojiModal from '../../atom/EmojiModal' import Icon from '../../atom/Icon' import { COLOR, SIZE } from '../../constant/style' @@ -12,14 +13,24 @@ import { } from '../../constant/icon' import { toast } from 'react-toastify' import calcEmojiModalLocation from '../../util/calculateEmojiModalLocation' -import { modalRecoil } from '../../store' -import { useRecoilState } from 'recoil' +import { modalRecoil, socketRecoil, workspaceRecoil } from '../../store' +import { useRecoilState, useRecoilValue } from 'recoil' +import updateReaction from '../../util/updateReaction' function ActionBar({ setOpenModal, chatId }) { + const { channelId } = useParams() const [modal, setModal] = useRecoilState(modalRecoil) + const workspaceUserInfo = useRecoilValue(workspaceRecoil) + const socket = useRecoilValue(socketRecoil) const sendHandler = emoji => { - console.log('TODO: send reaction', emoji.native) + updateReaction({ + workspaceUserInfo, + socket, + emoji: emoji.native || emoji, + chatId, + channelId, + }) } const closeHandler = () => { @@ -43,9 +54,15 @@ function ActionBar({ setOpenModal, chatId }) { return ( - 👍 - 👏 - 😄 + sendHandler('👍')}> + 👍 + + sendHandler('👏')}> + 👏 + + sendHandler('😄')}> + 😄 + From 12641c813b0f7a9ec968ae9593cb308f7d103c94 Mon Sep 17 00:00:00 2001 From: Ryou_Changyu Date: Sun, 13 Dec 2020 20:36:31 +0900 Subject: [PATCH 31/50] Feat: add Reaction update event on AddReactionButton MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AddReactionButton을 통해 리액션 상태를 업데이트하는 이벤트 핸들러 추가 --- .../AddReactionButton/AddReactionButton.js | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/frontend/src/atom/AddReactionButton/AddReactionButton.js b/frontend/src/atom/AddReactionButton/AddReactionButton.js index a672e1d0..f970e740 100644 --- a/frontend/src/atom/AddReactionButton/AddReactionButton.js +++ b/frontend/src/atom/AddReactionButton/AddReactionButton.js @@ -1,18 +1,29 @@ import React from 'react' import styled, { css } from 'styled-components' -import { useRecoilState } from 'recoil' -import { modalRecoil } from '../../store' +import { useParams } from 'react-router-dom' +import { useRecoilState, useRecoilValue } from 'recoil' +import { modalRecoil, socketRecoil, workspaceRecoil } from '../../store' import EmojiModal from '../EmojiModal' +import updateReaction from '../../util/updateReaction' import Icon from '../Icon' import { PLUS, SMILE } from '../../constant/icon' import { COLOR } from '../../constant/style' import calcEmojiModalLocation from '../../util/calculateEmojiModalLocation' -function AddReactionButton() { +function AddReactionButton({ chatId }) { + const { channelId } = useParams() const [modal, setModal] = useRecoilState(modalRecoil) + const workspaceUserInfo = useRecoilValue(workspaceRecoil) + const socket = useRecoilValue(socketRecoil) - const sendHandler = emoji => { - console.log('TODO: send reaction', emoji.native) + const updateReactionHandler = emoji => { + updateReaction({ + workspaceUserInfo, + socket, + emoji: emoji.native, + chatId, + channelId, + }) } const closeHandler = () => { @@ -24,7 +35,7 @@ function AddReactionButton() { setModal( Date: Sun, 13 Dec 2020 20:38:01 +0900 Subject: [PATCH 32/50] Refactor: update ThreadReactionCard component MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ThreadReactionCard에서 상태 변화에 따라 색상을 반영하고 ThreadReactionCard 클릭을 통해 리액션 업데이트 이벤트를 발생시킬 수 있도록 변경 --- .../ThreadReactionCard/ThreadReactionCard.js | 47 +++++++++++-------- 1 file changed, 28 insertions(+), 19 deletions(-) diff --git a/frontend/src/atom/ThreadReactionCard/ThreadReactionCard.js b/frontend/src/atom/ThreadReactionCard/ThreadReactionCard.js index 79f4fbb4..c9ceb9b3 100644 --- a/frontend/src/atom/ThreadReactionCard/ThreadReactionCard.js +++ b/frontend/src/atom/ThreadReactionCard/ThreadReactionCard.js @@ -1,40 +1,49 @@ import React, { useState, useEffect } from 'react' +import { useParams } from 'react-router-dom' import styled, { css } from 'styled-components' -import { useRecoilState } from 'recoil' -import { workspaceRecoil } from '../../store' +import { useRecoilState, useRecoilValue } from 'recoil' +import { workspaceRecoil, socketRecoil } from '../../store' import { COLOR } from '../../constant/style' +import updateReaction from '../../util/updateReaction' -function ThreadReactionCard({ emoji, users }) { +function ThreadReactionCard({ reaction, chatId }) { + const { channelId } = useParams() + const socket = useRecoilValue(socketRecoil) const [userInfo, setUserInfo] = useRecoilState(workspaceRecoil) const [myReaction, setMyReaction] = useState(false) useEffect(() => { setMyReaction(hasMyReaction()) - }, []) + }, [reaction.users.length]) const hasMyReaction = () => { - const result = users.every(user => { - return user._id !== userInfo._id + if (reaction.users[0] === undefined) return false + const result = reaction.users.every(user => { + return user && user._id !== userInfo._id }) return !result } - const removeMyReaction = () => { - console.log('TODO: remove my reaction', emoji) - } - - const addMyReaction = () => { - console.log('TODO: add my reaction', emoji) + const updateReactions = () => { + updateReaction({ + workspaceUserInfo: userInfo, + socket, + emoji: reaction.emoji, + chatId, + channelId, + }) } return ( - - {emoji} - {users.length} - + reaction.users.length !== 0 && ( + + {reaction.emoji} + {reaction.users.length} + + ) ) } From 6ce2639d3a2eb80200df19b6008645e70b183efd Mon Sep 17 00:00:00 2001 From: Ryou_Changyu Date: Sun, 13 Dec 2020 21:28:59 +0900 Subject: [PATCH 33/50] Refactor: update hover ActionBar event MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit chatMessage에 hover시 혹은 이모지 모달이 열렸을 경우에만 ActionBar 컴포넌트가 렌더링 되도록 변경 --- .../src/organism/ChatMessage/ChatMessage.js | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/frontend/src/organism/ChatMessage/ChatMessage.js b/frontend/src/organism/ChatMessage/ChatMessage.js index 5299d778..5a7fb058 100644 --- a/frontend/src/organism/ChatMessage/ChatMessage.js +++ b/frontend/src/organism/ChatMessage/ChatMessage.js @@ -12,9 +12,16 @@ const ChatMessage = forwardRef( ref, ) => { const [openModal, setOpenModal] = useState(false) - + const [hover, setHover] = useState(false) + console.log('hove', hover) return ( - + setHover(true)} + onMouseLeave={() => setHover(false)} + > - - + {(hover || openModal) && ( + + + + )} ) }, @@ -54,13 +63,7 @@ const ActionBarStyle = styled.div` top: -15px; right: 10px; border-radius: 5px; - display: none; - &:hover { - display: flex; - } - display: ${({ openModal }) => { - return openModal ? 'flex' : 'none' - }}; + display: flex; ` const MessageContents = styled.div` width: auto; @@ -83,9 +86,6 @@ const StyledMessageContainer = styled.div` }} &:hover { background-color: ${COLOR.HOVER_GRAY}; - ${ActionBarStyle} { - display: flex; - } } ` From a50a9baf9a0cb2524ad9b19cb8479c093991e0c4 Mon Sep 17 00:00:00 2001 From: Ryou_Changyu Date: Sun, 13 Dec 2020 22:08:37 +0900 Subject: [PATCH 34/50] Refactor: remove dead code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 불필요한 코드 제거 --- frontend/src/organism/ChatMessage/ChatMessage.js | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/src/organism/ChatMessage/ChatMessage.js b/frontend/src/organism/ChatMessage/ChatMessage.js index 5a7fb058..a2036cfc 100644 --- a/frontend/src/organism/ChatMessage/ChatMessage.js +++ b/frontend/src/organism/ChatMessage/ChatMessage.js @@ -13,7 +13,6 @@ const ChatMessage = forwardRef( ) => { const [openModal, setOpenModal] = useState(false) const [hover, setHover] = useState(false) - console.log('hove', hover) return ( Date: Sun, 13 Dec 2020 22:09:10 +0900 Subject: [PATCH 35/50] Fix: ThreadReactionCard bug fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ThreadReactionCard에서 _id를 찾지 못해 발생하는 버그 수정 --- frontend/src/atom/ThreadReactionCard/ThreadReactionCard.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/atom/ThreadReactionCard/ThreadReactionCard.js b/frontend/src/atom/ThreadReactionCard/ThreadReactionCard.js index c9ceb9b3..f09dc544 100644 --- a/frontend/src/atom/ThreadReactionCard/ThreadReactionCard.js +++ b/frontend/src/atom/ThreadReactionCard/ThreadReactionCard.js @@ -19,7 +19,7 @@ function ThreadReactionCard({ reaction, chatId }) { const hasMyReaction = () => { if (reaction.users[0] === undefined) return false const result = reaction.users.every(user => { - return user && user._id !== userInfo._id + return user && user._id !== null && user._id !== userInfo._id }) return !result } From da0b3d8c9c13b7eb439e62baf0693d59c6bdac62 Mon Sep 17 00:00:00 2001 From: Ryou_Changyu Date: Sun, 13 Dec 2020 22:30:26 +0900 Subject: [PATCH 36/50] Refactor: update default image url MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit github login시 profileUrl이 없을 경우에 default image url을 가질 수 있도록 예외처리를 해 주었습니다. --- backend/config/passport.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/backend/config/passport.js b/backend/config/passport.js index af553142..05319992 100644 --- a/backend/config/passport.js +++ b/backend/config/passport.js @@ -18,7 +18,9 @@ async function gitStrategyLogin(profiles) { const data = await User.create({ OAuthId: profiles.id, fullName: profiles.username, - profileUrl: profiles.photos[0].value, + profileUrl: + profiles.photos[0].value || + 'https://user-images.githubusercontent.com/56837413/102013276-583f6000-3d92-11eb-8184-186bc09f2a98.jpg', isDeleted: false, }) return { From 0d7e525d49f2d97992da7cc860669e4c0e0fa26e Mon Sep 17 00:00:00 2001 From: rockpell Date: Mon, 14 Dec 2020 01:22:26 +0900 Subject: [PATCH 37/50] Fix:downloadFile api MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit downloadFile api 삭제 --- backend/controller/file/file.js | 8 -------- backend/controller/file/index.js | 1 - backend/service/file.js | 3 --- 3 files changed, 12 deletions(-) diff --git a/backend/controller/file/file.js b/backend/controller/file/file.js index 0d66ab9f..95ca2092 100644 --- a/backend/controller/file/file.js +++ b/backend/controller/file/file.js @@ -8,14 +8,6 @@ exports.getFileURL = asyncWrapper(async (req, res) => { return res.status(code).json({ success, data }) }) -exports.downloadFile = asyncWrapper(async (req, res) => { - const { code, success, data } = await service.downloadFile({ - ...req.body, - creator: req.user.id, - }) - return res.status(code).json({ success, data }) -}) - exports.uploadFile = asyncWrapper(async (req, res) => { const { code, success, data } = await service.uploadFile({ file: req.file, diff --git a/backend/controller/file/index.js b/backend/controller/file/index.js index 61513ab2..e4ab4763 100644 --- a/backend/controller/file/index.js +++ b/backend/controller/file/index.js @@ -8,7 +8,6 @@ const storage = multer.memoryStorage() const uploader = multer({ storage: storage }) router.get('/', Auth, controller.getFileURL) -router.get('/download', Auth, controller.downloadFile) router.post('/', Auth, uploader.single('file'), controller.uploadFile) router.delete('/', Auth, controller.deleteFile) diff --git a/backend/service/file.js b/backend/service/file.js index bd7450f4..ed4bc1c8 100644 --- a/backend/service/file.js +++ b/backend/service/file.js @@ -24,8 +24,6 @@ const getFileURL = async ({ fileId }) => { } } -const downloadFile = async ({ fileId }) => {} - const uploadFile = async ({ file, userId }) => { verifyRequiredParams(file, userId) const fileName = `${file.fieldname}-${Date.now()}-${file.originalname}` @@ -85,6 +83,5 @@ const deleteFile = async ({ fileId }) => { module.exports = { uploadFile, getFileURL, - downloadFile, deleteFile, } From 40215e4e59e4bdcac0613a2640a77e6a82e08253 Mon Sep 17 00:00:00 2001 From: rockpell Date: Mon, 14 Dec 2020 01:23:28 +0900 Subject: [PATCH 38/50] Docs: file api MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit file api get, post, delete 추가 --- backend/docs/swagger.yaml | 155 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 155 insertions(+) diff --git a/backend/docs/swagger.yaml b/backend/docs/swagger.yaml index 248fdfeb..57ecee25 100644 --- a/backend/docs/swagger.yaml +++ b/backend/docs/swagger.yaml @@ -1137,6 +1137,161 @@ paths: default: description: Default error sample response + '/api/file/{fileId}': + summary: get chat messages + description: get chat messages + get: + tags: + - file + operationId: '' + parameters: + - name: fileId + in: path + description: fileId + required: true + explode: true + schema: + type: string + responses: + '200': + content: + application/json: + schema: + type: object + properties: + success: + type: string + data: + type: object + properties: + url: + type: string + originalName: + type: string + + description: success response + '400': + content: + application/json: + schema: + type: object + properties: + success: + type: boolean + description: not modified + '401': + content: + application/json: + schema: + type: object + properties: + success: + type: boolean + description: unauthorized + default: + description: Default error sample response + delete: + tags: + - file + operationId: '' + parameters: + - name: fileId + in: path + description: fileId + required: true + explode: true + schema: + type: string + responses: + '200': + content: + application/json: + schema: + type: object + properties: + success: + type: boolean + + description: success response + '400': + content: + application/json: + schema: + type: object + properties: + success: + type: boolean + description: not modified + '401': + content: + application/json: + schema: + type: object + properties: + success: + type: boolean + description: unauthorized + default: + description: Default error sample response + '/api/file': + summary: file upload + description: file upload + post: + tags: + - file + operationId: '' + parameters: + - name: file + in: query + description: file + required: true + explode: true + schema: + type: string + responses: + '200': + content: + application/json: + schema: + type: object + properties: + success: + type: string + data: + type: object + properties: + fileId: + type: string + fileName: + type: string + fileType: + type: string + creator: + type: string + etag: + type: string + description: success response + '400': + content: + application/json: + schema: + type: object + properties: + success: + type: boolean + description: not modified + '401': + content: + application/json: + schema: + type: object + properties: + success: + type: boolean + description: unauthorized + default: + description: Default error sample response + components: schemas: workspaceUserInfo: From 9fad5a874549327cd79e26ee46029e20bf441975 Mon Sep 17 00:00:00 2001 From: rockpell Date: Mon, 14 Dec 2020 11:57:57 +0900 Subject: [PATCH 39/50] Refactor: Button MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SmallButton을 삭제하고 Button에 size를 추가하였습니다 --- frontend/src/atom/Button/Button.js | 14 +++-- frontend/src/atom/Button/Button.stories.js | 10 +++ .../atom/Button/SmallButton/SmallButton.js | 61 ------------------- .../Button/SmallButton/SmallButton.stories.js | 23 ------- frontend/src/atom/Button/SmallButton/index.js | 1 - .../src/organism/FilePreview/FilePreview.js | 6 +- .../src/organism/ImgPreview/ImgPreview.js | 6 +- 7 files changed, 26 insertions(+), 95 deletions(-) delete mode 100644 frontend/src/atom/Button/SmallButton/SmallButton.js delete mode 100644 frontend/src/atom/Button/SmallButton/SmallButton.stories.js delete mode 100644 frontend/src/atom/Button/SmallButton/index.js diff --git a/frontend/src/atom/Button/Button.js b/frontend/src/atom/Button/Button.js index 9d493591..cb404bfb 100644 --- a/frontend/src/atom/Button/Button.js +++ b/frontend/src/atom/Button/Button.js @@ -6,19 +6,25 @@ const Button = ({ children, type = 'default', disabled = false, + size = 'default', }) => { return ( - + {children} ) } const StyledButton = styled.button` - font-size: 15px; + font-size: ${({ size }) => (size === 'small' ? '8px' : '15px')}; font-weight: 900; - height: 36px; - padding: 0 12px 1px; + height: ${({ size }) => (size === 'small' ? '' : '36px')}; + padding: ${({ size }) => (size === 'small' ? '' : '0 12px 1px')}; border-style: none; border-radius: 4px; outline: none; diff --git a/frontend/src/atom/Button/Button.stories.js b/frontend/src/atom/Button/Button.stories.js index 9ae4a6d0..92c9256e 100644 --- a/frontend/src/atom/Button/Button.stories.js +++ b/frontend/src/atom/Button/Button.stories.js @@ -1,5 +1,8 @@ import React from 'react' import Button from './Button' +import Icon from '../Icon' +import { CLOSE } from '../../constant/icon' +import { COLOR } from '../../constant/style' export default { title: 'Atom/Button', @@ -18,3 +21,10 @@ Transparent.args = { children: 'Transparent', type: 'transparent', } + +export const SmallIcon = Template.bind({}) +SmallIcon.args = { + children: , + type: 'icon', + size: 'small', +} diff --git a/frontend/src/atom/Button/SmallButton/SmallButton.js b/frontend/src/atom/Button/SmallButton/SmallButton.js deleted file mode 100644 index 34edf1bb..00000000 --- a/frontend/src/atom/Button/SmallButton/SmallButton.js +++ /dev/null @@ -1,61 +0,0 @@ -import React from 'react' -import styled from 'styled-components' -import { COLOR } from '../../../constant/style' - -const SmallButton = ({ - handleClick, - children, - type = 'default', - disabled = false, -}) => { - return ( - - {children} - - ) -} - -const StyledButton = styled.button` - font-size: 8px; - font-weight: 900; - border-style: none; - border-radius: 4px; - outline: none; - color: ${({ type, disabled }) => { - if (disabled) return COLOR.GRAY - if (type === 'transparent') return COLOR.GRAY - return COLOR.WHITE - }}; - cursor: ${({ disabled }) => (disabled ? 'default' : 'pointer')}; - background-color: ${({ type, disabled }) => { - if (disabled) return COLOR.LIGHT_GRAY - if (type === 'transparent') return 'transparent' - if (type === 'icon') return 'transparent' - return COLOR.GREEN - }}; - border: ${({ type }) => { - if (type === 'transparent') return `1px solid ${COLOR.TRANSPARENT_GRAY}` - if (type === 'icon') return 'transparent' - }}; - &:hover { - ${({ type, disabled }) => { - if (disabled) return - if (type === 'transparent') - return `background: ${COLOR.HOVER_GRAY}; box-shadow: 0 1px 4px rgba(0,0,0,0.3);` - if (type === 'default') - return `background: ${COLOR.HOVER_GREEN}; box-shadow: 0 1px 4px rgba(0,0,0,0.3);` - if (type === 'icon') - return `background: ${COLOR.HOVER_GRAY}; - box-shadow:0 1px 3px 0 rgba(0,0,0, 0.08);` - }} - } - ${({ type }) => { - if (type === 'icon') - return ` - &:hover i svg { - color: ${COLOR.ICON_HOVER}; - } - ` - }} -` -export default SmallButton diff --git a/frontend/src/atom/Button/SmallButton/SmallButton.stories.js b/frontend/src/atom/Button/SmallButton/SmallButton.stories.js deleted file mode 100644 index f365966a..00000000 --- a/frontend/src/atom/Button/SmallButton/SmallButton.stories.js +++ /dev/null @@ -1,23 +0,0 @@ -import React from 'react' -import SmallButton from './SmallButton' -import Icon from '../../Icon' -import { CLOSE } from '../../../constant/icon' -import { COLOR } from '../../../constant/style' - -export default { - title: 'Atom/SmallButton', - component: SmallButton, -} - -const Template = args => - -export const Default = Template.bind({}) -Default.args = { - children: 'Default', -} - -export const IconButton = Template.bind({}) -IconButton.args = { - children: , - type: 'icon', -} diff --git a/frontend/src/atom/Button/SmallButton/index.js b/frontend/src/atom/Button/SmallButton/index.js deleted file mode 100644 index 5f4d6c68..00000000 --- a/frontend/src/atom/Button/SmallButton/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './SmallButton' diff --git a/frontend/src/organism/FilePreview/FilePreview.js b/frontend/src/organism/FilePreview/FilePreview.js index 8c5b84bd..2292c62f 100644 --- a/frontend/src/organism/FilePreview/FilePreview.js +++ b/frontend/src/organism/FilePreview/FilePreview.js @@ -4,7 +4,7 @@ import request from '../../util/request' import Icon from '../../atom/Icon' import { CLOSE, FILE } from '../../constant/icon' import { COLOR } from '../../constant/style' -import SmallButton from '../../atom/Button/SmallButton' +import Button from '../../atom/Button/Button' function FilePreview({ type, fileId, setIsRender }) { const [fileData, setFileData] = useState({}) @@ -33,9 +33,9 @@ function FilePreview({ type, fileId, setIsRender }) { const deleteButton = () => { return ( - + ) } diff --git a/frontend/src/organism/ImgPreview/ImgPreview.js b/frontend/src/organism/ImgPreview/ImgPreview.js index b9a56c93..b479de4d 100644 --- a/frontend/src/organism/ImgPreview/ImgPreview.js +++ b/frontend/src/organism/ImgPreview/ImgPreview.js @@ -4,7 +4,7 @@ import request from '../../util/request' import Icon from '../../atom/Icon' import { CLOSE } from '../../constant/icon' import { COLOR } from '../../constant/style' -import SmallButton from '../../atom/Button/SmallButton' +import Button from '../../atom/Button' function ImgPreview({ type, fileId, setIsRender }) { const [fileData, setFileData] = useState({}) @@ -33,9 +33,9 @@ function ImgPreview({ type, fileId, setIsRender }) { const deleteButton = () => { return ( - + ) } From 76e69a00bb9f73b16475736313d3f564c72a4861 Mon Sep 17 00:00:00 2001 From: Ryou_Changyu Date: Mon, 14 Dec 2020 13:12:46 +0900 Subject: [PATCH 40/50] Refactor: update backend removeReaction and addReaction service MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 기존 updateReaction 한개로 리액션을 업데이트 했던 부분을 removeReaction과 addReaction으로 분리해 restful 하게 변경했습니다. --- backend/chatServer.js | 23 +++++++++++++++-------- backend/service/reaction.js | 17 +++++++++++------ 2 files changed, 26 insertions(+), 14 deletions(-) diff --git a/backend/chatServer.js b/backend/chatServer.js index 9807df16..c3fce5e3 100644 --- a/backend/chatServer.js +++ b/backend/chatServer.js @@ -3,7 +3,7 @@ import express from 'express' import { createServer } from 'http' import createChatServer from 'socket.io' import { createChatMessage } from './service/chat' -import { updateReaction } from './service/reaction' +import { addReaction, removeReaction } from './service/reaction' dotenv() const server = createServer(express()) @@ -35,13 +35,20 @@ namespace.on('connection', socket => { }) }) socket.on('update reaction', async data => { - const { emoji, chatId, userInfo, channelId } = data + const { emoji, chatId, userInfo, channelId, type } = data //1 = add, 0 = remove - const result = await updateReaction({ - workspaceUserInfoId, - chatId, - emoticon: emoji, - }) + const result = + type === 1 + ? await addReaction({ + workspaceUserInfoId, + chatId, + emoticon: emoji, + }) + : await removeReaction({ + workspaceUserInfoId, + chatId, + emoticon: emoji, + }) namespace.in(channelId).emit('update reaction', { reaction: { @@ -49,7 +56,7 @@ namespace.on('connection', socket => { emoji: emoji, workspaceUserInfoId: userInfo._id, displayName: userInfo.displayName, - type: result ? 1 : 0, + type: result ? type : false, }, }) }) diff --git a/backend/service/reaction.js b/backend/service/reaction.js index 67602d39..55f2910e 100644 --- a/backend/service/reaction.js +++ b/backend/service/reaction.js @@ -1,7 +1,7 @@ import { Reaction } from '../model/Reaction' import { verifyRequiredParams, dbErrorHandler } from '../util' -const updateReaction = async ({ chatId, workspaceUserInfoId, emoticon }) => { +const addReaction = async ({ chatId, workspaceUserInfoId, emoticon }) => { verifyRequiredParams(chatId, workspaceUserInfoId, emoticon) const isExist = await dbErrorHandler(() => Reaction.find({ chatId, workspaceUserInfoId, emoticon }), @@ -10,12 +10,17 @@ const updateReaction = async ({ chatId, workspaceUserInfoId, emoticon }) => { await dbErrorHandler(() => Reaction.create({ chatId, workspaceUserInfoId, emoticon }), ) - } else { - await dbErrorHandler(() => - Reaction.deleteOne({ chatId, workspaceUserInfoId, emoticon }), - ) } return isExist.length === 0 } -module.exports = { updateReaction } +const removeReaction = async ({ chatId, workspaceUserInfoId, emoticon }) => { + verifyRequiredParams(chatId, workspaceUserInfoId, emoticon) + const result = await dbErrorHandler(() => + Reaction.findOneAndDelete({ chatId, workspaceUserInfoId, emoticon }), + ) + + return result && true +} + +module.exports = { addReaction, removeReaction } From f0ab42c2eccc96134219a307f4911877eee48da6 Mon Sep 17 00:00:00 2001 From: Ryou_Changyu Date: Mon, 14 Dec 2020 13:13:47 +0900 Subject: [PATCH 41/50] Refactor: remove updateReaction util MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 분리한 updateReaction util을 제거 --- frontend/src/util/updateReaction.js | 20 -------------------- 1 file changed, 20 deletions(-) delete mode 100644 frontend/src/util/updateReaction.js diff --git a/frontend/src/util/updateReaction.js b/frontend/src/util/updateReaction.js deleted file mode 100644 index 8b1f16ed..00000000 --- a/frontend/src/util/updateReaction.js +++ /dev/null @@ -1,20 +0,0 @@ -function updateReaction({ - workspaceUserInfo, - socket, - emoji, - chatId, - channelId, -}) { - const reaction = { - emoji: emoji, - chatId: chatId, - channelId: channelId, - userInfo: { - _id: workspaceUserInfo._id, - displayName: workspaceUserInfo.displayName, - }, - } - socket.emit('update reaction', reaction) -} - -export default updateReaction From 6ae36cc43942602d13c57cf50ce440cdb187a705 Mon Sep 17 00:00:00 2001 From: Ryou_Changyu Date: Mon, 14 Dec 2020 13:15:33 +0900 Subject: [PATCH 42/50] Refactor: update reviewed ChatRoom code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 리뷰받은 코드 수정 - return value를 Array.map 결과 값으로 설정 - socket.off 코드 수정 - reaction update 예외처리 추가 --- frontend/src/organism/ChatRoom/ChatRoom.js | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/frontend/src/organism/ChatRoom/ChatRoom.js b/frontend/src/organism/ChatRoom/ChatRoom.js index 7bc20687..53560c0a 100644 --- a/frontend/src/organism/ChatRoom/ChatRoom.js +++ b/frontend/src/organism/ChatRoom/ChatRoom.js @@ -62,19 +62,22 @@ const ChatRoom = () => { const chageReactionState = (messages, reaction) => { let done = false - const arr = messages.map((message, idx) => { + if (reaction.type === false) { + return messages + } + return messages.map((message, idx) => { if (message._id === reaction.chatId) { message.reactions && message.reactions.map((item, idx) => { if (item.emoji === reaction.emoji) { if (reaction.type) { - const userInfo = [ + item.users = [ + ...item.users, { _id: reaction.workspaceUserInfoId, displayName: reaction.displayName, }, ] - item.users = [...item.users, ...userInfo] } else { item.users.map((user, idx) => { if (user._id === reaction.workspaceUserInfoId) { @@ -99,7 +102,6 @@ const ChatRoom = () => { } return message }) - return [...arr] } useEffect(() => { @@ -114,7 +116,10 @@ const ChatRoom = () => { }) } return () => { - socket && socket.off('new message') && socket.off('update reaction') + if (socket) { + socket.off('new message') + socket.off('update reaction') + } } }, [socket, channelId]) From 6691e0350ae307f70ae6e1d0913964b438b8ff5e Mon Sep 17 00:00:00 2001 From: Ryou_Changyu Date: Mon, 14 Dec 2020 13:17:17 +0900 Subject: [PATCH 43/50] Refactor: seperate updateReaction logic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updateReaction을 리액션 추가인지 삭제인지에 따라 type을 지정해 socket 통신 하도록 변경 --- .../AddReactionButton/AddReactionButton.js | 22 ++----- .../ThreadReactionCard/ThreadReactionCard.js | 36 +++++------- frontend/src/organism/ActionBar/ActionBar.js | 36 ++++-------- .../src/organism/ChatMessage/ChatMessage.js | 58 ++++++++++++++++++- .../ThreadReactionList/ThreadReactionList.js | 16 ++++- 5 files changed, 98 insertions(+), 70 deletions(-) diff --git a/frontend/src/atom/AddReactionButton/AddReactionButton.js b/frontend/src/atom/AddReactionButton/AddReactionButton.js index f970e740..d881f4a6 100644 --- a/frontend/src/atom/AddReactionButton/AddReactionButton.js +++ b/frontend/src/atom/AddReactionButton/AddReactionButton.js @@ -1,30 +1,16 @@ import React from 'react' import styled, { css } from 'styled-components' -import { useParams } from 'react-router-dom' -import { useRecoilState, useRecoilValue } from 'recoil' -import { modalRecoil, socketRecoil, workspaceRecoil } from '../../store' +import { useRecoilState } from 'recoil' +import { modalRecoil } from '../../store' import EmojiModal from '../EmojiModal' -import updateReaction from '../../util/updateReaction' + import Icon from '../Icon' import { PLUS, SMILE } from '../../constant/icon' import { COLOR } from '../../constant/style' import calcEmojiModalLocation from '../../util/calculateEmojiModalLocation' -function AddReactionButton({ chatId }) { - const { channelId } = useParams() +function AddReactionButton({ updateReactionHandler }) { const [modal, setModal] = useRecoilState(modalRecoil) - const workspaceUserInfo = useRecoilValue(workspaceRecoil) - const socket = useRecoilValue(socketRecoil) - - const updateReactionHandler = emoji => { - updateReaction({ - workspaceUserInfo, - socket, - emoji: emoji.native, - chatId, - channelId, - }) - } const closeHandler = () => { setModal(null) diff --git a/frontend/src/atom/ThreadReactionCard/ThreadReactionCard.js b/frontend/src/atom/ThreadReactionCard/ThreadReactionCard.js index f09dc544..fbc3f650 100644 --- a/frontend/src/atom/ThreadReactionCard/ThreadReactionCard.js +++ b/frontend/src/atom/ThreadReactionCard/ThreadReactionCard.js @@ -1,14 +1,10 @@ import React, { useState, useEffect } from 'react' -import { useParams } from 'react-router-dom' -import styled, { css } from 'styled-components' -import { useRecoilState, useRecoilValue } from 'recoil' -import { workspaceRecoil, socketRecoil } from '../../store' +import styled from 'styled-components' +import { useRecoilState } from 'recoil' +import { workspaceRecoil } from '../../store' import { COLOR } from '../../constant/style' -import updateReaction from '../../util/updateReaction' -function ThreadReactionCard({ reaction, chatId }) { - const { channelId } = useParams() - const socket = useRecoilValue(socketRecoil) +function ThreadReactionCard({ reaction, chatId, updateReactionHandler }) { const [userInfo, setUserInfo] = useRecoilState(workspaceRecoil) const [myReaction, setMyReaction] = useState(false) @@ -17,27 +13,25 @@ function ThreadReactionCard({ reaction, chatId }) { }, [reaction.users.length]) const hasMyReaction = () => { - if (reaction.users[0] === undefined) return false + if (reaction.users[0] === undefined) { + reaction.set = false + return false + } const result = reaction.users.every(user => { - return user && user._id !== null && user._id !== userInfo._id + return user?._id !== userInfo?._id }) + if (!result) { + reaction.set = true + } else { + reaction.set = false + } return !result } - const updateReactions = () => { - updateReaction({ - workspaceUserInfo: userInfo, - socket, - emoji: reaction.emoji, - chatId, - channelId, - }) - } - return ( reaction.users.length !== 0 && ( updateReactionHandler(reaction.emoji)} myReaction={myReaction} > {reaction.emoji} diff --git a/frontend/src/organism/ActionBar/ActionBar.js b/frontend/src/organism/ActionBar/ActionBar.js index cad7b54a..5d1f047c 100644 --- a/frontend/src/organism/ActionBar/ActionBar.js +++ b/frontend/src/organism/ActionBar/ActionBar.js @@ -1,9 +1,8 @@ -import React, { useState } from 'react' -import styled, { css } from 'styled-components' -import { useParams } from 'react-router-dom' +import React from 'react' +import styled from 'styled-components' import EmojiModal from '../../atom/EmojiModal' import Icon from '../../atom/Icon' -import { COLOR, SIZE } from '../../constant/style' +import { COLOR } from '../../constant/style' import { SMILE, COMMENTDOTS, @@ -11,27 +10,12 @@ import { BOOKMARK, ELLIPSISV, } from '../../constant/icon' -import { toast } from 'react-toastify' import calcEmojiModalLocation from '../../util/calculateEmojiModalLocation' -import { modalRecoil, socketRecoil, workspaceRecoil } from '../../store' -import { useRecoilState, useRecoilValue } from 'recoil' -import updateReaction from '../../util/updateReaction' +import { modalRecoil } from '../../store' +import { useRecoilState } from 'recoil' -function ActionBar({ setOpenModal, chatId }) { - const { channelId } = useParams() +function ActionBar({ setOpenModal, chatId, updateReactionHandler }) { const [modal, setModal] = useRecoilState(modalRecoil) - const workspaceUserInfo = useRecoilValue(workspaceRecoil) - const socket = useRecoilValue(socketRecoil) - - const sendHandler = emoji => { - updateReaction({ - workspaceUserInfo, - socket, - emoji: emoji.native || emoji, - chatId, - channelId, - }) - } const closeHandler = () => { setOpenModal(false) @@ -44,7 +28,7 @@ function ActionBar({ setOpenModal, chatId }) { setOpenModal(true) setModal( - sendHandler('👍')}> + updateReactionHandler('👍')}> 👍 - sendHandler('👏')}> + updateReactionHandler('👏')}> 👏 - sendHandler('😄')}> + updateReactionHandler('😄')}> 😄 diff --git a/frontend/src/organism/ChatMessage/ChatMessage.js b/frontend/src/organism/ChatMessage/ChatMessage.js index a2036cfc..5924e19e 100644 --- a/frontend/src/organism/ChatMessage/ChatMessage.js +++ b/frontend/src/organism/ChatMessage/ChatMessage.js @@ -5,14 +5,60 @@ import ChatContent from '../../atom/ChatContent' import ThreadReactionList from '../ThreadReactionList' import ActionBar from '../ActionBar' import { SIZE, COLOR } from '../../constant/style' +import { workspaceRecoil, socketRecoil } from '../../store' +import { useRecoilValue } from 'recoil' +import { useParams } from 'react-router-dom' const ChatMessage = forwardRef( ( { userInfo, reply, reactions, _id, createdAt, contents, type = 'chat' }, ref, ) => { + const { channelId } = useParams() const [openModal, setOpenModal] = useState(false) const [hover, setHover] = useState(false) + const workspaceUserInfo = useRecoilValue(workspaceRecoil) + const socket = useRecoilValue(socketRecoil) + + const updateReaction = ({ emoji, chatId, channelId, type }) => { + const reaction = { + emoji, + chatId, + channelId, + type, + userInfo: { + _id: workspaceUserInfo._id, + displayName: workspaceUserInfo.displayName, + }, + } + socket.emit('update reaction', reaction) + } + + const updateReactionHandler = emoji => { + let done = false + reactions.map((reaction, idx) => { + if (reaction.emoji === emoji.native || reaction.emoji === emoji) { + if (reaction.set) { + updateReaction({ + emoji: emoji.native || emoji, + chatId: _id, + channelId, + type: 0, + }) + done = true + } + } + }) + if (!done) { + updateReaction({ + emoji: emoji.native || emoji, + chatId: _id, + channelId, + type: 1, + }) + } + } + return ( - + )} {/* TODO view thread reply 구현 */} @@ -47,7 +97,11 @@ const ChatMessage = forwardRef( {/* TODO Action bar 구현 */} {(hover || openModal) && ( - + )} diff --git a/frontend/src/organism/ThreadReactionList/ThreadReactionList.js b/frontend/src/organism/ThreadReactionList/ThreadReactionList.js index ab5bd40c..d1c3bc85 100644 --- a/frontend/src/organism/ThreadReactionList/ThreadReactionList.js +++ b/frontend/src/organism/ThreadReactionList/ThreadReactionList.js @@ -3,15 +3,25 @@ import styled, { css } from 'styled-components' import ThreadReactionCard from '../../atom/ThreadReactionCard' import AddReactionButton from '../../atom/AddReactionButton' -function ThreadReactionList({ reactions, chatId }) { +function ThreadReactionList({ reactions, chatId, updateReactionHandler }) { const renderReactionCard = reactions.map((reaction, idx) => { - return + return ( + + ) }) return ( {renderReactionCard} - + ) } From cb175fe46d7d193fdfc560484019bca0f327415d Mon Sep 17 00:00:00 2001 From: rockpell Date: Mon, 14 Dec 2020 13:46:20 +0900 Subject: [PATCH 44/50] Refactor: s3, file service MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 필요한 패키지 가져오는 방법을 require 대신에 import로 변경 구조분해 할당 적용 --- backend/config/s3.js | 2 +- backend/service/file.js | 24 +++++++----------------- 2 files changed, 8 insertions(+), 18 deletions(-) diff --git a/backend/config/s3.js b/backend/config/s3.js index 19f04b04..72ec1303 100644 --- a/backend/config/s3.js +++ b/backend/config/s3.js @@ -1,4 +1,4 @@ -const AWS = require('aws-sdk') +import AWS from 'aws-sdk' require('dotenv').config() const S3 = new AWS.S3({ diff --git a/backend/service/file.js b/backend/service/file.js index ed4bc1c8..dc9d2d7f 100644 --- a/backend/service/file.js +++ b/backend/service/file.js @@ -2,13 +2,13 @@ import { verifyRequiredParams, dbErrorHandler } from '../util/' import statusCode from '../util/statusCode' import { S3, BUCKETNAME } from '../config/s3' import { File } from '../model/File' -const mongoose = require('mongoose') +import mongoose from 'mongoose' const ObjectId = mongoose.Types.ObjectId const getFileURL = async ({ fileId }) => { verifyRequiredParams(fileId) - const fileData = await dbErrorHandler(() => + const { name, originalName } = await dbErrorHandler(() => File.findOne({ _id: ObjectId(fileId), }), @@ -17,8 +17,8 @@ const getFileURL = async ({ fileId }) => { return { code: statusCode.OK, data: { - url: `${process.env.S3_ENDPOINT}/${process.env.S3_BUCKETNAME}/${fileData.name}`, - originalName: fileData.originalName, + url: `${process.env.S3_ENDPOINT}/${process.env.S3_BUCKETNAME}/${name}`, + originalName: originalName, }, success: true, } @@ -60,22 +60,12 @@ const uploadFile = async ({ file, userId }) => { const deleteFile = async ({ fileId }) => { verifyRequiredParams(fileId) - const fileData = await dbErrorHandler(() => - File.findOne({ - _id: ObjectId(fileId), - }), - ) - if (!fileData) { - return { code: statusCode.BAD_REQUEST, success: false } - } - await dbErrorHandler(() => - File.deleteOne({ - _id: ObjectId(fileId), - }), + const { name } = await dbErrorHandler(() => + File.findOneAndDelete({ _id: ObjectId(fileId) }), ) await S3.deleteObject({ Bucket: BUCKETNAME, - Key: fileData.name, + Key: name, }).promise() return { code: statusCode.OK, success: true } } From d41068204473f4c9f2e5c9bd2d9606c7eb7f9d24 Mon Sep 17 00:00:00 2001 From: rockpell Date: Mon, 14 Dec 2020 13:52:04 +0900 Subject: [PATCH 45/50] Refactor: remove etag from File Model --- backend/model/File.js | 3 --- backend/service/file.js | 2 -- 2 files changed, 5 deletions(-) diff --git a/backend/model/File.js b/backend/model/File.js index bb4bb871..0e0cbe52 100644 --- a/backend/model/File.js +++ b/backend/model/File.js @@ -19,9 +19,6 @@ const fileSchema = mongoose.Schema( type: Schema.Types.ObjectId, ref: 'User', }, - etag: { - type: String, - }, }, { timestamps: true }, ) diff --git a/backend/service/file.js b/backend/service/file.js index dc9d2d7f..a43f04c6 100644 --- a/backend/service/file.js +++ b/backend/service/file.js @@ -42,7 +42,6 @@ const uploadFile = async ({ file, userId }) => { path: '/', fileType: file.mimetype, creator: userId, - etag: result.ETag, }), ) return { @@ -52,7 +51,6 @@ const uploadFile = async ({ file, userId }) => { fileName: data.originalName, fileType: data.fileType, creator: data.creator, - etag: data.etag, }, success: true, } From 877556bc91c97b97ccd25d1561becccef7c2143f Mon Sep 17 00:00:00 2001 From: rockpell Date: Mon, 14 Dec 2020 13:57:52 +0900 Subject: [PATCH 46/50] Fix: FilePreview story MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 기본 fileId를 변경 --- frontend/src/organism/FilePreview/FilePreview.stories.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/organism/FilePreview/FilePreview.stories.js b/frontend/src/organism/FilePreview/FilePreview.stories.js index 40138ef6..04d6acbd 100644 --- a/frontend/src/organism/FilePreview/FilePreview.stories.js +++ b/frontend/src/organism/FilePreview/FilePreview.stories.js @@ -11,7 +11,7 @@ const Template = args => <>{isRender && } export const inputFilePreview = Template.bind({}) inputFilePreview.args = { type: 'input', // input, message - fileId: '5fd5e4018c8a82245fa0ab39', + fileId: '5fd6ea342d026a63752cd31b', setIsRender: () => { isRender = false }, @@ -20,5 +20,5 @@ inputFilePreview.args = { export const messageFilePreview = Template.bind({}) messageFilePreview.args = { type: 'message', // input, message - fileId: '5fd5e4018c8a82245fa0ab39', + fileId: '5fd6ea342d026a63752cd31b', } From f59fa6de5740d7b1c803e25c839979b1aab2a904 Mon Sep 17 00:00:00 2001 From: Ryou_Changyu Date: Mon, 14 Dec 2020 14:35:43 +0900 Subject: [PATCH 47/50] Refactor: null pointer exception refactor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit null 참조 예외처리를 추가 --- backend/config/passport.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/config/passport.js b/backend/config/passport.js index 05319992..bf03add2 100644 --- a/backend/config/passport.js +++ b/backend/config/passport.js @@ -19,7 +19,7 @@ async function gitStrategyLogin(profiles) { OAuthId: profiles.id, fullName: profiles.username, profileUrl: - profiles.photos[0].value || + profiles?.photos[0]?.value || 'https://user-images.githubusercontent.com/56837413/102013276-583f6000-3d92-11eb-8184-186bc09f2a98.jpg', isDeleted: false, }) From 11d3e7301b973819c1bdf2c7b18c69b0dca9a5b6 Mon Sep 17 00:00:00 2001 From: Ryou_Changyu Date: Mon, 14 Dec 2020 14:56:06 +0900 Subject: [PATCH 48/50] Fix: channelConfig default section bug fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit channelConfig에 기본 sectionName을 null로 선언하지 않아 발생하는 문제 해결 --- backend/service/channel.js | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/service/channel.js b/backend/service/channel.js index 07beb0a6..3b31bf1f 100644 --- a/backend/service/channel.js +++ b/backend/service/channel.js @@ -18,6 +18,7 @@ const createChannel = async params => { ChannelConfig.create({ channelId: result._id, workspaceUserInfoId: result.creator, + sectionName: null, }), ) return { From 5c8c3153446c82d1a6a86457a5ef7525d2749f01 Mon Sep 17 00:00:00 2001 From: Ryou_Changyu Date: Mon, 14 Dec 2020 15:06:46 +0900 Subject: [PATCH 49/50] Fix: channelConfig default section bug fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit channelConfig에 기본 sectionName을 null로 선언하지 않아 발생하는 문제 해결 --- backend/service/workspace.js | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/service/workspace.js b/backend/service/workspace.js index 00d717cc..c73e3e83 100644 --- a/backend/service/workspace.js +++ b/backend/service/workspace.js @@ -128,6 +128,7 @@ const invited = async ({ userId, code }) => { ChannelConfig.create({ channelId: ObjectId(workspaceData.default_channel), workspaceUserInfoId: ObjectId(createdWorkspaceUserData._id), + sectionName: null, }), ) From 1bf67c82e12938e3f2c3ec00005c46e24a0b8797 Mon Sep 17 00:00:00 2001 From: Ryou_Changyu Date: Mon, 14 Dec 2020 15:26:19 +0900 Subject: [PATCH 50/50] Fix: ChannelStarBtn default section bug fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ChannelStarBtn default section 조건분기 수정 --- backend/service/channel.js | 2 -- backend/service/workspace.js | 2 -- frontend/src/atom/ChannelStarBtn/ChannelStarBtn.js | 5 +++-- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/backend/service/channel.js b/backend/service/channel.js index 3b31bf1f..a2894da0 100644 --- a/backend/service/channel.js +++ b/backend/service/channel.js @@ -18,7 +18,6 @@ const createChannel = async params => { ChannelConfig.create({ channelId: result._id, workspaceUserInfoId: result.creator, - sectionName: null, }), ) return { @@ -78,7 +77,6 @@ const inviteUserDB = async ({ channelId, workspaceUserInfoId }) => { channelId, isMute: false, notification: 0, - sectionName: null, }) channelConfig.save() }) diff --git a/backend/service/workspace.js b/backend/service/workspace.js index c73e3e83..1d7f491f 100644 --- a/backend/service/workspace.js +++ b/backend/service/workspace.js @@ -46,7 +46,6 @@ const createWorkspace = async params => { ChannelConfig.create({ channelId: ObjectId(channelData._id), workspaceUserInfoId: ObjectId(channelData.creator), - sectionName: null, }), ) await dbErrorHandler(() => @@ -128,7 +127,6 @@ const invited = async ({ userId, code }) => { ChannelConfig.create({ channelId: ObjectId(workspaceData.default_channel), workspaceUserInfoId: ObjectId(createdWorkspaceUserData._id), - sectionName: null, }), ) diff --git a/frontend/src/atom/ChannelStarBtn/ChannelStarBtn.js b/frontend/src/atom/ChannelStarBtn/ChannelStarBtn.js index f1dbc328..c333ee85 100644 --- a/frontend/src/atom/ChannelStarBtn/ChannelStarBtn.js +++ b/frontend/src/atom/ChannelStarBtn/ChannelStarBtn.js @@ -9,6 +9,7 @@ import Icon from '../Icon' import { workspaceRecoil } from '../../store' import { STAR, COLOREDSTAR } from '../../constant/icon' import { atom, useRecoilState, useRecoilValue } from 'recoil' +import { isEmpty } from '../../util' import useChannelList from '../../hooks/useChannelList' function ChannelStarBtn({ channel }) { @@ -27,7 +28,7 @@ function ChannelStarBtn({ channel }) { const updateSection = async () => { try { let sectionName = null - if (sectionInfo === null) sectionName = 'Starred' + if (isEmpty(sectionInfo)) sectionName = 'Starred' const { data } = await request.PATCH('/api/channel/section', { workspaceUserInfoId: workspaceUserInfo._id, @@ -49,7 +50,7 @@ function ChannelStarBtn({ channel }) { return ( - {sectionInfo !== null ? ( + {!isEmpty(sectionInfo) ? ( ) : (