diff --git a/.github/ISSUE_TEMPLATE/api.md b/.github/ISSUE_TEMPLATE/api.md new file mode 100644 index 00000000..e0503a41 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/api.md @@ -0,0 +1,56 @@ +--- +name: API +about: 백엔드 API 이슈 탬플릿 +title: '작업 - 북마크 생성 API 구현 ' +labels: BE, API +assignees: Zinyon, pjy0416, shkilo + +--- + +## Task - Bookmark POST API +URL +``` +POST /api/task/:taskId/bookmark +``` + +Request +``` +{ + url : 'https://another...' +} +``` + +Request Description +| Name | Required | Type| Description | +| :------------- |:--------------|:------------|:------------| +| grant_type | **REQUIRED** | **CODE** | [RFC 6759#session-6](https://tools.ietf.org/html/rfc6749#section-6) 에 정의 된 인증유형에 대한 구분 값. 현재 다음의 2가지 type 만 사용가능. - 인증코드 사용시: `authorization_code`- 리프레시 토큰 사용 시: `refresh_token` | +| client_id | **REQUIRED** | **STRING** | 발급 된 애플리케이션 자격증명의 Client ID 값 | +| client_secret | **REQUIRED** | **STRING** | 발급 된 애플리케이션 자격증명의 Client SECRET 값 | +| code | **CONDITIONAL** | **STRING** | authorization code request 응답으로 부터 발급 받은 인증코드. grant_type 이 `code` 일 때 만 사용. | +| refresh_token | **CONDITIONAL** | **STRING** | 이전 토큰발급 요청을 통하여 발급 받은 리프레시 토큰.grant_type 이 `refresh_token ` 일 때 만 사용. | + +Response +``` +{ + 'message': 'ok' +} +``` + +Response Description +``` +| Name | Type | Description | +| :------------- |:--------------|:--------------| +| **primaryEmail** | **STRING** | 사용자 이메일 | +``` + +## API 명세 링크 +https://github.com/boostcamp-2020/Project04-C-Whale/wiki/Task-API + +## error code +| 상태 코드 | 오류 메시지 | 설명 | +|:-----:|:------:|:-----:| +| 400 | Bad Request | 요청이 잘못된 경우 발생합니다. | +| 401 | Unauthorized | 유효한 토큰을 header에 포함하지 않은 경우 발생합니다. | +| 403 | Forbidden | 해당 리소스에 대한 권한이 없는 요청에 대해 발생합니다. | +| 404 | Not Found | 해당 id의 bookmark나 task가 존재하지 않는 경우 발생합니다. | +| 500 | Internal Server Error | 서버에 문제가 생긴 경우 발생합니다. | diff --git a/.github/ISSUE_TEMPLATE/component.md b/.github/ISSUE_TEMPLATE/component.md new file mode 100644 index 00000000..1a24cf1f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/component.md @@ -0,0 +1,18 @@ +--- +name: Component +about: Client side 컴포넌트 탬플릿 +title: 로그인 Component 구현 +labels: FE +assignees: Zinyon, pjy0416, shkilo + +--- + +## 화면 기획서 +- [ 화면 기획서 ](https://docs.google.com/presentation/d/1TJlCLFvrsmgS0QGRNYfjvJIuQG5WdjccmZ_hFBwSYX0/edit#slide=id.gac1437662e_0_0 +) +- [ 컴포넌트 설계 기획서 ](https://github.com/boostcamp-2020/Project04-C-Whale/wiki/Component-%EC%84%A4%EA%B3%84-%ED%9A%8C%EC%9D%98) + +## 체크리스트 +- [ ] A +- [ ] B +- [ ] C diff --git a/client/.env.sample b/client/.env.sample index e25b276c..b14277f9 100644 --- a/client/.env.sample +++ b/client/.env.sample @@ -1,2 +1,4 @@ -NODE_ENV = -SERVER_URL = +CLIENT_DOMAIN_DEVELOP = http://localhost:8080 +NODE_ENV = development +VUE_APP_SERVER_URL = http://localhost:3000/api + diff --git a/client/.eslintrc.js b/client/.eslintrc.js index 829a939d..ba734f57 100644 --- a/client/.eslintrc.js +++ b/client/.eslintrc.js @@ -9,7 +9,8 @@ module.exports = { }, rules: { "no-console": process.env.NODE_ENV === "production" ? "warn" : "off", - "no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off" + "no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off", + "prettier/prettier": ['error', {endOfLine: 'auto'}] }, overrides: [ { diff --git a/client/package-lock.json b/client/package-lock.json index 6de6780b..b38101e1 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -1,6 +1,6 @@ { "name": "client", - "version": "0.1.0", + "version": "0.0.1", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -2779,7 +2779,6 @@ "version": "1.0.10", "resolved": "https://registry.npm.taobao.org/argparse/download/argparse-1.0.10.tgz", "integrity": "sha1-vNZ5HqWuCXJeF+WtmIE0zUCz2RE=", - "dev": true, "requires": { "sprintf-js": "~1.0.2" } @@ -2981,6 +2980,14 @@ "integrity": "sha1-1h9G2DslGSUOJ4Ta9bCUeai0HFk=", "dev": true }, + "axios": { + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.0.tgz", + "integrity": "sha512-fmkJBknJKoZwem3/IKSSLpkdNXZeBu5Q7GA/aRsr2btgrptmSCxi2oFjZHqGdK9DoTil9PIHlPIZw2EcRJXRvw==", + "requires": { + "follow-redirects": "^1.10.0" + } + }, "babel-code-frame": { "version": "6.26.0", "resolved": "https://registry.npm.taobao.org/babel-code-frame/download/babel-code-frame-6.26.0.tgz", @@ -6747,8 +6754,7 @@ "follow-redirects": { "version": "1.13.0", "resolved": "https://registry.npm.taobao.org/follow-redirects/download/follow-redirects-1.13.0.tgz", - "integrity": "sha1-tC6Nk6Kn7qXtiGM2dtZZe8jjhNs=", - "dev": true + "integrity": "sha1-tC6Nk6Kn7qXtiGM2dtZZe8jjhNs=" }, "for-in": { "version": "1.0.2", @@ -9091,6 +9097,14 @@ "verror": "1.10.0" } }, + "katex": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.6.0.tgz", + "integrity": "sha1-EkGOCRIcBckgQbazuftrqyE8tvM=", + "requires": { + "match-at": "^0.1.0" + } + }, "killable": { "version": "1.0.1", "resolved": "https://registry.npm.taobao.org/killable/download/killable-1.0.1.tgz", @@ -9156,6 +9170,14 @@ "integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=", "dev": true }, + "linkify-it": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-1.2.4.tgz", + "integrity": "sha1-B3NSbDF8j9E71TTuHRgP+Iq/iBo=", + "requires": { + "uc.micro": "^1.0.1" + } + }, "load-json-file": { "version": "4.0.0", "resolved": "https://registry.npm.taobao.org/load-json-file/download/load-json-file-4.0.0.tgz", @@ -9418,6 +9440,99 @@ "object-visit": "^1.0.0" } }, + "markdown-it": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-6.1.1.tgz", + "integrity": "sha1-ztA39Ec+6fUVOsQU933IPJG6knw=", + "requires": { + "argparse": "^1.0.7", + "entities": "~1.1.1", + "linkify-it": "~1.2.2", + "mdurl": "~1.0.1", + "uc.micro": "^1.0.1" + }, + "dependencies": { + "entities": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", + "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==" + } + } + }, + "markdown-it-abbr": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/markdown-it-abbr/-/markdown-it-abbr-1.0.4.tgz", + "integrity": "sha1-1mtTZFIcuz3Yqlna37ovtoZcj9g=" + }, + "markdown-it-deflist": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/markdown-it-deflist/-/markdown-it-deflist-2.1.0.tgz", + "integrity": "sha512-3OuqoRUlSxJiuQYu0cWTLHNhhq2xtoSFqsZK8plANg91+RJQU1ziQ6lA2LzmFAEes18uPBsHZpcX6We5l76Nzg==" + }, + "markdown-it-emoji": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/markdown-it-emoji/-/markdown-it-emoji-1.4.0.tgz", + "integrity": "sha1-m+4OmpkKljupbfaYDE/dsF37Tcw=" + }, + "markdown-it-footnote": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/markdown-it-footnote/-/markdown-it-footnote-2.0.0.tgz", + "integrity": "sha1-FOnE9o/xLPNU+jZa43gnboEEypQ=" + }, + "markdown-it-ins": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/markdown-it-ins/-/markdown-it-ins-2.0.0.tgz", + "integrity": "sha1-papqMPHi9x6Ul1Z8/f9A8f3mdIM=" + }, + "markdown-it-katex": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/markdown-it-katex/-/markdown-it-katex-2.0.3.tgz", + "integrity": "sha1-17hqGuoLnWSW+rTnkZoY/e9YnDk=", + "requires": { + "katex": "^0.6.0" + } + }, + "markdown-it-mark": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/markdown-it-mark/-/markdown-it-mark-2.0.0.tgz", + "integrity": "sha1-RqGqlHEFrtgYiXjgoBYXnkBPQsc=" + }, + "markdown-it-sub": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/markdown-it-sub/-/markdown-it-sub-1.0.0.tgz", + "integrity": "sha1-N1/WAm6ufdywEkl/ZBEZXqHjr+g=" + }, + "markdown-it-sup": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/markdown-it-sup/-/markdown-it-sup-1.0.0.tgz", + "integrity": "sha1-y5yf+RpSVawI8/09YyhuFd8KH8M=" + }, + "markdown-it-task-lists": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/markdown-it-task-lists/-/markdown-it-task-lists-2.1.1.tgz", + "integrity": "sha512-TxFAc76Jnhb2OUu+n3yz9RMu4CwGfaT788br6HhEDlvWfdeJcLUsxk1Hgw2yJio0OXsxv7pyIPmvECY7bMbluA==" + }, + "markdown-it-toc-and-anchor": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/markdown-it-toc-and-anchor/-/markdown-it-toc-and-anchor-4.2.0.tgz", + "integrity": "sha512-DusSbKtg8CwZ92ztN7bOojDpP4h0+w7BVOPuA3PHDIaabMsERYpwsazLYSP/UlKedoQjOz21mwlai36TQ04EpA==", + "requires": { + "clone": "^2.1.0", + "uslug": "^1.0.4" + }, + "dependencies": { + "clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=" + } + } + }, + "match-at": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/match-at/-/match-at-0.1.1.tgz", + "integrity": "sha512-h4Yd392z9mST+dzc+yjuybOGFNOZjmXIPKWjxBd1Bb23r4SmDOsk2NYCU2BMUBGbSpZqwVsZYNq26QS3xfaT3Q==" + }, "md5.js": { "version": "1.3.5", "resolved": "https://registry.npm.taobao.org/md5.js/download/md5.js-1.3.5.tgz", @@ -9435,6 +9550,11 @@ "integrity": "sha1-aZs8OKxvHXKAkaZGULZdOIUC/Vs=", "dev": true }, + "mdurl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", + "integrity": "sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4=" + }, "media-typer": { "version": "0.3.0", "resolved": "https://registry.npm.taobao.org/media-typer/download/media-typer-0.3.0.tgz", @@ -13002,8 +13122,7 @@ "sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npm.taobao.org/sprintf-js/download/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", - "dev": true + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" }, "sshpk": { "version": "1.16.1", @@ -13767,6 +13886,11 @@ "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", "dev": true }, + "uc.micro": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", + "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==" + }, "uglify-js": { "version": "3.4.10", "resolved": "https://registry.npm.taobao.org/uglify-js/download/uglify-js-3.4.10.tgz", @@ -13867,6 +13991,11 @@ "integrity": "sha1-tkb2m+OULavOzJ1mOcgNwQXvqmY=", "dev": true }, + "unorm": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/unorm/-/unorm-1.6.0.tgz", + "integrity": "sha512-b2/KCUlYZUeA7JFUuRJZPUtr4gZvBh7tavtv4fvk4+KV9pfGiR6CQAQAWl49ZpR3ts2dk4FYkP7EIgDJoiOLDA==" + }, "unpipe": { "version": "1.0.0", "resolved": "https://registry.npm.taobao.org/unpipe/download/unpipe-1.0.0.tgz", @@ -13991,6 +14120,14 @@ "integrity": "sha1-1QyMrHmhn7wg8pEfVuuXP04QBw8=", "dev": true }, + "uslug": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/uslug/-/uslug-1.0.4.tgz", + "integrity": "sha1-uaIvCRTgqGFAYz2swwLl9PpFBnc=", + "requires": { + "unorm": ">= 1.0.0" + } + }, "util": { "version": "0.11.1", "resolved": "https://registry.npm.taobao.org/util/download/util-0.11.1.tgz", @@ -14186,9 +14323,9 @@ } }, "vue-loader-v16": { - "version": "npm:vue-loader@16.0.0-rc.2", - "resolved": "https://registry.npm.taobao.org/vue-loader/download/vue-loader-16.0.0-rc.2.tgz?cache=0&sync_timestamp=1605670812037&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fvue-loader%2Fdownload%2Fvue-loader-16.0.0-rc.2.tgz", - "integrity": "sha1-tqfn8w0o81ZZqD3kH0oYMaQjKgQ=", + "version": "npm:vue-loader@16.1.1", + "resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-16.1.1.tgz", + "integrity": "sha512-wz/+HFg/3SBayHWAlZXARcnDTl3VOChrfW9YnxvAweiuyKX/7IGx1ad/4yJHmwhgWlOVYMAbTiI7GV8G33PfGQ==", "dev": true, "optional": true, "requires": { @@ -14199,8 +14336,8 @@ "dependencies": { "ansi-styles": { "version": "4.3.0", - "resolved": "https://registry.npm.taobao.org/ansi-styles/download/ansi-styles-4.3.0.tgz", - "integrity": "sha1-7dgDYornHATIWuegkG7a00tkiTc=", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "optional": true, "requires": { @@ -14209,8 +14346,8 @@ }, "chalk": { "version": "4.1.0", - "resolved": "https://registry.npm.taobao.org/chalk/download/chalk-4.1.0.tgz?cache=0&sync_timestamp=1602488009163&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fchalk%2Fdownload%2Fchalk-4.1.0.tgz", - "integrity": "sha1-ThSHCmGNni7dl92DRf2dncMVZGo=", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", "dev": true, "optional": true, "requires": { @@ -14220,8 +14357,8 @@ }, "color-convert": { "version": "2.0.1", - "resolved": "https://registry.npm.taobao.org/color-convert/download/color-convert-2.0.1.tgz", - "integrity": "sha1-ctOmjVmMm9s68q0ehPIdiWq9TeM=", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, "optional": true, "requires": { @@ -14230,22 +14367,22 @@ }, "color-name": { "version": "1.1.4", - "resolved": "https://registry.npm.taobao.org/color-name/download/color-name-1.1.4.tgz", - "integrity": "sha1-wqCah6y95pVD3m9j+jmVyCbFNqI=", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true, "optional": true }, "has-flag": { "version": "4.0.0", - "resolved": "https://registry.npm.taobao.org/has-flag/download/has-flag-4.0.0.tgz", - "integrity": "sha1-lEdx/ZyByBJlxNaUGGDaBrtZR5s=", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, "optional": true }, "loader-utils": { "version": "2.0.0", - "resolved": "https://registry.npm.taobao.org/loader-utils/download/loader-utils-2.0.0.tgz?cache=0&sync_timestamp=1584445115463&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Floader-utils%2Fdownload%2Floader-utils-2.0.0.tgz", - "integrity": "sha1-5MrOW4FtQloWa18JfhDNErNgZLA=", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz", + "integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==", "dev": true, "optional": true, "requires": { @@ -14256,8 +14393,8 @@ }, "supports-color": { "version": "7.2.0", - "resolved": "https://registry.npm.taobao.org/supports-color/download/supports-color-7.2.0.tgz", - "integrity": "sha1-G33NyzK4E4gBs+R4umpRyqiWSNo=", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "optional": true, "requires": { @@ -14266,6 +14403,33 @@ } } }, + "vue-markdown": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/vue-markdown/-/vue-markdown-2.2.4.tgz", + "integrity": "sha512-hoTX/W1UIdHZrp/b0vpHSsJXAEfWsafaQLgtE2VX4gY8O/C3L2Gabqu95gyG429rL4ML1SwGv+xsPABX7yfFIQ==", + "requires": { + "highlight.js": "^9.12.0", + "markdown-it": "^6.0.1", + "markdown-it-abbr": "^1.0.3", + "markdown-it-deflist": "^2.0.1", + "markdown-it-emoji": "^1.1.1", + "markdown-it-footnote": "^2.0.0", + "markdown-it-ins": "^2.0.0", + "markdown-it-katex": "^2.0.3", + "markdown-it-mark": "^2.0.0", + "markdown-it-sub": "^1.0.0", + "markdown-it-sup": "^1.0.0", + "markdown-it-task-lists": "^2.0.1", + "markdown-it-toc-and-anchor": "^4.1.2" + }, + "dependencies": { + "highlight.js": { + "version": "9.18.5", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-9.18.5.tgz", + "integrity": "sha512-a5bFyofd/BHCX52/8i8uJkjr9DYwXIPnM/plwI6W7ezItLGqzt7X2G2nXuYSfsIJdkwwj/g9DG1LkcGJI/dDoA==" + } + } + }, "vue-router": { "version": "3.4.9", "resolved": "https://registry.npm.taobao.org/vue-router/download/vue-router-3.4.9.tgz?cache=0&sync_timestamp=1605950629198&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fvue-router%2Fdownload%2Fvue-router-3.4.9.tgz", diff --git a/client/package.json b/client/package.json index ef58ba68..3d3125c1 100644 --- a/client/package.json +++ b/client/package.json @@ -9,8 +9,10 @@ "lint": "vue-cli-service lint" }, "dependencies": { + "axios": "^0.21.0", "core-js": "^3.6.5", "vue": "^2.6.11", + "vue-markdown": "^2.2.4", "vue-router": "^3.2.0", "vuetify": "^2.2.11", "vuex": "^3.4.0" diff --git a/client/src/App.vue b/client/src/App.vue index b5321e4f..3aa2f454 100644 --- a/client/src/App.vue +++ b/client/src/App.vue @@ -1,26 +1,65 @@ - + + + + + + diff --git a/client/src/api/comment.js b/client/src/api/comment.js new file mode 100644 index 00000000..c577fc0f --- /dev/null +++ b/client/src/api/comment.js @@ -0,0 +1,18 @@ +import myAxios from "./myAxios"; + +const labelAPI = { + getAllComments(taskId) { + return myAxios.get(`/task/${taskId}/comment`); + }, + createComment(data) { + return myAxios.post(`/task/${data.taskId}/comment`, data); + }, + updateComment(comment) { + return myAxios.put(`/task/${comment.taskId}/comment/${comment.id}`, comment); + }, + deleteComment(comment) { + return myAxios.delete(`/task/${comment.taskId}/comment/${comment.id}`, comment); + }, +}; + +export default labelAPI; diff --git a/client/src/api/label.js b/client/src/api/label.js new file mode 100644 index 00000000..e0713d27 --- /dev/null +++ b/client/src/api/label.js @@ -0,0 +1,9 @@ +import myAxios from "./myAxios"; + +const labelAPI = { + getLabels() { + return myAxios.get("/label"); + }, +}; + +export default labelAPI; diff --git a/client/src/api/myAxios.js b/client/src/api/myAxios.js new file mode 100644 index 00000000..be503f09 --- /dev/null +++ b/client/src/api/myAxios.js @@ -0,0 +1,14 @@ +import axios from "axios"; + +const baseURL = process.env.VUE_APP_SERVER_URL + "/api"; +// axios intercept 전역 설정 +const myAxios = axios.create({ + baseURL, +}); + +myAxios.interceptors.request.use((config) => { + config.headers.Authorization = "Bearer " + localStorage.getItem("token"); + return config; +}); + +export default myAxios; diff --git a/client/src/components/project/index.js b/client/src/api/priority.js similarity index 100% rename from client/src/components/project/index.js rename to client/src/api/priority.js diff --git a/client/src/api/project.js b/client/src/api/project.js new file mode 100644 index 00000000..5b8e65e4 --- /dev/null +++ b/client/src/api/project.js @@ -0,0 +1,33 @@ +import myAxios from "./myAxios"; + +const projectAPI = { + getProjectById(projectId) { + return myAxios.get(`/project/${projectId}`); + }, + getProjects() { + return myAxios.get("/project"); + }, + getTodayProject() { + return myAxios.get("/project/today"); + }, + createProject(data) { + return myAxios.post("/project", data); + }, + updateProject(projectId, data) { + return myAxios.patch(`/project/${projectId}`, data); + }, + createSection(projectId, data) { + return myAxios.post(`/project/${projectId}/section`, data); + }, + updateSection(projectId, sectionId, data) { + return myAxios.put(`/project/${projectId}/section/${sectionId}`, data); + }, + updateTaskPosition(projectId, sectionId, data) { + return myAxios.patch(`/project/${projectId}/section/${sectionId}/position`, data); + }, + deleteProject(projectId) { + return myAxios.delete(`/project/${projectId}`); + }, +}; + +export default projectAPI; diff --git a/client/src/api/task.js b/client/src/api/task.js new file mode 100644 index 00000000..98c48a26 --- /dev/null +++ b/client/src/api/task.js @@ -0,0 +1,18 @@ +import myAxios from "./myAxios"; + +const taskAPI = { + createTask({ projectId, sectionId, ...data }) { + return myAxios.post(`/project/${projectId}/section/${sectionId}/task`, data); + }, + getAllTasks() { + return myAxios.get("/task"); + }, + getTaskById(taskId) { + return myAxios.get(`/task/${taskId}`); + }, + updateTask(data) { + return myAxios.patch(`/task/${data.taskId}`, data); + }, +}; + +export default taskAPI; diff --git a/client/src/api/user.js b/client/src/api/user.js index e69de29b..ba0ace8b 100644 --- a/client/src/api/user.js +++ b/client/src/api/user.js @@ -0,0 +1,9 @@ +import myAxios from "./myAxios"; + +const userAPI = { + authorize() { + return myAxios.get("/user/me"); + }, +}; + +export default userAPI; diff --git a/client/src/components/comment/CommentFormContainer.vue b/client/src/components/comment/CommentFormContainer.vue new file mode 100644 index 00000000..5917bb5a --- /dev/null +++ b/client/src/components/comment/CommentFormContainer.vue @@ -0,0 +1,41 @@ + + + + + 댓글 추가 + + + + + diff --git a/client/src/components/comment/CommentItem.vue b/client/src/components/comment/CommentItem.vue new file mode 100644 index 00000000..45f3f2dc --- /dev/null +++ b/client/src/components/comment/CommentItem.vue @@ -0,0 +1,89 @@ + + + + + + + 업데이트 + 취소 + + + + + + {{ comment.updatedAt }} + {{ comment.content }} + + + + + mdi-pencil + 수정 + + + mdi-delete + 삭제 + + + + + + diff --git a/client/src/components/comment/CommentList.vue b/client/src/components/comment/CommentList.vue new file mode 100644 index 00000000..daf21e4a --- /dev/null +++ b/client/src/components/comment/CommentList.vue @@ -0,0 +1,21 @@ + + + + + + + + + + diff --git a/client/src/components/common/Alert.vue b/client/src/components/common/Alert.vue new file mode 100644 index 00000000..3f8ac4cf --- /dev/null +++ b/client/src/components/common/Alert.vue @@ -0,0 +1,40 @@ + + + {{ alert.message }} + + + + + + diff --git a/client/src/components/common/Header.vue b/client/src/components/common/Header.vue index 2688f8f4..5ddb8636 100644 --- a/client/src/components/common/Header.vue +++ b/client/src/components/common/Header.vue @@ -1,16 +1,82 @@ - - - Login - Today - - - Project - + + + + + + + 할고래DO + + + + mdi-plus + + + + + + + + + + + + mdi-account + + + + + + 로그아웃 + + + + + + + diff --git a/client/src/components/common/Spinner.vue b/client/src/components/common/Spinner.vue new file mode 100644 index 00000000..8f4cf802 --- /dev/null +++ b/client/src/components/common/Spinner.vue @@ -0,0 +1,27 @@ + + + + + + + diff --git a/client/src/components/common/UpdatableTitle.vue b/client/src/components/common/UpdatableTitle.vue new file mode 100644 index 00000000..175a9bd0 --- /dev/null +++ b/client/src/components/common/UpdatableTitle.vue @@ -0,0 +1,89 @@ + + + + + + + + 변경 + 취소 + + + + + + {{ originalTitle }} + + + + + + + diff --git a/client/src/components/menu/FavoriteProjectList.vue b/client/src/components/menu/FavoriteProjectList.vue new file mode 100644 index 00000000..594cd336 --- /dev/null +++ b/client/src/components/menu/FavoriteProjectList.vue @@ -0,0 +1,108 @@ + + + + + mdi-inbox + + + 관리함 {{ managedProject.taskCount }} + + + + + + mdi-calendar-today + + + + 오늘 {{ taskCount }} + + + + + + mdi-circle + + + + {{ favoriteProjectInfo.title }} {{ favoriteProjectInfo.taskCount }} + + + + + + + mdi-dots-horizontal + + + + + + + + 프로젝트 수정 + + + 프로젝트 삭제 + + + + + + + + + + + + + diff --git a/client/src/components/menu/FilterList.vue b/client/src/components/menu/FilterList.vue new file mode 100644 index 00000000..cee5b7f2 --- /dev/null +++ b/client/src/components/menu/FilterList.vue @@ -0,0 +1,34 @@ + + + + + + + 필터 + + + mdi-plus + + + + + + + + mdi-flag + + + {{ priority.title }} + + + + + + + diff --git a/client/src/components/menu/LabelList.vue b/client/src/components/menu/LabelList.vue new file mode 100644 index 00000000..34f53857 --- /dev/null +++ b/client/src/components/menu/LabelList.vue @@ -0,0 +1,40 @@ + + + + + + + 라벨 + + + mdi-plus + + + + + + + + mdi-label + + + {{ label.title }} + + + + + + + + + diff --git a/client/src/components/menu/LeftMenu.vue b/client/src/components/menu/LeftMenu.vue new file mode 100644 index 00000000..37f3fae1 --- /dev/null +++ b/client/src/components/menu/LeftMenu.vue @@ -0,0 +1,59 @@ + + + + + + + + + + + + diff --git a/client/src/components/menu/ProjectListContainer.vue b/client/src/components/menu/ProjectListContainer.vue new file mode 100644 index 00000000..467854f6 --- /dev/null +++ b/client/src/components/menu/ProjectListContainer.vue @@ -0,0 +1,138 @@ + + + + + + + 프로젝트 + + + mdi-plus + + + + + + + + mdi-circle + + + + {{ project.title + }}{{ project.taskCount }} + + + + + + + mdi-dots-horizontal + + + + + + 프로젝트 수정 + + + 프로젝트 삭제 + + + + + + + mdi-plus + + + 프로젝트 추가 + + + + + + + + + + + + + diff --git a/client/src/components/project/AddProjectModal.vue b/client/src/components/project/AddProjectModal.vue new file mode 100644 index 00000000..7f72ec4a --- /dev/null +++ b/client/src/components/project/AddProjectModal.vue @@ -0,0 +1,143 @@ + + + + + 프로젝트 추가 + + + + + + + + + + + + mdi-circle + + + {{ item.text }} + + + + + + + + 즐겨찾기 + + + + + 보기 + + + + + + + + + + + 취소 + + 추가 + + + + + + + + + + diff --git a/client/src/components/project/AddSection.vue b/client/src/components/project/AddSection.vue new file mode 100644 index 00000000..8ab124d2 --- /dev/null +++ b/client/src/components/project/AddSection.vue @@ -0,0 +1,72 @@ + + + + + + + + 섹션 추가 + 취소 + + + + + + + + diff --git a/client/src/components/project/AddTask.vue b/client/src/components/project/AddTask.vue new file mode 100644 index 00000000..f0b70b93 --- /dev/null +++ b/client/src/components/project/AddTask.vue @@ -0,0 +1,162 @@ + + + + + + + + + + + + {{ task.dueDate }} + + + + + + + + + mdi-inbox + {{ projectTitle }} + + + + + + mdi-inbox + + {{ projectInfo.title }} + + + + + + + + 작업 추가 + 취소 + + + + + + + mdi-plus + 작업 추가 + + + mdi-plus + 웹사이트를 작업으로 추가 + + + + + + + + + diff --git a/client/src/components/project/DeleteProjectModal.vue b/client/src/components/project/DeleteProjectModal.vue new file mode 100644 index 00000000..ff15affa --- /dev/null +++ b/client/src/components/project/DeleteProjectModal.vue @@ -0,0 +1,56 @@ + + + + + 프로젝트 삭제 + {{ projectInfo.title }}을 삭제하시겠습니까? + + + 취소 + 삭제 + + + + + + + + + diff --git a/client/src/components/project/ProjectContainer.vue b/client/src/components/project/ProjectContainer.vue new file mode 100644 index 00000000..eac12129 --- /dev/null +++ b/client/src/components/project/ProjectContainer.vue @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + diff --git a/client/src/components/project/ProjectContainerHeader.vue b/client/src/components/project/ProjectContainerHeader.vue new file mode 100644 index 00000000..12263633 --- /dev/null +++ b/client/src/components/project/ProjectContainerHeader.vue @@ -0,0 +1,67 @@ + + + + + + + + + + + + mdi-dots-horizontal + + + + + + 섹션 추가 + + + 보기 형태: 목록 + + + 보기 형태: 보드 + + + + + + + + + + diff --git a/client/src/components/project/SectionContainer.vue b/client/src/components/project/SectionContainer.vue new file mode 100644 index 00000000..b70f55ef --- /dev/null +++ b/client/src/components/project/SectionContainer.vue @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + diff --git a/client/src/components/project/TaskItem.vue b/client/src/components/project/TaskItem.vue new file mode 100644 index 00000000..700430cb --- /dev/null +++ b/client/src/components/project/TaskItem.vue @@ -0,0 +1,122 @@ + + + + + + + + + + + + + {{ task.title }} + + + + + + + + + + + diff --git a/client/src/components/project/UpdateProjectModal.vue b/client/src/components/project/UpdateProjectModal.vue new file mode 100644 index 00000000..fb73347d --- /dev/null +++ b/client/src/components/project/UpdateProjectModal.vue @@ -0,0 +1,137 @@ + + + + + 프로젝트 추가 + + + + + + + + + + + + mdi-circle + + + {{ item.text }} + + + + + + + + 즐겨찾기 + + + + + 보기 + + + + + + + + + + + 취소 + 수정 + + + + + + + + + diff --git a/client/src/components/task/ChildTaskList.vue b/client/src/components/task/ChildTaskList.vue new file mode 100644 index 00000000..8c84c2ed --- /dev/null +++ b/client/src/components/task/ChildTaskList.vue @@ -0,0 +1,27 @@ + + + + + + + + + diff --git a/client/src/components/task/Search.vue b/client/src/components/task/Search.vue new file mode 100644 index 00000000..09b9a12a --- /dev/null +++ b/client/src/components/task/Search.vue @@ -0,0 +1,94 @@ + + + + + + + + + {{ task.title }} + + + + + + + diff --git a/client/src/components/task/TaskDetailContainer.vue b/client/src/components/task/TaskDetailContainer.vue new file mode 100644 index 00000000..07f28e50 --- /dev/null +++ b/client/src/components/task/TaskDetailContainer.vue @@ -0,0 +1,68 @@ + + + + + + {{ projectTitle }} + + + mdi-close + + + + + + + + + + + + + + diff --git a/client/src/components/task/TaskDetailTabs.vue b/client/src/components/task/TaskDetailTabs.vue new file mode 100644 index 00000000..561fbddb --- /dev/null +++ b/client/src/components/task/TaskDetailTabs.vue @@ -0,0 +1,58 @@ + + + + {{ tab.title }} {{ tab.count }} + + + + + + + + + + + + 하이 북마크 + + + + + + + diff --git a/client/src/components/task/index.js b/client/src/components/task/index.js deleted file mode 100644 index e69de29b..00000000 diff --git a/client/src/components/today/TodayTasksContainer.vue b/client/src/components/today/TodayTasksContainer.vue new file mode 100644 index 00000000..b8e366b7 --- /dev/null +++ b/client/src/components/today/TodayTasksContainer.vue @@ -0,0 +1,58 @@ + + + + 기한이 지난 + + + + + + + + + + + + + + + 오늘 + + + + + + + + + + + + + + + + + + diff --git a/client/src/mixins/ListMixins.js b/client/src/mixins/ListMixins.js new file mode 100644 index 00000000..ab331177 --- /dev/null +++ b/client/src/mixins/ListMixins.js @@ -0,0 +1,7 @@ +import bus from "@/utils/bus.js"; + +export default { + mounted() { + bus.$emit("end:spinner"); + }, +}; diff --git a/client/src/plugins/vuetify.js b/client/src/plugins/vuetify.js index 2d5a28c6..0ead1cfa 100644 --- a/client/src/plugins/vuetify.js +++ b/client/src/plugins/vuetify.js @@ -7,8 +7,8 @@ export default new Vuetify({ theme: { themes: { light: { - whaleGreen: "#b7e1cd", - whaleBlue: "#161c70", + whaleGreen: "#07C4A3", + whaleBlue: "#1C2B82", }, }, }, diff --git a/client/src/router/index.js b/client/src/router/index.js index c50bfd2f..8f83538b 100644 --- a/client/src/router/index.js +++ b/client/src/router/index.js @@ -1,18 +1,39 @@ import Vue from "vue"; import VueRouter from "vue-router"; -import Login from "../views/Login.vue"; -import Today from "../views/Today.vue"; -import Project from "../views/Project.vue"; -import Task from "../views/Task.vue"; -import Home from "../views/Home.vue"; +import Login from "@/views/Login.vue"; +import Today from "@/views/Today.vue"; +import Project from "@/views/Project.vue"; +import Home from "@/views/Home.vue"; +import Task from "@/views/Task"; +import userAPI from "@/api/user"; +import bus from "@/utils/bus.js"; Vue.use(VueRouter); +const requireAuth = () => (from, to, next) => { + bus.$emit("start:spinner"); + if (localStorage.getItem("token")) { + return next(); + } + return next("/login"); +}; + +const redirectHome = () => async (from, to, next) => { + bus.$emit("start:spinner"); + try { + await userAPI.authorize(); + return next("/"); + } catch (err) { + return next(); + } +}; + const routes = [ { path: "/login", name: "Login", component: Login, + beforeEnter: redirectHome(), }, { path: "/", @@ -23,16 +44,27 @@ const routes = [ path: "today", name: "Today", component: Today, + beforeEnter: requireAuth(), + children: [ + { + path: "task/:taskId", + name: "TodayTaskDetail", + component: Task, + }, + ], }, { path: "project/:projectId", name: "Project", component: Project, - }, - { - path: "task/:taskId", - name: "Task", - component: Task, + beforeEnter: requireAuth(), + children: [ + { + path: "task/:taskId", + name: "ProjectTaskDetail", + component: Task, + }, + ], }, ], }, diff --git a/client/src/store/alert.js b/client/src/store/alert.js new file mode 100644 index 00000000..bea319fe --- /dev/null +++ b/client/src/store/alert.js @@ -0,0 +1,37 @@ +import router from "@/router"; + +const state = { + alert: { + message: "", + type: "", + }, +}; + +const mutations = { + SET_ERROR_ALERT(state, { data, status }) { + if (status === 401) { + state.alert = { message: "세션이 만료되었습니다", type: "error" }; + router.replace("/login").catch(() => {}); + return; + } else { + state.alert = { message: data.message, type: "error" }; + } + }, + CLEAR_ALERT(state) { + state.alert = { message: "", type: "" }; + }, + SET_SUCCESS_ALERT(state, message) { + state.alert = { message, type: "success" }; + }, +}; + +const getters = {}; + +const actions = {}; + +export default { + state, + mutations, + getters, + actions, +}; diff --git a/client/src/store/auth.js b/client/src/store/auth.js new file mode 100644 index 00000000..56587945 --- /dev/null +++ b/client/src/store/auth.js @@ -0,0 +1,58 @@ +import router from "@/router/index.js"; +import userAPI from "@/api/user.js"; + +const state = { + user: { + id: "", + name: "", + email: "", + }, + isAuth: false, +}; + +const mutations = { + SET_USER(state, user) { + state.user = { + id: user.id, + name: user.name, + email: user.email, + }; + state.isAuth = true; + }, + LOGOUT() { + localStorage.removeItem("token"); + location.replace("/login"); + return; + }, +}; + +const actions = { + async checkUser({ commit }) { + try { + const { data: user } = await userAPI.authorize(); + commit("SET_USER", user); + + if (location.pathname === "/" || location.pathname === "/login") { + router.replace("/today").catch(() => {}); + return; + } else { + router.replace(location.pathname).catch(() => {}); + return; + } + } catch (err) { + commit("SET_ERROR_ALERT", err.response); + router.replace("/login").catch(() => {}); + return; + } + }, + + logout({ commit }) { + commit("LOGOUT"); + }, +}; + +export default { + state, + actions, + mutations, +}; diff --git a/client/src/store/comment.js b/client/src/store/comment.js new file mode 100644 index 00000000..a91a079e --- /dev/null +++ b/client/src/store/comment.js @@ -0,0 +1,74 @@ +import commentAPI from "@/api/comment"; + +const SUCCESS_MESSAGE = "ok"; + +const state = { + comments: [], + commentCounts: 0, +}; + +const getters = { + comments: (state) => state.comments, + commentCounts: (state) => state.comments.length, +}; + +const actions = { + async fetchComments({ commit }, taskId) { + try { + const { data: comments } = await commentAPI.getAllComments(taskId); + comments.sort((comment1, comment2) => (comment1.createdAt > comment2.createdAt ? 1 : -1)); + commit("SET_COMMENTS", comments); + commit("SET_COMMENT_COUNTS", comments.length); + } catch (err) { + commit("SET_ERROR_ALERT", err.response); + } + }, + async addComment({ commit, dispatch }, comment) { + try { + const { data } = await commentAPI.createComment(comment); + if (data.message !== SUCCESS_MESSAGE) { + throw new Error(); + } + + await dispatch("fetchComments", comment.taskId); + } catch (err) { + commit("SET_ERROR_ALERT", err.response); + } + }, + + async updateComment({ commit, dispatch }, comment) { + try { + const { data } = await commentAPI.updateComment(comment); + if (data.message !== SUCCESS_MESSAGE) { + throw Error; + } + await dispatch("fetchComments", comment.taskId); + } catch (err) { + commit("SET_ERROR_ALERT", err.response); + } + }, + async deleteComment({ commit, dispatch }, comment) { + try { + const { data } = await commentAPI.deleteComment(comment); + if (data.message !== SUCCESS_MESSAGE) { + throw Error; + } + + await dispatch("fetchComments", comment.taskId); + } catch (err) { + commit("SET_ERROR_ALERT", err.response); + } + }, +}; + +const mutations = { + SET_COMMENTS: (state, comments) => (state.comments = comments), + SET_COMMENT_COUNTS: (state, counts) => (state.commentCounts = counts), +}; + +export default { + state, + getters, + actions, + mutations, +}; diff --git a/client/src/store/dragAndDrop.js b/client/src/store/dragAndDrop.js new file mode 100644 index 00000000..0224aa46 --- /dev/null +++ b/client/src/store/dragAndDrop.js @@ -0,0 +1,27 @@ +const state = { + draggingTask: {}, + dropTargetSection: {}, +}; + +const getters = { + draggingTask: (state) => state.draggingTask, + dropTargetSection: (state) => state.dropTargetSection, +}; + +const actions = {}; + +const mutations = { + SET_DRAGGING_TASK: (state, task) => { + state.draggingTask = task; + }, + SET_DROP_TARGET_SECTION: (state, newDropTargetSection) => { + state.dropTargetSection = newDropTargetSection; + }, +}; + +export default { + state, + getters, + actions, + mutations, +}; diff --git a/client/src/store/index.js b/client/src/store/index.js index 6fd67fcc..48c1d3ec 100644 --- a/client/src/store/index.js +++ b/client/src/store/index.js @@ -1,22 +1,16 @@ import Vue from "vue"; import Vuex from "vuex"; +import auth from "./auth"; +import project from "./project"; +import label from "./label"; +import priority from "./priority"; +import task from "./task"; +import alert from "./alert"; +import dragAndDrop from "./dragAndDrop"; +import comment from "./comment"; Vue.use(Vuex); export default new Vuex.Store({ - state: { - accessToken: null, - }, - mutations: { - LOGIN(state, { accessToken }) { - state.accessToken = accessToken; - localStorage.setItem("token", accessToken); - }, - LOGOUT(state) { - state.accessToken = null; - localStorage.removeItem("token"); - }, - }, - actions: {}, - modules: {}, + modules: { auth, project, label, priority, task, alert, dragAndDrop, comment }, }); diff --git a/client/src/store/label.js b/client/src/store/label.js new file mode 100644 index 00000000..c76e2b06 --- /dev/null +++ b/client/src/store/label.js @@ -0,0 +1,33 @@ +import labelAPI from "@/api/label"; + +const state = { + newLabel: {}, + labels: [], +}; + +const getters = { + labels: (state) => state.labels, +}; + +const mutations = { + SET_LABELS: (state, labels) => (state.labels = labels), +}; + +const actions = { + async fetchLabels({ commit }) { + try { + const { data: labels } = await labelAPI.getLabels(); + + commit("SET_LABELS", labels); + } catch (err) { + commit("SET_ERROR_ALERT", err.response); + } + }, +}; + +export default { + state, + getters, + actions, + mutations, +}; diff --git a/client/src/store/priority.js b/client/src/store/priority.js new file mode 100644 index 00000000..aca8a310 --- /dev/null +++ b/client/src/store/priority.js @@ -0,0 +1,46 @@ +const state = { + priorities: [], +}; + +const getters = { + priorities: (state) => state.priorities, +}; + +const mutations = { + SET_PRIORITIES: (state, priorities) => (state.priorities = priorities), +}; + +const actions = { + async fetchPriorities({ commit }) { + try { + const priorities = [ + { + id: "c3bb8b39-cdad-4db4-ac02-ae506d30ba2a", + title: "우선순위1", + }, + { + id: "248d7dd6-9f9b-4bff-b47d-43a8b07c9093", + title: "우선순위2", + }, + { + id: "ac7ac13c-53df-49a3-8617-654e23f3d043", + title: "우선순위3", + }, + { + id: "936f1c1d-e169-47c4-b544-1f8a0aff0a8d", + title: "우선순위4", + }, + ]; + commit("SET_PRIORITIES", priorities); + } catch (err) { + commit("SET_ERROR_ALERT", err.response); + } + }, +}; + +export default { + state, + getters, + actions, + mutations, +}; diff --git a/client/src/store/project.js b/client/src/store/project.js new file mode 100644 index 00000000..66f55d40 --- /dev/null +++ b/client/src/store/project.js @@ -0,0 +1,213 @@ +import projectAPI from "../api/project"; +import taskAPI from "../api/task"; +import router from "@/router"; + +const state = { + currentProject: { + id: "", + title: "", + isList: null, + sections: [], + }, + projectInfos: [], + projectList: {}, +}; + +const getters = { + currentProject: (state) => state.currentProject, + todayProject: (state) => state.todayProject, + projectInfos: (state) => state.projectInfos, + namedProjectInfos: (state) => + state.projectInfos.filter((project) => project.title !== "관리함" && !project.isFavorite), + managedProject: (state) => state.projectInfos.find((project) => project.title === "관리함"), + favoriteProjectInfos: (state) => state.projectInfos.filter((project) => project.isFavorite), + projectList: (state) => state.projectList, +}; + +const actions = { + async fetchCurrentProject({ commit }, projectId) { + try { + const { data: project } = await projectAPI.getProjectById(projectId); + + commit("SET_CURRENT_PROJECT", project); + } catch (err) { + commit("SET_ERROR_ALERT", err.response); + } + }, + + async fetchTodayProject({ commit }) { + try { + const { data: todayProject } = await projectAPI.getTodayProject(); + + commit("SET_TODAY_PROJECT", todayProject); + } catch (err) { + commit("SET_ERROR_ALERT", err.response); + } + }, + + async updateProjectTitle({ dispatch, commit }, { projectId, title }) { + try { + const { data } = await projectAPI.updateProject(projectId, { title }); + + if (data.message !== "ok") { + throw new Error(); + } + + await dispatch("fetchCurrentProject", projectId); + await dispatch("fetchAllTasks"); + } catch (err) { + commit("SET_ERROR_ALERT", err.response); + } + }, + async updateProject({ dispatch, commit }, { projectId, data }) { + try { + await projectAPI.updateProject(projectId, data); + + await dispatch("fetchProjectInfos"); + commit("SET_SUCCESS_ALERT", "프로젝트가 수정되었습니다."); + } catch (err) { + commit("SET_ERROR_ALERT", err.response); + } + }, + async deleteProject({ dispatch, commit }, { projectId }) { + try { + await projectAPI.deleteProject(projectId); + await dispatch("fetchProjectInfos"); + commit("SET_SUCCESS_ALERT", "프로젝트가 삭제되었습니다."); + } catch (err) { + commit("SET_ERROR_ALERT", err.response); + } + }, + async addSection({ dispatch, commit }, { projectId, section }) { + try { + const { data } = await projectAPI.createSection(projectId, { + title: section.title, + }); + + if (data.message !== "ok") { + throw new Error(); + } + + await dispatch("fetchCurrentProject", projectId); + } catch (err) { + commit("SET_ERROR_ALERT", err.response); + } + }, + + async updateSectionTitle({ dispatch, commit }, { projectId, sectionId, title }) { + try { + const { data } = await projectAPI.updateSection(projectId, sectionId, { title }); + + if (data.message !== "ok") { + throw new Error(); + } + + await dispatch("fetchCurrentProject", projectId); + await dispatch("fetchAllTasks"); + } catch (err) { + commit("SET_ERROR_ALERT", err.response); + } + }, + + async updateTaskToDone({ dispatch, commit }, { projectId, taskId }) { + try { + const { data } = await taskAPI.updateTask(taskId, { isDone: true }); + + if (data.message !== "ok") { + throw new Error(); + } + + await dispatch("fetchCurrentProject", projectId); + await dispatch("fetchAllTasks"); + } catch (err) { + commit("SET_ERROR_ALERT", err.response); + } + }, + + async addTask({ dispatch, commit }, task) { + try { + const { data } = await taskAPI.createTask(task); + + if (data.message !== "ok") { + throw new Error(); + } + + await dispatch("fetchCurrentProject", task.projectId); + await dispatch("fetchAllTasks"); + commit("ADD_TASK_COUNT", task.projectId); + } catch (err) { + commit("SET_ERROR_ALERT", err.response); + } + }, + + async fetchProjectInfos({ commit }) { + try { + const { data: projectInfos } = await projectAPI.getProjects(); + + commit("SET_PROJECT_INFOS", projectInfos); + } catch (err) { + commit("SET_ERROR_ALERT", err.response); + // alert("프로젝트 전체 정보 조회 요청 실패"); + } + }, + async addProject({ dispatch, commit }, data) { + try { + const response = await projectAPI.createProject(data); + await dispatch("fetchProjectInfos"); + + commit("SET_SUCCESS_ALERT", "프로젝트가 생성되었습니다."); + router.push("/project/" + response.data.projectId); + } catch (err) { + commit("SET_ERROR_ALERT", err.response); + } + }, + async changeTaskPosition({ rootState, dispatch }, { orderedTasks }) { + const { draggingTask, dropTargetSection } = rootState.dragAndDrop; + + try { + await taskAPI.updateTask(draggingTask.id, { + sectionId: dropTargetSection.id, + }); + + const { data } = await projectAPI.updateTaskPosition( + dropTargetSection.projectId, + dropTargetSection.id, + { + orderedTasks, + } + ); + + if (data.message !== "ok") { + throw new Error(); + } + } catch (err) { + alert("위치 변경 실패"); + } + + await dispatch("fetchCurrentProject", dropTargetSection.projectId); + await dispatch("fetchAllTasks"); + }, +}; + +const mutations = { + SET_CURRENT_PROJECT: (state, currentProject) => { + const newlyFetchedProject = {}; + newlyFetchedProject[currentProject.id] = currentProject; + state.projectList = { ...state.projectList, ...newlyFetchedProject }; + state.currentProject = currentProject; + }, + SET_PROJECT_INFOS: (state, projectInfos) => (state.projectInfos = projectInfos), + SET_TODAY_PROJECT: (state, todayProject) => (state.todayProject = todayProject), + ADD_TASK_COUNT: (state, projectId) => { + const copyed = [...state.projectInfos]; + copyed.find((projectInfo) => projectInfo.id === projectId).taskCount += 1; + state.projectInfos = [...copyed]; + }, +}; + +export default { + state, + getters, + actions, + mutations, +}; diff --git a/client/src/store/task.js b/client/src/store/task.js new file mode 100644 index 00000000..99e02fe9 --- /dev/null +++ b/client/src/store/task.js @@ -0,0 +1,64 @@ +import taskAPI from "../api/task"; +import { isToday } from "@/utils/date"; + +const SUCCESS_MESSAGE = "ok"; + +const state = { + newTask: {}, + tasks: [], + currentTask: {}, +}; + +const getters = { + currentTask: (state) => state.currentTask, + todayTasks: (state) => state.tasks.filter((task) => isToday(task.dueDate)), + expiredTasks: (state) => state.tasks.filter((task) => !isToday(task.dueDate)), + taskCount: (state) => { + return state.tasks.reduce((acc, task) => acc + task.tasks.length, state.tasks.length); + }, +}; + +const actions = { + async fetchAllTasks({ commit }) { + try { + const { data } = await taskAPI.getAllTasks(); + commit("SET_TASKS", data.tasks); + } catch (err) { + commit("SET_ERROR_ALERT", err.response); + } + }, + startDragTask({ commit }, { task }) { + commit("SET_DRAGGING_TASK", task); + }, + async fetchCurrentTask({ commit }, taskId) { + try { + const { data: task } = await taskAPI.getTaskById(taskId); + commit("SET_CURRENT_TASK", task); + } catch (err) { + commit("SET_ERROR_ALERT", err.response); + } + }, + async updateTask({ commit, dispatch }, task) { + try { + const { data } = await taskAPI.updateTask(task); + if (data.message !== SUCCESS_MESSAGE) { + throw Error; + } + dispatch("fetchAllTasks"); + } catch (err) { + commit("SET_ERROR_ALERT", err.response); + } + }, +}; + +const mutations = { + SET_TASKS: (state, tasks) => (state.tasks = tasks), + SET_CURRENT_TASK: (state, currentTask) => (state.currentTask = currentTask), +}; + +export default { + state, + getters, + actions, + mutations, +}; diff --git a/client/src/utils/bus.js b/client/src/utils/bus.js new file mode 100644 index 00000000..968c96ba --- /dev/null +++ b/client/src/utils/bus.js @@ -0,0 +1,2 @@ +import Vue from "vue"; +export default new Vue(); diff --git a/client/src/utils/color.js b/client/src/utils/color.js new file mode 100644 index 00000000..110da694 --- /dev/null +++ b/client/src/utils/color.js @@ -0,0 +1,10 @@ +const colors = [ + { value: "#FFA7A7", text: "레드" }, + { value: "#FFE08C", text: "옐로우" }, + { value: "#B7F0B1", text: "그린" }, + { value: "#B2CCFF", text: "라이트블루" }, + { value: "#FFB2D9", text: "핑크" }, + { value: "#BDBDBD", text: "그레이" }, +]; + +export { colors }; diff --git a/client/src/utils/date.js b/client/src/utils/date.js new file mode 100644 index 00000000..fbae6b98 --- /dev/null +++ b/client/src/utils/date.js @@ -0,0 +1,22 @@ +const isToday = (inputDate) => { + const today = new Date(Date.now()); + + const targetDate = new Date(inputDate); + + return ( + today.getFullYear() === targetDate.getFullYear() && + today.getMonth() === targetDate.getMonth() && + today.getDate() === targetDate.getDate() + ); +}; + +const getTodayString = () => { + const today = new Date(); + const dd = String(today.getDate()).padStart(2, "0"); + const mm = String(today.getMonth() + 1).padStart(2, "0"); //January is 0! + const yyyy = today.getFullYear(); + + return `${yyyy}-${mm}-${dd}`; +}; + +export { isToday, getTodayString }; diff --git a/client/src/utils/whaleApi/index.js b/client/src/utils/whaleApi/index.js new file mode 100644 index 00000000..a62d0772 --- /dev/null +++ b/client/src/utils/whaleApi/index.js @@ -0,0 +1,8 @@ +const extensionId = process.env.VUE_APP_EXTENSION_ID; +const port = whale.runtime.connect(extensionId, { name: `greetings` }); + +const getCurrentTabUrl = (cb) => { + whale.runtime.sendMessage(extensionId, "getCurrentTabUrl", cb); +}; + +export default { test, getCurrentTabUrl }; diff --git a/client/src/views/Home.vue b/client/src/views/Home.vue index 48a66853..db1a260b 100644 --- a/client/src/views/Home.vue +++ b/client/src/views/Home.vue @@ -1,21 +1,43 @@ - - menuNav - Header - + + - - - - - + + + + + + + + diff --git a/client/src/views/Login.vue b/client/src/views/Login.vue index 4b412be5..a19caaa0 100644 --- a/client/src/views/Login.vue +++ b/client/src/views/Login.vue @@ -3,22 +3,28 @@ - + + diff --git a/client/src/views/Project.vue b/client/src/views/Project.vue index 836fd6e5..9dcf308b 100644 --- a/client/src/views/Project.vue +++ b/client/src/views/Project.vue @@ -1,11 +1,32 @@ - Project View + + diff --git a/client/src/views/Task.vue b/client/src/views/Task.vue index bd219277..1e3331f9 100644 --- a/client/src/views/Task.vue +++ b/client/src/views/Task.vue @@ -1,11 +1,57 @@ - - Task View - + + + + 데이터를 불러오는 중입니다 + + + + diff --git a/client/src/views/Today.vue b/client/src/views/Today.vue index 71fa24ec..3244d58c 100644 --- a/client/src/views/Today.vue +++ b/client/src/views/Today.vue @@ -1,11 +1,18 @@ - - Today View - + diff --git a/client/vue.config.js b/client/vue.config.js index 828c5f66..599ccadf 100644 --- a/client/vue.config.js +++ b/client/vue.config.js @@ -1,9 +1,4 @@ module.exports = { transpileDependencies: ["vuetify"], - chainWebpack: (config) => { - config.module.rule('eslint').use('eslint-loader').tap((options) => { - options.fix = true; - return options; - }) - } + lintOnSave: false, }; diff --git a/iOS/HalgoraeDO.xcodeproj/project.pbxproj b/iOS/HalgoraeDO.xcodeproj/project.pbxproj index 072ae67a..640d4690 100644 --- a/iOS/HalgoraeDO.xcodeproj/project.pbxproj +++ b/iOS/HalgoraeDO.xcodeproj/project.pbxproj @@ -20,6 +20,10 @@ 4C3F5700256CF8A7006D7C9F /* TaskContentConfigure.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3F56FF256CF8A7006D7C9F /* TaskContentConfigure.swift */; }; 4C3F5705256CF8B9006D7C9F /* TaskContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3F5704256CF8B9006D7C9F /* TaskContentView.swift */; }; 4C3F573F256D2263006D7C9F /* RoundButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3F573E256D2263006D7C9F /* RoundButton.swift */; }; + 4C42361B257162C90068A252 /* TaskListModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3F567D256B8CB3006D7C9F /* TaskListModels.swift */; }; + 4C42361E257162F10068A252 /* Priority.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CCD3256256F8BA700A46D10 /* Priority.swift */; }; + 4C4236AD2573B4640068A252 /* PopoverViewModelType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CCD324D256F84BD00A46D10 /* PopoverViewModelType.swift */; }; + 4C4236B12573B4C90068A252 /* Priority+PopoverViewModelType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C4236B02573B4C90068A252 /* Priority+PopoverViewModelType.swift */; }; 4CCD31E8256EBC0C00A46D10 /* TaskListWorkerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0CA5F0256E9AF100E66045 /* TaskListWorkerTests.swift */; }; 4CCD31E9256EBC0C00A46D10 /* TaskListInteractorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0CA60C256EAA7300E66045 /* TaskListInteractorTests.swift */; }; 4CCD31EA256EBC0C00A46D10 /* TaskListPresenterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0CA629256EABF700E66045 /* TaskListPresenterTests.swift */; }; @@ -71,10 +75,11 @@ 4C3F56FF256CF8A7006D7C9F /* TaskContentConfigure.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskContentConfigure.swift; sourceTree = ""; }; 4C3F5704256CF8B9006D7C9F /* TaskContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskContentView.swift; sourceTree = ""; }; 4C3F573E256D2263006D7C9F /* RoundButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoundButton.swift; sourceTree = ""; }; + 4C4236A425739BFC0068A252 /* TaskListTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; name = TaskListTests.xctest; path = "/Users/os/woong/iOS/Boostcamp2020/group_final/Project04-C-Whale/iOS/build/Debug-iphoneos/TaskListTests.xctest"; sourceTree = ""; }; + 4C4236B02573B4C90068A252 /* Priority+PopoverViewModelType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Priority+PopoverViewModelType.swift"; sourceTree = ""; }; 4CCD31DD256EBC0000A46D10 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 4CCD31F8256EBC8000A46D10 /* TaskListDisplaySpy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskListDisplaySpy.swift; sourceTree = ""; }; 4CCD31FE256EBCEF00A46D10 /* TaskListPresenterSpy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskListPresenterSpy.swift; sourceTree = ""; }; - 4CCD3242256F83BF00A46D10 /* TaskListTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = TaskListTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 4CCD3247256F847800A46D10 /* PopoverViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopoverViewController.swift; sourceTree = ""; }; 4CCD324D256F84BD00A46D10 /* PopoverViewModelType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopoverViewModelType.swift; sourceTree = ""; }; 4CCD3256256F8BA700A46D10 /* Priority.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Priority.swift; sourceTree = ""; }; @@ -157,6 +162,7 @@ children = ( 4C3F5694256BA228006D7C9F /* Task.swift */, 4CCD3256256F8BA700A46D10 /* Priority.swift */, + 4C4236B02573B4C90068A252 /* Priority+PopoverViewModelType.swift */, ); path = Models; sourceTree = ""; @@ -221,7 +227,6 @@ AAEE20DC2564E4A900668FAD /* HalgoraeDO */, 4C3F5657256B80B4006D7C9F /* HalgoraeDO.app */, 4CCD31DA256EBC0000A46D10 /* TaskListTests */, - 4CCD3242256F83BF00A46D10 /* TaskListTests.xctest */, ); sourceTree = ""; }; @@ -292,7 +297,7 @@ ); name = TaskListTests; productName = TaskListTests; - productReference = 4CCD3242256F83BF00A46D10 /* TaskListTests.xctest */; + productReference = 4C4236A425739BFC0068A252 /* TaskListTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; AAEE20D92564E4A900668FAD /* HalgoraeDO */ = { @@ -374,6 +379,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 4C42361B257162C90068A252 /* TaskListModels.swift in Sources */, 4CCD3205256EBD3A00A46D10 /* Task.swift in Sources */, 4CCD320E256EBD4600A46D10 /* TaskListInteractor.swift in Sources */, 4CCD3208256EBD3E00A46D10 /* TaskListWorker.swift in Sources */, @@ -382,8 +388,10 @@ 4CCD31FF256EBCEF00A46D10 /* TaskListPresenterSpy.swift in Sources */, 4CCD31E9256EBC0C00A46D10 /* TaskListInteractorTests.swift in Sources */, 4CCD31EA256EBC0C00A46D10 /* TaskListPresenterTests.swift in Sources */, + 4C4236AD2573B4640068A252 /* PopoverViewModelType.swift in Sources */, 4CCD31E8256EBC0C00A46D10 /* TaskListWorkerTests.swift in Sources */, 4CCD31F9256EBC8000A46D10 /* TaskListDisplaySpy.swift in Sources */, + 4C42361E257162F10068A252 /* Priority.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -403,6 +411,7 @@ AA354C2A2570002600072657 /* TaskAddViewController.swift in Sources */, 4CCD32772570A4EF00A46D10 /* MenuViewController.swift in Sources */, 4C0CA636256EAC5200E66045 /* TaskListDisplayLogic.swift in Sources */, + 4C4236B12573B4C90068A252 /* Priority+PopoverViewModelType.swift in Sources */, AAEE20E02564E4A900668FAD /* SceneDelegate.swift in Sources */, 4C3F5683256B8F4B006D7C9F /* TaskListRouter.swift in Sources */, 4C3F566A256B82F2006D7C9F /* TaskBoardViewController.swift in Sources */, diff --git a/iOS/HalgoraeDO.xcodeproj/project.xcworkspace/xcuserdata/os.xcuserdatad/UserInterfaceState.xcuserstate b/iOS/HalgoraeDO.xcodeproj/project.xcworkspace/xcuserdata/os.xcuserdatad/UserInterfaceState.xcuserstate index 4c7eaba2..d2669b1d 100644 Binary files a/iOS/HalgoraeDO.xcodeproj/project.xcworkspace/xcuserdata/os.xcuserdatad/UserInterfaceState.xcuserstate and b/iOS/HalgoraeDO.xcodeproj/project.xcworkspace/xcuserdata/os.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/iOS/HalgoraeDO/Sources/Models/Priority+PopoverViewModelType.swift b/iOS/HalgoraeDO/Sources/Models/Priority+PopoverViewModelType.swift new file mode 100644 index 00000000..174a9618 --- /dev/null +++ b/iOS/HalgoraeDO/Sources/Models/Priority+PopoverViewModelType.swift @@ -0,0 +1,37 @@ +// +// Priority+ViewModelType.swift +// HalgoraeDO +// +// Created by woong on 2020/11/29. +// + +import UIKit + +// MARK: - For PopoverViewModel + +extension Priority { + struct ViewModel: PopoverViewModelType { + var title: String + var tintColor: UIColor? + var image: UIImage? + } + + var color: UIColor { + switch self { + case .one: return .red + case .two: return .blue + case .three: return .orange + case .four: return .black + } + } + + var viewModel: ViewModel { + let image = UIImage(systemName: "flag.fill")?.scaled(to: .init(width: 30, height: 30)) + switch self { + case .one: return ViewModel(title: title, tintColor: color, image: image) + case .two: return ViewModel(title: title, tintColor: color, image: image) + case .three: return ViewModel(title: title, tintColor: color, image: image) + case .four: return ViewModel(title: title, tintColor: color, image: image) + } + } +} diff --git a/iOS/HalgoraeDO/Sources/Models/Priority.swift b/iOS/HalgoraeDO/Sources/Models/Priority.swift index 22152295..dfbe3488 100644 --- a/iOS/HalgoraeDO/Sources/Models/Priority.swift +++ b/iOS/HalgoraeDO/Sources/Models/Priority.swift @@ -5,7 +5,7 @@ // Created by woong on 2020/11/26. // -import UIKit +import Foundation enum Priority: Int, CaseIterable { case one = 1 @@ -22,23 +22,3 @@ enum Priority: Int, CaseIterable { } } } - -// MARK: - For PopoverViewModel - -extension Priority { - struct ViewModel: PopoverViewModelType { - var title: String - var tintColor: UIColor? - var image: UIImage? - } - - var viewModel: ViewModel { - let image = UIImage(systemName: "flag.fill")?.scaled(to: .init(width: 30, height: 30)) - switch self { - case .one: return ViewModel(title: title, tintColor: .red, image: image) - case .two: return ViewModel(title: title, tintColor: .blue, image: image) - case .three: return ViewModel(title: title, tintColor: .orange, image: image) - case .four: return ViewModel(title: title, tintColor: .black, image: image) - } - } -} diff --git a/iOS/HalgoraeDO/Sources/Models/Task.swift b/iOS/HalgoraeDO/Sources/Models/Task.swift index 8cd27cba..b4fc53c6 100644 --- a/iOS/HalgoraeDO/Sources/Models/Task.swift +++ b/iOS/HalgoraeDO/Sources/Models/Task.swift @@ -15,22 +15,22 @@ class Task { var section: String var title: String var isCompleted: Bool - var depth: Int + var priority: Priority weak var parent: Task? private(set) var subTasks: [Task] init(section: String = "", title: String, isCompleted: Bool = false, - depth: Int = 0, + priority: Priority = .four, parent: Task? = nil, subTasks: [Task] = []) { self.section = section self.title = title self.isCompleted = isCompleted - self.depth = depth self.parent = parent + self.priority = priority self.subTasks = subTasks self.subTasks.forEach { $0.parent = self } } @@ -42,13 +42,11 @@ class Task { func insert(_ task: Task, at index: Int) { assert(!(0..! = nil + private var dataSource: UICollectionViewDiffableDataSource! = nil private var lineView: UIView = UIView() private var startIndex: IndexPath? private var startPoint: CGPoint? @@ -35,7 +37,7 @@ class TaskBoardViewController: UIViewController { override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) - interactor?.fetchTasks() + interactor?.fetchTasks(request: .init(showCompleted: false)) } // MARK: - Initialize @@ -115,22 +117,14 @@ class TaskBoardViewController: UIViewController { // MARK: - TaskList Display Logic extension TaskBoardViewController: TaskListDisplayLogic { - func displayDetail(of task: Task) { - - } - - func set(editingMode: Bool) { - + func displayFetchTasks(viewModel: TaskListModels.FetchTasks.ViewModel) { + let snapShot = snapshot(taskItems: viewModel.displayedTasks) + dataSource.apply(snapShot, animatingDifferences: false) } - func display(numberOfSelectedTasks count: Int) { + func displayDetail(of task: Task) { } - - func display(tasks: [Task]) { - let snapShot = snapshot(taskItems: tasks) - dataSource.apply(snapShot, animatingDifferences: false) - } } // MARK: - Configure CollectionView Layout @@ -174,9 +168,9 @@ private extension TaskBoardViewController { private extension TaskBoardViewController { private func configureDataSource() { - let cellRegistration = UICollectionView.CellRegistration { [weak self] (cell, indexPath, taskItem) in + let cellRegistration = UICollectionView.CellRegistration { [weak self] (cell, indexPath, taskItem) in - cell.task = taskItem + cell.taskViewModel = taskItem cell.finishHandler = { [weak self] task in guard let self = self, let task = task @@ -203,19 +197,19 @@ private extension TaskBoardViewController { cell.backgroundConfiguration = background } - self.dataSource = UICollectionViewDiffableDataSource(collectionView: taskBoardCollectionView, cellProvider: { (collectionview, indexPath, task) -> UICollectionViewCell? in + self.dataSource = UICollectionViewDiffableDataSource(collectionView: taskBoardCollectionView, cellProvider: { (collectionview, indexPath, task) -> UICollectionViewCell? in return collectionview.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: task) }) } - private func snapshot(taskItems: [Task]) -> NSDiffableDataSourceSnapshot { + private func snapshot(taskItems: [TaskVM]) -> NSDiffableDataSourceSnapshot { - var snapshot = NSDiffableDataSourceSnapshot() + var snapshot = NSDiffableDataSourceSnapshot() for i in 0..<2 { snapshot.appendSections(["\(i)"]) // TODO: 뷰 테스트를 위한 Task배열을 바로 만들어 넣어주는데 이 배열을 taskItems로 변경하기 - snapshot.appendItems([Task(section: "123", title: "123", isCompleted: false, depth: 0, parent: nil, subTasks: []),Task(section: "123", title: "123", isCompleted: false, depth: 0, parent: nil, subTasks: []),Task(section: "123", title: "123", isCompleted: false, depth: 0, parent: nil, subTasks: []),Task(section: "123", title: "123", isCompleted: false, depth: 0, parent: nil, subTasks: []),Task(section: "123", title: "123", isCompleted: false, depth: 0, parent: nil, subTasks: [])], toSection: "\(i)") +// snapshot.appendItems([TaskVM(section: "123", title: "123", isCompleted: false, depth: 0, parent: nil, subTasks: []),TaskVM(section: "123", title: "123", isCompleted: false, depth: 0, parent: nil, subTasks: []),TaskVM(section: "123", title: "123", isCompleted: false, depth: 0, parent: nil, subTasks: []),TaskVM(section: "123", title: "123", isCompleted: false, depth: 0, parent: nil, subTasks: []),TaskVM(section: "123", title: "123", isCompleted: false, depth: 0, parent: nil, subTasks: [])], toSection: "\(i)") } return snapshot diff --git a/iOS/HalgoraeDO/Sources/Scenes/TaskList/TaskListDisplayLogic.swift b/iOS/HalgoraeDO/Sources/Scenes/TaskList/TaskListDisplayLogic.swift index a629f227..eca01f1c 100644 --- a/iOS/HalgoraeDO/Sources/Scenes/TaskList/TaskListDisplayLogic.swift +++ b/iOS/HalgoraeDO/Sources/Scenes/TaskList/TaskListDisplayLogic.swift @@ -8,8 +8,6 @@ import Foundation protocol TaskListDisplayLogic { - func display(tasks: [Task]) + func displayFetchTasks(viewModel: TaskListModels.FetchTasks.ViewModel) func displayDetail(of task: Task) - func set(editingMode: Bool) - func display(numberOfSelectedTasks count: Int) } diff --git a/iOS/HalgoraeDO/Sources/Scenes/TaskList/TaskListInteractor.swift b/iOS/HalgoraeDO/Sources/Scenes/TaskList/TaskListInteractor.swift index 4630b77f..2ae70ffd 100644 --- a/iOS/HalgoraeDO/Sources/Scenes/TaskList/TaskListInteractor.swift +++ b/iOS/HalgoraeDO/Sources/Scenes/TaskList/TaskListInteractor.swift @@ -8,10 +8,7 @@ import Foundation protocol TaskListBusinessLogic { - func fetchTasks() - func change(editingMode: Bool, animated: Bool) - func select(task: Task) - func deSelect(task: Task) + func fetchTasks(request: TaskListModels.FetchTasks.Request) } protocol TaskListDataStore { @@ -29,31 +26,8 @@ class TaskListInteractor: TaskListDataStore { } extension TaskListInteractor: TaskListBusinessLogic { - func fetchTasks() { + func fetchTasks(request: TaskListModels.FetchTasks.Request) { let tasks = worker.getTasks() - presenter.present(tasks: tasks) - } - - func change(editingMode: Bool, animated: Bool) { - worker.isEditingMode = editingMode - presenter.set(editingMode: editingMode) - } - - func select(task: Task) { - guard !worker.isEditingMode else { - worker.append(selected: task) - presenter.present(numberOfSelectedTasks: worker.selectedTasks.count) - return - } - presenter.presentDetail(of: task) - } - - func deSelect(task: Task) { - guard worker.isEditingMode else { - return - } - - worker.remove(selected: task) - presenter.present(numberOfSelectedTasks: worker.selectedTasks.count) + presenter.presentFetchTasks(response: TaskListModels.FetchTasks.Response(tasks: tasks)) } } diff --git a/iOS/HalgoraeDO/Sources/Scenes/TaskList/TaskListModels.swift b/iOS/HalgoraeDO/Sources/Scenes/TaskList/TaskListModels.swift index 77a87de7..aeba46f3 100644 --- a/iOS/HalgoraeDO/Sources/Scenes/TaskList/TaskListModels.swift +++ b/iOS/HalgoraeDO/Sources/Scenes/TaskList/TaskListModels.swift @@ -5,18 +5,120 @@ // Created by woong on 2020/11/23. // -import Foundation +import UIKit enum TaskListModels { - struct Request { + + // MARK: - Use cases + + enum FetchTasks { + struct Request { + var showCompleted: Bool + } + + struct Response { + var tasks: [Task] + } + struct ViewModel { + var displayedTasks: [DisplayedTask] + } } - struct Response { + enum FinishTask { + struct Request { + var displayedTasks: [DisplayedTask] + } + + struct Response { + var task: Task + } + + struct ViewModel { + var displayedTask: DisplayedTask + } + } + enum ReorderTask { + struct Request { + var displayedTask: DisplayedTask + } + + struct Response { + var task: Task + } + + struct ViewModel { + var displayedTask: DisplayedTask + } } - struct ViewModel { + enum CreateTask { + struct Request { + var taskFields: TaskFields + } + + struct Response { + var task: Task + } + + struct ViewModel { + var displayedTask: DisplayedTask + } + } +} + +// MARK: - Models + +extension TaskListModels { + + struct TaskFields { + + } + struct DisplayedTask: Hashable { + var id: UUID + var title: String + var isCompleted: Bool + var tintColor: UIColor + var position: Int + var parentPosition: Int? + var subItems: [DisplayedTask] + + init(id: UUID, + title: String, + isCompleted: Bool = false, + tintColor: UIColor, + position: Int, + parentPosition: Int?, + subItems: [DisplayedTask]) { + self.id = id + self.title = title + self.isCompleted = isCompleted + self.tintColor = tintColor + self.position = position + self.parentPosition = parentPosition + self.subItems = subItems + } + + init(task: Task, position: Int, parentPosition: Int?) { + self.id = task.identifier + self.title = task.title + self.isCompleted = task.isCompleted + self.tintColor = task.priority.color + self.position = position + self.parentPosition = parentPosition + self.subItems = task.subTasks.enumerated().compactMap { (idx, task) in + DisplayedTask(task: task, position: idx, parentPosition: position) + } + } + + static func ==(lhs: Self, rhs: Self) -> Bool { + return lhs.id == rhs.id + } + + func hash(into hasher: inout Hasher) { + hasher.combine(id) + } } } diff --git a/iOS/HalgoraeDO/Sources/Scenes/TaskList/TaskListPresenter.swift b/iOS/HalgoraeDO/Sources/Scenes/TaskList/TaskListPresenter.swift index f816ce3f..77ad5df8 100644 --- a/iOS/HalgoraeDO/Sources/Scenes/TaskList/TaskListPresenter.swift +++ b/iOS/HalgoraeDO/Sources/Scenes/TaskList/TaskListPresenter.swift @@ -8,10 +8,8 @@ import Foundation protocol TaskListPresentLogic { - func present(tasks: [Task]) - func set(editingMode: Bool) + func presentFetchTasks(response: TaskListModels.FetchTasks.Response) func presentDetail(of task: Task) - func present(numberOfSelectedTasks count: Int) } class TaskListPresenter { @@ -23,19 +21,14 @@ class TaskListPresenter { } extension TaskListPresenter: TaskListPresentLogic { - func present(tasks: [Task]) { - viewController.display(tasks: tasks) - } - - func set(editingMode: Bool) { - viewController.set(editingMode: editingMode) + func presentFetchTasks(response: TaskListModels.FetchTasks.Response) { + let taskViewModels = response.tasks.enumerated().map { (idx, task) in + TaskListModels.DisplayedTask(task: task, position: idx, parentPosition: nil) + } + viewController.displayFetchTasks(viewModel: TaskListModels.FetchTasks.ViewModel(displayedTasks: taskViewModels)) } func presentDetail(of task: Task) { - viewController.displayDetail(of: task) - } - - func present(numberOfSelectedTasks count: Int) { - viewController.display(numberOfSelectedTasks: count) + } } diff --git a/iOS/HalgoraeDO/Sources/Scenes/TaskList/TaskListViewController.swift b/iOS/HalgoraeDO/Sources/Scenes/TaskList/TaskListViewController.swift index a37d1942..808674d0 100644 --- a/iOS/HalgoraeDO/Sources/Scenes/TaskList/TaskListViewController.swift +++ b/iOS/HalgoraeDO/Sources/Scenes/TaskList/TaskListViewController.swift @@ -9,11 +9,21 @@ import UIKit class TaskListViewController: UIViewController { + typealias TaskVM = TaskListModels.DisplayedTask + // MARK: - Properties + /// 임시 property + private var projectTitle = "할고래DO" private var interactor: TaskListBusinessLogic? private var router: (TaskListRoutingLogic & TaskListDataPassing)? - private var dataSource: UICollectionViewDiffableDataSource! = nil + private var dataSource: UICollectionViewDiffableDataSource! = nil + private(set) var selectedTasks = Set() { + didSet { + guard isEditing else { return } + title = "\(selectedTasks.count) 개 선택됨" + } + } // MARK: - View Life Cycle @@ -26,7 +36,7 @@ class TaskListViewController: UIViewController { override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) - interactor?.fetchTasks() + interactor?.fetchTasks(request: .init(showCompleted: false)) } // MARK: - Views @@ -40,7 +50,8 @@ class TaskListViewController: UIViewController { private func configureLogic() { let presenter = TaskListPresenter(viewController: self) - let interactor = TaskListInteractor(presenter: presenter, worker: TaskListWorker()) + let interactor = TaskListInteractor(presenter: presenter, + worker: TaskListWorker()) self.interactor = interactor } @@ -49,7 +60,18 @@ class TaskListViewController: UIViewController { override func setEditing(_ editing: Bool, animated: Bool) { super.setEditing(editing, animated: animated) - interactor?.change(editingMode: editing, animated: animated) + set(editingMode: editing) + } + + func set(editingMode: Bool) { + if !editingMode { + selectedTasks.removeAll() + } + title = editingMode ? "\(selectedTasks.count) 개 선택됨" : projectTitle + taskListCollectionView.isEditing = editingMode + moreButton.title = editingMode ? "취소" : "More" + addButton.isHidden = editingMode + editToolBar.isHidden = !editingMode } // MARK: IBActions @@ -97,28 +119,15 @@ class TaskListViewController: UIViewController { // MARK: - TaskList Display Logic extension TaskListViewController: TaskListDisplayLogic { - func display(tasks: [Task]) { - let snapShot = snapshot(taskItems: tasks) - let sectionTitle = "" - dataSource.apply(snapShot, to: sectionTitle, animatingDifferences: true) + + func displayFetchTasks(viewModel: TaskListModels.FetchTasks.ViewModel) { + let snapShot = snapshot(taskItems: viewModel.displayedTasks) + dataSource.apply(snapShot, to: projectTitle, animatingDifferences: true) } func displayDetail(of task: Task) { } - - func set(editingMode: Bool) { - taskListCollectionView.isEditing = editingMode - title = editingMode ? "0개 선택됨" : "할고래DO" - moreButton.title = editingMode ? "취소" : "More" - addButton.isHidden = editingMode - editToolBar.isHidden = !editingMode - } - - func display(numberOfSelectedTasks count: Int) { - guard isEditing else { return } - title = "\(count) 개 선택됨" - } } // MARK: - Configure CollectionView Layout @@ -134,15 +143,14 @@ private extension TaskListViewController { var listConfiguration = UICollectionLayoutListConfiguration(appearance: .plain) listConfiguration.leadingSwipeActionsConfigurationProvider = { indexPath in let editAction = UIContextualAction(style: .normal, title: "Edit") { [weak self] (action, view, completion) in - if !(self?.isEditing ?? true) { - self?.setEditing(true, animated: true) + guard let self = self else { return } + if !self.isEditing { + self.setEditing(true, animated: true) } - if let task = self?.dataSource.snapshot().itemIdentifiers[indexPath.item] { - self?.interactor?.select(task: task) - } - - self?.taskListCollectionView.selectItem(at: indexPath, animated: true, scrollPosition: .init()) + let taskVM = self.dataSource.snapshot().itemIdentifiers[indexPath.item] + self.selectedTasks.insert(taskVM) + self.taskListCollectionView.selectItem(at: indexPath, animated: true, scrollPosition: .init()) } return UISwipeActionsConfiguration(actions: [editAction]) } @@ -156,9 +164,9 @@ private extension TaskListViewController { private extension TaskListViewController { func configureDataSource() { - let cellRegistration = UICollectionView.CellRegistration { [weak self] (cell, _: IndexPath, taskItem) in + let cellRegistration = UICollectionView.CellRegistration { [weak self] (cell, _: IndexPath, taskItem) in - cell.task = taskItem + cell.taskViewModel = taskItem cell.finishHandler = { [weak self] task in guard let self = self, let task = task @@ -166,6 +174,8 @@ private extension TaskListViewController { return } + + var currentSnapshot = self.dataSource.snapshot() if task.isCompleted { currentSnapshot.deleteItems([task]) @@ -175,22 +185,23 @@ private extension TaskListViewController { } let disclosureOptions = UICellAccessory.OutlineDisclosureOptions(style: .automatic) - cell.accessories = taskItem.subTasks.isEmpty ? [] : [.outlineDisclosure(options: disclosureOptions)] + + cell.accessories = taskItem.subItems.isEmpty ? [] : [.outlineDisclosure(options: disclosureOptions)] } - self.dataSource = UICollectionViewDiffableDataSource(collectionView: taskListCollectionView, cellProvider: { (collectionView, indexPath, task) -> UICollectionViewCell? in + self.dataSource = UICollectionViewDiffableDataSource(collectionView: taskListCollectionView, cellProvider: { (collectionView, indexPath, task) -> UICollectionViewCell? in return collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: task) }) } - func snapshot(taskItems: [Task]) -> NSDiffableDataSourceSectionSnapshot { - var snapshot = NSDiffableDataSourceSectionSnapshot() + func snapshot(taskItems: [TaskVM]) -> NSDiffableDataSourceSectionSnapshot { + var snapshot = NSDiffableDataSourceSectionSnapshot() - func addItems(_ taskItems: [Task], to parent: Task?) { + func addItems(_ taskItems: [TaskVM], to parent: TaskVM?) { snapshot.append(taskItems, to: parent) - for taskItem in taskItems where !taskItem.subTasks.isEmpty { - addItems(taskItem.subTasks, to: taskItem) + for taskItem in taskItems where !taskItem.subItems.isEmpty { + addItems(taskItem.subItems, to: taskItem) } } @@ -203,16 +214,21 @@ private extension TaskListViewController { extension TaskListViewController: UICollectionViewDelegate { func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { - let task = dataSource.snapshot().itemIdentifiers[indexPath.item] - interactor?.select(task: task) - - if !isEditing { - collectionView.deselectItem(at: indexPath, animated: true) + let taskVM = dataSource.snapshot().itemIdentifiers[indexPath.item] + guard !isEditing else { + selectedTasks.insert(taskVM) + return } + + collectionView.deselectItem(at: indexPath, animated: true) + // TODO: request show detail task } func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) { - let task = dataSource.snapshot().itemIdentifiers[indexPath.item] - interactor?.deSelect(task: task) + let taskVM = dataSource.snapshot().itemIdentifiers[indexPath.item] + guard !isEditing else { + selectedTasks.remove(taskVM) + return + } } } diff --git a/iOS/HalgoraeDO/Sources/Scenes/TaskList/TaskListWorker.swift b/iOS/HalgoraeDO/Sources/Scenes/TaskList/TaskListWorker.swift index 3e45372e..1017f31b 100644 --- a/iOS/HalgoraeDO/Sources/Scenes/TaskList/TaskListWorker.swift +++ b/iOS/HalgoraeDO/Sources/Scenes/TaskList/TaskListWorker.swift @@ -8,15 +8,7 @@ import Foundation class TaskListWorker { - private(set) var selectedTasks = Set() - var isEditingMode = false { - didSet { - guard isEditingMode else { - selectedTasks.removeAll() - return - } - } - } + var tasks = [Task]() func getTasks() -> [Task] { return [ @@ -29,12 +21,4 @@ class TaskListWorker { Task(title: "두 말하면 섭함"), ] } - - func append(selected task: Task) { - selectedTasks.insert(task) - } - - func remove(selected task: Task) { - selectedTasks.remove(task) - } } diff --git a/iOS/HalgoraeDO/Sources/Scenes/TaskList/Views/TaskCollectionViewListCell.swift b/iOS/HalgoraeDO/Sources/Scenes/TaskList/Views/TaskCollectionViewListCell.swift index 6af9195c..1b513a1a 100644 --- a/iOS/HalgoraeDO/Sources/Scenes/TaskList/Views/TaskCollectionViewListCell.swift +++ b/iOS/HalgoraeDO/Sources/Scenes/TaskList/Views/TaskCollectionViewListCell.swift @@ -9,23 +9,23 @@ import UIKit class TaskCollectionViewListCell: UICollectionViewListCell { - weak var task: Task? - var finishHandler: ((Task?) -> Void)? + var taskViewModel: TaskListModels.DisplayedTask? + var finishHandler: ((TaskListModels.DisplayedTask?) -> Void)? override func updateConfiguration(using state: UICellConfigurationState) { backgroundConfiguration?.backgroundColor = (state.isSelected || state.isHighlighted) ? .lightGray : .white var taskContentConfiguration = TaskContentConfiguration().updated(for: state) - taskContentConfiguration.title = task?.title - taskContentConfiguration.isCompleted = task?.isCompleted + taskContentConfiguration.title = taskViewModel?.title + taskContentConfiguration.isCompleted = taskViewModel?.isCompleted contentConfiguration = taskContentConfiguration if let taskContentView = contentView as? TaskContentView { taskContentView.completeHandler = { [weak self] isCompleted in - self?.task?.isCompleted = isCompleted - self?.finishHandler?(self?.task) + self?.taskViewModel?.isCompleted = isCompleted + self?.finishHandler?(self?.taskViewModel) } } } diff --git a/iOS/HalgoraeDO/Sources/Scenes/TaskList/Views/TaskContentView.swift b/iOS/HalgoraeDO/Sources/Scenes/TaskList/Views/TaskContentView.swift index d924a2a4..45477c0c 100644 --- a/iOS/HalgoraeDO/Sources/Scenes/TaskList/Views/TaskContentView.swift +++ b/iOS/HalgoraeDO/Sources/Scenes/TaskList/Views/TaskContentView.swift @@ -99,7 +99,7 @@ private extension TaskContentView { stackView.topAnchor.constraint(equalTo: layoutMarginsGuide.topAnchor), stackView.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor, constant: 10), - stackView.trailingAnchor.constraint(equalTo: layoutMarginsGuide.trailingAnchor), + stackView.trailingAnchor.constraint(equalTo: layoutMarginsGuide.trailingAnchor, constant: -10), stackView.bottomAnchor.constraint(equalTo: layoutMarginsGuide.bottomAnchor), completeButtonHeight, completeButton.widthAnchor.constraint(equalToConstant: 30), diff --git a/iOS/TaskListTests/Mocks/TaskListDisplaySpy.swift b/iOS/TaskListTests/Mocks/TaskListDisplaySpy.swift index c06ffbc8..c3b6085f 100644 --- a/iOS/TaskListTests/Mocks/TaskListDisplaySpy.swift +++ b/iOS/TaskListTests/Mocks/TaskListDisplaySpy.swift @@ -8,12 +8,13 @@ import Foundation class TaskListDisplaySpy: TaskListDisplayLogic { + var displayTasks = false var displayDetail = false var setEditingMode = false var displayNumberOfSelectedTasks = false - func display(tasks: [Task]) { + func display(tasks: [TaskListModels.TaskViewModel]) { displayTasks = true } diff --git a/iOS/TaskListTests/TaskListInteractorTests.swift b/iOS/TaskListTests/TaskListInteractorTests.swift index d9b0f9ca..776cd0e1 100644 --- a/iOS/TaskListTests/TaskListInteractorTests.swift +++ b/iOS/TaskListTests/TaskListInteractorTests.swift @@ -19,73 +19,4 @@ class TaskListInteractorTests: XCTestCase { worker = TaskListWorker() interactor = TaskListInteractor(presenter: presenter, worker: worker) } - - func test_change_editingMode_true() { - // When - interactor.change(editingMode: true, animated: true) - - // Then - XCTAssertEqual(worker.isEditingMode, true) - XCTAssertEqual(presenter.setEditingMode, true) - } - - func test_change_editingMode_false() { - // When - interactor.change(editingMode: false, animated: true) - - // Then - XCTAssertEqual(worker.isEditingMode, false) - XCTAssertEqual(presenter.setEditingMode, true) - } - - func test_select_oneTask_editingMode() { - // When - interactor.change(editingMode: true, animated: true) - interactor.select(task: Task(title: "1")) - - // Then - XCTAssertEqual(worker.selectedTasks.count, 1) - XCTAssertEqual(presenter.presentNumberOfSelectedTasks, 1) - } - - func test_select_task_notEditingMode() { - // When - interactor.change(editingMode: false, animated: true) - let task = Task(title: "1") - interactor.select(task: task) - - // Then - XCTAssertNotNil(presenter.presentDetail_task) - XCTAssertEqual(task, presenter.presentDetail_task) - } - - func test_deSelect_hasThreeTask_editingMode() { - // Given - interactor.change(editingMode: true, animated: true) - let t3 = Task(title: "3") - worker.append(selected: Task(title: "1")) - worker.append(selected: Task(title: "2")) - worker.append(selected: t3) - - // When - interactor.deSelect(task: t3) - - // Then - XCTAssertEqual(worker.selectedTasks.count, 2) - XCTAssertEqual(presenter.presentNumberOfSelectedTasks, 2) - } - - func test_deSelect_hasThreeTask_not_editingMode_success() { - // Given - interactor.change(editingMode: false, animated: true) - let t1 = Task(title: "1") - worker.append(selected: t1) - - // When - interactor.deSelect(task: t1) - - // Then - XCTAssertEqual(worker.selectedTasks.count, 1) - XCTAssertEqual(presenter.presentNumberOfSelectedTasks, -1) - } } diff --git a/iOS/TaskListTests/TaskListWorkerTests.swift b/iOS/TaskListTests/TaskListWorkerTests.swift index b4bdc9b7..e072d8a8 100644 --- a/iOS/TaskListTests/TaskListWorkerTests.swift +++ b/iOS/TaskListTests/TaskListWorkerTests.swift @@ -9,60 +9,4 @@ import XCTest class TaskListWorkerTests: XCTestCase { - func test_numberOfSelectedTasks_init() { - // When - let worker = TaskListWorker() - - // Then - XCTAssertEqual(worker.selectedTasks.count, 0) - } - - func test_editingMode_false_init() { - // When - let worker = TaskListWorker() - - // Then - XCTAssertEqual(worker.isEditingMode, false) - } - - func test_append_editingMode_on() { - // Given - let worker = TaskListWorker() - worker.isEditingMode = true - - // When - worker.append(selected: Task(title: "test")) - - // Then - XCTAssertEqual(worker.selectedTasks.count, 1) - } - - func test_remove_onEditingMode() { - // Given - let worker = TaskListWorker() - worker.isEditingMode = true - let task = Task(title: "test") - - // When - worker.append(selected: task) - worker.remove(selected: task) - - // Then - XCTAssertEqual(worker.selectedTasks.count, 0) - } - - func test_selectedTasks_empty_turnOffEditingMode_success() { - // Given - let worker = TaskListWorker() - worker.isEditingMode = true - worker.append(selected: Task(title: "1")) - worker.append(selected: Task(title: "1")) - worker.append(selected: Task(title: "1")) - - // When - worker.isEditingMode = false - - // Then - XCTAssertEqual(worker.selectedTasks.isEmpty, true) - } } diff --git a/server/.babelrc.js b/server/.babelrc.js new file mode 100644 index 00000000..fd6ec5f3 --- /dev/null +++ b/server/.babelrc.js @@ -0,0 +1,6 @@ +const plugins = [ + ['@babel/plugin-proposal-decorators', { legacy: true }], + ['@babel/plugin-proposal-class-properties', { loose: true }], +]; + +module.exports = { plugins }; diff --git a/server/.eslintrc.js b/server/.eslintrc.js index 46a1d525..39580dbc 100644 --- a/server/.eslintrc.js +++ b/server/.eslintrc.js @@ -8,8 +8,12 @@ module.exports = { }, plugins: ['prettier', 'jest'], extends: ['airbnb-base', 'eslint-config-prettier', 'prettier'], + parser: '@babel/eslint-parser', parserOptions: { ecmaVersion: 12, + babelOptions: { + configFile: './server/.babelrc.js', + }, }, rules: { 'prettier/prettier': [ diff --git a/server/jsconfig.json b/server/jsconfig.json new file mode 100644 index 00000000..c94d37e3 --- /dev/null +++ b/server/jsconfig.json @@ -0,0 +1,6 @@ +{ + "compilerOptions": { + "experimentalDecorators": true, + }, + "exclude": ["node_modules"] +} diff --git a/server/package-lock.json b/server/package-lock.json index 14100614..a4c843dc 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -1,14 +1,66 @@ { "name": "halgoraedo", - "version": "0.0.0", + "version": "0.0.1", "lockfileVersion": 1, "requires": true, "dependencies": { + "@babel/cli": { + "version": "7.12.8", + "resolved": "https://registry.npmjs.org/@babel/cli/-/cli-7.12.8.tgz", + "integrity": "sha512-/6nQj11oaGhLmZiuRUfxsujiPDc9BBReemiXgIbxc+M5W+MIiFKYwvNDJvBfnGKNsJTKbUfEheKc9cwoPHAVQA==", + "requires": { + "@nicolo-ribaudo/chokidar-2": "2.1.8-no-fsevents", + "chokidar": "^3.4.0", + "commander": "^4.0.1", + "convert-source-map": "^1.1.0", + "fs-readdir-recursive": "^1.1.0", + "glob": "^7.0.0", + "lodash": "^4.17.19", + "make-dir": "^2.1.0", + "slash": "^2.0.0", + "source-map": "^0.5.0" + }, + "dependencies": { + "commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==" + }, + "make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "requires": { + "pify": "^4.0.1", + "semver": "^5.6.0" + } + }, + "pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==" + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + }, + "slash": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", + "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==" + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + } + } + }, "@babel/code-frame": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", - "dev": true, "requires": { "@babel/highlight": "^7.10.4" } @@ -17,7 +69,6 @@ "version": "7.12.3", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.12.3.tgz", "integrity": "sha512-0qXcZYKZp3/6N2jKYVxZv0aNCsxTSVCiK72DTiTYZAu7sjg73W0/aynWjMbiGd87EQL4WyA8reiJVh92AVla9g==", - "dev": true, "requires": { "@babel/code-frame": "^7.10.4", "@babel/generator": "^7.12.1", @@ -41,7 +92,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.0.tgz", "integrity": "sha512-jjO6JD2rKfiZQnBoRzhRTbXjHLGLfH+UtGkWLc/UXAh/rzZMyjbgn0NcfFpqT8nd1kTtFnDiJcrIFkq4UKeJVg==", - "dev": true, "requires": { "ms": "2.1.2" } @@ -50,7 +100,6 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.3.tgz", "integrity": "sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA==", - "dev": true, "requires": { "minimist": "^1.2.5" } @@ -58,19 +107,51 @@ "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "semver": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" }, "source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + } + } + }, + "@babel/eslint-parser": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.12.1.tgz", + "integrity": "sha512-cc7WQHnHQY3++/bghgbDtPx+5bf6xTsokyGzV6Qzh65NLz/unv+mPQuACkQ9GFhIhcTFv6yqwNaEcfX7EkOEsg==", + "dev": true, + "requires": { + "eslint-scope": "5.1.0", + "eslint-visitor-keys": "^1.3.0", + "semver": "^6.3.0" + }, + "dependencies": { + "eslint-scope": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.0.tgz", + "integrity": "sha512-iiGRvtxWqgtx5m8EyQUJihBloE4EnYeGE/bz1wSPwJE6tZuJUtHlhqDM4Xj2ukE8Dyy1+HCZ4hE0fzIVMzb58w==", + "dev": true, + "requires": { + "esrecurse": "^4.1.0", + "estraverse": "^4.1.1" + } + }, + "eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", "dev": true } } @@ -79,7 +160,6 @@ "version": "7.12.5", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.12.5.tgz", "integrity": "sha512-m16TQQJ8hPt7E+OS/XVQg/7U184MLXtvuGbCdA7na61vha+ImkyyNM/9DDA0unYCVZn3ZOhng+qz48/KBOT96A==", - "dev": true, "requires": { "@babel/types": "^7.12.5", "jsesc": "^2.5.1", @@ -89,16 +169,26 @@ "source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" } } }, + "@babel/helper-create-class-features-plugin": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.12.1.tgz", + "integrity": "sha512-hkL++rWeta/OVOBTRJc9a5Azh5mt5WgZUGAKMD8JM141YsE08K//bp1unBBieO6rUKkIPyUE0USQ30jAy3Sk1w==", + "requires": { + "@babel/helper-function-name": "^7.10.4", + "@babel/helper-member-expression-to-functions": "^7.12.1", + "@babel/helper-optimise-call-expression": "^7.10.4", + "@babel/helper-replace-supers": "^7.12.1", + "@babel/helper-split-export-declaration": "^7.10.4" + } + }, "@babel/helper-function-name": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz", "integrity": "sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ==", - "dev": true, "requires": { "@babel/helper-get-function-arity": "^7.10.4", "@babel/template": "^7.10.4", @@ -109,7 +199,6 @@ "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz", "integrity": "sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A==", - "dev": true, "requires": { "@babel/types": "^7.10.4" } @@ -118,7 +207,6 @@ "version": "7.12.1", "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.12.1.tgz", "integrity": "sha512-k0CIe3tXUKTRSoEx1LQEPFU9vRQfqHtl+kf8eNnDqb4AUJEy5pz6aIiog+YWtVm2jpggjS1laH68bPsR+KWWPQ==", - "dev": true, "requires": { "@babel/types": "^7.12.1" } @@ -127,7 +215,6 @@ "version": "7.12.5", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.12.5.tgz", "integrity": "sha512-SR713Ogqg6++uexFRORf/+nPXMmWIn80TALu0uaFb+iQIUoR7bOC7zBWyzBs5b3tBBJXuyD0cRu1F15GyzjOWA==", - "dev": true, "requires": { "@babel/types": "^7.12.5" } @@ -136,7 +223,6 @@ "version": "7.12.1", "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.12.1.tgz", "integrity": "sha512-QQzehgFAZ2bbISiCpmVGfiGux8YVFXQ0abBic2Envhej22DVXV9nCFaS5hIQbkyo1AdGb+gNME2TSh3hYJVV/w==", - "dev": true, "requires": { "@babel/helper-module-imports": "^7.12.1", "@babel/helper-replace-supers": "^7.12.1", @@ -153,7 +239,6 @@ "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.10.4.tgz", "integrity": "sha512-n3UGKY4VXwXThEiKrgRAoVPBMqeoPgHVqiHZOanAJCG9nQUL2pLRQirUzl0ioKclHGpGqRgIOkgcIJaIWLpygg==", - "dev": true, "requires": { "@babel/types": "^7.10.4" } @@ -161,14 +246,12 @@ "@babel/helper-plugin-utils": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", - "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", - "dev": true + "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==" }, "@babel/helper-replace-supers": { "version": "7.12.5", "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.12.5.tgz", "integrity": "sha512-5YILoed0ZyIpF4gKcpZitEnXEJ9UoDRki1Ey6xz46rxOzfNMAhVIJMoune1hmPVxh40LRv1+oafz7UsWX+vyWA==", - "dev": true, "requires": { "@babel/helper-member-expression-to-functions": "^7.12.1", "@babel/helper-optimise-call-expression": "^7.10.4", @@ -180,7 +263,6 @@ "version": "7.12.1", "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.12.1.tgz", "integrity": "sha512-OxBp7pMrjVewSSC8fXDFrHrBcJATOOFssZwv16F3/6Xtc138GHybBfPbm9kfiqQHKhYQrlamWILwlDCeyMFEaA==", - "dev": true, "requires": { "@babel/types": "^7.12.1" } @@ -189,7 +271,6 @@ "version": "7.11.0", "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.11.0.tgz", "integrity": "sha512-74Vejvp6mHkGE+m+k5vHY93FX2cAtrw1zXrZXRlG4l410Nm9PxfEiVTn1PjDPV5SnmieiueY4AFg2xqhNFuuZg==", - "dev": true, "requires": { "@babel/types": "^7.11.0" } @@ -197,14 +278,12 @@ "@babel/helper-validator-identifier": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", - "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", - "dev": true + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==" }, "@babel/helpers": { "version": "7.12.5", "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.12.5.tgz", "integrity": "sha512-lgKGMQlKqA8meJqKsW6rUnc4MdUk35Ln0ATDqdM1a/UpARODdI4j5Y5lVfUScnSNkJcdCRAaWkspykNoFg9sJA==", - "dev": true, "requires": { "@babel/template": "^7.10.4", "@babel/traverse": "^7.12.5", @@ -215,7 +294,6 @@ "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", - "dev": true, "requires": { "@babel/helper-validator-identifier": "^7.10.4", "chalk": "^2.0.0", @@ -226,7 +304,6 @@ "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, "requires": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", @@ -235,11 +312,52 @@ } } }, + "@babel/node": { + "version": "7.12.6", + "resolved": "https://registry.npmjs.org/@babel/node/-/node-7.12.6.tgz", + "integrity": "sha512-A1TpW2X05ZkI5+WV7Aa24QX4LyGwrGUQPflG1CyBdr84jUuH0mhkE2BQWSQAlfRnp4bMLjeveMJIhS20JaOfVQ==", + "requires": { + "@babel/register": "^7.12.1", + "commander": "^4.0.1", + "core-js": "^3.2.1", + "lodash": "^4.17.19", + "node-environment-flags": "^1.0.5", + "regenerator-runtime": "^0.13.4", + "resolve": "^1.13.1", + "v8flags": "^3.1.1" + }, + "dependencies": { + "commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==" + } + } + }, "@babel/parser": { "version": "7.12.5", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.12.5.tgz", - "integrity": "sha512-FVM6RZQ0mn2KCf1VUED7KepYeUWoVShczewOCfm3nzoBybaih51h+sYVVGthW9M6lPByEPTQf+xm27PBdlpwmQ==", - "dev": true + "integrity": "sha512-FVM6RZQ0mn2KCf1VUED7KepYeUWoVShczewOCfm3nzoBybaih51h+sYVVGthW9M6lPByEPTQf+xm27PBdlpwmQ==" + }, + "@babel/plugin-proposal-class-properties": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.12.1.tgz", + "integrity": "sha512-cKp3dlQsFsEs5CWKnN7BnSHOd0EOW8EKpEjkoz1pO2E5KzIDNV9Ros1b0CnmbVgAGXJubOYVBOGCT1OmJwOI7w==", + "requires": { + "@babel/helper-create-class-features-plugin": "^7.12.1", + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-proposal-decorators": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.12.1.tgz", + "integrity": "sha512-knNIuusychgYN8fGJHONL0RbFxLGawhXOJNLBk75TniTsZZeA+wdkDuv6wp4lGwzQEKjZi6/WYtnb3udNPmQmQ==", + "dev": true, + "requires": { + "@babel/helper-create-class-features-plugin": "^7.12.1", + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/plugin-syntax-decorators": "^7.12.1" + } }, "@babel/plugin-syntax-async-generators": { "version": "7.8.4", @@ -268,6 +386,15 @@ "@babel/helper-plugin-utils": "^7.10.4" } }, + "@babel/plugin-syntax-decorators": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.12.1.tgz", + "integrity": "sha512-ir9YW5daRrTYiy9UJ2TzdNIJEZu8KclVzDcfSt4iEmOtwQ4llPtWInNKJyKnVXp1vE4bbVd5S31M/im3mYMO1w==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, "@babel/plugin-syntax-import-meta": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", @@ -349,11 +476,43 @@ "@babel/helper-plugin-utils": "^7.10.4" } }, + "@babel/register": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/register/-/register-7.12.1.tgz", + "integrity": "sha512-XWcmseMIncOjoydKZnWvWi0/5CUCD+ZYKhRwgYlWOrA8fGZ/FjuLRpqtIhLOVD/fvR1b9DQHtZPn68VvhpYf+Q==", + "requires": { + "find-cache-dir": "^2.0.0", + "lodash": "^4.17.19", + "make-dir": "^2.1.0", + "pirates": "^4.0.0", + "source-map-support": "^0.5.16" + }, + "dependencies": { + "make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "requires": { + "pify": "^4.0.1", + "semver": "^5.6.0" + } + }, + "pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==" + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + } + } + }, "@babel/template": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz", "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==", - "dev": true, "requires": { "@babel/code-frame": "^7.10.4", "@babel/parser": "^7.10.4", @@ -364,7 +523,6 @@ "version": "7.12.5", "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.12.5.tgz", "integrity": "sha512-xa15FbQnias7z9a62LwYAA5SZZPkHIXpd42C6uW68o8uTuua96FHZy1y61Va5P/i83FAAcMpW8+A/QayntzuqA==", - "dev": true, "requires": { "@babel/code-frame": "^7.10.4", "@babel/generator": "^7.12.5", @@ -381,7 +539,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.0.tgz", "integrity": "sha512-jjO6JD2rKfiZQnBoRzhRTbXjHLGLfH+UtGkWLc/UXAh/rzZMyjbgn0NcfFpqT8nd1kTtFnDiJcrIFkq4UKeJVg==", - "dev": true, "requires": { "ms": "2.1.2" } @@ -389,14 +546,12 @@ "globals": { "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==" }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" } } }, @@ -404,7 +559,6 @@ "version": "7.12.6", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.6.tgz", "integrity": "sha512-hwyjw6GvjBLiyy3W0YQf0Z5Zf4NpYejUnKFcfcUhZCSffoBBp30w6wP2Wn6pk31jMYZvcOrB/1b7cGXvEoKogA==", - "dev": true, "requires": { "@babel/helper-validator-identifier": "^7.10.4", "lodash": "^4.17.19", @@ -741,6 +895,222 @@ "chalk": "^4.0.0" } }, + "@nicolo-ribaudo/chokidar-2": { + "version": "2.1.8-no-fsevents", + "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/chokidar-2/-/chokidar-2-2.1.8-no-fsevents.tgz", + "integrity": "sha512-+nb9vWloHNNMFHjGofEam3wopE3m1yuambrrd/fnPc+lFOMB9ROTqQlche9ByFWNkdNqfSgR/kkQtQ8DzEWt2w==", + "optional": true, + "requires": { + "anymatch": "^2.0.0", + "async-each": "^1.0.1", + "braces": "^2.3.2", + "glob-parent": "^3.1.0", + "inherits": "^2.0.3", + "is-binary-path": "^1.0.0", + "is-glob": "^4.0.0", + "normalize-path": "^3.0.0", + "path-is-absolute": "^1.0.0", + "readdirp": "^2.2.1", + "upath": "^1.1.1" + }, + "dependencies": { + "anymatch": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", + "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", + "optional": true, + "requires": { + "micromatch": "^3.1.4", + "normalize-path": "^2.1.1" + }, + "dependencies": { + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "optional": true, + "requires": { + "remove-trailing-separator": "^1.0.1" + } + } + } + }, + "binary-extensions": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz", + "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==", + "optional": true + }, + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "optional": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "optional": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "optional": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "optional": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "glob-parent": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", + "optional": true, + "requires": { + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" + }, + "dependencies": { + "is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "optional": true, + "requires": { + "is-extglob": "^2.1.0" + } + } + } + }, + "is-binary-path": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", + "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", + "optional": true, + "requires": { + "binary-extensions": "^1.0.0" + } + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "optional": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "optional": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "optional": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "optional": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "readdirp": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", + "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", + "optional": true, + "requires": { + "graceful-fs": "^4.1.11", + "micromatch": "^3.1.10", + "readable-stream": "^2.0.2" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "optional": true, + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "optional": true, + "requires": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + } + } + } + }, "@nodelib/fs.scandir": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz", @@ -909,6 +1279,11 @@ "integrity": "sha512-RJJrrySY7A8havqpGObOB4W92QXKJo63/jFLLgpvOtsGUqbQZ9Sbgl35KMm1DjC6j7AvmmU2bIno+3IyEaemaw==", "dev": true }, + "@types/validator": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.0.0.tgz", + "integrity": "sha512-WAy5txG7aFX8Vw3sloEKp5p/t/Xt8jD3GRD9DacnFv6Vo8ubudAsRTXgxpQwU0mpzY/H8U4db3roDuCMjShBmw==" + }, "@types/yargs": { "version": "15.0.10", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.10.tgz", @@ -1112,7 +1487,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==", - "dev": true, "requires": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" @@ -1412,8 +1786,7 @@ "binary-extensions": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.1.0.tgz", - "integrity": "sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ==", - "dev": true + "integrity": "sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ==" }, "bluebird": { "version": "3.7.2", @@ -1540,7 +1913,6 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, "requires": { "fill-range": "^7.0.1" } @@ -1632,8 +2004,7 @@ "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==" }, "bytes": { "version": "3.0.0", @@ -1792,7 +2163,6 @@ "version": "3.4.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.4.3.tgz", "integrity": "sha512-DtM3g7juCXQxFVSNPNByEC2+NImtBuxQQvWlHunpJIS5Ocr0lG306cC7FCi7cEA0fzmybPUIl4txBIobk1gGOQ==", - "dev": true, "requires": { "anymatch": "~3.1.1", "braces": "~3.0.2", @@ -1816,6 +2186,11 @@ "integrity": "sha512-uc2Vix1frTfnuzxxu1Hp4ktSvM3QaI4oXl4ZUqL1wjTu/BGki9TrCWoqLTg/drR1KwAEarXuRFCG2Svr1GxPFw==", "dev": true }, + "class-transformer": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.3.1.tgz", + "integrity": "sha512-cKFwohpJbuMovS8xVLmn8N2AUbAuc8pVo4zEfsUVo8qgECOogns1WVk/FkOZoxhOPTyTYFckuoH+13FO+MQ8GA==" + }, "class-utils": { "version": "0.3.6", "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", @@ -1837,6 +2212,24 @@ } } }, + "class-validator": { + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.12.2.tgz", + "integrity": "sha512-TDzPzp8BmpsbPhQpccB3jMUE/3pK0TyqamrK0kcx+ZeFytMA+O6q87JZZGObHHnoo9GM8vl/JppIyKWeEA/EVw==", + "requires": { + "@types/validator": "13.0.0", + "google-libphonenumber": "^3.2.8", + "tslib": ">=1.9.0", + "validator": "13.0.0" + }, + "dependencies": { + "validator": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.0.0.tgz", + "integrity": "sha512-anYx5fURbgF04lQV18nEQWZ/3wHGnxiKdG4aL8J+jEDsm98n/sU/bey+tYk6tnGJzm7ioh5FoqrAiQ6m03IgaA==" + } + } + }, "cli-boxes": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.1.tgz", @@ -1903,101 +2296,291 @@ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true }, - "string-width": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", - "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "string-width": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", + "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + } + } + } + }, + "clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=" + }, + "clone-response": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz", + "integrity": "sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=", + "dev": true, + "requires": { + "mimic-response": "^1.0.0" + } + }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", + "dev": true + }, + "code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" + }, + "collect-v8-coverage": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz", + "integrity": "sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg==", + "dev": true + }, + "collection-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", + "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", + "requires": { + "map-visit": "^1.0.0", + "object-visit": "^1.0.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + }, + "colors": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/colors/-/colors-0.6.2.tgz", + "integrity": "sha1-JCP+ZnisDF2uiFLl0OW+CMmXq8w=" + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" + }, + "commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=" + }, + "component-emitter": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==" + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "concurrently": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-5.3.0.tgz", + "integrity": "sha512-8MhqOB6PWlBfA2vJ8a0bSFKATOdWlHiQlk11IfmQBPaHVP8oP2gsh2MObE6UR3hqDHqvaIvLTyceNW6obVuFHQ==", + "dev": true, + "requires": { + "chalk": "^2.4.2", + "date-fns": "^2.0.1", + "lodash": "^4.17.15", + "read-pkg": "^4.0.1", + "rxjs": "^6.5.2", + "spawn-command": "^0.0.2-1", + "supports-color": "^6.1.0", + "tree-kill": "^1.2.2", + "yargs": "^13.3.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "dependencies": { + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "cliui": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", + "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", + "dev": true, + "requires": { + "string-width": "^3.1.0", + "strip-ansi": "^5.2.0", + "wrap-ansi": "^5.1.0" + } + }, + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", + "dev": true, + "requires": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + } + }, + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + }, + "read-pkg": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-4.0.1.tgz", + "integrity": "sha1-ljYlN48+HE1IyFhytabsfV0JMjc=", + "dev": true, + "requires": { + "normalize-package-data": "^2.3.2", + "parse-json": "^4.0.0", + "pify": "^3.0.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "wrap-ansi": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", + "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.0", + "string-width": "^3.0.0", + "strip-ansi": "^5.0.0" + } + }, + "yargs": { + "version": "13.3.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz", + "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==", + "dev": true, + "requires": { + "cliui": "^5.0.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^13.1.2" + } + }, + "yargs-parser": { + "version": "13.1.2", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", + "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", "dev": true, "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.0" + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" } } } }, - "clone": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", - "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=" - }, - "clone-response": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz", - "integrity": "sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=", - "dev": true, - "requires": { - "mimic-response": "^1.0.0" - } - }, - "co": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", - "dev": true - }, - "code-point-at": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" - }, - "collect-v8-coverage": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz", - "integrity": "sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg==", - "dev": true - }, - "collection-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", - "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", - "requires": { - "map-visit": "^1.0.0", - "object-visit": "^1.0.0" - } - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" - }, - "colors": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/colors/-/colors-0.6.2.tgz", - "integrity": "sha1-JCP+ZnisDF2uiFLl0OW+CMmXq8w=" - }, - "combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, - "requires": { - "delayed-stream": "~1.0.0" - } - }, - "commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" - }, - "component-emitter": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", - "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==" - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" - }, "config-chain": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.12.tgz", @@ -2047,7 +2630,6 @@ "version": "1.7.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz", "integrity": "sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==", - "dev": true, "requires": { "safe-buffer": "~5.1.1" } @@ -2073,11 +2655,25 @@ "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=" }, + "core-js": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.8.1.tgz", + "integrity": "sha512-9Id2xHY1W7m8hCl8NkhQn5CufmF/WuR30BTRewvCXc1aZd3kMECwNZ69ndLbekKfakw9Rf2Xyc+QR6E7Gg+obg==" + }, "core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" }, + "cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "requires": { + "object-assign": "^4", + "vary": "^1" + } + }, "cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -2152,6 +2748,12 @@ "whatwg-url": "^8.0.0" } }, + "date-fns": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.16.1.tgz", + "integrity": "sha512-sAJVKx/FqrLYHAQeN7VpJrPhagZc9R4ImZIWYRFZaaohR3KzmuK88touwsSwSVT8Qcbd4zoDsnGfX4GFB4imyQ==", + "dev": true + }, "debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -2563,8 +3165,7 @@ "escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "dev": true + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" }, "escodegen": { "version": "1.14.3", @@ -3288,7 +3889,6 @@ "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, "requires": { "to-regex-range": "^5.0.1" } @@ -3307,6 +3907,83 @@ "unpipe": "~1.0.0" } }, + "find-cache-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", + "integrity": "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==", + "requires": { + "commondir": "^1.0.1", + "make-dir": "^2.0.0", + "pkg-dir": "^3.0.0" + }, + "dependencies": { + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "requires": { + "locate-path": "^3.0.0" + } + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "requires": { + "pify": "^4.0.1", + "semver": "^5.6.0" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "requires": { + "p-limit": "^2.0.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" + }, + "pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==" + }, + "pkg-dir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", + "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", + "requires": { + "find-up": "^3.0.0" + } + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + } + } + }, "find-up": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", @@ -3721,6 +4398,11 @@ "universalify": "^0.1.0" } }, + "fs-readdir-recursive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz", + "integrity": "sha512-GNanXlVr2pf02+sPN40XN8HG+ePaNcvM0q5mZBd668Obwb0yD5GiUbZOFgwn8kGMY6I3mdyDJzieUy3PTYyTRA==" + }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -3730,7 +4412,6 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz", "integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==", - "dev": true, "optional": true }, "function-bind": { @@ -3755,8 +4436,7 @@ "gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==" }, "get-caller-file": { "version": "2.0.5", @@ -3825,7 +4505,6 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", - "dev": true, "requires": { "is-glob": "^4.0.1" } @@ -3870,6 +4549,11 @@ } } }, + "google-libphonenumber": { + "version": "3.2.15", + "resolved": "https://registry.npmjs.org/google-libphonenumber/-/google-libphonenumber-3.2.15.tgz", + "integrity": "sha512-tbCIuzMoH34RdrbFRw5kijAZn/p6JMQvsgtr1glg2ugbwqrMPlOL8pHNK8cyGo9B6SXpcMm4hdyDqwomR+HPRg==" + }, "got": { "version": "9.6.0", "resolved": "https://registry.npmjs.org/got/-/got-9.6.0.tgz", @@ -3928,8 +4612,7 @@ "has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" }, "has-symbols": { "version": "1.0.1", @@ -3989,6 +4672,14 @@ "integrity": "sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw==", "dev": true }, + "homedir-polyfill": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", + "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==", + "requires": { + "parse-passwd": "^1.0.0" + } + }, "hosted-git-info": { "version": "2.8.8", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz", @@ -4234,7 +4925,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, "requires": { "binary-extensions": "^2.0.0" } @@ -4366,8 +5056,7 @@ "is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" }, "is-obj": { "version": "2.0.0", @@ -5200,8 +5889,7 @@ "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, "js-yaml": { "version": "3.14.0", @@ -5256,8 +5944,7 @@ "jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", - "dev": true + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==" }, "json-buffer": { "version": "3.0.0", @@ -5265,6 +5952,12 @@ "integrity": "sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg=", "dev": true }, + "json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "dev": true + }, "json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", @@ -5923,6 +6616,22 @@ "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", "dev": true }, + "node-environment-flags": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/node-environment-flags/-/node-environment-flags-1.0.6.tgz", + "integrity": "sha512-5Evy2epuL+6TM0lCQGpFIj6KwiEsGh1SrHUhTbNX+sLbBtjidPZFAnVK9y5yU1+h//RitLbRHTIMyxQPtxMdHw==", + "requires": { + "object.getownpropertydescriptors": "^2.0.3", + "semver": "^5.7.0" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + } + } + }, "node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -5932,8 +6641,7 @@ "node-modules-regexp": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/node-modules-regexp/-/node-modules-regexp-1.0.0.tgz", - "integrity": "sha1-jZ2+KJZKSsVxLpExZCEHxx6Q7EA=", - "dev": true + "integrity": "sha1-jZ2+KJZKSsVxLpExZCEHxx6Q7EA=" }, "node-notifier": { "version": "8.0.0", @@ -6189,6 +6897,37 @@ "has": "^1.0.3" } }, + "object.getownpropertydescriptors": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.1.tgz", + "integrity": "sha512-6DtXgZ/lIZ9hqx4GtZETobXLR/ZLaa0aqV0kzbn80Rf8Z2e/XFnhA0I7p07N2wH8bBBltr2xQPi6sbKWAY2Eng==", + "requires": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "es-abstract": "^1.18.0-next.1" + }, + "dependencies": { + "es-abstract": { + "version": "1.18.0-next.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.1.tgz", + "integrity": "sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA==", + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.2", + "is-negative-zero": "^2.0.0", + "is-regex": "^1.1.1", + "object-inspect": "^1.8.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.1", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + } + } + }, "object.pick": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", @@ -6357,6 +7096,11 @@ "error-ex": "^1.2.0" } }, + "parse-passwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", + "integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=" + }, "parse5": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz", @@ -6498,8 +7242,7 @@ "picomatch": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", - "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==", - "dev": true + "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==" }, "pify": { "version": "2.3.0", @@ -6511,7 +7254,6 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.1.tgz", "integrity": "sha512-WuNqLTbMI3tmfef2TKxlQmAiLHKtFhlsCZnPIpuv2Ow0RDVO8lfy1Opf4NUzlMXLjPl+Men7AuVdX6TA+s+uGA==", - "dev": true, "requires": { "node-modules-regexp": "^1.0.0" } @@ -6804,11 +7546,15 @@ "version": "3.5.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.5.0.tgz", "integrity": "sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ==", - "dev": true, "requires": { "picomatch": "^2.2.1" } }, + "regenerator-runtime": { + "version": "0.13.7", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz", + "integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==" + }, "regex-not": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", @@ -7052,6 +7798,15 @@ "integrity": "sha512-zb/1OuZ6flOlH6tQyMPUrE3x3Ulxjlo9WIVXR4yVYi4H9UXQaeIsPbLn2R3O3vQCnDKkAl2qHiuocKKX4Tz/Sw==", "dev": true }, + "rxjs": { + "version": "6.6.3", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.3.tgz", + "integrity": "sha512-trsQc+xYYXZ3urjOiJOuCOa5N3jAZ3eiSpQB5hIT8zGlL2QfnHLJ2r7GMkBGuIausdJN1OneaI6gQlsqNHHmZQ==", + "dev": true, + "requires": { + "tslib": "^1.9.0" + } + }, "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", @@ -7655,8 +8410,7 @@ "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" }, "source-map-resolve": { "version": "0.5.3", @@ -7674,7 +8428,6 @@ "version": "0.5.19", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", - "dev": true, "requires": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" @@ -7685,6 +8438,12 @@ "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=" }, + "spawn-command": { + "version": "0.0.2-1", + "resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2-1.tgz", + "integrity": "sha1-YvXpRmmBwbeW3Fkpk34RycaSG9A=", + "dev": true + }, "spdx-correct": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", @@ -8039,7 +8798,6 @@ "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, "requires": { "has-flag": "^3.0.0" } @@ -8171,8 +8929,7 @@ "to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", - "dev": true + "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=" }, "to-object-path": { "version": "0.3.0", @@ -8213,7 +8970,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, "requires": { "is-number": "^7.0.0" } @@ -8252,6 +9008,12 @@ "punycode": "^2.1.1" } }, + "tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true + }, "tsconfig-paths": { "version": "3.9.0", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.9.0.tgz", @@ -8267,8 +9029,7 @@ "tslib": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" }, "tsutils": { "version": "3.17.1", @@ -8596,6 +9357,14 @@ } } }, + "v8flags": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.2.0.tgz", + "integrity": "sha512-mH8etigqMfiGWdeXpaaqGfs6BndypxusHHcv2qSHyZkGEznCd/qAXCWWRzeowtL54147cktFOC4P5y+kl8d8Jg==", + "requires": { + "homedir-polyfill": "^1.0.1" + } + }, "validate-npm-package-license": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", diff --git a/server/package.json b/server/package.json index 2b8a8feb..9b320712 100644 --- a/server/package.json +++ b/server/package.json @@ -3,11 +3,13 @@ "version": "0.0.1", "private": true, "scripts": { - "start": "node ./src/app.js", - "dev": "nodemon ./src/app.js", + "build": "babel src --out-dir dist", + "start": "forever ./dist/app.js", + "dev": "nodemon --exec babel-node ./src/app.js", "seed": "npx sequelize-cli db:seed:all", "unseed": "npx sequelize-cli db:seed:undo:all", - "test": "jest --detectOpenHandles --forceExit" + "test": "jest --detectOpenHandles --forceExit", + "concurrent": "concurrently \"npm run dev\" \"npm run serve --prefix ../client\"" }, "jest": { "moduleNameMapper": { @@ -20,12 +22,20 @@ "^@utils/(.*)$": "/src/utils/$1", "^@routes(.*)$": "/src/routes/$1", "^@routes/(.*)$": "/src/routes/$1", + "^@services/(.*)$": "/src/services/$1", "^@controllers/(.*)$": "/src/controllers/$1", "^@passport(.*)$": "/src/passport/$1", "^@passport/(.*)$": "/src/passport/$1" } }, "dependencies": { + "@babel/cli": "^7.12.8", + "@babel/core": "^7.12.3", + "@babel/node": "^7.12.6", + "@babel/plugin-proposal-class-properties": "^7.12.1", + "class-transformer": "^0.3.1", + "class-validator": "^0.12.2", + "cors": "^2.8.5", "dotenv": "^8.2.0", "express": "~4.16.1", "forever": "^3.0.4", @@ -41,6 +51,9 @@ "uuid": "^8.3.1" }, "devDependencies": { + "@babel/eslint-parser": "^7.12.1", + "@babel/plugin-proposal-decorators": "^7.12.1", + "concurrently": "^5.3.0", "eslint": "^7.13.0", "eslint-config-airbnb-base": "^14.2.0", "eslint-config-prettier": "^6.15.0", diff --git a/server/src/app.js b/server/src/app.js index 358f7754..a394aeca 100644 --- a/server/src/app.js +++ b/server/src/app.js @@ -7,7 +7,8 @@ const loader = require('@root/loaders'); const startServer = () => { const app = express(); loader(app); - app.listen(process.env.PORT); + const port = process.env.NODE_ENV === 'test' ? process.env.TEST_PORT : process.env.PORT; + app.listen(port); return app; }; diff --git a/server/src/controllers/comment.js b/server/src/controllers/comment.js new file mode 100644 index 00000000..8a040bd2 --- /dev/null +++ b/server/src/controllers/comment.js @@ -0,0 +1,29 @@ +const commentService = require('@services/comment'); +const { asyncTryCatch } = require('@utils/async-try-catch'); +const { responseHandler } = require('@utils/handler'); + +const getComments = asyncTryCatch(async (req, res) => { + const comments = await commentService.retrieveAllByTaskId(req.params.taskId); + + responseHandler(res, 200, comments); +}); + +const createComment = asyncTryCatch(async (req, res) => { + await commentService.create(req.params.taskId, req.body); + + responseHandler(res, 201, { message: 'ok' }); +}); + +const updateComment = asyncTryCatch(async (req, res) => { + await commentService.update(req.params.commentId, req.body); + + responseHandler(res, 200, { message: 'ok' }); +}); + +const deleteComment = asyncTryCatch(async (req, res) => { + await commentService.remove(req.params.commentId); + + responseHandler(res, 200, { message: 'ok' }); +}); + +module.exports = { getComments, createComment, updateComment, deleteComment }; diff --git a/server/src/controllers/label.js b/server/src/controllers/label.js index 603df685..7e6fa081 100644 --- a/server/src/controllers/label.js +++ b/server/src/controllers/label.js @@ -1,60 +1,39 @@ -const labelModel = require('@models').models.label; +const labelService = require('@services/label'); const { responseHandler } = require('@utils/handler'); +const { asyncTryCatch } = require('@utils/async-try-catch'); -const getAllLabels = async (req, res, next) => { - try { - const labels = await labelModel.findAll({ - where: { userId: req.user.id }, - attributes: ['id', 'title', 'color'], - }); +const getAllLabels = asyncTryCatch(async (req, res) => { + const labels = await labelService.retrieveAll(req.user.id); - responseHandler(res, 200, { labels }); - } catch (err) { - next(err); - } -}; + responseHandler(res, 200, labels); +}); -const createLabel = async (req, res, next) => { - try { - const { title, color } = req.body; - const result = await labelModel.create({ title, color, userId: req.user.id }); - if (result) { - responseHandler(res, 201); - } else { - throw Error('Internal Server Error'); - } - } catch (err) { - next(err); +const createLabel = asyncTryCatch(async (req, res) => { + const isCreated = await labelService.create({ userId: req.user.id, ...req.body }); + + if (!isCreated) { + throw Error('Internal Server Error'); } -}; + responseHandler(res, 201); +}); -const updateLabel = async (req, res, next) => { - const { labelId } = req.params; - const { title, color } = req.body; +const updateLabel = asyncTryCatch(async (req, res) => { + const isUpdated = await labelService.update({ id: req.params.labelId, ...req.body }); - try { - const result = await labelModel.update({ title, color }, { where: { id: labelId } }); - if (result[0] !== 1) { - throw Error('Internal Server Error'); - } - responseHandler(res, 200); - } catch (err) { - next(err); + if (!isUpdated) { + throw Error('Internal Server Error'); } -}; + responseHandler(res, 200); +}); -const removeLabel = async (req, res, next) => { - const { labelId } = req.params; - try { - const result = await labelModel.destroy({ where: { id: labelId } }); - if (result !== 1) { - throw Error('Internal Server Error'); - } - responseHandler(res, 200); - } catch (err) { - next(err); +const removeLabel = asyncTryCatch(async (req, res) => { + const result = await labelService.remove(req.params.labelId); + + if (!result) { + throw Error('Internal Server Error'); } -}; + responseHandler(res, 200); +}); const isValidRequestDatas = (req, res, next) => { const error = new Error('Bad Request'); @@ -73,40 +52,33 @@ const isValidRequestDatas = (req, res, next) => { } }; -const isOwnLabel = async (req, res, next) => { +const isOwnLabel = asyncTryCatch(async (req, res, next) => { const { labelId } = req.params; const { id } = req.user; - const error = new Error('Forbidden'); - error.status = 403; - try { - const label = await labelModel.findByPk(labelId, { attributes: ['userId'] }); - if (id !== label.userId) { - throw error; - } - next(); - } catch (err) { - next(err); + const label = await labelService.retrieveById(labelId); + if (id !== label.userId) { + const error = new Error('Forbidden'); + error.status = 403; + throw error; } -}; + next(); +}); -const isValidLabelId = async (req, res, next) => { +const isValidLabelId = asyncTryCatch(async (req, res, next) => { const { labelId } = req.params; const error = new Error('Not Found'); error.status = 404; - try { - if (!labelId || labelId === '') { - throw error; - } - const label = await labelModel.findByPk(labelId); - if (!label) { - throw error; - } - next(); - } catch (err) { - next(err); + + if (!labelId || labelId === '') { + throw error; } -}; + const label = await labelService.retrieveById(labelId); + if (!label) { + throw error; + } + next(); +}); module.exports = { getAllLabels, diff --git a/server/src/controllers/project.js b/server/src/controllers/project.js index 15dba932..c5df9c27 100644 --- a/server/src/controllers/project.js +++ b/server/src/controllers/project.js @@ -1,168 +1,79 @@ -const sequelize = require('@models'); -const { Op } = require('sequelize'); - -const { models } = sequelize; +const ProjectDto = require('@models/dto/project'); +const projectService = require('@services/project'); const { responseHandler } = require('@utils/handler'); const { asyncTryCatch } = require('@utils/async-try-catch'); -const getTodayStartEnd = require('@utils/today-start-end'); +const { validator, getErrorMsg } = require('@utils/validator'); const getProjects = asyncTryCatch(async (req, res) => { - const projects = await models.project.findAll({ - attributes: ['id', 'title', [sequelize.fn('COUNT', sequelize.col('tasks.id')), 'taskCount']], - include: { - model: models.task, - attributes: [], - }, - group: ['project.id'], - }); - - const { todayStart, todayEnd } = getTodayStartEnd(); - - const todayProject = { - title: '오늘', - }; - todayProject.taskCount = await models.task.count({ - where: { - dueDate: { - [Op.and]: { - [Op.gt]: todayStart, - [Op.lt]: todayEnd, - }, - }, - }, - }); - projects.push(todayProject); - - responseHandler(res, 201, projects); -}); + const projects = await projectService.retrieveProjects(); -const getProjectById = asyncTryCatch(async (req, res) => { - const project = await models.project.findByPk(req.params.projectId, { - attributes: ['id', 'title', 'isList'], - include: { - model: models.section, - include: { - model: models.task, - where: { parentId: null }, - include: ['priority', 'labels', 'alarm', 'tasks'], - }, - }, - order: [ - [models.section, models.task, 'position', 'ASC'], - [models.section, models.task, models.task, 'position', 'ASC'], - ], - }); - - responseHandler(res, 201, project); + responseHandler(res, 200, projects); }); -const createProject = asyncTryCatch(async (req, res) => { - await sequelize.transaction(async t => { - const project = await models.project.create(req.body, { - transaction: t, - }); - const section = await models.section.create( - {}, - { - transaction: t, - }, - ); - await section.setProject(project, { - transaction: t, - }); - }); - - responseHandler(res, 201, { - message: 'ok', - }); -}); +const getTodayProject = asyncTryCatch(async (req, res) => { + const todayProject = await projectService.retrieveTodayProject(); -const updateProject = asyncTryCatch(async (req, res) => { - await models.project.update(req.body, { - where: { - id: req.params.projectId, - }, - }); - responseHandler(res, 201, { - message: 'ok', - }); + responseHandler(res, 200, todayProject); }); -const deleteProject = asyncTryCatch(async (req, res) => { - await models.project.destroy({ - where: { - id: req.params.projectId, - }, - }); - responseHandler(res, 201, { - message: 'ok', - }); -}); +const getProjectById = asyncTryCatch(async (req, res) => { + const project = await projectService.retrieveById(req.params.projectId); -const createSection = asyncTryCatch(async (req, res) => { - await sequelize.transaction(async t => { - const project = await models.project.findByPk(req.params.projectId); - const section = await models.section.create(req.body, { - transaction: t, - }); - await section.setProject(project, { - transaction: t, - }); - }); - - responseHandler(res, 201, { - message: 'ok', - }); + responseHandler(res, 200, project); }); -const updateSectionTaskPositions = asyncTryCatch(async (req, res) => { - const { orderedTasks } = req.body; - - await sequelize.transaction(async t => { - await Promise.all( - orderedTasks.map(async (taskId, position) => { - await models.task.update({ position }, { where: { id: taskId } }, { transaction: t }); - }), - ); - }); +const createProject = asyncTryCatch(async (req, res) => { + const { id: creatorId } = req.user; - responseHandler(res, 201, { - message: 'ok', - }); -}); + try { + await validator(ProjectDto, req.body, { groups: ['create'] }); + } catch (errs) { + const message = getErrorMsg(errs); + const err = new Error(message); + err.status = 400; + throw err; + } -const updateSection = asyncTryCatch(async (req, res) => { - await models.section.update(req.body, { - where: { - id: req.params.sectionId, - }, - }); + const projectId = await projectService.create({ creatorId, ...req.body }); - responseHandler(res, 201, { - message: 'ok', - }); + responseHandler(res, 201, { message: 'ok', projectId }); }); -const deleteSection = asyncTryCatch(async (req, res) => { - await models.project.destroy({ - where: { - id: req.params.sectionId, - }, - }); +const updateProject = asyncTryCatch(async (req, res) => { + try { + await validator(ProjectDto, { id: req.params.projectId, ...req.body }); + } catch (errs) { + const message = getErrorMsg(errs); + const err = new Error(message); + err.status = 400; + throw err; + } + + await projectService.update({ id: req.params.projectId, ...req.body }); + + responseHandler(res, 200, { message: 'ok' }); +}); - responseHandler(res, 201, { - message: 'ok', - }); +const deleteProject = asyncTryCatch(async (req, res) => { + try { + await validator(ProjectDto, { id: req.params.projectId }); + } catch (errs) { + const message = getErrorMsg(errs); + const err = new Error(message); + err.status = 400; + throw err; + } + + await projectService.remove({ id: req.params.projectId }); + + responseHandler(res, 200, { message: 'ok' }); }); module.exports = { getProjects, + getTodayProject, getProjectById, createProject, updateProject, deleteProject, - createSection, - updateSection, - updateSectionTaskPositions, - deleteSection, }; diff --git a/server/src/controllers/section.js b/server/src/controllers/section.js new file mode 100644 index 00000000..6e3abc78 --- /dev/null +++ b/server/src/controllers/section.js @@ -0,0 +1,59 @@ +const SectionDto = require('@models/dto/section'); +const sectionService = require('@services/section'); +const { validator, getErrorMsg } = require('@utils/validator'); +const { responseHandler } = require('@utils/handler'); +const { asyncTryCatch } = require('@utils/async-try-catch'); + +const createSection = asyncTryCatch(async (req, res) => { + const { projectId } = req.params; + try { + await validator(SectionDto, req.body, { groups: ['create'] }); + } catch (errs) { + const message = getErrorMsg(errs); + const err = new Error(message); + err.status = 400; + throw err; + } + + // TODO projectId를 따로 빼야 하나 ? + await sectionService.create({ projectId, ...req.body }); + + responseHandler(res, 201, { message: 'ok' }); +}); + +const updateTaskPositions = asyncTryCatch(async (req, res) => { + await sectionService.updateTaskPositions(req.body.orderedTasks); + + responseHandler(res, 200, { message: 'ok' }); +}); + +const updateSection = asyncTryCatch(async (req, res) => { + const { sectionId } = req.params; + + try { + await validator(SectionDto, req.body, { groups: ['update'] }); + } catch (errs) { + const message = getErrorMsg(errs); + const err = new Error(message); + err.status = 400; + throw err; + } + + // TODO sectionId를 따로 빼야 하나 ? + await sectionService.update({ id: sectionId, ...req.body }); + + responseHandler(res, 200, { message: 'ok' }); +}); + +const deleteSection = asyncTryCatch(async (req, res) => { + await sectionService.remove(req.params.sectionId); + + responseHandler(res, 200, { message: 'ok' }); +}); + +module.exports = { + createSection, + updateSection, + updateTaskPositions, + deleteSection, +}; diff --git a/server/src/controllers/task.js b/server/src/controllers/task.js index 410b8ffb..83d70c5f 100644 --- a/server/src/controllers/task.js +++ b/server/src/controllers/task.js @@ -1,115 +1,67 @@ -const sequelize = require('@models'); -const { models } = require('@models'); +const TaskDto = require('@models/dto/task'); +const taskService = require('@services/task'); +const { validator, getErrorMsg } = require('@utils/validator'); const { asyncTryCatch } = require('@utils/async-try-catch'); const { responseHandler } = require('@utils/handler'); -const { isValidDueDate } = require('@utils/date'); const getTaskById = asyncTryCatch(async (req, res) => { - const task = await models.task.findByPk(req.params.taskId, { - include: [ - 'labels', - 'priority', - 'alarm', - 'bookmarks', - { - model: models.task, - include: ['labels', 'priority', 'alarm', 'bookmarks'], - }, - ], - order: [[models.task, 'position', 'ASC']], - }); - - responseHandler(res, 201, task); -}); - -const createOrUpdateTask = asyncTryCatch(async (req, res) => { - const { labelIdList, dueDate, ...rest } = req.body; - - if (!isValidDueDate(dueDate)) { - const err = new Error('유효하지 않은 dueDate'); + const id = req.params.taskId; + try { + await validator(TaskDto, { id }, { groups: ['retrieve'] }); + } catch (errs) { + const message = getErrorMsg(errs); + const err = new Error(message); err.status = 400; throw err; } - const { taskId } = req.params; - await sequelize.transaction(async t => { - let task; - if (taskId !== undefined) { - await models.task.update( - { dueDate, ...rest }, - { - where: { id: taskId }, - }, - { transaction: t }, - ); - task = await models.task.findByPk(taskId, { transaction: t }); - } - - task = await models.task.create({ dueDate, ...rest }, { transaction: t }); - await task.setLabels(JSON.parse(labelIdList), { transaction: t }); - }); + const task = await taskService.retrieveById(id); - responseHandler(res, 201, { message: 'ok' }); + responseHandler(res, 200, { task }); }); -const deleteTask = asyncTryCatch(async (req, res) => { - await models.task.destroy({ - where: { - id: req.params.taskId, - }, - }); +const getAllTasks = asyncTryCatch(async (req, res) => { + const tasks = await taskService.retrieveAll(req.user.id); - responseHandler(res, 201, { - message: 'ok', - }); + responseHandler(res, 200, { tasks }); }); -const getComments = asyncTryCatch(async (req, res) => { - const task = await models.task.findByPk(req.params.taskId); - const comments = await task.getComments(); +const createTask = asyncTryCatch(async (req, res) => { + try { + await validator(TaskDto, req.body, { groups: ['create'] }); + } catch (errs) { + const message = getErrorMsg(errs); + const err = new Error(message); + err.status = 400; + throw err; + } + const { projectId, sectionId } = req.params; + const task = { ...req.body, projectId, sectionId }; - responseHandler(res, 201, comments); + await taskService.create(task); + responseHandler(res, 201, { message: 'ok' }); }); -const createComment = asyncTryCatch(async (req, res) => { +const updateTask = asyncTryCatch(async (req, res) => { const { taskId } = req.params; - await models.comment.create({ ...req.body, taskId }); + const task = { ...req.body }; - responseHandler(res, 201, { - message: 'ok', - }); -}); - -const updateComment = asyncTryCatch(async (req, res) => { - await models.comment.update(req.body, { - where: { - id: req.params.commentId, - }, - }); + try { + await validator(TaskDto, task, { groups: ['patch'] }); + } catch (errs) { + const message = getErrorMsg(errs); + const err = new Error(message); + err.status = 400; + throw err; + } - responseHandler(res, 201, { - message: 'ok', - }); + await taskService.update({ id: taskId, ...task }); + responseHandler(res, 200, { message: 'ok' }); }); -const deleteComment = asyncTryCatch(async (req, res) => { - await models.comment.destroy({ - where: { - id: req.params.commentId, - }, - }); - - responseHandler(res, 201, { - message: 'ok', - }); +const deleteTask = asyncTryCatch(async (req, res) => { + await taskService.remove(req.params.taskId); + responseHandler(res, 200, { message: 'ok' }); }); -module.exports = { - getTaskById, - createOrUpdateTask, - deleteTask, - getComments, - createComment, - updateComment, - deleteComment, -}; +module.exports = { getTaskById, getAllTasks, createTask, updateTask, deleteTask }; diff --git a/server/src/controllers/user.js b/server/src/controllers/user.js index 12dbafd7..b1e82497 100644 --- a/server/src/controllers/user.js +++ b/server/src/controllers/user.js @@ -1,28 +1,19 @@ const { responseHandler } = require('@utils/handler'); const { createJWT } = require('@utils/auth'); +const { asyncTryCatch } = require('@utils/async-try-catch'); -const naverLogin = (req, res, next) => { - const clientURL = - process.env.NODE_ENV === 'development' - ? process.env.CLIENT_DOMAIN_DEVELOP - : process.env.CLIENT_DOMAIN_PRODUCTION; - try { - const { user } = req; - const token = createJWT(user); - res.header('Authentication', token); - res.status(200).redirect(`${clientURL}?token=${token}`); - } catch (err) { - next(err); - } -}; +const naverLogin = asyncTryCatch(async (req, res) => { + const { CLIENT_URL } = process.env; + const { user } = req; + const token = createJWT(user); -const getOwnInfo = (req, res, next) => { - try { - const { user } = req; - responseHandler(res, 200, user); - } catch (err) { - next(err); - } -}; + res.header('Authentication', token); + res.status(200).redirect(`${CLIENT_URL}?token=${token}`); +}); + +const getOwnInfo = asyncTryCatch(async (req, res) => { + const { user } = req; + responseHandler(res, 200, user); +}); module.exports = { naverLogin, getOwnInfo }; diff --git a/server/src/loaders/express.js b/server/src/loaders/express.js index e3e9b317..731a692c 100644 --- a/server/src/loaders/express.js +++ b/server/src/loaders/express.js @@ -1,5 +1,6 @@ const express = require('express'); const logger = require('morgan'); +const cors = require('cors'); const passport = require('passport'); const passportConfig = require('@passport'); @@ -9,6 +10,7 @@ const expressLoader = app => { app.use(logger('dev')); app.use(express.json()); app.use(express.urlencoded({ extended: false })); + app.use(cors()); // TODO localhost:8080 만 허용하도록 변경해야 함 app.use(passport.initialize()); passportConfig(passport); diff --git a/server/src/models/dto/project.js b/server/src/models/dto/project.js new file mode 100644 index 00000000..b001cb36 --- /dev/null +++ b/server/src/models/dto/project.js @@ -0,0 +1,36 @@ +const { + IsString, + ValidateIf, + IsHexColor, + IsUUID, + MinLength, + IsBoolean, +} = require('class-validator'); +const errorMessage = require('@utils/error-messages'); + +class ProjectDto { + @ValidateIf(o => !!o.id) + @IsString() + @IsUUID('4') + id; + + @ValidateIf(o => !!o.title) + @IsString({ groups: ['create'], message: errorMessage.TYPE_ERROR('title') }) + @MinLength(1, { groups: ['create'], message: errorMessage.INVALID_INPUT_ERROR('title') }) + title; + + @ValidateIf(o => !!o.color) + @IsString({ groups: ['create'], message: errorMessage.TYPE_ERROR('color') }) + @IsHexColor({ groups: ['created'], message: errorMessage.INVALID_INPUT_ERROR('color') }) + color; + + @ValidateIf(o => !!o.isList) + @IsBoolean({ groups: ['create'], message: errorMessage.TYPE_ERROR('isList') }) + isList; + + @ValidateIf(o => !!o.isFavorite) + @IsBoolean({ message: errorMessage.TYPE_ERROR('isFavorite') }) + isFavorite; +} + +module.exports = ProjectDto; diff --git a/server/src/models/dto/section.js b/server/src/models/dto/section.js new file mode 100644 index 00000000..4fdc5866 --- /dev/null +++ b/server/src/models/dto/section.js @@ -0,0 +1,16 @@ +const { IsUUID, IsString, IsInt, MinLength } = require('class-validator'); +const errorMessage = require('@models/dto/error-messages'); + +class SectionDto { + @IsUUID('4') + id; + + @IsString({ groups: ['create', 'update'] }, { message: errorMessage.wrongProperty('title') }) + @MinLength(1, { groups: ['create', 'update'] }, { message: errorMessage.wrongProperty('title') }) + title; + + @IsInt() + position; +} + +module.exports = SectionDto; diff --git a/server/src/models/dto/task.js b/server/src/models/dto/task.js new file mode 100644 index 00000000..a50be43c --- /dev/null +++ b/server/src/models/dto/task.js @@ -0,0 +1,70 @@ +const { + IsInt, + IsString, + IsBoolean, + MinLength, + IsDateString, + IsUUID, + IsOptional, + IsEmpty, +} = require('class-validator'); +const errorMessage = require('@utils/error-messages'); +const { isAfterToday } = require('@utils/validator'); + +class TaskDto { + @IsEmpty({ groups: ['create', 'patch'], message: errorMessage.UNNECESSARY_INPUT_ERROR('id') }) + @IsString({ groups: ['retrieve'], message: errorMessage.TYPE_ERROR('id') }) + @IsUUID('4', { groups: ['retrieve'], message: errorMessage.INVALID_INPUT_ERROR('id') }) + id; + + @IsOptional({ groups: ['patch'] }) + @IsString({ groups: ['create', 'patch'], message: errorMessage.TYPE_ERROR('title') }) + @MinLength(1, { groups: ['create', 'patch'], message: errorMessage.INVALID_INPUT_ERROR('title') }) + title; + + @IsOptional({ groups: ['patch'] }) + @IsDateString( + { strict: true }, + { groups: ['create', 'patch'], message: errorMessage.TYPE_ERROR('dueDate') }, + ) + @isAfterToday('dueDate', { groups: ['create', 'patch'], message: errorMessage.DUEDATE_ERROR }) + dueDate; + + @IsOptional({ groups: ['patch'] }) + @IsInt({ groups: ['patch'], message: errorMessage.TYPE_ERROR('position') }) + position; + + @IsOptional({ groups: ['patch'] }) + @IsBoolean({ groups: ['patch'], message: errorMessage.TYPE_ERROR('isDone') }) + isDone; + + @IsOptional({ groups: ['create', 'patch'] }) + @IsString({ groups: ['create', 'patch'], message: errorMessage.TYPE_ERROR('parentId') }) + @IsUUID('4', { + groups: ['create', 'patch'], + message: errorMessage.INVALID_INPUT_ERROR('parentId'), + }) + parentId; + + @IsEmpty({ groups: ['create'], message: errorMessage.UNNECESSARY_INPUT_ERROR('sectionId') }) + @IsOptional({ groups: ['patch'] }) + @IsString({ groups: ['patch'], message: errorMessage.TYPE_ERROR('sectionId') }) + @IsUUID('4', { groups: ['patch'], message: errorMessage.INVALID_INPUT_ERROR('sectionId') }) + sectionId; + + @IsEmpty({ groups: ['create'], message: errorMessage.UNNECESSARY_INPUT_ERROR('projectId') }) + @IsOptional({ groups: ['patch'] }) + @IsString({ groups: ['patch'], message: errorMessage.TYPE_ERROR('projectId') }) + @IsUUID('4', { groups: ['patch'], message: errorMessage.INVALID_INPUT_ERROR('projectId') }) + projectId; + + @IsOptional({ groups: ['patch'] }) + @IsUUID('4', { groups: ['patch'], message: errorMessage.INVALID_INPUT_ERROR('priorityId') }) + priorityId; + + @IsOptional({ groups: ['patch'] }) + @IsUUID('4', { groups: ['patch'], message: errorMessage.INVALID_INPUT_ERROR('alarmId') }) + alarmId; +} + +module.exports = TaskDto; diff --git a/server/src/models/project.js b/server/src/models/project.js index d6646f46..3e56c26a 100644 --- a/server/src/models/project.js +++ b/server/src/models/project.js @@ -13,6 +13,10 @@ module.exports = sequelize => { allowNull: false, type: DataTypes.STRING, }, + color: { + allowNull: true, + type: DataTypes.STRING, + }, isList: { allowNull: false, type: DataTypes.BOOLEAN, diff --git a/server/src/models/section.js b/server/src/models/section.js index 0f54aecf..162a006e 100644 --- a/server/src/models/section.js +++ b/server/src/models/section.js @@ -12,6 +12,9 @@ module.exports = sequelize => { title: { type: DataTypes.STRING, }, + position: { + type: DataTypes.INTEGER, + }, }, { tableName: 'section' }, ); diff --git a/server/src/models/seeders/index.js b/server/src/models/seeders/index.js index e69de29b..c49c317e 100644 --- a/server/src/models/seeders/index.js +++ b/server/src/models/seeders/index.js @@ -0,0 +1,279 @@ +const users = [ + { + id: 'ff4dd832-1567-4d74-b41d-bd85e96ce329', + email: 'email@example.com', + name: 'tony', + provider: 'naver', + createdAt: new Date(), + updatedAt: new Date(), + }, + { + id: 'd0325b52-b8d2-4623-b537-79b0d01cee1d', + email: 'zin0@naver.com', + name: 'zin0', + provider: 'naver', + createdAt: new Date(), + updatedAt: new Date(), + }, + { + id: '58719e77-5f55-4ca7-9552-ff94fbb07ea4', + email: 'dimple0416@naver.com', + name: '박진영', + provider: 'naver', + createdAt: new Date(), + updatedAt: new Date(), + }, +]; + +const priorities = [ + { + id: 'c3bb8b39-cdad-4db4-ac02-ae506d30ba2a', + title: '우선순위1', + createdAt: new Date(), + updatedAt: new Date(), + }, + { + id: '248d7dd6-9f9b-4bff-b47d-43a8b07c9093', + title: '우선순위2', + createdAt: new Date(), + updatedAt: new Date(), + }, + { + id: 'ac7ac13c-53df-49a3-8617-654e23f3d043', + title: '우선순위3', + createdAt: new Date(), + updatedAt: new Date(), + }, + { + id: '936f1c1d-e169-47c4-b544-1f8a0aff0a8d', + title: '우선순위4', + createdAt: new Date(), + updatedAt: new Date(), + }, +]; + +const projects = [ + { + id: 'b7f253e5-7b6b-4ee2-b94e-369ffcdffb5f', + creatorId: users[0].id, + title: '프로젝트 1', + color: '#FFA7A7', + isList: true, + isFavorite: false, + createdAt: new Date(), + updatedAt: new Date(), + }, + { + id: 'f7605077-96ec-4365-88fc-a9c3af4a084e', + creatorId: users[0].id, + title: '프로젝트 2', + color: '#FFE08C', + isList: false, + isFavorite: true, + createdAt: new Date(), + updatedAt: new Date(), + }, + { + id: 'ed298270-fc55-45d7-bc41-bb63ffd09b4a', + creatorId: users[1].id, + title: '프로젝트 3', + color: '#B7F0B1', + isList: false, + isFavorite: true, + createdAt: new Date(), + updatedAt: new Date(), + }, +]; + +const sections = [ + { + id: '7abf0633-bce2-4972-9249-69f287db8a47', + title: '섹션 1', + projectId: projects[0].id, + position: 0, + createdAt: new Date(), + updatedAt: new Date(), + }, + { + id: 'cccf0633-bce2-4972-9249-69f287db8a47', + title: '섹션 2', + projectId: projects[0].id, + position: 1, + createdAt: new Date(), + updatedAt: new Date(), + }, + { + id: 'dbfc6305-877e-46b9-ac63-d56fb82681dd', + title: '섹션 3', + projectId: projects[2].id, + position: 1, + createdAt: new Date(), + updatedAt: new Date(), + }, +]; + +const tasks = [ + { + id: '13502adf-83dd-4e8e-9acf-5c5a0abd5b1b', + projectId: projects[0].id, + sectionId: sections[0].id, + parentId: null, + priorityId: priorities[0].id, + title: '작업 1', + dueDate: new Date(), + position: 0, + isDone: false, + createdAt: new Date(), + updatedAt: new Date(), + }, + { + id: 'cd62f93c-9233-46a9-a5cf-ec18ad5a36f4', + projectId: projects[0].id, + sectionId: sections[0].id, + title: '작업 2', + dueDate: new Date(), + position: 1, + isDone: false, + createdAt: new Date(), + updatedAt: new Date(), + }, + { + id: '7d62f93c-9233-46a9-a5cf-ec18ad5a36f4', + projectId: projects[0].id, + sectionId: sections[0].id, + title: '작업 3', + dueDate: new Date('2020-10-24T14:23:24.090Z'), + position: 2, + isDone: false, + createdAt: new Date(), + updatedAt: new Date(), + }, + { + id: '8d62f93c-9233-46a9-a5cf-ec18ad5a36f4', + projectId: projects[0].id, + sectionId: sections[0].id, + parentId: '13502adf-83dd-4e8e-9acf-5c5a0abd5b1b', + title: '작업 4', + dueDate: new Date('2020-10-24T14:23:24.090Z'), + position: 0, + isDone: false, + createdAt: new Date(), + updatedAt: new Date(), + }, + { + id: '9d62f93c-9233-46a9-a5cf-ec18ad5a36f4', + projectId: projects[0].id, + sectionId: sections[0].id, + parentId: '13502adf-83dd-4e8e-9acf-5c5a0abd5b1b', + title: '작업 5', + dueDate: new Date('2020-10-24T14:23:24.090Z'), + position: 1, + isDone: false, + createdAt: new Date(), + updatedAt: new Date(), + }, + { + id: '1d62f93c-9233-46a9-a5cf-ec18ad5a36f4', + projectId: projects[0].id, + sectionId: sections[1].id, + title: '작업 6', + dueDate: new Date('2021-10-24T14:23:24.090Z'), + position: 0, + isDone: false, + createdAt: new Date(), + updatedAt: new Date(), + }, + { + id: '1g62f93c-9233-46a9-a5cf-ec18ad5a36f4', + projectId: projects[0].id, + sectionId: sections[1].id, + title: '작업 7', + dueDate: new Date('2021-10-24T14:23:24.090Z'), + position: 1, + isDone: false, + createdAt: new Date(), + updatedAt: new Date(), + }, + { + id: '4a83b457-e67e-43d8-a284-78ea7fc440d5', + projectId: projects[2].id, + sectionId: sections[2].id, + title: '진영', + dueDate: new Date(), + position: 1, + isDone: false, + createdAt: new Date(), + updatedAt: new Date(), + }, +]; + +const comments = [ + { + id: '6200bcb9-f871-439b-9507-57abbde3d468', + content: '댓글 1', + taskId: 'cd62f93c-9233-46a9-a5cf-ec18ad5a36f4', + createdAt: new Date(), + updatedAt: new Date(), + }, +]; + +const labels = [ + { + id: '54eefed3-3652-443f-85c9-7dfe87b23f82', + color: 'red', + title: '라벨 1', + userId: 'ff4dd832-1567-4d74-b41d-bd85e96ce329', + createdAt: new Date(), + updatedAt: new Date(), + }, + { + id: 'f77c5c11-d753-4e76-9a73-7cf89a3fd569', + color: '#FFFFFF', + title: '라벨 2', + userId: 'd0325b52-b8d2-4623-b537-79b0d01cee1d', + createdAt: new Date(), + updatedAt: new Date(), + }, +]; + +const bookmarks = [ + { + id: 'cb8eb131-ad2e-4677-a4e5-c8ec078b28e8', + url: 'https://www.uuidgenerator.net/version4', + createdAt: new Date(), + updatedAt: new Date(), + }, +]; + +const alarms = [ + { + id: 'e23a789c-ce37-45bd-bb6b-20602edfe221', + createdAt: new Date(), + updatedAt: new Date(), + }, +]; + +module.exports = { + up: async queryInterface => { + await queryInterface.bulkInsert('user', users, {}); + await queryInterface.bulkInsert('priority', priorities, {}); + await queryInterface.bulkInsert('project', projects, {}); + await queryInterface.bulkInsert('section', sections, {}); + await queryInterface.bulkInsert('task', tasks, {}); + await queryInterface.bulkInsert('comment', comments, {}); + await queryInterface.bulkInsert('label', labels, {}); + await queryInterface.bulkInsert('bookmark', bookmarks, {}); + await queryInterface.bulkInsert('alarm', alarms, {}); + }, + down: async queryInterface => { + await queryInterface.bulkDelete('user', null, {}); + await queryInterface.bulkDelete('priority', null, {}); + await queryInterface.bulkDelete('project', null, {}); + await queryInterface.bulkDelete('task', null, {}); + await queryInterface.bulkDelete('section', null, {}); + await queryInterface.bulkDelete('comment', null, {}); + await queryInterface.bulkDelete('label', null, {}); + await queryInterface.bulkDelete('bookmark', null, {}); + await queryInterface.bulkDelete('alarm', null, {}); + }, +}; diff --git a/server/src/passport/jwt-strategy.js b/server/src/passport/jwt-strategy.js index 14fbc5f0..d6c02f4c 100644 --- a/server/src/passport/jwt-strategy.js +++ b/server/src/passport/jwt-strategy.js @@ -3,7 +3,7 @@ const { ExtractJwt } = require('passport-jwt'); const userModel = require('@models').models.user; const jwtConfig = { - jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken('authorization'), + jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), secretOrKey: process.env.JWT_SECRET, }; @@ -17,6 +17,7 @@ const jwtVerify = async (payload, done) => { const user = await userModel.findByPk(payload.id, { attributes: { exclude: ['createdAt', 'updatedAt'] }, }); + if (user) { done(null, user.dataValues); } else { diff --git a/server/src/passport/naver-strategy.js b/server/src/passport/naver-strategy.js index 054021bc..e89c6a1f 100644 --- a/server/src/passport/naver-strategy.js +++ b/server/src/passport/naver-strategy.js @@ -1,6 +1,6 @@ const NaverStrategy = require('passport-naver').Strategy; -// const userService = require('@services/user'); -const userModel = require('@models').models.user; +const userService = require('@services/user'); +const projectServire = require('@services/project'); const data = { clientID: process.env.NAVER_CLIENT_ID, @@ -12,13 +12,8 @@ const getNaverUser = async (accessToken, refreshToken, profile, done) => { const NAVER = 'naver'; try { const { email, nickname } = profile._json; - - // let user = await userService.retrieveByEmail(email, NAVER); - const [user] = await userModel.findOrCreate({ - where: { email, provider: NAVER }, - attributes: ['id', 'email', 'name', 'provider'], - defaults: { email, name: nickname, provider: NAVER }, - }); + const [user] = await userService.retrieveOrCreate({ email, nickname, provider: NAVER }); + await projectServire.findOrCreate({ creatorId: user.id, title: '관리함', isList: true }); return done(null, user.toJSON()); } catch (err) { diff --git a/server/src/routes/index.js b/server/src/routes/index.js index f7efe93d..d50d7444 100644 --- a/server/src/routes/index.js +++ b/server/src/routes/index.js @@ -4,10 +4,11 @@ const userRouter = require('@routes/user'); const taskRouter = require('@routes/task'); const projectRouter = require('@routes/project'); const labelRouter = require('@routes/label'); +const { authenticateUser } = require('@utils/auth'); router.use('/user', userRouter); -router.use('/task', taskRouter); -router.use('/project', projectRouter); -router.use('/label', labelRouter); +router.use('/task', authenticateUser, taskRouter); +router.use('/project', authenticateUser, projectRouter); +router.use('/label', authenticateUser, labelRouter); module.exports = router; diff --git a/server/src/routes/label.js b/server/src/routes/label.js index 97c8bfd1..ace00852 100644 --- a/server/src/routes/label.js +++ b/server/src/routes/label.js @@ -1,17 +1,14 @@ const router = require('express').Router(); -const { authenticateUser } = require('@utils/auth'); const labelController = require('@controllers/label'); -router.get('/', authenticateUser, labelController.getAllLabels); -router.post( - '/', - authenticateUser, - labelController.isValidRequestDatas, - labelController.createLabel, -); +// TODO: 리팩토링 해야한다 +// controller에서 validation check는 받은 정보가 맞는 타입인지만 체크 +// middle ware는 controller로 가는 과정에서 체크되거나 추가되는 정보 +// 아래에 own label이나 이런 것들은 서비스에서 책임을 져야한다. +router.get('/', labelController.getAllLabels); +router.post('/', labelController.isValidRequestDatas, labelController.createLabel); router.put( '/:labelId', - authenticateUser, labelController.isValidRequestDatas, labelController.isValidLabelId, labelController.isOwnLabel, @@ -19,7 +16,6 @@ router.put( ); router.delete( '/:labelId', - authenticateUser, labelController.isValidLabelId, labelController.isOwnLabel, labelController.removeLabel, diff --git a/server/src/routes/project.js b/server/src/routes/project.js index 0e004a0d..ab2fa131 100644 --- a/server/src/routes/project.js +++ b/server/src/routes/project.js @@ -1,16 +1,22 @@ const router = require('express').Router(); const projectController = require('@controllers/project'); +const sectionController = require('@controllers/section'); +const taskController = require('@controllers/task'); +// TODO: today 반환하는 라우터, validation check +router.get('/today', projectController.getTodayProject); router.get('/', projectController.getProjects); -router.get('/:projectId', projectController.getProjectById); router.post('/', projectController.createProject); +router.get('/:projectId', projectController.getProjectById); router.put('/:projectId', projectController.updateProject); router.patch('/:projectId', projectController.updateProject); router.delete('/:projectId', projectController.deleteProject); -router.post('/:projectId/section', projectController.createSection); -router.post('/:projectId/section/:sectionId/task', projectController.updateSectionTaskPositions); -router.put('/:projectId/section/:sectionId', projectController.updateSection); -router.delete('/:projectId/section/:sectionId', projectController.deleteSection); +router.post('/:projectId/section', sectionController.createSection); +router.put('/:projectId/section/:sectionId', sectionController.updateSection); +router.delete('/:projectId/section/:sectionId', sectionController.deleteSection); +router.patch('/:projectId/section/:sectionId/position', sectionController.updateTaskPositions); + +router.post('/:projectId/section/:sectionId/task', taskController.createTask); module.exports = router; diff --git a/server/src/routes/task.js b/server/src/routes/task.js index cc94e451..b66a6526 100644 --- a/server/src/routes/task.js +++ b/server/src/routes/task.js @@ -1,14 +1,15 @@ const router = require('express').Router(); const taskController = require('@controllers/task'); +const commentController = require('@controllers/comment'); +router.get('/', taskController.getAllTasks); router.get('/:taskId', taskController.getTaskById); -router.post('/', taskController.createOrUpdateTask); -router.post('/:taskId', taskController.createOrUpdateTask); +router.patch('/:taskId', taskController.updateTask); router.delete('/:taskId', taskController.deleteTask); -router.get('/:taskId/comment', taskController.getComments); -router.post('/:taskId/comment', taskController.createComment); -router.put('/:taskId/comment/:commentId', taskController.updateComment); -router.delete('/:taskId/comment/:commentId', taskController.deleteComment); +router.get('/:taskId/comment', commentController.getComments); +router.post('/:taskId/comment', commentController.createComment); +router.put('/:taskId/comment/:commentId', commentController.updateComment); +router.delete('/:taskId/comment/:commentId', commentController.deleteComment); module.exports = router; diff --git a/server/src/routes/user.js b/server/src/routes/user.js index 15e719a6..eeba913d 100644 --- a/server/src/routes/user.js +++ b/server/src/routes/user.js @@ -2,6 +2,7 @@ const router = require('express').Router(); const { passportNaverAuthenticate, authenticateUser } = require('@utils/auth'); const userController = require('@controllers/user'); +// TODO: refresh token 구현 router.get('/oauth/naver', passportNaverAuthenticate); router.get('/oauth/naver/callback', passportNaverAuthenticate, userController.naverLogin); diff --git a/server/src/services/comment.js b/server/src/services/comment.js new file mode 100644 index 00000000..c2e2752f --- /dev/null +++ b/server/src/services/comment.js @@ -0,0 +1,30 @@ +const taskModel = require('@models').models.task; +const commentModel = require('@models').models.comment; + +const retrieveAllByTaskId = async taskId => { + const task = await taskModel.findByPk(taskId); + const comments = await task.getComments(); + + return comments; +}; + +const create = async (taskId, commentData) => { + const result = await commentModel.create({ ...commentData, taskId }); + + return result; +}; + +const update = async (id, data) => { + const result = await commentModel.update(data, { where: { id } }); + + return result; +}; + +const remove = async id => { + const result = await commentModel.destroy({ + where: { id }, + }); + return result; +}; + +module.exports = { retrieveAllByTaskId, create, update, remove }; diff --git a/server/src/services/label.js b/server/src/services/label.js new file mode 100644 index 00000000..2adb7af3 --- /dev/null +++ b/server/src/services/label.js @@ -0,0 +1,38 @@ +const labelModel = require('@models').models.label; + +const retrieveAll = async userId => { + const labels = await labelModel.findAll({ + where: { userId }, + attributes: ['id', 'title', 'color'], + }); + + return labels; +}; + +const retrieveById = async id => { + const label = await labelModel.findByPk(id, { attributes: ['userId'] }); + + return label; +}; + +const create = async ({ userId, ...data }) => { + const { title, color } = data; + const label = await labelModel.create({ title, color, userId }); + + return label.userId === userId && label.title === title && label.color === color; +}; + +const update = async ({ id, ...data }) => { + const { title, color } = data; + const result = await labelModel.update({ title, color }, { where: { id } }); + + return result[0] === 1; +}; + +const remove = async id => { + const result = await labelModel.destroy({ where: { id } }); + + return result === 1; +}; + +module.exports = { retrieveAll, retrieveById, create, update, remove }; diff --git a/server/src/services/project.js b/server/src/services/project.js new file mode 100644 index 00000000..b950adb0 --- /dev/null +++ b/server/src/services/project.js @@ -0,0 +1,138 @@ +const Op = require('sequelize'); +const sequelize = require('@models'); +const getTodayStartEnd = require('@utils/today-start-end'); + +const { models } = sequelize; +const projectModel = models.project; + +const retrieveProjects = async () => { + const projects = await projectModel.findAll({ + raw: true, + attributes: [ + 'id', + 'title', + 'color', + 'isFavorite', + 'isList', + [sequelize.fn('COUNT', sequelize.col('tasks.id')), 'taskCount'], + [sequelize.col('sections.id'), 'defaultSectionId'], + ], + include: [ + { + model: models.task, + attributes: [], + }, + { + model: models.section, + required: false, + where: { position: 0 }, + attributes: [], + }, + ], + group: ['project.id', 'sections.id'], + }); + + return projects; +}; + +const retrieveTodayProject = async () => { + const TODAY = '오늘'; + + const { todayStart, todayEnd } = getTodayStartEnd(); + + const todayProject = { + title: TODAY, + }; + + todayProject.taskCount = await models.task.count({ + where: { + dueDate: { + [Op.and]: { + [Op.gt]: todayStart, + [Op.lt]: todayEnd, + }, + }, + }, + }); + + return todayProject; +}; + +const retrieveById = async id => { + const project = await projectModel.findByPk(id, { + attributes: ['id', 'title', 'isList'], + include: { + model: models.section, + include: { + model: models.task, + where: { isDone: false, parentId: null }, + include: [ + 'priority', + 'labels', + 'alarm', + { + model: models.task, + where: { isDone: false }, + required: false, + }, + ], + required: false, + }, + }, + order: [ + [models.section, 'position', 'ASC'], + [models.section, models.task, 'position', 'ASC'], + [models.section, models.task, models.task, 'position', 'ASC'], + ], + }); + return project; +}; + +const create = async data => { + const result = await sequelize.transaction(async t => { + const project = await projectModel.create(data, { + transaction: t, + }); + const section = await models.section.create( + { title: '기본 섹션', position: 0 }, + { + transaction: t, + }, + ); + await section.setProject(project, { + transaction: t, + }); + return section.projectId; + }); + + return result; +}; + +const findOrCreate = async data => { + const [result] = await projectModel.findAll({ where: data }); + if (result) return true; + + return await create(data); +}; + +const update = async ({ id, ...data }) => { + const result = await projectModel.update(data, { where: { id } }); + + return result === 1; +}; + +const remove = async ({ id }) => { + const result = await projectModel.destroy({ where: { id } }); + + return result === 1; +}; + +module.exports = { + retrieveProjects, + retrieveTodayProject, + retrieveById, + create, + findOrCreate, + update, + remove, +}; diff --git a/server/src/services/section.js b/server/src/services/section.js new file mode 100644 index 00000000..5415c97b --- /dev/null +++ b/server/src/services/section.js @@ -0,0 +1,59 @@ +/* eslint-disable no-return-await */ +const sequelize = require('@models'); + +const { models } = sequelize; +const sectionModel = models.section; + +const create = async ({ projectId, ...data }) => { + const result = await sequelize.transaction(async t => { + const project = await models.project.findByPk(projectId, { + include: sectionModel, + }); + + const maxPosition = project.toJSON().sections.reduce((max, section) => { + return Math.max(max, section.position); + }, 0); + + const section = await sectionModel.create( + { ...data, position: maxPosition + 1 }, + { transaction: t }, + ); + await section.setProject(project, { + transaction: t, + }); + return section; + }); + + return !!result; +}; + +const update = async ({ id, ...data }) => { + const [result] = await models.section.update(data, { where: { id } }); + + return result !== 0; +}; + +const updateTaskPositions = async orderedTasks => { + const result = await sequelize.transaction(async t => { + return await Promise.all( + orderedTasks.map(async (taskId, position) => { + return await models.task.update( + { position, parentId: null }, + { where: { id: taskId } }, + { transaction: t }, + ); + }), + ); + }); + + return ( + result.length === orderedTasks.length && result.every(countArray => countArray.length !== 0) + ); +}; + +const remove = async id => { + const result = await sectionModel.destroy({ where: { id } }); + + return result === 1; +}; +module.exports = { create, update, updateTaskPositions, remove }; diff --git a/server/src/services/task.js b/server/src/services/task.js new file mode 100644 index 00000000..5cffffc3 --- /dev/null +++ b/server/src/services/task.js @@ -0,0 +1,110 @@ +const sequelize = require('@models'); + +const { models } = sequelize; +const taskModel = models.task; + +const retrieveById = async id => { + const task = await taskModel.findByPk(id, { + include: [ + 'labels', + 'priority', + 'alarm', + 'bookmarks', + { + model: taskModel, + include: ['labels', 'priority', 'alarm', 'bookmarks'], + }, + ], + order: [[taskModel, 'position', 'ASC']], + }); + return task; +}; + +const retrieveAll = async userId => { + // const tasks = await taskModel.findAll({ + // attributes: ['id', 'title'], + // include: { + // model: models.project, + // attributes: [], + // where: { creatorId: userId }, + // }, + // // order: [['title', 'ASC']], + // }); + + const task = await taskModel.findAll({ + where: { isDone: false }, + include: [ + 'labels', + 'priority', + 'alarm', + 'bookmarks', + { + model: taskModel, + include: ['labels', 'priority', 'alarm', 'bookmarks'], + }, + { + model: models.project, + attributes: [], + where: { creatorId: userId }, + }, + ], + order: [[taskModel, 'position', 'ASC']], + }); + return task; +}; + +const create = async ({ projectId, sectionId, ...taskData }) => { + const { labelIdList, dueDate, ...rest } = taskData; + const result = await sequelize.transaction(async t => { + const section = await models.section.findByPk(sectionId, { include: 'tasks' }); + + const maxPosition = section.toJSON().tasks.reduce((max, task) => { + return Math.max(max, task.position); + }, 0); + + const task = await models.task.create( + { projectId, sectionId, dueDate, position: maxPosition + 1, ...rest }, + { transaction: t }, + ); + if (labelIdList) { + await task.setLabels(JSON.parse(labelIdList), { transaction: t }); + } + + return task; + }); + + return !!result; +}; + +const update = async taskData => { + const { id, labelIdList, dueDate, ...rest } = taskData; + const result = await sequelize.transaction(async t => { + await taskModel.update( + { dueDate, ...rest }, + { + where: { id }, + }, + { transaction: t }, + ); + + const task = await taskModel.findByPk(id, { transaction: t }); + if (labelIdList) { + await task.setLabels(JSON.parse(labelIdList), { transaction: t }); + } + return true; + }); + + return result; +}; + +const remove = async id => { + const result = await taskModel.destroy({ + where: { + id, + }, + }); + + return result; +}; + +module.exports = { retrieveById, retrieveAll, create, update, remove }; diff --git a/server/src/services/user.js b/server/src/services/user.js new file mode 100644 index 00000000..a56b46d4 --- /dev/null +++ b/server/src/services/user.js @@ -0,0 +1,26 @@ +const userModel = require('@models').models.user; + +const retrieveById = async id => { + const foundUser = await userModel.findByPk(id, { + attributes: ['id', 'email', 'name', 'provider'], + }); + return foundUser; +}; + +const retrieveOrCreate = async userData => { + const { email, name, provider } = userData; + const user = await userModel.findOrCreate({ + where: { email, provider }, + attributes: ['id', 'email', 'name', 'provider'], + defaults: { email, name, provider }, + }); + + return user; +}; + +const register = async data => { + const result = await userModel.create(data); + return result; +}; + +module.exports = { retrieveById, retrieveOrCreate, register }; diff --git a/server/src/utils/date.js b/server/src/utils/date.js index 7d33da2c..609f1bf6 100644 --- a/server/src/utils/date.js +++ b/server/src/utils/date.js @@ -1,4 +1,7 @@ const isValidDueDate = inputDate => { + if (inputDate === undefined) { + return true; + } const dueDate = new Date(inputDate); const today = new Date(Date.now()); diff --git a/server/src/utils/error-messages.js b/server/src/utils/error-messages.js new file mode 100644 index 00000000..ab077889 --- /dev/null +++ b/server/src/utils/error-messages.js @@ -0,0 +1,7 @@ +module.exports = { + TYPE_ERROR: property => `${property} 타입이 올바르지 않습니다.`, + UNNECESSARY_INPUT_ERROR: property => `불필요한 값이 포함되어 있습니다. => ${property}`, + INVALID_INPUT_ERROR: property => `${property}값이 올바르지 않습니다.`, + NOT_FOUND_ERROR: property => `존재하지 않는 ${property}입니다.`, + DUEDATE_ERROR: 'dueDate는 현재시간보다 이전을 설정할 수 없습니다.', +}; diff --git a/server/src/utils/validator.js b/server/src/utils/validator.js new file mode 100644 index 00000000..1c1592d8 --- /dev/null +++ b/server/src/utils/validator.js @@ -0,0 +1,33 @@ +const { validateOrReject, registerDecorator } = require('class-validator'); +const { plainToClass } = require('class-transformer'); +const { isValidDueDate } = require('@utils/date'); + +const validator = async (Dto, object, options) => { + const classObject = plainToClass(Dto, object); + await validateOrReject(classObject, { ...options, stopAtFirstError: true }); +}; + +const getErrorMsg = errorArray => { + const [validationError] = errorArray; + const [message] = Object.values(validationError.constraints); + + return message; +}; +const isAfterToday = (property, validationOptions) => { + return (object, propertyName) => { + registerDecorator({ + name: 'isAfterToday', + target: object.constructor, + propertyName, + constraints: [property], + options: validationOptions, + validator: { + validate(value, args) { + return isValidDueDate(value); // you can return a Promise here as well, if you want to make async validation + }, + }, + }); + }; +}; + +module.exports = { validator, getErrorMsg, isAfterToday }; diff --git a/server/test/label.api.test.js b/server/test/label.api.test.js index 2d040509..e4db62c8 100644 --- a/server/test/label.api.test.js +++ b/server/test/label.api.test.js @@ -36,7 +36,7 @@ describe('label api', () => { try { request(app) .get('/api/label') // when - .set('Authorization', createJWT(seeder.users[0])) + .set('Authorization', `Bearer ${createJWT(seeder.users[0])}`) .end((err, res) => { if (err) { throw err; @@ -79,7 +79,7 @@ describe('label api', () => { try { request(app) .post('/api/label') // when - .set('Authorization', createJWT(seeder.users[0])) + .set('Authorization', `Bearer ${createJWT(seeder.users[0])}`) .send(expectedLabel) .end((err, res) => { if (err) { @@ -126,7 +126,7 @@ describe('label api', () => { try { request(app) .post('/api/label') // when - .set('Authorization', createJWT(seeder.users[0])) + .set('Authorization', `Bearer ${createJWT(seeder.users[0])}`) .send(expectedLabel) .end((err, res) => { if (err) { @@ -150,7 +150,7 @@ describe('label api', () => { try { request(app) .post('/api/label') // when - .set('Authorization', createJWT(seeder.users[0])) + .set('Authorization', `Bearer ${createJWT(seeder.users[0])}`) .send(expectedLabel) .end((err, res) => { if (err) { @@ -176,7 +176,7 @@ describe('label api', () => { try { request(app) .put(`/api/label/${seeder.labels[0].id}`) // when - .set('Authorization', createJWT(seeder.users[0])) + .set('Authorization', `Bearer ${createJWT(seeder.users[0])}`) .send(expectedLabel) .end((err, res) => { if (err) { @@ -225,7 +225,7 @@ describe('label api', () => { try { request(app) .put(`/api/label/${seeder.labels[0].id}`) // when - .set('Authorization', createJWT(seeder.users[0])) + .set('Authorization', `Bearer ${createJWT(seeder.users[0])}`) .send(expectedLabel) .end((err, res) => { if (err) { @@ -250,7 +250,7 @@ describe('label api', () => { try { request(app) .put(`/api/label/${seeder.labels[0].id}`) // when - .set('Authorization', createJWT(seeder.users[1])) + .set('Authorization', `Bearer ${createJWT(seeder.users[1])}`) .send(expectedLabel) .end((err, res) => { if (err) { @@ -275,7 +275,7 @@ describe('label api', () => { try { request(app) .put(`/api/label/nothing`) // when - .set('Authorization', createJWT(seeder.users[1])) + .set('Authorization', `Bearer ${createJWT(seeder.users[1])}`) .send(expectedLabel) .end((err, res) => { if (err) { @@ -301,7 +301,7 @@ describe('label api', () => { try { request(app) .delete(`/api/label/${seeder.labels[1].id}`) // when - .set('Authorization', createJWT(seeder.users[0])) + .set('Authorization', `Bearer ${createJWT(seeder.users[0])}`) .send(expectedLabel) .end((err, res) => { if (err) { @@ -326,7 +326,7 @@ describe('label api', () => { try { request(app) .delete(`/api/label/nothing`) // when - .set('Authorization', createJWT(seeder.users[1])) + .set('Authorization', `Bearer ${createJWT(seeder.users[1])}`) .send(expectedLabel) .end((err, res) => { if (err) { @@ -351,7 +351,7 @@ describe('label api', () => { try { request(app) .delete(`/api/label/${seeder.labels[1].id}`) // when - .set('Authorization', createJWT(seeder.users[1])) + .set('Authorization', `Bearer ${createJWT(seeder.users[1])}`) .send(expectedLabel) .end((err, res) => { if (err) { diff --git a/server/test/project.api.test.js b/server/test/project.api.test.js index e93e8631..b61aa48c 100644 --- a/server/test/project.api.test.js +++ b/server/test/project.api.test.js @@ -2,6 +2,8 @@ require('module-alias/register'); const request = require('supertest'); const app = require('@root/app'); const seeder = require('@test/test-seed'); +const status = require('@test/response-status'); +const { createJWT } = require('@utils/auth'); beforeAll(async done => { await seeder.up(); @@ -13,246 +15,226 @@ afterAll(async done => { done(); }); -const SUCCESS_CODE = 201; -const SUCCESS_MSG = 'ok'; - describe('get all projects', () => { - it('project get all 일반', done => { - const expectedProjects = [ - { id: 'b7f253e5-7b6b-4ee2-b94e-369ffcdffb5f', taskCount: 5, title: '프로젝트 1' }, - { id: 'f7605077-96ec-4365-88fc-a9c3af4a084e', taskCount: 0, title: '프로젝트 2' }, - { taskCount: 2, title: '오늘' }, - ]; - - try { - request(app) - .get('/api/project') - .end((err, res) => { - if (err) { - throw err; - } - - expect(res.body).toEqual(expectedProjects); - done(); - }); - } catch (err) { - done(err); - } + it('project get all 일반', async done => { + // given + const expectedUser = seeder.users[0]; + const expectedProjects = seeder.projects.map(project => { + const tasks = seeder.tasks.filter( + task => project.creatorId === expectedUser.id && task.projectId === project.id, + ); + const { id, title } = project; + return { id, title, taskCount: tasks.length }; + }); + + // when + const res = await request(app) + .get('/api/project') + .set('Authorization', `Bearer ${createJWT(expectedUser)}`); + const recievedProjects = res.body; + + // then + expect( + recievedProjects.every(project => + expectedProjects.some( + expectedProject => + Object.entries(project).toString === Object.entries(expectedProject).toString, + ), + ), + ).toBeTruthy(); + + done(); }); }); describe('get project by id', () => { - it('project get by id 일반', done => { - const expectedChildTaskId = '8d62f93c-9233-46a9-a5cf-ec18ad5a36f4'; - - try { - request(app) - .get('/api/project/b7f253e5-7b6b-4ee2-b94e-369ffcdffb5f') - .end((err, res) => { - if (err) { - throw err; - } - const childTask = res.body.sections[0].tasks[0].tasks[0]; - expect(childTask.id).toEqual(expectedChildTaskId); - done(); - }); - } catch (err) { - done(err); - } + it('project get by id 일반', async done => { + // given + const expectedChildTaskId = seeder.tasks[3].id; + const expectedProjectId = seeder.projects[0].id; + const expectedUser = seeder.users[0]; + + // when + const res = await request(app) + .get(`/api/project/${expectedProjectId}`) + .set('Authorization', `Bearer ${createJWT(expectedUser)}`); + + const childTask = res.body.sections[0].tasks[0].tasks[0]; + + // then + expect(childTask.id).toEqual(expectedChildTaskId); + done(); }); }); describe('create project', () => { - it('create project 일반', done => { + it('create project 일반', async done => { + // given const requestBody = { title: '새 프로젝트', + color: '#FFA7A7', isList: true, }; - - try { - request(app) - .post('/api/project/') - .send(requestBody) - .end((err, res) => { - if (err) { - throw err; - } - expect(res.status).toBe(SUCCESS_CODE); - expect(res.body.message).toBe(SUCCESS_MSG); - done(); - }); - } catch (err) { - done(err); - } + const expectedUser = seeder.users[0]; + + // when + const res = await request(app) + .post('/api/project/') + .set('Authorization', `Bearer ${createJWT(expectedUser)}`) + .send(requestBody); + + // then + expect(res.status).toBe(status.SUCCESS.POST.CODE); + expect(res.body.message).toBe(status.SUCCESS.MSG); + done(); }); }); describe('update project', () => { - it('update project PUT', done => { + it('update project PUT', async done => { + // given + const expectedProjectId = seeder.projects[0].id; const requestBody = { - title: '변경된 프로젝트', + title: 'PUT으로 변경된 프로젝트', + color: '#FFA7A7', isList: true, isFavorite: true, }; - - try { - request(app) - .put('/api/project/b7f253e5-7b6b-4ee2-b94e-369ffcdffb5f') - .send(requestBody) - .end((err, res) => { - if (err) { - throw err; - } - expect(res.status).toBe(SUCCESS_CODE); - expect(res.body.message).toBe(SUCCESS_MSG); - done(); - }); - } catch (err) { - done(err); - } + const expectedUser = seeder.users[0]; + + // when + const res = await request(app) + .put(`/api/project/${expectedProjectId}`) + .set('Authorization', `Bearer ${createJWT(expectedUser)}`) + .send(requestBody); + + // then + expect(res.status).toBe(status.SUCCESS.CODE); + expect(res.body.message).toBe(status.SUCCESS.MSG); + done(); }); - it('update project PATCH', done => { + it('update project PATCH', async done => { + // given + const expectedUser = seeder.users[0]; + const expectedProjectId = seeder.projects[0].id; const requestBody = { - title: '변경된 프로젝트', - isList: true, - isFavorite: true, + title: 'PATCH로 변경된 프로젝트!!', }; - try { - request(app) - .patch('/api/project/b7f253e5-7b6b-4ee2-b94e-369ffcdffb5f') - .send(requestBody) - .end((err, res) => { - if (err) { - throw err; - } - expect(res.status).toBe(SUCCESS_CODE); - expect(res.body.message).toBe(SUCCESS_MSG); - done(); - }); - } catch (err) { - done(err); - } + // when + const res = await request(app) + .patch(`/api/project/${expectedProjectId}`) + .set('Authorization', `Bearer ${createJWT(expectedUser)}`) + .send(requestBody); + + // then + expect(res.status).toBe(status.SUCCESS.CODE); + expect(res.body.message).toBe(status.SUCCESS.MSG); + done(); }); }); describe('delete project', () => { - it('delete project 일반', done => { - try { - request(app) - .delete('/api/project/f7605077-96ec-4365-88fc-a9c3af4a084e') - .end((err, res) => { - if (err) { - throw err; - } - expect(res.status).toBe(SUCCESS_CODE); - expect(res.body.message).toBe(SUCCESS_MSG); - done(); - }); - } catch (err) { - done(err); - } + it('delete project 일반', async done => { + // given + const expectedUser = seeder.users[0]; + const expectedProjectId = seeder.projects[1].id; + // when + const res = await request(app) + .delete(`/api/project/${expectedProjectId}`) + .set('Authorization', `Bearer ${createJWT(expectedUser)}`); + + // then + expect(res.status).toBe(status.SUCCESS.CODE); + expect(res.body.message).toBe(status.SUCCESS.MSG); + done(); }); }); describe('create section', () => { - it('create project 일반', done => { + it('create project 일반', async done => { + // given + const expectedUser = seeder.users[0]; + const expectedProjectId = seeder.projects[0].id; const requestBody = { title: '새로운 섹션', }; - try { - request(app) - .post('/api/project/b7f253e5-7b6b-4ee2-b94e-369ffcdffb5f/section') - .send(requestBody) - .end((err, res) => { - if (err) { - throw err; - } - expect(res.status).toBe(SUCCESS_CODE); - expect(res.body.message).toBe(SUCCESS_MSG); - done(); - }); - } catch (err) { - done(err); - } + // when + const res = await request(app) + .post(`/api/project/${expectedProjectId}/section`) + .set('Authorization', `Bearer ${createJWT(expectedUser)}`) + .send(requestBody); + + // then + expect(res.status).toBe(status.SUCCESS.POST.CODE); + expect(res.body.message).toBe(status.SUCCESS.MSG); + done(); }); }); describe('update section task positions', () => { - it('update section task positions 일반', done => { + it('update section task positions 일반', async done => { + // given + const expectedUser = seeder.users[0]; + const expectedProjectId = seeder.projects[0].id; + const expectedSectionId = seeder.sections[0].id; const requestBody = { - orderedTasks: [ - '7d62f93c-9233-46a9-a5cf-ec18ad5a36f4', - 'cd62f93c-9233-46a9-a5cf-ec18ad5a36f4', - '13502adf-83dd-4e8e-9acf-5c5a0abd5b1b', - ], + orderedTasks: [seeder.tasks[2].id, seeder.tasks[1].id, seeder.tasks[0].id], }; - try { - request(app) - .post( - '/api/project/b7f253e5-7b6b-4ee2-b94e-369ffcdffb5f/section/7abf0633-bce2-4972-9249-69f287db8a47/task', - ) - .send(requestBody) - .end((err, res) => { - if (err) { - throw err; - } - expect(res.status).toBe(SUCCESS_CODE); - expect(res.body.message).toBe(SUCCESS_MSG); - done(); - }); - } catch (err) { - done(err); - } + // when + const res = await request(app) + .post(`/api/project/${expectedProjectId}/section/${expectedSectionId}/task/position`) + .set('Authorization', `Bearer ${createJWT(expectedUser)}`) + .send(requestBody); + + // then + expect(res.status).toBe(status.SUCCESS.CODE); + expect(res.body.message).toBe(status.SUCCESS.MSG); + done(); }); }); describe('update section', () => { - it('update section 일반', done => { + it('update section 일반', async done => { + // given + const expectedUser = seeder.users[0]; + const expectedProjectId = seeder.projects[0].id; + const expectedSectionId = seeder.sections[0].id; const requestBody = { title: '바뀐 섹션', }; - try { - request(app) - .put( - '/api/project/b7f253e5-7b6b-4ee2-b94e-369ffcdffb5f/section/7abf0633-bce2-4972-9249-69f287db8a47', - ) - .send(requestBody) - .end((err, res) => { - if (err) { - throw err; - } - expect(res.status).toBe(SUCCESS_CODE); - expect(res.body.message).toBe(SUCCESS_MSG); - done(); - }); - } catch (err) { - done(err); - } + // when + const res = await request(app) + .put(`/api/project/${expectedProjectId}/section/${expectedSectionId}`) + .set('Authorization', `Bearer ${createJWT(expectedUser)}`) + .send(requestBody); + + // then + expect(res.status).toBe(status.SUCCESS.CODE); + expect(res.body.message).toBe(status.SUCCESS.MSG); + done(); }); }); describe('delete section', () => { - it('update section 일반', done => { - try { - request(app) - .delete( - '/api/project/b7f253e5-7b6b-4ee2-b94e-369ffcdffb5f/section/7abf0633-bce2-4972-9249-69f287db8a47', - ) - .end((err, res) => { - if (err) { - throw err; - } - expect(res.status).toBe(SUCCESS_CODE); - expect(res.body.message).toBe(SUCCESS_MSG); - done(); - }); - } catch (err) { - done(err); - } + it('update section 일반', async done => { + // given + const expectedUser = seeder.users[0]; + const expectedProjectId = seeder.projects[1].id; + const expectedSectionId = seeder.sections[1].id; + + // when + const res = await request(app) + .delete(`/api/project/${expectedProjectId}/section/${expectedSectionId}`) + .set('Authorization', `Bearer ${createJWT(expectedUser)}`); + + // then + expect(res.status).toBe(status.SUCCESS.CODE); + expect(res.body.message).toBe(status.SUCCESS.MSG); + done(); }); }); diff --git a/server/test/project.task.api.test.js b/server/test/project.task.api.test.js new file mode 100644 index 00000000..a1c46236 --- /dev/null +++ b/server/test/project.task.api.test.js @@ -0,0 +1,349 @@ +require('module-alias/register'); +const request = require('supertest'); +const app = require('@root/app'); +const seeder = require('@test/test-seed'); +const status = require('@test/response-status'); +const { createJWT } = require('@utils/auth'); +const errorMessage = require('@utils/error-messages'); + +beforeAll(async done => { + await seeder.up(); + done(); +}); + +afterAll(async done => { + await seeder.down(); + done(); +}); + +describe('post task', () => { + it('성공하는 task 생성', async done => { + // given + const expectedProjectId = seeder.projects[0].id; + const expectedSectionId = seeder.sections[0].id; + const newTask = { + title: '할일', + labelIdList: JSON.stringify(seeder.labels.map(label => label.id)), + priorityId: seeder.priorities[0].id, + dueDate: new Date(), + parentId: null, + alarmId: seeder.alarms[0].id, + position: 1, + }; + + // when + const res = await request(app) + .post(`/api/project/${expectedProjectId}/section/${expectedSectionId}/task`) + .set('Authorization', `Bearer ${createJWT(seeder.users[0])}`) + .send(newTask); + + // then + expect(res.status).toBe(status.SUCCESS.POST.CODE); + expect(res.body.message).toBe(status.SUCCESS.MSG); + done(); + }); + + it('label 없이 생성', async done => { + // given + const expectedProjectId = seeder.projects[0].id; + const expectedSectionId = seeder.sections[0].id; + const newTask = { + title: '할일', + labelIdList: JSON.stringify([]), + priorityId: seeder.priorities[1].id, + dueDate: new Date(), + parentId: null, + alarmId: seeder.alarms[0].id, + position: 1, + }; + + // when + const res = await request(app) + .post(`/api/project/${expectedProjectId}/section/${expectedSectionId}/task`) + .set('Authorization', `Bearer ${createJWT(seeder.users[0])}`) + .send(newTask); + + // then + expect(res.status).toBe(status.SUCCESS.POST.CODE); + expect(res.body.message).toBe(status.SUCCESS.MSG); + done(); + }); + + it('priority 없이 생성', async done => { + // given + const expectedProjectId = seeder.projects[0].id; + const expectedSectionId = seeder.sections[0].id; + const newTask = { + title: '할일', + labelIdList: JSON.stringify([]), + priorityId: null, + dueDate: new Date(), + parentId: null, + alarmId: seeder.alarms[0].id, + position: 1, + }; + + // when + const res = await request(app) + .post(`/api/project/${expectedProjectId}/section/${expectedSectionId}/task`) + .set('Authorization', `Bearer ${createJWT(seeder.users[0])}`) + .send(newTask); + + // then + expect(res.status).toBe(status.SUCCESS.POST.CODE); + expect(res.body.message).toBe(status.SUCCESS.MSG); + done(); + }); + + it('하위 할일 생성', async done => { + // given + const expectedProjectId = seeder.projects[0].id; + const expectedSectionId = seeder.sections[0].id; + const newTask = { + title: '할일', + labelIdList: JSON.stringify([]), + priorityId: seeder.priorities[1].id, + dueDate: new Date(), + parentId: seeder.tasks[0].id, + alarmId: seeder.alarms[0].id, + position: 1, + }; + + // when + const res = await request(app) + .post(`/api/project/${expectedProjectId}/section/${expectedSectionId}/task`) + .set('Authorization', `Bearer ${createJWT(seeder.users[0])}`) + .send(newTask); + + // then + expect(res.status).toBe(status.SUCCESS.POST.CODE); + expect(res.body.message).toBe(status.SUCCESS.MSG); + done(); + }); + + it('alarm 없이 생성', async done => { + // given + const expectedProjectId = seeder.projects[0].id; + const expectedSectionId = seeder.sections[0].id; + const newTask = { + title: '할일', + labelIdList: JSON.stringify([]), + priorityId: seeder.priorities[1].id, + dueDate: new Date(), + parentId: seeder.tasks[0].id, + alarmId: null, + position: 1, + }; + + // when + const res = await request(app) + .post(`/api/project/${expectedProjectId}/section/${expectedSectionId}/task`) + .set('Authorization', `Bearer ${createJWT(seeder.users[0])}`) + .send(newTask); + + // then + expect(res.status).toBe(status.SUCCESS.POST.CODE); + expect(res.body.message).toBe(status.SUCCESS.MSG); + done(); + }); + + it('유효하지 않은 duedate 생성', async done => { + // given + const expectedProjectId = seeder.projects[0].id; + const expectedSectionId = seeder.sections[0].id; + const newTask = { + title: '할일', + projectId: seeder.projects[1].id, + labelIdList: JSON.stringify([]), + priorityId: seeder.priorities[1].id, + dueDate: '2020-10-28', + parentId: seeder.tasks[0].id, + alarmId: seeder.alarms[0].id, + position: 1, + }; + + // when + const res = await request(app) + .post(`/api/project/${expectedProjectId}/section/${expectedSectionId}/task`) + .set('Authorization', `Bearer ${createJWT(seeder.users[0])}`) + .send(newTask); + + // then + expect(res.status).toBe(status.BAD_REQUEST.CODE); + expect(res.body.message).toBe(errorMessage.DUEDATE_ERROR); + done(); + }); + it('잘못된 parentId 생성', async done => { + // given + const expectedProjectId = seeder.projects[0].id; + const expectedSectionId = seeder.sections[0].id; + const expectedParentId = 'wrongId'; + const newTask = { + title: '할일', + labelIdList: JSON.stringify(seeder.labels.map(label => label.id)), + priorityId: seeder.priorities[0].id, + dueDate: new Date(), + parentId: expectedParentId, + alarmId: seeder.alarms[0].id, + position: 1, + }; + // when + const res = await request(app) + .post(`/api/project/${expectedProjectId}/section/${expectedSectionId}/task`) + .set('Authorization', `Bearer ${createJWT(seeder.users[0])}`) + .send(newTask); + + // then + expect(res.status).toBe(status.BAD_REQUEST.CODE); + expect(res.body.message).toBe(errorMessage.INVALID_INPUT_ERROR('parentId')); + done(); + }); + it('작업 id가 포함된 생성', async done => { + // given + const expectedId = seeder.projects[1].id; + const expectedProjectId = seeder.projects[0].id; + const expectedSectionId = seeder.sections[0].id; + const newTask = { + id: expectedId, + title: '할일', + labelIdList: JSON.stringify(seeder.labels.map(label => label.id)), + priorityId: seeder.priorities[0].id, + dueDate: new Date(), + parentId: null, + alarmId: seeder.alarms[0].id, + position: 1, + }; + // when + const res = await request(app) + .post(`/api/project/${expectedProjectId}/section/${expectedSectionId}/task`) + .set('Authorization', `Bearer ${createJWT(seeder.users[0])}`) + .send(newTask); + + // then + expect(res.status).toBe(status.BAD_REQUEST.CODE); + expect(res.body.message).toBe(errorMessage.UNNECESSARY_INPUT_ERROR('id')); + done(); + }); + it('프로젝트 id가 포함된 생성', async done => { + // given + const expectedProjectId = seeder.projects[0].id; + const expectedSectionId = seeder.sections[0].id; + const newTask = { + title: '할일', + projectId: expectedProjectId, + labelIdList: JSON.stringify(seeder.labels.map(label => label.id)), + priorityId: seeder.priorities[0].id, + dueDate: new Date(), + parentId: null, + alarmId: seeder.alarms[0].id, + position: 1, + }; + // when + const res = await request(app) + .post(`/api/project/${expectedProjectId}/section/${expectedSectionId}/task`) + .set('Authorization', `Bearer ${createJWT(seeder.users[0])}`) + .send(newTask); + + // then + expect(res.status).toBe(status.BAD_REQUEST.CODE); + expect(res.body.message).toBe(errorMessage.UNNECESSARY_INPUT_ERROR('projectId')); + done(); + }); + it('섹션 id가 포함된 생성', async done => { + // given + const expectedProjectId = seeder.projects[0].id; + const expectedSectionId = seeder.sections[0].id; + const newTask = { + title: '할일', + sectionId: expectedSectionId, + labelIdList: JSON.stringify(seeder.labels.map(label => label.id)), + priorityId: seeder.priorities[0].id, + dueDate: new Date(), + parentId: null, + alarmId: seeder.alarms[0].id, + position: 1, + }; + // when + const res = await request(app) + .post(`/api/project/${expectedProjectId}/section/${expectedSectionId}/task`) + .set('Authorization', `Bearer ${createJWT(seeder.users[0])}`) + .send(newTask); + + // then + expect(res.status).toBe(status.BAD_REQUEST.CODE); + expect(res.body.message).toBe(errorMessage.UNNECESSARY_INPUT_ERROR('sectionId')); + done(); + }); + it('빈 문자열 title 생성', async done => { + // given + const expectedProjectId = seeder.projects[0].id; + const expectedSectionId = seeder.sections[0].id; + const newTask = { + title: '', + labelIdList: JSON.stringify(seeder.labels.map(label => label.id)), + priorityId: seeder.priorities[0].id, + dueDate: new Date(), + parentId: null, + alarmId: seeder.alarms[0].id, + position: 1, + }; + // when + const res = await request(app) + .post(`/api/project/${expectedProjectId}/section/${expectedSectionId}/task`) + .set('Authorization', `Bearer ${createJWT(seeder.users[0])}`) + .send(newTask); + + // then + expect(res.status).toBe(status.BAD_REQUEST.CODE); + expect(res.body.message).toBe(errorMessage.INVALID_INPUT_ERROR('title')); + done(); + }); + it('존재하지 않는 projectId 생성', async done => { + // given + const expectedProjectId = 'wrongId'; + const expectedSectionId = seeder.sections[0].id; + const newTask = { + title: '할일', + labelIdList: JSON.stringify(seeder.labels.map(label => label.id)), + priorityId: seeder.priorities[0].id, + dueDate: new Date(), + parentId: null, + alarmId: seeder.alarms[0].id, + position: 1, + }; + // when + const res = await request(app) + .post(`/api/project/${expectedProjectId}/section/${expectedSectionId}/task`) + .set('Authorization', `Bearer ${createJWT(seeder.users[0])}`) + .send(newTask); + + // then + expect(res.status).toBe(status.NOT_FOUND.CODE); + expect(res.body.message).toBe(errorMessage.NOT_FOUND_ERROR('projectId')); + done(); + }); + it('존재하지 않는 sectionId 생성', async done => { + // given + const expectedProjectId = seeder.projects[0].id; + const expectedSectionId = 'wrongId'; + const newTask = { + title: '할일', + labelIdList: JSON.stringify(seeder.labels.map(label => label.id)), + priorityId: seeder.priorities[0].id, + dueDate: new Date(), + parentId: null, + alarmId: seeder.alarms[0].id, + position: 1, + }; + // when + const res = await request(app) + .post(`/api/project/${expectedProjectId}/section/${expectedSectionId}/task`) + .set('Authorization', `Bearer ${createJWT(seeder.users[0])}`) + .send(newTask); + + // then + expect(res.status).toBe(status.NOT_FOUND.CODE); + expect(res.body.message).toBe(errorMessage.NOT_FOUND_ERROR('sectionId')); + done(); + }); +}); diff --git a/server/test/task.api.test.js b/server/test/task.api.test.js index d3bf3ecb..651477b7 100644 --- a/server/test/task.api.test.js +++ b/server/test/task.api.test.js @@ -2,7 +2,9 @@ require('module-alias/register'); const request = require('supertest'); const app = require('@root/app'); const seeder = require('@test/test-seed'); -const { projects, tasks, labels, priorities, alarms } = require('@test/test-seed'); +const status = require('@test/response-status'); +const { createJWT } = require('@utils/auth'); +const errorMessage = require('@utils/error-messages'); beforeAll(async done => { await seeder.up(); @@ -14,216 +16,352 @@ afterAll(async done => { done(); }); -const SUCCESS_CODE = 201; -const SUCCESS_MSG = 'ok'; - -describe('get task by id', () => { - it('get task by id 일반', done => { - const expectedChildTaskId = '8d62f93c-9233-46a9-a5cf-ec18ad5a36f4'; +describe('get All task', () => { + it('성공 조건', async done => { + // given + const expectedUser = seeder.users[0]; + const expectedTasks = seeder.tasks + .filter(task => { + const projects = seeder.projects.filter(project => project.creatorId === expectedUser.id); + return projects.some(project => project.id === task.projectId); + }) + .map(task => { + const { id, title } = task; + return { id, title }; + }); + try { + // when + const res = await request(app) + .get('/api/task') + .set('Authorization', `Bearer ${createJWT(expectedUser)}`); + + const { tasks } = res.body; + + // then + expect( + tasks.every(task => expectedTasks.some(expectedTask => expectedTask.id === task.id)), + ).toBeTruthy(); + done(); + } catch (err) { + done(err); + } + }); + it('task가 없는 유저', async done => { + // given + const expectedTasks = []; + try { + // when + const res = await request(app) + .get('/api/task') + .set('Authorization', `Bearer ${createJWT(seeder.users[2])}`); + + const { tasks } = res.body; + // then + expect(tasks).toStrictEqual(expectedTasks); + done(); + } catch (err) { + done(err); + } + }); + it('토큰 값이 없는 경우 ', async done => { + // given try { - request(app) - .get('/api/task/13502adf-83dd-4e8e-9acf-5c5a0abd5b1b') - .end((err, res) => { - if (err) { - throw err; - } - - const firstChildTaskId = res.body.tasks[0].id; - expect(firstChildTaskId).toEqual(expectedChildTaskId); - done(); - }); + // when + const res = await request(app).get('/api/task'); + + // then + expect(res.status).toBe(status.UNAUTHORIZED.CODE); + expect(res.body.message).toBe(status.UNAUTHORIZED.MSG); + done(); } catch (err) { done(err); } }); }); -describe('post task', () => { - it('일반 task 생성', async done => { +describe('get task by id', () => { + it('get task by id 성공', async done => { // given - const newTask = { - title: '할일', - projectId: projects[0].id, - labelIdList: JSON.stringify(labels.map(label => label.id)), - priorityId: priorities[0].id, - dueDate: '2021-11-28', - parentId: null, - alarmId: alarms[0].id, - position: 1, - }; + const taskId = seeder.tasks[0].id; + const expectedChildren = seeder.tasks.filter(task => task.parentId === taskId); - const res = await request(app).post('/api/task').send(newTask); - expect(res.status).toBe(201); - expect(res.body.message).toBe('ok'); - done(); + try { + // when + const res = await request(app) + .get(`/api/task/${taskId}`) + .set('Authorization', `Bearer ${createJWT(seeder.users[0])}`); + + const recievedChildren = res.body.task.tasks.filter( + childTask => childTask.parentId === taskId, + ); + + // then + recievedChildren.forEach(recievedChild => { + expect( + expectedChildren.some(expectedChild => recievedChild.id === expectedChild.id), + ).toBeTruthy(); + }); + + done(); + } catch (err) { + done(err); + } }); + it('잘못된 id 값 요청', async done => { + // given + const taskId = 'invalidId'; - it('project 없이 생성', async done => { + try { + // when + const res = await request(app) + .get(`/api/task/${taskId}`) + .set('Authorization', `Bearer ${createJWT(seeder.users[0])}`); + + // then + expect(res.status).toBe(status.BAD_REQUEST.CODE); + expect(res.body.message).toBe(errorMessage.INVALID_INPUT_ERROR('id')); + done(); + } catch (err) { + done(err); + } + }); + it('자신의 task id가 아닌 경우', async done => { // given - const newTask = { - title: '할일', - projectId: null, - labelIdList: JSON.stringify(labels.map(label => label.id)), - priorityId: priorities[0].id, - dueDate: '2020-11-28', - parentId: null, - alarmId: alarms[0].id, - position: 1, - }; + const taskId = seeder.tasks[0].id; - const res = await request(app).post('/api/task').send(newTask); + try { + // when + const res = await request(app) + .get(`/api/task/${taskId}`) + .set('Authorization', `Bearer ${createJWT(seeder.users[1])}`); - expect(res.status).toBe(201); - expect(res.body.message).toBe('ok'); - done(); - }); + // then + expect(res.status).toBe(status.FORBIDDEN.CODE); + expect(res.body.message).toBe(status.FORBIDDEN.MSG); - it('label 없이 생성', async done => { + done(); + } catch (err) { + done(err); + } + }); + it('존재하지 않는 task id인 경우', async done => { // given - const newTask = { - title: '할일', - projectId: projects[0].id, - labelIdList: JSON.stringify([]), - priorityId: priorities[1].id, - dueDate: '2020-11-28', - parentId: null, - alarmId: alarms[0].id, - position: 1, - }; + const taskId = 'c213d58a-661a-4395-b0da-fb48dc11fa2e'; - const res = await request(app).post('/api/task').send(newTask); + try { + // when + const res = await request(app) + .get(`/api/task/${taskId}`) + .set('Authorization', `Bearer ${createJWT(seeder.users[0])}`); - expect(res.status).toBe(201); - expect(res.body.message).toBe('ok'); - done(); + // then + expect(res.status).toBe(status.NOT_FOUND.CODE); + expect(res.body.message).toBe(status.NOT_FOUND.MSG); + + done(); + } catch (err) { + done(err); + } }); +}); - it('priority 없이 생성', async done => { +describe('patch task with id', () => { + it('patch task with id 성공', async done => { // given - const newTask = { + const taskId = seeder.tasks[0].id; + const patchTask = { title: '할일', - projectId: projects[0].id, - labelIdList: JSON.stringify([]), - priorityId: null, - dueDate: '2020-11-28', + projectId: seeder.projects[0].id, + sectionId: seeder.sections[0].id, + priorityId: seeder.priorities[0].id, + dueDate: new Date(), parentId: null, - alarmId: alarms[0].id, + alarmId: seeder.alarms[0].id, position: 1, }; - const res = await request(app).post('/api/task').send(newTask); + try { + // when + const res = await request(app) + .patch(`/api/task/${taskId}`) + .set('Authorization', `Bearer ${createJWT(seeder.users[0])}`) + .send(patchTask); + + // then + expect(res.status).toBe(status.SUCCESS.CODE); + expect(res.body.message).toBe(status.SUCCESS.MSG); + done(); + } catch (err) { + done(err); + } + }); + it('isDone 성공', async done => { + // given + const taskId = seeder.tasks[0].id; + const patchTask = { isDone: true }; - expect(res.status).toBe(201); - expect(res.body.message).toBe('ok'); - done(); + try { + // when + const res = await request(app) + .patch(`/api/task/${taskId}`) + .set('Authorization', `Bearer ${createJWT(seeder.users[0])}`) + .send(patchTask); + + // then + expect(res.status).toBe(status.SUCCESS.CODE); + expect(res.body.message).toBe(status.SUCCESS.MSG); + done(); + } catch (err) { + done(err); + } }); - it('하위 할일 생성', async done => { + it('id값이 포함된 수정', async done => { // given - const newTask = { - title: '할일', - projectId: projects[1].id, - labelIdList: JSON.stringify([]), - priorityId: priorities[1].id, - dueDate: '2020-11-28', - parentId: tasks[0].id, - alarmId: alarms[0].id, - position: 1, - }; + const patchTask = { id: seeder.tasks[0].id, title: '졸리다' }; - const res = await request(app).post('/api/task').send(newTask); + // when + const res = await request(app) + .patch(`/api/task/${patchTask.id}`) + .set('Authorization', `Bearer ${createJWT(seeder.users[0])}`) + .send(patchTask); - expect(res.status).toBe(201); - expect(res.body.message).toBe('ok'); + // then + expect(res.status).toBe(status.BAD_REQUEST.CODE); + expect(res.body.message).toBe(errorMessage.UNNECESSARY_INPUT_ERROR('id')); done(); }); - - it('alarm 없이 생성', async done => { + it('잘못된 title 수정', async done => { // given - const newTask = { - title: '할일', - projectId: projects[1].id, - labelIdList: JSON.stringify([]), - priorityId: priorities[1].id, - dueDate: '2020-11-28', - parentId: tasks[0].id, - alarmId: null, - position: 1, - }; - - const res = await request(app).post('/api/task').send(newTask); - - expect(res.status).toBe(201); - expect(res.body.message).toBe('ok'); + const taskId = seeder.tasks[0].id; + const patchTask = { title: '' }; + + // when + const res = await request(app) + .patch(`/api/task/${taskId}`) + .set('Authorization', `Bearer ${createJWT(seeder.users[0])}`) + .send(patchTask); + + // then + expect(res.status).toBe(status.BAD_REQUEST.CODE); + expect(res.body.message).toBe(errorMessage.INVALID_INPUT_ERROR('title')); done(); }); - it('alarm 없이 생성', async done => { + it('잘못된 parentId 수정', async done => { // given - const newTask = { - title: '할일', - projectId: projects[1].id, - labelIdList: JSON.stringify([]), - priorityId: priorities[1].id, - dueDate: '2020-10-28', - parentId: tasks[0].id, - alarmId: null, - position: 1, - }; - - const res = await request(app).post('/api/task').send(newTask); - - expect(res.status).toBe(400); - expect(res.body.message).toBe('유효하지 않은 dueDate'); + const taskId = seeder.tasks[0].id; + const patchTask = { parentId: 'invalidId' }; + + // when + const res = await request(app) + .patch(`/api/task/${taskId}`) + .set('Authorization', `Bearer ${createJWT(seeder.users[0])}`) + .send(patchTask); + + // then + expect(res.status).toBe(status.BAD_REQUEST.CODE); + expect(res.body.message).toBe(errorMessage.INVALID_INPUT_ERROR('parentId')); done(); }); -}); - -describe('post task with id (업데이트)', () => { - it('post task with id 일반', done => { - const newTask = { - title: '할일', - projectId: projects[0].id, - labelIdList: JSON.stringify(labels.map(label => label.id)), - priorityId: priorities[0].id, - dueDate: '2021-11-28', - parentId: null, - alarmId: alarms[0].id, - position: 1, - }; - try { - request(app) - .post('/api/task/13502adf-83dd-4e8e-9acf-5c5a0abd5b1b') - .send(newTask) - .end((err, res) => { - if (err) { - throw err; - } - expect(res.status).toBe(201); - expect(res.body.message).toBe('ok'); - done(); - }); - } catch (err) { - done(err); - } + it('잘못된 projectId 수정', async done => { + // given + const taskId = seeder.tasks[0].id; + const patchTask = { projectId: 'invalidId' }; + + // when + const res = await request(app) + .patch(`/api/task/${taskId}`) + .set('Authorization', `Bearer ${createJWT(seeder.users[0])}`) + .send(patchTask); + + // then + expect(res.status).toBe(status.BAD_REQUEST.CODE); + expect(res.body.message).toBe(errorMessage.INVALID_INPUT_ERROR('projectId')); + done(); + }); + it('잘못된 priorityId 수정', async done => { + // given + const taskId = seeder.tasks[0].id; + const patchTask = { priorityId: 'invalidId' }; + + // when + const res = await request(app) + .patch(`/api/task/${taskId}`) + .set('Authorization', `Bearer ${createJWT(seeder.users[0])}`) + .send(patchTask); + + // then + expect(res.status).toBe(status.BAD_REQUEST.CODE); + expect(res.body.message).toBe(errorMessage.INVALID_INPUT_ERROR('priorityId')); + done(); + }); + it('잘못된 alarmId 수정', async done => { + // given + const taskId = seeder.tasks[0].id; + const patchTask = { alarmId: 'invalidId' }; + + // when + const res = await request(app) + .patch(`/api/task/${taskId}`) + .set('Authorization', `Bearer ${createJWT(seeder.users[0])}`) + .send(patchTask); + + // then + expect(res.status).toBe(status.BAD_REQUEST.CODE); + expect(res.body.message).toBe(errorMessage.INVALID_INPUT_ERROR('alarmId')); + done(); + }); + it('잘못된 isDone 수정', async done => { + // given + const taskId = seeder.tasks[0].id; + const patchTask = { isDone: 'hi' }; + + // when + const res = await request(app) + .patch(`/api/task/${taskId}`) + .set('Authorization', `Bearer ${createJWT(seeder.users[0])}`) + .send(patchTask); + + // then + expect(res.status).toBe(status.BAD_REQUEST.CODE); + expect(res.body.message).toBe(errorMessage.TYPE_ERROR('isDone')); + done(); + }); + it('잘못된 duedate 수정', async done => { + // given + const taskId = seeder.tasks[0].id; + const patchTask = { duedate: new Date('2020-11-11') }; + + // when + const res = await request(app) + .patch(`/api/task/${taskId}`) + .set('Authorization', `Bearer ${createJWT(seeder.users[0])}`) + .send(patchTask); + + // then + expect(res.status).toBe(status.BAD_REQUEST.CODE); + expect(res.body.message).toBe(errorMessage.DUEDATE_ERROR); + done(); }); }); describe('delete task', () => { - it('delete task 일반', done => { + it('delete task 일반', async done => { + // given + const taskId = seeder.tasks[0].id; try { - request(app) - .delete('/api/task/13502adf-83dd-4e8e-9acf-5c5a0abd5b1b') - .end((err, res) => { - if (err) { - throw err; - } - expect(res.status).toBe(201); - expect(res.body.message).toBe('ok'); - done(); - }); + // when + const res = await request(app) + .delete(`/api/task/${taskId}`) + .set('Authorization', `Bearer ${createJWT(seeder.users[0])}`); + + // then + expect(res.status).toBe(status.SUCCESS.CODE); + expect(res.body.message).toBe(status.SUCCESS.MSG); + done(); } catch (err) { done(err); } @@ -231,21 +369,21 @@ describe('delete task', () => { }); describe('get comments', () => { - it('get comments 일반', done => { - const expectedCommentId = '6200bcb9-f871-439b-9507-57abbde3d468'; + it('get comments 일반', async done => { + // given + const expectedCommentId = seeder.comments[0].id; + const taskId = seeder.tasks[1].id; try { - request(app) - .get('/api/task/cd62f93c-9233-46a9-a5cf-ec18ad5a36f4/comment') - .end((err, res) => { - if (err) { - throw err; - } - - const firstCommentId = res.body[0].id; - expect(firstCommentId).toEqual(expectedCommentId); - done(); - }); + // when + const res = await request(app) + .get(`/api/task/${taskId}/comment`) + .set('Authorization', `Bearer ${createJWT(seeder.users[0])}`); + const firstCommentId = res.body[0].id; + + // then + expect(firstCommentId).toEqual(expectedCommentId); + done(); } catch (err) { done(err); } @@ -253,23 +391,24 @@ describe('get comments', () => { }); describe('create comment', () => { - it('create comment 일반', done => { + it('create comment 일반', async done => { + // given const requestBody = { content: '새로운 댓글', }; + const taskId = seeder.tasks[1].id; try { - request(app) - .post('/api/task/cd62f93c-9233-46a9-a5cf-ec18ad5a36f4/comment') - .send(requestBody) - .end((err, res) => { - if (err) { - throw err; - } - expect(res.status).toBe(SUCCESS_CODE); - expect(res.body.message).toBe(SUCCESS_MSG); - done(); - }); + // when + const res = await request(app) + .post(`/api/task/${taskId}/comment`) + .set('Authorization', `Bearer ${createJWT(seeder.users[0])}`) + .send(requestBody); + + // then + expect(res.status).toBe(status.SUCCESS.POST.CODE); + expect(res.body.message).toBe(status.SUCCESS.MSG); + done(); } catch (err) { done(err); } @@ -277,25 +416,24 @@ describe('create comment', () => { }); describe('update comment', () => { - it('update comment 일반', done => { + it('update comment 일반', async done => { + // given const requestBody = { content: '바뀐 댓글', }; - + const taskId = seeder.tasks[1].id; + const commentId = seeder.comments[0].id; try { - request(app) - .put( - '/api/task/cd62f93c-9233-46a9-a5cf-ec18ad5a36f4/comment/6200bcb9-f871-439b-9507-57abbde3d468', - ) - .send(requestBody) - .end((err, res) => { - if (err) { - throw err; - } - expect(res.status).toBe(SUCCESS_CODE); - expect(res.body.message).toBe(SUCCESS_MSG); - done(); - }); + // when + const res = await request(app) + .put(`/api/task/${taskId}/comment/${commentId}`) + .set('Authorization', `Bearer ${createJWT(seeder.users[0])}`) + .send(requestBody); + + // then + expect(res.status).toBe(status.SUCCESS.CODE); + expect(res.body.message).toBe(status.SUCCESS.MSG); + done(); } catch (err) { done(err); } @@ -303,25 +441,25 @@ describe('update comment', () => { }); describe('delete comment', () => { - it('delete comment 일반', done => { + it('delete comment 일반', async done => { + // given const requestBody = { content: '바뀐 댓글', }; + const taskId = seeder.tasks[1].id; + const commentId = seeder.comments[0].id; try { - request(app) - .delete( - '/api/task/cd62f93c-9233-46a9-a5cf-ec18ad5a36f4/comment/6200bcb9-f871-439b-9507-57abbde3d468', - ) - .send(requestBody) - .end((err, res) => { - if (err) { - throw err; - } - expect(res.status).toBe(SUCCESS_CODE); - expect(res.body.message).toBe(SUCCESS_MSG); - done(); - }); + // when + const res = await request(app) + .delete(`/api/task/${taskId}/comment/${commentId}`) + .set('Authorization', `Bearer ${createJWT(seeder.users[0])}`) + .send(requestBody); + + // then + expect(res.status).toBe(status.SUCCESS.CODE); + expect(res.body.message).toBe(status.SUCCESS.MSG); + done(); } catch (err) { done(err); } diff --git a/server/test/test-seed.js b/server/test/test-seed.js index 1f545bd5..99c83b35 100644 --- a/server/test/test-seed.js +++ b/server/test/test-seed.js @@ -17,6 +17,14 @@ const users = [ createdAt: new Date(), updatedAt: new Date(), }, + { + id: 'c83abea3-bc93-44a4-b56c-dda6c742a94d', + email: 'kyle@naver.com', + name: 'kyle', + provider: 'naver', + createdAt: new Date(), + updatedAt: new Date(), + }, ]; const priorities = [ @@ -49,8 +57,9 @@ const priorities = [ const projects = [ { id: 'b7f253e5-7b6b-4ee2-b94e-369ffcdffb5f', - creatorId: 'ff4dd832-1567-4d74-b41d-bd85e96ce329', + creatorId: users[0].id, title: '프로젝트 1', + color: '#FFA7A7', isList: true, isFavorite: false, createdAt: new Date(), @@ -58,8 +67,19 @@ const projects = [ }, { id: 'f7605077-96ec-4365-88fc-a9c3af4a084e', - creatorId: 'ff4dd832-1567-4d74-b41d-bd85e96ce329', + creatorId: users[0].id, title: '프로젝트 2', + color: '#FFE08C', + isList: false, + isFavorite: true, + createdAt: new Date(), + updatedAt: new Date(), + }, + { + id: 'ed298270-fc55-45d7-bc41-bb63ffd09b4a', + creatorId: users[1].id, + title: '프로젝트 3', + color: '#B7F0B1', isList: false, isFavorite: true, createdAt: new Date(), @@ -74,6 +94,18 @@ const sections = [ createdAt: new Date(), updatedAt: new Date(), }, + { + id: 'efd8be46-ebc7-438d-931a-036a4b28789f', + projectId: projects[1].id, + createdAt: new Date(), + updatedAt: new Date(), + }, + { + id: 'cd57769e-7227-44da-bf6c-60f1a49ff6e3', + projectId: projects[2].id, + createdAt: new Date(), + updatedAt: new Date(), + }, ]; const tasks = [ @@ -136,6 +168,18 @@ const tasks = [ createdAt: new Date(), updatedAt: new Date(), }, + { + id: '568d0233-0e6e-4ff2-bdae-4c492e55f111', + projectId: projects[2].id, + sectionId: sections[2].id, + parentId: null, + title: '작업 6', + dueDate: new Date(), + position: 1, + isDone: false, + createdAt: new Date(), + updatedAt: new Date(), + }, ]; const comments = [ diff --git a/server/test/user.api.test.js b/server/test/user.api.test.js index 9fcfcf1c..26a6611c 100644 --- a/server/test/user.api.test.js +++ b/server/test/user.api.test.js @@ -25,7 +25,7 @@ describe('user api', () => { try { request(app) .get('/api/user/me') // when - .set('Authorization', createJWT(seeder.users[0])) + .set('Authorization', `Bearer ${createJWT(seeder.users[0])}`) .end((err, res) => { if (err) { throw err; diff --git a/whale/background.js b/whale/background.js new file mode 100644 index 00000000..6e6d9ebc --- /dev/null +++ b/whale/background.js @@ -0,0 +1,28 @@ +whale.runtime.onConnectExternal.addListener((port) => { + const contextAddTask = (info) => { + port.postMessage(info); + }; + + whale.contextMenus.create({ + id: '123', + title: `할고래DO 할일로 추가`, + contexts: ['selection'], + }); + + whale.contextMenus.onClicked.addListener((info) => { + if (info.menuItemId == '123') { + port.postMessage(info); + } + }); +}); + +whale.runtime.onMessageExternal.addListener((message, sender, sendResponse) => { + switch (message) { + case 'getCurrentTabUrl': + whale.tabs.query({ active: true, lastFocusedWindow: true }, (tabs) => { + const currentTab = tabs[0]; + sendResponse({ url: currentTab.url, title: currentTab.title }); + }); + break; + } +}); diff --git a/whale/images/icon.png b/whale/images/icon.png new file mode 100644 index 00000000..3b31eb8d Binary files /dev/null and b/whale/images/icon.png differ diff --git a/whale/index.html b/whale/index.html new file mode 100644 index 00000000..6fd8e403 --- /dev/null +++ b/whale/index.html @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/whale/manifest.json b/whale/manifest.json new file mode 100644 index 00000000..11d9bb82 --- /dev/null +++ b/whale/manifest.json @@ -0,0 +1,28 @@ +{ + "manifest_version": 2, + "name": "안녕 웨일", + "version": "1.0.0", + + "description": "확장앱 설명", + + "sidebar_action": { + "default_icon": { + "16": "images/icon.png" + }, + "default_page": "index.html" + }, + + "background": { + "scripts": ["background.js"], + "persistent": false + }, + + "permissions": [ + "tabs", + "contextMenus" + ], + + "externally_connectable": { + "matches": ["http://localhost/*"] + } +} \ No newline at end of file
{{ originalTitle }}