From 01f708e350fc59099ad22ef4c7e9ce4919149c2a Mon Sep 17 00:00:00 2001 From: TaeYoon Date: Fri, 5 Aug 2022 13:43:29 +0900 Subject: [PATCH 01/51] =?UTF-8?q?fix:=20=EC=8A=A4=ED=84=B0=EB=94=94=20?= =?UTF-8?q?=EA=B0=80=EC=9E=85=20=ED=9B=84=20=EC=9C=A0=EC=A0=80=20role?= =?UTF-8?q?=EC=9D=84=20refetch=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/pages/detail-page/hooks/useDetailPage.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/pages/detail-page/hooks/useDetailPage.ts b/frontend/src/pages/detail-page/hooks/useDetailPage.ts index 69cfcd0a2..0700aabc8 100644 --- a/frontend/src/pages/detail-page/hooks/useDetailPage.ts +++ b/frontend/src/pages/detail-page/hooks/useDetailPage.ts @@ -40,6 +40,7 @@ const useDetailPage = () => { onSuccess: () => { alert('가입했습니다 :D'); detailQueryResult.refetch(); + userRoleQueryResult.refetch(); }, }, ); From b9574cfec08c05e695fd74349a2fe2624232a0b3 Mon Sep 17 00:00:00 2001 From: airman5573 <68623798+airman5573@users.noreply.github.com> Date: Mon, 8 Aug 2022 22:50:23 +0900 Subject: [PATCH 02/51] =?UTF-8?q?[FE]=20issue226:=20=EA=B0=81=EC=A2=85=20?= =?UTF-8?q?=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95=20(#228)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: 싴, 뷁 등 나눔폰트에서 지원되지 않는 글자가 있기 때문에 폰트 변경 Co-authored-by: TaeYoon * fix: 스터디 디테일 페이지에서 인원이 7명일때 더보기 버튼이 보이지 않는 오류 Co-authored-by: TaeYoon * fix: 스터디 생성시 etc(미분류) 태그를 기본으로 선택하도록 수정 및 선택 안함 제거 Co-authored-by: TaeYoon * fix: 최대 모집 인원이 없을때 무한대 텍스트 표시 Co-authored-by: TaeYoon * refactor: Tag파일 이름을 Subject로 수정 Co-authored-by: TaeYoon * WIP * feat: svg 직접 사용 Co-authored-by: TaeYoon --- frontend/env/.env.local | 2 +- frontend/package-lock.json | 371 +++++++++++++++++- frontend/package.json | 2 +- .../components/arrow-button/ArrowButton.tsx | 36 +- frontend/src/index.tsx | 2 +- frontend/src/layout/header/Header.tsx | 53 ++- .../components/search-bar/SearchBar.tsx | 19 +- .../create-study-page/CreateStudyPage.tsx | 4 +- .../Subject.style.tsx} | 2 +- .../{tag/Tag.tsx => subject/Subject.tsx} | 23 +- .../study-float-box/StudyFloatBox.tsx | 2 +- .../StudyMemberSection.tsx | 25 +- .../components/my-study-card/MyStudyCard.tsx | 44 ++- frontend/src/styles/Globalstyles.tsx | 72 +++- frontend/webpack/webpack.common.js | 1 - frontend/webpack/webpack.dev.js | 1 + frontend/webpack/webpack.local.js | 1 + 17 files changed, 605 insertions(+), 55 deletions(-) rename frontend/src/pages/create-study-page/components/{tag/Tag.style.tsx => subject/Subject.style.tsx} (51%) rename frontend/src/pages/create-study-page/components/{tag/Tag.tsx => subject/Subject.tsx} (67%) diff --git a/frontend/env/.env.local b/frontend/env/.env.local index 5d27bfa26..479996cc2 100644 --- a/frontend/env/.env.local +++ b/frontend/env/.env.local @@ -1,2 +1,2 @@ -API_URL="" +API_URL="https://dev.moamoa.ne.kr" CLIENT_ID="cb83d95cd5644436b090" diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 38047700e..b87e38e56 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -15,7 +15,6 @@ "marked": "^4.0.18", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-icons": "^4.4.0", "react-query": "^3.39.1", "react-router-dom": "^6.3.0" }, @@ -66,6 +65,7 @@ "prettier": "^2.7.1", "typescript": "^4.7.4", "webpack": "^5.73.0", + "webpack-bundle-analyzer": "^4.5.0", "webpack-cli": "^4.10.0", "webpack-dev-server": "^4.9.2", "webpack-merge": "^5.8.0" @@ -3076,6 +3076,12 @@ "node": ">= 8" } }, + "node_modules/@polka/url": { + "version": "1.0.0-next.21", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.21.tgz", + "integrity": "sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==", + "dev": true + }, "node_modules/@storybook/addon-actions": { "version": "6.5.9", "resolved": "https://registry.npmjs.org/@storybook/addon-actions/-/addon-actions-6.5.9.tgz", @@ -14192,6 +14198,12 @@ "integrity": "sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==", "dev": true }, + "node_modules/duplexer": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", + "dev": true + }, "node_modules/duplexify": { "version": "3.7.1", "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", @@ -16966,6 +16978,21 @@ "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" } }, + "node_modules/gzip-size": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", + "integrity": "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==", + "dev": true, + "dependencies": { + "duplexer": "^0.1.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/handle-thing": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", @@ -20362,6 +20389,15 @@ "mkdirp": "bin/cmd.js" } }, + "node_modules/mrmime": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-1.0.1.tgz", + "integrity": "sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -21156,6 +21192,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/opener": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", + "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==", + "dev": true, + "bin": { + "opener": "bin/opener-bin.js" + } + }, "node_modules/optionator": { "version": "0.8.3", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", @@ -22482,14 +22527,6 @@ "react": "^18.2.0" } }, - "node_modules/react-icons": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.4.0.tgz", - "integrity": "sha512-fSbvHeVYo/B5/L4VhB7sBA1i2tS8MkT0Hb9t2H1AVPkwGfVHLJCqyr2Py9dKMxsyM63Eng1GkdZfbWj+Fmv8Rg==", - "peerDependencies": { - "react": "*" - } - }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -24036,6 +24073,20 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true }, + "node_modules/sirv": { + "version": "1.0.19", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-1.0.19.tgz", + "integrity": "sha512-JuLThK3TnZG1TAKDwNIqNq6QA2afLOCcm+iE8D1Kj3GA40pSPsxQjjJl0J8X3tsR7T+CP1GavpzLwYkgVLWrZQ==", + "dev": true, + "dependencies": { + "@polka/url": "^1.0.0-next.20", + "mrmime": "^1.0.0", + "totalist": "^1.0.0" + }, + "engines": { + "node": ">= 10" + } + }, "node_modules/sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", @@ -25471,6 +25522,15 @@ "node": ">=0.6" } }, + "node_modules/totalist": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-1.1.0.tgz", + "integrity": "sha512-gduQwd1rOdDMGxFG1gEvhV88Oirdo2p+KjoYFU7k2g+i7n6AFFbDQ5kMPUsW0pNbfQsB/cwXvT1i4Bue0s9g5g==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/tough-cookie": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", @@ -26745,6 +26805,150 @@ } } }, + "node_modules/webpack-bundle-analyzer": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.5.0.tgz", + "integrity": "sha512-GUMZlM3SKwS8Z+CKeIFx7CVoHn3dXFcUAjT/dcZQQmfSZGvitPfMob2ipjai7ovFFqPvTqkEZ/leL4O0YOdAYQ==", + "dev": true, + "dependencies": { + "acorn": "^8.0.4", + "acorn-walk": "^8.0.0", + "chalk": "^4.1.0", + "commander": "^7.2.0", + "gzip-size": "^6.0.0", + "lodash": "^4.17.20", + "opener": "^1.5.2", + "sirv": "^1.0.7", + "ws": "^7.3.1" + }, + "bin": { + "webpack-bundle-analyzer": "lib/bin/analyzer.js" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/webpack-bundle-analyzer/node_modules/acorn": { + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.0.tgz", + "integrity": "sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/webpack-bundle-analyzer/node_modules/acorn-walk": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/webpack-bundle-analyzer/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/webpack-bundle-analyzer/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/webpack-bundle-analyzer/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/webpack-bundle-analyzer/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/webpack-bundle-analyzer/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/webpack-bundle-analyzer/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/webpack-bundle-analyzer/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/webpack-bundle-analyzer/node_modules/ws": { + "version": "7.5.9", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", + "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", + "dev": true, + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/webpack-cli": { "version": "4.10.0", "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-4.10.0.tgz", @@ -29697,6 +29901,12 @@ } } }, + "@polka/url": { + "version": "1.0.0-next.21", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.21.tgz", + "integrity": "sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==", + "dev": true + }, "@storybook/addon-actions": { "version": "6.5.9", "resolved": "https://registry.npmjs.org/@storybook/addon-actions/-/addon-actions-6.5.9.tgz", @@ -38342,6 +38552,12 @@ "integrity": "sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==", "dev": true }, + "duplexer": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", + "dev": true + }, "duplexify": { "version": "3.7.1", "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", @@ -40552,6 +40768,15 @@ "integrity": "sha512-qbHgh8Ix+j/qY+a/ZcJnFQ+j8ezakqPiHwPiZhV/3PgGlgf96QMBB5/f2rkiC9sgLoy/xvT6TSiaf2nTHJh5iA==", "dev": true }, + "gzip-size": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", + "integrity": "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==", + "dev": true, + "requires": { + "duplexer": "^0.1.2" + } + }, "handle-thing": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", @@ -43114,6 +43339,12 @@ } } }, + "mrmime": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-1.0.1.tgz", + "integrity": "sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw==", + "dev": true + }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -43738,6 +43969,12 @@ "is-wsl": "^2.2.0" } }, + "opener": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", + "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==", + "dev": true + }, "optionator": { "version": "0.8.3", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", @@ -44761,12 +44998,6 @@ "scheduler": "^0.23.0" } }, - "react-icons": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.4.0.tgz", - "integrity": "sha512-fSbvHeVYo/B5/L4VhB7sBA1i2tS8MkT0Hb9t2H1AVPkwGfVHLJCqyr2Py9dKMxsyM63Eng1GkdZfbWj+Fmv8Rg==", - "requires": {} - }, "react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -45993,6 +46224,17 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true }, + "sirv": { + "version": "1.0.19", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-1.0.19.tgz", + "integrity": "sha512-JuLThK3TnZG1TAKDwNIqNq6QA2afLOCcm+iE8D1Kj3GA40pSPsxQjjJl0J8X3tsR7T+CP1GavpzLwYkgVLWrZQ==", + "dev": true, + "requires": { + "@polka/url": "^1.0.0-next.20", + "mrmime": "^1.0.0", + "totalist": "^1.0.0" + } + }, "sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", @@ -47161,6 +47403,12 @@ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", "dev": true }, + "totalist": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-1.1.0.tgz", + "integrity": "sha512-gduQwd1rOdDMGxFG1gEvhV88Oirdo2p+KjoYFU7k2g+i7n6AFFbDQ5kMPUsW0pNbfQsB/cwXvT1i4Bue0s9g5g==", + "dev": true + }, "tough-cookie": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", @@ -48240,6 +48488,99 @@ } } }, + "webpack-bundle-analyzer": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.5.0.tgz", + "integrity": "sha512-GUMZlM3SKwS8Z+CKeIFx7CVoHn3dXFcUAjT/dcZQQmfSZGvitPfMob2ipjai7ovFFqPvTqkEZ/leL4O0YOdAYQ==", + "dev": true, + "requires": { + "acorn": "^8.0.4", + "acorn-walk": "^8.0.0", + "chalk": "^4.1.0", + "commander": "^7.2.0", + "gzip-size": "^6.0.0", + "lodash": "^4.17.20", + "opener": "^1.5.2", + "sirv": "^1.0.7", + "ws": "^7.3.1" + }, + "dependencies": { + "acorn": { + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.0.tgz", + "integrity": "sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==", + "dev": true + }, + "acorn-walk": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", + "dev": true + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "ws": { + "version": "7.5.9", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", + "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", + "dev": true, + "requires": {} + } + } + }, "webpack-cli": { "version": "4.10.0", "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-4.10.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 47443e850..2b4cf9cc5 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -72,6 +72,7 @@ "prettier": "^2.7.1", "typescript": "^4.7.4", "webpack": "^5.73.0", + "webpack-bundle-analyzer": "^4.5.0", "webpack-cli": "^4.10.0", "webpack-dev-server": "^4.9.2", "webpack-merge": "^5.8.0" @@ -83,7 +84,6 @@ "marked": "^4.0.18", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-icons": "^4.4.0", "react-query": "^3.39.1", "react-router-dom": "^6.3.0" }, diff --git a/frontend/src/components/arrow-button/ArrowButton.tsx b/frontend/src/components/arrow-button/ArrowButton.tsx index 3601f0b93..5509c09bb 100644 --- a/frontend/src/components/arrow-button/ArrowButton.tsx +++ b/frontend/src/components/arrow-button/ArrowButton.tsx @@ -1,5 +1,3 @@ -import { BsChevronLeft, BsChevronRight } from 'react-icons/bs'; - import * as S from '@components/arrow-button/ArrowButton.style'; export type SlideButtonProps = { @@ -8,6 +6,40 @@ export type SlideButtonProps = { onSlideButtonClick: React.MouseEventHandler; }; +const BsChevronLeft = () => ( + + + +); + +const BsChevronRight = () => ( + + + +); + const SlideButton: React.FC = ({ direction, ariaLabel, diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx index 478c9b739..80dd71072 100644 --- a/frontend/src/index.tsx +++ b/frontend/src/index.tsx @@ -16,7 +16,7 @@ import App from './App'; if (process.env.NODE_ENV == 'development') { // eslint-disable-next-line @typescript-eslint/no-var-requires const { worker } = require('./mocks/browser'); - worker.start(); + // worker.start();j } const $root = document.getElementById('root'); diff --git a/frontend/src/layout/header/Header.tsx b/frontend/src/layout/header/Header.tsx index bfda705fe..edd9b1e8d 100644 --- a/frontend/src/layout/header/Header.tsx +++ b/frontend/src/layout/header/Header.tsx @@ -1,8 +1,7 @@ import { useContext, useState } from 'react'; -import { BiBookmark } from 'react-icons/bi'; -import { MdOutlineLogin, MdOutlineLogout } from 'react-icons/md'; import { Link, useNavigate } from 'react-router-dom'; +// TODO: 불필요한 Import , 상수 자체가 필요 없음 import { PATH, PROFILE_IMAGE_URL } from '@constants'; import { useAuth } from '@hooks/useAuth'; @@ -22,6 +21,50 @@ export type HeaderProps = { className?: string; }; +const MdOutlineLogin = () => ( + + + + +); + +const MdOutlineLogout = () => ( + + + + +); + +const BiBookmark = () => ( + + + +); + const Header: React.FC = ({ className }) => { const { setKeyword } = useContext(SearchContext); @@ -58,7 +101,7 @@ const Header: React.FC = ({ className }) => { - + 내 스터디 @@ -68,7 +111,7 @@ const Header: React.FC = ({ className }) => { {openDropDownBox && ( - + 로그아웃 @@ -77,7 +120,7 @@ const Header: React.FC = ({ className }) => { ) : ( - + 로그인 diff --git a/frontend/src/layout/header/components/search-bar/SearchBar.tsx b/frontend/src/layout/header/components/search-bar/SearchBar.tsx index ebd586799..090b694a9 100644 --- a/frontend/src/layout/header/components/search-bar/SearchBar.tsx +++ b/frontend/src/layout/header/components/search-bar/SearchBar.tsx @@ -1,5 +1,3 @@ -import { FiSearch } from 'react-icons/fi'; - import * as S from '@layout/header/components/search-bar/SearchBar.style'; export type SearchBarProps = { @@ -7,6 +5,23 @@ export type SearchBarProps = { inputName?: string; }; +const FiSearch = () => ( + + + + +); + const SearchBar: React.FC = ({ onSubmit, inputName = 'keyword' }) => { return ( diff --git a/frontend/src/pages/create-study-page/CreateStudyPage.tsx b/frontend/src/pages/create-study-page/CreateStudyPage.tsx index 6575fe948..77a8d9869 100644 --- a/frontend/src/pages/create-study-page/CreateStudyPage.tsx +++ b/frontend/src/pages/create-study-page/CreateStudyPage.tsx @@ -12,7 +12,7 @@ import Excerpt from '@create-study-page/components/excerpt/Excerpt'; import MaxMemberCount from '@create-study-page/components/max-member-count/MaxMemberCount'; import Period from '@create-study-page/components/period/Peroid'; import Publish from '@create-study-page/components/publish/Publish'; -import Tag from '@create-study-page/components/tag/Tag'; +import Subject from '@create-study-page/components/subject/Subject'; import Title from '@create-study-page/components/title/Title'; import useCreateStudyPage from '@create-study-page/hooks/useCreateStudyPage'; @@ -47,7 +47,7 @@ const CreateStudyPage: React.FC = () => { margin-bottom: 15px; `} /> - { +const Subject = ({ className }: SubjectProps) => { const { register } = useFormContext(); const { data, isLoading, isError } = useGetTagList(); @@ -25,16 +25,15 @@ const Tag = ({ className }: TagProps) => { return ( @@ -43,15 +42,15 @@ const Tag = ({ className }: TagProps) => { }; return ( - + - + {render()} - + ); }; -export default Tag; +export default Subject; diff --git a/frontend/src/pages/detail-page/components/study-float-box/StudyFloatBox.tsx b/frontend/src/pages/detail-page/components/study-float-box/StudyFloatBox.tsx index d1aa12bd3..8c0c1e69a 100644 --- a/frontend/src/pages/detail-page/components/study-float-box/StudyFloatBox.tsx +++ b/frontend/src/pages/detail-page/components/study-float-box/StudyFloatBox.tsx @@ -75,7 +75,7 @@ const StudyFloatBox: React.FC = ({ 모집인원 - {currentMemberCount} / {maxMemberCount} + {currentMemberCount} / {maxMemberCount ?? '∞'} diff --git a/frontend/src/pages/detail-page/components/study-member-section/StudyMemberSection.tsx b/frontend/src/pages/detail-page/components/study-member-section/StudyMemberSection.tsx index 01c07bdb7..76ad6c50c 100644 --- a/frontend/src/pages/detail-page/components/study-member-section/StudyMemberSection.tsx +++ b/frontend/src/pages/detail-page/components/study-member-section/StudyMemberSection.tsx @@ -1,5 +1,4 @@ import { useState } from 'react'; -import { TbCrown } from 'react-icons/tb'; import { DEFAULT_VISIBLE_STUDY_MEMBER_CARD_COUNT } from '@constants'; @@ -16,6 +15,24 @@ export type StudyMemberSectionProps = { members: Array; }; +const TbCrown = () => ( + + + + + +); + const StudyMemberSection: React.FC = ({ owner, members }) => { const [showAll, setShowAll] = useState(false); @@ -35,7 +52,7 @@ const StudyMemberSection: React.FC = ({ owner, members <> - + = ({ owner, members <> - + = ({ owner, members 스터디원 {totalMembers.length}명 {renderMembers()} - {members.length > DEFAULT_VISIBLE_STUDY_MEMBER_CARD_COUNT && ( + {totalMembers.length > DEFAULT_VISIBLE_STUDY_MEMBER_CARD_COUNT && ( ; +const TbCrown = () => ( + + + + + +); + +const HiOutlineTrash = () => ( + + + +); + const MyStudyCard: React.FC = ({ title, ownerName, @@ -30,7 +64,7 @@ const MyStudyCard: React.FC = ({ {title} - + {ownerName} @@ -44,7 +78,7 @@ const MyStudyCard: React.FC = ({ {startDate} ~ {endDate || ''} - + diff --git a/frontend/src/styles/Globalstyles.tsx b/frontend/src/styles/Globalstyles.tsx index f431c0caf..51722e7f5 100644 --- a/frontend/src/styles/Globalstyles.tsx +++ b/frontend/src/styles/Globalstyles.tsx @@ -18,7 +18,7 @@ const GlobalStyles = () => { color: ${theme.colors.black}; } - @font-face { + /* @font-face { font-family: 'NanumSquareRound'; font-weight: 300; src: url(https://hangeul.pstatic.net/hangeul_static/webfont/NanumSquareRound/NanumSquareRoundL.eot); @@ -72,10 +72,78 @@ const GlobalStyles = () => { format('woff'), url(https://hangeul.pstatic.net/hangeul_static/webfont/NanumSquareRound/NanumSquareRoundEB.ttf) format('truetype'); + } */ + + @font-face { + font-family: 'Pretendard'; + src: url('https://cdn.jsdelivr.net/gh/Project-Noonnu/noonfonts_2107@1.1/Pretendard-Thin.woff') format('woff'); + font-weight: 100; + font-style: normal; + } + + @font-face { + font-family: 'Pretendard'; + src: url('https://cdn.jsdelivr.net/gh/Project-Noonnu/noonfonts_2107@1.1/Pretendard-ExtraLight.woff') + format('woff'); + font-weight: 200; + font-style: normal; + } + + @font-face { + font-family: 'Pretendard'; + src: url('https://cdn.jsdelivr.net/gh/Project-Noonnu/noonfonts_2107@1.1/Pretendard-Light.woff') format('woff'); + font-weight: 300; + font-style: normal; + } + + @font-face { + font-family: 'Pretendard'; + src: url('https://cdn.jsdelivr.net/gh/Project-Noonnu/noonfonts_2107@1.1/Pretendard-Regular.woff') + format('woff'); + font-weight: 400; + font-style: normal; + } + + @font-face { + font-family: 'Pretendard'; + src: url('https://cdn.jsdelivr.net/gh/Project-Noonnu/noonfonts_2107@1.1/Pretendard-Medium.woff') + format('woff'); + font-weight: 500; + font-style: normal; + } + + @font-face { + font-family: 'Pretendard'; + src: url('https://cdn.jsdelivr.net/gh/Project-Noonnu/noonfonts_2107@1.1/Pretendard-SemiBold.woff') + format('woff'); + font-weight: 600; + font-style: normal; + } + + @font-face { + font-family: 'Pretendard'; + src: url('https://cdn.jsdelivr.net/gh/Project-Noonnu/noonfonts_2107@1.1/Pretendard-Bold.woff') format('woff'); + font-weight: 700; + font-style: normal; + } + + @font-face { + font-family: 'Pretendard'; + src: url('https://cdn.jsdelivr.net/gh/Project-Noonnu/noonfonts_2107@1.1/Pretendard-ExtraBold.woff') + format('woff'); + font-weight: 800; + font-style: normal; + } + + @font-face { + font-family: 'Pretendard'; + src: url('https://cdn.jsdelivr.net/gh/Project-Noonnu/noonfonts_2107@1.1/Pretendard-Black.woff') format('woff'); + font-weight: 900; + font-style: normal; } body { - font-family: 'NanumSquareRound', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', + font-family: 'Pretendard', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; background-color: ${theme.colors.secondary.light}; } diff --git a/frontend/webpack/webpack.common.js b/frontend/webpack/webpack.common.js index b7f35e415..85fe92ee9 100644 --- a/frontend/webpack/webpack.common.js +++ b/frontend/webpack/webpack.common.js @@ -7,7 +7,6 @@ const { CleanWebpackPlugin } = require('clean-webpack-plugin'); module.exports = { mode: 'development', entry: join(__dirname, '../src/index.tsx'), - devtool: 'eval-source-map', output: { filename: 'main.js', path: join(__dirname, '../dist'), diff --git a/frontend/webpack/webpack.dev.js b/frontend/webpack/webpack.dev.js index 6ae4fab08..c6bf3d46a 100644 --- a/frontend/webpack/webpack.dev.js +++ b/frontend/webpack/webpack.dev.js @@ -9,6 +9,7 @@ const common = require('./webpack.common'); module.exports = merge(common, { mode: 'production', + devtool: 'eval-source-map', plugins: [ new webpack.DefinePlugin({ 'process.env.API_URL': JSON.stringify(process.env.API_URL), diff --git a/frontend/webpack/webpack.local.js b/frontend/webpack/webpack.local.js index c95550103..4412f35f5 100644 --- a/frontend/webpack/webpack.local.js +++ b/frontend/webpack/webpack.local.js @@ -9,6 +9,7 @@ const common = require('./webpack.common'); module.exports = merge(common, { mode: 'development', + devtool: 'eval-source-map', devServer: { open: true, port: 3000, From 252cbe25ccd7f1b5a6f2fcf13a789b249ed3e5ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=A7=84=ED=98=81?= Date: Tue, 9 Aug 2022 14:03:27 +0900 Subject: [PATCH 03/51] =?UTF-8?q?[BE]=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EB=A6=AC=ED=8C=A9=ED=84=B0=EB=A7=81=20(#222)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: 인수 테스트 픽스처 생성 메서드 추가 * refactor: 인수 테스트 픽스처 정리 * refactor: 테스트 setUp 제거 * refactor: Steps를 추상 클래스로 변경 --- .../service/response/WriterResponse.java | 2 + .../request/CreatingStudyRequestBuilder.java | 75 ++++++ .../acceptance/AcceptanceTest.java | 60 +---- .../acceptance/fixture/MemberFixtures.java | 38 +++ .../acceptance/fixture/StudyFixtures.java | 34 +++ .../acceptance/fixture/TagFixtures.java | 31 +++ .../review/ReviewsAcceptanceTest.java | 177 ------------- .../acceptance/steps/AfterLoginSteps.java | 87 +++++++ .../acceptance/steps/CreatingStudyStep.java | 59 +++++ .../acceptance/steps/LoginSteps.java | 66 +++++ .../SetRequiredDataToCreatingStudySteps.java | 20 ++ .../woowacourse/acceptance/steps/Steps.java | 68 +++++ .../acceptance/steps/StudyRelatedSteps.java | 51 ++++ .../study/GettingMyStudiesAcceptanceTest.java | 139 ----------- .../GettingStudyDetailsAcceptanceTest.java | 141 ----------- .../ParticipationStudyAcceptanceTest.java | 152 ----------- .../{ => test}/auth/AuthAcceptanceTest.java | 2 +- .../{ => test}/cors/CorsAcceptanceTest.java | 2 +- .../member/MemberAcceptanceTest.java | 26 +- .../test/review/ReviewsAcceptanceTest.java | 235 ++++++++++++++++++ .../AutoCloseEnrollmentAcceptanceTest.java | 2 +- .../study/CreatingStudyAcceptanceTest.java | 25 +- .../study/GettingMyStudiesAcceptanceTest.java | 116 +++++++++ .../GettingStudiesSummaryAcceptanceTest.java | 125 ++++------ .../GettingStudyDetailsAcceptanceTest.java | 120 +++++++++ .../ParticipationStudyAcceptanceTest.java | 36 +++ .../study/SearchingStudiesAcceptanceTest.java | 98 +++----- .../{ => test}/tag/TagAcceptanceTest.java | 18 +- .../repository/MemberRepositoryTest.java | 3 - .../moamoa/member/query/MemberDaoTest.java | 24 -- .../controller/ReviewControllerTest.java | 3 - .../SearchingReviewControllerTest.java | 10 +- .../controller/MyStudyControllerTest.java | 10 - .../SearchingStudyControllerTest.java | 13 - .../moamoa/study/query/MyStudyDaoTest.java | 10 - .../study/query/StudyDetailsDaoTest.java | 10 - .../study/query/StudySummaryDaoTest.java | 10 - .../study/service/MyStudyServiceTest.java | 10 - .../SearchingTagControllerTest.java | 16 -- .../moamoa/tag/query/TagDaoTest.java | 10 - backend/src/test/resources/data.sql | 9 + 41 files changed, 1152 insertions(+), 991 deletions(-) create mode 100644 backend/src/main/java/com/woowacourse/moamoa/study/service/request/CreatingStudyRequestBuilder.java create mode 100644 backend/src/test/java/com/woowacourse/acceptance/fixture/MemberFixtures.java create mode 100644 backend/src/test/java/com/woowacourse/acceptance/fixture/StudyFixtures.java create mode 100644 backend/src/test/java/com/woowacourse/acceptance/fixture/TagFixtures.java delete mode 100644 backend/src/test/java/com/woowacourse/acceptance/review/ReviewsAcceptanceTest.java create mode 100644 backend/src/test/java/com/woowacourse/acceptance/steps/AfterLoginSteps.java create mode 100644 backend/src/test/java/com/woowacourse/acceptance/steps/CreatingStudyStep.java create mode 100644 backend/src/test/java/com/woowacourse/acceptance/steps/LoginSteps.java create mode 100644 backend/src/test/java/com/woowacourse/acceptance/steps/SetRequiredDataToCreatingStudySteps.java create mode 100644 backend/src/test/java/com/woowacourse/acceptance/steps/Steps.java create mode 100644 backend/src/test/java/com/woowacourse/acceptance/steps/StudyRelatedSteps.java delete mode 100644 backend/src/test/java/com/woowacourse/acceptance/study/GettingMyStudiesAcceptanceTest.java delete mode 100644 backend/src/test/java/com/woowacourse/acceptance/study/GettingStudyDetailsAcceptanceTest.java delete mode 100644 backend/src/test/java/com/woowacourse/acceptance/study/ParticipationStudyAcceptanceTest.java rename backend/src/test/java/com/woowacourse/acceptance/{ => test}/auth/AuthAcceptanceTest.java (98%) rename backend/src/test/java/com/woowacourse/acceptance/{ => test}/cors/CorsAcceptanceTest.java (93%) rename backend/src/test/java/com/woowacourse/acceptance/{ => test}/member/MemberAcceptanceTest.java (58%) create mode 100644 backend/src/test/java/com/woowacourse/acceptance/test/review/ReviewsAcceptanceTest.java rename backend/src/test/java/com/woowacourse/acceptance/{ => test}/study/AutoCloseEnrollmentAcceptanceTest.java (96%) rename backend/src/test/java/com/woowacourse/acceptance/{ => test}/study/CreatingStudyAcceptanceTest.java (84%) create mode 100644 backend/src/test/java/com/woowacourse/acceptance/test/study/GettingMyStudiesAcceptanceTest.java rename backend/src/test/java/com/woowacourse/acceptance/{ => test}/study/GettingStudiesSummaryAcceptanceTest.java (59%) create mode 100644 backend/src/test/java/com/woowacourse/acceptance/test/study/GettingStudyDetailsAcceptanceTest.java create mode 100644 backend/src/test/java/com/woowacourse/acceptance/test/study/ParticipationStudyAcceptanceTest.java rename backend/src/test/java/com/woowacourse/acceptance/{ => test}/study/SearchingStudiesAcceptanceTest.java (69%) rename backend/src/test/java/com/woowacourse/acceptance/{ => test}/tag/TagAcceptanceTest.java (79%) delete mode 100644 backend/src/test/java/com/woowacourse/moamoa/member/query/MemberDaoTest.java create mode 100644 backend/src/test/resources/data.sql diff --git a/backend/src/main/java/com/woowacourse/moamoa/review/service/response/WriterResponse.java b/backend/src/main/java/com/woowacourse/moamoa/review/service/response/WriterResponse.java index 870133c9c..a0441720d 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/review/service/response/WriterResponse.java +++ b/backend/src/main/java/com/woowacourse/moamoa/review/service/response/WriterResponse.java @@ -7,11 +7,13 @@ import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; +import lombok.ToString; @Getter @NoArgsConstructor(access = AccessLevel.PRIVATE) @AllArgsConstructor @EqualsAndHashCode +@ToString public class WriterResponse { @JsonProperty("id") diff --git a/backend/src/main/java/com/woowacourse/moamoa/study/service/request/CreatingStudyRequestBuilder.java b/backend/src/main/java/com/woowacourse/moamoa/study/service/request/CreatingStudyRequestBuilder.java new file mode 100644 index 000000000..2de4346b8 --- /dev/null +++ b/backend/src/main/java/com/woowacourse/moamoa/study/service/request/CreatingStudyRequestBuilder.java @@ -0,0 +1,75 @@ +package com.woowacourse.moamoa.study.service.request; + +import java.time.LocalDate; +import java.util.List; + +public class CreatingStudyRequestBuilder { + + private String title; + + private String excerpt; + + private String thumbnail; + + private String description; + + private Integer maxMemberCount; + + private LocalDate startDate; + + private LocalDate enrollmentEndDate; + + private LocalDate endDate; + + private List tagIds; + + public CreatingStudyRequestBuilder title(final String title) { + this.title = title; + return this; + } + + public CreatingStudyRequestBuilder excerpt(final String excerpt) { + this.excerpt = excerpt; + return this; + } + + public CreatingStudyRequestBuilder thumbnail(final String thumbnail) { + this.thumbnail = thumbnail; + return this; + } + + public CreatingStudyRequestBuilder description(final String description) { + this.description = description; + return this; + } + + public CreatingStudyRequestBuilder maxMemberCount(final Integer maxMemberCount) { + this.maxMemberCount = maxMemberCount; + return this; + } + + public CreatingStudyRequestBuilder startDate(final LocalDate startDate) { + this.startDate = startDate; + return this; + } + + public CreatingStudyRequestBuilder enrollmentEndDate(final LocalDate enrollmentEndDate) { + this.enrollmentEndDate = enrollmentEndDate; + return this; + } + + public CreatingStudyRequestBuilder endDate(final LocalDate endDate) { + this.endDate = endDate; + return this; + } + + public CreatingStudyRequestBuilder tagIds(final List tagIds) { + this.tagIds = tagIds; + return this; + } + + public CreatingStudyRequest build() { + return new CreatingStudyRequest(title, excerpt, thumbnail, description, maxMemberCount, startDate, + enrollmentEndDate, endDate, tagIds); + } +} diff --git a/backend/src/test/java/com/woowacourse/acceptance/AcceptanceTest.java b/backend/src/test/java/com/woowacourse/acceptance/AcceptanceTest.java index 1bfce2b30..f702e883a 100644 --- a/backend/src/test/java/com/woowacourse/acceptance/AcceptanceTest.java +++ b/backend/src/test/java/com/woowacourse/acceptance/AcceptanceTest.java @@ -1,8 +1,5 @@ package com.woowacourse.acceptance; -import static org.springframework.http.HttpHeaders.AUTHORIZATION; -import static org.springframework.http.HttpHeaders.CONTENT_TYPE; -import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; import static org.springframework.restdocs.restassured3.RestAssuredRestDocumentation.documentationConfiguration; import static org.springframework.test.web.client.match.MockRestRequestMatchers.content; @@ -13,11 +10,10 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import com.woowacourse.acceptance.steps.Steps; import com.woowacourse.moamoa.MoamoaApplication; import com.woowacourse.moamoa.auth.service.oauthclient.response.GithubProfileResponse; import com.woowacourse.moamoa.auth.service.request.AccessTokenRequest; -import com.woowacourse.moamoa.review.service.request.WriteReviewRequest; -import com.woowacourse.moamoa.study.service.request.CreatingStudyRequest; import io.restassured.RestAssured; import io.restassured.builder.RequestSpecBuilder; import io.restassured.specification.RequestSpecification; @@ -31,7 +27,6 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import org.springframework.boot.web.server.LocalServerPort; -import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; @@ -92,14 +87,17 @@ protected void setRestAssuredPort() { @BeforeEach void mockingGithubServer() { mockServer = MockRestServiceServer.createServer(restTemplate); + Steps.mockServer = mockServer; + Steps.clientId = clientId; + Steps.clientSecret = clientSecret; + Steps.objectMapper = objectMapper; + Steps.clearTokenCaches(); } @AfterEach void tearDown() { jdbcTemplate.update("DELETE FROM study_tag"); jdbcTemplate.update("DELETE FROM study_member"); - jdbcTemplate.update("DELETE FROM tag"); - jdbcTemplate.update("DELETE FROM category"); jdbcTemplate.update("DELETE FROM review"); jdbcTemplate.update("DELETE FROM study"); jdbcTemplate.update("DELETE FROM member"); @@ -122,52 +120,6 @@ protected String getBearerTokenBySignInOrUp(GithubProfileResponse response) { return "Bearer " + token; } - protected long createStudy(String jwtToken, CreatingStudyRequest request) { - try { - final String location = RestAssured.given().log().all() - .header(HttpHeaders.AUTHORIZATION, jwtToken) - .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) - .body(objectMapper.writeValueAsString(request)) - .when().log().all() - .post("/api/studies") - .then().log().all() - .statusCode(HttpStatus.CREATED.value()) - .extract().header(HttpHeaders.LOCATION); - return Long.parseLong(location.replaceAll("/api/studies/", "")); - } catch (Exception e) { - Assertions.fail("스터디 생성 실패"); - return -1; - } - } - - protected long createReview(String jwtToken, Long studyId, WriteReviewRequest request) { - try { - final String location = RestAssured.given().log().all() - .header(HttpHeaders.AUTHORIZATION, jwtToken) - .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) - .pathParams("study-id", studyId) - .body(objectMapper.writeValueAsString(request)) - .when().post("/api/studies/{study-id}/reviews") - .then().log().all() - .statusCode(HttpStatus.CREATED.value()) - .extract().header(HttpHeaders.LOCATION); - return Long.parseLong(location.replaceAll("/api/studies/" + studyId + "/reviews/", "")); - } catch (Exception e) { - Assertions.fail("리뷰 작성 실패"); - return -1; - } - } - - protected void participateStudy(String jwtToken, Long studyId) { - RestAssured.given().log().all() - .header(CONTENT_TYPE, APPLICATION_JSON_VALUE) - .header(AUTHORIZATION, jwtToken) - .when().log().all() - .post("/api/studies/" + studyId) - .then().log().all() - .statusCode(HttpStatus.OK.value()); - } - private void mockingGithubServer(String authorizationCode, GithubProfileResponse response) { try { mockingGithubServerForGetAccessToken(authorizationCode, Map.of("access_token", "access-token", diff --git a/backend/src/test/java/com/woowacourse/acceptance/fixture/MemberFixtures.java b/backend/src/test/java/com/woowacourse/acceptance/fixture/MemberFixtures.java new file mode 100644 index 000000000..ee32adbdc --- /dev/null +++ b/backend/src/test/java/com/woowacourse/acceptance/fixture/MemberFixtures.java @@ -0,0 +1,38 @@ +package com.woowacourse.acceptance.fixture; + +import com.woowacourse.moamoa.auth.service.oauthclient.response.GithubProfileResponse; + +public class MemberFixtures { + + public static final long 짱구_깃허브_ID = 1L; + public static final String 짱구_이름 = "jjanggu"; + public static final String 짱구_이미지_URL = "https://jjanggu"; + public static final String 짱구_프로필_URL = "github.com/jjanggu"; + + public static final long 그린론_깃허브_ID = 2L; + public static final String 그린론_이름 = "greenlawn"; + public static final String 그린론_이미지_URL = "https://greenlawn"; + public static final String 그린론_프로필_URL = "github.com/greenlawn"; + + public static final long 디우_깃허브_ID = 3L; + public static final String 디우_이름 = "dwoo"; + public static final String 디우_이미지_URL = "https://dwoo"; + public static final String 디우_프로필_URL = "github.com/dwoo"; + + public static final long 베루스_깃허브_ID = 4L; + public static final String 베루스_이름 = "verus"; + public static final String 베루스_이미지_URL = "https://verus"; + public static final String 베루스_프로필_URL = "github.com/verus"; + + public static final GithubProfileResponse 짱구_깃허브_프로필 = + new GithubProfileResponse(짱구_깃허브_ID, 짱구_이름, 짱구_이미지_URL, 짱구_프로필_URL); + + public static final GithubProfileResponse 그린론_깃허브_프로필 = + new GithubProfileResponse(그린론_깃허브_ID, 그린론_이름, 그린론_이미지_URL, 그린론_프로필_URL); + + public static final GithubProfileResponse 디우_깃허브_프로필 = + new GithubProfileResponse(디우_깃허브_ID, 디우_이름, 디우_이미지_URL, 디우_프로필_URL); + + public static final GithubProfileResponse 베루스_깃허브_프로필 = + new GithubProfileResponse(베루스_깃허브_ID, 베루스_이름, 베루스_이미지_URL, 베루스_프로필_URL); +} diff --git a/backend/src/test/java/com/woowacourse/acceptance/fixture/StudyFixtures.java b/backend/src/test/java/com/woowacourse/acceptance/fixture/StudyFixtures.java new file mode 100644 index 000000000..e6ae847a4 --- /dev/null +++ b/backend/src/test/java/com/woowacourse/acceptance/fixture/StudyFixtures.java @@ -0,0 +1,34 @@ +package com.woowacourse.acceptance.fixture; + +public class StudyFixtures { + + public static final String 자바_스터디_제목 = "Java 스터디"; + public static final String 자바_스터디_요약 = "자바 설명"; + public static final String 자바_스터디_썸네일 = "java thumbnail"; + public static final String 자바_스터디_설명 = "그린론의 우당탕탕 자바 스터디입니다."; + + public static final String 리액트_스터디_제목 = "React 스터디"; + public static final String 리액트_스터디_요약 = "리액트 설명"; + public static final String 리액트_스터디_썸네일 = "react thumbnail"; + public static final String 리액트_스터디_설명 = "디우의 뤼액트 스터디입니다."; + + public static final String 자바스크립트_스터디_제목 = "javaScript 스터디"; + public static final String 자바스크립트_스터디_요약 = "자바스크립트 설명"; + public static final String 자바스크립트_스터디_썸네일 = "javascript thumbnail"; + public static final String 자바스크립트_스터디_설명 = "그린론의 자바스크립트 접해보기"; + + public static final String HTTP_스터디_제목 = "HTTP 스터디"; + public static final String HTTP_스터디_요약 = "HTTP 설명"; + public static final String HTTP_스터디_썸네일 = "http thumbnail"; + public static final String HTTP_스터디_설명 = "디우의 HTTP 정복하기"; + + public static final String 알고리즘_스터디_제목 = "알고리즘 스터디"; + public static final String 알고리즘_스터디_요약 = "알고리즘 설명"; + public static final String 알고리즘_스터디_썸네일 = "algorithm thumbnail"; + public static final String 알고리즘_스터디_설명 = "알고리즘을 TDD로 풀자의 베루스입니다."; + + public static final String 리눅스_스터디_제목 = "Linux 스터디"; + public static final String 리눅스_스터디_요약 = "리눅스 설명"; + public static final String 리눅스_스터디_썸네일 = "linux thumbnail"; + public static final String 리눅스_스터디_설명 = "Linux를 공부하자의 베루스입니다."; +} diff --git a/backend/src/test/java/com/woowacourse/acceptance/fixture/TagFixtures.java b/backend/src/test/java/com/woowacourse/acceptance/fixture/TagFixtures.java new file mode 100644 index 000000000..63026e661 --- /dev/null +++ b/backend/src/test/java/com/woowacourse/acceptance/fixture/TagFixtures.java @@ -0,0 +1,31 @@ +package com.woowacourse.acceptance.fixture; + +import static com.woowacourse.fixtures.CategoryFixtures.AREA_응답; +import static com.woowacourse.fixtures.CategoryFixtures.GENERATION_응답; +import static com.woowacourse.fixtures.CategoryFixtures.SUBJECT_응답; + +import com.woowacourse.moamoa.tag.query.response.TagData; +import com.woowacourse.moamoa.tag.query.response.TagSummaryData; + +public class TagFixtures { + + public static final Long 자바_태그_ID = 1L; + public static final String 자바_태그명 = "Java"; + public static final String 자바_태그_설명 = "자바"; + + public static final Long 우테코4기_태그_ID = 2L; + public static final String 우테코4기_태그명 = "4기"; + public static final String 우테코4기_태그_설명 = "우테코4기"; + + public static final Long BE_태그_ID = 3L; + public static final String BE_태그명 = "BE"; + public static final String BE_태그_설명 = "백엔드"; + + public static final Long FE_태그_ID = 4L; + public static final String FE_태그명 = "FE"; + public static final String FE_태그_설명 = "프론트엔드"; + + public static final Long 리액트_태그_ID = 5L; + public static final String 리액트_태그명 = "React"; + public static final String 리액트_태그_설명 = "리액트"; +} diff --git a/backend/src/test/java/com/woowacourse/acceptance/review/ReviewsAcceptanceTest.java b/backend/src/test/java/com/woowacourse/acceptance/review/ReviewsAcceptanceTest.java deleted file mode 100644 index 8d49310d3..000000000 --- a/backend/src/test/java/com/woowacourse/acceptance/review/ReviewsAcceptanceTest.java +++ /dev/null @@ -1,177 +0,0 @@ -package com.woowacourse.acceptance.review; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.springframework.restdocs.restassured3.RestAssuredRestDocumentation.document; - -import com.woowacourse.acceptance.AcceptanceTest; -import com.woowacourse.moamoa.auth.service.oauthclient.response.GithubProfileResponse; -import com.woowacourse.moamoa.member.query.data.MemberData; -import com.woowacourse.moamoa.review.service.request.EditingReviewRequest; -import com.woowacourse.moamoa.review.service.request.WriteReviewRequest; -import com.woowacourse.moamoa.review.service.response.ReviewResponse; -import com.woowacourse.moamoa.review.service.response.ReviewsResponse; -import com.woowacourse.moamoa.review.service.response.WriterResponse; -import com.woowacourse.moamoa.study.service.request.CreatingStudyRequest; -import io.restassured.RestAssured; -import java.time.LocalDate; -import java.util.List; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; - -@DisplayName("리뷰 인수 테스트") -public class ReviewsAcceptanceTest extends AcceptanceTest { - - private static final MemberData JJANGGU = new MemberData(1L, "jjanggu", "https://image", "github.com"); - private static final MemberData GREENLAWN = new MemberData(2L, "greenlawn", "https://image", "github.com"); - private static final MemberData DWOO = new MemberData(3L, "dwoo", "https://image", "github.com"); - private static final MemberData VERUS = new MemberData(4L, "verus", "https://image", "github.com"); - - private Long javaStudyId; - private Long javaReviewId1; - private Long javaReviewId2; - - private List javaReviews; - - @BeforeEach - void initDataBase() { - final String jjangguToken = getBearerTokenBySignInOrUp(toGithubProfileResponse(JJANGGU)); - final String greenlawnToken = getBearerTokenBySignInOrUp(toGithubProfileResponse(GREENLAWN)); - final String dwooToken = getBearerTokenBySignInOrUp(toGithubProfileResponse(DWOO)); - final String verusToken = getBearerTokenBySignInOrUp(toGithubProfileResponse(VERUS)); - - final LocalDate startDate = LocalDate.now(); - CreatingStudyRequest javaStudyRequest = CreatingStudyRequest.builder() - .title("java 스터디").excerpt("자바 설명").thumbnail("java image").description("자바 소개") - .startDate(startDate) - .build(); - CreatingStudyRequest reactStudyRequest = CreatingStudyRequest.builder() - .title("react 스터디").excerpt("리액트 설명").thumbnail("react image").description("리액트 소개") - .startDate(startDate) - .build(); - - javaStudyId = createStudy(jjangguToken, javaStudyRequest); - long reactStudyId = createStudy(jjangguToken, reactStudyRequest); - - participateStudy(greenlawnToken, javaStudyId); - participateStudy(dwooToken, javaStudyId); - participateStudy(verusToken, javaStudyId); - - final LocalDate createdAt = LocalDate.now(); - final LocalDate lastModifiedDate = LocalDate.now(); - - // 리뷰 추가 - javaReviewId1 = createReview(jjangguToken, javaStudyId, new WriteReviewRequest("리뷰 내용1")); - javaReviewId2 = createReview(greenlawnToken, javaStudyId, new WriteReviewRequest("리뷰 내용2")); - long javaReviewId3 = createReview(dwooToken, javaStudyId, new WriteReviewRequest("리뷰 내용3")); - long javaReviewId4 = createReview(verusToken, javaStudyId, new WriteReviewRequest("리뷰 내용4")); - createReview(jjangguToken, reactStudyId, new WriteReviewRequest("리뷰 내용5")); - - final ReviewResponse 리뷰_내용1 = new ReviewResponse(javaReviewId1, new WriterResponse(JJANGGU), createdAt, - lastModifiedDate, "리뷰 내용1"); - final ReviewResponse 리뷰_내용2 = new ReviewResponse(javaReviewId2, new WriterResponse(GREENLAWN), createdAt, - lastModifiedDate, "리뷰 내용2"); - final ReviewResponse 리뷰_내용3 = new ReviewResponse(javaReviewId3, new WriterResponse(DWOO), createdAt, - lastModifiedDate, "리뷰 내용3"); - final ReviewResponse 리뷰_내용4 = new ReviewResponse(javaReviewId4, new WriterResponse(VERUS), createdAt, - lastModifiedDate, "리뷰 내용4"); - javaReviews = List.of( - 리뷰_내용4, - 리뷰_내용3, - 리뷰_내용2, - 리뷰_내용1 - ); - } - - private static GithubProfileResponse toGithubProfileResponse(MemberData memberData) { - return new GithubProfileResponse(memberData.getGithubId(), memberData.getUsername(), memberData.getImageUrl(), - memberData.getProfileUrl()); - } - - @DisplayName("리뷰를 작성한다.") - @Test - void create() { - final String token = getBearerTokenBySignInOrUp(toGithubProfileResponse(JJANGGU)); - final WriteReviewRequest writeReviewRequest = new WriteReviewRequest("짱구의 스터디 리뷰입니다."); - - RestAssured.given(spec).log().all() - .header(HttpHeaders.AUTHORIZATION, token) - .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) - .header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE) - .body(writeReviewRequest) - .filter(document("reviews/create")) - .when().log().all() - .post("/api/studies/1/reviews") - .then().log().all() - .statusCode(HttpStatus.CREATED.value()); - } - - @DisplayName("스터디에 달린 전체 리뷰 목록을 조회할 수 있다.") - @Test - void getAllReviews() { - final ReviewsResponse reviewsResponse = RestAssured.given(spec).log().all() - .filter(document("reviews/list")) - .when().log().all() - .get("api/studies/1/reviews") - .then().log().all() - .statusCode(HttpStatus.OK.value()) - .extract().as(ReviewsResponse.class); - - assertThat(reviewsResponse.getTotalCount()).isEqualTo(4); - assertThat(reviewsResponse.getReviews()) - .containsExactlyInAnyOrderElementsOf(javaReviews); - } - - @DisplayName("원하는 갯수만큼 스터디에 달린 리뷰 목록을 조회할 수 있다.") - @Test - public void getReviewsBySize() { - final ReviewsResponse reviewsResponse = RestAssured.given(spec).log().all() - .filter(document("reviews/list-certain-number")) - .when().log().all() - .get("/api/studies/1/reviews?size=2") - .then() - .statusCode(HttpStatus.OK.value()) - .extract().as(ReviewsResponse.class); - - assertThat(reviewsResponse.getTotalCount()).isEqualTo(4); - assertThat(reviewsResponse.getReviews()).containsExactlyInAnyOrderElementsOf(javaReviews.subList(0, 2)); - } - - @DisplayName("자신이 참여한 스터디에 작성한 리뷰를 삭제할 수 있다.") - @Test - void deleteReview() { - final String token = getBearerTokenBySignInOrUp(toGithubProfileResponse(JJANGGU)); - - RestAssured.given(spec).log().all() - .filter(document("reviews/delete")) - .header(HttpHeaders.AUTHORIZATION, token) - .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) - .pathParam("study-id", javaStudyId) - .pathParam("review-id", javaReviewId1) - .when().log().all() - .delete("/api/studies/{study-id}/reviews/{review-id}") - .then().statusCode(HttpStatus.NO_CONTENT.value()); - } - - @DisplayName("자신이 참여한 스터디에 작성한 리뷰를 수정할 수 있다.") - @Test - void updateReview() { - final String token = getBearerTokenBySignInOrUp(toGithubProfileResponse(JJANGGU)); - final EditingReviewRequest request = new EditingReviewRequest("edit review"); - - RestAssured.given(spec).log().all() - .filter(document("reviews/update")) - .header(HttpHeaders.AUTHORIZATION, token) - .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) - .header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE) - .pathParam("study-id", javaStudyId) - .pathParam("review-id", javaReviewId1) - .body(request) - .when().log().all() - .put("/api/studies/{study-id}/reviews/{review-id}") - .then().statusCode(HttpStatus.NO_CONTENT.value()); - } -} diff --git a/backend/src/test/java/com/woowacourse/acceptance/steps/AfterLoginSteps.java b/backend/src/test/java/com/woowacourse/acceptance/steps/AfterLoginSteps.java new file mode 100644 index 000000000..2180a3b2b --- /dev/null +++ b/backend/src/test/java/com/woowacourse/acceptance/steps/AfterLoginSteps.java @@ -0,0 +1,87 @@ +package com.woowacourse.acceptance.steps; + +import static com.woowacourse.acceptance.fixture.StudyFixtures.HTTP_스터디_설명; +import static com.woowacourse.acceptance.fixture.StudyFixtures.HTTP_스터디_썸네일; +import static com.woowacourse.acceptance.fixture.StudyFixtures.HTTP_스터디_요약; +import static com.woowacourse.acceptance.fixture.StudyFixtures.HTTP_스터디_제목; +import static com.woowacourse.acceptance.fixture.StudyFixtures.리눅스_스터디_설명; +import static com.woowacourse.acceptance.fixture.StudyFixtures.리눅스_스터디_썸네일; +import static com.woowacourse.acceptance.fixture.StudyFixtures.리눅스_스터디_요약; +import static com.woowacourse.acceptance.fixture.StudyFixtures.리눅스_스터디_제목; +import static com.woowacourse.acceptance.fixture.StudyFixtures.리액트_스터디_설명; +import static com.woowacourse.acceptance.fixture.StudyFixtures.리액트_스터디_썸네일; +import static com.woowacourse.acceptance.fixture.StudyFixtures.리액트_스터디_요약; +import static com.woowacourse.acceptance.fixture.StudyFixtures.리액트_스터디_제목; +import static com.woowacourse.acceptance.fixture.StudyFixtures.알고리즘_스터디_설명; +import static com.woowacourse.acceptance.fixture.StudyFixtures.알고리즘_스터디_썸네일; +import static com.woowacourse.acceptance.fixture.StudyFixtures.알고리즘_스터디_요약; +import static com.woowacourse.acceptance.fixture.StudyFixtures.알고리즘_스터디_제목; +import static com.woowacourse.acceptance.fixture.StudyFixtures.자바_스터디_설명; +import static com.woowacourse.acceptance.fixture.StudyFixtures.자바_스터디_썸네일; +import static com.woowacourse.acceptance.fixture.StudyFixtures.자바_스터디_요약; +import static com.woowacourse.acceptance.fixture.StudyFixtures.자바_스터디_제목; +import static com.woowacourse.acceptance.fixture.StudyFixtures.자바스크립트_스터디_설명; +import static com.woowacourse.acceptance.fixture.StudyFixtures.자바스크립트_스터디_썸네일; +import static com.woowacourse.acceptance.fixture.StudyFixtures.자바스크립트_스터디_요약; +import static com.woowacourse.acceptance.fixture.StudyFixtures.자바스크립트_스터디_제목; + +import com.woowacourse.moamoa.study.service.request.CreatingStudyRequestBuilder; + +public class AfterLoginSteps extends Steps { + + private final String token; + + AfterLoginSteps(final String token) { + this.token = token; + } + + public SetRequiredDataToCreatingStudySteps 자바_스터디를() { + CreatingStudyRequestBuilder builder = new CreatingStudyRequestBuilder() + .title(자바_스터디_제목).excerpt(자바_스터디_요약).description(자바_스터디_설명).thumbnail(자바_스터디_썸네일); + + return new SetRequiredDataToCreatingStudySteps(token, builder); + } + + public SetRequiredDataToCreatingStudySteps 리액트_스터디를() { + CreatingStudyRequestBuilder builder = new CreatingStudyRequestBuilder() + .title(리액트_스터디_제목).excerpt(리액트_스터디_요약).description(리액트_스터디_설명).thumbnail(리액트_스터디_썸네일); + + return new SetRequiredDataToCreatingStudySteps(token, builder); + } + + public SetRequiredDataToCreatingStudySteps 자바스크립트_스터디를() { + CreatingStudyRequestBuilder builder = new CreatingStudyRequestBuilder() + .title(자바스크립트_스터디_제목).excerpt(자바스크립트_스터디_요약) + .description(자바스크립트_스터디_설명).thumbnail(자바스크립트_스터디_썸네일); + + return new SetRequiredDataToCreatingStudySteps(token, builder); + } + + public SetRequiredDataToCreatingStudySteps HTTP_스터디를() { + CreatingStudyRequestBuilder builder = new CreatingStudyRequestBuilder() + .title(HTTP_스터디_제목).excerpt(HTTP_스터디_요약) + .description(HTTP_스터디_설명).thumbnail(HTTP_스터디_썸네일); + + return new SetRequiredDataToCreatingStudySteps(token, builder); + } + + public SetRequiredDataToCreatingStudySteps 알고리즘_스터디를() { + CreatingStudyRequestBuilder builder = new CreatingStudyRequestBuilder() + .title(알고리즘_스터디_제목).excerpt(알고리즘_스터디_요약) + .description(알고리즘_스터디_설명).thumbnail(알고리즘_스터디_썸네일); + + return new SetRequiredDataToCreatingStudySteps(token, builder); + } + + public SetRequiredDataToCreatingStudySteps 리눅스_스터디를() { + CreatingStudyRequestBuilder builder = new CreatingStudyRequestBuilder() + .title(리눅스_스터디_제목).excerpt(리눅스_스터디_요약) + .description(리눅스_스터디_설명).thumbnail(리눅스_스터디_썸네일); + + return new SetRequiredDataToCreatingStudySteps(token, builder); + } + + public StudyRelatedSteps 스터디에(final Long studyId) { + return new StudyRelatedSteps(studyId, token); + } +} diff --git a/backend/src/test/java/com/woowacourse/acceptance/steps/CreatingStudyStep.java b/backend/src/test/java/com/woowacourse/acceptance/steps/CreatingStudyStep.java new file mode 100644 index 000000000..315c0c149 --- /dev/null +++ b/backend/src/test/java/com/woowacourse/acceptance/steps/CreatingStudyStep.java @@ -0,0 +1,59 @@ +package com.woowacourse.acceptance.steps; + +import com.woowacourse.moamoa.study.service.request.CreatingStudyRequestBuilder; +import io.restassured.RestAssured; +import java.time.LocalDate; +import java.util.List; +import org.junit.jupiter.api.Assertions; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; + +public class CreatingStudyStep extends Steps { + + private final String token; + private final CreatingStudyRequestBuilder builder; + + CreatingStudyStep(final String token, final CreatingStudyRequestBuilder builder) { + this.token = token; + this.builder = builder; + } + + public CreatingStudyStep 모집종료일자는(LocalDate date) { + builder.enrollmentEndDate(date); + return this; + } + + public CreatingStudyStep 종료일자는(LocalDate date) { + builder.endDate(date); + return this; + } + + public CreatingStudyStep 태그는(Long... tagIds) { + builder.tagIds(List.of(tagIds)); + return this; + } + + public CreatingStudyStep 모집인원은(int number) { + builder.maxMemberCount(number); + return this; + } + + public long 생성한다() { + try { + final String location = RestAssured.given().log().all() + .header(HttpHeaders.AUTHORIZATION, token) + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) + .body(objectMapper.writeValueAsString(builder.build())) + .when().log().all() + .post("/api/studies") + .then().log().all() + .statusCode(HttpStatus.CREATED.value()) + .extract().header(HttpHeaders.LOCATION); + return Long.parseLong(location.replaceAll("/api/studies/", "")); + } catch (Exception e) { + Assertions.fail("스터디 생성 실패"); + return -1; + } + } +} diff --git a/backend/src/test/java/com/woowacourse/acceptance/steps/LoginSteps.java b/backend/src/test/java/com/woowacourse/acceptance/steps/LoginSteps.java new file mode 100644 index 000000000..eedebb1d4 --- /dev/null +++ b/backend/src/test/java/com/woowacourse/acceptance/steps/LoginSteps.java @@ -0,0 +1,66 @@ +package com.woowacourse.acceptance.steps; + +import static com.woowacourse.acceptance.fixture.MemberFixtures.그린론_깃허브_프로필; +import static com.woowacourse.acceptance.fixture.MemberFixtures.디우_깃허브_프로필; +import static com.woowacourse.acceptance.fixture.MemberFixtures.베루스_깃허브_프로필; +import static com.woowacourse.acceptance.fixture.MemberFixtures.짱구_깃허브_프로필; + +import com.woowacourse.moamoa.auth.service.oauthclient.response.GithubProfileResponse; +import io.restassured.RestAssured; +import org.springframework.http.HttpStatus; + +public class LoginSteps extends Steps { + + private final GithubProfileResponse githubProfile; + + LoginSteps(final GithubProfileResponse githubProfile) { + this.githubProfile = githubProfile; + } + + public static LoginSteps 짱구가() { + return new LoginSteps(짱구_깃허브_프로필); + } + + public static LoginSteps 그린론이() { + return new LoginSteps(그린론_깃허브_프로필); + } + + public static LoginSteps 디우가() { + return new LoginSteps(디우_깃허브_프로필); + } + + public static LoginSteps 베루스가() { + return new LoginSteps(베루스_깃허브_프로필); + } + + public AfterLoginSteps 로그인하고() { + return new AfterLoginSteps(getIssuedBearerToken()); + } + + public String 로그인한다() { + return getIssuedBearerToken(); + } + + private String getIssuedBearerToken() { + if (tokenCache.containsKey(githubProfile.getGitgubId())) { + return tokenCache.get(githubProfile.getGitgubId()); + } + final String bearerToken = requestBearerToken(); + tokenCache.put(githubProfile.getGitgubId(), bearerToken); + return bearerToken; + } + + private String requestBearerToken() { + final String authorizationCode = "Authorization Code"; + mockingGithubServer(authorizationCode, githubProfile); + final String token = RestAssured.given().log().all() + .param("code", authorizationCode) + .when() + .post("/api/login/token") + .then().log().all() + .statusCode(HttpStatus.OK.value()) + .extract().jsonPath().getString("token"); + mockServer.reset(); + return "Bearer " + token; + } +} diff --git a/backend/src/test/java/com/woowacourse/acceptance/steps/SetRequiredDataToCreatingStudySteps.java b/backend/src/test/java/com/woowacourse/acceptance/steps/SetRequiredDataToCreatingStudySteps.java new file mode 100644 index 000000000..7f9d35370 --- /dev/null +++ b/backend/src/test/java/com/woowacourse/acceptance/steps/SetRequiredDataToCreatingStudySteps.java @@ -0,0 +1,20 @@ +package com.woowacourse.acceptance.steps; + +import com.woowacourse.moamoa.study.service.request.CreatingStudyRequestBuilder; +import java.time.LocalDate; + +public class SetRequiredDataToCreatingStudySteps extends Steps { + + private final String token; + private final CreatingStudyRequestBuilder builder; + + SetRequiredDataToCreatingStudySteps(final String token, final CreatingStudyRequestBuilder builder) { + this.token = token; + this.builder = builder; + } + + public CreatingStudyStep 시작일자는(LocalDate date) { + builder.startDate(date); + return new CreatingStudyStep(token, builder); + } +} diff --git a/backend/src/test/java/com/woowacourse/acceptance/steps/Steps.java b/backend/src/test/java/com/woowacourse/acceptance/steps/Steps.java new file mode 100644 index 000000000..c28b01795 --- /dev/null +++ b/backend/src/test/java/com/woowacourse/acceptance/steps/Steps.java @@ -0,0 +1,68 @@ +package com.woowacourse.acceptance.steps; + +import static org.springframework.test.web.client.match.MockRestRequestMatchers.content; +import static org.springframework.test.web.client.match.MockRestRequestMatchers.header; +import static org.springframework.test.web.client.match.MockRestRequestMatchers.method; +import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo; +import static org.springframework.test.web.client.response.MockRestResponseCreators.withStatus; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.woowacourse.moamoa.auth.service.oauthclient.response.GithubProfileResponse; +import com.woowacourse.moamoa.auth.service.request.AccessTokenRequest; +import java.util.HashMap; +import java.util.Map; +import org.junit.jupiter.api.Assertions; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.test.web.client.MockRestServiceServer; + +public abstract class Steps { + + protected final static Map tokenCache = new HashMap<>(); + + public static String clientId; + public static MockRestServiceServer mockServer; + public static String clientSecret; + public static ObjectMapper objectMapper; + + protected static void mockingGithubServer(String authorizationCode, GithubProfileResponse response) { + try { + mockingGithubServerForGetAccessToken(authorizationCode, Map.of("access_token", "access-token", + "token_type", "bearer", + "scope", "")); + mockingGithubServerForGetProfile("access-token", HttpStatus.OK, response); + } catch (Exception e) { + Assertions.fail(e.getMessage()); + } + } + + private static void mockingGithubServerForGetAccessToken(final String authorizationCode, + final Map accessTokenResponse) + throws JsonProcessingException { + mockServer.expect(requestTo("https://github.com/login/oauth/access_token")) + .andExpect(method(HttpMethod.POST)) + .andExpect(content().json(objectMapper.writeValueAsString( + new AccessTokenRequest(clientId, clientSecret, authorizationCode)))) + .andRespond(withStatus(HttpStatus.OK) + .contentType(MediaType.APPLICATION_JSON) + .body(objectMapper.writeValueAsString(accessTokenResponse))); + } + + private static void mockingGithubServerForGetProfile(final String accessToken, final HttpStatus status, + final GithubProfileResponse response) + throws JsonProcessingException { + + mockServer.expect(requestTo("https://api.github.com/user")) + .andExpect(method(HttpMethod.GET)) + .andExpect(header("Authorization", "token " + accessToken)) + .andRespond(withStatus(status) + .contentType(MediaType.APPLICATION_JSON) + .body(objectMapper.writeValueAsString(response))); + } + + public static void clearTokenCaches() { + tokenCache.clear(); + } +} diff --git a/backend/src/test/java/com/woowacourse/acceptance/steps/StudyRelatedSteps.java b/backend/src/test/java/com/woowacourse/acceptance/steps/StudyRelatedSteps.java new file mode 100644 index 000000000..f9759364d --- /dev/null +++ b/backend/src/test/java/com/woowacourse/acceptance/steps/StudyRelatedSteps.java @@ -0,0 +1,51 @@ +package com.woowacourse.acceptance.steps; + +import static org.springframework.http.HttpHeaders.AUTHORIZATION; +import static org.springframework.http.HttpHeaders.CONTENT_TYPE; +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; + +import com.woowacourse.moamoa.review.service.request.WriteReviewRequest; +import io.restassured.RestAssured; +import org.junit.jupiter.api.Assertions; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; + +public class StudyRelatedSteps extends Steps { + + private final Long studyId; + private final String token; + + StudyRelatedSteps(final Long studyId, final String token) { + this.studyId = studyId; + this.token = token; + } + + public void 참여한다() { + RestAssured.given().log().all() + .header(CONTENT_TYPE, APPLICATION_JSON_VALUE) + .header(AUTHORIZATION, token) + .when().log().all() + .post("/api/studies/" + studyId) + .then().log().all() + .statusCode(HttpStatus.OK.value()); + } + + public long 리뷰를_작성한다(String content) { + try { + final String location = RestAssured.given().log().all() + .header(HttpHeaders.AUTHORIZATION, token) + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) + .pathParams("study-id", studyId) + .body(objectMapper.writeValueAsString(new WriteReviewRequest(content))) + .when().post("/api/studies/{study-id}/reviews") + .then().log().all() + .statusCode(HttpStatus.CREATED.value()) + .extract().header(HttpHeaders.LOCATION); + return Long.parseLong(location.replaceAll("/api/studies/" + studyId + "/reviews/", "")); + } catch (Exception e) { + Assertions.fail("리뷰 작성 실패"); + return -1; + } + } +} diff --git a/backend/src/test/java/com/woowacourse/acceptance/study/GettingMyStudiesAcceptanceTest.java b/backend/src/test/java/com/woowacourse/acceptance/study/GettingMyStudiesAcceptanceTest.java deleted file mode 100644 index bb7c48969..000000000 --- a/backend/src/test/java/com/woowacourse/acceptance/study/GettingMyStudiesAcceptanceTest.java +++ /dev/null @@ -1,139 +0,0 @@ -package com.woowacourse.acceptance.study; - -import static org.hamcrest.Matchers.contains; -import static org.hamcrest.Matchers.is; -import static org.springframework.http.HttpHeaders.AUTHORIZATION; -import static org.springframework.http.HttpHeaders.CONTENT_TYPE; -import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; -import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; -import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; -import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; -import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; -import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; -import static org.springframework.restdocs.request.RequestDocumentation.requestParameters; -import static org.springframework.restdocs.restassured3.RestAssuredRestDocumentation.document; - -import com.woowacourse.acceptance.AcceptanceTest; -import com.woowacourse.moamoa.auth.service.oauthclient.response.GithubProfileResponse; -import io.restassured.RestAssured; -import java.time.LocalDateTime; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpStatus; -import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.restdocs.payload.JsonFieldType; - -public class GettingMyStudiesAcceptanceTest extends AcceptanceTest { - - @Autowired - private JdbcTemplate jdbcTemplate; - - @BeforeEach - void initDatabase() { - jdbcTemplate.update("INSERT INTO member(id, github_id, username, image_url, profile_url) VALUES (1, 1, 'jjanggu', 'https://image', 'github.com')"); - jdbcTemplate.update("INSERT INTO member(id, github_id, username, image_url, profile_url) VALUES (2, 2, 'greenlawn', 'https://image', 'github.com')"); - jdbcTemplate.update("INSERT INTO member(id, github_id, username, image_url, profile_url) VALUES (3, 3, 'dwoo', 'https://image', 'github.com')"); - jdbcTemplate.update("INSERT INTO member(id, github_id, username, image_url, profile_url) VALUES (4, 4, 'verus', 'https://image', 'github.com')"); - - final LocalDateTime now = LocalDateTime.now(); - - jdbcTemplate.update("INSERT INTO study(id, title, excerpt, thumbnail, recruitment_status, study_status, description, current_member_count, max_member_count, created_at, start_date, owner_id) " - + "VALUES (1, 'Java 스터디', '자바 설명', 'java thumbnail', 'RECRUITMENT_START', 'PREPARE', '그린론의 우당탕탕 자바 스터디입니다.', 3, 10, '" + now + "', '2021-12-08', 2)"); - jdbcTemplate.update("INSERT INTO study(id, title, excerpt, thumbnail, recruitment_status, study_status, description, current_member_count, max_member_count, created_at, enrollment_end_date, start_date, end_date, owner_id) " - + "VALUES (2, 'React 스터디', '리액트 설명', 'react thumbnail', 'RECRUITMENT_START', 'PREPARE', '디우의 뤼액트 스터디입니다.', 4, 5, '" + now + "', '2021-11-09', '2021-11-10', '2021-12-08', 3)"); - jdbcTemplate.update("INSERT INTO study(id, title, excerpt, thumbnail, recruitment_status, study_status, description, current_member_count, max_member_count, created_at, start_date, owner_id) " - + "VALUES (3, 'javaScript 스터디', '자바스크립트 설명', 'javascript thumbnail', 'RECRUITMENT_START', 'PREPARE', '그린론의 자바스크립트 접해보기', 3, 20, '" + now + "', '2022-08-03', 2)"); - jdbcTemplate.update("INSERT INTO study(id, title, excerpt, thumbnail, recruitment_status, study_status, description, max_member_count, created_at, start_date, owner_id) " - + "VALUES (4, 'HTTP 스터디', 'HTTP 설명', 'http thumbnail', 'RECRUITMENT_END', 'PREPARE', '디우의 HTTP 정복하기', 5, '" + now + "', '2022-08-03', 3)"); - jdbcTemplate.update("INSERT INTO study(id, title, excerpt, thumbnail, recruitment_status, study_status, description, current_member_count, created_at, owner_id, start_date) " - + "VALUES (5, '알고리즘 스터디', '알고리즘 설명', 'algorithm thumbnail', 'RECRUITMENT_END', 'PREPARE', '알고리즘을 TDD로 풀자의 베루스입니다.', 1, '" + now + "', 4, '2021-12-06')"); - jdbcTemplate.update("INSERT INTO study(id, title, excerpt, thumbnail, recruitment_status, study_status, description, current_member_count, created_at, owner_id, start_date, enrollment_end_date, end_date) " - + "VALUES (6, 'Linux 스터디', '리눅스 설명', 'linux thumbnail', 'RECRUITMENT_END', 'PREPARE', 'Linux를 공부하자의 베루스입니다.', 1, '" + now + "', 4, '2021-12-06', '2021-12-07', '2022-01-07')"); - jdbcTemplate.update("INSERT INTO study(id, title, excerpt, thumbnail, recruitment_status, study_status, description, current_member_count, max_member_count, created_at, owner_id, start_date, enrollment_end_date, end_date) " - + "VALUES (7, 'OS 스터디', 'OS 설명', 'os thumbnail', 'RECRUITMENT_END', 'PREPARE', 'OS를 공부하자의 베루스입니다.', 1, 6, '" + now + "', 4, '2021-12-06', '2021-12-07', '2022-01-07')"); - - jdbcTemplate.update("INSERT INTO category(id, name) VALUES (1, 'generation')"); - jdbcTemplate.update("INSERT INTO category(id, name) VALUES (2, 'area')"); - jdbcTemplate.update("INSERT INTO category(id, name) VALUES (3, 'subject')"); - - jdbcTemplate.update("INSERT INTO tag(id, name, description, category_id) VALUES (1, 'Java', '자바', 3)"); - jdbcTemplate.update("INSERT INTO tag(id, name, description, category_id) VALUES (2, '4기', '우테코4기', 1)"); - jdbcTemplate.update("INSERT INTO tag(id, name, description, category_id) VALUES (3, 'BE', '백엔드', 2)"); - jdbcTemplate.update("INSERT INTO tag(id, name, description, category_id) VALUES (4, 'FE', '프론트엔드', 2)"); - jdbcTemplate.update("INSERT INTO tag(id, name, description, category_id) VALUES (5, 'React', '리액트', 3)"); - - jdbcTemplate.update("INSERT INTO study_tag(study_id, tag_id) VALUES (1, 1)"); - jdbcTemplate.update("INSERT INTO study_tag(study_id, tag_id) VALUES (1, 2)"); - jdbcTemplate.update("INSERT INTO study_tag(study_id, tag_id) VALUES (1, 3)"); - - jdbcTemplate.update("INSERT INTO study_tag(study_id, tag_id) VALUES (2, 2)"); - jdbcTemplate.update("INSERT INTO study_tag(study_id, tag_id) VALUES (2, 4)"); - jdbcTemplate.update("INSERT INTO study_tag(study_id, tag_id) VALUES (2, 5)"); - - jdbcTemplate.update("INSERT INTO study_tag(study_id, tag_id) VALUES (3, 2)"); - jdbcTemplate.update("INSERT INTO study_tag(study_id, tag_id) VALUES (3, 4)"); - - jdbcTemplate.update("INSERT INTO study_tag(study_id, tag_id) VALUES (4, 2)"); - jdbcTemplate.update("INSERT INTO study_tag(study_id, tag_id) VALUES (4, 3)"); - - jdbcTemplate.update("INSERT INTO study_member(study_id, member_id) VALUES (1, 3)"); - jdbcTemplate.update("INSERT INTO study_member(study_id, member_id) VALUES (1, 4)"); - - jdbcTemplate.update("INSERT INTO study_member(study_id, member_id) VALUES (2, 1)"); - jdbcTemplate.update("INSERT INTO study_member(study_id, member_id) VALUES (2, 2)"); - jdbcTemplate.update("INSERT INTO study_member(study_id, member_id) VALUES (2, 4)"); - - jdbcTemplate.update("INSERT INTO study_member(study_id, member_id) VALUES (3, 3)"); - jdbcTemplate.update("INSERT INTO study_member(study_id, member_id) VALUES (3, 4)"); - } - - @DisplayName("내가 참여한 스터디를 조회한다.") - @Test - void getMyStudies() { - final String token = getBearerTokenBySignInOrUp(new GithubProfileResponse(1L, "jjanggu", "https://image", "github.com")); - - RestAssured.given(spec).log().all() - .filter(document("studies/myStudy")) - .header(HttpHeaders.AUTHORIZATION, token) - .when().log().all() - .get("/api/my/studies") - .then().log().all() - .statusCode(HttpStatus.OK.value()) - .body("studies[0].id", is(2)) - .body("studies[0].title", is("React 스터디")) - .body("studies[0].studyStatus", is("PREPARE")) - .body("studies[0].currentMemberCount", is(4)) - .body("studies[0].maxMemberCount", is(5)) - .body("studies[0].startDate", is("2021-11-10")) - .body("studies[0].endDate", is("2021-12-08")) - .body("studies[0].owner.username", is("dwoo")) - .body("studies[0].owner.imageUrl", is("https://image")) - .body("studies[0].owner.profileUrl", is("github.com")) - .body("studies[0].tags.id", contains(2, 4, 5)) - .body("studies[0].tags.name", contains("4기", "FE", "React")); - } - - @Test - @DisplayName("특정 스터디에서 나의 Role을 확인한다.") - void isMyStudy() { - final String token = getBearerTokenBySignInOrUp(new GithubProfileResponse(4L, "verus", "https://image", "github.com")); - - RestAssured.given(spec).log().all() - .filter(document("members/me/role", - requestHeaders(headerWithName("Authorization").description("Bearer Token")), - requestParameters(parameterWithName("study-id").description("스터디 ID")), - responseFields(fieldWithPath("role").type(JsonFieldType.STRING).description("해당 스터디에서 사용자의 역할")))) - .filter(document("members/me/role")) - .header(CONTENT_TYPE, APPLICATION_JSON_VALUE) - .header(AUTHORIZATION, token) - .queryParam("study-id", 7) - .when() - .get("/api/members/me/role") - .then().log().all() - .statusCode(HttpStatus.OK.value()) - .body("role", is("OWNER")); - } -} diff --git a/backend/src/test/java/com/woowacourse/acceptance/study/GettingStudyDetailsAcceptanceTest.java b/backend/src/test/java/com/woowacourse/acceptance/study/GettingStudyDetailsAcceptanceTest.java deleted file mode 100644 index 62f42dc91..000000000 --- a/backend/src/test/java/com/woowacourse/acceptance/study/GettingStudyDetailsAcceptanceTest.java +++ /dev/null @@ -1,141 +0,0 @@ -package com.woowacourse.acceptance.study; - -import static org.hamcrest.Matchers.contains; -import static org.hamcrest.Matchers.empty; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.not; -import static org.hamcrest.Matchers.nullValue; -import static org.springframework.restdocs.restassured3.RestAssuredRestDocumentation.document; - -import com.woowacourse.acceptance.AcceptanceTest; -import com.woowacourse.moamoa.auth.service.oauthclient.response.GithubProfileResponse; -import io.restassured.RestAssured; -import java.time.LocalDateTime; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpStatus; -import org.springframework.jdbc.core.JdbcTemplate; - -public class GettingStudyDetailsAcceptanceTest extends AcceptanceTest { - - @Autowired - private JdbcTemplate jdbcTemplate; - - @BeforeEach - void initDataBase() { - getBearerTokenBySignInOrUp(new GithubProfileResponse(1L, "jjanggu", "https://image", "github.com")); - getBearerTokenBySignInOrUp(new GithubProfileResponse(2L, "greenlawn", "https://image", "github.com")); - getBearerTokenBySignInOrUp(new GithubProfileResponse(3L, "dwoo", "https://image", "github.com")); - getBearerTokenBySignInOrUp(new GithubProfileResponse(4L, "verus", "https://image", "github.com")); - - final LocalDateTime now = LocalDateTime.now(); - - jdbcTemplate.update("INSERT INTO study(id, title, excerpt, thumbnail, recruitment_status, study_status, description, current_member_count, max_member_count, created_at, start_date, owner_id) " - + "VALUES (1, 'Java 스터디', '자바 설명', 'java thumbnail', 'RECRUITMENT_START', 'PREPARE', '그린론의 우당탕탕 자바 스터디입니다.', 3, 10, '" + now + "', '2021-12-08', 2)"); - jdbcTemplate.update("INSERT INTO study(id, title, excerpt, thumbnail, recruitment_status, study_status, description, current_member_count, max_member_count, created_at, enrollment_end_date, start_date, end_date, owner_id) " - + "VALUES (2, 'React 스터디', '리액트 설명', 'react thumbnail', 'RECRUITMENT_START', 'PREPARE', '디우의 뤼액트 스터디입니다.', 4, 5, '" + now + "', '2021-11-09', '2021-11-10', '2021-12-08', 3)"); - jdbcTemplate.update("INSERT INTO study(id, title, excerpt, thumbnail, recruitment_status, study_status, description, current_member_count, max_member_count, created_at, start_date, owner_id) " - + "VALUES (3, 'javaScript 스터디', '자바스크립트 설명', 'javascript thumbnail', 'RECRUITMENT_START', 'PREPARE', '그린론의 자바스크립트 접해보기', 3, 20, '" + now + "', '2022-08-03', 2)"); - jdbcTemplate.update("INSERT INTO study(id, title, excerpt, thumbnail, recruitment_status, study_status, description, max_member_count, created_at, start_date, owner_id) " - + "VALUES (4, 'HTTP 스터디', 'HTTP 설명', 'http thumbnail', 'RECRUITMENT_END', 'PREPARE', '디우의 HTTP 정복하기', 5, '" + now + "', '2022-08-03', 3)"); - jdbcTemplate.update("INSERT INTO study(id, title, excerpt, thumbnail, recruitment_status, study_status, description, current_member_count, created_at, owner_id, start_date) " - + "VALUES (5, '알고리즘 스터디', '알고리즘 설명', 'algorithm thumbnail', 'RECRUITMENT_END', 'PREPARE', '알고리즘을 TDD로 풀자의 베루스입니다.', 1, '" + now + "', 4, '2021-12-06')"); - jdbcTemplate.update("INSERT INTO study(id, title, excerpt, thumbnail, recruitment_status, study_status, description, current_member_count, created_at, owner_id, start_date, enrollment_end_date, end_date) " - + "VALUES (6, 'Linux 스터디', '리눅스 설명', 'linux thumbnail', 'RECRUITMENT_END', 'PREPARE', 'Linux를 공부하자의 베루스입니다.', 1, '" + now + "', 4, '2021-12-06', '2021-12-07', '2022-01-07')"); - - jdbcTemplate.update("INSERT INTO category(id, name) VALUES (1, 'generation')"); - jdbcTemplate.update("INSERT INTO category(id, name) VALUES (2, 'area')"); - jdbcTemplate.update("INSERT INTO category(id, name) VALUES (3, 'subject')"); - - jdbcTemplate.update("INSERT INTO tag(id, name, description, category_id) VALUES (1, 'Java', '자바', 3)"); - jdbcTemplate.update("INSERT INTO tag(id, name, description, category_id) VALUES (2, '4기', '우테코4기', 1)"); - jdbcTemplate.update("INSERT INTO tag(id, name, description, category_id) VALUES (3, 'BE', '백엔드', 2)"); - jdbcTemplate.update("INSERT INTO tag(id, name, description, category_id) VALUES (4, 'FE', '프론트엔드', 2)"); - jdbcTemplate.update("INSERT INTO tag(id, name, description, category_id) VALUES (5, 'React', '리액트', 3)"); - - jdbcTemplate.update("INSERT INTO study_tag(study_id, tag_id) VALUES (1, 1)"); - jdbcTemplate.update("INSERT INTO study_tag(study_id, tag_id) VALUES (1, 2)"); - jdbcTemplate.update("INSERT INTO study_tag(study_id, tag_id) VALUES (1, 3)"); - - jdbcTemplate.update("INSERT INTO study_tag(study_id, tag_id) VALUES (2, 2)"); - jdbcTemplate.update("INSERT INTO study_tag(study_id, tag_id) VALUES (2, 4)"); - jdbcTemplate.update("INSERT INTO study_tag(study_id, tag_id) VALUES (2, 5)"); - - jdbcTemplate.update("INSERT INTO study_tag(study_id, tag_id) VALUES (3, 2)"); - jdbcTemplate.update("INSERT INTO study_tag(study_id, tag_id) VALUES (3, 4)"); - - jdbcTemplate.update("INSERT INTO study_tag(study_id, tag_id) VALUES (4, 2)"); - jdbcTemplate.update("INSERT INTO study_tag(study_id, tag_id) VALUES (4, 3)"); - - jdbcTemplate.update("INSERT INTO study_member(study_id, member_id) VALUES (1, 3)"); - jdbcTemplate.update("INSERT INTO study_member(study_id, member_id) VALUES (1, 4)"); - - jdbcTemplate.update("INSERT INTO study_member(study_id, member_id) VALUES (2, 1)"); - jdbcTemplate.update("INSERT INTO study_member(study_id, member_id) VALUES (2, 2)"); - jdbcTemplate.update("INSERT INTO study_member(study_id, member_id) VALUES (2, 4)"); - - jdbcTemplate.update("INSERT INTO study_member(study_id, member_id) VALUES (3, 3)"); - jdbcTemplate.update("INSERT INTO study_member(study_id, member_id) VALUES (3, 4)"); - } - - @DisplayName("스터디 요약 정보 외에 상세 정보를 포함하여 조회할 수 있다.") - @Test - public void getStudyDetails() { - RestAssured.given(spec).log().all() - .filter(document("studies/details")) - .when().log().all() - .get("/api/studies/2") - .then().log().all() - .statusCode(HttpStatus.OK.value()) - .body("id", not(empty())) - .body("title", is("React 스터디")) - .body("excerpt", is("리액트 설명")) - .body("thumbnail", is("react thumbnail")) - .body("recruitmentStatus", is("RECRUITMENT_START")) - .body("description", is("디우의 뤼액트 스터디입니다.")) - .body("currentMemberCount", is(4)) - .body("maxMemberCount", is(5)) - .body("enrollmentEndDate", is("2021-11-09")) - .body("startDate", is("2021-11-10")) - .body("endDate", is("2021-12-08")) - .body("owner.id", is(3)) - .body("owner.username", is("dwoo")) - .body("owner.imageUrl", is("https://image")) - .body("owner.profileUrl", is("github.com")) - .body("members.id", not(empty())) - .body("members.username", contains("jjanggu", "greenlawn", "verus")) - .body("members.imageUrl", contains("https://image", "https://image", "https://image")) - .body("members.profileUrl", contains("github.com", "github.com", "github.com")) - .body("tags.id", not(empty())) - .body("tags.name", contains("4기", "FE", "React")); - } - - @DisplayName("선택 데이터가 없는 스터디 세부사항을 조회한다.") - @Test - public void getNotHasOptionalDataStudyDetails() { - RestAssured.given().log().all() - .when().log().all() - .get("/api/studies/5") - .then().log().all() - .statusCode(HttpStatus.OK.value()) - .body("id", is(5)) - .body("title", is("알고리즘 스터디")) - .body("excerpt", is("알고리즘 설명")) - .body("thumbnail", is("algorithm thumbnail")) - .body("recruitmentStatus", is("RECRUITMENT_END")) - .body("description", is("알고리즘을 TDD로 풀자의 베루스입니다.")) - .body("currentMemberCount", is(1)) - .body("maxMemberCount", is(nullValue())) - .body("enrollmentEndDate", is(nullValue())) - .body("startDate", is("2021-12-06")) - .body("endDate", is(nullValue())) - .body("owner.id", is(4)) - .body("owner.username", is("verus")) - .body("owner.imageUrl", is("https://image")) - .body("owner.profileUrl", is("github.com")) - .body("members", is(empty())) - .body("tags", is(empty())); - } -} diff --git a/backend/src/test/java/com/woowacourse/acceptance/study/ParticipationStudyAcceptanceTest.java b/backend/src/test/java/com/woowacourse/acceptance/study/ParticipationStudyAcceptanceTest.java deleted file mode 100644 index 6c8f92c9d..000000000 --- a/backend/src/test/java/com/woowacourse/acceptance/study/ParticipationStudyAcceptanceTest.java +++ /dev/null @@ -1,152 +0,0 @@ -package com.woowacourse.acceptance.study; - -import static org.springframework.http.HttpHeaders.AUTHORIZATION; -import static org.springframework.http.HttpHeaders.CONTENT_TYPE; -import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; -import static org.springframework.restdocs.restassured3.RestAssuredRestDocumentation.document; - -import com.woowacourse.acceptance.AcceptanceTest; -import com.woowacourse.moamoa.auth.service.oauthclient.response.GithubProfileResponse; -import io.restassured.RestAssured; -import java.time.LocalDateTime; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpStatus; -import org.springframework.jdbc.core.JdbcTemplate; - -public class ParticipationStudyAcceptanceTest extends AcceptanceTest { - - @Autowired - private JdbcTemplate jdbcTemplate; - - @BeforeEach - void initDataBase() { - getBearerTokenBySignInOrUp(new GithubProfileResponse(1L, "jjanggu", "https://image", "github.com")); - getBearerTokenBySignInOrUp(new GithubProfileResponse(2L, "greenlawn", "https://image", "github.com")); - getBearerTokenBySignInOrUp(new GithubProfileResponse(3L, "dwoo", "https://image", "github.com")); - getBearerTokenBySignInOrUp(new GithubProfileResponse(4L, "verus", "https://image", "github.com")); - - final LocalDateTime now = LocalDateTime.now(); - - jdbcTemplate.update( - "INSERT INTO study(id, title, excerpt, thumbnail, recruitment_status, study_status, description, current_member_count, max_member_count, created_at, start_date, owner_id) " - + "VALUES (1, 'Java 스터디', '자바 설명', 'java thumbnail', 'RECRUITMENT_START', 'PREPARE', '그린론의 우당탕탕 자바 스터디입니다.', 3, 10, '" + now + "', '2021-12-08', 2)"); - - jdbcTemplate.update( - "INSERT INTO study(id, title, excerpt, thumbnail, recruitment_status, study_status, description, current_member_count, max_member_count, created_at, enrollment_end_date, start_date, end_date, owner_id) " - + "VALUES (2, 'React 스터디', '리액트 설명', 'react thumbnail', 'RECRUITMENT_END', 'PREPARE', '디우의 뤼액트 스터디입니다.', 4, 5, '" + now + "', '1999-01-01', '2021-11-10', '2021-12-08', 3)"); - - jdbcTemplate.update( - "INSERT INTO study(id, title, excerpt, thumbnail, recruitment_status, study_status, description, current_member_count, max_member_count, created_at, start_date, owner_id) " - + "VALUES (3, 'Java 스터디', '자바 설명', 'java thumbnail', 'RECRUITMENT_END', 'PREPARE', '그린론의 우당탕탕 자바 스터디입니다.', 3, 10, '" + now + "', '2021-12-08', 2)"); - - jdbcTemplate.update( - "INSERT INTO study(id, title, excerpt, thumbnail, recruitment_status, study_status, description, current_member_count, max_member_count, created_at, start_date, owner_id) " - + "VALUES (4, 'Java 스터디', '자바 설명', 'java thumbnail', 'RECRUITMENT_END', 'PREPARE', '그린론의 우당탕탕 자바 스터디입니다.', 3, 3, '" + now + "', '2021-12-08', 2)"); - - jdbcTemplate.update("INSERT INTO study_member(study_id, member_id) VALUES (1, 1)"); - jdbcTemplate.update("INSERT INTO study_member(study_id, member_id) VALUES (1, 4)"); - - jdbcTemplate.update("INSERT INTO study_member(study_id, member_id) VALUES (2, 1)"); - jdbcTemplate.update("INSERT INTO study_member(study_id, member_id) VALUES (2, 2)"); - - jdbcTemplate.update("INSERT INTO study_member(study_id, member_id) VALUES (3, 1)"); - jdbcTemplate.update("INSERT INTO study_member(study_id, member_id) VALUES (3, 4)"); - - jdbcTemplate.update("INSERT INTO study_member(study_id, member_id) VALUES (1, 1)"); - jdbcTemplate.update("INSERT INTO study_member(study_id, member_id) VALUES (1, 4)"); - } - - @DisplayName("아직 스터디에 가입되지 않은 회원은 스터디에 참여가 가능하다.") - @Test - public void participateStudy() { - final String jwtToken = getBearerTokenBySignInOrUp( - new GithubProfileResponse(3L, "dwoo", "https://image", "github.com")); - - RestAssured.given(spec).log().all() - .filter(document("studies/participant")) - .header(CONTENT_TYPE, APPLICATION_JSON_VALUE) - .header(AUTHORIZATION, jwtToken) - .when().log().all() - .post("/api/studies/1") - .then().log().all() - .statusCode(HttpStatus.OK.value()); - } - - @DisplayName("한 명의 회원은 동일한 스터디에 대해서 한 번만 가입이 가능하다.") - @Test - public void participateOnlyOnce() { - final String jwtToken = getBearerTokenBySignInOrUp( - new GithubProfileResponse(1L, "jjanggu", "https://image", "github.com")); - - RestAssured.given().log().all() - .header(CONTENT_TYPE, APPLICATION_JSON_VALUE) - .header(AUTHORIZATION, jwtToken) - .when().log().all() - .post("/api/studies/1") - .then().log().all() - .statusCode(HttpStatus.BAD_REQUEST.value()); - } - - @DisplayName("모집기간이 이미 만료된 스터디에 신청시에는 에러가 발생한다.") - @Test - public void get400WithEndRecruit() { - final String jwtToken = getBearerTokenBySignInOrUp( - new GithubProfileResponse(4L, "verus", "https://image", "github.com")); - - RestAssured.given().log().all() - .header(CONTENT_TYPE, APPLICATION_JSON_VALUE) - .header(AUTHORIZATION, jwtToken) - .when().log().all() - .post("/api/studies/2") - .then().log().all() - .statusCode(HttpStatus.BAD_REQUEST.value()); - } - - @DisplayName("CLOSE 상태의 스터디에 참여 요청시 400 에러가 발생한다.") - @Test - public void participateCloseStudy() { - final String jwtToken = getBearerTokenBySignInOrUp( - new GithubProfileResponse(3L, "dwoo", "https://image", "github.com")); - - RestAssured.given().log().all() - .header(CONTENT_TYPE, APPLICATION_JSON_VALUE) - .header(AUTHORIZATION, jwtToken) - .when().log().all() - .post("/api/studies/3") - .then().log().all() - .statusCode(HttpStatus.BAD_REQUEST.value()); - } - - @DisplayName("인원이 가득찬 스터디에 참여하면 400 에러가 발생한다.") - @Test - public void participateFullStudy() { - final String jwtToken = getBearerTokenBySignInOrUp( - new GithubProfileResponse(3L, "dwoo", "https://image", "github.com")); - - RestAssured.given().log().all() - .header(CONTENT_TYPE, APPLICATION_JSON_VALUE) - .header(AUTHORIZATION, jwtToken) - .when().log().all() - .post("/api/studies/4") - .then().log().all() - .statusCode(HttpStatus.BAD_REQUEST.value()); - } - - @DisplayName("스터디장(owner)은 스터디에 참여할 수 없다.") - @Test - public void participateOwner() { - final String jwtToken = getBearerTokenBySignInOrUp( - new GithubProfileResponse(2L, "greenlawn", "https://image", "github.com")); - - RestAssured.given().log().all() - .header(CONTENT_TYPE, APPLICATION_JSON_VALUE) - .header(AUTHORIZATION, jwtToken) - .when().log().all() - .post("/api/studies/1") - .then().log().all() - .statusCode(HttpStatus.BAD_REQUEST.value()); - } -} diff --git a/backend/src/test/java/com/woowacourse/acceptance/auth/AuthAcceptanceTest.java b/backend/src/test/java/com/woowacourse/acceptance/test/auth/AuthAcceptanceTest.java similarity index 98% rename from backend/src/test/java/com/woowacourse/acceptance/auth/AuthAcceptanceTest.java rename to backend/src/test/java/com/woowacourse/acceptance/test/auth/AuthAcceptanceTest.java index a6d6233ed..6f5bc2a67 100644 --- a/backend/src/test/java/com/woowacourse/acceptance/auth/AuthAcceptanceTest.java +++ b/backend/src/test/java/com/woowacourse/acceptance/test/auth/AuthAcceptanceTest.java @@ -1,4 +1,4 @@ -package com.woowacourse.acceptance.auth; +package com.woowacourse.acceptance.test.auth; import static org.hamcrest.Matchers.notNullValue; import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; diff --git a/backend/src/test/java/com/woowacourse/acceptance/cors/CorsAcceptanceTest.java b/backend/src/test/java/com/woowacourse/acceptance/test/cors/CorsAcceptanceTest.java similarity index 93% rename from backend/src/test/java/com/woowacourse/acceptance/cors/CorsAcceptanceTest.java rename to backend/src/test/java/com/woowacourse/acceptance/test/cors/CorsAcceptanceTest.java index 5329dc614..86185c487 100644 --- a/backend/src/test/java/com/woowacourse/acceptance/cors/CorsAcceptanceTest.java +++ b/backend/src/test/java/com/woowacourse/acceptance/test/cors/CorsAcceptanceTest.java @@ -1,4 +1,4 @@ -package com.woowacourse.acceptance.cors; +package com.woowacourse.acceptance.test.cors; import com.woowacourse.acceptance.AcceptanceTest; import io.restassured.RestAssured; diff --git a/backend/src/test/java/com/woowacourse/acceptance/member/MemberAcceptanceTest.java b/backend/src/test/java/com/woowacourse/acceptance/test/member/MemberAcceptanceTest.java similarity index 58% rename from backend/src/test/java/com/woowacourse/acceptance/member/MemberAcceptanceTest.java rename to backend/src/test/java/com/woowacourse/acceptance/test/member/MemberAcceptanceTest.java index 2f4d12694..700ed9feb 100644 --- a/backend/src/test/java/com/woowacourse/acceptance/member/MemberAcceptanceTest.java +++ b/backend/src/test/java/com/woowacourse/acceptance/test/member/MemberAcceptanceTest.java @@ -1,30 +1,28 @@ -package com.woowacourse.acceptance.member; +package com.woowacourse.acceptance.test.member; +import static com.woowacourse.acceptance.fixture.MemberFixtures.베루스_깃허브_ID; +import static com.woowacourse.acceptance.fixture.MemberFixtures.베루스_이름; +import static com.woowacourse.acceptance.fixture.MemberFixtures.베루스_이미지_URL; +import static com.woowacourse.acceptance.fixture.MemberFixtures.베루스_프로필_URL; +import static com.woowacourse.acceptance.steps.LoginSteps.베루스가; import static org.assertj.core.api.Assertions.assertThat; import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; import static org.springframework.restdocs.restassured3.RestAssuredRestDocumentation.document; import com.woowacourse.acceptance.AcceptanceTest; -import com.woowacourse.moamoa.auth.service.oauthclient.response.GithubProfileResponse; import com.woowacourse.moamoa.member.service.response.MemberResponse; import io.restassured.RestAssured; import org.apache.http.HttpHeaders; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.http.HttpStatus; public class MemberAcceptanceTest extends AcceptanceTest { - private String token; - - @BeforeEach - void setUp() { - token = getBearerTokenBySignInOrUp(new GithubProfileResponse(1L, "verus", "image", "profile")); - } - @Test void getCurrentMember() { + final String token = 베루스가().로그인한다(); + final MemberResponse memberResponse = RestAssured.given(spec).log().all() .header(HttpHeaders.AUTHORIZATION, token) .filter(document("members/me", @@ -35,9 +33,9 @@ void getCurrentMember() { .statusCode(HttpStatus.OK.value()) .extract().as(MemberResponse.class); - assertThat(memberResponse.getId()).isEqualTo(1L); - assertThat(memberResponse.getUsername()).isEqualTo("verus"); - assertThat(memberResponse.getImageUrl()).isEqualTo("image"); - assertThat(memberResponse.getProfileUrl()).isEqualTo("profile"); + assertThat(memberResponse.getId()).isEqualTo(베루스_깃허브_ID); + assertThat(memberResponse.getUsername()).isEqualTo(베루스_이름); + assertThat(memberResponse.getImageUrl()).isEqualTo(베루스_이미지_URL); + assertThat(memberResponse.getProfileUrl()).isEqualTo(베루스_프로필_URL); } } diff --git a/backend/src/test/java/com/woowacourse/acceptance/test/review/ReviewsAcceptanceTest.java b/backend/src/test/java/com/woowacourse/acceptance/test/review/ReviewsAcceptanceTest.java new file mode 100644 index 000000000..eebf81d2d --- /dev/null +++ b/backend/src/test/java/com/woowacourse/acceptance/test/review/ReviewsAcceptanceTest.java @@ -0,0 +1,235 @@ +package com.woowacourse.acceptance.test.review; + +import static com.woowacourse.acceptance.fixture.MemberFixtures.그린론_깃허브_ID; +import static com.woowacourse.acceptance.fixture.MemberFixtures.그린론_이름; +import static com.woowacourse.acceptance.fixture.MemberFixtures.그린론_이미지_URL; +import static com.woowacourse.acceptance.fixture.MemberFixtures.그린론_프로필_URL; +import static com.woowacourse.acceptance.fixture.MemberFixtures.디우_깃허브_ID; +import static com.woowacourse.acceptance.fixture.MemberFixtures.디우_이름; +import static com.woowacourse.acceptance.fixture.MemberFixtures.디우_이미지_URL; +import static com.woowacourse.acceptance.fixture.MemberFixtures.디우_프로필_URL; +import static com.woowacourse.acceptance.fixture.MemberFixtures.베루스_깃허브_ID; +import static com.woowacourse.acceptance.fixture.MemberFixtures.베루스_이름; +import static com.woowacourse.acceptance.fixture.MemberFixtures.베루스_이미지_URL; +import static com.woowacourse.acceptance.fixture.MemberFixtures.베루스_프로필_URL; +import static com.woowacourse.acceptance.fixture.MemberFixtures.짱구_깃허브_ID; +import static com.woowacourse.acceptance.fixture.MemberFixtures.짱구_이름; +import static com.woowacourse.acceptance.fixture.MemberFixtures.짱구_이미지_URL; +import static com.woowacourse.acceptance.fixture.MemberFixtures.짱구_프로필_URL; +import static com.woowacourse.acceptance.steps.LoginSteps.그린론이; +import static com.woowacourse.acceptance.steps.LoginSteps.디우가; +import static com.woowacourse.acceptance.steps.LoginSteps.베루스가; +import static com.woowacourse.acceptance.steps.LoginSteps.짱구가; +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.restdocs.restassured3.RestAssuredRestDocumentation.document; + +import com.woowacourse.acceptance.AcceptanceTest; +import com.woowacourse.moamoa.review.service.request.EditingReviewRequest; +import com.woowacourse.moamoa.review.service.request.WriteReviewRequest; +import com.woowacourse.moamoa.review.service.response.ReviewResponse; +import com.woowacourse.moamoa.review.service.response.ReviewsResponse; +import com.woowacourse.moamoa.review.service.response.WriterResponse; +import io.restassured.RestAssured; +import java.time.LocalDate; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; + +@DisplayName("리뷰 인수 테스트") +public class ReviewsAcceptanceTest extends AcceptanceTest { + + @DisplayName("리뷰를 작성한다.") + @Test + void create() { + // arrange + LocalDate 지금 = LocalDate.now(); + long 자바_스터디 = 짱구가().로그인하고().자바_스터디를().시작일자는(지금).생성한다(); + final String token = 짱구가().로그인한다(); + + final WriteReviewRequest writeReviewRequest = new WriteReviewRequest("짱구의 스터디 리뷰입니다."); + + // act & assert + RestAssured.given(spec).log().all() + .header(HttpHeaders.AUTHORIZATION, token) + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) + .header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE) + .pathParam("study-id", 자바_스터디) + .body(writeReviewRequest) + .filter(document("reviews/create")) + .when().log().all() + .post("/api/studies/{study-id}/reviews") + .then().log().all() + .statusCode(HttpStatus.CREATED.value()); + } + + @DisplayName("스터디에 달린 전체 리뷰 목록을 조회할 수 있다.") + @Test + void getAllReviews() { + // arrange + final LocalDate 지금 = LocalDate.now(); + + long 자바_스터디_ID = 짱구가().로그인하고().자바_스터디를().시작일자는(지금).생성한다(); + long 리액트_스터디_ID = 짱구가().로그인하고().리액트_스터디를().시작일자는(지금).생성한다(); + + 그린론이().로그인하고().스터디에(자바_스터디_ID).참여한다(); + 디우가().로그인하고().스터디에(자바_스터디_ID).참여한다(); + 베루스가().로그인하고().스터디에(자바_스터디_ID).참여한다(); + + long 짱구_리뷰_ID = 짱구가().로그인하고().스터디에(자바_스터디_ID).리뷰를_작성한다("리뷰 내용1"); + long 그린론_리뷰_ID = 그린론이().로그인하고().스터디에(자바_스터디_ID).리뷰를_작성한다("리뷰 내용2"); + long 디우_리뷰_ID = 디우가().로그인하고().스터디에(자바_스터디_ID).리뷰를_작성한다("리뷰 내용3"); + long 베루스_리뷰_ID = 베루스가().로그인하고().스터디에(자바_스터디_ID).리뷰를_작성한다("리뷰 내용4"); + 짱구가().로그인하고().스터디에(리액트_스터디_ID).리뷰를_작성한다("리뷰 내용5"); + + // act + final ReviewsResponse reviewsResponse = RestAssured.given(spec).log().all() + .pathParam("study-id", 자바_스터디_ID) + .filter(document("reviews/list")) + .when().log().all() + .get("api/studies/{study-id}/reviews") + .then().log().all() + .statusCode(HttpStatus.OK.value()) + .extract().as(ReviewsResponse.class); + + // assert + final LocalDate 리뷰_생성일 = 지금; + final LocalDate 리뷰_수정일 = 지금; + + final WriterResponse 짱구 = new WriterResponse(짱구_깃허브_ID, 짱구_이름, 짱구_이미지_URL, 짱구_프로필_URL); + final ReviewResponse 짱구_리뷰 = new ReviewResponse(짱구_리뷰_ID, 짱구, 리뷰_생성일, 리뷰_수정일, "리뷰 내용1"); + + final WriterResponse 그린론 = new WriterResponse(그린론_깃허브_ID, 그린론_이름, 그린론_이미지_URL, 그린론_프로필_URL); + final ReviewResponse 그린론_리뷰 = new ReviewResponse(그린론_리뷰_ID, 그린론, 리뷰_생성일, 리뷰_수정일, "리뷰 내용2"); + + final WriterResponse 디우 = new WriterResponse(디우_깃허브_ID, 디우_이름, 디우_이미지_URL, 디우_프로필_URL); + final ReviewResponse 디우_리뷰 = new ReviewResponse(디우_리뷰_ID, 디우, 리뷰_생성일, 리뷰_수정일, "리뷰 내용3"); + + final WriterResponse 베루스 = new WriterResponse(베루스_깃허브_ID, 베루스_이름, 베루스_이미지_URL, 베루스_프로필_URL); + final ReviewResponse 베루스_리뷰 = new ReviewResponse(베루스_리뷰_ID, 베루스, 리뷰_생성일, 리뷰_수정일, "리뷰 내용4"); + + assertThat(reviewsResponse.getTotalCount()).isEqualTo(4); + assertThat(reviewsResponse.getReviews()) + .containsExactlyInAnyOrderElementsOf(List.of(베루스_리뷰, 디우_리뷰, 그린론_리뷰, 짱구_리뷰)); + } + + @DisplayName("원하는 갯수만큼 스터디에 달린 리뷰 목록을 조회할 수 있다.") + @Test + public void getReviewsBySize() { + // arrange + final LocalDate 지금 = LocalDate.now(); + + long 자바_스터디_ID = 짱구가().로그인하고().자바_스터디를().시작일자는(지금).생성한다(); + long 리액트_스터디_ID = 짱구가().로그인하고().리액트_스터디를().시작일자는(지금).생성한다(); + + 그린론이().로그인하고().스터디에(자바_스터디_ID).참여한다(); + 디우가().로그인하고().스터디에(자바_스터디_ID).참여한다(); + 베루스가().로그인하고().스터디에(자바_스터디_ID).참여한다(); + + long 짱구_리뷰_ID = 짱구가().로그인하고().스터디에(자바_스터디_ID).리뷰를_작성한다("리뷰 내용1"); + long 그린론_리뷰_ID = 그린론이().로그인하고().스터디에(자바_스터디_ID).리뷰를_작성한다("리뷰 내용2"); + long 디우_리뷰_ID = 디우가().로그인하고().스터디에(자바_스터디_ID).리뷰를_작성한다("리뷰 내용3"); + long 베루스_리뷰_ID = 베루스가().로그인하고().스터디에(자바_스터디_ID).리뷰를_작성한다("리뷰 내용4"); + 짱구가().로그인하고().스터디에(리액트_스터디_ID).리뷰를_작성한다("리뷰 내용5"); + + // act + final ReviewsResponse reviewsResponse = RestAssured.given(spec).log().all() + .pathParam("study-id", 자바_스터디_ID) + .filter(document("reviews/list-certain-number")) + .when().log().all() + .get("/api/studies/{study-id}/reviews?size=2") + .then() + .statusCode(HttpStatus.OK.value()) + .extract().as(ReviewsResponse.class); + + // assert + final LocalDate 리뷰_생성일 = 지금; + final LocalDate 리뷰_수정일 = 지금; + + final WriterResponse 디우 = new WriterResponse(디우_깃허브_ID, 디우_이름, 디우_이미지_URL, 디우_프로필_URL); + final ReviewResponse 디우_리뷰 = new ReviewResponse(디우_리뷰_ID, 디우, 리뷰_생성일, 리뷰_수정일, "리뷰 내용3"); + + final WriterResponse 베루스 = new WriterResponse(베루스_깃허브_ID, 베루스_이름, 베루스_이미지_URL, 베루스_프로필_URL); + final ReviewResponse 베루스_리뷰 = new ReviewResponse(베루스_리뷰_ID, 베루스, 리뷰_생성일, 리뷰_수정일, "리뷰 내용4"); + + assertThat(reviewsResponse.getTotalCount()).isEqualTo(4); + assertThat(reviewsResponse.getReviews()).containsExactly(베루스_리뷰, 디우_리뷰); + } + + @DisplayName("자신이 참여한 스터디에 작성한 리뷰를 삭제할 수 있다.") + @Test + void deleteReview() { + // arrange + LocalDate 지금 = LocalDate.now(); + long 자바_스터디_ID = 짱구가().로그인하고().자바_스터디를().시작일자는(지금).생성한다(); + long 짱구_리뷰_ID = 짱구가().로그인하고().스터디에(자바_스터디_ID).리뷰를_작성한다("리뷰 내용1"); + String token = 짱구가().로그인한다(); + + // act + RestAssured.given(spec).log().all() + .filter(document("reviews/delete")) + .header(HttpHeaders.AUTHORIZATION, token) + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) + .pathParam("study-id", 자바_스터디_ID) + .pathParam("review-id", 짱구_리뷰_ID) + .when().log().all() + .delete("/api/studies/{study-id}/reviews/{review-id}") + .then().statusCode(HttpStatus.NO_CONTENT.value()); + + // assert + final ReviewsResponse response = RestAssured.given(spec).log().all() + .pathParam("study-id", 자바_스터디_ID) + .when().log().all() + .get("/api/studies/{study-id}/reviews") + .then() + .statusCode(HttpStatus.OK.value()) + .extract().as(ReviewsResponse.class); + + assertThat(response.getReviews()).isEmpty(); + assertThat(response.getTotalCount()).isEqualTo(0); + } + + @DisplayName("자신이 참여한 스터디에 작성한 리뷰를 수정할 수 있다.") + @Test + void updateReview() { + // arrange + LocalDate 지금 = LocalDate.now(); + long 자바_스터디_ID = 짱구가().로그인하고().자바_스터디를().시작일자는(지금).생성한다(); + long 짱구_리뷰_ID = 짱구가().로그인하고().스터디에(자바_스터디_ID).리뷰를_작성한다("리뷰 내용1"); + String token = 짱구가().로그인한다(); + + EditingReviewRequest request = new EditingReviewRequest("수정 리뷰"); + + // act + RestAssured.given(spec).log().all() + .filter(document("reviews/update")) + .header(HttpHeaders.AUTHORIZATION, token) + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) + .header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE) + .pathParam("study-id", 자바_스터디_ID) + .pathParam("review-id", 짱구_리뷰_ID) + .body(request) + .when().log().all() + .put("/api/studies/{study-id}/reviews/{review-id}") + .then().statusCode(HttpStatus.NO_CONTENT.value()); + + // assert + final ReviewsResponse response = RestAssured.given(spec).log().all() + .pathParam("study-id", 자바_스터디_ID) + .when().log().all() + .get("/api/studies/{study-id}/reviews") + .then() + .statusCode(HttpStatus.OK.value()) + .extract().as(ReviewsResponse.class); + + final LocalDate 리뷰_생성일 = 지금; + final LocalDate 리뷰_수정일 = 지금; + final WriterResponse 짱구 = new WriterResponse(짱구_깃허브_ID, 짱구_이름, 짱구_이미지_URL, 짱구_프로필_URL); + final ReviewResponse 짱구_리뷰 = new ReviewResponse(짱구_리뷰_ID, 짱구, 리뷰_생성일, 리뷰_수정일, "수정 리뷰"); + assertThat(response.getReviews()).containsExactlyInAnyOrder(짱구_리뷰); + assertThat(response.getTotalCount()).isEqualTo(1); + } +} diff --git a/backend/src/test/java/com/woowacourse/acceptance/study/AutoCloseEnrollmentAcceptanceTest.java b/backend/src/test/java/com/woowacourse/acceptance/test/study/AutoCloseEnrollmentAcceptanceTest.java similarity index 96% rename from backend/src/test/java/com/woowacourse/acceptance/study/AutoCloseEnrollmentAcceptanceTest.java rename to backend/src/test/java/com/woowacourse/acceptance/test/study/AutoCloseEnrollmentAcceptanceTest.java index 42a0476dc..c24322fd0 100644 --- a/backend/src/test/java/com/woowacourse/acceptance/study/AutoCloseEnrollmentAcceptanceTest.java +++ b/backend/src/test/java/com/woowacourse/acceptance/test/study/AutoCloseEnrollmentAcceptanceTest.java @@ -1,4 +1,4 @@ -package com.woowacourse.acceptance.study; +package com.woowacourse.acceptance.test.study; import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.Matchers.contains; diff --git a/backend/src/test/java/com/woowacourse/acceptance/study/CreatingStudyAcceptanceTest.java b/backend/src/test/java/com/woowacourse/acceptance/test/study/CreatingStudyAcceptanceTest.java similarity index 84% rename from backend/src/test/java/com/woowacourse/acceptance/study/CreatingStudyAcceptanceTest.java rename to backend/src/test/java/com/woowacourse/acceptance/test/study/CreatingStudyAcceptanceTest.java index ad41785d0..db2959a0d 100644 --- a/backend/src/test/java/com/woowacourse/acceptance/study/CreatingStudyAcceptanceTest.java +++ b/backend/src/test/java/com/woowacourse/acceptance/test/study/CreatingStudyAcceptanceTest.java @@ -1,5 +1,6 @@ -package com.woowacourse.acceptance.study; +package com.woowacourse.acceptance.test.study; +import static com.woowacourse.acceptance.steps.LoginSteps.짱구가; import static org.assertj.core.api.Assertions.assertThat; import static org.springframework.http.HttpHeaders.AUTHORIZATION; import static org.springframework.http.HttpHeaders.CONTENT_TYPE; @@ -7,14 +8,11 @@ import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; -import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; -import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; import static org.springframework.restdocs.request.RequestDocumentation.requestParameters; import static org.springframework.restdocs.restassured3.RestAssuredRestDocumentation.document; import com.woowacourse.acceptance.AcceptanceTest; -import com.woowacourse.moamoa.auth.service.oauthclient.response.GithubProfileResponse; import io.restassured.RestAssured; import java.time.LocalDate; import java.time.format.DateTimeFormatter; @@ -23,7 +21,6 @@ import java.util.Map; import java.util.regex.Pattern; import java.util.stream.Stream; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -31,18 +28,9 @@ import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.ValueSource; import org.springframework.http.HttpStatus; -import org.springframework.restdocs.payload.JsonFieldType; public class CreatingStudyAcceptanceTest extends AcceptanceTest { - @BeforeEach - void initDataBase() { - getBearerTokenBySignInOrUp(new GithubProfileResponse(1L, "jjanggu", "https://image", "github.com")); - getBearerTokenBySignInOrUp(new GithubProfileResponse(2L, "greenlawn", "https://image", "github.com")); - getBearerTokenBySignInOrUp(new GithubProfileResponse(3L, "dwoo", "https://image", "github.com")); - getBearerTokenBySignInOrUp(new GithubProfileResponse(4L, "verus", "https://image", "github.com")); - } - @DisplayName("유효하지 않은 토큰으로 스터디 개설 시 401을 반환한다.") @ParameterizedTest @ValueSource(strings = {"Invalid Token", "bearer Invalid Token"}) @@ -71,8 +59,7 @@ void get401WhenUsingEmptyToken() { @ParameterizedTest @MethodSource("provideBlankForRequiredFields") void get400WhenSetBlankToRequiredField(Map param) { - final String jwtToken = getBearerTokenBySignInOrUp( - new GithubProfileResponse(1L, "jjanggu", "https://image", "github.com")); + final String jwtToken = 짱구가().로그인한다(); RestAssured .given().log().all() @@ -115,8 +102,7 @@ public static Stream provideBlankForRequiredFields() { @ParameterizedTest @MethodSource("provideInvalidFormatForOptionalFields") void get400WhenSetInvalidFormatToOptionalFields(Map optionalBody) { - final String jwtToken = getBearerTokenBySignInOrUp( - new GithubProfileResponse(1L, "jjanggu", "https://image", "github.com")); + final String jwtToken = 짱구가().로그인한다(); Map requiredBody = Map.of("title", "제목", "excerpt", "자바를 공부하는 스터디", "thumbnail", "image", "description", "스터디 상세 설명입니다.", "startDate", "2022-07-20"); @@ -147,8 +133,7 @@ public static Stream provideInvalidFormatForOptionalFields() { @Test @DisplayName("정상적인 스터디 생성") void createStudy() { - final String jwtToken = getBearerTokenBySignInOrUp( - new GithubProfileResponse(1L, "jjanggu", "https://image", "github.com")); + final String jwtToken = 짱구가().로그인한다(); final String location = RestAssured.given(spec).log().all() .filter(document("studies/create", diff --git a/backend/src/test/java/com/woowacourse/acceptance/test/study/GettingMyStudiesAcceptanceTest.java b/backend/src/test/java/com/woowacourse/acceptance/test/study/GettingMyStudiesAcceptanceTest.java new file mode 100644 index 000000000..12b8ff32f --- /dev/null +++ b/backend/src/test/java/com/woowacourse/acceptance/test/study/GettingMyStudiesAcceptanceTest.java @@ -0,0 +1,116 @@ +package com.woowacourse.acceptance.test.study; + +import static com.woowacourse.acceptance.fixture.MemberFixtures.그린론_깃허브_ID; +import static com.woowacourse.acceptance.fixture.MemberFixtures.그린론_이름; +import static com.woowacourse.acceptance.fixture.MemberFixtures.그린론_이미지_URL; +import static com.woowacourse.acceptance.fixture.MemberFixtures.그린론_프로필_URL; +import static com.woowacourse.acceptance.fixture.MemberFixtures.디우_깃허브_ID; +import static com.woowacourse.acceptance.fixture.MemberFixtures.디우_이름; +import static com.woowacourse.acceptance.fixture.MemberFixtures.디우_이미지_URL; +import static com.woowacourse.acceptance.fixture.MemberFixtures.디우_프로필_URL; +import static com.woowacourse.acceptance.fixture.StudyFixtures.리액트_스터디_제목; +import static com.woowacourse.acceptance.fixture.StudyFixtures.자바_스터디_제목; +import static com.woowacourse.acceptance.fixture.TagFixtures.BE_태그_ID; +import static com.woowacourse.acceptance.fixture.TagFixtures.리액트_태그_ID; +import static com.woowacourse.acceptance.fixture.TagFixtures.리액트_태그_설명; +import static com.woowacourse.acceptance.fixture.TagFixtures.자바_태그_ID; +import static com.woowacourse.acceptance.fixture.TagFixtures.자바_태그_설명; +import static com.woowacourse.acceptance.steps.LoginSteps.그린론이; +import static com.woowacourse.acceptance.steps.LoginSteps.디우가; +import static com.woowacourse.moamoa.study.domain.StudyStatus.IN_PROGRESS; +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.is; +import static org.springframework.http.HttpHeaders.AUTHORIZATION; +import static org.springframework.http.HttpHeaders.CONTENT_TYPE; +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; +import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; +import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.request.RequestDocumentation.requestParameters; +import static org.springframework.restdocs.restassured3.RestAssuredRestDocumentation.document; + +import com.woowacourse.acceptance.AcceptanceTest; +import com.woowacourse.moamoa.member.query.data.MemberData; +import com.woowacourse.moamoa.study.domain.StudyStatus; +import com.woowacourse.moamoa.study.query.data.MyStudySummaryData; +import com.woowacourse.moamoa.study.service.response.MyStudiesResponse; +import com.woowacourse.moamoa.study.service.response.MyStudyResponse; +import com.woowacourse.moamoa.tag.query.response.TagSummaryData; +import io.restassured.RestAssured; +import java.time.LocalDate; +import java.util.List; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpStatus; +import org.springframework.restdocs.payload.JsonFieldType; + +public class GettingMyStudiesAcceptanceTest extends AcceptanceTest { + + @Disabled // 그린론이 해결할 버그 관련 인수 테스트 + @DisplayName("내가 참여한 스터디를 조회한다.") + @Test + void getMyStudies() { + // arrange + LocalDate 지금 = LocalDate.now(); + long 자바_스터디_ID = 그린론이().로그인하고().자바_스터디를().시작일자는(지금).태그는(자바_태그_ID, BE_태그_ID).생성한다(); + long 리액트_스터디_ID = 디우가().로그인하고().리액트_스터디를().시작일자는(지금.plusDays(10)).생성한다(); + 그린론이().로그인하고().스터디에(리액트_스터디_ID).참여한다(); + final String token = 그린론이().로그인한다(); + + // act + final MyStudiesResponse body = RestAssured.given(spec).log().all() + .filter(document("studies/myStudy")) + .header(AUTHORIZATION, token) + .when().log().all() + .get("/api/my/studies") + .then().log().all() + .statusCode(HttpStatus.OK.value()) + .extract().as(MyStudiesResponse.class); + + // assert + MyStudyResponse expectedJava = new MyStudyResponse( + new MyStudySummaryData(자바_스터디_ID, 자바_스터디_제목, IN_PROGRESS, 1, + null, 지금.toString(), null), + new MemberData(그린론_깃허브_ID, 그린론_이름, 그린론_이미지_URL, 그린론_프로필_URL), + List.of(new TagSummaryData(자바_태그_ID, 자바_태그_설명), new TagSummaryData(리액트_태그_ID, 리액트_태그_설명))); + + MyStudyResponse expectedReact = new MyStudyResponse( + new MyStudySummaryData(리액트_스터디_ID, 리액트_스터디_제목, StudyStatus.PREPARE, 1, + null, 지금.plusDays(10).toString(), null), + new MemberData(디우_깃허브_ID, 디우_이름, 디우_이미지_URL, 디우_프로필_URL), + List.of()); + + assertThat(body.getStudies()) + .hasSize(2) + .containsExactlyInAnyOrder(expectedJava, expectedReact); + } + + @Test + @DisplayName("특정 스터디에서 나의 Role을 확인한다.") + void isMyStudy() { + // arrange + LocalDate 지금 = LocalDate.now(); + long 자바_스터디_ID = 그린론이().로그인하고().자바_스터디를().시작일자는(지금).생성한다(); + String token = 그린론이().로그인한다(); + + // act & assert + RestAssured.given(spec).log().all() + .filter(document("members/me/role", + requestHeaders(headerWithName("Authorization").description("Bearer Token")), + requestParameters(parameterWithName("study-id").description("스터디 ID")), + responseFields( + fieldWithPath("role").type(JsonFieldType.STRING).description("해당 스터디에서 사용자의 역할")))) + .filter(document("members/me/role")) + .header(CONTENT_TYPE, APPLICATION_JSON_VALUE) + .header(AUTHORIZATION, token) + .queryParam("study-id", 자바_스터디_ID) + .when() + .get("/api/members/me/role") + .then().log().all() + .statusCode(HttpStatus.OK.value()) + .body("role", is("OWNER")); + } +} diff --git a/backend/src/test/java/com/woowacourse/acceptance/study/GettingStudiesSummaryAcceptanceTest.java b/backend/src/test/java/com/woowacourse/acceptance/test/study/GettingStudiesSummaryAcceptanceTest.java similarity index 59% rename from backend/src/test/java/com/woowacourse/acceptance/study/GettingStudiesSummaryAcceptanceTest.java rename to backend/src/test/java/com/woowacourse/acceptance/test/study/GettingStudiesSummaryAcceptanceTest.java index 8698e0935..842e52473 100644 --- a/backend/src/test/java/com/woowacourse/acceptance/study/GettingStudiesSummaryAcceptanceTest.java +++ b/backend/src/test/java/com/woowacourse/acceptance/test/study/GettingStudiesSummaryAcceptanceTest.java @@ -1,5 +1,7 @@ -package com.woowacourse.acceptance.study; +package com.woowacourse.acceptance.test.study; +import static com.woowacourse.acceptance.fixture.TagFixtures.*; +import static com.woowacourse.acceptance.steps.LoginSteps.짱구가; import static java.util.stream.Collectors.toList; import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.Matchers.blankOrNullString; @@ -8,13 +10,10 @@ import static org.springframework.restdocs.restassured3.RestAssuredRestDocumentation.document; import com.woowacourse.acceptance.AcceptanceTest; -import com.woowacourse.moamoa.auth.service.oauthclient.response.GithubProfileResponse; import com.woowacourse.moamoa.study.service.StudyResponse; -import com.woowacourse.moamoa.study.service.request.CreatingStudyRequest; import com.woowacourse.moamoa.study.service.response.StudiesResponse; import com.woowacourse.moamoa.tag.query.response.TagSummaryData; import io.restassured.RestAssured; -import io.restassured.response.ValidatableResponse; import java.time.LocalDate; import java.util.List; import org.junit.jupiter.api.BeforeEach; @@ -22,16 +21,11 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; -import org.springframework.jdbc.core.JdbcTemplate; @DisplayName("스터디 목록 조회 인수 테스트") public class GettingStudiesSummaryAcceptanceTest extends AcceptanceTest { - @Autowired - private JdbcTemplate jdbcTemplate; - private Long javaStudyId; private Long reactStudyId; private Long javascriptStudyId; @@ -47,60 +41,31 @@ public class GettingStudiesSummaryAcceptanceTest extends AcceptanceTest { @BeforeEach void initDataBase() { - jdbcTemplate.update("INSERT INTO category(id, name) VALUES (1, 'generation')"); - jdbcTemplate.update("INSERT INTO category(id, name) VALUES (2, 'area')"); - jdbcTemplate.update("INSERT INTO category(id, name) VALUES (3, 'subject')"); - - jdbcTemplate.update("INSERT INTO tag(id, name, description, category_id) VALUES (1, 'Java', '자바', 3)"); - jdbcTemplate.update("INSERT INTO tag(id, name, description, category_id) VALUES (2, '4기', '우테코4기', 1)"); - jdbcTemplate.update("INSERT INTO tag(id, name, description, category_id) VALUES (3, 'BE', '백엔드', 2)"); - jdbcTemplate.update("INSERT INTO tag(id, name, description, category_id) VALUES (4, 'FE', '프론트엔드', 2)"); - jdbcTemplate.update("INSERT INTO tag(id, name, description, category_id) VALUES (5, 'React', '리액트', 3)"); - - String token = getBearerTokenBySignInOrUp( - new GithubProfileResponse(1L, "jjanggu", "https://image", "github.com")); - - CreatingStudyRequest javaRequest = CreatingStudyRequest.builder() - .title("Java 스터디").excerpt("자바 설명").thumbnail("java thumbnail") - .description("그린론의 우당탕탕 자바 스터디입니다.").startDate(LocalDate.now().plusDays(1)) - .tagIds(List.of(1L, 2L, 3L)) - .build(); - javaStudyId = createStudy(token, javaRequest); - - CreatingStudyRequest reactRequest = CreatingStudyRequest.builder() - .title("React 스터디").excerpt("리액트 설명").thumbnail("react thumbnail") - .description("디우의 뤼액트 스터디입니다.").startDate(LocalDate.now().plusDays(2)) - .tagIds(List.of(2L, 4L, 5L)) - .build(); - reactStudyId = createStudy(token, reactRequest); - - CreatingStudyRequest javascriptRequest = CreatingStudyRequest.builder() - .title("javaScript 스터디").excerpt("자바스크립트 설명").thumbnail("javascript thumbnail") - .description("그린론의 자바스크립트 접해보기").startDate(LocalDate.now().plusDays(3)) - .tagIds(List.of(2L, 4L)) - .build(); - javascriptStudyId = createStudy(token, javascriptRequest); - - CreatingStudyRequest httpRequest = CreatingStudyRequest.builder() - .title("HTTP 스터디").excerpt("HTTP 설명").thumbnail("http thumbnail") - .description("디우의 HTTP 정복하기").startDate(LocalDate.now().plusDays(3)) - .tagIds(List.of(2L)) - .build(); - httpStudyId = createStudy(token, httpRequest); - - CreatingStudyRequest algorithmRequest = CreatingStudyRequest.builder() - .title("알고리즘 스터디").excerpt("알고리즘 설명").thumbnail("algorithm thumbnail") - .description("알고리즘을 TDD로 풀자의 베루스입니다.").startDate(LocalDate.now().plusDays(2)) - .tagIds(List.of(2L)) - .build(); - algorithmStudyId = createStudy(token, algorithmRequest); - - CreatingStudyRequest linuxRequest = CreatingStudyRequest.builder() - .title("Linux 스터디").excerpt("리눅스 설명").thumbnail("linux thumbnail") - .description("Linux를 공부하자의 베루스입니다.").startDate(LocalDate.now().plusDays(2)) - .tagIds(List.of(2L, 3L)) - .build(); - linuxStudyId = createStudy(token, linuxRequest); + LocalDate 지금 = LocalDate.now(); + + javaStudyId = 짱구가().로그인하고().자바_스터디를() + .시작일자는(지금).태그는(자바_태그_ID, 우테코4기_태그_ID, BE_태그_ID) + .생성한다(); + + reactStudyId = 짱구가().로그인하고().리액트_스터디를() + .시작일자는(지금).태그는(우테코4기_태그_ID, FE_태그_ID, 리액트_태그_ID) + .생성한다(); + + javascriptStudyId = 짱구가().로그인하고().자바스크립트_스터디를() + .시작일자는(지금).태그는(우테코4기_태그_ID, FE_태그_ID) + .생성한다(); + + httpStudyId = 짱구가().로그인하고().HTTP_스터디를() + .시작일자는(지금).태그는(우테코4기_태그_ID) + .생성한다(); + + algorithmStudyId = 짱구가().로그인하고().알고리즘_스터디를() + .시작일자는(지금).태그는(우테코4기_태그_ID) + .생성한다(); + + linuxStudyId = 짱구가().로그인하고().리눅스_스터디를() + .시작일자는(지금).태그는(우테코4기_태그_ID, BE_태그_ID) + .생성한다(); javaTag = new TagSummaryData(1L, "Java"); fourTag = new TagSummaryData(2L, "4기"); @@ -112,7 +77,13 @@ void initDataBase() { @DisplayName("첫번째 페이지의 스터디 목록을 조회 한다.") @Test public void getFirstPageOfStudies() { - final StudiesResponse studiesResponse = 페이징을_통한_스터디_목록_조회(0, 3) + final StudiesResponse studiesResponse = RestAssured.given(spec).log().all() + .filter(document("studies/summary")) + .queryParam("page", 0) + .queryParam("size", 3) + .when().log().all() + .get("/api/studies") + .then().log().all() .statusCode(HttpStatus.OK.value()) .extract().as(StudiesResponse.class); @@ -134,7 +105,13 @@ public void getFirstPageOfStudies() { @DisplayName("마지막 페이지의 스터디 목록을 조회 한다.") @Test public void getLastPageOfStudies() { - final StudiesResponse studiesResponse = 페이징을_통한_스터디_목록_조회(1, 3) + final StudiesResponse studiesResponse = RestAssured.given(spec).log().all() + .filter(document("studies/summary")) + .queryParam("page", 1) + .queryParam("size", 3) + .when().log().all() + .get("/api/studies") + .then().log().all() .statusCode(HttpStatus.OK.value()) .extract().as(StudiesResponse.class); @@ -157,7 +134,13 @@ public void getLastPageOfStudies() { @ParameterizedTest @CsvSource({"-1,3", "1,0", "one,1", "1,one"}) public void response400WhenRequestByInvalidPagingInfo(String page, String size) { - 페이징을_통한_스터디_목록_조회(page, size) + RestAssured.given(spec).log().all() + .filter(document("studies/summary")) + .queryParam("page", page) + .queryParam("size", size) + .when().log().all() + .get("/api/studies") + .then().log().all() .statusCode(HttpStatus.BAD_REQUEST.value()) .body("message", not(blankOrNullString())); } @@ -210,14 +193,4 @@ public void getStudiesByDefaultPagingInfo() { ) ); } - - private ValidatableResponse 페이징을_통한_스터디_목록_조회(Object page, Object size) { - return RestAssured.given(spec).log().all() - .filter(document("studies/summary")) - .queryParam("page", page) - .queryParam("size", size) - .when().log().all() - .get("/api/studies") - .then().log().all(); - } } diff --git a/backend/src/test/java/com/woowacourse/acceptance/test/study/GettingStudyDetailsAcceptanceTest.java b/backend/src/test/java/com/woowacourse/acceptance/test/study/GettingStudyDetailsAcceptanceTest.java new file mode 100644 index 000000000..730a78de2 --- /dev/null +++ b/backend/src/test/java/com/woowacourse/acceptance/test/study/GettingStudyDetailsAcceptanceTest.java @@ -0,0 +1,120 @@ +package com.woowacourse.acceptance.test.study; + +import static com.woowacourse.acceptance.fixture.MemberFixtures.그린론_이름; +import static com.woowacourse.acceptance.fixture.MemberFixtures.그린론_이미지_URL; +import static com.woowacourse.acceptance.fixture.MemberFixtures.그린론_프로필_URL; +import static com.woowacourse.acceptance.fixture.MemberFixtures.디우_깃허브_ID; +import static com.woowacourse.acceptance.fixture.MemberFixtures.디우_이름; +import static com.woowacourse.acceptance.fixture.MemberFixtures.디우_이미지_URL; +import static com.woowacourse.acceptance.fixture.MemberFixtures.디우_프로필_URL; +import static com.woowacourse.acceptance.fixture.MemberFixtures.베루스_깃허브_ID; +import static com.woowacourse.acceptance.fixture.MemberFixtures.베루스_이름; +import static com.woowacourse.acceptance.fixture.MemberFixtures.베루스_이미지_URL; +import static com.woowacourse.acceptance.fixture.MemberFixtures.베루스_프로필_URL; +import static com.woowacourse.acceptance.fixture.MemberFixtures.짱구_이름; +import static com.woowacourse.acceptance.fixture.MemberFixtures.짱구_이미지_URL; +import static com.woowacourse.acceptance.fixture.MemberFixtures.짱구_프로필_URL; +import static com.woowacourse.acceptance.fixture.StudyFixtures.리액트_스터디_설명; +import static com.woowacourse.acceptance.fixture.StudyFixtures.리액트_스터디_썸네일; +import static com.woowacourse.acceptance.fixture.StudyFixtures.리액트_스터디_요약; +import static com.woowacourse.acceptance.fixture.StudyFixtures.리액트_스터디_제목; +import static com.woowacourse.acceptance.fixture.StudyFixtures.알고리즘_스터디_설명; +import static com.woowacourse.acceptance.fixture.StudyFixtures.알고리즘_스터디_썸네일; +import static com.woowacourse.acceptance.fixture.StudyFixtures.알고리즘_스터디_요약; +import static com.woowacourse.acceptance.fixture.StudyFixtures.알고리즘_스터디_제목; +import static com.woowacourse.acceptance.fixture.TagFixtures.FE_태그_ID; +import static com.woowacourse.acceptance.fixture.TagFixtures.리액트_태그_ID; +import static com.woowacourse.acceptance.fixture.TagFixtures.우테코4기_태그_ID; +import static com.woowacourse.acceptance.steps.LoginSteps.그린론이; +import static com.woowacourse.acceptance.steps.LoginSteps.디우가; +import static com.woowacourse.acceptance.steps.LoginSteps.베루스가; +import static com.woowacourse.acceptance.steps.LoginSteps.짱구가; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.nullValue; +import static org.springframework.restdocs.restassured3.RestAssuredRestDocumentation.document; + +import com.woowacourse.acceptance.AcceptanceTest; +import io.restassured.RestAssured; +import java.time.LocalDate; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpStatus; + +public class GettingStudyDetailsAcceptanceTest extends AcceptanceTest { + + @DisplayName("스터디 요약 정보 외에 상세 정보를 포함하여 조회할 수 있다.") + @Test + public void getStudyDetails() { + LocalDate 지금 = LocalDate.now(); + long 리액트_스터디 = 디우가().로그인하고().리액트_스터디를() + .시작일자는(지금).모집종료일자는(지금.plusDays(4)).종료일자는(지금.plusDays(10)) + .태그는(우테코4기_태그_ID, FE_태그_ID, 리액트_태그_ID).모집인원은(5) + .생성한다(); + 짱구가().로그인하고().스터디에(리액트_스터디).참여한다(); + 그린론이().로그인하고().스터디에(리액트_스터디).참여한다(); + 베루스가().로그인하고().스터디에(리액트_스터디).참여한다(); + + RestAssured.given(spec).log().all() + .filter(document("studies/details")) + .pathParam("study-id", 리액트_스터디) + .when().log().all() + .get("/api/studies/{study-id}") + .then().log().all() + .statusCode(HttpStatus.OK.value()) + .body("id", not(empty())) + .body("title", is(리액트_스터디_제목)) + .body("excerpt", is(리액트_스터디_요약)) + .body("thumbnail", is(리액트_스터디_썸네일)) + .body("recruitmentStatus", is("RECRUITMENT_START")) + .body("description", is(리액트_스터디_설명)) + .body("currentMemberCount", is(4)) + .body("maxMemberCount", is(5)) + .body("enrollmentEndDate", is(지금.plusDays(4).toString())) + .body("startDate", is(지금.toString())) + .body("endDate", is(지금.plusDays(10).toString())) + .body("owner.id", is((int)디우_깃허브_ID)) + .body("owner.username", is(디우_이름)) + .body("owner.imageUrl", is(디우_이미지_URL)) + .body("owner.profileUrl", is(디우_프로필_URL)) + .body("members.id", not(empty())) + .body("members.username", contains(짱구_이름, 그린론_이름, 베루스_이름)) + .body("members.imageUrl", contains(짱구_이미지_URL, 그린론_이미지_URL, 베루스_이미지_URL)) + .body("members.profileUrl", contains(짱구_프로필_URL, 그린론_프로필_URL, 베루스_프로필_URL)) + .body("tags.id", not(empty())) + .body("tags.name", contains("4기", "FE", "React")); + } + + @DisplayName("선택 데이터가 없는 스터디 세부사항을 조회한다.") + @Test + public void getNotHasOptionalDataStudyDetails() { + LocalDate 지금 = LocalDate.now(); + final long 알고리즘_스터디 = 베루스가().로그인하고().알고리즘_스터디를().시작일자는(지금).생성한다(); + + RestAssured.given().log().all() + .pathParam("study-id", 알고리즘_스터디) + .when().log().all() + .get("/api/studies/{study-id}") + .then().log().all() + .statusCode(HttpStatus.OK.value()) + .body("id", is((int)알고리즘_스터디)) + .body("title", is(알고리즘_스터디_제목)) + .body("excerpt", is(알고리즘_스터디_요약)) + .body("thumbnail", is(알고리즘_스터디_썸네일)) + .body("recruitmentStatus", is("RECRUITMENT_START")) + .body("description", is(알고리즘_스터디_설명)) + .body("currentMemberCount", is(1)) + .body("maxMemberCount", is(nullValue())) + .body("enrollmentEndDate", is(nullValue())) + .body("startDate", is(지금.toString())) + .body("endDate", is(nullValue())) + .body("owner.id", is((int)베루스_깃허브_ID)) + .body("owner.username", is(베루스_이름)) + .body("owner.imageUrl", is(베루스_이미지_URL)) + .body("owner.profileUrl", is(베루스_프로필_URL)) + .body("members", is(empty())) + .body("tags", is(empty())); + } +} diff --git a/backend/src/test/java/com/woowacourse/acceptance/test/study/ParticipationStudyAcceptanceTest.java b/backend/src/test/java/com/woowacourse/acceptance/test/study/ParticipationStudyAcceptanceTest.java new file mode 100644 index 000000000..f9009eacc --- /dev/null +++ b/backend/src/test/java/com/woowacourse/acceptance/test/study/ParticipationStudyAcceptanceTest.java @@ -0,0 +1,36 @@ +package com.woowacourse.acceptance.test.study; + +import static com.woowacourse.acceptance.steps.LoginSteps.그린론이; +import static com.woowacourse.acceptance.steps.LoginSteps.디우가; +import static org.springframework.http.HttpHeaders.AUTHORIZATION; +import static org.springframework.http.HttpHeaders.CONTENT_TYPE; +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; +import static org.springframework.restdocs.restassured3.RestAssuredRestDocumentation.document; + +import com.woowacourse.acceptance.AcceptanceTest; +import io.restassured.RestAssured; +import java.time.LocalDate; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpStatus; + +public class ParticipationStudyAcceptanceTest extends AcceptanceTest { + + @DisplayName("아직 스터디에 가입되지 않은 회원은 스터디에 참여가 가능하다.") + @Test + public void participateStudy() { + LocalDate 지금 = LocalDate.now(); + long 자바_스터디_ID = 그린론이().로그인하고().자바_스터디를().시작일자는(지금).모집인원은(10).생성한다(); + String token = 디우가().로그인한다(); + + RestAssured.given(spec).log().all() + .filter(document("studies/participant")) + .header(CONTENT_TYPE, APPLICATION_JSON_VALUE) + .header(AUTHORIZATION, token) + .pathParam("study-id", 자바_스터디_ID) + .when().log().all() + .post("/api/studies/{study-id}") + .then().log().all() + .statusCode(HttpStatus.OK.value()); + } +} diff --git a/backend/src/test/java/com/woowacourse/acceptance/study/SearchingStudiesAcceptanceTest.java b/backend/src/test/java/com/woowacourse/acceptance/test/study/SearchingStudiesAcceptanceTest.java similarity index 69% rename from backend/src/test/java/com/woowacourse/acceptance/study/SearchingStudiesAcceptanceTest.java rename to backend/src/test/java/com/woowacourse/acceptance/test/study/SearchingStudiesAcceptanceTest.java index 7fbdaca72..ec9819552 100644 --- a/backend/src/test/java/com/woowacourse/acceptance/study/SearchingStudiesAcceptanceTest.java +++ b/backend/src/test/java/com/woowacourse/acceptance/test/study/SearchingStudiesAcceptanceTest.java @@ -1,5 +1,11 @@ -package com.woowacourse.acceptance.study; - +package com.woowacourse.acceptance.test.study; + +import static com.woowacourse.acceptance.fixture.TagFixtures.BE_태그_ID; +import static com.woowacourse.acceptance.fixture.TagFixtures.FE_태그_ID; +import static com.woowacourse.acceptance.fixture.TagFixtures.리액트_태그_ID; +import static com.woowacourse.acceptance.fixture.TagFixtures.우테코4기_태그_ID; +import static com.woowacourse.acceptance.fixture.TagFixtures.자바_태그_ID; +import static com.woowacourse.acceptance.steps.LoginSteps.짱구가; import static org.hamcrest.Matchers.blankOrNullString; import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.empty; @@ -10,93 +16,45 @@ import static org.springframework.restdocs.restassured3.RestAssuredRestDocumentation.document; import com.woowacourse.acceptance.AcceptanceTest; -import com.woowacourse.moamoa.auth.service.oauthclient.response.GithubProfileResponse; -import com.woowacourse.moamoa.study.service.request.CreatingStudyRequest; import io.restassured.RestAssured; import java.time.LocalDate; -import java.time.LocalDateTime; -import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; -import org.springframework.jdbc.core.JdbcTemplate; @DisplayName("키워드 검색 인수 테스트") public class SearchingStudiesAcceptanceTest extends AcceptanceTest { - @Autowired - private JdbcTemplate jdbcTemplate; - private long javaStudyId; - private long reactStudyId; - private long javascriptStudyId; - private long httpStudyId; - private long algorithmStudyId; - private long linuxStudyId; - @BeforeEach - void initDataBase() { - final String token = getBearerTokenBySignInOrUp(new GithubProfileResponse(1L, "jjanggu", "https://image", "github.com")); - getBearerTokenBySignInOrUp(new GithubProfileResponse(2L, "greenlawn", "https://image", "github.com")); - getBearerTokenBySignInOrUp(new GithubProfileResponse(3L, "dwoo", "https://image", "github.com")); - getBearerTokenBySignInOrUp(new GithubProfileResponse(4L, "verus", "https://image", "github.com")); - - jdbcTemplate.update("INSERT INTO category(id, name) VALUES (1, 'generation')"); - jdbcTemplate.update("INSERT INTO category(id, name) VALUES (2, 'area')"); - jdbcTemplate.update("INSERT INTO category(id, name) VALUES (3, 'subject')"); - - jdbcTemplate.update("INSERT INTO tag(id, name, description, category_id) VALUES (1, 'Java', '자바', 3)"); - jdbcTemplate.update("INSERT INTO tag(id, name, description, category_id) VALUES (2, '4기', '우테코4기', 1)"); - jdbcTemplate.update("INSERT INTO tag(id, name, description, category_id) VALUES (3, 'BE', '백엔드', 2)"); - jdbcTemplate.update("INSERT INTO tag(id, name, description, category_id) VALUES (4, 'FE', '프론트엔드', 2)"); - jdbcTemplate.update("INSERT INTO tag(id, name, description, category_id) VALUES (5, 'React', '리액트', 3)"); - - final LocalDateTime now = LocalDateTime.now(); + void setUp() { + LocalDate 지금 = LocalDate.now(); - CreatingStudyRequest javaRequest = CreatingStudyRequest.builder() - .title("Java 스터디").excerpt("자바 설명").thumbnail("java thumbnail") - .description("그린론의 우당탕탕 자바 스터디입니다.").startDate(LocalDate.now().plusDays(1)) - .tagIds(List.of(1L, 2L, 3L)) - .build(); - javaStudyId = createStudy(token, javaRequest); + 짱구가().로그인하고().자바_스터디를() + .시작일자는(지금).태그는(자바_태그_ID, 우테코4기_태그_ID, BE_태그_ID) + .생성한다(); - CreatingStudyRequest reactRequest = CreatingStudyRequest.builder() - .title("React 스터디").excerpt("리액트 설명").thumbnail("react thumbnail") - .description("디우의 뤼액트 스터디입니다.").startDate(LocalDate.now().plusDays(2)) - .tagIds(List.of(2L, 4L, 5L)) - .build(); - reactStudyId = createStudy(token, reactRequest); + 짱구가().로그인하고().리액트_스터디를() + .시작일자는(지금).태그는(우테코4기_태그_ID, FE_태그_ID, 리액트_태그_ID) + .생성한다(); - CreatingStudyRequest javascriptRequest = CreatingStudyRequest.builder() - .title("javaScript 스터디").excerpt("자바스크립트 설명").thumbnail("javascript thumbnail") - .description("그린론의 자바스크립트 접해보기").startDate(LocalDate.now().plusDays(3)) - .tagIds(List.of(2L, 4L)) - .build(); - javascriptStudyId = createStudy(token, javascriptRequest); + 짱구가().로그인하고().자바스크립트_스터디를() + .시작일자는(지금).태그는(우테코4기_태그_ID, FE_태그_ID) + .생성한다(); - CreatingStudyRequest httpRequest = CreatingStudyRequest.builder() - .title("HTTP 스터디").excerpt("HTTP 설명").thumbnail("http thumbnail") - .description("디우의 HTTP 정복하기").startDate(LocalDate.now().plusDays(3)) - .tagIds(List.of(2L, 3L)) - .build(); - httpStudyId = createStudy(token, httpRequest); + 짱구가().로그인하고().HTTP_스터디를() + .시작일자는(지금).태그는(우테코4기_태그_ID, BE_태그_ID) + .생성한다(); - CreatingStudyRequest algorithmRequest = CreatingStudyRequest.builder() - .title("알고리즘 스터디").excerpt("알고리즘 설명").thumbnail("algorithm thumbnail") - .description("알고리즘을 TDD로 풀자의 베루스입니다.").startDate(LocalDate.now().plusDays(2)) - .tagIds(List.of()) - .build(); - algorithmStudyId = createStudy(token, algorithmRequest); + 짱구가().로그인하고().알고리즘_스터디를() + .시작일자는(지금) + .생성한다(); - CreatingStudyRequest linuxRequest = CreatingStudyRequest.builder() - .title("Linux 스터디").excerpt("리눅스 설명").thumbnail("linux thumbnail") - .description("Linux를 공부하자의 베루스입니다.").startDate(LocalDate.now().plusDays(2)) - .tagIds(List.of()) - .build(); - linuxStudyId = createStudy(token, linuxRequest); + 짱구가().로그인하고().리눅스_스터디를() + .시작일자는(지금) + .생성한다(); } @DisplayName("잘못된 페이징 정보로 목록을 검색시 400에러를 응답한다.") diff --git a/backend/src/test/java/com/woowacourse/acceptance/tag/TagAcceptanceTest.java b/backend/src/test/java/com/woowacourse/acceptance/test/tag/TagAcceptanceTest.java similarity index 79% rename from backend/src/test/java/com/woowacourse/acceptance/tag/TagAcceptanceTest.java rename to backend/src/test/java/com/woowacourse/acceptance/test/tag/TagAcceptanceTest.java index b70fcbd59..a7bb614ca 100644 --- a/backend/src/test/java/com/woowacourse/acceptance/tag/TagAcceptanceTest.java +++ b/backend/src/test/java/com/woowacourse/acceptance/test/tag/TagAcceptanceTest.java @@ -1,4 +1,4 @@ -package com.woowacourse.acceptance.tag; +package com.woowacourse.acceptance.test.tag; import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.empty; @@ -17,22 +17,6 @@ public class TagAcceptanceTest extends AcceptanceTest { - @Autowired - private JdbcTemplate jdbcTemplate; - - @BeforeEach - void initDataBase() { - jdbcTemplate.update("INSERT INTO category(id, name) VALUES (1, 'generation')"); - jdbcTemplate.update("INSERT INTO category(id, name) VALUES (2, 'area')"); - jdbcTemplate.update("INSERT INTO category(id, name) VALUES (3, 'subject')"); - - jdbcTemplate.update("INSERT INTO tag(id, name, description, category_id) VALUES (1, 'Java', '자바', 3)"); - jdbcTemplate.update("INSERT INTO tag(id, name, description, category_id) VALUES (2, '4기', '우테코4기', 1)"); - jdbcTemplate.update("INSERT INTO tag(id, name, description, category_id) VALUES (3, 'BE', '백엔드', 2)"); - jdbcTemplate.update("INSERT INTO tag(id, name, description, category_id) VALUES (4, 'FE', '프론트엔드', 2)"); - jdbcTemplate.update("INSERT INTO tag(id, name, description, category_id) VALUES (5, 'React', '리액트', 3)"); - } - @DisplayName("전체 태그 목록을 조회한다.") @Test void getAllFilters() { diff --git a/backend/src/test/java/com/woowacourse/moamoa/member/domain/repository/MemberRepositoryTest.java b/backend/src/test/java/com/woowacourse/moamoa/member/domain/repository/MemberRepositoryTest.java index 9628fc20b..9ae24152a 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/member/domain/repository/MemberRepositoryTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/member/domain/repository/MemberRepositoryTest.java @@ -17,9 +17,6 @@ @RepositoryTest class MemberRepositoryTest { - @Autowired - private JdbcTemplate jdbcTemplate; - @Autowired private MemberRepository memberRepository; diff --git a/backend/src/test/java/com/woowacourse/moamoa/member/query/MemberDaoTest.java b/backend/src/test/java/com/woowacourse/moamoa/member/query/MemberDaoTest.java deleted file mode 100644 index 3b76a122e..000000000 --- a/backend/src/test/java/com/woowacourse/moamoa/member/query/MemberDaoTest.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.woowacourse.moamoa.member.query; - -import com.woowacourse.moamoa.common.RepositoryTest; -import org.junit.jupiter.api.BeforeEach; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.jdbc.core.JdbcTemplate; - -@RepositoryTest -class MemberDaoTest { - - @Autowired - private MemberDao memberDao; - - @Autowired - private JdbcTemplate jdbcTemplate; - - @BeforeEach - void setUp() { - jdbcTemplate.update("INSERT INTO member(id, github_id, username, image_url, profile_url) VALUES (1, 1, 'jjanggu', 'https://image', 'github.com')"); - jdbcTemplate.update("INSERT INTO member(id, github_id, username, image_url, profile_url) VALUES (2, 2, 'greenlawn', 'https://image', 'github.com')"); - jdbcTemplate.update("INSERT INTO study(id, title, excerpt, thumbnail, status, description, current_member_count, max_member_count, created_date, start_date, owner_id) VALUES (1, 'Java 스터디', '자바 설명', 'java thumbnail', 'OPEN', '그린론의 우당탕탕 자바 스터디입니다.', 3, 10, '2021-11-08T11:58:20.551705', '2021-12-08T11:58:20.657123', 2)"); - jdbcTemplate.update("INSERT INTO study_member(study_id, member_id) VALUES (1, 1)"); - } -} diff --git a/backend/src/test/java/com/woowacourse/moamoa/review/controller/ReviewControllerTest.java b/backend/src/test/java/com/woowacourse/moamoa/review/controller/ReviewControllerTest.java index 04f07d7ee..9413b0054 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/review/controller/ReviewControllerTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/review/controller/ReviewControllerTest.java @@ -38,9 +38,6 @@ public class ReviewControllerTest { @Autowired private StudyRepository studyRepository; - @Autowired - private JdbcTemplate jdbcTemplate; - @Autowired private EntityManager entityManager; diff --git a/backend/src/test/java/com/woowacourse/moamoa/review/controller/SearchingReviewControllerTest.java b/backend/src/test/java/com/woowacourse/moamoa/review/controller/SearchingReviewControllerTest.java index 6970dcb5a..753be2067 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/review/controller/SearchingReviewControllerTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/review/controller/SearchingReviewControllerTest.java @@ -47,9 +47,6 @@ class SearchingReviewControllerTest { @Autowired private StudyRepository studyRepository; - @Autowired - private JdbcTemplate jdbcTemplate; - @Autowired private EntityManager entityManager; @@ -116,12 +113,7 @@ void setUp() { LocalDate.now(), "리뷰 내용3"); final ReviewResponse 리뷰_내용4 = new ReviewResponse(javaReviewId4, new WriterResponse(VERUS), LocalDate.now(), LocalDate.now(), "리뷰 내용4"); - javaReviews = List.of( - 리뷰_내용4, - 리뷰_내용3, - 리뷰_내용2, - 리뷰_내용1 - ); + javaReviews = List.of(리뷰_내용4, 리뷰_내용3, 리뷰_내용2, 리뷰_내용1); entityManager.flush(); entityManager.clear(); diff --git a/backend/src/test/java/com/woowacourse/moamoa/study/controller/MyStudyControllerTest.java b/backend/src/test/java/com/woowacourse/moamoa/study/controller/MyStudyControllerTest.java index 0c751ea63..9f08fc79f 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/study/controller/MyStudyControllerTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/study/controller/MyStudyControllerTest.java @@ -67,16 +67,6 @@ void setUp() { jdbcTemplate.update("INSERT INTO study(id, title, excerpt, thumbnail, recruitment_status, study_status, description, current_member_count, created_at, owner_id, start_date, enrollment_end_date, end_date) " + "VALUES (6, 'Linux 스터디', '리눅스 설명', 'linux thumbnail', 'RECRUITMENT_END', 'PREPARE', 'Linux를 공부하자의 베루스입니다.', 1, '" + now + "', 4, '2021-12-06', '2021-12-07', '2022-01-07')"); - jdbcTemplate.update("INSERT INTO category(id, name) VALUES (1, 'generation')"); - jdbcTemplate.update("INSERT INTO category(id, name) VALUES (2, 'area')"); - jdbcTemplate.update("INSERT INTO category(id, name) VALUES (3, 'subject')"); - - jdbcTemplate.update("INSERT INTO tag(id, name, description, category_id) VALUES (1, 'Java', '자바', 3)"); - jdbcTemplate.update("INSERT INTO tag(id, name, description, category_id) VALUES (2, '4기', '우테코4기', 1)"); - jdbcTemplate.update("INSERT INTO tag(id, name, description, category_id) VALUES (3, 'BE', '백엔드', 2)"); - jdbcTemplate.update("INSERT INTO tag(id, name, description, category_id) VALUES (4, 'FE', '프론트엔드', 2)"); - jdbcTemplate.update("INSERT INTO tag(id, name, description, category_id) VALUES (5, 'React', '리액트', 3)"); - jdbcTemplate.update("INSERT INTO study_tag(study_id, tag_id) VALUES (1, 1)"); jdbcTemplate.update("INSERT INTO study_tag(study_id, tag_id) VALUES (1, 2)"); jdbcTemplate.update("INSERT INTO study_tag(study_id, tag_id) VALUES (1, 3)"); diff --git a/backend/src/test/java/com/woowacourse/moamoa/study/controller/SearchingStudyControllerTest.java b/backend/src/test/java/com/woowacourse/moamoa/study/controller/SearchingStudyControllerTest.java index 8983aef32..8d53effb9 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/study/controller/SearchingStudyControllerTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/study/controller/SearchingStudyControllerTest.java @@ -60,9 +60,6 @@ public class SearchingStudyControllerTest { @Autowired private EntityManager entityManager; - @Autowired - private JdbcTemplate jdbcTemplate; - private Long javaStudyId; private Long reactStudyId; private Long javaScriptId; @@ -76,16 +73,6 @@ public class SearchingStudyControllerTest { @BeforeEach void initDataBase() { - jdbcTemplate.update("INSERT INTO category(id, name) VALUES (1, 'generation')"); - jdbcTemplate.update("INSERT INTO category(id, name) VALUES (2, 'area')"); - jdbcTemplate.update("INSERT INTO category(id, name) VALUES (3, 'subject')"); - - jdbcTemplate.update("INSERT INTO tag(id, name, description, category_id) VALUES (1, 'Java', '자바', 3)"); - jdbcTemplate.update("INSERT INTO tag(id, name, description, category_id) VALUES (2, '4기', '우테코4기', 1)"); - jdbcTemplate.update("INSERT INTO tag(id, name, description, category_id) VALUES (3, 'BE', '백엔드', 2)"); - jdbcTemplate.update("INSERT INTO tag(id, name, description, category_id) VALUES (4, 'FE', '프론트엔드', 2)"); - jdbcTemplate.update("INSERT INTO tag(id, name, description, category_id) VALUES (5, 'React', '리액트', 3)"); - jjanggu = memberRepository.save(new Member(1L, "jjanggu", "https://image", "github.com")); greenlawn = memberRepository.save(new Member(2L, "greenlawn", "https://image", "github.com")); dwoo = memberRepository.save(new Member(3L, "dwoo", "https://image", "github.com")); diff --git a/backend/src/test/java/com/woowacourse/moamoa/study/query/MyStudyDaoTest.java b/backend/src/test/java/com/woowacourse/moamoa/study/query/MyStudyDaoTest.java index 38de2a52f..b15ea3245 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/study/query/MyStudyDaoTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/study/query/MyStudyDaoTest.java @@ -49,16 +49,6 @@ void initDataBase() { jdbcTemplate.update("INSERT INTO study(id, title, excerpt, thumbnail, recruitment_status, study_status, description, current_member_count, created_at, owner_id, start_date, enrollment_end_date, end_date) " + "VALUES (6, 'Linux 스터디', '리눅스 설명', 'linux thumbnail', 'RECRUITMENT_END', 'PREPARE', 'Linux를 공부하자의 베루스입니다.', 1, '" + now + "', 4, '2021-12-06', '2021-12-07', '2022-01-07')"); - jdbcTemplate.update("INSERT INTO category(id, name) VALUES (1, 'generation')"); - jdbcTemplate.update("INSERT INTO category(id, name) VALUES (2, 'area')"); - jdbcTemplate.update("INSERT INTO category(id, name) VALUES (3, 'subject')"); - - jdbcTemplate.update("INSERT INTO tag(id, name, description, category_id) VALUES (1, 'Java', '자바', 3)"); - jdbcTemplate.update("INSERT INTO tag(id, name, description, category_id) VALUES (2, '4기', '우테코4기', 1)"); - jdbcTemplate.update("INSERT INTO tag(id, name, description, category_id) VALUES (3, 'BE', '백엔드', 2)"); - jdbcTemplate.update("INSERT INTO tag(id, name, description, category_id) VALUES (4, 'FE', '프론트엔드', 2)"); - jdbcTemplate.update("INSERT INTO tag(id, name, description, category_id) VALUES (5, 'React', '리액트', 3)"); - jdbcTemplate.update("INSERT INTO study_tag(study_id, tag_id) VALUES (1, 1)"); jdbcTemplate.update("INSERT INTO study_tag(study_id, tag_id) VALUES (1, 2)"); jdbcTemplate.update("INSERT INTO study_tag(study_id, tag_id) VALUES (1, 3)"); diff --git a/backend/src/test/java/com/woowacourse/moamoa/study/query/StudyDetailsDaoTest.java b/backend/src/test/java/com/woowacourse/moamoa/study/query/StudyDetailsDaoTest.java index 03fda4415..c0cf96dd5 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/study/query/StudyDetailsDaoTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/study/query/StudyDetailsDaoTest.java @@ -45,16 +45,6 @@ void initDataBase() { jdbcTemplate.update("INSERT INTO study(id, title, excerpt, thumbnail, recruitment_status, study_status, description, current_member_count, created_at, owner_id, start_date, enrollment_end_date, end_date) " + "VALUES (6, 'Linux 스터디', '리눅스 설명', 'linux thumbnail', 'RECRUITMENT_END', 'PREPARE', 'Linux를 공부하자의 베루스입니다.', 1, '" + now + "', 4, '2021-12-06', '2021-12-07', '2022-01-07')"); - jdbcTemplate.update("INSERT INTO category(id, name) VALUES (1, 'generation')"); - jdbcTemplate.update("INSERT INTO category(id, name) VALUES (2, 'area')"); - jdbcTemplate.update("INSERT INTO category(id, name) VALUES (3, 'subject')"); - - jdbcTemplate.update("INSERT INTO tag(id, name, description, category_id) VALUES (1, 'Java', '자바', 3)"); - jdbcTemplate.update("INSERT INTO tag(id, name, description, category_id) VALUES (2, '4기', '우테코4기', 1)"); - jdbcTemplate.update("INSERT INTO tag(id, name, description, category_id) VALUES (3, 'BE', '백엔드', 2)"); - jdbcTemplate.update("INSERT INTO tag(id, name, description, category_id) VALUES (4, 'FE', '프론트엔드', 2)"); - jdbcTemplate.update("INSERT INTO tag(id, name, description, category_id) VALUES (5, 'React', '리액트', 3)"); - jdbcTemplate.update("INSERT INTO study_tag(study_id, tag_id) VALUES (1, 1)"); jdbcTemplate.update("INSERT INTO study_tag(study_id, tag_id) VALUES (1, 2)"); jdbcTemplate.update("INSERT INTO study_tag(study_id, tag_id) VALUES (1, 3)"); diff --git a/backend/src/test/java/com/woowacourse/moamoa/study/query/StudySummaryDaoTest.java b/backend/src/test/java/com/woowacourse/moamoa/study/query/StudySummaryDaoTest.java index 858901c8f..a4d9a7ed2 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/study/query/StudySummaryDaoTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/study/query/StudySummaryDaoTest.java @@ -68,16 +68,6 @@ public class StudySummaryDaoTest { @BeforeEach void initDataBase() { - jdbcTemplate.update("INSERT INTO category(id, name) VALUES (1, 'generation')"); - jdbcTemplate.update("INSERT INTO category(id, name) VALUES (2, 'area')"); - jdbcTemplate.update("INSERT INTO category(id, name) VALUES (3, 'subject')"); - - jdbcTemplate.update("INSERT INTO tag(id, name, description, category_id) VALUES (1, 'Java', '자바', 3)"); - jdbcTemplate.update("INSERT INTO tag(id, name, description, category_id) VALUES (2, '4기', '우테코4기', 1)"); - jdbcTemplate.update("INSERT INTO tag(id, name, description, category_id) VALUES (3, 'BE', '백엔드', 2)"); - jdbcTemplate.update("INSERT INTO tag(id, name, description, category_id) VALUES (4, 'FE', '프론트엔드', 2)"); - jdbcTemplate.update("INSERT INTO tag(id, name, description, category_id) VALUES (5, 'React', '리액트', 3)"); - jjanggu = memberRepository.save(new Member(1L, "jjanggu", "https://image", "github.com")); greenlawn = memberRepository.save(new Member(2L, "greenlawn", "https://image", "github.com")); dwoo = memberRepository.save(new Member(3L, "dwoo", "https://image", "github.com")); diff --git a/backend/src/test/java/com/woowacourse/moamoa/study/service/MyStudyServiceTest.java b/backend/src/test/java/com/woowacourse/moamoa/study/service/MyStudyServiceTest.java index e9d0234f4..75366b968 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/study/service/MyStudyServiceTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/study/service/MyStudyServiceTest.java @@ -70,16 +70,6 @@ void setUp() { jdbcTemplate.update("INSERT INTO study(id, title, excerpt, thumbnail, recruitment_status, study_status, description, current_member_count, max_member_count, created_at, owner_id, start_date, enrollment_end_date, end_date) " + "VALUES (7, 'OS 스터디', 'OS 설명', 'os thumbnail', 'RECRUITMENT_END', 'PREPARE', 'OS를 공부하자의 베루스입니다.', 1, 6, '" + now + "', 4, '2021-12-06', '2021-12-07', '2022-01-07')"); - jdbcTemplate.update("INSERT INTO category(id, name) VALUES (1, 'generation')"); - jdbcTemplate.update("INSERT INTO category(id, name) VALUES (2, 'area')"); - jdbcTemplate.update("INSERT INTO category(id, name) VALUES (3, 'subject')"); - - jdbcTemplate.update("INSERT INTO tag(id, name, description, category_id) VALUES (1, 'Java', '자바', 3)"); - jdbcTemplate.update("INSERT INTO tag(id, name, description, category_id) VALUES (2, '4기', '우테코4기', 1)"); - jdbcTemplate.update("INSERT INTO tag(id, name, description, category_id) VALUES (3, 'BE', '백엔드', 2)"); - jdbcTemplate.update("INSERT INTO tag(id, name, description, category_id) VALUES (4, 'FE', '프론트엔드', 2)"); - jdbcTemplate.update("INSERT INTO tag(id, name, description, category_id) VALUES (5, 'React', '리액트', 3)"); - jdbcTemplate.update("INSERT INTO study_tag(study_id, tag_id) VALUES (1, 1)"); jdbcTemplate.update("INSERT INTO study_tag(study_id, tag_id) VALUES (1, 2)"); jdbcTemplate.update("INSERT INTO study_tag(study_id, tag_id) VALUES (1, 3)"); diff --git a/backend/src/test/java/com/woowacourse/moamoa/tag/controller/SearchingTagControllerTest.java b/backend/src/test/java/com/woowacourse/moamoa/tag/controller/SearchingTagControllerTest.java index ce063771b..881333ec0 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/tag/controller/SearchingTagControllerTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/tag/controller/SearchingTagControllerTest.java @@ -25,22 +25,6 @@ class SearchingTagControllerTest { private SearchingTagController searchingTagController; - @Autowired - private JdbcTemplate jdbcTemplate; - - @BeforeEach - void initDataBase() { - jdbcTemplate.update("INSERT INTO category(id, name) VALUES (1, 'generation')"); - jdbcTemplate.update("INSERT INTO category(id, name) VALUES (2, 'area')"); - jdbcTemplate.update("INSERT INTO category(id, name) VALUES (3, 'subject')"); - - jdbcTemplate.update("INSERT INTO tag(id, name, description, category_id) VALUES (1, 'Java', '자바', 3)"); - jdbcTemplate.update("INSERT INTO tag(id, name, description, category_id) VALUES (2, '4기', '우테코4기', 1)"); - jdbcTemplate.update("INSERT INTO tag(id, name, description, category_id) VALUES (3, 'BE', '백엔드', 2)"); - jdbcTemplate.update("INSERT INTO tag(id, name, description, category_id) VALUES (4, 'FE', '프론트엔드', 2)"); - jdbcTemplate.update("INSERT INTO tag(id, name, description, category_id) VALUES (5, 'React', '리액트', 3)"); - } - @BeforeEach void setUp() { searchingTagController = new SearchingTagController(new SearchingTagService(tagDao)); diff --git a/backend/src/test/java/com/woowacourse/moamoa/tag/query/TagDaoTest.java b/backend/src/test/java/com/woowacourse/moamoa/tag/query/TagDaoTest.java index 3e8697625..91e0518ea 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/tag/query/TagDaoTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/tag/query/TagDaoTest.java @@ -45,16 +45,6 @@ void initDataBase() { jdbcTemplate.update("INSERT INTO study(id, title, excerpt, thumbnail, recruitment_status, study_status, description, current_member_count, created_at, owner_id, start_date, enrollment_end_date, end_date) " + "VALUES (6, 'Linux 스터디', '리눅스 설명', 'linux thumbnail', 'RECRUITMENT_END', 'PREPARE', 'Linux를 공부하자의 베루스입니다.', 1, '" + now + "', 4, '2021-12-06', '2021-12-07', '2022-01-07')"); - jdbcTemplate.update("INSERT INTO category(id, name) VALUES (1, 'generation')"); - jdbcTemplate.update("INSERT INTO category(id, name) VALUES (2, 'area')"); - jdbcTemplate.update("INSERT INTO category(id, name) VALUES (3, 'subject')"); - - jdbcTemplate.update("INSERT INTO tag(id, name, description, category_id) VALUES (1, 'Java', '자바', 3)"); - jdbcTemplate.update("INSERT INTO tag(id, name, description, category_id) VALUES (2, '4기', '우테코4기', 1)"); - jdbcTemplate.update("INSERT INTO tag(id, name, description, category_id) VALUES (3, 'BE', '백엔드', 2)"); - jdbcTemplate.update("INSERT INTO tag(id, name, description, category_id) VALUES (4, 'FE', '프론트엔드', 2)"); - jdbcTemplate.update("INSERT INTO tag(id, name, description, category_id) VALUES (5, 'React', '리액트', 3)"); - jdbcTemplate.update("INSERT INTO study_tag(study_id, tag_id) VALUES (1, 1)"); jdbcTemplate.update("INSERT INTO study_tag(study_id, tag_id) VALUES (1, 2)"); jdbcTemplate.update("INSERT INTO study_tag(study_id, tag_id) VALUES (1, 3)"); diff --git a/backend/src/test/resources/data.sql b/backend/src/test/resources/data.sql new file mode 100644 index 000000000..beee99bd8 --- /dev/null +++ b/backend/src/test/resources/data.sql @@ -0,0 +1,9 @@ +INSERT INTO category(id, name) VALUES (1, 'generation'); +INSERT INTO category(id, name) VALUES (2, 'area'); +INSERT INTO category(id, name) VALUES (3, 'subject'); + +INSERT INTO tag(id, name, description, category_id) VALUES (1, 'Java', '자바', 3); +INSERT INTO tag(id, name, description, category_id) VALUES (2, '4기', '우테코4기', 1); +INSERT INTO tag(id, name, description, category_id) VALUES (3, 'BE', '백엔드', 2); +INSERT INTO tag(id, name, description, category_id) VALUES (4, 'FE', '프론트엔드', 2); +INSERT INTO tag(id, name, description, category_id) VALUES (5, 'React', '리액트', 3); From 3d578069750f8220ce7e566449596ffc10f2bdf9 Mon Sep 17 00:00:00 2001 From: jaeseo yoo Date: Tue, 9 Aug 2022 14:14:38 +0900 Subject: [PATCH 04/51] =?UTF-8?q?[BE]=20issue223:=20=08=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=B5=9C=EC=A0=81=ED=99=94=20?= =?UTF-8?q?(#224)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: 제약 조건 임시 제거 및 데이터 삭제 TRUNCATE로 변경 * feat: 인증 exception 추가 * refactor: WebMvcTest 테스트 환경 통일 * refactor: 불필요한 SpringBootTest 환경 제거 * refactor: AuthentiactionRequestMatcher 빈 실제 객체 사용 --- .../common/advice/CommonControllerAdvice.java | 7 +- .../acceptance/AcceptanceTest.java | 27 ++----- .../com/woowacourse/moamoa/WebMVCTest.java | 75 +++++++++++++++++++ .../auth/controller/AuthControllerTest.java | 24 +----- .../AuthenticationArgumentResolverTest.java | 24 ++---- .../AuthenticationInterceptorTest.java | 20 +---- .../AuthenticationRequestMatcherTest.java | 15 +--- .../infrastructure/TokenProviderTest.java | 13 ++-- .../moamoa/auth/service/AuthServiceTest.java | 24 +----- .../moamoa/auth/service/OAuthClientTest.java | 12 +-- .../member/webmvc/MemberWebMvcTest.java | 26 +------ .../webmvc/BadRequestReviewWebMvcTest.java | 34 +-------- .../webmvc/UnauthorizedReviewWebMvcTest.java | 32 +------- .../BadRequestMyMemberRoleWebMvcTest.java | 33 ++------ .../webmvc/UnauthorizedMyStudyWebMvcTest.java | 23 +----- 15 files changed, 133 insertions(+), 256 deletions(-) create mode 100644 backend/src/test/java/com/woowacourse/moamoa/WebMVCTest.java diff --git a/backend/src/main/java/com/woowacourse/moamoa/common/advice/CommonControllerAdvice.java b/backend/src/main/java/com/woowacourse/moamoa/common/advice/CommonControllerAdvice.java index d94f489c5..0c888fbcb 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/common/advice/CommonControllerAdvice.java +++ b/backend/src/main/java/com/woowacourse/moamoa/common/advice/CommonControllerAdvice.java @@ -9,7 +9,7 @@ import com.woowacourse.moamoa.common.exception.UnauthorizedException; import com.woowacourse.moamoa.study.domain.exception.InvalidPeriodException; import com.woowacourse.moamoa.study.service.exception.FailureParticipationException; -import lombok.extern.slf4j.Slf4j; + import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.http.converter.HttpMessageNotReadableException; @@ -17,6 +17,9 @@ import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; +import io.jsonwebtoken.JwtException; +import lombok.extern.slf4j.Slf4j; + @RestControllerAdvice @Slf4j public class CommonControllerAdvice { @@ -40,7 +43,7 @@ public ResponseEntity handleBadRequest(final Exception e) { return ResponseEntity.badRequest().body(new ErrorResponse(e.getMessage())); } - @ExceptionHandler(UnauthorizedException.class) + @ExceptionHandler({UnauthorizedException.class, JwtException.class}) public ResponseEntity handleUnauthorized(final Exception e) { log.error("UnauthorizedException : {}", e.getMessage()); return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); diff --git a/backend/src/test/java/com/woowacourse/acceptance/AcceptanceTest.java b/backend/src/test/java/com/woowacourse/acceptance/AcceptanceTest.java index f702e883a..418d9f090 100644 --- a/backend/src/test/java/com/woowacourse/acceptance/AcceptanceTest.java +++ b/backend/src/test/java/com/woowacourse/acceptance/AcceptanceTest.java @@ -96,30 +96,17 @@ void mockingGithubServer() { @AfterEach void tearDown() { - jdbcTemplate.update("DELETE FROM study_tag"); - jdbcTemplate.update("DELETE FROM study_member"); - jdbcTemplate.update("DELETE FROM review"); - jdbcTemplate.update("DELETE FROM study"); - jdbcTemplate.update("DELETE FROM member"); - + jdbcTemplate.update("SET REFERENTIAL_INTEGRITY FALSE"); + jdbcTemplate.update("TRUNCATE TABLE member"); + jdbcTemplate.update("TRUNCATE TABLE study_tag"); + jdbcTemplate.update("TRUNCATE TABLE study_member"); + jdbcTemplate.update("TRUNCATE TABLE review"); + jdbcTemplate.update("TRUNCATE TABLE study"); + jdbcTemplate.update("SET REFERENTIAL_INTEGRITY TRUE"); jdbcTemplate.update("ALTER TABLE member AUTO_INCREMENT = 1"); jdbcTemplate.update("ALTER TABLE study AUTO_INCREMENT = 1"); } - protected String getBearerTokenBySignInOrUp(GithubProfileResponse response) { - final String authorizationCode = "Authorization Code"; - mockingGithubServer(authorizationCode, response); - final String token = RestAssured.given().log().all() - .param("code", authorizationCode) - .when() - .post("/api/login/token") - .then().log().all() - .statusCode(HttpStatus.OK.value()) - .extract().jsonPath().getString("token"); - mockServer.reset(); - return "Bearer " + token; - } - private void mockingGithubServer(String authorizationCode, GithubProfileResponse response) { try { mockingGithubServerForGetAccessToken(authorizationCode, Map.of("access_token", "access-token", diff --git a/backend/src/test/java/com/woowacourse/moamoa/WebMVCTest.java b/backend/src/test/java/com/woowacourse/moamoa/WebMVCTest.java new file mode 100644 index 000000000..f12467f10 --- /dev/null +++ b/backend/src/test/java/com/woowacourse/moamoa/WebMVCTest.java @@ -0,0 +1,75 @@ +package com.woowacourse.moamoa; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import com.woowacourse.moamoa.auth.config.AuthRequestMatchConfig; +import com.woowacourse.moamoa.auth.controller.AuthController; +import com.woowacourse.moamoa.auth.controller.AuthenticationInterceptor; +import com.woowacourse.moamoa.auth.controller.matcher.AuthenticationRequestMatcher; +import com.woowacourse.moamoa.auth.infrastructure.JwtTokenProvider; +import com.woowacourse.moamoa.auth.infrastructure.TokenProvider; +import com.woowacourse.moamoa.auth.service.AuthService; +import com.woowacourse.moamoa.member.controller.MemberController; +import com.woowacourse.moamoa.member.service.MemberService; +import com.woowacourse.moamoa.review.controller.ReviewController; +import com.woowacourse.moamoa.review.controller.SearchingReviewController; +import com.woowacourse.moamoa.review.service.ReviewService; +import com.woowacourse.moamoa.review.service.SearchingReviewService; +import com.woowacourse.moamoa.study.controller.MyStudyController; +import com.woowacourse.moamoa.study.service.MyStudyService; + +import javax.servlet.http.HttpServletRequest; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.web.context.request.NativeWebRequest; + +@WebMvcTest({ + MyStudyController.class, + ReviewController.class, + SearchingReviewController.class, + MemberController.class, + MyStudyController.class, + AuthController.class +}) +@Import({JwtTokenProvider.class, AuthRequestMatchConfig.class}) +public abstract class WebMVCTest { + + @Autowired + protected MockMvc mockMvc; + + @Autowired + protected TokenProvider tokenProvider; + + @Autowired + protected AuthRequestMatchConfig authRequestMatchConfig; + + @Autowired + protected AuthenticationInterceptor authenticationInterceptor; + + @Autowired + protected ObjectMapper objectMapper; + + @MockBean + protected ReviewService reviewService; + + @MockBean + protected SearchingReviewService searchingReviewService; + + @MockBean + protected MemberService memberService; + + @MockBean + protected AuthService authService; + + @MockBean + protected MyStudyService myStudyService; + + @MockBean + protected HttpServletRequest httpServletRequest; + + @MockBean + protected NativeWebRequest nativeWebRequest; +} diff --git a/backend/src/test/java/com/woowacourse/moamoa/auth/controller/AuthControllerTest.java b/backend/src/test/java/com/woowacourse/moamoa/auth/controller/AuthControllerTest.java index 398e8f8bd..47e22f18b 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/auth/controller/AuthControllerTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/auth/controller/AuthControllerTest.java @@ -6,31 +6,13 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import com.woowacourse.moamoa.auth.controller.matcher.AuthenticationRequestMatcher; -import com.woowacourse.moamoa.auth.infrastructure.TokenProvider; -import com.woowacourse.moamoa.auth.service.AuthService; +import com.woowacourse.moamoa.WebMVCTest; import com.woowacourse.moamoa.auth.service.response.TokenResponse; + import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.test.web.servlet.MockMvc; - -@WebMvcTest(controllers = {AuthController.class}) -public class AuthControllerTest { - - @Autowired - private MockMvc mockMvc; - - @MockBean - private AuthService authService; - - @MockBean - private TokenProvider tokenProvider; - @MockBean - private AuthenticationRequestMatcher authenticationRequestMatcher; +public class AuthControllerTest extends WebMVCTest { @DisplayName("Authorization 요청과 응답 형식을 확인한다.") @Test diff --git a/backend/src/test/java/com/woowacourse/moamoa/auth/controller/AuthenticationArgumentResolverTest.java b/backend/src/test/java/com/woowacourse/moamoa/auth/controller/AuthenticationArgumentResolverTest.java index d14cf029d..8f2141a96 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/auth/controller/AuthenticationArgumentResolverTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/auth/controller/AuthenticationArgumentResolverTest.java @@ -4,30 +4,20 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.BDDMockito.given; -import com.woowacourse.moamoa.auth.infrastructure.TokenProvider; -import com.woowacourse.moamoa.common.exception.UnauthorizedException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpHeaders; + import java.util.Collections; import java.util.List; import javax.servlet.http.HttpServletRequest; + import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.http.HttpHeaders; -import org.springframework.web.context.request.NativeWebRequest; - -@SpringBootTest -class AuthenticationArgumentResolverTest { - - @MockBean - private NativeWebRequest nativeWebRequest; - @MockBean - private HttpServletRequest httpServletRequest; +import com.woowacourse.moamoa.WebMVCTest; +import com.woowacourse.moamoa.common.exception.UnauthorizedException; - @Autowired - private TokenProvider tokenProvider; +class AuthenticationArgumentResolverTest extends WebMVCTest { @Autowired private AuthenticationArgumentResolver authenticationArgumentResolver; diff --git a/backend/src/test/java/com/woowacourse/moamoa/auth/controller/AuthenticationInterceptorTest.java b/backend/src/test/java/com/woowacourse/moamoa/auth/controller/AuthenticationInterceptorTest.java index d516b262f..6c68edcc9 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/auth/controller/AuthenticationInterceptorTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/auth/controller/AuthenticationInterceptorTest.java @@ -4,30 +4,18 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.BDDMockito.given; -import com.woowacourse.moamoa.auth.infrastructure.TokenProvider; -import com.woowacourse.moamoa.common.exception.UnauthorizedException; import java.util.Collections; import java.util.List; -import javax.servlet.http.HttpServletRequest; + import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; -@SpringBootTest -class AuthenticationInterceptorTest { - - @MockBean - private HttpServletRequest httpServletRequest; - - @Autowired - private AuthenticationInterceptor authenticationInterceptor; +import com.woowacourse.moamoa.WebMVCTest; +import com.woowacourse.moamoa.common.exception.UnauthorizedException; - @Autowired - private TokenProvider tokenProvider; +class AuthenticationInterceptorTest extends WebMVCTest { @DisplayName("Preflight 요청인지 확인한다.") @Test diff --git a/backend/src/test/java/com/woowacourse/moamoa/auth/controller/matcher/AuthenticationRequestMatcherTest.java b/backend/src/test/java/com/woowacourse/moamoa/auth/controller/matcher/AuthenticationRequestMatcherTest.java index a79f72ad3..d34a88c26 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/auth/controller/matcher/AuthenticationRequestMatcherTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/auth/controller/matcher/AuthenticationRequestMatcherTest.java @@ -3,29 +3,18 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.BDDMockito.given; -import com.woowacourse.moamoa.MoamoaApplication; -import javax.servlet.http.HttpServletRequest; +import com.woowacourse.moamoa.WebMVCTest; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; -import org.springframework.boot.test.mock.mockito.MockBean; -@SpringBootTest( - webEnvironment = WebEnvironment.RANDOM_PORT, - classes = {MoamoaApplication.class} -) -class AuthenticationRequestMatcherTest { +class AuthenticationRequestMatcherTest extends WebMVCTest { @Autowired private AuthenticationRequestMatcher authenticationRequestMatcher; - @MockBean - private HttpServletRequest httpServletRequest; - @DisplayName("사용자 인증이 필요한 요청인 경우") @ParameterizedTest @CsvSource(value = { diff --git a/backend/src/test/java/com/woowacourse/moamoa/auth/infrastructure/TokenProviderTest.java b/backend/src/test/java/com/woowacourse/moamoa/auth/infrastructure/TokenProviderTest.java index 7c996b924..a0eb0cb71 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/auth/infrastructure/TokenProviderTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/auth/infrastructure/TokenProviderTest.java @@ -2,28 +2,25 @@ import static org.assertj.core.api.Assertions.assertThat; +import com.woowacourse.moamoa.WebMVCTest; import com.woowacourse.moamoa.auth.config.AuthenticationExtractor; + import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.security.Keys; + import java.nio.charset.StandardCharsets; import java.util.Date; import javax.crypto.SecretKey; + import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; import org.springframework.mock.web.MockHttpServletRequest; -import org.springframework.test.context.jdbc.Sql; -@SpringBootTest -class TokenProviderTest { +class TokenProviderTest extends WebMVCTest { private static final String TEST_SECRET_KEY = "9d0bd354d2a68141d2ced83c26fe3fb72046783c19e7b727a45804d7d80c96a1541f9decbc3833519bd168ff7735d15a0e0737f40b20977bece9d8c0220425a1"; - @Autowired - private TokenProvider tokenProvider; - @DisplayName("만료된 토큰인지 확인한다") @Test void isExpiredToken() { diff --git a/backend/src/test/java/com/woowacourse/moamoa/auth/service/AuthServiceTest.java b/backend/src/test/java/com/woowacourse/moamoa/auth/service/AuthServiceTest.java index 67b42020e..6ca8156df 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/auth/service/AuthServiceTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/auth/service/AuthServiceTest.java @@ -6,31 +6,13 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import com.woowacourse.moamoa.auth.controller.AuthController; -import com.woowacourse.moamoa.auth.controller.matcher.AuthenticationRequestMatcher; -import com.woowacourse.moamoa.auth.infrastructure.TokenProvider; +import com.woowacourse.moamoa.WebMVCTest; import com.woowacourse.moamoa.auth.service.response.TokenResponse; + import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.test.web.servlet.MockMvc; - -@WebMvcTest(controllers = {AuthController.class}) -class AuthServiceTest { - - @Autowired - private MockMvc mockMvc; - - @MockBean - private AuthService authService; - - @MockBean - private TokenProvider tokenProvider; - @MockBean - private AuthenticationRequestMatcher authenticationRequestMatcher; +class AuthServiceTest extends WebMVCTest { @DisplayName("Authorization code를 받아서 token을 발급한다.") @Test diff --git a/backend/src/test/java/com/woowacourse/moamoa/auth/service/OAuthClientTest.java b/backend/src/test/java/com/woowacourse/moamoa/auth/service/OAuthClientTest.java index b3223eb63..a5280ca3a 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/auth/service/OAuthClientTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/auth/service/OAuthClientTest.java @@ -5,11 +5,6 @@ import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo; import static org.springframework.test.web.client.response.MockRestResponseCreators.withStatus; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.woowacourse.moamoa.MoamoaApplication; -import com.woowacourse.moamoa.auth.service.oauthclient.OAuthClient; -import com.woowacourse.moamoa.auth.service.oauthclient.response.GithubProfileResponse; import java.util.Map; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -20,10 +15,15 @@ import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; -import org.springframework.test.context.jdbc.Sql; import org.springframework.test.web.client.MockRestServiceServer; import org.springframework.web.client.RestTemplate; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.woowacourse.moamoa.MoamoaApplication; +import com.woowacourse.moamoa.auth.service.oauthclient.OAuthClient; +import com.woowacourse.moamoa.auth.service.oauthclient.response.GithubProfileResponse; + @SpringBootTest( webEnvironment = WebEnvironment.RANDOM_PORT, classes = {MoamoaApplication.class} diff --git a/backend/src/test/java/com/woowacourse/moamoa/member/webmvc/MemberWebMvcTest.java b/backend/src/test/java/com/woowacourse/moamoa/member/webmvc/MemberWebMvcTest.java index 3e10c2a95..881d6876a 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/member/webmvc/MemberWebMvcTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/member/webmvc/MemberWebMvcTest.java @@ -1,41 +1,21 @@ package com.woowacourse.moamoa.member.webmvc; -import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import com.woowacourse.moamoa.auth.config.AuthRequestMatchConfig; -import com.woowacourse.moamoa.auth.infrastructure.JwtTokenProvider; -import com.woowacourse.moamoa.auth.infrastructure.TokenProvider; -import com.woowacourse.moamoa.member.controller.MemberController; -import com.woowacourse.moamoa.member.service.MemberService; -import com.woowacourse.moamoa.member.service.exception.MemberNotFoundException; import org.apache.http.HttpHeaders; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.context.annotation.Import; -import org.springframework.test.web.servlet.MockMvc; - -@WebMvcTest(controllers = {MemberController.class}) -@Import({JwtTokenProvider.class, AuthRequestMatchConfig.class}) -public class MemberWebMvcTest { - @Autowired - MockMvc mockMvc; - - @MockBean - MemberService memberService; +import com.woowacourse.moamoa.WebMVCTest; +import com.woowacourse.moamoa.member.service.exception.MemberNotFoundException; - @Autowired - TokenProvider tokenProvider; +public class MemberWebMvcTest extends WebMVCTest { @DisplayName("잘못된 토큰 사용시 401 에러 반환") @ParameterizedTest diff --git a/backend/src/test/java/com/woowacourse/moamoa/review/webmvc/BadRequestReviewWebMvcTest.java b/backend/src/test/java/com/woowacourse/moamoa/review/webmvc/BadRequestReviewWebMvcTest.java index 9cc25dff6..f62241fad 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/review/webmvc/BadRequestReviewWebMvcTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/review/webmvc/BadRequestReviewWebMvcTest.java @@ -5,43 +5,15 @@ import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.woowacourse.moamoa.auth.config.AuthRequestMatchConfig; -import com.woowacourse.moamoa.auth.infrastructure.JwtTokenProvider; -import com.woowacourse.moamoa.auth.infrastructure.TokenProvider; -import com.woowacourse.moamoa.review.controller.ReviewController; -import com.woowacourse.moamoa.review.controller.SearchingReviewController; -import com.woowacourse.moamoa.review.service.ReviewService; -import com.woowacourse.moamoa.review.service.SearchingReviewService; +import com.woowacourse.moamoa.WebMVCTest; import com.woowacourse.moamoa.review.service.request.WriteReviewRequest; + import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.context.annotation.Import; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; -import org.springframework.test.web.servlet.MockMvc; - -@WebMvcTest(controllers = {ReviewController.class, SearchingReviewController.class}) -@Import({JwtTokenProvider.class, AuthRequestMatchConfig.class}) -public class BadRequestReviewWebMvcTest { - - @Autowired - private MockMvc mockMvc; - - @Autowired - private TokenProvider tokenProvider; - - @Autowired - private ObjectMapper objectMapper; - - @MockBean - private ReviewService reviewService; - @MockBean - private SearchingReviewService searchingReviewService; +class BadRequestReviewWebMvcTest extends WebMVCTest { @DisplayName("필수 데이터인 후기 내용이 공백인 경우 400을 반환한다.") @Test diff --git a/backend/src/test/java/com/woowacourse/moamoa/review/webmvc/UnauthorizedReviewWebMvcTest.java b/backend/src/test/java/com/woowacourse/moamoa/review/webmvc/UnauthorizedReviewWebMvcTest.java index c8a04c2b0..93cf269df 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/review/webmvc/UnauthorizedReviewWebMvcTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/review/webmvc/UnauthorizedReviewWebMvcTest.java @@ -4,41 +4,15 @@ import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.woowacourse.moamoa.auth.controller.matcher.AuthenticationRequestMatcher; -import com.woowacourse.moamoa.auth.infrastructure.JwtTokenProvider; -import com.woowacourse.moamoa.auth.infrastructure.TokenProvider; -import com.woowacourse.moamoa.review.controller.ReviewController; -import com.woowacourse.moamoa.review.service.ReviewService; +import com.woowacourse.moamoa.WebMVCTest; import com.woowacourse.moamoa.review.service.request.WriteReviewRequest; + import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.context.annotation.Import; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; -import org.springframework.test.web.servlet.MockMvc; - -@WebMvcTest(controllers = ReviewController.class) -@Import(JwtTokenProvider.class) -public class UnauthorizedReviewWebMvcTest { - - @Autowired - private MockMvc mockMvc; - - @Autowired - private TokenProvider tokenProvider; - - @Autowired - private ObjectMapper objectMapper; - - @MockBean - private ReviewService reviewService; - @MockBean - private AuthenticationRequestMatcher authenticationRequestMatcher; +public class UnauthorizedReviewWebMvcTest extends WebMVCTest { @DisplayName("유효하지 않은 토큰으로 리뷰 작성하려는 경우 401 에러을 반환한다.") @Test diff --git a/backend/src/test/java/com/woowacourse/moamoa/study/webmvc/BadRequestMyMemberRoleWebMvcTest.java b/backend/src/test/java/com/woowacourse/moamoa/study/webmvc/BadRequestMyMemberRoleWebMvcTest.java index a42f5338b..a6d5b946c 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/study/webmvc/BadRequestMyMemberRoleWebMvcTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/study/webmvc/BadRequestMyMemberRoleWebMvcTest.java @@ -4,42 +4,19 @@ import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import com.woowacourse.moamoa.WebMVCTest; + import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.context.annotation.Import; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; -import org.springframework.test.web.servlet.MockMvc; - -import com.woowacourse.moamoa.auth.controller.matcher.AuthenticationRequestMatcher; -import com.woowacourse.moamoa.auth.infrastructure.JwtTokenProvider; -import com.woowacourse.moamoa.study.controller.MyStudyController; -import com.woowacourse.moamoa.study.service.MyStudyService; - -@WebMvcTest(controllers = MyStudyController.class) -@Import(JwtTokenProvider.class) -public class BadRequestMyMemberRoleWebMvcTest { - - @Autowired - private MockMvc mockMvc; - - @MockBean - private MyStudyService myStudyService; - - @MockBean - private AuthenticationRequestMatcher authenticationRequestMatcher; - @Autowired - private JwtTokenProvider jwtTokenProvider; +public class BadRequestMyMemberRoleWebMvcTest extends WebMVCTest { @DisplayName("study Id가 없을 경우 400 에러가 발생한다. ") @Test void getMyStudiesWithoutStudyId() throws Exception { - final String token = "Bearer " + jwtTokenProvider.createToken(1L); + final String token = "Bearer " + tokenProvider.createToken(1L); mockMvc.perform(get("/api/members/me/role") .header(HttpHeaders.AUTHORIZATION, token) @@ -51,7 +28,7 @@ void getMyStudiesWithoutStudyId() throws Exception { @DisplayName("study Id가 String일 경우 400 에러가 발생한다.") @Test void getMyStudiesWithoutStudyId1() throws Exception { - final String token = "Bearer " + jwtTokenProvider.createToken(1L); + final String token = "Bearer " + tokenProvider.createToken(1L); mockMvc.perform(get("/api/members/me/role") .param("study-id", "one") diff --git a/backend/src/test/java/com/woowacourse/moamoa/study/webmvc/UnauthorizedMyStudyWebMvcTest.java b/backend/src/test/java/com/woowacourse/moamoa/study/webmvc/UnauthorizedMyStudyWebMvcTest.java index 0b9f275a4..6909a17fa 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/study/webmvc/UnauthorizedMyStudyWebMvcTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/study/webmvc/UnauthorizedMyStudyWebMvcTest.java @@ -7,29 +7,10 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.context.annotation.Import; -import org.springframework.test.web.servlet.MockMvc; -import com.woowacourse.moamoa.auth.controller.matcher.AuthenticationRequestMatcher; -import com.woowacourse.moamoa.auth.infrastructure.JwtTokenProvider; -import com.woowacourse.moamoa.study.controller.MyStudyController; -import com.woowacourse.moamoa.study.service.MyStudyService; +import com.woowacourse.moamoa.WebMVCTest; -@WebMvcTest(controllers = MyStudyController.class) -@Import(JwtTokenProvider.class) -class UnauthorizedMyStudyWebMvcTest { - - @Autowired - private MockMvc mockMvc; - - @MockBean - private MyStudyService myStudyService; - - @MockBean - private AuthenticationRequestMatcher authenticationRequestMatcher; +class UnauthorizedMyStudyWebMvcTest extends WebMVCTest { @DisplayName("헤더에 Authorization 코드가 없이, 내 스터디를 조회할 경우 401 에러가 발생한다.") @ParameterizedTest From 9961e58c1ce5197b56c092bae573b3638fa25f05 Mon Sep 17 00:00:00 2001 From: jaeseo yoo Date: Wed, 10 Aug 2022 17:33:51 +0900 Subject: [PATCH 05/51] =?UTF-8?q?[BE]=20issue221:=20=EB=82=B4=EA=B0=80=20?= =?UTF-8?q?=EC=B0=B8=EC=97=AC=ED=95=9C=20=EC=8A=A4=ED=84=B0=EB=94=94=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=EC=98=A4=EB=A5=98=20(#233)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: 스터디 ID가 비어있는 경우, findStudyOwnerWithTags 메서드에서 빈 Map을 바로 반환하도록 변경 * fix: 스터디에 태그가 없는 경우 발생했던 NPE 문제 해결 * fix: 내가 참여한 스터디 JOIN 쿼리 변경 * refactor: tags가 없는 경우 빈 리스트를 반환하도록 변경 * refactor: tags null인 경우 빈 리스트 반환 * refactor: ids String 변환 제거 --- .../moamoa/study/query/MyStudyDao.java | 57 ++++++++++++++----- .../study/query/data/MyStudySummaryData.java | 2 + .../query/data/StudyOwnerAndTagsData.java | 4 -- .../moamoa/study/service/MyStudyService.java | 28 ++++++++- .../service/response/MyStudiesResponse.java | 8 ++- .../service/response/MyStudyResponse.java | 4 ++ .../tag/query/response/TagSummaryData.java | 2 + .../study/GettingMyStudiesAcceptanceTest.java | 8 ++- .../controller/MyStudyControllerTest.java | 36 ++++++++---- .../moamoa/study/query/MyStudyDaoTest.java | 26 ++++++++- .../study/service/MyStudyServiceTest.java | 39 ++++++++++++- 11 files changed, 174 insertions(+), 40 deletions(-) diff --git a/backend/src/main/java/com/woowacourse/moamoa/study/query/MyStudyDao.java b/backend/src/main/java/com/woowacourse/moamoa/study/query/MyStudyDao.java index f46f5c86a..924bcb3d3 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/study/query/MyStudyDao.java +++ b/backend/src/main/java/com/woowacourse/moamoa/study/query/MyStudyDao.java @@ -3,10 +3,10 @@ import com.woowacourse.moamoa.member.query.data.MemberData; import com.woowacourse.moamoa.study.domain.StudyStatus; import com.woowacourse.moamoa.study.query.data.MyStudySummaryData; -import com.woowacourse.moamoa.study.query.data.StudyOwnerAndTagsData; import com.woowacourse.moamoa.tag.query.response.TagSummaryData; import java.util.ArrayList; +import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -32,7 +32,7 @@ public class MyStudyDao { final String title = rs.getString("title"); final String studyStatus = rs.getString("study_status"); final int currentMemberCount = rs.getInt("current_member_count"); - final int maxMemberCount = rs.getInt("max_member_count"); + final Integer maxMemberCount = rs.getObject("max_member_count", Integer.class); final String startDate = rs.getString("start_date"); final String endDate = rs.getString("end_date"); @@ -40,8 +40,8 @@ public class MyStudyDao { currentMemberCount, maxMemberCount, startDate, endDate); }; - private static final ResultSetExtractor> OWNER_WITH_TAG_ROW_MAPPER = rs -> { - Map result = new LinkedHashMap<>(); + private static final ResultSetExtractor> OWNER_ROW_MAPPER = rs -> { + Map result = new LinkedHashMap<>(); Long studyId; while (rs.next()) { @@ -53,14 +53,26 @@ public class MyStudyDao { String imageUrl = rs.getString("image_url"); String profileUrl = rs.getString("profile_url"); - result.put(studyId, new StudyOwnerAndTagsData(new MemberData(githubId, username, imageUrl, profileUrl), - new ArrayList<>())); + result.put(studyId, new MemberData(githubId, username, imageUrl, profileUrl)); + } + } + return result; + }; + + private static final ResultSetExtractor>> TAG_ROW_MAPPER = rs -> { + Map> result = new LinkedHashMap<>(); + + while (rs.next()) { + Long studyId = rs.getLong("study.id"); + + if (!result.containsKey(studyId)) { + result.put(studyId, new ArrayList<>()); } final Long tagId = rs.getLong("tag.id"); final String tagName = rs.getString("tag.name"); - result.get(studyId) - .addTag(new TagSummaryData(tagId, tagName)); + + result.get(studyId).add(new TagSummaryData(tagId, tagName)); } return result; }; @@ -68,25 +80,42 @@ public class MyStudyDao { public List findMyStudyByMemberId(Long id) { String sql = "SELECT DISTINCT study.id, study.title, study.study_status, study.current_member_count, " + "study.max_member_count, study.start_date, study.end_date " - + "FROM study_member JOIN study ON study_member.study_id = study.id " + + "FROM study LEFT JOIN study_member ON study_member.study_id = study.id " + "WHERE study_member.member_id = :id OR study.owner_id = :id"; return jdbcTemplate.query(sql, Map.of("id", id), MY_STUDY_SUMMARY_ROW_MAPPER); } - public Map findStudyOwnerWithTags(List studyIds) { + public Map findOwners(List studyIds) { + if (studyIds.isEmpty()) { + return new HashMap<>(); + } + + SqlParameterSource parameters = new MapSqlParameterSource("ids", studyIds); + + String sql = "SELECT study.id, member.github_id, member.username, member.image_url, member.profile_url " + + "FROM study JOIN member ON member.id = study.owner_id " + + "WHERE study.id IN (:ids)"; + + return jdbcTemplate.query(sql, parameters, OWNER_ROW_MAPPER); + } + + public Map> findTags(List studyIds) { + if (studyIds.isEmpty()) { + return new HashMap<>(); + } + List ids = studyIds.stream() .map(Object::toString) .collect(Collectors.toList()); SqlParameterSource parameters = new MapSqlParameterSource("ids", ids); - String sql = "SELECT study.id, member.github_id, member.username, member.image_url, member.profile_url, tag.id, tag.name " - + "FROM study JOIN member ON member.id = study.owner_id " - + "JOIN study_tag ON study.id = study_tag.study_id " + String sql = "SELECT study.id, tag.id, tag.name " + + "FROM study JOIN study_tag ON study.id = study_tag.study_id " + "JOIN tag ON tag.id = study_tag.tag_id " + "WHERE study.id IN (:ids)"; - return jdbcTemplate.query(sql, parameters, OWNER_WITH_TAG_ROW_MAPPER); + return jdbcTemplate.query(sql, parameters, TAG_ROW_MAPPER); } } diff --git a/backend/src/main/java/com/woowacourse/moamoa/study/query/data/MyStudySummaryData.java b/backend/src/main/java/com/woowacourse/moamoa/study/query/data/MyStudySummaryData.java index 636ae23af..a8aa1acda 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/study/query/data/MyStudySummaryData.java +++ b/backend/src/main/java/com/woowacourse/moamoa/study/query/data/MyStudySummaryData.java @@ -2,11 +2,13 @@ import com.woowacourse.moamoa.study.domain.StudyStatus; import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; @NoArgsConstructor @AllArgsConstructor +@EqualsAndHashCode @Getter public class MyStudySummaryData { diff --git a/backend/src/main/java/com/woowacourse/moamoa/study/query/data/StudyOwnerAndTagsData.java b/backend/src/main/java/com/woowacourse/moamoa/study/query/data/StudyOwnerAndTagsData.java index d176c4238..9fe08143d 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/study/query/data/StudyOwnerAndTagsData.java +++ b/backend/src/main/java/com/woowacourse/moamoa/study/query/data/StudyOwnerAndTagsData.java @@ -14,8 +14,4 @@ public class StudyOwnerAndTagsData { private MemberData owner; private List tags; - - public void addTag(TagSummaryData tagSummaryData) { - this.tags.add(tagSummaryData); - } } diff --git a/backend/src/main/java/com/woowacourse/moamoa/study/service/MyStudyService.java b/backend/src/main/java/com/woowacourse/moamoa/study/service/MyStudyService.java index 684d058a0..6037bf18b 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/study/service/MyStudyService.java +++ b/backend/src/main/java/com/woowacourse/moamoa/study/service/MyStudyService.java @@ -2,6 +2,7 @@ import com.woowacourse.moamoa.member.domain.Member; import com.woowacourse.moamoa.member.domain.repository.MemberRepository; +import com.woowacourse.moamoa.member.query.data.MemberData; import com.woowacourse.moamoa.study.domain.Study; import com.woowacourse.moamoa.study.domain.repository.StudyRepository; import com.woowacourse.moamoa.study.query.MyStudyDao; @@ -13,6 +14,8 @@ import com.woowacourse.moamoa.study.query.data.MyStudySummaryData; import com.woowacourse.moamoa.study.service.response.MyStudiesResponse; +import com.woowacourse.moamoa.tag.query.response.TagSummaryData; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; @@ -43,9 +46,30 @@ public MyStudiesResponse getStudies(final Long githubId) { .map(MyStudySummaryData::getId) .collect(Collectors.toList()); - final Map studyOwnerWithTags = myStudyDao.findStudyOwnerWithTags(studyIds); + Map ownerWithTags = getOwnerWithTags(studyIds); - return new MyStudiesResponse(mapToResponse(myStudySummaryData, studyOwnerWithTags)); + return new MyStudiesResponse(mapToResponse(myStudySummaryData, ownerWithTags)); + } + + private Map getOwnerWithTags(final List studyIds) { + final Map owners = myStudyDao.findOwners(studyIds); + final Map> tags = myStudyDao.findTags(studyIds); + + return mapOwnerWithTags(studyIds, owners, tags); + } + + private Map mapOwnerWithTags(final List studyIds, + final Map owners, + final Map> tags) { + Map result = new HashMap<>(); + for (Long id : studyIds) { + if (tags.get(id) == null) { + result.put(id, new StudyOwnerAndTagsData(owners.get(id), List.of())); + continue; + } + result.put(id, new StudyOwnerAndTagsData(owners.get(id), tags.get(id))); + } + return result; } private List mapToResponse(final List myStudySummaryData, diff --git a/backend/src/main/java/com/woowacourse/moamoa/study/service/response/MyStudiesResponse.java b/backend/src/main/java/com/woowacourse/moamoa/study/service/response/MyStudiesResponse.java index c93d39f10..72d5b3678 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/study/service/response/MyStudiesResponse.java +++ b/backend/src/main/java/com/woowacourse/moamoa/study/service/response/MyStudiesResponse.java @@ -2,11 +2,17 @@ import java.util.List; import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; @AllArgsConstructor +@NoArgsConstructor +@EqualsAndHashCode @Getter +@ToString public class MyStudiesResponse { - private final List studies; + private List studies; } diff --git a/backend/src/main/java/com/woowacourse/moamoa/study/service/response/MyStudyResponse.java b/backend/src/main/java/com/woowacourse/moamoa/study/service/response/MyStudyResponse.java index 56ee8b3a0..6772893d1 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/study/service/response/MyStudyResponse.java +++ b/backend/src/main/java/com/woowacourse/moamoa/study/service/response/MyStudyResponse.java @@ -6,12 +6,16 @@ import com.woowacourse.moamoa.tag.query.response.TagSummaryData; import java.util.List; import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; +import lombok.ToString; @NoArgsConstructor @AllArgsConstructor @Getter +@EqualsAndHashCode +@ToString public class MyStudyResponse { private Long id; diff --git a/backend/src/main/java/com/woowacourse/moamoa/tag/query/response/TagSummaryData.java b/backend/src/main/java/com/woowacourse/moamoa/tag/query/response/TagSummaryData.java index 24dc98745..b98c2a4c9 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/tag/query/response/TagSummaryData.java +++ b/backend/src/main/java/com/woowacourse/moamoa/tag/query/response/TagSummaryData.java @@ -4,11 +4,13 @@ import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; +import lombok.ToString; @NoArgsConstructor @AllArgsConstructor @Getter @EqualsAndHashCode +@ToString public class TagSummaryData { private Long id; diff --git a/backend/src/test/java/com/woowacourse/acceptance/test/study/GettingMyStudiesAcceptanceTest.java b/backend/src/test/java/com/woowacourse/acceptance/test/study/GettingMyStudiesAcceptanceTest.java index 12b8ff32f..0c6dbfb26 100644 --- a/backend/src/test/java/com/woowacourse/acceptance/test/study/GettingMyStudiesAcceptanceTest.java +++ b/backend/src/test/java/com/woowacourse/acceptance/test/study/GettingMyStudiesAcceptanceTest.java @@ -11,10 +11,13 @@ import static com.woowacourse.acceptance.fixture.StudyFixtures.리액트_스터디_제목; import static com.woowacourse.acceptance.fixture.StudyFixtures.자바_스터디_제목; import static com.woowacourse.acceptance.fixture.TagFixtures.BE_태그_ID; +import static com.woowacourse.acceptance.fixture.TagFixtures.BE_태그_설명; +import static com.woowacourse.acceptance.fixture.TagFixtures.BE_태그명; import static com.woowacourse.acceptance.fixture.TagFixtures.리액트_태그_ID; import static com.woowacourse.acceptance.fixture.TagFixtures.리액트_태그_설명; import static com.woowacourse.acceptance.fixture.TagFixtures.자바_태그_ID; import static com.woowacourse.acceptance.fixture.TagFixtures.자바_태그_설명; +import static com.woowacourse.acceptance.fixture.TagFixtures.자바_태그명; import static com.woowacourse.acceptance.steps.LoginSteps.그린론이; import static com.woowacourse.acceptance.steps.LoginSteps.디우가; import static com.woowacourse.moamoa.study.domain.StudyStatus.IN_PROGRESS; @@ -49,7 +52,6 @@ public class GettingMyStudiesAcceptanceTest extends AcceptanceTest { - @Disabled // 그린론이 해결할 버그 관련 인수 테스트 @DisplayName("내가 참여한 스터디를 조회한다.") @Test void getMyStudies() { @@ -75,10 +77,10 @@ void getMyStudies() { new MyStudySummaryData(자바_스터디_ID, 자바_스터디_제목, IN_PROGRESS, 1, null, 지금.toString(), null), new MemberData(그린론_깃허브_ID, 그린론_이름, 그린론_이미지_URL, 그린론_프로필_URL), - List.of(new TagSummaryData(자바_태그_ID, 자바_태그_설명), new TagSummaryData(리액트_태그_ID, 리액트_태그_설명))); + List.of(new TagSummaryData(자바_태그_ID, 자바_태그명), new TagSummaryData(BE_태그_ID, BE_태그명))); MyStudyResponse expectedReact = new MyStudyResponse( - new MyStudySummaryData(리액트_스터디_ID, 리액트_스터디_제목, StudyStatus.PREPARE, 1, + new MyStudySummaryData(리액트_스터디_ID, 리액트_스터디_제목, StudyStatus.PREPARE, 2, null, 지금.plusDays(10).toString(), null), new MemberData(디우_깃허브_ID, 디우_이름, 디우_이미지_URL, 디우_프로필_URL), List.of()); diff --git a/backend/src/test/java/com/woowacourse/moamoa/study/controller/MyStudyControllerTest.java b/backend/src/test/java/com/woowacourse/moamoa/study/controller/MyStudyControllerTest.java index 9f08fc79f..6d93be425 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/study/controller/MyStudyControllerTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/study/controller/MyStudyControllerTest.java @@ -55,17 +55,23 @@ void setUp() { final LocalDateTime now = LocalDateTime.now(); jdbcTemplate.update("INSERT INTO study(id, title, excerpt, thumbnail, recruitment_status, study_status, description, current_member_count, max_member_count, created_at, start_date, owner_id) " - + "VALUES (1, 'Java 스터디', '자바 설명', 'java thumbnail', 'RECRUITMENT_START', 'PREPARE', '그린론의 우당탕탕 자바 스터디입니다.', 3, 10, '" + now + "', '2021-12-08', 2)"); + + "VALUES (1, 'Java 스터디', '자바 설명', 'java thumbnail', 'RECRUITMENT_START', 'PREPARE', '그린론의 우당탕탕 자바 스터디입니다.', 3, 10, '" + + now + "', '2021-12-08', 2)"); jdbcTemplate.update("INSERT INTO study(id, title, excerpt, thumbnail, recruitment_status, study_status, description, current_member_count, max_member_count, created_at, enrollment_end_date, start_date, end_date, owner_id) " - + "VALUES (2, 'React 스터디', '리액트 설명', 'react thumbnail', 'RECRUITMENT_START', 'PREPARE', '디우의 뤼액트 스터디입니다.', 4, 5, '" + now + "', '2021-11-09', '2021-11-10', '2021-12-08', 3)"); + + "VALUES (2, 'React 스터디', '리액트 설명', 'react thumbnail', 'RECRUITMENT_START', 'PREPARE', '디우의 뤼액트 스터디입니다.', 4, 5, '" + + now + "', '2021-11-09', '2021-11-10', '2021-12-08', 3)"); jdbcTemplate.update("INSERT INTO study(id, title, excerpt, thumbnail, recruitment_status, study_status, description, current_member_count, max_member_count, created_at, start_date, owner_id) " - + "VALUES (3, 'javaScript 스터디', '자바스크립트 설명', 'javascript thumbnail', 'RECRUITMENT_START', 'PREPARE', '그린론의 자바스크립트 접해보기', 3, 20, '" + now + "', '2022-08-03', 2)"); + + "VALUES (3, 'javaScript 스터디', '자바스크립트 설명', 'javascript thumbnail', 'RECRUITMENT_START', 'PREPARE', '그린론의 자바스크립트 접해보기', 3, 20, '" + + now + "', '2022-08-03', 2)"); jdbcTemplate.update("INSERT INTO study(id, title, excerpt, thumbnail, recruitment_status, study_status, description, max_member_count, created_at, start_date, owner_id) " - + "VALUES (4, 'HTTP 스터디', 'HTTP 설명', 'http thumbnail', 'RECRUITMENT_END', 'PREPARE', '디우의 HTTP 정복하기', 5, '" + now + "', '2022-08-03', 3)"); + + "VALUES (4, 'HTTP 스터디', 'HTTP 설명', 'http thumbnail', 'RECRUITMENT_END', 'PREPARE', '디우의 HTTP 정복하기', 5, '" + + now + "', '2022-08-03', 3)"); jdbcTemplate.update("INSERT INTO study(id, title, excerpt, thumbnail, recruitment_status, study_status, description, current_member_count, created_at, owner_id, start_date) " - + "VALUES (5, '알고리즘 스터디', '알고리즘 설명', 'algorithm thumbnail', 'RECRUITMENT_END', 'PREPARE', '알고리즘을 TDD로 풀자의 베루스입니다.', 1, '" + now + "', 4, '2021-12-06')"); + + "VALUES (5, '알고리즘 스터디', '알고리즘 설명', 'algorithm thumbnail', 'RECRUITMENT_END', 'PREPARE', '알고리즘을 TDD로 풀자의 베루스입니다.', 1, '" + + now + "', 4, '2021-12-06')"); jdbcTemplate.update("INSERT INTO study(id, title, excerpt, thumbnail, recruitment_status, study_status, description, current_member_count, created_at, owner_id, start_date, enrollment_end_date, end_date) " - + "VALUES (6, 'Linux 스터디', '리눅스 설명', 'linux thumbnail', 'RECRUITMENT_END', 'PREPARE', 'Linux를 공부하자의 베루스입니다.', 1, '" + now + "', 4, '2021-12-06', '2021-12-07', '2022-01-07')"); + + "VALUES (6, 'Linux 스터디', '리눅스 설명', 'linux thumbnail', 'RECRUITMENT_END', 'PREPARE', 'Linux를 공부하자의 베루스입니다.', 1, '" + + now + "', 4, '2021-12-06', '2021-12-07', '2022-01-07')"); jdbcTemplate.update("INSERT INTO study_tag(study_id, tag_id) VALUES (1, 1)"); jdbcTemplate.update("INSERT INTO study_tag(study_id, tag_id) VALUES (1, 2)"); @@ -100,12 +106,14 @@ void getMyStudies() { assertThat(myStudies.getStatusCode()).isEqualTo(HttpStatus.OK); assertThat(myStudies.getBody()).isNotNull(); assertThat(myStudies.getBody().getStudies()) - .hasSize(3) + .hasSize(5) .extracting("id", "title", "studyStatus", "currentMemberCount", "maxMemberCount") .containsExactlyElementsOf(List.of( tuple(1L, "Java 스터디", PREPARE, 3, 10), tuple(2L, "React 스터디", PREPARE, 4, 5), - tuple(3L, "javaScript 스터디", PREPARE, 3, 20)) + tuple(3L, "javaScript 스터디", PREPARE, 3, 20), + tuple(5L, "알고리즘 스터디", PREPARE, 1, null), + tuple(6L, "Linux 스터디", PREPARE, 1, null)) ); final List owners = myStudies.getBody() @@ -115,13 +123,15 @@ void getMyStudies() { .collect(Collectors.toList()); assertThat(owners) - .hasSize(3) + .hasSize(5) .extracting("githubId", "username", "imageUrl", "profileUrl") .containsExactlyElementsOf(List.of( tuple(2L, "greenlawn", "https://image", "github.com"), tuple(3L, "dwoo", "https://image", "github.com"), - tuple(2L, "greenlawn", "https://image", "github.com")) - ); + tuple(2L, "greenlawn", "https://image", "github.com"), + tuple(4L, "verus", "https://image", "github.com"), + tuple(4L, "verus", "https://image", "github.com") + )); final List> tags = myStudies.getBody() .getStudies() @@ -150,5 +160,9 @@ void getMyStudies() { .extracting("id", "name") .contains(tuple(2L, "4기"), tuple(4L, "FE")); + + assertThat(tags.get(3).size()).isZero(); + + assertThat(tags.get(4).size()).isZero(); } } diff --git a/backend/src/test/java/com/woowacourse/moamoa/study/query/MyStudyDaoTest.java b/backend/src/test/java/com/woowacourse/moamoa/study/query/MyStudyDaoTest.java index b15ea3245..e027cbe9c 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/study/query/MyStudyDaoTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/study/query/MyStudyDaoTest.java @@ -4,6 +4,8 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.tuple; +import com.woowacourse.moamoa.member.query.data.MemberData; +import com.woowacourse.moamoa.tag.query.response.TagSummaryData; import java.util.List; import com.woowacourse.moamoa.common.RepositoryTest; @@ -11,6 +13,7 @@ import java.time.LocalDateTime; +import java.util.Map; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -48,6 +51,9 @@ void initDataBase() { + "VALUES (5, '알고리즘 스터디', '알고리즘 설명', 'algorithm thumbnail', 'RECRUITMENT_END', 'PREPARE', '알고리즘을 TDD로 풀자의 베루스입니다.', 1, '" + now + "', 4, '2021-12-06')"); jdbcTemplate.update("INSERT INTO study(id, title, excerpt, thumbnail, recruitment_status, study_status, description, current_member_count, created_at, owner_id, start_date, enrollment_end_date, end_date) " + "VALUES (6, 'Linux 스터디', '리눅스 설명', 'linux thumbnail', 'RECRUITMENT_END', 'PREPARE', 'Linux를 공부하자의 베루스입니다.', 1, '" + now + "', 4, '2021-12-06', '2021-12-07', '2022-01-07')"); + jdbcTemplate.update("INSERT INTO study(id, title, excerpt, thumbnail, recruitment_status, study_status, description, current_member_count, created_at, owner_id, start_date, enrollment_end_date, end_date) " + + "VALUES (7, 'OS 스터디', 'OS 설명', 'os thumbnail', 'RECRUITMENT_END', 'PREPARE', 'OS를 공부하자의 그린론입니다.', 1, '" + now + "', 2, '2021-12-06', '2021-12-07', '2022-01-07')"); + jdbcTemplate.update("INSERT INTO study_tag(study_id, tag_id) VALUES (1, 1)"); jdbcTemplate.update("INSERT INTO study_tag(study_id, tag_id) VALUES (1, 2)"); @@ -80,14 +86,30 @@ void getMyStudies() { final List studySummaryData = myStudyDao.findMyStudyByMemberId(2L); assertThat(studySummaryData) - .hasSize(3) + .hasSize(4) .filteredOn(myStudySummaryData -> myStudySummaryData.getId() != null) .extracting("title", "studyStatus", "currentMemberCount", "maxMemberCount", "startDate", "endDate") .contains( tuple("Java 스터디", PREPARE, 3, 10, "2021-12-08", null), tuple("javaScript 스터디" ,PREPARE, 3, 20, "2022-08-03", null), tuple("React 스터디", PREPARE, 4, 5, "2021-11-10", "2021-12-08"), - tuple("React 스터디", PREPARE, 4, 5, "2021-11-10", "2021-12-08") + tuple("OS 스터디", PREPARE, 1, null, "2021-12-06", "2022-01-07") ); } + + @DisplayName("스터디 ID가 비어있을 경우, 스터디 방장 빈 맵을 반환한다.") + @Test + void findStudyOwnersByEmptyStudyId() { + final Map owners = myStudyDao.findOwners(List.of()); + + assertThat(owners.size()).isZero(); + } + + @DisplayName("스터디 ID가 비어있을 경우, 스터디 태그 빈 맵을 반환한다.") + @Test + void findStudyTagsByEmptyStudyId() { + final Map> tags = myStudyDao.findTags(List.of()); + + assertThat(tags.size()).isZero(); + } } diff --git a/backend/src/test/java/com/woowacourse/moamoa/study/service/MyStudyServiceTest.java b/backend/src/test/java/com/woowacourse/moamoa/study/service/MyStudyServiceTest.java index 75366b968..5f08aa9d6 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/study/service/MyStudyServiceTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/study/service/MyStudyServiceTest.java @@ -84,18 +84,16 @@ void setUp() { jdbcTemplate.update("INSERT INTO study_tag(study_id, tag_id) VALUES (4, 2)"); jdbcTemplate.update("INSERT INTO study_tag(study_id, tag_id) VALUES (4, 3)"); - jdbcTemplate.update("INSERT INTO study_tag(study_id, tag_id) VALUES (7, 2)"); - jdbcTemplate.update("INSERT INTO study_member(study_id, member_id) VALUES (1, 3)"); jdbcTemplate.update("INSERT INTO study_member(study_id, member_id) VALUES (1, 4)"); - jdbcTemplate.update("INSERT INTO study_member(study_id, member_id) VALUES (2, 1)"); jdbcTemplate.update("INSERT INTO study_member(study_id, member_id) VALUES (2, 2)"); jdbcTemplate.update("INSERT INTO study_member(study_id, member_id) VALUES (2, 4)"); jdbcTemplate.update("INSERT INTO study_member(study_id, member_id) VALUES (3, 3)"); jdbcTemplate.update("INSERT INTO study_member(study_id, member_id) VALUES (3, 4)"); + jdbcTemplate.update("INSERT INTO study_member(study_id, member_id) VALUES (7, 1)"); jdbcTemplate.update("INSERT INTO study_member(study_id, member_id) VALUES (7, 2)"); } @@ -140,6 +138,41 @@ void findMyStudies() { assertThat(tags).hasSize(4); } + @DisplayName("태그가 없는 스터디를 조회한다.") + @Test + void findMyStudiesWithoutTags() { + final MyStudiesResponse myStudiesResponse = myStudyService.getStudies(1L); + + final List owners = myStudiesResponse.getStudies() + .stream() + .map(MyStudyResponse::getOwner) + .collect(toList()); + + final List> tags = myStudiesResponse.getStudies() + .stream() + .map(MyStudyResponse::getTags) + .collect(toList()); + + final List studies = myStudiesResponse.getStudies(); + + assertThat(studies) + .hasSize(1) + .filteredOn(study -> study.getId() != null) + .extracting("title", "studyStatus", "currentMemberCount", "maxMemberCount") + .contains( + tuple("OS 스터디", PREPARE, 1, 6) + ); + + assertThat(owners) + .hasSize(1) + .extracting("githubId", "username", "imageUrl", "profileUrl") + .contains( + tuple(4L, "verus", "https://image", "github.com") + ); + + assertThat(tags.get(0).size()).isZero(); + } + @DisplayName("존재하지 않은 내가 참여한 스터디 조회 시 예외 발생") @Test void getMyStudyNotExistUser() { From 85059a884b61290f1e56d01303cef52a916cc1fa Mon Sep 17 00:00:00 2001 From: airman5573 <68623798+airman5573@users.noreply.github.com> Date: Thu, 11 Aug 2022 16:46:51 +0900 Subject: [PATCH 06/51] =?UTF-8?q?[FE]=20issue238:=20=EB=B9=8C=EB=93=9C=20?= =?UTF-8?q?=ED=94=84=EB=A1=9C=EC=84=B8=EC=8A=A4=20=EA=B0=9C=EC=84=A0=20(#2?= =?UTF-8?q?39)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: esbuild 적용 * refactor: 사용하지 않는 prop 삭제 * refactor: babel 삭제 * refactor: classnames package 삭제 * feat: esnext로 타겟 변경 * refactor: 절대 경로 수정 * chore: js 문법지원을 es2022로 변경 * chore: @types/classname pacakge 제거 --- frontend/.babelrc.json | 24 - frontend/package-lock.json | 1253 +++++++++++------ frontend/package.json | 13 +- .../components/button-group/ButtonGroup.tsx | 3 +- .../src/components/button/Button.style.ts | 65 +- frontend/src/components/button/Button.tsx | 30 +- .../components/checkbox/Checkbox.style.tsx | 8 + frontend/src/components/checkbox/Checkbox.tsx | 9 + .../PositiveNumberInput.style.tsx | 3 - .../PositiveNumberInput.tsx | 49 - frontend/src/hooks/usePositiveNumberInput.ts | 15 + frontend/src/layout/header/Header.style.tsx | 31 +- .../header/components/logo/Logo.style.tsx | 10 + .../components/search-bar/SearchBar.style.tsx | 9 + .../CreateStudyPage.style.tsx | 122 +- .../create-study-page/CreateStudyPage.tsx | 64 +- .../components/category/Category.style.tsx | 19 + .../components/category/Category.tsx | 62 +- .../description-tab/DescriptionTab.style.tsx | 171 ++- .../description-tab/DescriptionTab.tsx | 78 +- .../EnrollmentEndDate.style.tsx | 6 + .../enrollment-end-date/EnrollmentEndDate.tsx | 15 +- .../components/excerpt/Excerpt.style.tsx | 38 +- .../components/excerpt/Excerpt.tsx | 44 +- .../components/input/Input.style.tsx | 28 + .../max-member-count/MaxMemberCount.style.tsx | 17 + .../max-member-count/MaxMemberCount.tsx | 44 +- .../components/meta-box/MetaBox.tsx | 5 +- .../components/period/Period.style.tsx | 10 + .../components/period/Peroid.tsx | 32 +- .../components/publish/Publish.tsx | 3 - .../components/select/Select.style.tsx | 22 + .../components/subject/Subject.style.tsx | 2 + .../components/subject/Subject.tsx | 4 +- .../components/textarea/Textarea.style.tsx | 8 + .../components/title/Title.style.tsx | 44 + .../components/title/Title.tsx | 46 +- .../review-comment/ReviewComment.style.tsx | 16 +- .../review-edit-form/ReviewEditForm.style.tsx | 7 +- .../review-edit-form/ReviewEditForm.tsx | 5 +- frontend/webpack/webpack.common.js | 15 +- 41 files changed, 1332 insertions(+), 1117 deletions(-) delete mode 100644 frontend/.babelrc.json create mode 100644 frontend/src/components/checkbox/Checkbox.style.tsx create mode 100644 frontend/src/components/checkbox/Checkbox.tsx delete mode 100644 frontend/src/components/positive-number-input/PositiveNumberInput.style.tsx delete mode 100644 frontend/src/components/positive-number-input/PositiveNumberInput.tsx create mode 100644 frontend/src/hooks/usePositiveNumberInput.ts create mode 100644 frontend/src/pages/create-study-page/components/input/Input.style.tsx create mode 100644 frontend/src/pages/create-study-page/components/select/Select.style.tsx create mode 100644 frontend/src/pages/create-study-page/components/textarea/Textarea.style.tsx create mode 100644 frontend/src/pages/create-study-page/components/title/Title.style.tsx diff --git a/frontend/.babelrc.json b/frontend/.babelrc.json deleted file mode 100644 index 657c6abcf..000000000 --- a/frontend/.babelrc.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "presets": [ - "@babel/preset-env", - [ - "@babel/preset-react", - { - "runtime": "automatic", - "importSource": "@emotion/react" - } - ], - "@babel/preset-typescript" - ], - "plugins": [ - [ - "@emotion", - { - "sourceMap": true, - "autoLabel": "dev-only", - "labelFormat": "[local]", - "cssPropOptimization": true - } - ] - ] -} diff --git a/frontend/package-lock.json b/frontend/package-lock.json index b87e38e56..c16080810 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -19,12 +19,6 @@ "react-router-dom": "^6.3.0" }, "devDependencies": { - "@babel/cli": "^7.17.10", - "@babel/core": "^7.18.5", - "@babel/preset-env": "^7.18.2", - "@babel/preset-react": "^7.17.12", - "@babel/preset-typescript": "^7.17.12", - "@emotion/babel-plugin": "^11.9.2", "@emotion/react": "^11.9.3", "@emotion/styled": "^11.9.3", "@storybook/addon-actions": "^6.5.9", @@ -39,18 +33,17 @@ "@storybook/testing-library": "^0.0.13", "@testing-library/cypress": "^8.0.3", "@trivago/prettier-plugin-sort-imports": "^3.2.0", - "@types/classnames": "^2.3.1", "@types/dompurify": "^2.3.3", "@types/marked": "^4.0.3", "@types/react": "^18.0.14", "@types/react-dom": "^18.0.5", "@typescript-eslint/eslint-plugin": "^5.30.0", "@typescript-eslint/parser": "^5.30.5", - "babel-loader": "^8.2.5", - "classnames": "^2.3.1", "clean-webpack-plugin": "^4.0.0", "cypress": "^10.4.0", "dotenv": "^16.0.1", + "esbuild": "^0.14.54", + "esbuild-loader": "^2.19.0", "eslint": "^8.18.0", "eslint-config-prettier": "^8.5.0", "eslint-import-resolver-typescript": "^3.1.1", @@ -63,9 +56,9 @@ "html-webpack-plugin": "^5.5.0", "msw": "^0.43.0", "prettier": "^2.7.1", + "speed-measure-webpack-plugin": "^1.5.0", "typescript": "^4.7.4", "webpack": "^5.73.0", - "webpack-bundle-analyzer": "^4.5.0", "webpack-cli": "^4.10.0", "webpack-dev-server": "^4.9.2", "webpack-merge": "^5.8.0" @@ -83,35 +76,6 @@ "node": ">=6.0.0" } }, - "node_modules/@babel/cli": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/cli/-/cli-7.18.9.tgz", - "integrity": "sha512-e7TOtHVrAXBJGNgoROVxqx0mathd01oJGXIDekRfxdrISnRqfM795APwkDtse9GdyPYivjg3iXiko3sF3W7f5Q==", - "dev": true, - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.8", - "commander": "^4.0.1", - "convert-source-map": "^1.1.0", - "fs-readdir-recursive": "^1.1.0", - "glob": "^7.0.0", - "make-dir": "^2.1.0", - "slash": "^2.0.0" - }, - "bin": { - "babel": "bin/babel.js", - "babel-external-helpers": "bin/babel-external-helpers.js" - }, - "engines": { - "node": ">=6.9.0" - }, - "optionalDependencies": { - "@nicolo-ribaudo/chokidar-2": "2.1.8-no-fsevents.3", - "chokidar": "^3.4.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, "node_modules/@babel/code-frame": { "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz", @@ -2320,6 +2284,22 @@ "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz", "integrity": "sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA==" }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.14.54.tgz", + "integrity": "sha512-bZBrLAIX1kpWelV0XemxBZllyRmM6vgFQQG2GdNb+r3Fkp0FOh1NJSvekXDs7jq70k4euu1cryLMfU+mTXlEpw==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, "node_modules/@eslint/eslintrc": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.0.tgz", @@ -2890,13 +2870,6 @@ "node": ">=14" } }, - "node_modules/@nicolo-ribaudo/chokidar-2": { - "version": "2.1.8-no-fsevents.3", - "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/chokidar-2/-/chokidar-2-2.1.8-no-fsevents.3.tgz", - "integrity": "sha512-s88O1aVtXftvp5bCPB7WnmXc5IwOZZ7YPuwNPt+GtOOXpPvad1LfbmjYv+qII7zP6RU2QGnqve27dnLycEnyEQ==", - "dev": true, - "optional": true - }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -3076,12 +3049,6 @@ "node": ">= 8" } }, - "node_modules/@polka/url": { - "version": "1.0.0-next.21", - "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.21.tgz", - "integrity": "sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==", - "dev": true - }, "node_modules/@storybook/addon-actions": { "version": "6.5.9", "resolved": "https://registry.npmjs.org/@storybook/addon-actions/-/addon-actions-6.5.9.tgz", @@ -9163,16 +9130,6 @@ "@types/node": "*" } }, - "node_modules/@types/classnames": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/@types/classnames/-/classnames-2.3.1.tgz", - "integrity": "sha512-zeOWb0JGBoVmlQoznvqXbE0tEC/HONsnoUNH19Hc96NFsTAwTXbTqb8FMYkru1F/iqp7a18Ws3nWJvtA1sHD1A==", - "deprecated": "This is a stub types definition. classnames provides its own type definitions, so you do not need this installed.", - "dev": true, - "dependencies": { - "classnames": "*" - } - }, "node_modules/@types/connect": { "version": "3.4.35", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", @@ -12325,12 +12282,6 @@ "node": ">=0.10.0" } }, - "node_modules/classnames": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.1.tgz", - "integrity": "sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==", - "dev": true - }, "node_modules/clean-css": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.1.tgz", @@ -14198,12 +14149,6 @@ "integrity": "sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==", "dev": true }, - "node_modules/duplexer": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", - "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", - "dev": true - }, "node_modules/duplexify": { "version": "3.7.1", "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", @@ -14533,6 +14478,404 @@ "integrity": "sha512-EmTr31wppcaIAgblChZiuN/l9Y7DPyw8Xtbg7fIVngn6zMW+IEBJDJngeKC3x6wr0V/vcA2wqeFnaw1bFJbDdA==", "dev": true }, + "node_modules/esbuild": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.14.54.tgz", + "integrity": "sha512-Cy9llcy8DvET5uznocPyqL3BFRrFXSVqbgpMJ9Wz8oVjZlh/zUSNbPRbov0VX7VxN2JH1Oa0uNxZ7eLRb62pJA==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/linux-loong64": "0.14.54", + "esbuild-android-64": "0.14.54", + "esbuild-android-arm64": "0.14.54", + "esbuild-darwin-64": "0.14.54", + "esbuild-darwin-arm64": "0.14.54", + "esbuild-freebsd-64": "0.14.54", + "esbuild-freebsd-arm64": "0.14.54", + "esbuild-linux-32": "0.14.54", + "esbuild-linux-64": "0.14.54", + "esbuild-linux-arm": "0.14.54", + "esbuild-linux-arm64": "0.14.54", + "esbuild-linux-mips64le": "0.14.54", + "esbuild-linux-ppc64le": "0.14.54", + "esbuild-linux-riscv64": "0.14.54", + "esbuild-linux-s390x": "0.14.54", + "esbuild-netbsd-64": "0.14.54", + "esbuild-openbsd-64": "0.14.54", + "esbuild-sunos-64": "0.14.54", + "esbuild-windows-32": "0.14.54", + "esbuild-windows-64": "0.14.54", + "esbuild-windows-arm64": "0.14.54" + } + }, + "node_modules/esbuild-android-64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.14.54.tgz", + "integrity": "sha512-Tz2++Aqqz0rJ7kYBfz+iqyE3QMycD4vk7LBRyWaAVFgFtQ/O8EJOnVmTOiDWYZ/uYzB4kvP+bqejYdVKzE5lAQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-android-arm64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.14.54.tgz", + "integrity": "sha512-F9E+/QDi9sSkLaClO8SOV6etqPd+5DgJje1F9lOWoNncDdOBL2YF59IhsWATSt0TLZbYCf3pNlTHvVV5VfHdvg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-darwin-64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.14.54.tgz", + "integrity": "sha512-jtdKWV3nBviOd5v4hOpkVmpxsBy90CGzebpbO9beiqUYVMBtSc0AL9zGftFuBon7PNDcdvNCEuQqw2x0wP9yug==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-darwin-arm64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.54.tgz", + "integrity": "sha512-OPafJHD2oUPyvJMrsCvDGkRrVCar5aVyHfWGQzY1dWnzErjrDuSETxwA2HSsyg2jORLY8yBfzc1MIpUkXlctmw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-freebsd-64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.54.tgz", + "integrity": "sha512-OKwd4gmwHqOTp4mOGZKe/XUlbDJ4Q9TjX0hMPIDBUWWu/kwhBAudJdBoxnjNf9ocIB6GN6CPowYpR/hRCbSYAg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-freebsd-arm64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.54.tgz", + "integrity": "sha512-sFwueGr7OvIFiQT6WeG0jRLjkjdqWWSrfbVwZp8iMP+8UHEHRBvlaxL6IuKNDwAozNUmbb8nIMXa7oAOARGs1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-32": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.14.54.tgz", + "integrity": "sha512-1ZuY+JDI//WmklKlBgJnglpUL1owm2OX+8E1syCD6UAxcMM/XoWd76OHSjl/0MR0LisSAXDqgjT3uJqT67O3qw==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.54.tgz", + "integrity": "sha512-EgjAgH5HwTbtNsTqQOXWApBaPVdDn7XcK+/PtJwZLT1UmpLoznPd8c5CxqsH2dQK3j05YsB3L17T8vE7cp4cCg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-arm": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.14.54.tgz", + "integrity": "sha512-qqz/SjemQhVMTnvcLGoLOdFpCYbz4v4fUo+TfsWG+1aOu70/80RV6bgNpR2JCrppV2moUQkww+6bWxXRL9YMGw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-arm64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.54.tgz", + "integrity": "sha512-WL71L+0Rwv+Gv/HTmxTEmpv0UgmxYa5ftZILVi2QmZBgX3q7+tDeOQNqGtdXSdsL8TQi1vIaVFHUPDe0O0kdig==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-mips64le": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.54.tgz", + "integrity": "sha512-qTHGQB8D1etd0u1+sB6p0ikLKRVuCWhYQhAHRPkO+OF3I/iSlTKNNS0Lh2Oc0g0UFGguaFZZiPJdJey3AGpAlw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-ppc64le": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.54.tgz", + "integrity": "sha512-j3OMlzHiqwZBDPRCDFKcx595XVfOfOnv68Ax3U4UKZ3MTYQB5Yz3X1mn5GnodEVYzhtZgxEBidLWeIs8FDSfrQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-riscv64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.54.tgz", + "integrity": "sha512-y7Vt7Wl9dkOGZjxQZnDAqqn+XOqFD7IMWiewY5SPlNlzMX39ocPQlOaoxvT4FllA5viyV26/QzHtvTjVNOxHZg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-s390x": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.54.tgz", + "integrity": "sha512-zaHpW9dziAsi7lRcyV4r8dhfG1qBidQWUXweUjnw+lliChJqQr+6XD71K41oEIC3Mx1KStovEmlzm+MkGZHnHA==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-loader": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/esbuild-loader/-/esbuild-loader-2.19.0.tgz", + "integrity": "sha512-urGNVE6Tl2rqx92ElKi/LiExXjGvcH6HfDBFzJ9Ppwqh4n6Jmx8x7RKAyMzSM78b6CAaJLhDncG5sPrL0ROh5Q==", + "dev": true, + "dependencies": { + "esbuild": "^0.14.39", + "joycon": "^3.0.1", + "json5": "^2.2.0", + "loader-utils": "^2.0.0", + "tapable": "^2.2.0", + "webpack-sources": "^2.2.0" + }, + "funding": { + "url": "https://github.com/privatenumber/esbuild-loader?sponsor=1" + }, + "peerDependencies": { + "webpack": "^4.40.0 || ^5.0.0" + } + }, + "node_modules/esbuild-loader/node_modules/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, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/esbuild-loader/node_modules/webpack-sources": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-2.3.1.tgz", + "integrity": "sha512-y9EI9AO42JjEcrTJFOYmVywVZdKVUfOvDUPsJea5GIr1JOEGFVqwlY2K098fFoIjOkDzHn2AjRvM8dsBZu+gCA==", + "dev": true, + "dependencies": { + "source-list-map": "^2.0.1", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/esbuild-netbsd-64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.54.tgz", + "integrity": "sha512-PR01lmIMnfJTgeU9VJTDY9ZerDWVFIUzAtJuDHwwceppW7cQWjBBqP48NdeRtoP04/AtO9a7w3viI+PIDr6d+w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-openbsd-64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.54.tgz", + "integrity": "sha512-Qyk7ikT2o7Wu76UsvvDS5q0amJvmRzDyVlL0qf5VLsLchjCa1+IAvd8kTBgUxD7VBUUVgItLkk609ZHUc1oCaw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-sunos-64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.14.54.tgz", + "integrity": "sha512-28GZ24KmMSeKi5ueWzMcco6EBHStL3B6ubM7M51RmPwXQGLe0teBGJocmWhgwccA1GeFXqxzILIxXpHbl9Q/Kw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-32": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.14.54.tgz", + "integrity": "sha512-T+rdZW19ql9MjS7pixmZYVObd9G7kcaZo+sETqNH4RCkuuYSuv9AGHUVnPoP9hhuE1WM1ZimHz1CIBHBboLU7w==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.14.54.tgz", + "integrity": "sha512-AoHTRBUuYwXtZhjXZbA1pGfTo8cJo3vZIcWGLiUcTNgHpJJMC1rVA44ZereBHMJtotyN71S8Qw0npiCIkW96cQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-arm64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.54.tgz", + "integrity": "sha512-M0kuUvXhot1zOISQGXwWn6YtS+Y/1RT9WrVIOywZnJHo3jCDyewAc79aKNQWFCQm+xNHVTq9h8dZKvygoXQQRg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, "node_modules/escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", @@ -16577,12 +16920,6 @@ "integrity": "sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q==", "dev": true }, - "node_modules/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==", - "dev": true - }, "node_modules/fs-write-stream-atomic": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz", @@ -16978,21 +17315,6 @@ "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" } }, - "node_modules/gzip-size": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", - "integrity": "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==", - "dev": true, - "dependencies": { - "duplexer": "^0.1.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/handle-thing": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", @@ -19060,6 +19382,15 @@ "node": ">=8" } }, + "node_modules/joycon": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", + "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, "node_modules/js-levenshtein": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.6.tgz", @@ -20389,15 +20720,6 @@ "mkdirp": "bin/cmd.js" } }, - "node_modules/mrmime": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-1.0.1.tgz", - "integrity": "sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw==", - "dev": true, - "engines": { - "node": ">=10" - } - }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -21192,15 +21514,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/opener": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", - "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==", - "dev": true, - "bin": { - "opener": "bin/opener-bin.js" - } - }, "node_modules/optionator": { "version": "0.8.3", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", @@ -24073,20 +24386,6 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true }, - "node_modules/sirv": { - "version": "1.0.19", - "resolved": "https://registry.npmjs.org/sirv/-/sirv-1.0.19.tgz", - "integrity": "sha512-JuLThK3TnZG1TAKDwNIqNq6QA2afLOCcm+iE8D1Kj3GA40pSPsxQjjJl0J8X3tsR7T+CP1GavpzLwYkgVLWrZQ==", - "dev": true, - "dependencies": { - "@polka/url": "^1.0.0-next.20", - "mrmime": "^1.0.0", - "totalist": "^1.0.0" - }, - "engines": { - "node": ">= 10" - } - }, "node_modules/sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", @@ -24495,6 +24794,91 @@ "wbuf": "^1.7.3" } }, + "node_modules/speed-measure-webpack-plugin": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/speed-measure-webpack-plugin/-/speed-measure-webpack-plugin-1.5.0.tgz", + "integrity": "sha512-Re0wX5CtM6gW7bZA64ONOfEPEhwbiSF/vz6e2GvadjuaPrQcHTQdRGsD8+BE7iUOysXH8tIenkPCQBEcspXsNg==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0" + }, + "engines": { + "node": ">=6.0.0" + }, + "peerDependencies": { + "webpack": "^1 || ^2 || ^3 || ^4 || ^5" + } + }, + "node_modules/speed-measure-webpack-plugin/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/speed-measure-webpack-plugin/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/speed-measure-webpack-plugin/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/speed-measure-webpack-plugin/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/speed-measure-webpack-plugin/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/speed-measure-webpack-plugin/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/split-string": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", @@ -25522,15 +25906,6 @@ "node": ">=0.6" } }, - "node_modules/totalist": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/totalist/-/totalist-1.1.0.tgz", - "integrity": "sha512-gduQwd1rOdDMGxFG1gEvhV88Oirdo2p+KjoYFU7k2g+i7n6AFFbDQ5kMPUsW0pNbfQsB/cwXvT1i4Bue0s9g5g==", - "dev": true, - "engines": { - "node": ">=6" - } - }, "node_modules/tough-cookie": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", @@ -26805,150 +27180,6 @@ } } }, - "node_modules/webpack-bundle-analyzer": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.5.0.tgz", - "integrity": "sha512-GUMZlM3SKwS8Z+CKeIFx7CVoHn3dXFcUAjT/dcZQQmfSZGvitPfMob2ipjai7ovFFqPvTqkEZ/leL4O0YOdAYQ==", - "dev": true, - "dependencies": { - "acorn": "^8.0.4", - "acorn-walk": "^8.0.0", - "chalk": "^4.1.0", - "commander": "^7.2.0", - "gzip-size": "^6.0.0", - "lodash": "^4.17.20", - "opener": "^1.5.2", - "sirv": "^1.0.7", - "ws": "^7.3.1" - }, - "bin": { - "webpack-bundle-analyzer": "lib/bin/analyzer.js" - }, - "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/webpack-bundle-analyzer/node_modules/acorn": { - "version": "8.8.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.0.tgz", - "integrity": "sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==", - "dev": true, - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/webpack-bundle-analyzer/node_modules/acorn-walk": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", - "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", - "dev": true, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/webpack-bundle-analyzer/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/webpack-bundle-analyzer/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/webpack-bundle-analyzer/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/webpack-bundle-analyzer/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/webpack-bundle-analyzer/node_modules/commander": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", - "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", - "dev": true, - "engines": { - "node": ">= 10" - } - }, - "node_modules/webpack-bundle-analyzer/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/webpack-bundle-analyzer/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/webpack-bundle-analyzer/node_modules/ws": { - "version": "7.5.9", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", - "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", - "dev": true, - "engines": { - "node": ">=8.3.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, "node_modules/webpack-cli": { "version": "4.10.0", "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-4.10.0.tgz", @@ -27734,23 +27965,6 @@ "@jridgewell/trace-mapping": "^0.3.9" } }, - "@babel/cli": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/cli/-/cli-7.18.9.tgz", - "integrity": "sha512-e7TOtHVrAXBJGNgoROVxqx0mathd01oJGXIDekRfxdrISnRqfM795APwkDtse9GdyPYivjg3iXiko3sF3W7f5Q==", - "dev": true, - "requires": { - "@jridgewell/trace-mapping": "^0.3.8", - "@nicolo-ribaudo/chokidar-2": "2.1.8-no-fsevents.3", - "chokidar": "^3.4.0", - "commander": "^4.0.1", - "convert-source-map": "^1.1.0", - "fs-readdir-recursive": "^1.1.0", - "glob": "^7.0.0", - "make-dir": "^2.1.0", - "slash": "^2.0.0" - } - }, "@babel/code-frame": { "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz", @@ -29321,6 +29535,13 @@ "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz", "integrity": "sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA==" }, + "@esbuild/linux-loong64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.14.54.tgz", + "integrity": "sha512-bZBrLAIX1kpWelV0XemxBZllyRmM6vgFQQG2GdNb+r3Fkp0FOh1NJSvekXDs7jq70k4euu1cryLMfU+mTXlEpw==", + "dev": true, + "optional": true + }, "@eslint/eslintrc": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.0.tgz", @@ -29773,13 +29994,6 @@ "strict-event-emitter": "^0.2.4" } }, - "@nicolo-ribaudo/chokidar-2": { - "version": "2.1.8-no-fsevents.3", - "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/chokidar-2/-/chokidar-2-2.1.8-no-fsevents.3.tgz", - "integrity": "sha512-s88O1aVtXftvp5bCPB7WnmXc5IwOZZ7YPuwNPt+GtOOXpPvad1LfbmjYv+qII7zP6RU2QGnqve27dnLycEnyEQ==", - "dev": true, - "optional": true - }, "@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -29901,12 +30115,6 @@ } } }, - "@polka/url": { - "version": "1.0.0-next.21", - "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.21.tgz", - "integrity": "sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==", - "dev": true - }, "@storybook/addon-actions": { "version": "6.5.9", "resolved": "https://registry.npmjs.org/@storybook/addon-actions/-/addon-actions-6.5.9.tgz", @@ -34529,15 +34737,6 @@ "@types/node": "*" } }, - "@types/classnames": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/@types/classnames/-/classnames-2.3.1.tgz", - "integrity": "sha512-zeOWb0JGBoVmlQoznvqXbE0tEC/HONsnoUNH19Hc96NFsTAwTXbTqb8FMYkru1F/iqp7a18Ws3nWJvtA1sHD1A==", - "dev": true, - "requires": { - "classnames": "*" - } - }, "@types/connect": { "version": "3.4.35", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", @@ -37077,12 +37276,6 @@ } } }, - "classnames": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.1.tgz", - "integrity": "sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==", - "dev": true - }, "clean-css": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.1.tgz", @@ -38552,12 +38745,6 @@ "integrity": "sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==", "dev": true }, - "duplexer": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", - "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", - "dev": true - }, "duplexify": { "version": "3.7.1", "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", @@ -38846,6 +39033,207 @@ "integrity": "sha512-EmTr31wppcaIAgblChZiuN/l9Y7DPyw8Xtbg7fIVngn6zMW+IEBJDJngeKC3x6wr0V/vcA2wqeFnaw1bFJbDdA==", "dev": true }, + "esbuild": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.14.54.tgz", + "integrity": "sha512-Cy9llcy8DvET5uznocPyqL3BFRrFXSVqbgpMJ9Wz8oVjZlh/zUSNbPRbov0VX7VxN2JH1Oa0uNxZ7eLRb62pJA==", + "dev": true, + "requires": { + "@esbuild/linux-loong64": "0.14.54", + "esbuild-android-64": "0.14.54", + "esbuild-android-arm64": "0.14.54", + "esbuild-darwin-64": "0.14.54", + "esbuild-darwin-arm64": "0.14.54", + "esbuild-freebsd-64": "0.14.54", + "esbuild-freebsd-arm64": "0.14.54", + "esbuild-linux-32": "0.14.54", + "esbuild-linux-64": "0.14.54", + "esbuild-linux-arm": "0.14.54", + "esbuild-linux-arm64": "0.14.54", + "esbuild-linux-mips64le": "0.14.54", + "esbuild-linux-ppc64le": "0.14.54", + "esbuild-linux-riscv64": "0.14.54", + "esbuild-linux-s390x": "0.14.54", + "esbuild-netbsd-64": "0.14.54", + "esbuild-openbsd-64": "0.14.54", + "esbuild-sunos-64": "0.14.54", + "esbuild-windows-32": "0.14.54", + "esbuild-windows-64": "0.14.54", + "esbuild-windows-arm64": "0.14.54" + } + }, + "esbuild-android-64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.14.54.tgz", + "integrity": "sha512-Tz2++Aqqz0rJ7kYBfz+iqyE3QMycD4vk7LBRyWaAVFgFtQ/O8EJOnVmTOiDWYZ/uYzB4kvP+bqejYdVKzE5lAQ==", + "dev": true, + "optional": true + }, + "esbuild-android-arm64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.14.54.tgz", + "integrity": "sha512-F9E+/QDi9sSkLaClO8SOV6etqPd+5DgJje1F9lOWoNncDdOBL2YF59IhsWATSt0TLZbYCf3pNlTHvVV5VfHdvg==", + "dev": true, + "optional": true + }, + "esbuild-darwin-64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.14.54.tgz", + "integrity": "sha512-jtdKWV3nBviOd5v4hOpkVmpxsBy90CGzebpbO9beiqUYVMBtSc0AL9zGftFuBon7PNDcdvNCEuQqw2x0wP9yug==", + "dev": true, + "optional": true + }, + "esbuild-darwin-arm64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.54.tgz", + "integrity": "sha512-OPafJHD2oUPyvJMrsCvDGkRrVCar5aVyHfWGQzY1dWnzErjrDuSETxwA2HSsyg2jORLY8yBfzc1MIpUkXlctmw==", + "dev": true, + "optional": true + }, + "esbuild-freebsd-64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.54.tgz", + "integrity": "sha512-OKwd4gmwHqOTp4mOGZKe/XUlbDJ4Q9TjX0hMPIDBUWWu/kwhBAudJdBoxnjNf9ocIB6GN6CPowYpR/hRCbSYAg==", + "dev": true, + "optional": true + }, + "esbuild-freebsd-arm64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.54.tgz", + "integrity": "sha512-sFwueGr7OvIFiQT6WeG0jRLjkjdqWWSrfbVwZp8iMP+8UHEHRBvlaxL6IuKNDwAozNUmbb8nIMXa7oAOARGs1Q==", + "dev": true, + "optional": true + }, + "esbuild-linux-32": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.14.54.tgz", + "integrity": "sha512-1ZuY+JDI//WmklKlBgJnglpUL1owm2OX+8E1syCD6UAxcMM/XoWd76OHSjl/0MR0LisSAXDqgjT3uJqT67O3qw==", + "dev": true, + "optional": true + }, + "esbuild-linux-64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.54.tgz", + "integrity": "sha512-EgjAgH5HwTbtNsTqQOXWApBaPVdDn7XcK+/PtJwZLT1UmpLoznPd8c5CxqsH2dQK3j05YsB3L17T8vE7cp4cCg==", + "dev": true, + "optional": true + }, + "esbuild-linux-arm": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.14.54.tgz", + "integrity": "sha512-qqz/SjemQhVMTnvcLGoLOdFpCYbz4v4fUo+TfsWG+1aOu70/80RV6bgNpR2JCrppV2moUQkww+6bWxXRL9YMGw==", + "dev": true, + "optional": true + }, + "esbuild-linux-arm64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.54.tgz", + "integrity": "sha512-WL71L+0Rwv+Gv/HTmxTEmpv0UgmxYa5ftZILVi2QmZBgX3q7+tDeOQNqGtdXSdsL8TQi1vIaVFHUPDe0O0kdig==", + "dev": true, + "optional": true + }, + "esbuild-linux-mips64le": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.54.tgz", + "integrity": "sha512-qTHGQB8D1etd0u1+sB6p0ikLKRVuCWhYQhAHRPkO+OF3I/iSlTKNNS0Lh2Oc0g0UFGguaFZZiPJdJey3AGpAlw==", + "dev": true, + "optional": true + }, + "esbuild-linux-ppc64le": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.54.tgz", + "integrity": "sha512-j3OMlzHiqwZBDPRCDFKcx595XVfOfOnv68Ax3U4UKZ3MTYQB5Yz3X1mn5GnodEVYzhtZgxEBidLWeIs8FDSfrQ==", + "dev": true, + "optional": true + }, + "esbuild-linux-riscv64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.54.tgz", + "integrity": "sha512-y7Vt7Wl9dkOGZjxQZnDAqqn+XOqFD7IMWiewY5SPlNlzMX39ocPQlOaoxvT4FllA5viyV26/QzHtvTjVNOxHZg==", + "dev": true, + "optional": true + }, + "esbuild-linux-s390x": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.54.tgz", + "integrity": "sha512-zaHpW9dziAsi7lRcyV4r8dhfG1qBidQWUXweUjnw+lliChJqQr+6XD71K41oEIC3Mx1KStovEmlzm+MkGZHnHA==", + "dev": true, + "optional": true + }, + "esbuild-loader": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/esbuild-loader/-/esbuild-loader-2.19.0.tgz", + "integrity": "sha512-urGNVE6Tl2rqx92ElKi/LiExXjGvcH6HfDBFzJ9Ppwqh4n6Jmx8x7RKAyMzSM78b6CAaJLhDncG5sPrL0ROh5Q==", + "dev": true, + "requires": { + "esbuild": "^0.14.39", + "joycon": "^3.0.1", + "json5": "^2.2.0", + "loader-utils": "^2.0.0", + "tapable": "^2.2.0", + "webpack-sources": "^2.2.0" + }, + "dependencies": { + "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 + }, + "webpack-sources": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-2.3.1.tgz", + "integrity": "sha512-y9EI9AO42JjEcrTJFOYmVywVZdKVUfOvDUPsJea5GIr1JOEGFVqwlY2K098fFoIjOkDzHn2AjRvM8dsBZu+gCA==", + "dev": true, + "requires": { + "source-list-map": "^2.0.1", + "source-map": "^0.6.1" + } + } + } + }, + "esbuild-netbsd-64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.54.tgz", + "integrity": "sha512-PR01lmIMnfJTgeU9VJTDY9ZerDWVFIUzAtJuDHwwceppW7cQWjBBqP48NdeRtoP04/AtO9a7w3viI+PIDr6d+w==", + "dev": true, + "optional": true + }, + "esbuild-openbsd-64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.54.tgz", + "integrity": "sha512-Qyk7ikT2o7Wu76UsvvDS5q0amJvmRzDyVlL0qf5VLsLchjCa1+IAvd8kTBgUxD7VBUUVgItLkk609ZHUc1oCaw==", + "dev": true, + "optional": true + }, + "esbuild-sunos-64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.14.54.tgz", + "integrity": "sha512-28GZ24KmMSeKi5ueWzMcco6EBHStL3B6ubM7M51RmPwXQGLe0teBGJocmWhgwccA1GeFXqxzILIxXpHbl9Q/Kw==", + "dev": true, + "optional": true + }, + "esbuild-windows-32": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.14.54.tgz", + "integrity": "sha512-T+rdZW19ql9MjS7pixmZYVObd9G7kcaZo+sETqNH4RCkuuYSuv9AGHUVnPoP9hhuE1WM1ZimHz1CIBHBboLU7w==", + "dev": true, + "optional": true + }, + "esbuild-windows-64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.14.54.tgz", + "integrity": "sha512-AoHTRBUuYwXtZhjXZbA1pGfTo8cJo3vZIcWGLiUcTNgHpJJMC1rVA44ZereBHMJtotyN71S8Qw0npiCIkW96cQ==", + "dev": true, + "optional": true + }, + "esbuild-windows-arm64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.54.tgz", + "integrity": "sha512-M0kuUvXhot1zOISQGXwWn6YtS+Y/1RT9WrVIOywZnJHo3jCDyewAc79aKNQWFCQm+xNHVTq9h8dZKvygoXQQRg==", + "dev": true, + "optional": true + }, "escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", @@ -40457,12 +40845,6 @@ "integrity": "sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q==", "dev": true }, - "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==", - "dev": true - }, "fs-write-stream-atomic": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz", @@ -40768,15 +41150,6 @@ "integrity": "sha512-qbHgh8Ix+j/qY+a/ZcJnFQ+j8ezakqPiHwPiZhV/3PgGlgf96QMBB5/f2rkiC9sgLoy/xvT6TSiaf2nTHJh5iA==", "dev": true }, - "gzip-size": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", - "integrity": "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==", - "dev": true, - "requires": { - "duplexer": "^0.1.2" - } - }, "handle-thing": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", @@ -42294,6 +42667,12 @@ } } }, + "joycon": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", + "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", + "dev": true + }, "js-levenshtein": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.6.tgz", @@ -43339,12 +43718,6 @@ } } }, - "mrmime": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-1.0.1.tgz", - "integrity": "sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw==", - "dev": true - }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -43969,12 +44342,6 @@ "is-wsl": "^2.2.0" } }, - "opener": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", - "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==", - "dev": true - }, "optionator": { "version": "0.8.3", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", @@ -46224,17 +46591,6 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true }, - "sirv": { - "version": "1.0.19", - "resolved": "https://registry.npmjs.org/sirv/-/sirv-1.0.19.tgz", - "integrity": "sha512-JuLThK3TnZG1TAKDwNIqNq6QA2afLOCcm+iE8D1Kj3GA40pSPsxQjjJl0J8X3tsR7T+CP1GavpzLwYkgVLWrZQ==", - "dev": true, - "requires": { - "@polka/url": "^1.0.0-next.20", - "mrmime": "^1.0.0", - "totalist": "^1.0.0" - } - }, "sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", @@ -46582,6 +46938,66 @@ "wbuf": "^1.7.3" } }, + "speed-measure-webpack-plugin": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/speed-measure-webpack-plugin/-/speed-measure-webpack-plugin-1.5.0.tgz", + "integrity": "sha512-Re0wX5CtM6gW7bZA64ONOfEPEhwbiSF/vz6e2GvadjuaPrQcHTQdRGsD8+BE7iUOysXH8tIenkPCQBEcspXsNg==", + "dev": true, + "requires": { + "chalk": "^4.1.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, "split-string": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", @@ -47403,12 +47819,6 @@ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", "dev": true }, - "totalist": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/totalist/-/totalist-1.1.0.tgz", - "integrity": "sha512-gduQwd1rOdDMGxFG1gEvhV88Oirdo2p+KjoYFU7k2g+i7n6AFFbDQ5kMPUsW0pNbfQsB/cwXvT1i4Bue0s9g5g==", - "dev": true - }, "tough-cookie": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", @@ -48488,99 +48898,6 @@ } } }, - "webpack-bundle-analyzer": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.5.0.tgz", - "integrity": "sha512-GUMZlM3SKwS8Z+CKeIFx7CVoHn3dXFcUAjT/dcZQQmfSZGvitPfMob2ipjai7ovFFqPvTqkEZ/leL4O0YOdAYQ==", - "dev": true, - "requires": { - "acorn": "^8.0.4", - "acorn-walk": "^8.0.0", - "chalk": "^4.1.0", - "commander": "^7.2.0", - "gzip-size": "^6.0.0", - "lodash": "^4.17.20", - "opener": "^1.5.2", - "sirv": "^1.0.7", - "ws": "^7.3.1" - }, - "dependencies": { - "acorn": { - "version": "8.8.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.0.tgz", - "integrity": "sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==", - "dev": true - }, - "acorn-walk": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", - "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", - "dev": true - }, - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "commander": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", - "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - }, - "ws": { - "version": "7.5.9", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", - "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", - "dev": true, - "requires": {} - } - } - }, "webpack-cli": { "version": "4.10.0", "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-4.10.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 2b4cf9cc5..f8b002b22 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -26,12 +26,6 @@ }, "license": "ISC", "devDependencies": { - "@babel/cli": "^7.17.10", - "@babel/core": "^7.18.5", - "@babel/preset-env": "^7.18.2", - "@babel/preset-react": "^7.17.12", - "@babel/preset-typescript": "^7.17.12", - "@emotion/babel-plugin": "^11.9.2", "@emotion/react": "^11.9.3", "@emotion/styled": "^11.9.3", "@storybook/addon-actions": "^6.5.9", @@ -46,18 +40,17 @@ "@storybook/testing-library": "^0.0.13", "@testing-library/cypress": "^8.0.3", "@trivago/prettier-plugin-sort-imports": "^3.2.0", - "@types/classnames": "^2.3.1", "@types/dompurify": "^2.3.3", "@types/marked": "^4.0.3", "@types/react": "^18.0.14", "@types/react-dom": "^18.0.5", "@typescript-eslint/eslint-plugin": "^5.30.0", "@typescript-eslint/parser": "^5.30.5", - "babel-loader": "^8.2.5", - "classnames": "^2.3.1", "clean-webpack-plugin": "^4.0.0", "cypress": "^10.4.0", "dotenv": "^16.0.1", + "esbuild": "^0.14.54", + "esbuild-loader": "^2.19.0", "eslint": "^8.18.0", "eslint-config-prettier": "^8.5.0", "eslint-import-resolver-typescript": "^3.1.1", @@ -70,9 +63,9 @@ "html-webpack-plugin": "^5.5.0", "msw": "^0.43.0", "prettier": "^2.7.1", + "speed-measure-webpack-plugin": "^1.5.0", "typescript": "^4.7.4", "webpack": "^5.73.0", - "webpack-bundle-analyzer": "^4.5.0", "webpack-cli": "^4.10.0", "webpack-dev-server": "^4.9.2", "webpack-merge": "^5.8.0" diff --git a/frontend/src/components/button-group/ButtonGroup.tsx b/frontend/src/components/button-group/ButtonGroup.tsx index 24b635917..17ba9b149 100644 --- a/frontend/src/components/button-group/ButtonGroup.tsx +++ b/frontend/src/components/button-group/ButtonGroup.tsx @@ -3,13 +3,14 @@ import type { MakeOptional } from '@custom-types'; import * as S from '@components/button-group/ButtonGroup.style'; export type ButtonGroupProps = { + className?: string; children: React.ReactNode; variation: 'flex-start' | 'flex-end'; }; type OptionalButtonGroupProps = MakeOptional; -const ButtonGroup: React.FC = ({ children, variation = 'flex-end' }) => { +const ButtonGroup: React.FC = ({ className, children, variation = 'flex-end' }) => { return {children}; }; diff --git a/frontend/src/components/button/Button.style.ts b/frontend/src/components/button/Button.style.ts index f12abbaf8..15d491ab6 100644 --- a/frontend/src/components/button/Button.style.ts +++ b/frontend/src/components/button/Button.style.ts @@ -3,11 +3,11 @@ import styled from '@emotion/styled'; import type { ButtonProps } from '@components/button/Button'; -const applyOutlineButtonStyle = ({ theme, isLoading }: { theme: Theme; isLoading?: boolean }) => css` +const applyOutlineButtonStyle = ({ theme }: { theme: Theme }) => css` transition: 0.3s; background-color: transparent; border: 1px solid ${theme.colors.primary.base}; - color: ${isLoading ? 'transparent' : theme.colors.primary.base}; + color: ${theme.colors.primary.base}; &:hover { background-color: ${theme.colors.primary.base}; @@ -16,57 +16,23 @@ const applyOutlineButtonStyle = ({ theme, isLoading }: { theme: Theme; isLoading } `; -export const LoadingIndicator = styled.div` - display: flex; - align-items: center; - justify-content: center; - - .spinning-loader { - width: 20px; - height: 20px; - border: 5px solid rgba(29, 161, 242, 0.2); - border-radius: 50%; - background: transparent; - animation-name: rotate-s-loader; - animation-iteration-count: infinite; - animation-duration: 1s; - animation-timing-function: linear; - position: relative; - .dot { - width: 5px; - height: 5px; - background-color: rgb(29, 161, 242); - border-radius: 50%; - position: absolute; - left: -3px; - } - - @keyframes rotate-s-loader { - from { - transform: rotate(0); - } - to { - transform: rotate(360deg); - } - } - } -`; - export const Button = styled.button` - ${({ theme, fluid, outline, isLoading }) => css` + ${({ theme, fluid, outline }) => css` width: ${fluid ? '100%' : 'auto'}; padding: 20px 10px; + min-height: 30px; text-align: center; font-size: 20px; + color: ${theme.colors.white}; + border: none; border-radius: 10px; background-color: ${theme.colors.primary.base}; - color: ${isLoading ? theme.colors.primary.base : 'white'}; white-space: nowrap; - ${outline && applyOutlineButtonStyle({ theme, isLoading })} + ${outline && applyOutlineButtonStyle({ theme })} &:hover { background-color: ${theme.colors.primary.light}; @@ -82,20 +48,3 @@ export const Button = styled.button` } `} `; - -export const ButtonContainer = styled.div` - position: relative; - height: 100%; - - ${LoadingIndicator} { - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; - } - - ${Button} { - min-height: 30px; - } -`; diff --git a/frontend/src/components/button/Button.tsx b/frontend/src/components/button/Button.tsx index eeefeab01..f192d43dc 100644 --- a/frontend/src/components/button/Button.tsx +++ b/frontend/src/components/button/Button.tsx @@ -11,22 +11,11 @@ export type ButtonProps = { fluid?: boolean; disabled?: boolean; outline?: boolean; - isLoading?: boolean; onClick?: React.MouseEventHandler; }; type OptionalButtonProps = MakeOptional; -const LoadingIndicator: React.FC = () => { - return ( - -
-
-
-
- ); -}; - const Button: React.FC = ({ className, children, @@ -34,18 +23,21 @@ const Button: React.FC = ({ fluid = true, disabled = false, outline = false, - isLoading = false, onClick: handleClick = noop, }) => { return ( - - - {/* isLoading상태에 관계 없이 children을 뿌려준다. 높이를 유지하기 위함이다. + + {/* isLoading상태에 관계 없이 children을 뿌려준다. 높이를 유지하기 위함이다. 대신 color를 background-color와 동일하게 맞춘다 */} - {children} - - {isLoading && } - + {children} + ); }; diff --git a/frontend/src/components/checkbox/Checkbox.style.tsx b/frontend/src/components/checkbox/Checkbox.style.tsx new file mode 100644 index 000000000..ecca54cbb --- /dev/null +++ b/frontend/src/components/checkbox/Checkbox.style.tsx @@ -0,0 +1,8 @@ +import styled from '@emotion/styled'; + +export const Checkbox = styled.input` + &:focus { + border: none; + outline: none; + } +`; diff --git a/frontend/src/components/checkbox/Checkbox.tsx b/frontend/src/components/checkbox/Checkbox.tsx new file mode 100644 index 000000000..0fd5a7141 --- /dev/null +++ b/frontend/src/components/checkbox/Checkbox.tsx @@ -0,0 +1,9 @@ +import * as S from './Checkbox.style'; + +export type CheckboxProps = React.HTMLProps; + +const Checkbox: React.FC = ({ className, id, checked, onChange }) => { + return ; +}; + +export default Checkbox; diff --git a/frontend/src/components/positive-number-input/PositiveNumberInput.style.tsx b/frontend/src/components/positive-number-input/PositiveNumberInput.style.tsx deleted file mode 100644 index 233e0217d..000000000 --- a/frontend/src/components/positive-number-input/PositiveNumberInput.style.tsx +++ /dev/null @@ -1,3 +0,0 @@ -import styled from '@emotion/styled'; - -export const NumberInput = styled.input``; diff --git a/frontend/src/components/positive-number-input/PositiveNumberInput.tsx b/frontend/src/components/positive-number-input/PositiveNumberInput.tsx deleted file mode 100644 index 5977ad31b..000000000 --- a/frontend/src/components/positive-number-input/PositiveNumberInput.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { forwardRef } from 'react'; - -import * as S from '@components/positive-number-input/PositiveNumberInput.style'; - -type NumberInputProps = { - id?: string; - placeholder?: string; - className?: string; - value: number; - max?: number; - onChange: (val: number | '') => void; -}; - -const nums = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']; -const arrows = ['ArrowLeft', 'ArrowRight', 'ArrowDown', 'ArrowUp']; - -const PositiveNumberInput = forwardRef(({ id, value, onChange, ...props }, ref) => { - const handleChange = ({ target }: React.ChangeEvent) => { - if (target.value === '') { - onChange(''); - return; - } - onChange(Number(target.value)); - }; - - const handleKeyDown = (e: React.KeyboardEvent) => { - const { key } = e; - if (key !== 'Backspace' && !nums.includes(key) && !arrows.includes(key)) { - e.preventDefault(); - return; - } - }; - - return ( - - ); -}); - -PositiveNumberInput.displayName = 'PositiveNumberInput'; - -export default PositiveNumberInput; diff --git a/frontend/src/hooks/usePositiveNumberInput.ts b/frontend/src/hooks/usePositiveNumberInput.ts new file mode 100644 index 000000000..69edf4121 --- /dev/null +++ b/frontend/src/hooks/usePositiveNumberInput.ts @@ -0,0 +1,15 @@ +const nums = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']; +const arrows = ['ArrowLeft', 'ArrowRight', 'ArrowDown', 'ArrowUp']; + +const usePositiveNumberInput = () => { + const handleKeyDown = (e: React.KeyboardEvent) => { + const { key } = e; + if (key !== 'Backspace' && !nums.includes(key) && !arrows.includes(key)) { + e.preventDefault(); + return; + } + }; + return { handleKeyDown }; +}; + +export default usePositiveNumberInput; diff --git a/frontend/src/layout/header/Header.style.tsx b/frontend/src/layout/header/Header.style.tsx index 69bf827fb..359bdbfe1 100644 --- a/frontend/src/layout/header/Header.style.tsx +++ b/frontend/src/layout/header/Header.style.tsx @@ -3,9 +3,6 @@ import styled from '@emotion/styled'; import { mqDown } from '@utils'; -import * as Logo from '@layout/header/components/logo/Logo.style'; -import * as SearchBar from '@layout/header/components/search-bar/SearchBar.style'; - export const SearchBarContainer = styled.div` position: absolute; left: 50%; @@ -14,6 +11,13 @@ export const SearchBarContainer = styled.div` width: 100%; max-width: 400px; + + ${mqDown('lg')} { + position: static; + left: 0; + top: 0; + transform: none; + } `; export const Row = styled.header` @@ -30,33 +34,12 @@ export const Row = styled.header` background-color: ${theme.colors.secondary.light}; border-bottom: 1px solid ${theme.colors.secondary.dark}; - ${mqDown('lg')} { - ${Logo.ImageContainer} { - margin-right: 0; - } - ${Logo.BorderText} { - display: none; - } - ${SearchBarContainer} { - position: static; - left: 0; - top: 0; - transform: none; - } - } - ${mqDown('md')} { padding: 16px 24px; - ${SearchBar.Input} { - font-size: 18px; - } } ${mqDown('sm')} { padding: 10px 12px; - ${SearchBar.Input} { - font-size: 16px; - } } `} `; diff --git a/frontend/src/layout/header/components/logo/Logo.style.tsx b/frontend/src/layout/header/components/logo/Logo.style.tsx index 2b0501bc1..7e071179a 100644 --- a/frontend/src/layout/header/components/logo/Logo.style.tsx +++ b/frontend/src/layout/header/components/logo/Logo.style.tsx @@ -1,6 +1,8 @@ import { css } from '@emotion/react'; import styled from '@emotion/styled'; +import { mqDown } from '@utils'; + export const Row = styled.div` display: flex; justify-content: center; @@ -16,6 +18,10 @@ export const ImageContainer = styled.div` img { width: 100%; } + + ${mqDown('lg')} { + margin-right: 0; + } `; export const BorderText = styled.h1` @@ -24,5 +30,9 @@ export const BorderText = styled.h1` font-size: 40px; font-weight: 800; -webkit-text-stroke: 1px ${theme.colors.primary.dark}; + + ${mqDown('lg')} { + display: none; + } `} `; diff --git a/frontend/src/layout/header/components/search-bar/SearchBar.style.tsx b/frontend/src/layout/header/components/search-bar/SearchBar.style.tsx index 58fd1bd0c..b3a48a865 100644 --- a/frontend/src/layout/header/components/search-bar/SearchBar.style.tsx +++ b/frontend/src/layout/header/components/search-bar/SearchBar.style.tsx @@ -1,6 +1,8 @@ import { css } from '@emotion/react'; import styled from '@emotion/styled'; +import { mqDown } from '@utils'; + export const Container = styled.div` position: relative; `; @@ -25,6 +27,13 @@ export const Input = styled.input` } } } + + ${mqDown('md')} { + font-size: 18px; + } + ${mqDown('sm')} { + font-size: 16px; + } `} `; diff --git a/frontend/src/pages/create-study-page/CreateStudyPage.style.tsx b/frontend/src/pages/create-study-page/CreateStudyPage.style.tsx index d94474fe4..272dceace 100644 --- a/frontend/src/pages/create-study-page/CreateStudyPage.style.tsx +++ b/frontend/src/pages/create-study-page/CreateStudyPage.style.tsx @@ -3,107 +3,45 @@ import styled from '@emotion/styled'; import { mqDown } from '@utils'; -import { DescriptionTab } from '@create-study-page/components/description-tab/DescriptionTab.style'; +import { DescriptionTab as OriginalDesriptionTab } from '@create-study-page/components/description-tab/DescriptionTab.style'; const sidebarWidth = 280; const mainGabSidebar = 40; -export const CreateStudyPage = styled.div` - ${({ theme }) => css` - input, - textarea, - select { - box-shadow: 0 0 0 transparent; - border-radius: 4px; - border: 1px solid ${theme.colors.secondary.base}; - background-color: ${theme.colors.white}; - padding: 4px 8px; +export const CreateStudyPage = styled.div``; - &:focus { - border: none; - outline: 1px solid ${theme.colors.primary.light}; - } - } +export const Form = styled.form``; - input[type='checkbox'] { - &:focus { - border: none; - outline: none; - } - } - - select { - line-height: 2; - border-color: ${theme.colors.secondary.base}; - box-shadow: none; - border-radius: 3px; - padding: 0 24px 0 8px; - min-height: 30px; - max-width: 25rem; - -webkit-appearance: none; - background: ${theme.colors.white}; - url(data:image/svg+xml;charset=US-ASCII,%3Csvg%20width%3D%2220%22%20height%3D%2220%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20d%3D%22M5%206l5%205%205-5%202%201-7%207-7-7%202-1z%22%20fill%3D%22%23555%22%2F%3E%3C%2Fsvg%3E) - no-repeat right 5px top 55%; - background-size: 16px 16px; - vertical-align: middle; - } - - textarea { - overflow: auto; - padding: 8px 10px; - } - - input.invalid, - textarea.invalid { - border: none !important; - outline: 2px solid red !important; - } +export const DescriptionTab = styled(OriginalDesriptionTab)` + margin-bottom: 15px; +`; - & > form { - & > .title { - margin-bottom: 20px; - } - & > .inner { - display: flex; - column-gap: 40px; - & > .main { - flex-grow: 1; - max-width: calc(100% - ${mainGabSidebar}px - ${sidebarWidth}px); - .title-input { - display: block; - width: 100%; - font-size: 24px; - line-height: 24px; +export const Container = styled.div` + display: flex; + column-gap: 40px; - border-radius: 4px; - border: 1px solid ${theme.colors.secondary.dark}; - background-color: ${theme.colors.white}; - color: ${theme.colors.black}; + ${mqDown('md')} { + flex-direction: column; + column-gap: 0; + } +`; - padding: 6px 12px; +export const Main = styled.div` + flex-grow: 1; + max-width: calc(100% - ${mainGabSidebar}px - ${sidebarWidth}px); - margin-bottom: 20px; - } - ${DescriptionTab} { - margin-bottom: 15px; - } - } - & > .sidebar { - min-width: 280px; - } + ${mqDown('md')} { + max-width: 100%; + margin-bottom: 15px; + } +`; - ${mqDown('md')} { - flex-direction: column; - column-gap: 0; - & > .main { - max-width: 100%; - margin-bottom: 15px; - } - & > .sidebar { - min-width: 100%; - } - } - } - } - `} +export const Sidebar = styled.ul` + min-width: 280px; + li { + margin-bottom: 15px; + } + ${mqDown('md')} { + min-width: 100%; + } `; diff --git a/frontend/src/pages/create-study-page/CreateStudyPage.tsx b/frontend/src/pages/create-study-page/CreateStudyPage.tsx index 77a8d9869..47f32cb33 100644 --- a/frontend/src/pages/create-study-page/CreateStudyPage.tsx +++ b/frontend/src/pages/create-study-page/CreateStudyPage.tsx @@ -1,5 +1,3 @@ -import { css } from '@emotion/react'; - import { FormProvider } from '@hooks/useForm'; import Wrapper from '@components/wrapper/Wrapper'; @@ -23,44 +21,36 @@ const CreateStudyPage: React.FC = () => { -
-

스터디 개설하기

-
-
+ +

스터디 개설하기

+ + <DescriptionTab /> <Excerpt /> - </div> - <div className="sidebar"> - <Publish - css={css` - margin-bottom: 15px; - `} - /> - <MaxMemberCount - css={css` - margin-bottom: 15px; - `} - /> - <Category - css={css` - margin-bottom: 15px; - `} - /> - <Subject - css={css` - margin-bottom: 15px; - `} - /> - <Period - css={css` - margin-bottom: 15px; - `} - /> - <EnrollmentEndDate /> - </div> - </div> - </form> + </S.Main> + <S.Sidebar> + <li> + <Publish /> + </li> + <li> + <MaxMemberCount /> + </li> + <li> + <Category /> + </li> + <li> + <Subject /> + </li> + <li> + <Period /> + </li> + <li> + <EnrollmentEndDate /> + </li> + </S.Sidebar> + </S.Container> + </S.Form> </FormProvider> </S.CreateStudyPage> </Wrapper> diff --git a/frontend/src/pages/create-study-page/components/category/Category.style.tsx b/frontend/src/pages/create-study-page/components/category/Category.style.tsx index 2ad6b4ce2..6b6ea3ea6 100644 --- a/frontend/src/pages/create-study-page/components/category/Category.style.tsx +++ b/frontend/src/pages/create-study-page/components/category/Category.style.tsx @@ -1,3 +1,22 @@ import styled from '@emotion/styled'; +export { Select } from '@create-study-page/components/select/Select.style'; + export const Category = styled.div``; + +export const Generation = styled.div` + margin-bottom: 6px; +`; + +export const Label = styled.label` + margin-right: 10px; +`; + +export const Area = styled.div` + display: flex; +`; + +export const AreaCheckboxContainer = styled.div` + display: flex; + margin-right: 8px; +`; diff --git a/frontend/src/pages/create-study-page/components/category/Category.tsx b/frontend/src/pages/create-study-page/components/category/Category.tsx index 1581099e3..6c6673021 100644 --- a/frontend/src/pages/create-study-page/components/category/Category.tsx +++ b/frontend/src/pages/create-study-page/components/category/Category.tsx @@ -4,6 +4,8 @@ import type { Tag } from '@custom-types'; import { useFormContext } from '@hooks/useForm'; +import Checkbox from '@components/checkbox/Checkbox'; + import * as S from '@create-study-page/components/category/Category.style'; import MetaBox from '@create-study-page/components/meta-box/MetaBox'; import useGetTagList from '@create-study-page/hooks/useGetTagList'; @@ -42,48 +44,21 @@ const Category = ({ className }: CategoryProps) => { return ( <> - <div - css={css` - margin-bottom: 6px; - `} - > - <label - css={css` - margin-right: 10px; - `} - htmlFor="generation" - > - 기수 : - </label> - <select id="generation" {...register('generation')}> + <S.Generation> + <S.Label htmlFor="generation">기수 :</S.Label> + <S.Select id="generation" {...register('generation')}> <option>선택 안함</option> {generations.map(({ id, name }) => ( <option key={id} value={id}> {name} </option> ))} - </select> - </div> - <div - css={css` - display: flex; - `} - > - <span - css={css` - margin-right: 10px; - `} - > - 영역 : - </span> - {} - <div - css={css` - display: flex; - margin-right: 8px; - `} - > - <input + </S.Select> + </S.Generation> + <S.Area> + <S.Label>영역 :</S.Label> + <S.AreaCheckboxContainer> + <Checkbox css={css` margin-right: 4px; `} @@ -93,14 +68,9 @@ const Category = ({ className }: CategoryProps) => { {...register('area-fe')} /> <label htmlFor="area-fe">FE</label> - </div> - <div - css={css` - display: flex; - margin-right: 8px; - `} - > - <input + </S.AreaCheckboxContainer> + <S.AreaCheckboxContainer> + <Checkbox css={css` margin-right: 4px; `} @@ -110,8 +80,8 @@ const Category = ({ className }: CategoryProps) => { {...register('area-be')} /> <label htmlFor="area-be">BE</label> - </div> - </div> + </S.AreaCheckboxContainer> + </S.Area> </> ); }; diff --git a/frontend/src/pages/create-study-page/components/description-tab/DescriptionTab.style.tsx b/frontend/src/pages/create-study-page/components/description-tab/DescriptionTab.style.tsx index 878110e5b..d1508e97e 100644 --- a/frontend/src/pages/create-study-page/components/description-tab/DescriptionTab.style.tsx +++ b/frontend/src/pages/create-study-page/components/description-tab/DescriptionTab.style.tsx @@ -1,79 +1,114 @@ -import { css } from '@emotion/react'; +import { Theme, css } from '@emotion/react'; import styled from '@emotion/styled'; +import { Textarea as OriginalTextArea } from '@create-study-page/components/textarea/Textarea.style'; + export const DescriptionTab = styled.div` ${({ theme }) => css` border-radius: 6px; border: 1px solid ${theme.colors.secondary.dark}; overflow: hidden; - .tab-list-container { - padding-left: 10px; - padding-top: 10px; - background-color: ${theme.colors.secondary.light}; - border-bottom: 1px solid ${theme.colors.secondary.dark}; - .tab-list { - display: flex; - .tab { - .tab-item-button { - border: none; - line-height: 24px; - font-weight: 600; - padding: 8px 16px; - transition: color 0.2s cubic-bezier(0.3, 0, 0.5, 1); - background-color: inherit; - color: ${theme.colors.secondary.base}; - - margin-bottom: -1px; - - &.active { - background-color: ${theme.colors.white}; - color: ${theme.colors.black}; - border-top-left-radius: 12px; - border-top-right-radius: 12px; - border: 1px solid ${theme.colors.secondary.dark}; - border-bottom: none; - } - } - } - } - } - .tab-panels-container { - padding: 10px; + + margin-bottom: 20px; + `} +`; + +export const TabListContainer = styled.div` + ${({ theme }) => css` + padding-left: 10px; + padding-top: 10px; + background-color: ${theme.colors.secondary.light}; + border-bottom: 1px solid ${theme.colors.secondary.dark}; + `}; +`; + +export const TabList = styled.ul` + display: flex; +`; + +export const Tab = styled.li``; + +type TabItemButtonProps = { + isActive: boolean; +}; + +const activeTabItemButtonStyle = (theme: Theme) => css` + background-color: ${theme.colors.white}; + color: ${theme.colors.black}; + border-top-left-radius: 12px; + border-top-right-radius: 12px; + border: 1px solid ${theme.colors.secondary.dark}; + border-bottom: none; +`; + +export const TabItemButton = styled.button<TabItemButtonProps>` + ${({ theme, isActive }) => css` + border: none; + line-height: 24px; + font-weight: 600; + padding: 8px 16px; + transition: color 0.2s cubic-bezier(0.3, 0, 0.5, 1); + background-color: inherit; + color: ${theme.colors.secondary.base}; + + margin-bottom: -1px; + + ${isActive && activeTabItemButtonStyle(theme)} + `} +`; + +export const Label = styled.label` + display: block; + + height: 0; + width: 0; + + visibility: hidden; +`; + +export const Textarea = styled(OriginalTextArea)` + ${({ theme }) => css` + display: block; + + width: 100%; + height: 100%; + padding: 12px; + + border-radius: 6px; + background-color: ${theme.colors.secondary.light}; + border: 1px solid ${theme.colors.secondary.dark}; + font-size: 16px; + + &:focus { background-color: ${theme.colors.white}; - height: 50vh; - .tab-panels { - min-height: 100%; - height: 100%; - .tab-panel { - height: 100%; - display: none; - - &.active { - display: block; - } - - .tab-content { - height: 100%; - - textarea { - display: block; - - width: 100%; - height: 100%; - padding: 12px; - - border-radius: 6px; - background-color: ${theme.colors.secondary.light}; - border: 1px solid ${theme.colors.secondary.dark}; - font-size: 16px; - - &:focus { - background-color: ${theme.colors.white}; - } - } - } - } - } } `} `; + +export const TabPanelsContainer = styled.div` + ${({ theme }) => css` + padding: 10px; + background-color: ${theme.colors.white}; + height: 50vh; + `} +`; + +export const TabPanels = styled.div` + min-height: 100%; + height: 100%; +`; + +type TabPanelProps = { + isActive: boolean; +}; + +export const TabPanel = styled.div<TabPanelProps>` + ${({ isActive }) => css` + height: 100%; + display: ${isActive ? 'block' : 'none'}; + `} +`; + +export const TabContent = styled.div` + height: 100%; +`; diff --git a/frontend/src/pages/create-study-page/components/description-tab/DescriptionTab.tsx b/frontend/src/pages/create-study-page/components/description-tab/DescriptionTab.tsx index 5a55bd1ed..8c3518165 100644 --- a/frontend/src/pages/create-study-page/components/description-tab/DescriptionTab.tsx +++ b/frontend/src/pages/create-study-page/components/description-tab/DescriptionTab.tsx @@ -1,8 +1,5 @@ -import cn from 'classnames'; import { useEffect, useState } from 'react'; -import { css } from '@emotion/react'; - import { DESCRIPTION_LENGTH } from '@constants'; import { makeValidationResult, useFormContext } from '@hooks/useForm'; @@ -29,6 +26,8 @@ const DescriptionTab = () => { const [activeTab, setActiveTab] = useState<TabIds>(studyDescriptionTabIds.write); + const isValid = !!errors['description']?.hasError; + const handleNavItemClick = (tabId: string) => () => { setActiveTab(tabId); }; @@ -44,49 +43,38 @@ const DescriptionTab = () => { return ( <S.DescriptionTab> - <div className="tab-list-container"> - <ul className="tab-list"> - <li className="tab"> - <button - className={cn('tab-item-button', { active: activeTab === studyDescriptionTabIds.write })} + <S.TabListContainer> + <S.TabList> + <S.Tab> + <S.TabItemButton type="button" + isActive={activeTab === studyDescriptionTabIds.write} onClick={handleNavItemClick(studyDescriptionTabIds.write)} > Write - </button> - </li> - <li className="tab"> - <button - className={cn('tab-item-button', { active: activeTab === studyDescriptionTabIds.preview })} + </S.TabItemButton> + </S.Tab> + <S.Tab> + <S.TabItemButton type="button" + isActive={activeTab === studyDescriptionTabIds.preview} onClick={handleNavItemClick(studyDescriptionTabIds.preview)} > Preview - </button> - </li> - </ul> - </div> - <div className="tab-panels-container"> - <div className="tab-panels"> - <div className={cn('tab-panel', { active: activeTab === studyDescriptionTabIds.write })}> - <div className="tab-content"> - <label // TODO: HiddenLabel Component 생성 - htmlFor="description" - css={css` - display: block; - - height: 0; - width: 0; - - visibility: hidden; - `} - > - 소개글 - </label> - <textarea + </S.TabItemButton> + </S.Tab> + </S.TabList> + </S.TabListContainer> + <S.TabPanelsContainer> + <S.TabPanels> + <S.TabPanel isActive={activeTab === studyDescriptionTabIds.write}> + <S.TabContent> + {/* TODO: HiddenLabel Component 생성 */} + <S.Label htmlFor="description">소개글</S.Label> + <S.Textarea id="description" placeholder={`*스터디 소개글(${DESCRIPTION_LENGTH.MAX.VALUE}자 제한)`} - className={cn({ invalid: !!errors['description']?.hasError })} + isValid={isValid} {...register('description', { validate: (val: string) => { if (val.length < DESCRIPTION_LENGTH.MIN.VALUE) { @@ -99,16 +87,16 @@ const DescriptionTab = () => { maxLength: DESCRIPTION_LENGTH.MAX.VALUE, required: true, })} - ></textarea> - </div> - </div> - <div className={cn('tab-panel', { active: activeTab === studyDescriptionTabIds.preview })}> - <div className="tab-content"> + ></S.Textarea> + </S.TabContent> + </S.TabPanel> + <S.TabPanel isActive={activeTab === studyDescriptionTabIds.preview}> + <S.TabContent> <MarkdownRender markdownContent={description} /> - </div> - </div> - </div> - </div> + </S.TabContent> + </S.TabPanel> + </S.TabPanels> + </S.TabPanelsContainer> </S.DescriptionTab> ); }; diff --git a/frontend/src/pages/create-study-page/components/enrollment-end-date/EnrollmentEndDate.style.tsx b/frontend/src/pages/create-study-page/components/enrollment-end-date/EnrollmentEndDate.style.tsx index 748e5611c..d790dca41 100644 --- a/frontend/src/pages/create-study-page/components/enrollment-end-date/EnrollmentEndDate.style.tsx +++ b/frontend/src/pages/create-study-page/components/enrollment-end-date/EnrollmentEndDate.style.tsx @@ -1,3 +1,9 @@ import styled from '@emotion/styled'; +export { Input } from '@create-study-page/components/input/Input.style'; + export const EnrollmentEndDate = styled.div``; + +export const Label = styled.label` + margin-right: 10px; +`; diff --git a/frontend/src/pages/create-study-page/components/enrollment-end-date/EnrollmentEndDate.tsx b/frontend/src/pages/create-study-page/components/enrollment-end-date/EnrollmentEndDate.tsx index 4ef6ad5d9..300d6a2e1 100644 --- a/frontend/src/pages/create-study-page/components/enrollment-end-date/EnrollmentEndDate.tsx +++ b/frontend/src/pages/create-study-page/components/enrollment-end-date/EnrollmentEndDate.tsx @@ -1,7 +1,5 @@ import { useMemo } from 'react'; -import { css } from '@emotion/react'; - import { getNextYear, getToday } from '@utils'; import { useFormContext } from '@hooks/useForm'; @@ -24,21 +22,14 @@ const EnrollmentEndDate = ({ className }: PeriodProps) => { <MetaBox.Title>스터디 신청 마감일</MetaBox.Title> <MetaBox.Content> <div> - <label - htmlFor="enrollment-end-date" - css={css` - margin-right: 10px; - `} - > - 마감일자 : - </label> - <input + <S.Label htmlFor="enrollment-end-date">마감일자 :</S.Label> + <S.Input type="date" id="enrollment-end-date" min={today} max={nextYear} {...register('enrollment-end-date')} - ></input> + /> </div> </MetaBox.Content> </MetaBox> diff --git a/frontend/src/pages/create-study-page/components/excerpt/Excerpt.style.tsx b/frontend/src/pages/create-study-page/components/excerpt/Excerpt.style.tsx index bcd83cec5..98f54c9a5 100644 --- a/frontend/src/pages/create-study-page/components/excerpt/Excerpt.style.tsx +++ b/frontend/src/pages/create-study-page/components/excerpt/Excerpt.style.tsx @@ -1,10 +1,34 @@ import styled from '@emotion/styled'; -export const Excerpt = styled.div` - textarea { - display: block; - min-height: 50px; - width: 100%; - font-size: 16px; - } +import { Textarea as OriginalTextarea } from '@create-study-page/components/textarea/Textarea.style'; + +export const Excerpt = styled.div``; + +export const Container = styled.div` + position: relative; +`; + +export const Label = styled.label` + display: block; + + height: 0; + width: 0; + + visibility: hidden; +`; + +export const LetterCounterContainer = styled.div` + position: absolute; + right: 4px; + bottom: 2px; + + display: flex; + justify-content: flex-end; +`; + +export const Textarea = styled(OriginalTextarea)` + display: block; + min-height: 50px; + width: 100%; + font-size: 16px; `; diff --git a/frontend/src/pages/create-study-page/components/excerpt/Excerpt.tsx b/frontend/src/pages/create-study-page/components/excerpt/Excerpt.tsx index 95be64895..12e39d4da 100644 --- a/frontend/src/pages/create-study-page/components/excerpt/Excerpt.tsx +++ b/frontend/src/pages/create-study-page/components/excerpt/Excerpt.tsx @@ -1,7 +1,3 @@ -import cn from 'classnames'; - -import { css } from '@emotion/react'; - import { EXCERPT_LENGTH } from '@constants'; import { makeValidationResult, useFormContext } from '@hooks/useForm'; @@ -29,40 +25,16 @@ const Excerpt = ({ className }: ExcerptProps) => { <MetaBox> <MetaBox.Title>한줄소개</MetaBox.Title> <MetaBox.Content> - <div - css={css` - position: relative; - `} - > - <div - css={css` - position: absolute; - right: 4px; - bottom: 2px; - - display: flex; - justify-content: flex-end; - `} - > + <S.Container> + <S.LetterCounterContainer> <LetterCounter count={count} maxCount={maxCount} /> - </div> - <label // TODO: HiddenLabel Component 생성 - htmlFor="excerpt" - css={css` - display: block; - - height: 0; - width: 0; - - visibility: hidden; - `} - > - 소개글 - </label> - <textarea + </S.LetterCounterContainer> + {/* TODO: HiddenLabel Component 생성 */} + <S.Label htmlFor="excerpt">소개글</S.Label> + <S.Textarea id="excerpt" placeholder="*한줄소개를 입력해주세요" - className={cn({ invalid: !!errors['excerpt']?.hasError })} + isValid={!!errors['excerpt']?.hasError} {...register('excerpt', { validate: (val: string) => { if (val.length < EXCERPT_LENGTH.MIN.VALUE) { @@ -79,7 +51,7 @@ const Excerpt = ({ className }: ExcerptProps) => { required: true, })} /> - </div> + </S.Container> </MetaBox.Content> </MetaBox> </S.Excerpt> diff --git a/frontend/src/pages/create-study-page/components/input/Input.style.tsx b/frontend/src/pages/create-study-page/components/input/Input.style.tsx new file mode 100644 index 000000000..47e05ce91 --- /dev/null +++ b/frontend/src/pages/create-study-page/components/input/Input.style.tsx @@ -0,0 +1,28 @@ +import { css } from '@emotion/react'; +import styled from '@emotion/styled'; + +const invalidInputStyle = () => css` + border: none !important; + outline: 2px solid red !important; +`; + +type InputProps = { + isValid?: boolean; +}; + +export const Input = styled.input<InputProps>` + ${({ theme, isValid }) => css` + box-shadow: 0 0 0 transparent; + border-radius: 4px; + border: 1px solid ${theme.colors.secondary.base}; + background-color: ${theme.colors.white}; + padding: 4px 8px; + + &:focus { + border: 1px solid ${theme.colors.primary.light}; + outline: 1px solid transparent; + } + + ${isValid && invalidInputStyle()} + `} +`; diff --git a/frontend/src/pages/create-study-page/components/max-member-count/MaxMemberCount.style.tsx b/frontend/src/pages/create-study-page/components/max-member-count/MaxMemberCount.style.tsx index 08d238ece..0a6446fc7 100644 --- a/frontend/src/pages/create-study-page/components/max-member-count/MaxMemberCount.style.tsx +++ b/frontend/src/pages/create-study-page/components/max-member-count/MaxMemberCount.style.tsx @@ -1,3 +1,20 @@ import styled from '@emotion/styled'; +import OriginalCheckbox from '@components/checkbox/Checkbox'; + +export { Input } from '@create-study-page/components/input/Input.style'; + export const MaxMemberCount = styled.div``; + +export const Container = styled.div` + display: flex; + margin-bottom: 8px; +`; + +export const Checkbox = styled(OriginalCheckbox)` + margin-left: 4px; +`; + +export const Label = styled.label` + margin-right: 10px; +`; diff --git a/frontend/src/pages/create-study-page/components/max-member-count/MaxMemberCount.tsx b/frontend/src/pages/create-study-page/components/max-member-count/MaxMemberCount.tsx index 7735df37a..3668fbe63 100644 --- a/frontend/src/pages/create-study-page/components/max-member-count/MaxMemberCount.tsx +++ b/frontend/src/pages/create-study-page/components/max-member-count/MaxMemberCount.tsx @@ -1,12 +1,9 @@ import { useState } from 'react'; -import { css } from '@emotion/react'; - import { MEMBER_COUNT } from '@constants'; import { useFormContext } from '@hooks/useForm'; - -import PositiveNumberInput from '@components/positive-number-input/PositiveNumberInput'; +import usePositiveNumberInput from '@hooks/usePositiveNumberInput'; import * as S from '@create-study-page/components/max-member-count/MaxMemberCount.style'; import MetaBox from '@create-study-page/components/meta-box/MetaBox'; @@ -25,6 +22,8 @@ const MaxMemberCount = ({ className }: MaxMemberCountProps) => { const { register } = useFormContext(); + const { handleKeyDown } = usePositiveNumberInput(); + const handleNoSelectCheckboxChange = () => { setWillSelectMaxMember(prev => { if (prev) removeField(maxMemberCountName); @@ -37,45 +36,26 @@ const MaxMemberCount = ({ className }: MaxMemberCountProps) => { <MetaBox> <MetaBox.Title>스터디 최대 인원</MetaBox.Title> <MetaBox.Content> - <div - css={css` - display: flex; - margin-bottom: 8px; - `} - > - <label htmlFor="no-select">선택 안함</label> - <input - css={css` - margin-left: 4px; - `} - type="checkbox" - id="no-select" - checked={!willSelectMaxMember} - onChange={handleNoSelectCheckboxChange} - /> - </div> + <S.Container> + <S.Label htmlFor="no-select">선택 안함</S.Label> + <S.Checkbox id="no-select" checked={!willSelectMaxMember} onChange={handleNoSelectCheckboxChange} /> + </S.Container> {willSelectMaxMember && ( <> - <label - htmlFor={maxMemberCountName} - css={css` - margin-right: 10px; - `} - > - 최대 인원 : - </label> - <PositiveNumberInput + <S.Label htmlFor={maxMemberCountName}>최대 인원 :</S.Label> + <S.Input {...register(maxMemberCountName, { min: MEMBER_COUNT.MIN.VALUE, max: MEMBER_COUNT.MAX.VALUE, })} + onKeyDown={handleKeyDown} + type="number" id={maxMemberCountName} placeholder="최대 인원" - value={count} onChange={value => { setCount(Number(value)); }} - ></PositiveNumberInput> + /> </> )} </MetaBox.Content> diff --git a/frontend/src/pages/create-study-page/components/meta-box/MetaBox.tsx b/frontend/src/pages/create-study-page/components/meta-box/MetaBox.tsx index c13f83789..db783cebd 100644 --- a/frontend/src/pages/create-study-page/components/meta-box/MetaBox.tsx +++ b/frontend/src/pages/create-study-page/components/meta-box/MetaBox.tsx @@ -1,14 +1,13 @@ -import cn from 'classnames'; import { ReactNode } from 'react'; import * as S from '@create-study-page/components/meta-box/MetaBox.style'; const MetaBoxTitle = ({ className, children }: { className?: string; children: ReactNode }) => { - return <h2 className={cn('title', className)}>{children}</h2>; + return <h2 className={className}>{children}</h2>; }; const MetaBoxContent = ({ className, children }: { className?: string; children: ReactNode }) => { - return <div className={cn('content', className)}>{children}</div>; + return <div className={className}>{children}</div>; }; const MetaBox = ({ className, children }: { className?: string; children: ReactNode }) => { diff --git a/frontend/src/pages/create-study-page/components/period/Period.style.tsx b/frontend/src/pages/create-study-page/components/period/Period.style.tsx index bb255b571..c741db64f 100644 --- a/frontend/src/pages/create-study-page/components/period/Period.style.tsx +++ b/frontend/src/pages/create-study-page/components/period/Period.style.tsx @@ -1,3 +1,13 @@ import styled from '@emotion/styled'; +export { Input } from '@create-study-page/components/input/Input.style'; + export const Period = styled.div``; + +export const Container = styled.div` + margin-bottom: 12px; +`; + +export const Label = styled.label` + margin-right: 10px; +`; diff --git a/frontend/src/pages/create-study-page/components/period/Peroid.tsx b/frontend/src/pages/create-study-page/components/period/Peroid.tsx index b08c6aee5..7d9ab6d48 100644 --- a/frontend/src/pages/create-study-page/components/period/Peroid.tsx +++ b/frontend/src/pages/create-study-page/components/period/Peroid.tsx @@ -25,20 +25,9 @@ const Period = ({ className }: PeriodProps) => { <MetaBox> <MetaBox.Title>스터디 운영 기간</MetaBox.Title> <MetaBox.Content> - <div - css={css` - margin-bottom: 12px; - `} - > - <label - htmlFor="start-date" - css={css` - margin-right: 10px; - `} - > - *스터디 시작 : - </label> - <input + <S.Container> + <S.Label htmlFor="start-date">*스터디 시작 :</S.Label> + <S.Input type="date" id="start-date" min={today} @@ -49,18 +38,11 @@ const Period = ({ className }: PeriodProps) => { max: nextYear, required: true, })} - ></input> - </div> + /> + </S.Container> <div> - <label - htmlFor="end-date" - css={css` - margin-right: 10px; - `} - > - 스터디 종료 : - </label> - <input type="date" id="end-date" min={today} max={nextYear} {...register('end-date')}></input> + <S.Label htmlFor="end-date">스터디 종료 :</S.Label> + <S.Input type="date" id="end-date" min={today} max={nextYear} {...register('end-date')} /> </div> </MetaBox.Content> </MetaBox> diff --git a/frontend/src/pages/create-study-page/components/publish/Publish.tsx b/frontend/src/pages/create-study-page/components/publish/Publish.tsx index 57b8448db..a88ca7e5a 100644 --- a/frontend/src/pages/create-study-page/components/publish/Publish.tsx +++ b/frontend/src/pages/create-study-page/components/publish/Publish.tsx @@ -13,8 +13,6 @@ type PublishProps = { }; const Publish = ({ className, onPublishButtonClick: handlePublishButtonClick }: PublishProps) => { - const { formState } = useFormContext(); - return ( <S.Publish className={className}> <MetaBox> @@ -29,7 +27,6 @@ const Publish = ({ className, onPublishButtonClick: handlePublishButtonClick }: fluid={true} onClick={handlePublishButtonClick} outline={true} - isLoading={formState.isSubmitting} > 개설하기 </Button> diff --git a/frontend/src/pages/create-study-page/components/select/Select.style.tsx b/frontend/src/pages/create-study-page/components/select/Select.style.tsx new file mode 100644 index 000000000..40b990e71 --- /dev/null +++ b/frontend/src/pages/create-study-page/components/select/Select.style.tsx @@ -0,0 +1,22 @@ +import { css } from '@emotion/react'; +import styled from '@emotion/styled'; + +import { Input } from '@create-study-page/components/input/Input.style'; + +export const Select = styled(Input.withComponent('select'))` + ${({ theme }) => css` + line-height: 2; + border-color: ${theme.colors.secondary.base}; + box-shadow: none; + border-radius: 3px; + padding: 0 24px 0 8px; + min-height: 30px; + max-width: 25rem; + -webkit-appearance: none; + background: ${theme.colors.white}; + url(data:image/svg+xml;charset=US-ASCII,%3Csvg%20width%3D%2220%22%20height%3D%2220%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20d%3D%22M5%206l5%205%205-5%202%201-7%207-7-7%202-1z%22%20fill%3D%22%23555%22%2F%3E%3C%2Fsvg%3E) + no-repeat right 5px top 55%; + background-size: 16px 16px; + vertical-align: middle; + `} +`; diff --git a/frontend/src/pages/create-study-page/components/subject/Subject.style.tsx b/frontend/src/pages/create-study-page/components/subject/Subject.style.tsx index 055ca3eec..e33c64767 100644 --- a/frontend/src/pages/create-study-page/components/subject/Subject.style.tsx +++ b/frontend/src/pages/create-study-page/components/subject/Subject.style.tsx @@ -1,3 +1,5 @@ import styled from '@emotion/styled'; +export { Select } from '@create-study-page/components/select/Select.style'; + export const Subject = styled.div``; diff --git a/frontend/src/pages/create-study-page/components/subject/Subject.tsx b/frontend/src/pages/create-study-page/components/subject/Subject.tsx index a83c25c9e..15d5f3a7c 100644 --- a/frontend/src/pages/create-study-page/components/subject/Subject.tsx +++ b/frontend/src/pages/create-study-page/components/subject/Subject.tsx @@ -24,7 +24,7 @@ const Subject = ({ className }: SubjectProps) => { const subjects = tags.filter(({ category }) => category.name === 'subject'); return ( - <select + <S.Select id="subject-list" css={css` width: 100%; @@ -36,7 +36,7 @@ const Subject = ({ className }: SubjectProps) => { {description} </option> ))} - </select> + </S.Select> ); } }; diff --git a/frontend/src/pages/create-study-page/components/textarea/Textarea.style.tsx b/frontend/src/pages/create-study-page/components/textarea/Textarea.style.tsx new file mode 100644 index 000000000..229903f0d --- /dev/null +++ b/frontend/src/pages/create-study-page/components/textarea/Textarea.style.tsx @@ -0,0 +1,8 @@ +import styled from '@emotion/styled'; + +import { Input } from '@create-study-page/components/input/Input.style'; + +export const Textarea = styled(Input.withComponent('textarea'))` + overflow: auto; + padding: 8px 10px; +`; diff --git a/frontend/src/pages/create-study-page/components/title/Title.style.tsx b/frontend/src/pages/create-study-page/components/title/Title.style.tsx new file mode 100644 index 000000000..7389f760d --- /dev/null +++ b/frontend/src/pages/create-study-page/components/title/Title.style.tsx @@ -0,0 +1,44 @@ +import { css } from '@emotion/react'; +import styled from '@emotion/styled'; + +import { Input as OriginalInput } from '@create-study-page/components/input/Input.style'; + +export const Container = styled.div` + position: relative; +`; + +export const LetterCounterContainer = styled.div` + position: absolute; + right: 4px; + bottom: 2px; + + display: flex; + justify-content: flex-end; +`; + +export const Label = styled.label` + display: block; + + height: 0; + width: 0; + + visibility: hidden; +`; + +export const Input = styled(OriginalInput)` + ${({ theme }) => css` + display: block; + width: 100%; + font-size: 24px; + line-height: 24px; + + border-radius: 4px; + border: 1px solid ${theme.colors.secondary.dark}; + background-color: ${theme.colors.white}; + color: ${theme.colors.black}; + + padding: 6px 12px; + + margin-bottom: 20px; + `} +`; diff --git a/frontend/src/pages/create-study-page/components/title/Title.tsx b/frontend/src/pages/create-study-page/components/title/Title.tsx index b4eebfef5..d339289d2 100644 --- a/frontend/src/pages/create-study-page/components/title/Title.tsx +++ b/frontend/src/pages/create-study-page/components/title/Title.tsx @@ -1,55 +1,31 @@ -import cn from 'classnames'; import { useState } from 'react'; -import { css } from '@emotion/react'; - import { TITLE_LENGTH } from '@constants'; import { makeValidationResult, useFormContext } from '@hooks/useForm'; import LetterCounter from '@components/letter-counter/LetterCounter'; +import * as S from '@create-study-page/components/title/Title.style'; + const Title: React.FC = () => { const { register, formState } = useFormContext(); const { errors } = formState; const [count, setCount] = useState(0); + const isValid = !!errors['title']?.hasError; return ( - <div - css={css` - position: relative; - `} - > - <div - css={css` - position: absolute; - right: 4px; - bottom: 2px; - - display: flex; - justify-content: flex-end; - `} - > + <S.Container> + <S.LetterCounterContainer> <LetterCounter count={count} maxCount={TITLE_LENGTH.MAX.VALUE} /> - </div> - <label // TODO: HiddenLabel Component 생성 - htmlFor="title" - css={css` - display: block; - - height: 0; - width: 0; - - visibility: hidden; - `} - > - 스터디 이름 - </label> - <input + </S.LetterCounterContainer> + {/* TODO: HiddenLabel Component 생성 */} + <S.Label htmlFor="title">스터디 이름</S.Label> + <S.Input id="title" - className={cn('title-input', { invalid: !!errors['title']?.hasError })} type="text" placeholder="*스터디 이름" + isValid={isValid} {...register('title', { validate: (val: string) => { if (val.length < TITLE_LENGTH.MIN.VALUE) { @@ -67,7 +43,7 @@ const Title: React.FC = () => { required: true, })} /> - </div> + </S.Container> ); }; diff --git a/frontend/src/pages/study-room-page/components/review-tab-panel/components/review-comment/ReviewComment.style.tsx b/frontend/src/pages/study-room-page/components/review-tab-panel/components/review-comment/ReviewComment.style.tsx index b2750458b..cf52e5921 100644 --- a/frontend/src/pages/study-room-page/components/review-tab-panel/components/review-comment/ReviewComment.style.tsx +++ b/frontend/src/pages/study-room-page/components/review-tab-panel/components/review-comment/ReviewComment.style.tsx @@ -30,14 +30,14 @@ export const Content = styled.div` line-height: 20px; `; -export const DropDownMenu = styled.ul` - ${({ theme }) => css` +export const DropDownMenu = styled.ul<DropDownProps>` + ${({ theme, isOpen }) => css` position: absolute; top: calc(100% + 3px); right: 6px; z-index: 3; - display: flex; + display: ${isOpen ? 'flex' : 'none'}; flex-direction: column; row-gap: 8px; @@ -63,14 +63,8 @@ export const DropDownMenu = styled.ul` `} `; -export const DropDown = styled.div<DropDownProps>` - ${({ isOpen }) => css` - position: relative; - - ${DropDownMenu} { - display: ${isOpen ? 'flex' : 'none'}; - } - `} +export const DropDown = styled.div` + position: relative; `; export const ReviewCommentHead = styled.div` diff --git a/frontend/src/pages/study-room-page/components/review-tab-panel/components/review-edit-form/ReviewEditForm.style.tsx b/frontend/src/pages/study-room-page/components/review-tab-panel/components/review-edit-form/ReviewEditForm.style.tsx index 3953c85ec..39479dd8f 100644 --- a/frontend/src/pages/study-room-page/components/review-tab-panel/components/review-edit-form/ReviewEditForm.style.tsx +++ b/frontend/src/pages/study-room-page/components/review-tab-panel/components/review-edit-form/ReviewEditForm.style.tsx @@ -65,12 +65,13 @@ export const ReviewEditFormHead = styled(ReviewFormHead)``; export const ReviewEditFormBody = styled(ReviewFormBody)``; export const ReviewEditFormFooter = styled(ReviewFormFooter)` - ${ButtonGroup} { - column-gap: 10px; - } button { border-radius: 4px; padding: 8px 10px; font-size: 12px; } `; + +export const ReviewEditFormFooterButtonGroup = styled(ButtonGroup)` + column-gap: 10px; +`; diff --git a/frontend/src/pages/study-room-page/components/review-tab-panel/components/review-edit-form/ReviewEditForm.tsx b/frontend/src/pages/study-room-page/components/review-tab-panel/components/review-edit-form/ReviewEditForm.tsx index f5d3070b2..a83b0aa42 100644 --- a/frontend/src/pages/study-room-page/components/review-tab-panel/components/review-edit-form/ReviewEditForm.tsx +++ b/frontend/src/pages/study-room-page/components/review-tab-panel/components/review-edit-form/ReviewEditForm.tsx @@ -12,7 +12,6 @@ import { makeValidationResult, useForm } from '@hooks/useForm'; import type { UseFormSubmitResult } from '@hooks/useForm'; import Avatar from '@components/avatar/Avatar'; -import ButtonGroup from '@components/button-group/ButtonGroup'; import Button from '@components/button/Button'; import LetterCounter from '@components/letter-counter/LetterCounter'; import useLetterCount from '@components/letter-counter/useLetterCount'; @@ -97,12 +96,12 @@ const ReviewEditForm: React.FC<ReviewEditFormProps> = ({ </S.ReviewEditFormBody> <S.ReviewEditFormFooter> <LetterCounter count={count} maxCount={maxCount} /> - <ButtonGroup variation="flex-end"> + <S.ReviewEditFormFooterButtonGroup variation="flex-end"> <S.CancelButton type="button" onClick={onCancelEditBtnClick}> 취소 </S.CancelButton> <Button>수정</Button> - </ButtonGroup> + </S.ReviewEditFormFooterButtonGroup> </S.ReviewEditFormFooter> </S.ReviewEditForm> ); diff --git a/frontend/webpack/webpack.common.js b/frontend/webpack/webpack.common.js index 85fe92ee9..57e9401f7 100644 --- a/frontend/webpack/webpack.common.js +++ b/frontend/webpack/webpack.common.js @@ -4,6 +4,8 @@ const { join, resolve } = require('path'); const HtmlWebpackPlugin = require('html-webpack-plugin'); const { CleanWebpackPlugin } = require('clean-webpack-plugin'); +const { ESBuildMinifyPlugin } = require('esbuild-loader'); + module.exports = { mode: 'development', entry: join(__dirname, '../src/index.tsx'), @@ -17,7 +19,11 @@ module.exports = { { test: /\.(ts|tsx)$/, exclude: /node_modules/, - use: ['babel-loader'], + loader: 'esbuild-loader', + options: { + loader: 'tsx', + target: 'es2022', + }, }, { test: /\.(png|jpg|jpeg)$/i, @@ -58,4 +64,11 @@ module.exports = { '@mocks': resolve(__dirname, '../src/mocks'), }, }, + optimization: { + minimizer: [ + new ESBuildMinifyPlugin({ + target: 'es2020', + }), + ], + }, }; From 9a174a4d4a983e97ffe95a37790c13c2f031c91d Mon Sep 17 00:00:00 2001 From: Donggyu <a29661498@gmail.com> Date: Fri, 12 Aug 2022 13:49:39 +0900 Subject: [PATCH 07/51] =?UTF-8?q?feat:=20Refresh=20Token=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9=ED=95=98=EA=B8=B0=20(#236)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: Refresh Token 추가 * feat: 만료 토큰 예외 처리 및 액세스 토큰 재발급 구현 * feat: 토큰 재발급 요청 처리 구현 * feat: 리프래시 토큰 쿠키에 담기로 변경 * feat: RefreshToken을 통해 AccessToken 재발급 구현 * test: 테스트 수정 * feat: RefreshToken 저장 로직 추가 * feat: DB 검증 코드 추가 * fix: STRING 수정 * feat: index.adoc 수정 * docs: index.adoc 수정 * feat: Origin 설정 * feat: 로그아웃 기능 구현 * feat: 4001 코드 추가 & expiredTime 추가 * feat: 피드백 반영 및 application.yml 수정 * feat: 쿠키 sameSite 제거 및 Transactional 명시 * refactor: 테스트 코드 수정 * refactor: response DTO 네이밍 변경 * fix: INTERNAL_SERVER_ERROR -> UNAUTHORIZED 로 수정 * refactor: 메소드 AuthAcceptanceTest 로 내림 --- backend/src/docs/asciidoc/index.adoc | 6 + backend/src/docs/asciidoc/index.html | 4 +- .../auth/config/AuthRequestMatchConfig.java | 14 +- .../auth/config/AuthenticationExtractor.java | 4 +- .../auth/controller/AuthController.java | 52 +++++++- .../AuthenticationArgumentResolver.java | 2 +- .../woowacourse/moamoa/auth/domain/Token.java | 36 ++++++ .../domain/repository/TokenRepository.java | 10 ++ .../RefreshTokenExpirationException.java | 9 ++ .../exception/TokenExpirationException.java | 10 ++ .../exception/TokenNotFoundException.java | 10 ++ .../auth/infrastructure/JwtTokenProvider.java | 61 ++++++++- .../auth/infrastructure/TokenProvider.java | 8 +- .../moamoa/auth/service/AuthService.java | 52 +++++++- .../response/GithubProfileResponse.java | 14 +- .../service/request/AccessTokenRequest.java | 8 -- .../service/response/AccessTokenResponse.java | 12 ++ .../auth/service/response/TokenResponse.java | 14 -- .../auth/service/response/TokensResponse.java | 12 ++ .../common/advice/CommonControllerAdvice.java | 28 ++-- .../common/advice/response/ErrorResponse.java | 10 ++ .../moamoa/common/config/WebConfig.java | 4 +- .../member/controller/MemberController.java | 8 +- .../acceptance/AcceptanceTest.java | 14 +- .../acceptance/steps/LoginSteps.java | 15 ++- .../test/auth/AuthAcceptanceTest.java | 92 ++++++++++++- .../test/cors/CorsAcceptanceTest.java | 2 - .../test/member/MemberAcceptanceTest.java | 4 +- .../auth/controller/AuthControllerTest.java | 10 +- .../AuthenticationArgumentResolverTest.java | 2 +- .../AuthenticationInterceptorTest.java | 2 +- .../infrastructure/TokenProviderTest.java | 8 +- .../moamoa/auth/service/AuthServiceTest.java | 121 +++++++++++++++--- .../member/webmvc/MemberWebMvcTest.java | 2 +- .../SearchingReviewControllerTest.java | 5 +- .../webmvc/BadRequestReviewWebMvcTest.java | 4 +- .../BadRequestMyMemberRoleWebMvcTest.java | 4 +- backend/src/test/resources/schema.sql | 8 ++ 38 files changed, 539 insertions(+), 142 deletions(-) create mode 100644 backend/src/main/java/com/woowacourse/moamoa/auth/domain/Token.java create mode 100644 backend/src/main/java/com/woowacourse/moamoa/auth/domain/repository/TokenRepository.java create mode 100644 backend/src/main/java/com/woowacourse/moamoa/auth/exception/RefreshTokenExpirationException.java create mode 100644 backend/src/main/java/com/woowacourse/moamoa/auth/exception/TokenExpirationException.java create mode 100644 backend/src/main/java/com/woowacourse/moamoa/auth/exception/TokenNotFoundException.java create mode 100644 backend/src/main/java/com/woowacourse/moamoa/auth/service/response/AccessTokenResponse.java delete mode 100644 backend/src/main/java/com/woowacourse/moamoa/auth/service/response/TokenResponse.java create mode 100644 backend/src/main/java/com/woowacourse/moamoa/auth/service/response/TokensResponse.java diff --git a/backend/src/docs/asciidoc/index.adoc b/backend/src/docs/asciidoc/index.adoc index ffcc4d65e..05dd77482 100644 --- a/backend/src/docs/asciidoc/index.adoc +++ b/backend/src/docs/asciidoc/index.adoc @@ -12,6 +12,12 @@ === Github 로그인 operation::auth/login[snippets='http-request,request-parameters,http-response,response-fields'] +=== 리프래시 토큰 +operation::auth/refresh[snippets='http-request,http-response'] + +=== 로그아웃 +operation::auth/logout[snippets='http-request,http-response'] + [[Member]] == 회원 diff --git a/backend/src/docs/asciidoc/index.html b/backend/src/docs/asciidoc/index.html index 193bfdad2..870bb1bbc 100644 --- a/backend/src/docs/asciidoc/index.html +++ b/backend/src/docs/asciidoc/index.html @@ -475,7 +475,7 @@ <h3 id="_github_로그인"><a class="link" href="#_github_로그인">Github 로 <h4 id="_github_로그인_http_request"><a class="link" href="#_github_로그인_http_request">HTTP request</a></h4> <div class="listingblock"> <div class="content"> -<pre class="highlightjs highlight nowrap"><code class="language-http hljs" data-lang="http">POST /api/login/token?code=authorization-code HTTP/1.1 +<pre class="highlightjs highlight nowrap"><code class="language-http hljs" data-lang="http">POST /api/auth/login?code=authorization-code HTTP/1.1 Content-Type: application/json Accept: application/json Host: localhost:8080</code></pre> @@ -890,4 +890,4 @@ <h4 id="My-Role_response_fields"><a class="link" href="#My-Role_response_fields" } </script> </body> -</html> \ No newline at end of file +</html> diff --git a/backend/src/main/java/com/woowacourse/moamoa/auth/config/AuthRequestMatchConfig.java b/backend/src/main/java/com/woowacourse/moamoa/auth/config/AuthRequestMatchConfig.java index 821ca7459..ca6abeb60 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/auth/config/AuthRequestMatchConfig.java +++ b/backend/src/main/java/com/woowacourse/moamoa/auth/config/AuthRequestMatchConfig.java @@ -1,11 +1,15 @@ package com.woowacourse.moamoa.auth.config; +import static org.springframework.http.HttpMethod.DELETE; +import static org.springframework.http.HttpMethod.GET; +import static org.springframework.http.HttpMethod.POST; +import static org.springframework.http.HttpMethod.PUT; + import com.woowacourse.moamoa.auth.controller.matcher.AuthenticationRequestMatcher; import com.woowacourse.moamoa.auth.controller.matcher.AuthenticationRequestMatcherBuilder; import lombok.AllArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.http.HttpMethod; @Configuration @AllArgsConstructor @@ -14,10 +18,10 @@ public class AuthRequestMatchConfig { @Bean public AuthenticationRequestMatcher authenticationRequestMatcher() { return new AuthenticationRequestMatcherBuilder() - .addUpAuthenticationPath(HttpMethod.POST, "/api/studies", "/api/studies/\\d+/reviews", "/api/studies/\\d+/reviews/\\d+") - .addUpAuthenticationPath(HttpMethod.GET, "/api/my/studies", "/api/members/me", "/api/members/me/role") - .addUpAuthenticationPath(HttpMethod.PUT, "/api/studies/\\d+/reviews/\\d+") - .addUpAuthenticationPath(HttpMethod.DELETE, "/api/studies/\\d+/reviews/\\d+") + .addUpAuthenticationPath(POST, "/api/studies", "/api/studies/\\d+/reviews", "/api/studies/\\d+/reviews/\\d+") + .addUpAuthenticationPath(GET, "/api/my/studies", "/api/members/me", "/api/members/me/role") + .addUpAuthenticationPath(PUT, "/api/studies/\\d+/reviews/\\d+") + .addUpAuthenticationPath(DELETE, "/api/studies/\\d+/reviews/\\d+") .build(); } } diff --git a/backend/src/main/java/com/woowacourse/moamoa/auth/config/AuthenticationExtractor.java b/backend/src/main/java/com/woowacourse/moamoa/auth/config/AuthenticationExtractor.java index 5bc64399a..ebfda47b1 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/auth/config/AuthenticationExtractor.java +++ b/backend/src/main/java/com/woowacourse/moamoa/auth/config/AuthenticationExtractor.java @@ -7,8 +7,8 @@ public class AuthenticationExtractor { - private static String BEARER_TYPE = "Bearer"; - private static String ACCESS_TOKEN_TYPE = AuthenticationExtractor.class.getSimpleName() + ".ACCESS_TOKEN_TYPE"; + private static final String BEARER_TYPE = "Bearer"; + private static final String ACCESS_TOKEN_TYPE = AuthenticationExtractor.class.getSimpleName() + ".ACCESS_TOKEN_TYPE"; public static String extract(HttpServletRequest request) { Enumeration<String> headers = request.getHeaders(AUTHORIZATION); diff --git a/backend/src/main/java/com/woowacourse/moamoa/auth/controller/AuthController.java b/backend/src/main/java/com/woowacourse/moamoa/auth/controller/AuthController.java index 9596a74bd..ef362e67e 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/auth/controller/AuthController.java +++ b/backend/src/main/java/com/woowacourse/moamoa/auth/controller/AuthController.java @@ -1,9 +1,15 @@ package com.woowacourse.moamoa.auth.controller; +import com.woowacourse.moamoa.auth.config.AuthenticationPrincipal; import com.woowacourse.moamoa.auth.service.AuthService; -import com.woowacourse.moamoa.auth.service.response.TokenResponse; +import com.woowacourse.moamoa.auth.service.response.AccessTokenResponse; +import com.woowacourse.moamoa.auth.service.response.TokensResponse; import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseCookie; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.CookieValue; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @@ -12,10 +18,48 @@ @RequiredArgsConstructor public class AuthController { + private static final String REFRESH_TOKEN = "refreshToken"; + private static final int REFRESH_TOKEN_EXPIRATION = 7 * 24 * 60 * 60; + private final AuthService authService; - @PostMapping("/api/login/token") - public ResponseEntity<TokenResponse> login(@RequestParam final String code) { - return ResponseEntity.ok().body(authService.createToken(code)); + @PostMapping("/api/auth/login") + public ResponseEntity<AccessTokenResponse> login(@RequestParam final String code) { + final TokensResponse tokenResponse = authService.createToken(code); + + final AccessTokenResponse response = new AccessTokenResponse(tokenResponse.getAccessToken(), authService.getExpireTime()); + final ResponseCookie cookie = putTokenInCookie(tokenResponse); + + return ResponseEntity.ok().header("Set-Cookie", cookie.toString()).body(response); + } + + @GetMapping("/api/auth/refresh") + public ResponseEntity<AccessTokenResponse> refreshToken(@AuthenticationPrincipal Long githubId, @CookieValue String refreshToken) { + return ResponseEntity.ok().body(authService.refreshToken(githubId, refreshToken)); + } + + @DeleteMapping("/api/auth/logout") + public ResponseEntity<Void> logout(@AuthenticationPrincipal Long githubId) { + authService.logout(githubId); + + return ResponseEntity.noContent().header("Set-Cookie", removeCookie().toString()).build(); + } + + private ResponseCookie putTokenInCookie(final TokensResponse tokenResponse) { + return ResponseCookie.from(REFRESH_TOKEN, tokenResponse.getRefreshToken()) + .maxAge(REFRESH_TOKEN_EXPIRATION) + .path("/") + .secure(true) + .httpOnly(true) + .build(); + } + + private ResponseCookie removeCookie() { + return ResponseCookie.from(REFRESH_TOKEN, null) + .maxAge(0) + .path("/") + .secure(true) + .httpOnly(true) + .build(); } } diff --git a/backend/src/main/java/com/woowacourse/moamoa/auth/controller/AuthenticationArgumentResolver.java b/backend/src/main/java/com/woowacourse/moamoa/auth/controller/AuthenticationArgumentResolver.java index bcac02054..1e4472da9 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/auth/controller/AuthenticationArgumentResolver.java +++ b/backend/src/main/java/com/woowacourse/moamoa/auth/controller/AuthenticationArgumentResolver.java @@ -28,8 +28,8 @@ public boolean supportsParameter(final MethodParameter parameter) { public Object resolveArgument(final MethodParameter parameter, final ModelAndViewContainer mavContainer, final NativeWebRequest webRequest, final WebDataBinderFactory binderFactory) { final HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class); - final String token = AuthenticationExtractor.extract(request); + if (token == null) { throw new UnauthorizedException("인증 타입이 올바르지 않습니다."); } diff --git a/backend/src/main/java/com/woowacourse/moamoa/auth/domain/Token.java b/backend/src/main/java/com/woowacourse/moamoa/auth/domain/Token.java new file mode 100644 index 000000000..33760f67a --- /dev/null +++ b/backend/src/main/java/com/woowacourse/moamoa/auth/domain/Token.java @@ -0,0 +1,36 @@ +package com.woowacourse.moamoa.auth.domain; + +import static javax.persistence.GenerationType.IDENTITY; +import static lombok.AccessLevel.PROTECTED; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.Id; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor(access = PROTECTED) +@AllArgsConstructor +public class Token { + + @Id + @GeneratedValue(strategy = IDENTITY) + private Long id; + + @Column(nullable = false) + private Long githubId; + + private String refreshToken; + + public Token(final Long githubId, final String refreshToken) { + this(null, githubId, refreshToken); + } + + public void updateRefreshToken(final String refreshToken) { + this.refreshToken = refreshToken; + } +} diff --git a/backend/src/main/java/com/woowacourse/moamoa/auth/domain/repository/TokenRepository.java b/backend/src/main/java/com/woowacourse/moamoa/auth/domain/repository/TokenRepository.java new file mode 100644 index 000000000..c5c3fc487 --- /dev/null +++ b/backend/src/main/java/com/woowacourse/moamoa/auth/domain/repository/TokenRepository.java @@ -0,0 +1,10 @@ +package com.woowacourse.moamoa.auth.domain.repository; + +import com.woowacourse.moamoa.auth.domain.Token; +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface TokenRepository extends JpaRepository<Token, Long> { + + Optional<Token> findByGithubId(Long githubId); +} diff --git a/backend/src/main/java/com/woowacourse/moamoa/auth/exception/RefreshTokenExpirationException.java b/backend/src/main/java/com/woowacourse/moamoa/auth/exception/RefreshTokenExpirationException.java new file mode 100644 index 000000000..146bdb374 --- /dev/null +++ b/backend/src/main/java/com/woowacourse/moamoa/auth/exception/RefreshTokenExpirationException.java @@ -0,0 +1,9 @@ +package com.woowacourse.moamoa.auth.exception; + +import com.woowacourse.moamoa.common.exception.UnauthorizedException; + +public class RefreshTokenExpirationException extends UnauthorizedException { + public RefreshTokenExpirationException() { + super("만료된 리프래시 토큰입니다."); + } +} diff --git a/backend/src/main/java/com/woowacourse/moamoa/auth/exception/TokenExpirationException.java b/backend/src/main/java/com/woowacourse/moamoa/auth/exception/TokenExpirationException.java new file mode 100644 index 000000000..3524d8e76 --- /dev/null +++ b/backend/src/main/java/com/woowacourse/moamoa/auth/exception/TokenExpirationException.java @@ -0,0 +1,10 @@ +package com.woowacourse.moamoa.auth.exception; + +import com.woowacourse.moamoa.common.exception.UnauthorizedException; + +public class TokenExpirationException extends UnauthorizedException { + + public TokenExpirationException() { + super("만료된 토큰입니다."); + } +} diff --git a/backend/src/main/java/com/woowacourse/moamoa/auth/exception/TokenNotFoundException.java b/backend/src/main/java/com/woowacourse/moamoa/auth/exception/TokenNotFoundException.java new file mode 100644 index 000000000..f947fff24 --- /dev/null +++ b/backend/src/main/java/com/woowacourse/moamoa/auth/exception/TokenNotFoundException.java @@ -0,0 +1,10 @@ +package com.woowacourse.moamoa.auth.exception; + +import com.woowacourse.moamoa.common.exception.UnauthorizedException; + +public class TokenNotFoundException extends UnauthorizedException { + + public TokenNotFoundException() { + super("토큰이 존재하지 않습니다."); + } +} diff --git a/backend/src/main/java/com/woowacourse/moamoa/auth/infrastructure/JwtTokenProvider.java b/backend/src/main/java/com/woowacourse/moamoa/auth/infrastructure/JwtTokenProvider.java index 0c305f46b..64772bdf3 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/auth/infrastructure/JwtTokenProvider.java +++ b/backend/src/main/java/com/woowacourse/moamoa/auth/infrastructure/JwtTokenProvider.java @@ -1,5 +1,7 @@ package com.woowacourse.moamoa.auth.infrastructure; +import com.woowacourse.moamoa.auth.exception.RefreshTokenExpirationException; +import com.woowacourse.moamoa.auth.service.response.TokensResponse; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jws; import io.jsonwebtoken.JwtException; @@ -15,6 +17,8 @@ @Component public class JwtTokenProvider implements TokenProvider { + private static final long REFRESH_TOKEN_EXPIRATION = 7 * 24 * 60 * 60 * 1000; // 7일 + private final SecretKey key; private final long validityInMilliseconds; @@ -27,16 +31,23 @@ public JwtTokenProvider( } @Override - public String createToken(final Long payload) { + public TokensResponse createToken(final Long payload) { final Date now = new Date(); - final Date validity = new Date(now.getTime() + validityInMilliseconds); - return Jwts.builder() + String accessToken = Jwts.builder() .setSubject(payload.toString()) .setIssuedAt(now) - .setExpiration(validity) + .setExpiration(new Date(now.getTime() + validityInMilliseconds)) + .signWith(key, SignatureAlgorithm.HS256) + .compact(); + + String refreshToken = Jwts.builder() + .setIssuedAt(now) + .setExpiration(new Date(now.getTime() + REFRESH_TOKEN_EXPIRATION)) .signWith(key, SignatureAlgorithm.HS256) .compact(); + + return new TokensResponse(accessToken, refreshToken); } @Override @@ -57,11 +68,47 @@ public boolean validateToken(final String token) { .build() .parseClaimsJws(token); - return !claims.getBody() - .getExpiration() - .before(new Date()); + Date tokenExpirationDate = claims.getBody().getExpiration(); + validateTokenExpiration(tokenExpirationDate); + + return true; } catch (JwtException | IllegalArgumentException e) { return false; } } + + @Override + public String recreationAccessToken(final Long githubId, final String refreshToken) { + Jws<Claims> claims = Jwts.parserBuilder() + .setSigningKey(key) + .build() + .parseClaimsJws(refreshToken); + + Date tokenExpirationDate = claims.getBody().getExpiration(); + validateTokenExpiration(tokenExpirationDate); + + return createAccessToken(githubId); + } + + private void validateTokenExpiration(Date tokenExpirationDate) { + if (tokenExpirationDate.before(new Date())) { + throw new RefreshTokenExpirationException(); + } + } + + private String createAccessToken(final Long githubId) { + final Date now = new Date(); + + return Jwts.builder() + .setSubject(Long.toString(githubId)) + .setIssuedAt(now) + .setExpiration(new Date(now.getTime() + validityInMilliseconds)) + .signWith(key, SignatureAlgorithm.HS256) + .compact(); + } + + @Override + public long getValidityInMilliseconds() { + return validityInMilliseconds; + } } diff --git a/backend/src/main/java/com/woowacourse/moamoa/auth/infrastructure/TokenProvider.java b/backend/src/main/java/com/woowacourse/moamoa/auth/infrastructure/TokenProvider.java index 014343e4f..daed6cd84 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/auth/infrastructure/TokenProvider.java +++ b/backend/src/main/java/com/woowacourse/moamoa/auth/infrastructure/TokenProvider.java @@ -1,10 +1,16 @@ package com.woowacourse.moamoa.auth.infrastructure; +import com.woowacourse.moamoa.auth.service.response.TokensResponse; + public interface TokenProvider { - String createToken(final Long payload); + TokensResponse createToken(final Long payload); String getPayload(final String token); boolean validateToken(final String token); + + String recreationAccessToken(final Long githubId, final String refreshToken); + + long getValidityInMilliseconds(); } diff --git a/backend/src/main/java/com/woowacourse/moamoa/auth/service/AuthService.java b/backend/src/main/java/com/woowacourse/moamoa/auth/service/AuthService.java index 442e57568..22082543e 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/auth/service/AuthService.java +++ b/backend/src/main/java/com/woowacourse/moamoa/auth/service/AuthService.java @@ -1,10 +1,16 @@ package com.woowacourse.moamoa.auth.service; +import com.woowacourse.moamoa.auth.domain.Token; +import com.woowacourse.moamoa.auth.domain.repository.TokenRepository; +import com.woowacourse.moamoa.auth.exception.TokenNotFoundException; import com.woowacourse.moamoa.auth.infrastructure.TokenProvider; import com.woowacourse.moamoa.auth.service.oauthclient.OAuthClient; import com.woowacourse.moamoa.auth.service.oauthclient.response.GithubProfileResponse; -import com.woowacourse.moamoa.auth.service.response.TokenResponse; +import com.woowacourse.moamoa.auth.service.response.AccessTokenResponse; +import com.woowacourse.moamoa.auth.service.response.TokensResponse; +import com.woowacourse.moamoa.common.exception.UnauthorizedException; import com.woowacourse.moamoa.member.service.MemberService; +import java.util.Optional; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -17,15 +23,51 @@ public class AuthService { private final MemberService memberService; private final TokenProvider tokenProvider; private final OAuthClient oAuthClient; + private final TokenRepository tokenRepository; @Transactional - public TokenResponse createToken(final String code) { + public TokensResponse createToken(final String code) { final String accessToken = oAuthClient.getAccessToken(code); final GithubProfileResponse githubProfileResponse = oAuthClient.getProfile(accessToken); - memberService.saveOrUpdate(githubProfileResponse.toMember()); - final String jwtToken = tokenProvider.createToken(githubProfileResponse.getGitgubId()); - return new TokenResponse(jwtToken); + final Long githubId = githubProfileResponse.getGithubId(); + final Optional<Token> token = tokenRepository.findByGithubId(githubId); + + final TokensResponse tokenResponse = tokenProvider.createToken(githubProfileResponse.getGithubId()); + + if (token.isPresent()) { + token.get().updateRefreshToken(tokenResponse.getRefreshToken()); + return tokenResponse; + } + + tokenRepository.save(new Token(githubProfileResponse.getGithubId(), tokenResponse.getRefreshToken())); + + return tokenResponse; + } + + public AccessTokenResponse refreshToken(final Long githubId, final String refreshToken) { + final Token token = tokenRepository.findByGithubId(githubId) + .orElseThrow(TokenNotFoundException::new); + + if (!token.getRefreshToken().equals(refreshToken)) { + throw new UnauthorizedException("유효하지 않은 토큰입니다."); + } + + String accessToken = tokenProvider.recreationAccessToken(githubId, refreshToken); + + return new AccessTokenResponse(accessToken, tokenProvider.getValidityInMilliseconds()); + } + + @Transactional + public void logout(final Long githubId) { + final Token token = tokenRepository.findByGithubId(githubId) + .orElseThrow(TokenNotFoundException::new); + + tokenRepository.delete(token); + } + + public long getExpireTime() { + return tokenProvider.getValidityInMilliseconds(); } } diff --git a/backend/src/main/java/com/woowacourse/moamoa/auth/service/oauthclient/response/GithubProfileResponse.java b/backend/src/main/java/com/woowacourse/moamoa/auth/service/oauthclient/response/GithubProfileResponse.java index 6a7364c14..330c76e56 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/auth/service/oauthclient/response/GithubProfileResponse.java +++ b/backend/src/main/java/com/woowacourse/moamoa/auth/service/oauthclient/response/GithubProfileResponse.java @@ -2,15 +2,17 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.woowacourse.moamoa.member.domain.Member; +import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; @Getter @NoArgsConstructor +@AllArgsConstructor public class GithubProfileResponse { @JsonProperty("id") - private Long gitgubId; + private Long githubId; @JsonProperty("login") private String username; @@ -21,15 +23,7 @@ public class GithubProfileResponse { @JsonProperty("html_url") private String profileUrl; - public GithubProfileResponse(final Long gitgubId, final String username, final String imageUrl, - final String profileUrl) { - this.gitgubId = gitgubId; - this.username = username; - this.imageUrl = imageUrl; - this.profileUrl = profileUrl; - } - public Member toMember() { - return new Member(gitgubId, username, imageUrl, profileUrl); + return new Member(githubId, username, imageUrl, profileUrl); } } diff --git a/backend/src/main/java/com/woowacourse/moamoa/auth/service/request/AccessTokenRequest.java b/backend/src/main/java/com/woowacourse/moamoa/auth/service/request/AccessTokenRequest.java index 6e0d26fc2..80ce902b5 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/auth/service/request/AccessTokenRequest.java +++ b/backend/src/main/java/com/woowacourse/moamoa/auth/service/request/AccessTokenRequest.java @@ -16,14 +16,6 @@ public class AccessTokenRequest { private final String code; - public String getClientId() { - return clientId; - } - - public String getClientSecret() { - return clientSecret; - } - public String getCode() { return code; } diff --git a/backend/src/main/java/com/woowacourse/moamoa/auth/service/response/AccessTokenResponse.java b/backend/src/main/java/com/woowacourse/moamoa/auth/service/response/AccessTokenResponse.java new file mode 100644 index 000000000..f89683d1b --- /dev/null +++ b/backend/src/main/java/com/woowacourse/moamoa/auth/service/response/AccessTokenResponse.java @@ -0,0 +1,12 @@ +package com.woowacourse.moamoa.auth.service.response; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor +@Getter +public class AccessTokenResponse { + + private final String accessToken; + private final long expiredTime; +} diff --git a/backend/src/main/java/com/woowacourse/moamoa/auth/service/response/TokenResponse.java b/backend/src/main/java/com/woowacourse/moamoa/auth/service/response/TokenResponse.java deleted file mode 100644 index 47bbbb7c4..000000000 --- a/backend/src/main/java/com/woowacourse/moamoa/auth/service/response/TokenResponse.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.woowacourse.moamoa.auth.service.response; - -public class TokenResponse { - - private final String token; - - public TokenResponse(final String token) { - this.token = token; - } - - public String getToken() { - return token; - } -} diff --git a/backend/src/main/java/com/woowacourse/moamoa/auth/service/response/TokensResponse.java b/backend/src/main/java/com/woowacourse/moamoa/auth/service/response/TokensResponse.java new file mode 100644 index 000000000..fad30b049 --- /dev/null +++ b/backend/src/main/java/com/woowacourse/moamoa/auth/service/response/TokensResponse.java @@ -0,0 +1,12 @@ +package com.woowacourse.moamoa.auth.service.response; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor +@Getter +public class TokensResponse { + + private final String accessToken; + private final String refreshToken; +} diff --git a/backend/src/main/java/com/woowacourse/moamoa/common/advice/CommonControllerAdvice.java b/backend/src/main/java/com/woowacourse/moamoa/common/advice/CommonControllerAdvice.java index 0c888fbcb..98ede3ea6 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/common/advice/CommonControllerAdvice.java +++ b/backend/src/main/java/com/woowacourse/moamoa/common/advice/CommonControllerAdvice.java @@ -1,7 +1,10 @@ package com.woowacourse.moamoa.common.advice; import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR; +import static org.springframework.http.HttpStatus.NOT_FOUND; +import static org.springframework.http.HttpStatus.UNAUTHORIZED; +import com.woowacourse.moamoa.auth.exception.RefreshTokenExpirationException; import com.woowacourse.moamoa.common.advice.response.ErrorResponse; import com.woowacourse.moamoa.common.exception.BadRequestException; import com.woowacourse.moamoa.common.exception.InvalidFormatException; @@ -9,17 +12,14 @@ import com.woowacourse.moamoa.common.exception.UnauthorizedException; import com.woowacourse.moamoa.study.domain.exception.InvalidPeriodException; import com.woowacourse.moamoa.study.service.exception.FailureParticipationException; - -import org.springframework.http.HttpStatus; +import io.jsonwebtoken.JwtException; +import lombok.extern.slf4j.Slf4j; import org.springframework.http.ResponseEntity; import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; -import io.jsonwebtoken.JwtException; -import lombok.extern.slf4j.Slf4j; - @RestControllerAdvice @Slf4j public class CommonControllerAdvice { @@ -39,25 +39,31 @@ public ResponseEntity<ErrorResponse> handleBadRequest() { FailureParticipationException.class }) public ResponseEntity<ErrorResponse> handleBadRequest(final Exception e) { - log.error("HandleBadRequest : {}", e.getMessage()); + log.debug("HandleBadRequest : {}", e.getMessage()); return ResponseEntity.badRequest().body(new ErrorResponse(e.getMessage())); } @ExceptionHandler({UnauthorizedException.class, JwtException.class}) public ResponseEntity<Void> handleUnauthorized(final Exception e) { - log.error("UnauthorizedException : {}", e.getMessage()); - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); + log.debug("UnauthorizedException : {}", e.getMessage()); + return ResponseEntity.status(UNAUTHORIZED).build(); + } + + @ExceptionHandler(RefreshTokenExpirationException.class) + public ResponseEntity<ErrorResponse> handle(RefreshTokenExpirationException e) { + log.debug("RefreshTokenExpirationException : {}", e.getMessage()); + return ResponseEntity.status(UNAUTHORIZED).body(new ErrorResponse(e.getMessage(), 4001)); } @ExceptionHandler(NotFoundException.class) public ResponseEntity<ErrorResponse> handleNotFound(final Exception e) { - log.error("NotFoundException : {}", e.getMessage()); - return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse(e.getMessage())); + log.debug("NotFoundException : {}", e.getMessage()); + return ResponseEntity.status(NOT_FOUND).body(new ErrorResponse(e.getMessage())); } @ExceptionHandler(RuntimeException.class) public ResponseEntity<ErrorResponse> handleInternalServerError(RuntimeException e) { - log.error("RuntimeException : {}", e.getMessage()); + log.debug("RuntimeException : {}", e.getMessage()); return ResponseEntity.status(INTERNAL_SERVER_ERROR).body(new ErrorResponse("요청을 처리할 수 없습니다.")); } } diff --git a/backend/src/main/java/com/woowacourse/moamoa/common/advice/response/ErrorResponse.java b/backend/src/main/java/com/woowacourse/moamoa/common/advice/response/ErrorResponse.java index 0ab20d12c..3f9eb0ba0 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/common/advice/response/ErrorResponse.java +++ b/backend/src/main/java/com/woowacourse/moamoa/common/advice/response/ErrorResponse.java @@ -3,12 +3,22 @@ public class ErrorResponse { private final String message; + private int code; public ErrorResponse(final String message) { this.message = message; } + public ErrorResponse(final String message, final int code) { + this.message = message; + this.code = code; + } + public String getMessage() { return message; } + + public int getCode() { + return code; + } } diff --git a/backend/src/main/java/com/woowacourse/moamoa/common/config/WebConfig.java b/backend/src/main/java/com/woowacourse/moamoa/common/config/WebConfig.java index e4da1c683..12a44b543 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/common/config/WebConfig.java +++ b/backend/src/main/java/com/woowacourse/moamoa/common/config/WebConfig.java @@ -24,8 +24,10 @@ public void addArgumentResolvers(final List<HandlerMethodArgumentResolver> resol @Override public void addCorsMappings(final CorsRegistry registry) { registry.addMapping("/api/**") + .allowedOrigins("https://dev.moamoa.space", "https://moamoa.space") .allowedMethods(ALLOW_METHODS) - .exposedHeaders(HttpHeaders.LOCATION); + .exposedHeaders(HttpHeaders.LOCATION) + .allowCredentials(true); } @Bean diff --git a/backend/src/main/java/com/woowacourse/moamoa/member/controller/MemberController.java b/backend/src/main/java/com/woowacourse/moamoa/member/controller/MemberController.java index e02848a9f..2432bda40 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/member/controller/MemberController.java +++ b/backend/src/main/java/com/woowacourse/moamoa/member/controller/MemberController.java @@ -3,18 +3,16 @@ import com.woowacourse.moamoa.auth.config.AuthenticationPrincipal; import com.woowacourse.moamoa.member.service.response.MemberResponse; import com.woowacourse.moamoa.member.service.MemberService; +import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController +@RequiredArgsConstructor public class MemberController { - private MemberService memberService; - - public MemberController(final MemberService memberService) { - this.memberService = memberService; - } + private final MemberService memberService; @RequestMapping("/api/members/me") public ResponseEntity<MemberResponse> getCurrentMember( diff --git a/backend/src/test/java/com/woowacourse/acceptance/AcceptanceTest.java b/backend/src/test/java/com/woowacourse/acceptance/AcceptanceTest.java index 418d9f090..e0fdd177b 100644 --- a/backend/src/test/java/com/woowacourse/acceptance/AcceptanceTest.java +++ b/backend/src/test/java/com/woowacourse/acceptance/AcceptanceTest.java @@ -19,7 +19,6 @@ import io.restassured.specification.RequestSpecification; import java.util.Map; import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.extension.RegisterExtension; import org.springframework.beans.factory.annotation.Autowired; @@ -67,7 +66,7 @@ public class AcceptanceTest { @Value("${oauth2.github.client-secret}") private String clientSecret; - private MockRestServiceServer mockServer; + protected MockRestServiceServer mockServer; @BeforeEach protected void setRestDocumentation(RestDocumentationContextProvider restDocumentation) { @@ -107,17 +106,6 @@ void tearDown() { jdbcTemplate.update("ALTER TABLE study AUTO_INCREMENT = 1"); } - private void mockingGithubServer(String authorizationCode, GithubProfileResponse response) { - try { - mockingGithubServerForGetAccessToken(authorizationCode, Map.of("access_token", "access-token", - "token_type", "bearer", - "scope", "")); - mockingGithubServerForGetProfile("access-token", HttpStatus.OK, response); - } catch (Exception e) { - Assertions.fail(e.getMessage()); - } - } - protected void mockingGithubServerForGetAccessToken(final String authorizationCode, final Map<String, String> accessTokenResponse) throws JsonProcessingException { diff --git a/backend/src/test/java/com/woowacourse/acceptance/steps/LoginSteps.java b/backend/src/test/java/com/woowacourse/acceptance/steps/LoginSteps.java index eedebb1d4..c690b881f 100644 --- a/backend/src/test/java/com/woowacourse/acceptance/steps/LoginSteps.java +++ b/backend/src/test/java/com/woowacourse/acceptance/steps/LoginSteps.java @@ -42,25 +42,30 @@ public class LoginSteps extends Steps { } private String getIssuedBearerToken() { - if (tokenCache.containsKey(githubProfile.getGitgubId())) { - return tokenCache.get(githubProfile.getGitgubId()); + if (tokenCache.containsKey(githubProfile.getGithubId())) { + return tokenCache.get(githubProfile.getGithubId()); } + final String bearerToken = requestBearerToken(); - tokenCache.put(githubProfile.getGitgubId(), bearerToken); + tokenCache.put(githubProfile.getGithubId(), bearerToken); + return bearerToken; } private String requestBearerToken() { final String authorizationCode = "Authorization Code"; mockingGithubServer(authorizationCode, githubProfile); + final String token = RestAssured.given().log().all() .param("code", authorizationCode) .when() - .post("/api/login/token") + .post("/api/auth/login") .then().log().all() .statusCode(HttpStatus.OK.value()) - .extract().jsonPath().getString("token"); + .extract().jsonPath().getString("accessToken"); + mockServer.reset(); + return "Bearer " + token; } } diff --git a/backend/src/test/java/com/woowacourse/acceptance/test/auth/AuthAcceptanceTest.java b/backend/src/test/java/com/woowacourse/acceptance/test/auth/AuthAcceptanceTest.java index 6f5bc2a67..b41780ed6 100644 --- a/backend/src/test/java/com/woowacourse/acceptance/test/auth/AuthAcceptanceTest.java +++ b/backend/src/test/java/com/woowacourse/acceptance/test/auth/AuthAcceptanceTest.java @@ -1,8 +1,14 @@ package com.woowacourse.acceptance.test.auth; +import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; +import static org.springframework.http.HttpHeaders.AUTHORIZATION; +import static org.springframework.http.HttpHeaders.CONTENT_TYPE; +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; +import static org.springframework.restdocs.payload.JsonFieldType.NUMBER; +import static org.springframework.restdocs.payload.JsonFieldType.STRING; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; @@ -11,16 +17,22 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.woowacourse.acceptance.AcceptanceTest; +import com.woowacourse.moamoa.auth.domain.Token; +import com.woowacourse.moamoa.auth.domain.repository.TokenRepository; import com.woowacourse.moamoa.auth.service.oauthclient.response.GithubProfileResponse; import io.restassured.RestAssured; import java.util.Map; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; -import org.springframework.restdocs.payload.JsonFieldType; public class AuthAcceptanceTest extends AcceptanceTest { + @Autowired + private TokenRepository tokenRepository; + @DisplayName("Authorization code를 받아서 token을 발급한다.") @Test void getJwtToken() throws JsonProcessingException { @@ -35,13 +47,55 @@ void getJwtToken() throws JsonProcessingException { RestAssured.given(spec).log().all() .filter(document("auth/login", requestParameters(parameterWithName("code").description("Authorization code")), - responseFields(fieldWithPath("token").type(JsonFieldType.STRING).description("사용자 토큰")))) + responseFields( + fieldWithPath("accessToken").type(STRING).description("사용자 토큰"), + fieldWithPath("expiredTime").type(NUMBER).description("유효시간") + ))) .queryParam("code", authorizationCode) .when() - .post("/api/login/token") + .post("/api/auth/login") .then().log().all() .statusCode(HttpStatus.OK.value()) - .body("token", notNullValue()); + .body("accessToken", notNullValue()) + .body("expiredTime", notNullValue()); + } + + @DisplayName("RefreshToken 으로 AccessToken 을 재발급한다.") + @Test + void refreshToken() { + final String token = getBearerTokenBySignInOrUp(new GithubProfileResponse(4L, "verus", "https://image", "github.com")); + final Token foundToken = tokenRepository.findByGithubId(4L).get(); + + RestAssured.given(spec).log().all() + .filter(document("auth/refresh", + requestHeaders(headerWithName("Authorization").description("Bearer Token")))) + .cookie("refreshToken", foundToken.getRefreshToken()) + .header(CONTENT_TYPE, APPLICATION_JSON_VALUE) + .header(AUTHORIZATION, token) + .when() + .get("/api/auth/refresh") + .then().log().all() + .statusCode(HttpStatus.OK.value()) + .body("accessToken", notNullValue()); + } + + @DisplayName("로그아웃시에 쿠키를 제거해준다.") + @Test + public void logout() { + final String token = getBearerTokenBySignInOrUp(new GithubProfileResponse(4L, "verus", "https://image", "github.com")); + final Token foundToken = tokenRepository.findByGithubId(4L).get(); + + RestAssured.given(spec).log().all() + .filter(document("auth/logout", + requestHeaders(headerWithName("Authorization").description("Bearer Token")))) + .cookie("refreshToken", foundToken.getRefreshToken()) + .header(CONTENT_TYPE, APPLICATION_JSON_VALUE) + .header(AUTHORIZATION, token) + .when() + .delete("/api/auth/logout") + .then().log().all() + .statusCode(HttpStatus.NO_CONTENT.value()) + .cookie("refreshToken", is("")); } @Test @@ -56,7 +110,7 @@ void get401ByInvalidAuthorizationCode() throws JsonProcessingException { RestAssured.given().log().all() .param("code", invalidAuthorizationCode) .when() - .post("/api/login/token") + .post("/api/auth/login") .then().log().all() .statusCode(HttpStatus.UNAUTHORIZED.value()); } @@ -76,7 +130,7 @@ void get401ByInvalidAccessToken() throws JsonProcessingException { RestAssured.given().log().all() .param("code", authorizationCode) .when() - .post("/api/login/token") + .post("/api/auth/login") .then().log().all() .statusCode(HttpStatus.UNAUTHORIZED.value()); } @@ -85,4 +139,30 @@ private void mockingGithubServerForGetProfile(final String accessToken, final Ht throws JsonProcessingException { mockingGithubServerForGetProfile(accessToken, status, null); } + + private String getBearerTokenBySignInOrUp(GithubProfileResponse response) { + final String authorizationCode = "Authorization Code"; + mockingGithubServer(authorizationCode, response); + final String token = RestAssured.given().log().all() + .param("code", authorizationCode) + .when() + .post("/api/auth/login") + .then().log().all() + .statusCode(HttpStatus.OK.value()) + .extract().jsonPath().getString("accessToken"); + mockServer.reset(); + return "Bearer " + token; + } + + private void mockingGithubServer(String authorizationCode, GithubProfileResponse response) { + try { + mockingGithubServerForGetAccessToken(authorizationCode, Map.of( + "access_token", "access-token", + "token_type", "bearer", + "scope", "")); + mockingGithubServerForGetProfile("access-token", HttpStatus.OK, response); + } catch (Exception e) { + Assertions.fail(e.getMessage()); + } + } } diff --git a/backend/src/test/java/com/woowacourse/acceptance/test/cors/CorsAcceptanceTest.java b/backend/src/test/java/com/woowacourse/acceptance/test/cors/CorsAcceptanceTest.java index 86185c487..76cac2cf9 100644 --- a/backend/src/test/java/com/woowacourse/acceptance/test/cors/CorsAcceptanceTest.java +++ b/backend/src/test/java/com/woowacourse/acceptance/test/cors/CorsAcceptanceTest.java @@ -4,7 +4,6 @@ import io.restassured.RestAssured; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.springframework.test.context.jdbc.Sql; public class CorsAcceptanceTest extends AcceptanceTest { @@ -12,7 +11,6 @@ public class CorsAcceptanceTest extends AcceptanceTest { @Test void corsTest() { RestAssured.given() - .header("Origin", "https://xxx.com") .header("Access-Control-Request-Method", "GET") .when() .options("/api/studies") diff --git a/backend/src/test/java/com/woowacourse/acceptance/test/member/MemberAcceptanceTest.java b/backend/src/test/java/com/woowacourse/acceptance/test/member/MemberAcceptanceTest.java index 700ed9feb..74b2aeca9 100644 --- a/backend/src/test/java/com/woowacourse/acceptance/test/member/MemberAcceptanceTest.java +++ b/backend/src/test/java/com/woowacourse/acceptance/test/member/MemberAcceptanceTest.java @@ -1,5 +1,6 @@ package com.woowacourse.acceptance.test.member; +import static org.apache.http.HttpHeaders.AUTHORIZATION; import static com.woowacourse.acceptance.fixture.MemberFixtures.베루스_깃허브_ID; import static com.woowacourse.acceptance.fixture.MemberFixtures.베루스_이름; import static com.woowacourse.acceptance.fixture.MemberFixtures.베루스_이미지_URL; @@ -13,7 +14,6 @@ import com.woowacourse.acceptance.AcceptanceTest; import com.woowacourse.moamoa.member.service.response.MemberResponse; import io.restassured.RestAssured; -import org.apache.http.HttpHeaders; import org.junit.jupiter.api.Test; import org.springframework.http.HttpStatus; @@ -24,7 +24,7 @@ void getCurrentMember() { final String token = 베루스가().로그인한다(); final MemberResponse memberResponse = RestAssured.given(spec).log().all() - .header(HttpHeaders.AUTHORIZATION, token) + .header(AUTHORIZATION, token) .filter(document("members/me", requestHeaders(headerWithName("Authorization").description("Bearer Token")))) .when().log().all() diff --git a/backend/src/test/java/com/woowacourse/moamoa/auth/controller/AuthControllerTest.java b/backend/src/test/java/com/woowacourse/moamoa/auth/controller/AuthControllerTest.java index 47e22f18b..59cfd4788 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/auth/controller/AuthControllerTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/auth/controller/AuthControllerTest.java @@ -7,8 +7,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import com.woowacourse.moamoa.WebMVCTest; -import com.woowacourse.moamoa.auth.service.response.TokenResponse; - +import com.woowacourse.moamoa.auth.service.response.TokensResponse; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -17,11 +16,12 @@ public class AuthControllerTest extends WebMVCTest { @DisplayName("Authorization 요청과 응답 형식을 확인한다.") @Test void getJwtToken() throws Exception { - given(authService.createToken("Authorization code")).willReturn(new TokenResponse("jwt token")); + given(authService.createToken("Authorization code")) + .willReturn(new TokensResponse("jwt token", "refreshtoken")); - mockMvc.perform(post("/api/login/token?code=Authorization code")) + mockMvc.perform(post("/api/auth/login?code=Authorization code")) .andExpect(status().isOk()) - .andExpect(jsonPath("token").value("jwt token")) + .andExpect(jsonPath("$.accessToken").value("jwt token")) .andDo(print()); } } diff --git a/backend/src/test/java/com/woowacourse/moamoa/auth/controller/AuthenticationArgumentResolverTest.java b/backend/src/test/java/com/woowacourse/moamoa/auth/controller/AuthenticationArgumentResolverTest.java index 8f2141a96..1bb654cd9 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/auth/controller/AuthenticationArgumentResolverTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/auth/controller/AuthenticationArgumentResolverTest.java @@ -42,7 +42,7 @@ void validAuthorizationTypeIsBearer() { @DisplayName("Authorization 인증 타입이 Bearer인 경우 payload를 반환한다.") @Test void getToken() { - String wrongBearerToken = "Bearer " + tokenProvider.createToken(1L); + String wrongBearerToken = "Bearer " + tokenProvider.createToken(1L).getAccessToken(); given(nativeWebRequest.getNativeRequest(HttpServletRequest.class)) .willReturn(httpServletRequest); diff --git a/backend/src/test/java/com/woowacourse/moamoa/auth/controller/AuthenticationInterceptorTest.java b/backend/src/test/java/com/woowacourse/moamoa/auth/controller/AuthenticationInterceptorTest.java index 6c68edcc9..fa4fb8ab5 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/auth/controller/AuthenticationInterceptorTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/auth/controller/AuthenticationInterceptorTest.java @@ -29,7 +29,7 @@ void isPreflightRequest() { @DisplayName("유효한 토큰을 검증한다.") @Test void validateValidToken() { - final String token = tokenProvider.createToken(1L); + final String token = tokenProvider.createToken(1L).getAccessToken(); String bearerToken = "Bearer " + token; given(httpServletRequest.getMethod()) diff --git a/backend/src/test/java/com/woowacourse/moamoa/auth/infrastructure/TokenProviderTest.java b/backend/src/test/java/com/woowacourse/moamoa/auth/infrastructure/TokenProviderTest.java index a0eb0cb71..4fed7e132 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/auth/infrastructure/TokenProviderTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/auth/infrastructure/TokenProviderTest.java @@ -47,7 +47,7 @@ void isExpiredToken() { @DisplayName("유효한 토큰을 검증한다.") @Test void validateTokenByValidToken() { - String token = tokenProvider.createToken(1L); + String token = tokenProvider.createToken(1L).getAccessToken(); assertThat(tokenProvider.validateToken(token)).isTrue(); } @@ -55,7 +55,7 @@ void validateTokenByValidToken() { @DisplayName("유효하지 않은 토큰을 검증한다.") @Test void validateTokenByInvalidToken() { - String token = tokenProvider.createToken(1L); + String token = tokenProvider.createToken(1L).getAccessToken(); String invalidToken = token + "dummy"; @@ -65,7 +65,7 @@ void validateTokenByInvalidToken() { @DisplayName("JwtToken payload 검증한다.") @Test void validatePayload() { - String token = tokenProvider.createToken(1L); + String token = tokenProvider.createToken(1L).getAccessToken(); assertThat(tokenProvider.getPayload(token)).isEqualTo("1"); } @@ -73,7 +73,7 @@ void validatePayload() { @DisplayName("JwtToken 형식을 검증한다.") @Test void validateJwtTokenFormat() { - String token = tokenProvider.createToken(1L); + String token = tokenProvider.createToken(1L).getAccessToken(); final String[] parts = token.split("\\."); diff --git a/backend/src/test/java/com/woowacourse/moamoa/auth/service/AuthServiceTest.java b/backend/src/test/java/com/woowacourse/moamoa/auth/service/AuthServiceTest.java index 6ca8156df..4c2c0de24 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/auth/service/AuthServiceTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/auth/service/AuthServiceTest.java @@ -1,28 +1,113 @@ package com.woowacourse.moamoa.auth.service; -import static org.mockito.Mockito.when; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -import com.woowacourse.moamoa.WebMVCTest; -import com.woowacourse.moamoa.auth.service.response.TokenResponse; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import com.woowacourse.moamoa.auth.domain.Token; +import com.woowacourse.moamoa.auth.domain.repository.TokenRepository; +import com.woowacourse.moamoa.auth.infrastructure.TokenProvider; +import com.woowacourse.moamoa.auth.service.oauthclient.OAuthClient; +import com.woowacourse.moamoa.auth.service.oauthclient.response.GithubProfileResponse; +import com.woowacourse.moamoa.auth.service.response.AccessTokenResponse; +import com.woowacourse.moamoa.auth.service.response.TokensResponse; +import com.woowacourse.moamoa.common.RepositoryTest; +import com.woowacourse.moamoa.common.exception.UnauthorizedException; +import com.woowacourse.moamoa.member.domain.repository.MemberRepository; +import com.woowacourse.moamoa.member.query.MemberDao; +import com.woowacourse.moamoa.member.service.MemberService; +import java.util.Optional; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; + +@RepositoryTest +class AuthServiceTest { + + private AuthService authService; + + private OAuthClient oAuthClient; + + private TokenProvider tokenProvider; + + private MemberService memberService; + + @Autowired + private TokenRepository tokenRepository; + + @Autowired + private MemberRepository memberRepository; + + @Autowired + private MemberDao memberDao; + + @BeforeEach + void setUp() { + memberService = new MemberService(memberRepository, memberDao); + tokenProvider = Mockito.mock(TokenProvider.class); + oAuthClient = Mockito.mock(OAuthClient.class); + authService = new AuthService(memberService, tokenProvider, oAuthClient, tokenRepository); + + Mockito.when(oAuthClient.getAccessToken("authorization-code")).thenReturn("access-token"); + Mockito.when(oAuthClient.getProfile("access-token")) + .thenReturn(new GithubProfileResponse(1L, "dwoo", "imageUrl", "profileUrl")); + Mockito.when(tokenProvider.createToken(1L)) + .thenReturn(new TokensResponse("accessToken", "refreshToken")); + Mockito.when(tokenProvider.recreationAccessToken(1L, "refreshToken")) + .thenReturn("recreationAccessToken"); + } + + @DisplayName("RefreshToken 을 저장한다.") + @Test + public void saveRefreshToken() { + authService.createToken("authorization-code"); + final Token token = tokenRepository.findByGithubId(1L).get(); + + assertThat(token.getRefreshToken()).isEqualTo("refreshToken"); + } + + @DisplayName("RefreshToken 을 이용하여 AccessToken 을 업데이트한다.") + @Test + public void updateRefreshToken() { + authService.createToken("authorization-code"); + final Token token = tokenRepository.findByGithubId(1L).get(); + final String refreshToken = token.getRefreshToken(); + + final AccessTokenResponse accessTokenResponse = authService.refreshToken(1L, refreshToken); -class AuthServiceTest extends WebMVCTest { + assertThat(refreshToken).isNotBlank(); + assertThat(accessTokenResponse.getAccessToken()).isEqualTo("recreationAccessToken"); + } + + @DisplayName("DB에 저장되어 있지 않은 refresh token으로 access token을 발급받을 수 없다.") + @Test + public void validateRefreshToken() { + assertThatThrownBy(() -> authService.refreshToken(1L, "InvalidRefreshToken")) + .isInstanceOf(UnauthorizedException.class); + } - @DisplayName("Authorization code를 받아서 token을 발급한다.") + @DisplayName("refresh token을 통해 access token을 발급받을 수 있다.") @Test - void getTokenByAuthorizationCode() throws Exception { - when(authService.createToken("authorization-code")).thenReturn(new TokenResponse("this is jwt-token")); - - mockMvc.perform(post("/api/login/token") - .param("code", "authorization-code")) - .andExpect(status().isOk()) - .andExpect(jsonPath("token").value("this is jwt-token")) - .andDo(print()); + public void recreationAccessToken() { + authService.createToken("authorization-code"); + final Token token = tokenRepository.findByGithubId(1L).get(); + + assertDoesNotThrow(() -> authService.refreshToken(1L, token.getRefreshToken())); + } + + @DisplayName("로그아웃을 하면 Token 을 제거한다.") + @Test + public void logout() { + authService.createToken("authorization-code"); + final Token token = tokenRepository.findByGithubId(1L).get(); + + authService.logout(token.getGithubId()); + + final Optional<Token> foundToken = tokenRepository.findByGithubId(token.getGithubId()); + + assertThat(token).isNotNull(); + assertThat(foundToken.isEmpty()).isTrue(); } } diff --git a/backend/src/test/java/com/woowacourse/moamoa/member/webmvc/MemberWebMvcTest.java b/backend/src/test/java/com/woowacourse/moamoa/member/webmvc/MemberWebMvcTest.java index 881d6876a..eac8b078c 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/member/webmvc/MemberWebMvcTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/member/webmvc/MemberWebMvcTest.java @@ -31,7 +31,7 @@ void unauthorizedToken(String invalidToken) throws Exception { @DisplayName("사용자가 없는 경우 400 에러 반환") @Test void notFound() throws Exception { - final String token = tokenProvider.createToken(1L); + final String token = tokenProvider.createToken(1L).getAccessToken(); given(memberService.getByGithubId(any())).willThrow(MemberNotFoundException.class); mockMvc.perform(get("/api/members/me") diff --git a/backend/src/test/java/com/woowacourse/moamoa/review/controller/SearchingReviewControllerTest.java b/backend/src/test/java/com/woowacourse/moamoa/review/controller/SearchingReviewControllerTest.java index 753be2067..8296decd0 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/review/controller/SearchingReviewControllerTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/review/controller/SearchingReviewControllerTest.java @@ -3,6 +3,7 @@ import static org.assertj.core.api.Assertions.assertThat; import com.woowacourse.moamoa.common.RepositoryTest; +import com.woowacourse.moamoa.common.utils.DateTimeSystem; import com.woowacourse.moamoa.member.domain.Member; import com.woowacourse.moamoa.member.domain.repository.MemberRepository; import com.woowacourse.moamoa.member.query.data.MemberData; @@ -17,21 +18,17 @@ import com.woowacourse.moamoa.review.service.response.WriterResponse; import com.woowacourse.moamoa.study.domain.Study; import com.woowacourse.moamoa.study.domain.repository.StudyRepository; -import com.woowacourse.moamoa.common.utils.DateTimeSystem; import com.woowacourse.moamoa.study.service.StudyService; import com.woowacourse.moamoa.study.service.request.CreatingStudyRequest; - import java.time.LocalDate; import java.util.List; import javax.persistence.EntityManager; - import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.jdbc.core.JdbcTemplate; @RepositoryTest class SearchingReviewControllerTest { diff --git a/backend/src/test/java/com/woowacourse/moamoa/review/webmvc/BadRequestReviewWebMvcTest.java b/backend/src/test/java/com/woowacourse/moamoa/review/webmvc/BadRequestReviewWebMvcTest.java index f62241fad..f9c6d2e0e 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/review/webmvc/BadRequestReviewWebMvcTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/review/webmvc/BadRequestReviewWebMvcTest.java @@ -18,7 +18,7 @@ class BadRequestReviewWebMvcTest extends WebMVCTest { @DisplayName("필수 데이터인 후기 내용이 공백인 경우 400을 반환한다.") @Test void requestByBlankContent() throws Exception { - final String token = "Bearer " + tokenProvider.createToken(1L); + final String token = "Bearer " + tokenProvider.createToken(1L).getAccessToken(); final String content = objectMapper.writeValueAsString(new WriteReviewRequest("")); mockMvc.perform(post("/api/studies/1/reviews") @@ -32,7 +32,7 @@ void requestByBlankContent() throws Exception { @DisplayName("필수 데이터인 후기 내용이 null 값인 경우 400을 반환한다.") @Test void requestByEmptyContent() throws Exception { - final String token = "Bearer " + tokenProvider.createToken(1L); + final String token = "Bearer " + tokenProvider.createToken(1L).getAccessToken(); mockMvc.perform(post("/api/studies/1/reviews") .header(HttpHeaders.AUTHORIZATION, token) diff --git a/backend/src/test/java/com/woowacourse/moamoa/study/webmvc/BadRequestMyMemberRoleWebMvcTest.java b/backend/src/test/java/com/woowacourse/moamoa/study/webmvc/BadRequestMyMemberRoleWebMvcTest.java index a6d5b946c..c3e25b2b2 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/study/webmvc/BadRequestMyMemberRoleWebMvcTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/study/webmvc/BadRequestMyMemberRoleWebMvcTest.java @@ -16,7 +16,7 @@ public class BadRequestMyMemberRoleWebMvcTest extends WebMVCTest { @DisplayName("study Id가 없을 경우 400 에러가 발생한다. ") @Test void getMyStudiesWithoutStudyId() throws Exception { - final String token = "Bearer " + tokenProvider.createToken(1L); + final String token = "Bearer " + tokenProvider.createToken(1L).getAccessToken(); mockMvc.perform(get("/api/members/me/role") .header(HttpHeaders.AUTHORIZATION, token) @@ -28,7 +28,7 @@ void getMyStudiesWithoutStudyId() throws Exception { @DisplayName("study Id가 String일 경우 400 에러가 발생한다.") @Test void getMyStudiesWithoutStudyId1() throws Exception { - final String token = "Bearer " + tokenProvider.createToken(1L); + final String token = "Bearer " + tokenProvider.createToken(1L).getAccessToken(); mockMvc.perform(get("/api/members/me/role") .param("study-id", "one") diff --git a/backend/src/test/resources/schema.sql b/backend/src/test/resources/schema.sql index a08adda05..6d6ff5c7f 100644 --- a/backend/src/test/resources/schema.sql +++ b/backend/src/test/resources/schema.sql @@ -5,6 +5,7 @@ DROP TABLE IF EXISTS category; DROP TABLE IF EXISTS review; DROP TABLE IF EXISTS study; DROP TABLE IF EXISTS member; +DROP TABLE IF EXISTS token; CREATE TABLE member ( @@ -79,3 +80,10 @@ CREATE TABLE study_member FOREIGN KEY (study_id) REFERENCES study (id), FOREIGN KEY (member_id) REFERENCES member (id) ); + +CREATE TABLE token +( + id BIGINT PRIMARY KEY AUTO_INCREMENT, + github_id BIGINT NOT NULL UNIQUE, + refresh_token MEDIUMTEXT +); From cb8905e0659696f058966f0382b36ccfb808e6ee Mon Sep 17 00:00:00 2001 From: TaeYoon <uni613@naver.com> Date: Fri, 12 Aug 2022 13:50:31 +0900 Subject: [PATCH 08/51] =?UTF-8?q?[FE]=20=EB=A6=AC=ED=94=84=EB=A0=88?= =?UTF-8?q?=EC=8B=9C=20=ED=86=A0=ED=81=B0=20=EB=B0=A9=EC=8B=9D=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9=20(#244)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: Error -> AxiosError 타입 수정 * feat: refreshToken 조회 api 및 모킹 서버 구현 * feat: Header 컴포넌트 로그아웃 기능 수정 * feat: 사용자 친화 에러 메세지로 수정 * refactor: MyStudyPage에서 useMyStudyPage 훅 분리 * chore: auth 절대 경로 추가 * feat: accessToken저장 장소 변경 sessionStorage -> 지역 변수 * refactor: import문 및 파일명 변경 getRefreshToken -> getAccessToken postAccessToken -> postLogin * feat: 로그아웃시 refreshToken 삭제 요청 * refactor: 타입명 수정 * feat: tokenHandlers 수정 refreshToken 로직 추가 * refactor: console 제거 * feat: AccessTokenController 수정 * feat: silent refresh 적용 로그인 또는 refresh 요청 시 새로운 accessToken을 발급 받으면, 주기마다 silent refresh 발급 실패 시 로그아웃 * refactor: 사용하지 않는 로직 제거 * feat: 401에러가 났을 시 처리 * refactor: 주석 제거 * refactor: eslint에 따라 수정 * fix: cypress 오류 수정 * fix: ts cypress 메서드 인식 오류 수정 * fix: 드롭박스 버그 수정 * style: 스터디 생성 페이지 스타일 수정 * refactor: prettier 및 eslint 적용 * refactor: type EmptyObject -> null * refactor: type EmptyObject -> null * refactor: 메서드에 private 키워드 추가 * chore: tsconfig 수정 - lib 삭제: target이 관련 lib을 불러오므로 필요 없음 - types cypress/tsconfig.json으로 이동 * refactor: 에러 코드 상수 분리 * refactor: 로직 리팩토링 - early return - AccessTokenController 리팩토링 * chore: 프론트엔드 ci workflow에 테스트 추가 * feat: 토큰 만료 시간 수정 --- .github/workflows/deploy-frontend-dev.yml | 3 +- .github/workflows/frontend.yml | 39 +- frontend/.prettierrc.json | 5 +- frontend/.storybook/main.js | 1 + frontend/cypress.config.ts | 6 +- .../e2e/CreateStudyFormValidation.cy.tsx | 8 +- frontend/cypress/tsconfig.json | 11 + frontend/env/.env.local | 2 +- frontend/package-lock.json | 4212 ++++++++--------- frontend/src/api/axiosInstance.ts | 18 +- frontend/src/api/deleteRefreshToken.ts | 10 + frontend/src/api/deleteReview.ts | 4 +- frontend/src/api/getAccessToken.ts | 10 + frontend/src/api/index.ts | 8 +- frontend/src/api/postAccessToken.ts | 16 - frontend/src/api/postJoiningStudy.ts | 4 +- frontend/src/api/postLogin.ts | 16 + frontend/src/api/postNewStudy.ts | 7 +- frontend/src/api/postReview.ts | 4 +- frontend/src/api/putReview.ts | 4 +- frontend/src/auth/accessToken.ts | 63 + .../components/button-group/ButtonGroup.tsx | 6 +- frontend/src/constants.ts | 10 +- frontend/src/context/login/LoginProvider.tsx | 8 +- .../src/context/search/SearchProvider.tsx | 4 +- frontend/src/custom-types/index.ts | 11 +- frontend/src/custom-types/theme.d.ts | 1 + frontend/src/hooks/useAuth.ts | 26 +- frontend/src/hooks/useForm.tsx | 4 - frontend/src/hooks/useUserInfo.ts | 5 +- frontend/src/index.tsx | 51 +- frontend/src/layout/header/Header.tsx | 10 +- frontend/src/mocks/detailStudyHandlers.ts | 2 +- frontend/src/mocks/handlers.ts | 40 +- frontend/src/mocks/memberHandlers.ts | 2 +- frontend/src/mocks/tokenHandlers.ts | 38 +- .../CreateStudyPage.style.tsx | 8 +- .../create-study-page/CreateStudyPage.tsx | 2 +- .../max-member-count/MaxMemberCount.tsx | 8 +- .../components/meta-box/MetaBox.style.tsx | 24 +- .../components/meta-box/MetaBox.tsx | 4 +- .../components/period/Peroid.tsx | 2 - .../components/publish/Publish.tsx | 2 - .../create-study-page/hooks/useGetTagList.ts | 3 +- .../hooks/usePostNewStudy.ts | 5 +- .../StudyWideFloatBox.tsx | 1 - .../pages/detail-page/hooks/useDetailPage.ts | 10 +- .../pages/detail-page/hooks/useGetDetail.ts | 3 +- .../detail-page/hooks/useGetStudyReviews.ts | 3 +- .../hooks/useLoginRedirectPage.ts | 21 +- .../filter-section/FilterSection.tsx | 5 +- .../hooks/useGetInfiniteStudyList.ts | 3 +- .../src/pages/my-study-page/MyStudyPage.tsx | 28 +- .../my-study-page/hooks/useGetMyStudy.ts | 3 +- .../my-study-page/hooks/useMyStudyPage.ts | 27 + .../components/reivew-form/ReviewForm.tsx | 5 +- .../review-comment/ReviewComment.tsx | 4 +- .../review-comment/useReviewComment.ts | 5 +- .../review-edit-form/ReviewEditForm.tsx | 5 +- .../hooks/useStudyRoomPage.tsx | 7 +- frontend/tsconfig.json | 6 +- frontend/webpack/webpack.common.js | 1 + 62 files changed, 2480 insertions(+), 2384 deletions(-) create mode 100644 frontend/cypress/tsconfig.json create mode 100644 frontend/src/api/deleteRefreshToken.ts create mode 100644 frontend/src/api/getAccessToken.ts delete mode 100644 frontend/src/api/postAccessToken.ts create mode 100644 frontend/src/api/postLogin.ts create mode 100644 frontend/src/auth/accessToken.ts create mode 100644 frontend/src/pages/my-study-page/hooks/useMyStudyPage.ts diff --git a/.github/workflows/deploy-frontend-dev.yml b/.github/workflows/deploy-frontend-dev.yml index 9ec1f72fe..1ca6d9834 100644 --- a/.github/workflows/deploy-frontend-dev.yml +++ b/.github/workflows/deploy-frontend-dev.yml @@ -32,7 +32,8 @@ jobs: - name: Build run: npm run build-dev - - uses: cypress-io/github-action@v2 + - name: Test + uses: cypress-io/github-action@v2 with: start: npm run start wait-on: "http://localhost:3000" diff --git a/.github/workflows/frontend.yml b/.github/workflows/frontend.yml index f015bd1e2..c71af185b 100644 --- a/.github/workflows/frontend.yml +++ b/.github/workflows/frontend.yml @@ -2,7 +2,7 @@ name: frontend on: pull_request: - branches: [ "main", "develop" ] + branches: ["main", "develop"] defaults: run: @@ -10,7 +10,6 @@ defaults: jobs: build: - runs-on: ubuntu-latest strategy: @@ -18,17 +17,25 @@ jobs: node-version: [16.x] steps: - - uses: actions/checkout@v3 - - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v3 - with: - node-version: ${{ matrix.node-version }} - cache: 'npm' - cache-dependency-path: "./frontend/package-lock.json" - - - name: Install - run: npm install - - - name: Build - run: npm run build + - uses: actions/checkout@v3 + + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node-version }} + cache: "npm" + cache-dependency-path: "./frontend/package-lock.json" + + - name: Install + run: npm install + + - name: Build + run: npm run build + + - name: Test + uses: cypress-io/github-action@v2 + with: + start: npm run start + wait-on: "http://localhost:3000" + working-directory: frontend + config-file: cypress.config.ts diff --git a/frontend/.prettierrc.json b/frontend/.prettierrc.json index b81d0a844..382545cc9 100644 --- a/frontend/.prettierrc.json +++ b/frontend/.prettierrc.json @@ -6,7 +6,7 @@ "singleQuote": true, "trailingComma": "all", "bracketSpacing": true, - "jsxBracketSameLine": false, + "bracketSameLine": false, "arrowParens": "avoid", "endOfLine": "auto", @@ -20,6 +20,9 @@ "^@custom-types", "^@styles/(.*)$", "^@api", + "^@api/(.*)$", + "^@auth", + "^@auth/(.*)$", "^@hooks/(.*)$", "^@context/(.*)$", "^@layout", diff --git a/frontend/.storybook/main.js b/frontend/.storybook/main.js index 7eb0eaeed..6cc0326f2 100644 --- a/frontend/.storybook/main.js +++ b/frontend/.storybook/main.js @@ -20,6 +20,7 @@ module.exports = { '@utils': resolve(__dirname, '../src/utils'), '@constants': resolve(__dirname, '../src/constants.ts'), '@api': resolve(__dirname, '../src/api'), + '@auth': resolve(__dirname, '../src/auth'), '@context': resolve(__dirname, '../src/context'), '@detail-page': resolve(__dirname, '../src/pages/detail-page'), '@main-page': resolve(__dirname, '../src/pages/main-page'), diff --git a/frontend/cypress.config.ts b/frontend/cypress.config.ts index e7e804a0e..67da4550c 100644 --- a/frontend/cypress.config.ts +++ b/frontend/cypress.config.ts @@ -1,15 +1,11 @@ import { defineConfig } from 'cypress'; -import webpackConfig from './webpack/webpack.dev'; +import webpackConfig from './webpack/webpack.local'; export default defineConfig({ e2e: { - setupNodeEvents(on, config) { - // implement node event listeners here - }, baseUrl: 'http://localhost:3000', }, - component: { devServer: { framework: 'react', diff --git a/frontend/cypress/e2e/CreateStudyFormValidation.cy.tsx b/frontend/cypress/e2e/CreateStudyFormValidation.cy.tsx index 4dfaf7c93..f7cc5b86d 100644 --- a/frontend/cypress/e2e/CreateStudyFormValidation.cy.tsx +++ b/frontend/cypress/e2e/CreateStudyFormValidation.cy.tsx @@ -1,4 +1,4 @@ -import { ACCESS_TOKEN_KEY, DESCRIPTION_LENGTH, EXCERPT_LENGTH, PATH, TITLE_LENGTH } from '@constants'; +import { DESCRIPTION_LENGTH, EXCERPT_LENGTH, PATH, TITLE_LENGTH } from '@constants'; const studyTitle = 'studyTitle'; const description = 'description'; @@ -8,8 +8,10 @@ const startDate = 'startDate'; describe('스터디 개설 페이지 폼 유효성 테스트', () => { before(() => { - window.sessionStorage.setItem(ACCESS_TOKEN_KEY, 'asdfasf'); - cy.visit(PATH.CREATE_STUDY); + cy.visit(`${PATH.LOGIN}?code=hihihih`).then(() => { + cy.wait(1000); + cy.visit(PATH.CREATE_STUDY); + }); }); beforeEach(() => { diff --git a/frontend/cypress/tsconfig.json b/frontend/cypress/tsconfig.json new file mode 100644 index 000000000..ce2bccbca --- /dev/null +++ b/frontend/cypress/tsconfig.json @@ -0,0 +1,11 @@ +// cypress ts target을 ES6로 설정 +// esnext일 때 에러 발생 +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "target": "ES6", + "module": "ES6", + "types": ["cypress", "@testing-library/cypress"] //https://docs.cypress.io/guides/tooling/typescript-support#Configure-tsconfig-json + }, + "include": ["../src", "../node_modules/cypress", "./**/*.cy.ts", "./**/*.cy.tsx"] +} diff --git a/frontend/env/.env.local b/frontend/env/.env.local index 479996cc2..5d27bfa26 100644 --- a/frontend/env/.env.local +++ b/frontend/env/.env.local @@ -1,2 +1,2 @@ -API_URL="https://dev.moamoa.ne.kr" +API_URL="" CLIENT_ID="cb83d95cd5644436b090" diff --git a/frontend/package-lock.json b/frontend/package-lock.json index c16080810..9a162488e 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -96,20 +96,20 @@ } }, "node_modules/@babel/core": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.18.9.tgz", - "integrity": "sha512-1LIb1eL8APMy91/IMW+31ckrfBM4yCoLaVzoDhZUKSM4cu1L1nIidyxkCgzPAgrC5WEz36IPEr/eSeSF9pIn+g==", + "version": "7.18.10", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.18.10.tgz", + "integrity": "sha512-JQM6k6ENcBFKVtWvLavlvi/mPcpYZ3+R+2EySDEMSMbp7Mn4FexlbbJVrx2R7Ijhr01T8gyqrOaABWIOgxeUyw==", "dependencies": { "@ampproject/remapping": "^2.1.0", "@babel/code-frame": "^7.18.6", - "@babel/generator": "^7.18.9", + "@babel/generator": "^7.18.10", "@babel/helper-compilation-targets": "^7.18.9", "@babel/helper-module-transforms": "^7.18.9", "@babel/helpers": "^7.18.9", - "@babel/parser": "^7.18.9", - "@babel/template": "^7.18.6", - "@babel/traverse": "^7.18.9", - "@babel/types": "^7.18.9", + "@babel/parser": "^7.18.10", + "@babel/template": "^7.18.10", + "@babel/traverse": "^7.18.10", + "@babel/types": "^7.18.10", "convert-source-map": "^1.7.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -125,11 +125,11 @@ } }, "node_modules/@babel/generator": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.18.9.tgz", - "integrity": "sha512-wt5Naw6lJrL1/SGkipMiFxJjtyczUWTP38deiP1PO60HsBjDeKk08CGC3S8iVuvf0FmTdgKwU1KIXzSKL1G0Ug==", + "version": "7.18.12", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.18.12.tgz", + "integrity": "sha512-dfQ8ebCN98SvyL7IxNMCUtZQSq5R7kxgN+r8qYTGDmmSion1hX2C0zq2yo1bsCDhXixokv1SAWTZUMYbO/V5zg==", "dependencies": { - "@babel/types": "^7.18.9", + "@babel/types": "^7.18.10", "@jridgewell/gen-mapping": "^0.3.2", "jsesc": "^2.5.1" }, @@ -230,15 +230,13 @@ } }, "node_modules/@babel/helper-define-polyfill-provider": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.3.1.tgz", - "integrity": "sha512-J9hGMpJQmtWmj46B3kBHmL38UhJGhYX7eqkcq+2gsstyYt341HmPeWspihX43yVRA0mS+8GGk2Gckc7bY/HCmA==", + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.3.2.tgz", + "integrity": "sha512-r9QJJ+uDWrd+94BSPcP6/de67ygLtvVy6cK4luE6MOuDsZIdoaPBnfSpbO/+LTifjPckbKXRuI9BB/Z2/y3iTg==", "dev": true, "dependencies": { - "@babel/helper-compilation-targets": "^7.13.0", - "@babel/helper-module-imports": "^7.12.13", - "@babel/helper-plugin-utils": "^7.13.0", - "@babel/traverse": "^7.13.0", + "@babel/helper-compilation-targets": "^7.17.7", + "@babel/helper-plugin-utils": "^7.16.7", "debug": "^4.1.1", "lodash.debounce": "^4.0.8", "resolve": "^1.14.2", @@ -420,6 +418,14 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-string-parser": { + "version": "7.18.10", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.18.10.tgz", + "integrity": "sha512-XtIfWmeNY3i4t7t4D2t02q50HvqHybPqW2ki1kosnvWCwuCMeo81Jf0gwr85jy/neUdg5XDdeFE/80DXiO+njw==", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-validator-identifier": { "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.18.6.tgz", @@ -437,15 +443,15 @@ } }, "node_modules/@babel/helper-wrap-function": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.18.9.tgz", - "integrity": "sha512-cG2ru3TRAL6a60tfQflpEfs4ldiPwF6YW3zfJiRgmoFVIaC1vGnBBgatfec+ZUziPHkHSaXAuEck3Cdkf3eRpQ==", + "version": "7.18.11", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.18.11.tgz", + "integrity": "sha512-oBUlbv+rjZLh2Ks9SKi4aL7eKaAXBWleHzU89mP0G6BMUlRxSckk9tSIkgDGydhgFxHuGSlBQZfnaD47oBEB7w==", "dev": true, "dependencies": { "@babel/helper-function-name": "^7.18.9", - "@babel/template": "^7.18.6", - "@babel/traverse": "^7.18.9", - "@babel/types": "^7.18.9" + "@babel/template": "^7.18.10", + "@babel/traverse": "^7.18.11", + "@babel/types": "^7.18.10" }, "engines": { "node": ">=6.9.0" @@ -478,9 +484,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.18.9.tgz", - "integrity": "sha512-9uJveS9eY9DJ0t64YbIBZICtJy8a5QrDEVdiLCG97fVLpDTpGX7t8mMSb6OWw6Lrnjqj4O8zwjELX3dhoMgiBg==", + "version": "7.18.11", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.18.11.tgz", + "integrity": "sha512-9JKn5vN+hDt0Hdqn1PiJ2guflwP+B6Ga8qbDuoF0PzzVhrzsKIJo8yGqVk6CmMHiMei9w1C1Bp9IMJSIK+HPIQ==", "bin": { "parser": "bin/babel-parser.js" }, @@ -521,14 +527,14 @@ } }, "node_modules/@babel/plugin-proposal-async-generator-functions": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.18.6.tgz", - "integrity": "sha512-WAz4R9bvozx4qwf74M+sfqPMKfSqwM0phxPTR6iJIi8robgzXwkEgmeJG1gEKhm6sDqT/U9aV3lfcqybIpev8w==", + "version": "7.18.10", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.18.10.tgz", + "integrity": "sha512-1mFuY2TOsR1hxbjCo4QL+qlIjV07p4H4EUYw2J/WCqsvFV6V9X9z9YhXbWndc/4fw+hYGlDT7egYxliMp5O6Ew==", "dev": true, "dependencies": { - "@babel/helper-environment-visitor": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/helper-remap-async-to-generator": "^7.18.6", + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-plugin-utils": "^7.18.9", + "@babel/helper-remap-async-to-generator": "^7.18.9", "@babel/plugin-syntax-async-generators": "^7.8.4" }, "engines": { @@ -572,9 +578,9 @@ } }, "node_modules/@babel/plugin-proposal-decorators": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.18.9.tgz", - "integrity": "sha512-KD7zDNaD14CRpjQjVbV4EnH9lsKYlcpUrhZH37ei2IY+AlXrfAPy5pTmRUE4X6X1k8EsKXPraykxeaogqQvSGA==", + "version": "7.18.10", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.18.10.tgz", + "integrity": "sha512-wdGTwWF5QtpTY/gbBtQLAiCnoxfD4qMbN87NYZle1dOZ9Os8Y6zXcKrIaOU8W+TIvFUWVGG9tUgNww3CjXRVVw==", "dev": true, "dependencies": { "@babel/helper-create-class-features-plugin": "^7.18.9", @@ -607,9 +613,9 @@ } }, "node_modules/@babel/plugin-proposal-export-default-from": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-default-from/-/plugin-proposal-export-default-from-7.18.9.tgz", - "integrity": "sha512-1qtsLNCDm5awHLIt+2qAFDi31XC94r4QepMQcOosC7FpY6O+Bgay5f2IyAQt2wvm1TARumpFprnQt5pTIJ9nUg==", + "version": "7.18.10", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-default-from/-/plugin-proposal-export-default-from-7.18.10.tgz", + "integrity": "sha512-5H2N3R2aQFxkV4PIBUR/i7PUSwgTZjouJKzI8eKswfIjT0PhvzkPn0t0wIS5zn6maQuvtT0t1oHtMUz61LOuow==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.18.9", @@ -1472,16 +1478,16 @@ } }, "node_modules/@babel/plugin-transform-react-jsx": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.18.6.tgz", - "integrity": "sha512-Mz7xMPxoy9kPS/JScj6fJs03TZ/fZ1dJPlMjRAgTaxaS0fUBk8FV/A2rRgfPsVCZqALNwMexD+0Uaf5zlcKPpw==", + "version": "7.18.10", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.18.10.tgz", + "integrity": "sha512-gCy7Iikrpu3IZjYZolFE4M1Sm+nrh1/6za2Ewj77Z+XirT4TsbJcvOFOyF+fRPwU6AKKK136CZxx6L8AbSFG6A==", "dev": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.18.6", "@babel/helper-module-imports": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.9", "@babel/plugin-syntax-jsx": "^7.18.6", - "@babel/types": "^7.18.6" + "@babel/types": "^7.18.10" }, "engines": { "node": ">=6.9.0" @@ -1629,13 +1635,13 @@ } }, "node_modules/@babel/plugin-transform-typescript": { - "version": "7.18.8", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.18.8.tgz", - "integrity": "sha512-p2xM8HI83UObjsZGofMV/EdYjamsDm6MoN3hXPYIT0+gxIoopE+B7rPYKAxfrz9K9PK7JafTTjqYC6qipLExYA==", + "version": "7.18.12", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.18.12.tgz", + "integrity": "sha512-2vjjam0cum0miPkenUbQswKowuxs/NjMwIKEq0zwegRxXk12C9YOF9STXnaUptITOtOJHKHpzvvWYOjbm6tc0w==", "dev": true, "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6", + "@babel/helper-create-class-features-plugin": "^7.18.9", + "@babel/helper-plugin-utils": "^7.18.9", "@babel/plugin-syntax-typescript": "^7.18.6" }, "engines": { @@ -1646,12 +1652,12 @@ } }, "node_modules/@babel/plugin-transform-unicode-escapes": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.18.6.tgz", - "integrity": "sha512-XNRwQUXYMP7VLuy54cr/KS/WeL3AZeORhrmeZ7iewgu+X2eBqmpaLI/hzqr9ZxCeUoq0ASK4GUzSM0BDhZkLFw==", + "version": "7.18.10", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.18.10.tgz", + "integrity": "sha512-kKAdAI+YzPgGY/ftStBFXTI1LZFju38rYThnfMykS+IXy8BVx+res7s2fxf1l8I35DV2T97ezo6+SGrXz6B3iQ==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-plugin-utils": "^7.18.9" }, "engines": { "node": ">=6.9.0" @@ -1677,9 +1683,9 @@ } }, "node_modules/@babel/preset-env": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.18.9.tgz", - "integrity": "sha512-75pt/q95cMIHWssYtyfjVlvI+QEZQThQbKvR9xH+F/Agtw/s4Wfc2V9Bwd/P39VtixB7oWxGdH4GteTTwYJWMg==", + "version": "7.18.10", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.18.10.tgz", + "integrity": "sha512-wVxs1yjFdW3Z/XkNfXKoblxoHgbtUF7/l3PvvP4m02Qz9TZ6uZGxRVYjSQeR87oQmHco9zWitW5J82DJ7sCjvA==", "dev": true, "dependencies": { "@babel/compat-data": "^7.18.8", @@ -1688,7 +1694,7 @@ "@babel/helper-validator-option": "^7.18.6", "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.18.6", "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.18.9", - "@babel/plugin-proposal-async-generator-functions": "^7.18.6", + "@babel/plugin-proposal-async-generator-functions": "^7.18.10", "@babel/plugin-proposal-class-properties": "^7.18.6", "@babel/plugin-proposal-class-static-block": "^7.18.6", "@babel/plugin-proposal-dynamic-import": "^7.18.6", @@ -1748,13 +1754,13 @@ "@babel/plugin-transform-sticky-regex": "^7.18.6", "@babel/plugin-transform-template-literals": "^7.18.9", "@babel/plugin-transform-typeof-symbol": "^7.18.9", - "@babel/plugin-transform-unicode-escapes": "^7.18.6", + "@babel/plugin-transform-unicode-escapes": "^7.18.10", "@babel/plugin-transform-unicode-regex": "^7.18.6", "@babel/preset-modules": "^0.1.5", - "@babel/types": "^7.18.9", - "babel-plugin-polyfill-corejs2": "^0.3.1", - "babel-plugin-polyfill-corejs3": "^0.5.2", - "babel-plugin-polyfill-regenerator": "^0.3.1", + "@babel/types": "^7.18.10", + "babel-plugin-polyfill-corejs2": "^0.3.2", + "babel-plugin-polyfill-corejs3": "^0.5.3", + "babel-plugin-polyfill-regenerator": "^0.4.0", "core-js-compat": "^3.22.1", "semver": "^6.3.0" }, @@ -1879,31 +1885,31 @@ } }, "node_modules/@babel/template": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.18.6.tgz", - "integrity": "sha512-JoDWzPe+wgBsTTgdnIma3iHNFC7YVJoPssVBDjiHfNlyt4YcunDtcDOUmfVDfCK5MfdsaIoX9PkijPhjH3nYUw==", + "version": "7.18.10", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.18.10.tgz", + "integrity": "sha512-TI+rCtooWHr3QJ27kJxfjutghu44DLnasDMwpDqCXVTal9RLp3RSYNh4NdBrRP2cQAoG9A8juOQl6P6oZG4JxA==", "dependencies": { "@babel/code-frame": "^7.18.6", - "@babel/parser": "^7.18.6", - "@babel/types": "^7.18.6" + "@babel/parser": "^7.18.10", + "@babel/types": "^7.18.10" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.18.9.tgz", - "integrity": "sha512-LcPAnujXGwBgv3/WHv01pHtb2tihcyW1XuL9wd7jqh1Z8AQkTd+QVjMrMijrln0T7ED3UXLIy36P9Ao7W75rYg==", + "version": "7.18.11", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.18.11.tgz", + "integrity": "sha512-TG9PiM2R/cWCAy6BPJKeHzNbu4lPzOSZpeMfeNErskGpTJx6trEvFaVCbDvpcxwy49BKWmEPwiW8mrysNiDvIQ==", "dependencies": { "@babel/code-frame": "^7.18.6", - "@babel/generator": "^7.18.9", + "@babel/generator": "^7.18.10", "@babel/helper-environment-visitor": "^7.18.9", "@babel/helper-function-name": "^7.18.9", "@babel/helper-hoist-variables": "^7.18.6", "@babel/helper-split-export-declaration": "^7.18.6", - "@babel/parser": "^7.18.9", - "@babel/types": "^7.18.9", + "@babel/parser": "^7.18.11", + "@babel/types": "^7.18.10", "debug": "^4.1.0", "globals": "^11.1.0" }, @@ -1912,10 +1918,11 @@ } }, "node_modules/@babel/types": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.18.9.tgz", - "integrity": "sha512-WwMLAg2MvJmt/rKEVQBBhIVffMmnilX4oe0sRe7iPOHIGsqpruFHHdrfj4O1CMMtgMtCU4oPafZjDPCRgO57Wg==", + "version": "7.18.10", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.18.10.tgz", + "integrity": "sha512-MJvnbEiiNkpjo+LknnmRrqbY1GPUUggjv+wQVjetM/AONoupqRALB7I6jGqNUAZsKcRIEu2J6FRFvsczljjsaQ==", "dependencies": { + "@babel/helper-string-parser": "^7.18.10", "@babel/helper-validator-identifier": "^7.18.6", "to-fast-properties": "^2.0.0" }, @@ -2148,17 +2155,17 @@ } }, "node_modules/@emotion/babel-plugin": { - "version": "11.9.2", - "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.9.2.tgz", - "integrity": "sha512-Pr/7HGH6H6yKgnVFNEj2MVlreu3ADqftqjqwUvDy/OJzKFgxKeTQ+eeUf20FOTuHVkDON2iNa25rAXVYtWJCjw==", + "version": "11.10.0", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.10.0.tgz", + "integrity": "sha512-xVnpDAAbtxL1dsuSelU5A7BnY/lftws0wUexNJZTPsvX/1tM4GZJbclgODhvW4E+NH7E5VFcH0bBn30NvniPJA==", "dependencies": { - "@babel/helper-module-imports": "^7.12.13", - "@babel/plugin-syntax-jsx": "^7.12.13", - "@babel/runtime": "^7.13.10", - "@emotion/hash": "^0.8.0", - "@emotion/memoize": "^0.7.5", - "@emotion/serialize": "^1.0.2", - "babel-plugin-macros": "^2.6.1", + "@babel/helper-module-imports": "^7.16.7", + "@babel/plugin-syntax-jsx": "^7.17.12", + "@babel/runtime": "^7.18.3", + "@emotion/hash": "^0.9.0", + "@emotion/memoize": "^0.8.0", + "@emotion/serialize": "^1.1.0", + "babel-plugin-macros": "^3.1.0", "convert-source-map": "^1.5.0", "escape-string-regexp": "^4.0.0", "find-root": "^1.1.0", @@ -2170,47 +2177,47 @@ } }, "node_modules/@emotion/cache": { - "version": "11.9.3", - "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.9.3.tgz", - "integrity": "sha512-0dgkI/JKlCXa+lEXviaMtGBL0ynpx4osh7rjOXE71q9bIF8G+XhJgvi+wDu0B0IdCVx37BffiwXlN9I3UuzFvg==", - "dependencies": { - "@emotion/memoize": "^0.7.4", - "@emotion/sheet": "^1.1.1", - "@emotion/utils": "^1.0.0", - "@emotion/weak-memoize": "^0.2.5", + "version": "11.10.1", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.10.1.tgz", + "integrity": "sha512-uZTj3Yz5D69GE25iFZcIQtibnVCFsc/6+XIozyL3ycgWvEdif2uEw9wlUt6umjLr4Keg9K6xRPHmD8LGi+6p1A==", + "dependencies": { + "@emotion/memoize": "^0.8.0", + "@emotion/sheet": "^1.2.0", + "@emotion/utils": "^1.2.0", + "@emotion/weak-memoize": "^0.3.0", "stylis": "4.0.13" } }, "node_modules/@emotion/hash": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz", - "integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==" + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.0.tgz", + "integrity": "sha512-14FtKiHhy2QoPIzdTcvh//8OyBlknNs2nXRwIhG904opCby3l+9Xaf/wuPvICBF0rc1ZCNBd3nKe9cd2mecVkQ==" }, "node_modules/@emotion/is-prop-valid": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.1.3.tgz", - "integrity": "sha512-RFg04p6C+1uO19uG8N+vqanzKqiM9eeV1LDOG3bmkYmuOj7NbKNlFC/4EZq5gnwAIlcC/jOT24f8Td0iax2SXA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.0.tgz", + "integrity": "sha512-3aDpDprjM0AwaxGE09bOPkNxHpBd+kA6jty3RnaEXdweX1DF1U3VQpPYb0g1IStAuK7SVQ1cy+bNBBKp4W3Fjg==", "dev": true, "dependencies": { - "@emotion/memoize": "^0.7.4" + "@emotion/memoize": "^0.8.0" } }, "node_modules/@emotion/memoize": { - "version": "0.7.5", - "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.5.tgz", - "integrity": "sha512-igX9a37DR2ZPGYtV6suZ6whr8pTFtyHL3K/oLUotxpSVO2ASaprmAe2Dkq7tBo7CRY7MMDrAa9nuQP9/YG8FxQ==" + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.0.tgz", + "integrity": "sha512-G/YwXTkv7Den9mXDO7AhLWkE3q+I92B+VqAE+dYG4NGPaHZGvt3G8Q0p9vmE+sq7rTGphUbAvmQ9YpbfMQGGlA==" }, "node_modules/@emotion/react": { - "version": "11.9.3", - "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.9.3.tgz", - "integrity": "sha512-g9Q1GcTOlzOEjqwuLF/Zd9LC+4FljjPjDfxSM7KmEakm+hsHXk+bYZ2q+/hTJzr0OUNkujo72pXLQvXj6H+GJQ==", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@emotion/babel-plugin": "^11.7.1", - "@emotion/cache": "^11.9.3", - "@emotion/serialize": "^1.0.4", - "@emotion/utils": "^1.1.0", - "@emotion/weak-memoize": "^0.2.5", + "version": "11.10.0", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.10.0.tgz", + "integrity": "sha512-K6z9zlHxxBXwN8TcpwBKcEsBsOw4JWCCmR+BeeOWgqp8GIU1yA2Odd41bwdAAr0ssbQrbJbVnndvv7oiv1bZeQ==", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.10.0", + "@emotion/cache": "^11.10.0", + "@emotion/serialize": "^1.1.0", + "@emotion/utils": "^1.2.0", + "@emotion/weak-memoize": "^0.3.0", "hoist-non-react-statics": "^3.3.1" }, "peerDependencies": { @@ -2227,33 +2234,33 @@ } }, "node_modules/@emotion/serialize": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.0.4.tgz", - "integrity": "sha512-1JHamSpH8PIfFwAMryO2bNka+y8+KA5yga5Ocf2d7ZEiJjb7xlLW7aknBGZqJLajuLOvJ+72vN+IBSwPlXD1Pg==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.1.0.tgz", + "integrity": "sha512-F1ZZZW51T/fx+wKbVlwsfchr5q97iW8brAnXmsskz4d0hVB4O3M/SiA3SaeH06x02lSNzkkQv+n3AX3kCXKSFA==", "dependencies": { - "@emotion/hash": "^0.8.0", - "@emotion/memoize": "^0.7.4", - "@emotion/unitless": "^0.7.5", - "@emotion/utils": "^1.0.0", + "@emotion/hash": "^0.9.0", + "@emotion/memoize": "^0.8.0", + "@emotion/unitless": "^0.8.0", + "@emotion/utils": "^1.2.0", "csstype": "^3.0.2" } }, "node_modules/@emotion/sheet": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.1.1.tgz", - "integrity": "sha512-J3YPccVRMiTZxYAY0IOq3kd+hUP8idY8Kz6B/Cyo+JuXq52Ek+zbPbSQUrVQp95aJ+lsAW7DPL1P2Z+U1jGkKA==" + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.2.0.tgz", + "integrity": "sha512-OiTkRgpxescko+M51tZsMq7Puu/KP55wMT8BgpcXVG2hqXc0Vo0mfymJ/Uj24Hp0i083ji/o0aLddh08UEjq8w==" }, "node_modules/@emotion/styled": { - "version": "11.9.3", - "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.9.3.tgz", - "integrity": "sha512-o3sBNwbtoVz9v7WB1/Y/AmXl69YHmei2mrVnK7JgyBJ//Rst5yqPZCecEJlMlJrFeWHp+ki/54uN265V2pEcXA==", + "version": "11.10.0", + "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.10.0.tgz", + "integrity": "sha512-V9oaEH6V4KePeQpgUE83i8ht+4Ri3E8Djp/ZPJ4DQlqWhSKITvgzlR3/YQE2hdfP4Jw3qVRkANJz01LLqK9/TA==", "dev": true, "dependencies": { - "@babel/runtime": "^7.13.10", - "@emotion/babel-plugin": "^11.7.1", - "@emotion/is-prop-valid": "^1.1.3", - "@emotion/serialize": "^1.0.4", - "@emotion/utils": "^1.1.0" + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.10.0", + "@emotion/is-prop-valid": "^1.2.0", + "@emotion/serialize": "^1.1.0", + "@emotion/utils": "^1.2.0" }, "peerDependencies": { "@babel/core": "^7.0.0", @@ -2270,19 +2277,19 @@ } }, "node_modules/@emotion/unitless": { - "version": "0.7.5", - "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz", - "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==" + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.0.tgz", + "integrity": "sha512-VINS5vEYAscRl2ZUDiT3uMPlrFQupiKgHz5AA4bCH1miKBg4qtwkim1qPmJj/4WG6TreYMY111rEFsjupcOKHw==" }, "node_modules/@emotion/utils": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.1.0.tgz", - "integrity": "sha512-iRLa/Y4Rs5H/f2nimczYmS5kFJEbpiVvgN3XVfZ022IYhuNA1IRSHEizcof88LtCTXtl9S2Cxt32KgaXEu72JQ==" + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.2.0.tgz", + "integrity": "sha512-sn3WH53Kzpw8oQ5mgMmIzzyAaH2ZqFEbozVVBSYp538E06OSE6ytOp7pRAjNQR+Q/orwqdQYJSe2m3hCOeznkw==" }, "node_modules/@emotion/weak-memoize": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz", - "integrity": "sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA==" + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.0.tgz", + "integrity": "sha512-AHPmaAx+RYfZz0eYu6Gviiagpmiyw98ySSlQvCUhVGDRtDFe4DBS0x1bSjdF3gqUDYOczB+yYvBTtEylYSdRhg==" }, "node_modules/@esbuild/linux-loong64": { "version": "0.14.54", @@ -2327,9 +2334,9 @@ "dev": true }, "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "13.16.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.16.0.tgz", - "integrity": "sha512-A1lrQfpNF+McdPOnnFqY3kSN0AFTy485bTi1bkLk4mVPODIUEcSfhHgRqA+QdXPksrSTTztYXx37NFV+GpGk3Q==", + "version": "13.17.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.17.0.tgz", + "integrity": "sha512-1C+6nQRb1GwGMKm2dH/E7enFAMxGTmGI7/dEdhy/DNelv85w9B72t3uc5frtMNXIbzrarJJ/lTCjcaZwbLJmyw==", "dev": true, "dependencies": { "type-fest": "^0.20.2" @@ -2360,9 +2367,9 @@ "dev": true }, "node_modules/@humanwhocodes/config-array": { - "version": "0.9.5", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.9.5.tgz", - "integrity": "sha512-ObyMyWxZiCu/yTisA7uzx81s40xR2fD5Cg/2Kq7G02ajkNubJf6BopgDTmDyc3U7sXpNKM8cYOw7s7Tyr+DnCw==", + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.10.4.tgz", + "integrity": "sha512-mXAIHxZT3Vcpg83opl1wGlVZ9xydbfZO3r5YfRSH6Gpp2J/PfdBP0wbDa2sO6/qRbcalpoevVyW6A/fI6LfeMw==", "dev": true, "dependencies": { "@humanwhocodes/object-schema": "^1.2.1", @@ -2373,6 +2380,16 @@ "node": ">=10.10.0" } }, + "node_modules/@humanwhocodes/gitignore-to-minimatch": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@humanwhocodes/gitignore-to-minimatch/-/gitignore-to-minimatch-1.0.2.tgz", + "integrity": "sha512-rSqmMJDdLFUsyxR6FMtD00nfQKKLFb1kv+qBbOVKqErvloEIJLo5bDTJTQNTYgeyp78JsA7u/NPi5jT1GR/MuA==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, "node_modules/@humanwhocodes/object-schema": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", @@ -2540,15 +2557,6 @@ "node": ">=8" } }, - "node_modules/@jest/transform/node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/@jest/transform/node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -3050,18 +3058,18 @@ } }, "node_modules/@storybook/addon-actions": { - "version": "6.5.9", - "resolved": "https://registry.npmjs.org/@storybook/addon-actions/-/addon-actions-6.5.9.tgz", - "integrity": "sha512-wDYm3M1bN+zcYZV3Q24M03b/P8DDpvj1oSoY6VLlxDAi56h8qZB/voeIS2I6vWXOB79C5tbwljYNQO0GsufS0g==", + "version": "6.5.10", + "resolved": "https://registry.npmjs.org/@storybook/addon-actions/-/addon-actions-6.5.10.tgz", + "integrity": "sha512-vpCnEu81fmtYzOf0QsRYoDuf9wXgVVl2VysE1dWRebRhIUDU0JurrthTnw322e38D4FzaoNGqZE7wnBYBohzZA==", "dev": true, "dependencies": { - "@storybook/addons": "6.5.9", - "@storybook/api": "6.5.9", - "@storybook/client-logger": "6.5.9", - "@storybook/components": "6.5.9", - "@storybook/core-events": "6.5.9", + "@storybook/addons": "6.5.10", + "@storybook/api": "6.5.10", + "@storybook/client-logger": "6.5.10", + "@storybook/components": "6.5.10", + "@storybook/core-events": "6.5.10", "@storybook/csf": "0.0.2--canary.4566f4d.1", - "@storybook/theming": "6.5.9", + "@storybook/theming": "6.5.10", "core-js": "^3.8.2", "fast-deep-equal": "^3.1.3", "global": "^4.4.0", @@ -3107,18 +3115,18 @@ } }, "node_modules/@storybook/addon-backgrounds": { - "version": "6.5.9", - "resolved": "https://registry.npmjs.org/@storybook/addon-backgrounds/-/addon-backgrounds-6.5.9.tgz", - "integrity": "sha512-9k+GiY5aiANLOct34ar29jqgdi5ZpCqpZ86zPH0GsEC6ifH6nzP4trLU0vFUe260XDCvB4g8YaI7JZKPhozERg==", + "version": "6.5.10", + "resolved": "https://registry.npmjs.org/@storybook/addon-backgrounds/-/addon-backgrounds-6.5.10.tgz", + "integrity": "sha512-5uzQda3dh891h7BL8e9Ymk7BI+QgkkzDJXuA4mHjOXfIiD3S3efhJI8amXuBC2ZpIr6zmVit0MqZVyoVve46cQ==", "dev": true, "dependencies": { - "@storybook/addons": "6.5.9", - "@storybook/api": "6.5.9", - "@storybook/client-logger": "6.5.9", - "@storybook/components": "6.5.9", - "@storybook/core-events": "6.5.9", + "@storybook/addons": "6.5.10", + "@storybook/api": "6.5.10", + "@storybook/client-logger": "6.5.10", + "@storybook/components": "6.5.10", + "@storybook/core-events": "6.5.10", "@storybook/csf": "0.0.2--canary.4566f4d.1", - "@storybook/theming": "6.5.9", + "@storybook/theming": "6.5.10", "core-js": "^3.8.2", "global": "^4.4.0", "memoizerific": "^1.11.3", @@ -3144,20 +3152,20 @@ } }, "node_modules/@storybook/addon-controls": { - "version": "6.5.9", - "resolved": "https://registry.npmjs.org/@storybook/addon-controls/-/addon-controls-6.5.9.tgz", - "integrity": "sha512-VvjkgK32bGURKyWU2No6Q2B0RQZjLZk8D3neVNCnrWxwrl1G82StegxjRPn/UZm9+MZVPvTvI46nj1VdgOktnw==", + "version": "6.5.10", + "resolved": "https://registry.npmjs.org/@storybook/addon-controls/-/addon-controls-6.5.10.tgz", + "integrity": "sha512-lC2y3XcolmQAJwFurIyGrynAHPWmfNtTCdu3rQBTVGwyxCoNwdOOeC2jV0BRqX2+CW6OHzJr9frNWXPSaZ8c4w==", "dev": true, "dependencies": { - "@storybook/addons": "6.5.9", - "@storybook/api": "6.5.9", - "@storybook/client-logger": "6.5.9", - "@storybook/components": "6.5.9", - "@storybook/core-common": "6.5.9", + "@storybook/addons": "6.5.10", + "@storybook/api": "6.5.10", + "@storybook/client-logger": "6.5.10", + "@storybook/components": "6.5.10", + "@storybook/core-common": "6.5.10", "@storybook/csf": "0.0.2--canary.4566f4d.1", - "@storybook/node-logger": "6.5.9", - "@storybook/store": "6.5.9", - "@storybook/theming": "6.5.9", + "@storybook/node-logger": "6.5.10", + "@storybook/store": "6.5.10", + "@storybook/theming": "6.5.10", "core-js": "^3.8.2", "lodash": "^4.17.21", "ts-dedent": "^2.0.0" @@ -3180,29 +3188,29 @@ } }, "node_modules/@storybook/addon-docs": { - "version": "6.5.9", - "resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-6.5.9.tgz", - "integrity": "sha512-9lwOZyiOJFUgGd9ADVfcgpels5o0XOXqGMeVLuzT1160nopbZjNjo/3+YLJ0pyHRPpMJ4rmq2+vxRQR6PVRgPg==", + "version": "6.5.10", + "resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-6.5.10.tgz", + "integrity": "sha512-1kgjo3f0vL6GN8fTwLL05M/q/kDdzvuqwhxPY/v5hubFb3aQZGr2yk9pRBaLAbs4bez0yG0ASXcwhYnrEZUppg==", "dev": true, "dependencies": { "@babel/plugin-transform-react-jsx": "^7.12.12", "@babel/preset-env": "^7.12.11", "@jest/transform": "^26.6.2", "@mdx-js/react": "^1.6.22", - "@storybook/addons": "6.5.9", - "@storybook/api": "6.5.9", - "@storybook/components": "6.5.9", - "@storybook/core-common": "6.5.9", - "@storybook/core-events": "6.5.9", + "@storybook/addons": "6.5.10", + "@storybook/api": "6.5.10", + "@storybook/components": "6.5.10", + "@storybook/core-common": "6.5.10", + "@storybook/core-events": "6.5.10", "@storybook/csf": "0.0.2--canary.4566f4d.1", - "@storybook/docs-tools": "6.5.9", + "@storybook/docs-tools": "6.5.10", "@storybook/mdx1-csf": "^0.0.1", - "@storybook/node-logger": "6.5.9", - "@storybook/postinstall": "6.5.9", - "@storybook/preview-web": "6.5.9", - "@storybook/source-loader": "6.5.9", - "@storybook/store": "6.5.9", - "@storybook/theming": "6.5.9", + "@storybook/node-logger": "6.5.10", + "@storybook/postinstall": "6.5.10", + "@storybook/preview-web": "6.5.10", + "@storybook/source-loader": "6.5.10", + "@storybook/store": "6.5.10", + "@storybook/theming": "6.5.10", "babel-loader": "^8.0.0", "core-js": "^3.8.2", "fast-deep-equal": "^3.1.3", @@ -3249,23 +3257,23 @@ } }, "node_modules/@storybook/addon-essentials": { - "version": "6.5.9", - "resolved": "https://registry.npmjs.org/@storybook/addon-essentials/-/addon-essentials-6.5.9.tgz", - "integrity": "sha512-V9ThjKQsde4A2Es20pLFBsn0MWx2KCJuoTcTsANP4JDcbvEmj8UjbDWbs8jAU+yzJT5r+CI6NoWmQudv12ZOgw==", - "dev": true, - "dependencies": { - "@storybook/addon-actions": "6.5.9", - "@storybook/addon-backgrounds": "6.5.9", - "@storybook/addon-controls": "6.5.9", - "@storybook/addon-docs": "6.5.9", - "@storybook/addon-measure": "6.5.9", - "@storybook/addon-outline": "6.5.9", - "@storybook/addon-toolbars": "6.5.9", - "@storybook/addon-viewport": "6.5.9", - "@storybook/addons": "6.5.9", - "@storybook/api": "6.5.9", - "@storybook/core-common": "6.5.9", - "@storybook/node-logger": "6.5.9", + "version": "6.5.10", + "resolved": "https://registry.npmjs.org/@storybook/addon-essentials/-/addon-essentials-6.5.10.tgz", + "integrity": "sha512-PT2aiR4vgAyB0pl3HNBUa4/a7NDRxASxAazz7zt9ZDirkipDKfxwdcLeRoJzwSngVDWEhuz5/paN5x4eNp4Hww==", + "dev": true, + "dependencies": { + "@storybook/addon-actions": "6.5.10", + "@storybook/addon-backgrounds": "6.5.10", + "@storybook/addon-controls": "6.5.10", + "@storybook/addon-docs": "6.5.10", + "@storybook/addon-measure": "6.5.10", + "@storybook/addon-outline": "6.5.10", + "@storybook/addon-toolbars": "6.5.10", + "@storybook/addon-viewport": "6.5.10", + "@storybook/addons": "6.5.10", + "@storybook/api": "6.5.10", + "@storybook/core-common": "6.5.10", + "@storybook/node-logger": "6.5.10", "core-js": "^3.8.2", "regenerator-runtime": "^0.13.7", "ts-dedent": "^2.0.0" @@ -3332,21 +3340,21 @@ } }, "node_modules/@storybook/addon-interactions": { - "version": "6.5.9", - "resolved": "https://registry.npmjs.org/@storybook/addon-interactions/-/addon-interactions-6.5.9.tgz", - "integrity": "sha512-p3xBbrhmYTHvRO8MqAIr2DucgrXt38nJE71rogLNLsJ01rUN4JsLI8OkQAMQbqfIpwC27irMjQxJTp4HSzkFJA==", + "version": "6.5.10", + "resolved": "https://registry.npmjs.org/@storybook/addon-interactions/-/addon-interactions-6.5.10.tgz", + "integrity": "sha512-+O/ZuQjonpFmTdFRqjCimQTx4S4c1+S3dYCn6gD/E4xzqlQn1BQaER3paX/aBUKb3oRaSO9RUQ+uxePM4zBEwA==", "dev": true, "dependencies": { "@devtools-ds/object-inspector": "^1.1.2", - "@storybook/addons": "6.5.9", - "@storybook/api": "6.5.9", - "@storybook/client-logger": "6.5.9", - "@storybook/components": "6.5.9", - "@storybook/core-common": "6.5.9", - "@storybook/core-events": "6.5.9", + "@storybook/addons": "6.5.10", + "@storybook/api": "6.5.10", + "@storybook/client-logger": "6.5.10", + "@storybook/components": "6.5.10", + "@storybook/core-common": "6.5.10", + "@storybook/core-events": "6.5.10", "@storybook/csf": "0.0.2--canary.4566f4d.1", - "@storybook/instrumenter": "6.5.9", - "@storybook/theming": "6.5.9", + "@storybook/instrumenter": "6.5.10", + "@storybook/theming": "6.5.10", "core-js": "^3.8.2", "global": "^4.4.0", "jest-mock": "^27.0.6", @@ -3371,16 +3379,16 @@ } }, "node_modules/@storybook/addon-links": { - "version": "6.5.9", - "resolved": "https://registry.npmjs.org/@storybook/addon-links/-/addon-links-6.5.9.tgz", - "integrity": "sha512-4BYC7pkxL3NLRnEgTA9jpIkObQKril+XFj1WtmY/lngF90vvK0Kc/TtvTA2/5tSgrHfxEuPevIdxMIyLJ4ejWQ==", + "version": "6.5.10", + "resolved": "https://registry.npmjs.org/@storybook/addon-links/-/addon-links-6.5.10.tgz", + "integrity": "sha512-r3WzYIPz7WjHiaPObC2Tg6bHuZRBb/Kt/X+Eitw+jTqBel7ksvkO36tn81q8Eyj61qIdNQmUWAaX/0aewT0kLA==", "dev": true, "dependencies": { - "@storybook/addons": "6.5.9", - "@storybook/client-logger": "6.5.9", - "@storybook/core-events": "6.5.9", + "@storybook/addons": "6.5.10", + "@storybook/client-logger": "6.5.10", + "@storybook/core-events": "6.5.10", "@storybook/csf": "0.0.2--canary.4566f4d.1", - "@storybook/router": "6.5.9", + "@storybook/router": "6.5.10", "@types/qs": "^6.9.5", "core-js": "^3.8.2", "global": "^4.4.0", @@ -3407,16 +3415,16 @@ } }, "node_modules/@storybook/addon-measure": { - "version": "6.5.9", - "resolved": "https://registry.npmjs.org/@storybook/addon-measure/-/addon-measure-6.5.9.tgz", - "integrity": "sha512-0aA22wD0CIEUccsEbWkckCOXOwr4VffofMH1ToVCOeqBoyLOMB0gxFubESeprqM54CWsYh2DN1uujgD6508cwA==", + "version": "6.5.10", + "resolved": "https://registry.npmjs.org/@storybook/addon-measure/-/addon-measure-6.5.10.tgz", + "integrity": "sha512-ss7L1H5K5hXygDIoVwj+QyVXbve5V67x7CofLiLCgQYuJzfO16+sPGjiTGWMpTb4ijox2uKWnTkpilt5bCjXgw==", "dev": true, "dependencies": { - "@storybook/addons": "6.5.9", - "@storybook/api": "6.5.9", - "@storybook/client-logger": "6.5.9", - "@storybook/components": "6.5.9", - "@storybook/core-events": "6.5.9", + "@storybook/addons": "6.5.10", + "@storybook/api": "6.5.10", + "@storybook/client-logger": "6.5.10", + "@storybook/components": "6.5.10", + "@storybook/core-events": "6.5.10", "@storybook/csf": "0.0.2--canary.4566f4d.1", "core-js": "^3.8.2", "global": "^4.4.0" @@ -3439,16 +3447,16 @@ } }, "node_modules/@storybook/addon-outline": { - "version": "6.5.9", - "resolved": "https://registry.npmjs.org/@storybook/addon-outline/-/addon-outline-6.5.9.tgz", - "integrity": "sha512-oJ1DK3BDJr6aTlZc9axfOxV1oDkZO7hOohgUQDaKO1RZrSpyQsx2ViK2X6p/W7JhFJHKh7rv+nGCaVlLz3YIZA==", + "version": "6.5.10", + "resolved": "https://registry.npmjs.org/@storybook/addon-outline/-/addon-outline-6.5.10.tgz", + "integrity": "sha512-AjdaeQ+/iBKmGrAqRW4niwMB6AkgGnYmSzVs5Cf6F/Sb4Dp+vzgLNOwLABD9qs8Ri8dvHl5J4QpVwQKUhYZaOQ==", "dev": true, "dependencies": { - "@storybook/addons": "6.5.9", - "@storybook/api": "6.5.9", - "@storybook/client-logger": "6.5.9", - "@storybook/components": "6.5.9", - "@storybook/core-events": "6.5.9", + "@storybook/addons": "6.5.10", + "@storybook/api": "6.5.10", + "@storybook/client-logger": "6.5.10", + "@storybook/components": "6.5.10", + "@storybook/core-events": "6.5.10", "@storybook/csf": "0.0.2--canary.4566f4d.1", "core-js": "^3.8.2", "global": "^4.4.0", @@ -3473,16 +3481,16 @@ } }, "node_modules/@storybook/addon-toolbars": { - "version": "6.5.9", - "resolved": "https://registry.npmjs.org/@storybook/addon-toolbars/-/addon-toolbars-6.5.9.tgz", - "integrity": "sha512-6JFQNHYVZUwp17p5rppc+iQJ2QOIWPTF+ni1GMMThjc84mzXs2+899Sf1aPFTvrFJTklmT+bPX6x4aUTouVa1w==", + "version": "6.5.10", + "resolved": "https://registry.npmjs.org/@storybook/addon-toolbars/-/addon-toolbars-6.5.10.tgz", + "integrity": "sha512-S0Ljc6Wv+bPbx2e0iTveJ6bBDqjsemu+FZD4qDLsHreoI7DAcqyrF5Def1l8xNohixIVpx8dQpYXRtyzNlXekg==", "dev": true, "dependencies": { - "@storybook/addons": "6.5.9", - "@storybook/api": "6.5.9", - "@storybook/client-logger": "6.5.9", - "@storybook/components": "6.5.9", - "@storybook/theming": "6.5.9", + "@storybook/addons": "6.5.10", + "@storybook/api": "6.5.10", + "@storybook/client-logger": "6.5.10", + "@storybook/components": "6.5.10", + "@storybook/theming": "6.5.10", "core-js": "^3.8.2", "regenerator-runtime": "^0.13.7" }, @@ -3504,17 +3512,17 @@ } }, "node_modules/@storybook/addon-viewport": { - "version": "6.5.9", - "resolved": "https://registry.npmjs.org/@storybook/addon-viewport/-/addon-viewport-6.5.9.tgz", - "integrity": "sha512-thKS+iw6M7ueDQQ7M66STZ5rgtJKliAcIX6UCopo0Ffh4RaRYmX6MCjqtvBKk8joyXUvm9SpWQemJD9uBQrjgw==", + "version": "6.5.10", + "resolved": "https://registry.npmjs.org/@storybook/addon-viewport/-/addon-viewport-6.5.10.tgz", + "integrity": "sha512-RFMd+4kZljyuJjR9OJ2bFXHrSG7VTi5FDZYWEU+4W1sBxzC+JhnVnUP+HJH3gUxEFIRQC5neRzwWRE9RUUoALQ==", "dev": true, "dependencies": { - "@storybook/addons": "6.5.9", - "@storybook/api": "6.5.9", - "@storybook/client-logger": "6.5.9", - "@storybook/components": "6.5.9", - "@storybook/core-events": "6.5.9", - "@storybook/theming": "6.5.9", + "@storybook/addons": "6.5.10", + "@storybook/api": "6.5.10", + "@storybook/client-logger": "6.5.10", + "@storybook/components": "6.5.10", + "@storybook/core-events": "6.5.10", + "@storybook/theming": "6.5.10", "core-js": "^3.8.2", "global": "^4.4.0", "memoizerific": "^1.11.3", @@ -3539,18 +3547,18 @@ } }, "node_modules/@storybook/addons": { - "version": "6.5.9", - "resolved": "https://registry.npmjs.org/@storybook/addons/-/addons-6.5.9.tgz", - "integrity": "sha512-adwdiXg+mntfPocLc1KXjZXyLgGk7Aac699Fwe+OUYPEC5tW347Rm/kFatcE556d42o5czcRiq3ZSIGWnm9ieQ==", + "version": "6.5.10", + "resolved": "https://registry.npmjs.org/@storybook/addons/-/addons-6.5.10.tgz", + "integrity": "sha512-VD4tBCQ23PkSeDoxuHcKy0RfhIs3oMYjBacOZx7d0bvOzK9WjPyvE2ysDAh7r/ceqnwmWHAScIpE+I1RU7gl+g==", "dev": true, "dependencies": { - "@storybook/api": "6.5.9", - "@storybook/channels": "6.5.9", - "@storybook/client-logger": "6.5.9", - "@storybook/core-events": "6.5.9", + "@storybook/api": "6.5.10", + "@storybook/channels": "6.5.10", + "@storybook/client-logger": "6.5.10", + "@storybook/core-events": "6.5.10", "@storybook/csf": "0.0.2--canary.4566f4d.1", - "@storybook/router": "6.5.9", - "@storybook/theming": "6.5.9", + "@storybook/router": "6.5.10", + "@storybook/theming": "6.5.10", "@types/webpack-env": "^1.16.0", "core-js": "^3.8.2", "global": "^4.4.0", @@ -3566,18 +3574,18 @@ } }, "node_modules/@storybook/api": { - "version": "6.5.9", - "resolved": "https://registry.npmjs.org/@storybook/api/-/api-6.5.9.tgz", - "integrity": "sha512-9ylztnty4Y+ALU/ehW3BML9czjCAFsWvrwuCi6UgcwNjswwjSX3VRLhfD1KT3pl16ho//95LgZ0LnSwROCcPOA==", + "version": "6.5.10", + "resolved": "https://registry.npmjs.org/@storybook/api/-/api-6.5.10.tgz", + "integrity": "sha512-AkmgSPNEGdKp4oZA4KQ+RJsacw7GwfvjsVDnCkcXqS9zmSr/RNL0fhpcd60KKkmx/hGKPTDFpK3ZayxDrJ/h4A==", "dev": true, "dependencies": { - "@storybook/channels": "6.5.9", - "@storybook/client-logger": "6.5.9", - "@storybook/core-events": "6.5.9", + "@storybook/channels": "6.5.10", + "@storybook/client-logger": "6.5.10", + "@storybook/core-events": "6.5.10", "@storybook/csf": "0.0.2--canary.4566f4d.1", - "@storybook/router": "6.5.9", + "@storybook/router": "6.5.10", "@storybook/semver": "^7.3.2", - "@storybook/theming": "6.5.9", + "@storybook/theming": "6.5.10", "core-js": "^3.8.2", "fast-deep-equal": "^3.1.3", "global": "^4.4.0", @@ -3599,28 +3607,28 @@ } }, "node_modules/@storybook/builder-webpack4": { - "version": "6.5.9", - "resolved": "https://registry.npmjs.org/@storybook/builder-webpack4/-/builder-webpack4-6.5.9.tgz", - "integrity": "sha512-YOeA4++9uRZ8Hog1wC60yjaxBOiI1FRQNtax7b9E7g+kP8UlSCPCGcv4gls9hFmzbzTOPfQTWnToA9Oa6jzRVw==", + "version": "6.5.10", + "resolved": "https://registry.npmjs.org/@storybook/builder-webpack4/-/builder-webpack4-6.5.10.tgz", + "integrity": "sha512-AoKjsCNoQQoZXYwBDxO8s+yVEd5FjBJAaysEuUTHq2fb81jwLrGcEOo6hjw4jqfugZQIzYUEjPazlvubS78zpw==", "dev": true, "dependencies": { "@babel/core": "^7.12.10", - "@storybook/addons": "6.5.9", - "@storybook/api": "6.5.9", - "@storybook/channel-postmessage": "6.5.9", - "@storybook/channels": "6.5.9", - "@storybook/client-api": "6.5.9", - "@storybook/client-logger": "6.5.9", - "@storybook/components": "6.5.9", - "@storybook/core-common": "6.5.9", - "@storybook/core-events": "6.5.9", - "@storybook/node-logger": "6.5.9", - "@storybook/preview-web": "6.5.9", - "@storybook/router": "6.5.9", + "@storybook/addons": "6.5.10", + "@storybook/api": "6.5.10", + "@storybook/channel-postmessage": "6.5.10", + "@storybook/channels": "6.5.10", + "@storybook/client-api": "6.5.10", + "@storybook/client-logger": "6.5.10", + "@storybook/components": "6.5.10", + "@storybook/core-common": "6.5.10", + "@storybook/core-events": "6.5.10", + "@storybook/node-logger": "6.5.10", + "@storybook/preview-web": "6.5.10", + "@storybook/router": "6.5.10", "@storybook/semver": "^7.3.2", - "@storybook/store": "6.5.9", - "@storybook/theming": "6.5.9", - "@storybook/ui": "6.5.9", + "@storybook/store": "6.5.10", + "@storybook/theming": "6.5.10", + "@storybook/ui": "6.5.10", "@types/node": "^14.0.10 || ^16.0.0", "@types/webpack": "^4.41.26", "autoprefixer": "^9.8.6", @@ -3673,9 +3681,9 @@ "dev": true }, "node_modules/@storybook/builder-webpack4/node_modules/@types/node": { - "version": "16.11.45", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.45.tgz", - "integrity": "sha512-3rKg/L5x0rofKuuUt5zlXzOnKyIHXmIu5R8A0TuNDMF2062/AOIDBciFIjToLEJ/9F9DzkHNot+BpNsMI1OLdQ==", + "version": "16.11.47", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.47.tgz", + "integrity": "sha512-fpP+jk2zJ4VW66+wAMFoBJlx1bxmBKx4DUFf68UHgdGCOuyUTDlLWqsaNPJh7xhNDykyJ9eIzAygilP/4WoN8g==", "dev": true }, "node_modules/@storybook/builder-webpack4/node_modules/@webassemblyjs/ast": { @@ -3904,6 +3912,15 @@ "node": ">= 4.0" } }, + "node_modules/@storybook/builder-webpack4/node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, "node_modules/@storybook/builder-webpack4/node_modules/enhanced-resolve": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.5.0.tgz", @@ -4378,6 +4395,16 @@ "webpack": "^2.0.0 || ^3.0.0 || ^4.0.0" } }, + "node_modules/@storybook/builder-webpack4/node_modules/webpack-sources": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz", + "integrity": "sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==", + "dev": true, + "dependencies": { + "source-list-map": "^2.0.0", + "source-map": "~0.6.1" + } + }, "node_modules/@storybook/builder-webpack4/node_modules/webpack/node_modules/terser-webpack-plugin": { "version": "1.4.5", "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.4.5.tgz", @@ -4414,27 +4441,27 @@ "dev": true }, "node_modules/@storybook/builder-webpack5": { - "version": "6.5.9", - "resolved": "https://registry.npmjs.org/@storybook/builder-webpack5/-/builder-webpack5-6.5.9.tgz", - "integrity": "sha512-NUVZ4Qci6HWPuoH8U/zQkdBO5soGgu7QYrGC/LWU0tRfmmZxkjr7IUU14ppDpGPYgx3r7jkaQI1J/E1YEmSCWQ==", + "version": "6.5.10", + "resolved": "https://registry.npmjs.org/@storybook/builder-webpack5/-/builder-webpack5-6.5.10.tgz", + "integrity": "sha512-Hcsm/TzGRXHndgQCftt+pzI7GQJRqAv8A8ie5b3aFcodhJfK0qzZsQD4Y4ZWxXh1I/xe5t74Kl2qUJ40PX+geA==", "dev": true, "dependencies": { "@babel/core": "^7.12.10", - "@storybook/addons": "6.5.9", - "@storybook/api": "6.5.9", - "@storybook/channel-postmessage": "6.5.9", - "@storybook/channels": "6.5.9", - "@storybook/client-api": "6.5.9", - "@storybook/client-logger": "6.5.9", - "@storybook/components": "6.5.9", - "@storybook/core-common": "6.5.9", - "@storybook/core-events": "6.5.9", - "@storybook/node-logger": "6.5.9", - "@storybook/preview-web": "6.5.9", - "@storybook/router": "6.5.9", + "@storybook/addons": "6.5.10", + "@storybook/api": "6.5.10", + "@storybook/channel-postmessage": "6.5.10", + "@storybook/channels": "6.5.10", + "@storybook/client-api": "6.5.10", + "@storybook/client-logger": "6.5.10", + "@storybook/components": "6.5.10", + "@storybook/core-common": "6.5.10", + "@storybook/core-events": "6.5.10", + "@storybook/node-logger": "6.5.10", + "@storybook/preview-web": "6.5.10", + "@storybook/router": "6.5.10", "@storybook/semver": "^7.3.2", - "@storybook/store": "6.5.9", - "@storybook/theming": "6.5.9", + "@storybook/store": "6.5.10", + "@storybook/theming": "6.5.10", "@types/node": "^14.0.10 || ^16.0.0", "babel-loader": "^8.0.0", "babel-plugin-named-exports-order": "^0.0.2", @@ -4473,9 +4500,9 @@ } }, "node_modules/@storybook/builder-webpack5/node_modules/@types/node": { - "version": "16.11.45", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.45.tgz", - "integrity": "sha512-3rKg/L5x0rofKuuUt5zlXzOnKyIHXmIu5R8A0TuNDMF2062/AOIDBciFIjToLEJ/9F9DzkHNot+BpNsMI1OLdQ==", + "version": "16.11.47", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.47.tgz", + "integrity": "sha512-fpP+jk2zJ4VW66+wAMFoBJlx1bxmBKx4DUFf68UHgdGCOuyUTDlLWqsaNPJh7xhNDykyJ9eIzAygilP/4WoN8g==", "dev": true }, "node_modules/@storybook/builder-webpack5/node_modules/ansi-styles": { @@ -4533,6 +4560,22 @@ "integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==", "dev": true }, + "node_modules/@storybook/builder-webpack5/node_modules/cosmiconfig": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz", + "integrity": "sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==", + "dev": true, + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.1.0", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.7.2" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/@storybook/builder-webpack5/node_modules/css-loader": { "version": "5.2.7", "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-5.2.7.tgz", @@ -4675,9 +4718,9 @@ "dev": true }, "node_modules/@storybook/builder-webpack5/node_modules/postcss": { - "version": "8.4.14", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.14.tgz", - "integrity": "sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig==", + "version": "8.4.16", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.16.tgz", + "integrity": "sha512-ipHE1XBvKzm5xI7hiHCZJCSugxvsdq2mPnsq5+UF+VHCjiBvtDrlxJfMBToWaP9D5XlgNmcFGqoHmUn0EYEaRQ==", "dev": true, "funding": [ { @@ -4887,14 +4930,14 @@ "dev": true }, "node_modules/@storybook/channel-postmessage": { - "version": "6.5.9", - "resolved": "https://registry.npmjs.org/@storybook/channel-postmessage/-/channel-postmessage-6.5.9.tgz", - "integrity": "sha512-pX/0R8UW7ezBhCrafRaL20OvMRcmESYvQQCDgjqSzJyHkcG51GOhsd6Ge93eJ6QvRMm9+w0Zs93N2VKjVtz0Qw==", + "version": "6.5.10", + "resolved": "https://registry.npmjs.org/@storybook/channel-postmessage/-/channel-postmessage-6.5.10.tgz", + "integrity": "sha512-t9PTA0UzFvYa3IlOfpBOolfrRMPTjUMIeCQ6FNyM0aj5GqLKSvoQzP8NeoRpIrvyf6ljFKKdaMaZ3fiCvh45ag==", "dev": true, "dependencies": { - "@storybook/channels": "6.5.9", - "@storybook/client-logger": "6.5.9", - "@storybook/core-events": "6.5.9", + "@storybook/channels": "6.5.10", + "@storybook/client-logger": "6.5.10", + "@storybook/core-events": "6.5.10", "core-js": "^3.8.2", "global": "^4.4.0", "qs": "^6.10.0", @@ -4906,13 +4949,13 @@ } }, "node_modules/@storybook/channel-websocket": { - "version": "6.5.9", - "resolved": "https://registry.npmjs.org/@storybook/channel-websocket/-/channel-websocket-6.5.9.tgz", - "integrity": "sha512-xtHvSNwuOhkgALwVshKWsoFhDmuvcosdYfxcfFGEiYKXIu46tRS5ZXmpmgEC/0JAVkVoFj5nL8bV7IY5np6oaA==", + "version": "6.5.10", + "resolved": "https://registry.npmjs.org/@storybook/channel-websocket/-/channel-websocket-6.5.10.tgz", + "integrity": "sha512-RTXMZbMWCS3xU+4GVIdfnUXsKcwg/WTozy88/5OxaKjGw6KgRedqLAQJKJ6Y5XlnwIcWelirkHj/COwTTXhbPg==", "dev": true, "dependencies": { - "@storybook/channels": "6.5.9", - "@storybook/client-logger": "6.5.9", + "@storybook/channels": "6.5.10", + "@storybook/client-logger": "6.5.10", "core-js": "^3.8.2", "global": "^4.4.0", "telejson": "^6.0.8" @@ -4923,9 +4966,9 @@ } }, "node_modules/@storybook/channels": { - "version": "6.5.9", - "resolved": "https://registry.npmjs.org/@storybook/channels/-/channels-6.5.9.tgz", - "integrity": "sha512-FvGA35nV38UPXWOl9ERapFTJaxwSTamQ339s2Ev7E9riyRG+GRkgTWzf5kECJgS1PAYKd/7m/RqKJT9BVv6A5g==", + "version": "6.5.10", + "resolved": "https://registry.npmjs.org/@storybook/channels/-/channels-6.5.10.tgz", + "integrity": "sha512-lo26YZ6kWpHXLhuHJF4P/bICY7jD/rXEZqReKtGOSk1Lv99/xvG6pqmcy3hWLf3v3Dy/8otjRPSR7izFVIIZgQ==", "dev": true, "dependencies": { "core-js": "^3.8.2", @@ -4938,18 +4981,18 @@ } }, "node_modules/@storybook/client-api": { - "version": "6.5.9", - "resolved": "https://registry.npmjs.org/@storybook/client-api/-/client-api-6.5.9.tgz", - "integrity": "sha512-pc7JKJoWLesixUKvG2nV36HukUuYoGRyAgD3PpIV7qSBS4JixqZ3VAHFUtqV1UzfOSQTovLSl4a0rIRnpie6gA==", + "version": "6.5.10", + "resolved": "https://registry.npmjs.org/@storybook/client-api/-/client-api-6.5.10.tgz", + "integrity": "sha512-3wBWZl3NvMFgMovgEh+euiARAT2FXzpvTF4Q1gerGMNNDlrGxHnFvSuy4FHg/irtOGLa4yLz43ULFbYtpKw0Lg==", "dev": true, "dependencies": { - "@storybook/addons": "6.5.9", - "@storybook/channel-postmessage": "6.5.9", - "@storybook/channels": "6.5.9", - "@storybook/client-logger": "6.5.9", - "@storybook/core-events": "6.5.9", + "@storybook/addons": "6.5.10", + "@storybook/channel-postmessage": "6.5.10", + "@storybook/channels": "6.5.10", + "@storybook/client-logger": "6.5.10", + "@storybook/core-events": "6.5.10", "@storybook/csf": "0.0.2--canary.4566f4d.1", - "@storybook/store": "6.5.9", + "@storybook/store": "6.5.10", "@types/qs": "^6.9.5", "@types/webpack-env": "^1.16.0", "core-js": "^3.8.2", @@ -4974,9 +5017,9 @@ } }, "node_modules/@storybook/client-logger": { - "version": "6.5.9", - "resolved": "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-6.5.9.tgz", - "integrity": "sha512-DOHL6p0uiDd3gV/Sb2FR+Vh6OiPrrf8BrA06uvXWsMRIIvEEvnparxv9EvPg7FlmUX0T3nq7d3juwjx4F8Wbcg==", + "version": "6.5.10", + "resolved": "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-6.5.10.tgz", + "integrity": "sha512-/xA0MHOevXev68hyLMQw8Qo8KczSIdXOxliAgrycMTkDmw5eKeA8TP7B8zP3wGuq/e3MrdD9/8MWhb/IQBNC3w==", "dev": true, "dependencies": { "core-js": "^3.8.2", @@ -4988,19 +5031,17 @@ } }, "node_modules/@storybook/components": { - "version": "6.5.9", - "resolved": "https://registry.npmjs.org/@storybook/components/-/components-6.5.9.tgz", - "integrity": "sha512-BhfX980O9zn/1J4FNMeDo8ZvL1m5Ml3T4HRpfYmEBnf8oW5b5BeF6S2K2cwFStZRjWqm1feUcwNpZxCBVMkQnQ==", + "version": "6.5.10", + "resolved": "https://registry.npmjs.org/@storybook/components/-/components-6.5.10.tgz", + "integrity": "sha512-9OhgB8YQfGwOKjo/N96N5mrtJ6qDVVoEM1zuhea32tJUd2eYf0aSWpryA9VnOM0V1q/8DAoCg5rPBMYWMBU5uw==", "dev": true, "dependencies": { - "@storybook/client-logger": "6.5.9", + "@storybook/client-logger": "6.5.10", "@storybook/csf": "0.0.2--canary.4566f4d.1", - "@storybook/theming": "6.5.9", - "@types/react-syntax-highlighter": "11.0.5", + "@storybook/theming": "6.5.10", "core-js": "^3.8.2", "memoizerific": "^1.11.3", "qs": "^6.10.0", - "react-syntax-highlighter": "^15.4.5", "regenerator-runtime": "^0.13.7", "util-deprecate": "^1.0.2" }, @@ -5014,13 +5055,13 @@ } }, "node_modules/@storybook/core": { - "version": "6.5.9", - "resolved": "https://registry.npmjs.org/@storybook/core/-/core-6.5.9.tgz", - "integrity": "sha512-Mt3TTQnjQt2/pa60A+bqDsAOrYpohapdtt4DDZEbS8h0V6u11KyYYh3w7FCySlL+sPEyogj63l5Ec76Jah3l2w==", + "version": "6.5.10", + "resolved": "https://registry.npmjs.org/@storybook/core/-/core-6.5.10.tgz", + "integrity": "sha512-K86yYa0tYlMxADlwQTculYvPROokQau09SCVqpsLg3wJCTvYFL4+SIqcYoyBSbFmHOdnYbJgPydjN33MYLiOZQ==", "dev": true, "dependencies": { - "@storybook/core-client": "6.5.9", - "@storybook/core-server": "6.5.9" + "@storybook/core-client": "6.5.10", + "@storybook/core-server": "6.5.10" }, "funding": { "type": "opencollective", @@ -5044,21 +5085,21 @@ } }, "node_modules/@storybook/core-client": { - "version": "6.5.9", - "resolved": "https://registry.npmjs.org/@storybook/core-client/-/core-client-6.5.9.tgz", - "integrity": "sha512-LY0QbhShowO+PQx3gao3wdVjpKMH1AaSLmuI95FrcjoMmSXGf96jVLKQp9mJRGeHIsAa93EQBYuCihZycM3Kbg==", + "version": "6.5.10", + "resolved": "https://registry.npmjs.org/@storybook/core-client/-/core-client-6.5.10.tgz", + "integrity": "sha512-THsIjNrOrampTl0Lgfjvfjk1JnktKb4CQLOM80KpQb4cjDqorBjJmErzUkUQ2y3fXvrDmQ/kUREkShET4XEdtA==", "dev": true, "dependencies": { - "@storybook/addons": "6.5.9", - "@storybook/channel-postmessage": "6.5.9", - "@storybook/channel-websocket": "6.5.9", - "@storybook/client-api": "6.5.9", - "@storybook/client-logger": "6.5.9", - "@storybook/core-events": "6.5.9", + "@storybook/addons": "6.5.10", + "@storybook/channel-postmessage": "6.5.10", + "@storybook/channel-websocket": "6.5.10", + "@storybook/client-api": "6.5.10", + "@storybook/client-logger": "6.5.10", + "@storybook/core-events": "6.5.10", "@storybook/csf": "0.0.2--canary.4566f4d.1", - "@storybook/preview-web": "6.5.9", - "@storybook/store": "6.5.9", - "@storybook/ui": "6.5.9", + "@storybook/preview-web": "6.5.10", + "@storybook/store": "6.5.10", + "@storybook/ui": "6.5.10", "airbnb-js-shims": "^2.2.1", "ansi-to-html": "^0.6.11", "core-js": "^3.8.2", @@ -5086,9 +5127,9 @@ } }, "node_modules/@storybook/core-common": { - "version": "6.5.9", - "resolved": "https://registry.npmjs.org/@storybook/core-common/-/core-common-6.5.9.tgz", - "integrity": "sha512-NxOK0mrOCo0TWZ7Npc5HU66EKoRHlrtg18/ZixblLDWQMIqY9XCck8K1kJ8QYpYCHla+aHIsYUArFe2vhlEfZA==", + "version": "6.5.10", + "resolved": "https://registry.npmjs.org/@storybook/core-common/-/core-common-6.5.10.tgz", + "integrity": "sha512-Bx+VKkfWdrAmD8T51Sjq/mMhRaiapBHcpG4cU5bc3DMbg+LF2/yrgqv/cjVu+m5gHAzYCac5D7gqzBgvG7Myww==", "dev": true, "dependencies": { "@babel/core": "^7.12.10", @@ -5113,7 +5154,7 @@ "@babel/preset-react": "^7.12.10", "@babel/preset-typescript": "^7.12.7", "@babel/register": "^7.12.1", - "@storybook/node-logger": "6.5.9", + "@storybook/node-logger": "6.5.10", "@storybook/semver": "^7.3.2", "@types/node": "^14.0.10 || ^16.0.0", "@types/pretty-hrtime": "^1.0.0", @@ -5176,9 +5217,9 @@ } }, "node_modules/@storybook/core-common/node_modules/@types/node": { - "version": "16.11.45", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.45.tgz", - "integrity": "sha512-3rKg/L5x0rofKuuUt5zlXzOnKyIHXmIu5R8A0TuNDMF2062/AOIDBciFIjToLEJ/9F9DzkHNot+BpNsMI1OLdQ==", + "version": "16.11.47", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.47.tgz", + "integrity": "sha512-fpP+jk2zJ4VW66+wAMFoBJlx1bxmBKx4DUFf68UHgdGCOuyUTDlLWqsaNPJh7xhNDykyJ9eIzAygilP/4WoN8g==", "dev": true }, "node_modules/@storybook/core-common/node_modules/@webassemblyjs/ast": { @@ -5339,37 +5380,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/@storybook/core-common/node_modules/babel-plugin-macros": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", - "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", - "dev": true, - "dependencies": { - "@babel/runtime": "^7.12.5", - "cosmiconfig": "^7.0.0", - "resolve": "^1.19.0" - }, - "engines": { - "node": ">=10", - "npm": ">=6" - } - }, - "node_modules/@storybook/core-common/node_modules/babel-plugin-macros/node_modules/cosmiconfig": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.1.tgz", - "integrity": "sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ==", - "dev": true, - "dependencies": { - "@types/parse-json": "^4.0.0", - "import-fresh": "^3.2.1", - "parse-json": "^5.0.0", - "path-type": "^4.0.0", - "yaml": "^1.10.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/@storybook/core-common/node_modules/babel-plugin-polyfill-corejs3": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.1.7.tgz", @@ -5494,6 +5504,22 @@ "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "dev": true }, + "node_modules/@storybook/core-common/node_modules/cosmiconfig": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz", + "integrity": "sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==", + "dev": true, + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.1.0", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.7.2" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/@storybook/core-common/node_modules/enhanced-resolve": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.5.0.tgz", @@ -5800,15 +5826,6 @@ "randombytes": "^2.1.0" } }, - "node_modules/@storybook/core-common/node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/@storybook/core-common/node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -5987,6 +6004,16 @@ } } }, + "node_modules/@storybook/core-common/node_modules/webpack-sources": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz", + "integrity": "sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==", + "dev": true, + "dependencies": { + "source-list-map": "^2.0.0", + "source-map": "~0.6.1" + } + }, "node_modules/@storybook/core-common/node_modules/webpack/node_modules/schema-utils": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", @@ -6014,9 +6041,9 @@ "dev": true }, "node_modules/@storybook/core-events": { - "version": "6.5.9", - "resolved": "https://registry.npmjs.org/@storybook/core-events/-/core-events-6.5.9.tgz", - "integrity": "sha512-tXt7a3ZvJOCeEKpNa/B5rQM5VI7UJLlOh3IHOImWn4HqoBRrZvbourmac+PRZAtXpos0h3c6554Hjapj/Sny5Q==", + "version": "6.5.10", + "resolved": "https://registry.npmjs.org/@storybook/core-events/-/core-events-6.5.10.tgz", + "integrity": "sha512-EVb1gO1172klVIAABLOoigFMx0V88uctY0K/qVCO8n6v+wd2+0Ccn63kl+gTxsAC3WZ8XhXh9q2w5ImHklVECw==", "dev": true, "dependencies": { "core-js": "^3.8.2" @@ -6027,23 +6054,23 @@ } }, "node_modules/@storybook/core-server": { - "version": "6.5.9", - "resolved": "https://registry.npmjs.org/@storybook/core-server/-/core-server-6.5.9.tgz", - "integrity": "sha512-YeePGUrd5fQPvGzMhowh124KrcZURFpFXg1VB0Op3ESqCIsInoMZeObci4Gc+binMXC7vcv7aw3EwSLU37qJzQ==", + "version": "6.5.10", + "resolved": "https://registry.npmjs.org/@storybook/core-server/-/core-server-6.5.10.tgz", + "integrity": "sha512-jqwpA0ccA8X5ck4esWBid04+cEIVqirdAcqJeNb9IZAD+bRreO4Im8ilzr7jc5AmQ9fkqHs2NByFKh9TITp8NQ==", "dev": true, "dependencies": { "@discoveryjs/json-ext": "^0.5.3", - "@storybook/builder-webpack4": "6.5.9", - "@storybook/core-client": "6.5.9", - "@storybook/core-common": "6.5.9", - "@storybook/core-events": "6.5.9", + "@storybook/builder-webpack4": "6.5.10", + "@storybook/core-client": "6.5.10", + "@storybook/core-common": "6.5.10", + "@storybook/core-events": "6.5.10", "@storybook/csf": "0.0.2--canary.4566f4d.1", - "@storybook/csf-tools": "6.5.9", - "@storybook/manager-webpack4": "6.5.9", - "@storybook/node-logger": "6.5.9", + "@storybook/csf-tools": "6.5.10", + "@storybook/manager-webpack4": "6.5.10", + "@storybook/node-logger": "6.5.10", "@storybook/semver": "^7.3.2", - "@storybook/store": "6.5.9", - "@storybook/telemetry": "6.5.9", + "@storybook/store": "6.5.10", + "@storybook/telemetry": "6.5.10", "@types/node": "^14.0.10 || ^16.0.0", "@types/node-fetch": "^2.5.7", "@types/pretty-hrtime": "^1.0.0", @@ -6099,9 +6126,9 @@ } }, "node_modules/@storybook/core-server/node_modules/@types/node": { - "version": "16.11.45", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.45.tgz", - "integrity": "sha512-3rKg/L5x0rofKuuUt5zlXzOnKyIHXmIu5R8A0TuNDMF2062/AOIDBciFIjToLEJ/9F9DzkHNot+BpNsMI1OLdQ==", + "version": "16.11.47", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.47.tgz", + "integrity": "sha512-fpP+jk2zJ4VW66+wAMFoBJlx1bxmBKx4DUFf68UHgdGCOuyUTDlLWqsaNPJh7xhNDykyJ9eIzAygilP/4WoN8g==", "dev": true }, "node_modules/@storybook/core-server/node_modules/@webassemblyjs/ast": { @@ -6358,15 +6385,6 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, - "node_modules/@storybook/core-server/node_modules/commander": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", - "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", - "dev": true, - "engines": { - "node": ">= 6" - } - }, "node_modules/@storybook/core-server/node_modules/enhanced-resolve": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.5.0.tgz", @@ -6624,15 +6642,6 @@ "randombytes": "^2.1.0" } }, - "node_modules/@storybook/core-server/node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/@storybook/core-server/node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -6789,6 +6798,16 @@ } } }, + "node_modules/@storybook/core-server/node_modules/webpack-sources": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz", + "integrity": "sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==", + "dev": true, + "dependencies": { + "source-list-map": "^2.0.0", + "source-map": "~0.6.1" + } + }, "node_modules/@storybook/core-server/node_modules/webpack/node_modules/watchpack": { "version": "1.7.5", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.7.5.tgz", @@ -6825,9 +6844,9 @@ } }, "node_modules/@storybook/csf-tools": { - "version": "6.5.9", - "resolved": "https://registry.npmjs.org/@storybook/csf-tools/-/csf-tools-6.5.9.tgz", - "integrity": "sha512-RAdhsO2XmEDyWy0qNQvdKMLeIZAuyfD+tYlUwBHRU6DbByDucvwgMOGy5dF97YNJFmyo93EUYJzXjUrJs3U1LQ==", + "version": "6.5.10", + "resolved": "https://registry.npmjs.org/@storybook/csf-tools/-/csf-tools-6.5.10.tgz", + "integrity": "sha512-H77kZQEisu7+skzeIbNZwmE09OqLjwJTeFhLN1pcjxKVa30LEI3pBHcNBxVKqgxl+Yg3KkB7W/ArLO2N+i2ohw==", "dev": true, "dependencies": { "@babel/core": "^7.12.10", @@ -6859,14 +6878,14 @@ } }, "node_modules/@storybook/docs-tools": { - "version": "6.5.9", - "resolved": "https://registry.npmjs.org/@storybook/docs-tools/-/docs-tools-6.5.9.tgz", - "integrity": "sha512-UoTaXLvec8x+q+4oYIk/t8DBju9C3ZTGklqOxDIt+0kS3TFAqEgI3JhKXqQOXgN5zDcvLVSxi8dbVAeSxk2ktA==", + "version": "6.5.10", + "resolved": "https://registry.npmjs.org/@storybook/docs-tools/-/docs-tools-6.5.10.tgz", + "integrity": "sha512-/bvYgOO+CxMEcHifkjJg0A60OTGOhcjGxnsB1h0gJuxMrqA/7Qwc108bFmPiX0eiD1BovFkZLJV4O6OY7zP5Vw==", "dev": true, "dependencies": { "@babel/core": "^7.12.10", "@storybook/csf": "0.0.2--canary.4566f4d.1", - "@storybook/store": "6.5.9", + "@storybook/store": "6.5.10", "core-js": "^3.8.2", "doctrine": "^3.0.0", "lodash": "^4.17.21", @@ -6878,14 +6897,14 @@ } }, "node_modules/@storybook/instrumenter": { - "version": "6.5.9", - "resolved": "https://registry.npmjs.org/@storybook/instrumenter/-/instrumenter-6.5.9.tgz", - "integrity": "sha512-I2nu/6H0MAy8d+d3LY/G6oYEFyWlc8f2Qs2DhpYh5FiCgIpzvY0DMN05Lf8oaXdKHL3lPF/YLJH17FttekXs1w==", + "version": "6.5.10", + "resolved": "https://registry.npmjs.org/@storybook/instrumenter/-/instrumenter-6.5.10.tgz", + "integrity": "sha512-3yKJW68wTnGYEts2mJQG6M7ZE+fe54fuy5lBBzRtvWnC15uWTxuaiFp2kxH5b+stSCi4m71ws45RNiEafdBgEQ==", "dev": true, "dependencies": { - "@storybook/addons": "6.5.9", - "@storybook/client-logger": "6.5.9", - "@storybook/core-events": "6.5.9", + "@storybook/addons": "6.5.10", + "@storybook/client-logger": "6.5.10", + "@storybook/core-events": "6.5.10", "core-js": "^3.8.2", "global": "^4.4.0" }, @@ -6895,20 +6914,20 @@ } }, "node_modules/@storybook/manager-webpack4": { - "version": "6.5.9", - "resolved": "https://registry.npmjs.org/@storybook/manager-webpack4/-/manager-webpack4-6.5.9.tgz", - "integrity": "sha512-49LZlHqWc7zj9tQfOOANixPYmLxqWTTZceA6DSXnKd9xDiO2Gl23Y+l/CSPXNZGDB8QFAwpimwqyKJj/NLH45A==", + "version": "6.5.10", + "resolved": "https://registry.npmjs.org/@storybook/manager-webpack4/-/manager-webpack4-6.5.10.tgz", + "integrity": "sha512-N/TlNDhuhARuFipR/ZJ/xEVESz23iIbCsZ4VNehLHm8PpiGlQUehk+jMjWmz5XV0bJItwjRclY+CU3GjZKblfQ==", "dev": true, "dependencies": { "@babel/core": "^7.12.10", "@babel/plugin-transform-template-literals": "^7.12.1", "@babel/preset-react": "^7.12.10", - "@storybook/addons": "6.5.9", - "@storybook/core-client": "6.5.9", - "@storybook/core-common": "6.5.9", - "@storybook/node-logger": "6.5.9", - "@storybook/theming": "6.5.9", - "@storybook/ui": "6.5.9", + "@storybook/addons": "6.5.10", + "@storybook/core-client": "6.5.10", + "@storybook/core-common": "6.5.10", + "@storybook/node-logger": "6.5.10", + "@storybook/theming": "6.5.10", + "@storybook/ui": "6.5.10", "@types/node": "^14.0.10 || ^16.0.0", "@types/webpack": "^4.41.26", "babel-loader": "^8.0.0", @@ -6957,9 +6976,9 @@ "dev": true }, "node_modules/@storybook/manager-webpack4/node_modules/@types/node": { - "version": "16.11.45", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.45.tgz", - "integrity": "sha512-3rKg/L5x0rofKuuUt5zlXzOnKyIHXmIu5R8A0TuNDMF2062/AOIDBciFIjToLEJ/9F9DzkHNot+BpNsMI1OLdQ==", + "version": "16.11.47", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.47.tgz", + "integrity": "sha512-fpP+jk2zJ4VW66+wAMFoBJlx1bxmBKx4DUFf68UHgdGCOuyUTDlLWqsaNPJh7xhNDykyJ9eIzAygilP/4WoN8g==", "dev": true }, "node_modules/@storybook/manager-webpack4/node_modules/@webassemblyjs/ast": { @@ -7237,6 +7256,15 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "node_modules/@storybook/manager-webpack4/node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, "node_modules/@storybook/manager-webpack4/node_modules/enhanced-resolve": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.5.0.tgz", @@ -7720,6 +7748,16 @@ } } }, + "node_modules/@storybook/manager-webpack4/node_modules/webpack-sources": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz", + "integrity": "sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==", + "dev": true, + "dependencies": { + "source-list-map": "^2.0.0", + "source-map": "~0.6.1" + } + }, "node_modules/@storybook/manager-webpack4/node_modules/webpack/node_modules/terser-webpack-plugin": { "version": "1.4.5", "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.4.5.tgz", @@ -7756,20 +7794,20 @@ "dev": true }, "node_modules/@storybook/manager-webpack5": { - "version": "6.5.9", - "resolved": "https://registry.npmjs.org/@storybook/manager-webpack5/-/manager-webpack5-6.5.9.tgz", - "integrity": "sha512-J1GamphSsaZLNBEhn1awgxzOS8KfvzrHtVlAm2VHwW7j1E1DItROFJhGCgduYYuBiN9eqm+KIYrxcr6cRuoolQ==", + "version": "6.5.10", + "resolved": "https://registry.npmjs.org/@storybook/manager-webpack5/-/manager-webpack5-6.5.10.tgz", + "integrity": "sha512-uRo+6e5MiVOtyFVMYIKVqvpDveCjHyzXBfetSYR7rKEZoaDMEnLLiuF7DIH12lzxwmzCJ1gIc4lf5HFiTMNkgw==", "dev": true, "dependencies": { "@babel/core": "^7.12.10", "@babel/plugin-transform-template-literals": "^7.12.1", "@babel/preset-react": "^7.12.10", - "@storybook/addons": "6.5.9", - "@storybook/core-client": "6.5.9", - "@storybook/core-common": "6.5.9", - "@storybook/node-logger": "6.5.9", - "@storybook/theming": "6.5.9", - "@storybook/ui": "6.5.9", + "@storybook/addons": "6.5.10", + "@storybook/core-client": "6.5.10", + "@storybook/core-common": "6.5.10", + "@storybook/node-logger": "6.5.10", + "@storybook/theming": "6.5.10", + "@storybook/ui": "6.5.10", "@types/node": "^14.0.10 || ^16.0.0", "babel-loader": "^8.0.0", "case-sensitive-paths-webpack-plugin": "^2.3.0", @@ -7809,9 +7847,9 @@ } }, "node_modules/@storybook/manager-webpack5/node_modules/@types/node": { - "version": "16.11.45", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.45.tgz", - "integrity": "sha512-3rKg/L5x0rofKuuUt5zlXzOnKyIHXmIu5R8A0TuNDMF2062/AOIDBciFIjToLEJ/9F9DzkHNot+BpNsMI1OLdQ==", + "version": "16.11.47", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.47.tgz", + "integrity": "sha512-fpP+jk2zJ4VW66+wAMFoBJlx1bxmBKx4DUFf68UHgdGCOuyUTDlLWqsaNPJh7xhNDykyJ9eIzAygilP/4WoN8g==", "dev": true }, "node_modules/@storybook/manager-webpack5/node_modules/ansi-styles": { @@ -7954,9 +7992,9 @@ "dev": true }, "node_modules/@storybook/manager-webpack5/node_modules/postcss": { - "version": "8.4.14", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.14.tgz", - "integrity": "sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig==", + "version": "8.4.16", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.16.tgz", + "integrity": "sha512-ipHE1XBvKzm5xI7hiHCZJCSugxvsdq2mPnsq5+UF+VHCjiBvtDrlxJfMBToWaP9D5XlgNmcFGqoHmUn0EYEaRQ==", "dev": true, "funding": [ { @@ -8188,9 +8226,9 @@ } }, "node_modules/@storybook/node-logger": { - "version": "6.5.9", - "resolved": "https://registry.npmjs.org/@storybook/node-logger/-/node-logger-6.5.9.tgz", - "integrity": "sha512-nZZNZG2Wtwv6Trxi3FrnIqUmB55xO+X/WQGPT5iKlqNjdRIu/T72mE7addcp4rbuWCQfZUhcDDGpBOwKtBxaGg==", + "version": "6.5.10", + "resolved": "https://registry.npmjs.org/@storybook/node-logger/-/node-logger-6.5.10.tgz", + "integrity": "sha512-bYswXIKV7Stru8vYfkjUMNN8UhF7Qg7NRsUvG5Djt5lLIae1XmUIgnH40mU/nW4X4BSfcR9MKxsSsngvn2WmQg==", "dev": true, "dependencies": { "@types/npmlog": "^4.1.2", @@ -8275,9 +8313,9 @@ } }, "node_modules/@storybook/postinstall": { - "version": "6.5.9", - "resolved": "https://registry.npmjs.org/@storybook/postinstall/-/postinstall-6.5.9.tgz", - "integrity": "sha512-KQBupK+FMRrtSt8IL0MzCZ/w9qbd25Yxxp/+ajfWgZTRgsWgVFOqcDyMhS16eNbBp5qKIBCBDXfEF+/mK8HwQQ==", + "version": "6.5.10", + "resolved": "https://registry.npmjs.org/@storybook/postinstall/-/postinstall-6.5.10.tgz", + "integrity": "sha512-xqUdpnFHYkn8MgtV+QztvIsRWa6jQUk7QT1Mu17Y0S7PbslNGsuskRPHenHhACXBJF+TM86R+4BaAhnVYTmElw==", "dev": true, "dependencies": { "core-js": "^3.8.2" @@ -8288,17 +8326,17 @@ } }, "node_modules/@storybook/preview-web": { - "version": "6.5.9", - "resolved": "https://registry.npmjs.org/@storybook/preview-web/-/preview-web-6.5.9.tgz", - "integrity": "sha512-4eMrO2HJyZUYyL/j+gUaDvry6iGedshwT5MQqe7J9FaA+Q2pNARQRB1X53f410w7S4sObRmYIAIluWPYdWym9w==", + "version": "6.5.10", + "resolved": "https://registry.npmjs.org/@storybook/preview-web/-/preview-web-6.5.10.tgz", + "integrity": "sha512-sTC/o5gkvALOtcNgtApGKGN9EavvSxRHBeBh+5BQjV2qQ8ap+26RsfUizNBECAa2Jrn4osaDYn9HRhJLFL69WA==", "dev": true, "dependencies": { - "@storybook/addons": "6.5.9", - "@storybook/channel-postmessage": "6.5.9", - "@storybook/client-logger": "6.5.9", - "@storybook/core-events": "6.5.9", + "@storybook/addons": "6.5.10", + "@storybook/channel-postmessage": "6.5.10", + "@storybook/client-logger": "6.5.10", + "@storybook/core-events": "6.5.10", "@storybook/csf": "0.0.2--canary.4566f4d.1", - "@storybook/store": "6.5.9", + "@storybook/store": "6.5.10", "ansi-to-html": "^0.6.11", "core-js": "^3.8.2", "global": "^4.4.0", @@ -8320,24 +8358,24 @@ } }, "node_modules/@storybook/react": { - "version": "6.5.9", - "resolved": "https://registry.npmjs.org/@storybook/react/-/react-6.5.9.tgz", - "integrity": "sha512-Rp+QaTQAzxJhwuzJXVd49mnIBLQRlF8llTxPT2YoGHdrGkku/zl/HblQ6H2yzEf15367VyzaAv/BpLsO9Jlfxg==", + "version": "6.5.10", + "resolved": "https://registry.npmjs.org/@storybook/react/-/react-6.5.10.tgz", + "integrity": "sha512-m8S1qQrwA7pDGwdKEvL6LV3YKvSzVUY297Fq+xcTU3irnAy4sHDuFoLqV6Mi1510mErK1r8+rf+0R5rEXB219g==", "dev": true, "dependencies": { "@babel/preset-flow": "^7.12.1", "@babel/preset-react": "^7.12.10", "@pmmmwh/react-refresh-webpack-plugin": "^0.5.3", - "@storybook/addons": "6.5.9", - "@storybook/client-logger": "6.5.9", - "@storybook/core": "6.5.9", - "@storybook/core-common": "6.5.9", + "@storybook/addons": "6.5.10", + "@storybook/client-logger": "6.5.10", + "@storybook/core": "6.5.10", + "@storybook/core-common": "6.5.10", "@storybook/csf": "0.0.2--canary.4566f4d.1", - "@storybook/docs-tools": "6.5.9", - "@storybook/node-logger": "6.5.9", + "@storybook/docs-tools": "6.5.10", + "@storybook/node-logger": "6.5.10", "@storybook/react-docgen-typescript-plugin": "1.0.2-canary.6.9d540b91e815f8fc2f8829189deb00553559ff63.0", "@storybook/semver": "^7.3.2", - "@storybook/store": "6.5.9", + "@storybook/store": "6.5.10", "@types/estree": "^0.0.51", "@types/node": "^14.14.20 || ^16.0.0", "@types/webpack-env": "^1.16.0", @@ -8516,9 +8554,9 @@ } }, "node_modules/@storybook/react/node_modules/@types/node": { - "version": "16.11.45", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.45.tgz", - "integrity": "sha512-3rKg/L5x0rofKuuUt5zlXzOnKyIHXmIu5R8A0TuNDMF2062/AOIDBciFIjToLEJ/9F9DzkHNot+BpNsMI1OLdQ==", + "version": "16.11.47", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.47.tgz", + "integrity": "sha512-fpP+jk2zJ4VW66+wAMFoBJlx1bxmBKx4DUFf68UHgdGCOuyUTDlLWqsaNPJh7xhNDykyJ9eIzAygilP/4WoN8g==", "dev": true }, "node_modules/@storybook/react/node_modules/is-plain-object": { @@ -8552,12 +8590,12 @@ "dev": true }, "node_modules/@storybook/router": { - "version": "6.5.9", - "resolved": "https://registry.npmjs.org/@storybook/router/-/router-6.5.9.tgz", - "integrity": "sha512-G2Xp/2r8vU2O34eelE+G5VbEEVFDeHcCURrVJEROh6dq2asFJAPbzslVXSeCqgOTNLSpRDJ2NcN5BckkNqmqJg==", + "version": "6.5.10", + "resolved": "https://registry.npmjs.org/@storybook/router/-/router-6.5.10.tgz", + "integrity": "sha512-O+vNW/eEpYFF8eCg5jZjNQ6q2DKQVxqDRPCy9pJdEbvavMDZn6AFYgVK+VJe5F4211WW2yncOu922xObCxXJYg==", "dev": true, "dependencies": { - "@storybook/client-logger": "6.5.9", + "@storybook/client-logger": "6.5.10", "core-js": "^3.8.2", "memoizerific": "^1.11.3", "qs": "^6.10.0", @@ -8641,13 +8679,13 @@ } }, "node_modules/@storybook/source-loader": { - "version": "6.5.9", - "resolved": "https://registry.npmjs.org/@storybook/source-loader/-/source-loader-6.5.9.tgz", - "integrity": "sha512-H03nFKaP6borfWMTTa9igBA+Jm2ph+FoVJImWC/X+LAmLSJYYSXuqSgmiZ/DZvbjxS4k8vccE2HXogne1IvaRA==", + "version": "6.5.10", + "resolved": "https://registry.npmjs.org/@storybook/source-loader/-/source-loader-6.5.10.tgz", + "integrity": "sha512-1RxxRumpjs8VUUwES9LId+cuNQnixhZAcwCxd6jaKkTZbjiQCtAhXX6DBTjJGV1u/JnCsqEp5b1wB8j/EioNHw==", "dev": true, "dependencies": { - "@storybook/addons": "6.5.9", - "@storybook/client-logger": "6.5.9", + "@storybook/addons": "6.5.10", + "@storybook/client-logger": "6.5.10", "@storybook/csf": "0.0.2--canary.4566f4d.1", "core-js": "^3.8.2", "estraverse": "^5.2.0", @@ -8679,14 +8717,14 @@ } }, "node_modules/@storybook/store": { - "version": "6.5.9", - "resolved": "https://registry.npmjs.org/@storybook/store/-/store-6.5.9.tgz", - "integrity": "sha512-80pcDTcCwK6wUA63aWOp13urI77jfipIVee9mpVvbNyfrNN8kGv1BS0z/JHDxuV6rC4g7LG1fb+BurR0yki7BA==", + "version": "6.5.10", + "resolved": "https://registry.npmjs.org/@storybook/store/-/store-6.5.10.tgz", + "integrity": "sha512-RswrSYh2IiKkytFPxP9AvP+hekjrvHK2ILvyDk2ZgduCN4n5ivsekOb+N3M2t+dq1eLuW9or5n2T4OWwAwjxxQ==", "dev": true, "dependencies": { - "@storybook/addons": "6.5.9", - "@storybook/client-logger": "6.5.9", - "@storybook/core-events": "6.5.9", + "@storybook/addons": "6.5.10", + "@storybook/client-logger": "6.5.10", + "@storybook/core-events": "6.5.10", "@storybook/csf": "0.0.2--canary.4566f4d.1", "core-js": "^3.8.2", "fast-deep-equal": "^3.1.3", @@ -8709,23 +8747,14 @@ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, - "node_modules/@storybook/store/node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/@storybook/telemetry": { - "version": "6.5.9", - "resolved": "https://registry.npmjs.org/@storybook/telemetry/-/telemetry-6.5.9.tgz", - "integrity": "sha512-JluoHCRhHAr4X0eUNVBSBi1JIBA92404Tu1TPdbN7x6gCZxHXXPTSUTAnspXp/21cTdMhY2x+kfZQ8fmlGK4MQ==", + "version": "6.5.10", + "resolved": "https://registry.npmjs.org/@storybook/telemetry/-/telemetry-6.5.10.tgz", + "integrity": "sha512-+M5HILDFS8nDumLxeSeAwi1MTzIuV6UWzV4yB2wcsEXOBTdplcl9oYqFKtlst78oOIdGtpPYxYfivDlqxC2K4g==", "dev": true, "dependencies": { - "@storybook/client-logger": "6.5.9", - "@storybook/core-common": "6.5.9", + "@storybook/client-logger": "6.5.10", + "@storybook/core-common": "6.5.10", "chalk": "^4.1.0", "core-js": "^3.8.2", "detect-package-manager": "^2.0.1", @@ -8826,12 +8855,12 @@ } }, "node_modules/@storybook/theming": { - "version": "6.5.9", - "resolved": "https://registry.npmjs.org/@storybook/theming/-/theming-6.5.9.tgz", - "integrity": "sha512-KM0AMP5jMQPAdaO8tlbFCYqx9uYM/hZXGSVUhznhLYu7bhNAIK7ZVmXxyE/z/khM++8eUHzRoZGiO/cwCkg9Xw==", + "version": "6.5.10", + "resolved": "https://registry.npmjs.org/@storybook/theming/-/theming-6.5.10.tgz", + "integrity": "sha512-BvTQBBcSEwKKcsVmF+Ol6v0RIQUr+bxP7gb10wtfBd23mZTEFA0C1N5FnZr/dDeiBKG1pvf1UKvoYA731y0BsA==", "dev": true, "dependencies": { - "@storybook/client-logger": "6.5.9", + "@storybook/client-logger": "6.5.10", "core-js": "^3.8.2", "memoizerific": "^1.11.3", "regenerator-runtime": "^0.13.7" @@ -8846,20 +8875,20 @@ } }, "node_modules/@storybook/ui": { - "version": "6.5.9", - "resolved": "https://registry.npmjs.org/@storybook/ui/-/ui-6.5.9.tgz", - "integrity": "sha512-ryuPxJgtbb0gPXKGgGAUC+Z185xGAd1IvQ0jM5fJ0SisHXI8jteG3RaWhntOehi9qCg+64Vv6eH/cj9QYNHt1Q==", - "dev": true, - "dependencies": { - "@storybook/addons": "6.5.9", - "@storybook/api": "6.5.9", - "@storybook/channels": "6.5.9", - "@storybook/client-logger": "6.5.9", - "@storybook/components": "6.5.9", - "@storybook/core-events": "6.5.9", - "@storybook/router": "6.5.9", + "version": "6.5.10", + "resolved": "https://registry.npmjs.org/@storybook/ui/-/ui-6.5.10.tgz", + "integrity": "sha512-6iaoaRAiTqB1inTw35vao+5hjcDE0Qa0A3a9ZIeNa6yHvpB1k0lO/N/0PMrRdVvySYpXVD1iry4z4QYdo1rU+w==", + "dev": true, + "dependencies": { + "@storybook/addons": "6.5.10", + "@storybook/api": "6.5.10", + "@storybook/channels": "6.5.10", + "@storybook/client-logger": "6.5.10", + "@storybook/components": "6.5.10", + "@storybook/core-events": "6.5.10", + "@storybook/router": "6.5.10", "@storybook/semver": "^7.3.2", - "@storybook/theming": "6.5.9", + "@storybook/theming": "6.5.10", "core-js": "^3.8.2", "memoizerific": "^1.11.3", "qs": "^6.10.0", @@ -8893,9 +8922,9 @@ } }, "node_modules/@testing-library/dom": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-8.16.0.tgz", - "integrity": "sha512-uxF4zmnLHHDlmW4l+0WDjcgLVwCvH+OVLpD8Dfp+Bjfz85prwxWGbwXgJdLtkgjD0qfOzkJF9SmA6YZPsMYX4w==", + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-8.17.1.tgz", + "integrity": "sha512-KnH2MnJUzmFNPW6RIKfd+zf2Wue8mEKX0M3cpX6aKl5ZXrJM1/c/Pc8c2xDNYQCnJO48Sm5ITbMXgqTr3h4jxQ==", "dev": true, "dependencies": { "@babel/code-frame": "^7.10.4", @@ -9203,9 +9232,9 @@ } }, "node_modules/@types/express-serve-static-core": { - "version": "4.17.29", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.29.tgz", - "integrity": "sha512-uMd++6dMKS32EOuw1Uli3e3BPgdLIXmezcfHv7N4c1s3gkhikBplORPpMq3fuWkxncZN1reb16d5n8yhQ80x7Q==", + "version": "4.17.30", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.30.tgz", + "integrity": "sha512-gstzbTWro2/nFed1WXtf+TtrpwxH7Ggs4RLYTLbeVgIkUQOI3WG/JKjgeOU1zXDvezllupjrf8OPIdvTbIaVOQ==", "dev": true, "dependencies": { "@types/node": "*", @@ -9326,9 +9355,9 @@ } }, "node_modules/@types/mime": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", - "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz", + "integrity": "sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==", "dev": true }, "node_modules/@types/minimatch": { @@ -9338,9 +9367,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "18.0.6", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.0.6.tgz", - "integrity": "sha512-/xUq6H2aQm261exT6iZTMifUySEt4GR5KX8eYyY+C4MSNPqSh9oNIP7tz2GLKTlFaiBbgZNxffoR3CVRG+cljw==", + "version": "18.7.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.7.1.tgz", + "integrity": "sha512-GKX1Qnqxo4S+Z/+Z8KKPLpH282LD7jLHWJcVryOflnsnH+BtSDfieR6ObwBMwpnNws0bUK8GI7z0unQf9bARNQ==", "dev": true }, "node_modules/@types/node-fetch": { @@ -9401,9 +9430,9 @@ "dev": true }, "node_modules/@types/react": { - "version": "18.0.15", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.15.tgz", - "integrity": "sha512-iz3BtLuIYH1uWdsv6wXYdhozhqj20oD4/Hk2DNXIn1kFsmp9x8d9QB6FnPhfkbhd2PgEONt9Q1x/ebkwjfFLow==", + "version": "18.0.17", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.17.tgz", + "integrity": "sha512-38ETy4tL+rn4uQQi7mB81G7V1g0u2ryquNmsVIOKUAEIDK+3CUjZ6rSRpdvS99dNBnkLFL83qfmtLacGOTIhwQ==", "dev": true, "dependencies": { "@types/prop-types": "*", @@ -9420,15 +9449,6 @@ "@types/react": "*" } }, - "node_modules/@types/react-syntax-highlighter": { - "version": "11.0.5", - "resolved": "https://registry.npmjs.org/@types/react-syntax-highlighter/-/react-syntax-highlighter-11.0.5.tgz", - "integrity": "sha512-VIOi9i2Oj5XsmWWoB72p3KlZoEbdRAcechJa8Ztebw7bDl2YmR+odxIqhtJGp1q2EozHs02US+gzxJ9nuf56qg==", - "dev": true, - "dependencies": { - "@types/react": "*" - } - }, "node_modules/@types/retry": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", @@ -9451,12 +9471,12 @@ } }, "node_modules/@types/serve-static": { - "version": "1.13.10", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.10.tgz", - "integrity": "sha512-nCkHGI4w7ZgAdNkrEu0bv+4xNV/XDqW+DydknebMOQwkpDGx8G+HTlj7R7ABI8i8nKxVw0wtKPi1D+lPOkh4YQ==", + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-z5xyF6uh8CbjAu9760KDKsH2FcDxZ2tFCsA4HIMWE6IkiYMXfVoa+4f9KX+FN0ZLsaMw1WNG2ETLA6N+/YA+cg==", "dev": true, "dependencies": { - "@types/mime": "^1", + "@types/mime": "*", "@types/node": "*" } }, @@ -9616,14 +9636,14 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "5.30.7", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.30.7.tgz", - "integrity": "sha512-l4L6Do+tfeM2OK0GJsU7TUcM/1oN/N25xHm3Jb4z3OiDU4Lj8dIuxX9LpVMS9riSXQs42D1ieX7b85/r16H9Fw==", + "version": "5.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.33.0.tgz", + "integrity": "sha512-jHvZNSW2WZ31OPJ3enhLrEKvAZNyAFWZ6rx9tUwaessTc4sx9KmgMNhVcqVAl1ETnT5rU5fpXTLmY9YvC1DCNg==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "5.30.7", - "@typescript-eslint/type-utils": "5.30.7", - "@typescript-eslint/utils": "5.30.7", + "@typescript-eslint/scope-manager": "5.33.0", + "@typescript-eslint/type-utils": "5.33.0", + "@typescript-eslint/utils": "5.33.0", "debug": "^4.3.4", "functional-red-black-tree": "^1.0.1", "ignore": "^5.2.0", @@ -9664,12 +9684,12 @@ } }, "node_modules/@typescript-eslint/experimental-utils": { - "version": "5.30.7", - "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-5.30.7.tgz", - "integrity": "sha512-r218ZVL0zFBYzEq8/9K2ZhRgsmKUhm8xd3sWChgvTbmP98kHGuY83IUl64SS9fx9OSBM9vMLdzBfox4eDdm/ZQ==", + "version": "5.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-5.33.0.tgz", + "integrity": "sha512-NvRsNe+T90QrSVlgdV9/U8/chfqGmShvKUE7hWZTAUUCF6hZty/R+eMPVGldKcUDq7uRQaK6+V8gv5OwVDqC+g==", "dev": true, "dependencies": { - "@typescript-eslint/utils": "5.30.7" + "@typescript-eslint/utils": "5.33.0" }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -9683,14 +9703,14 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "5.30.7", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.30.7.tgz", - "integrity": "sha512-Rg5xwznHWWSy7v2o0cdho6n+xLhK2gntImp0rJroVVFkcYFYQ8C8UJTSuTw/3CnExBmPjycjmUJkxVmjXsld6A==", + "version": "5.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.33.0.tgz", + "integrity": "sha512-cgM5cJrWmrDV2KpvlcSkelTBASAs1mgqq+IUGKJvFxWrapHpaRy5EXPQz9YaKF3nZ8KY18ILTiVpUtbIac86/w==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "5.30.7", - "@typescript-eslint/types": "5.30.7", - "@typescript-eslint/typescript-estree": "5.30.7", + "@typescript-eslint/scope-manager": "5.33.0", + "@typescript-eslint/types": "5.33.0", + "@typescript-eslint/typescript-estree": "5.33.0", "debug": "^4.3.4" }, "engines": { @@ -9710,13 +9730,13 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "5.30.7", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.30.7.tgz", - "integrity": "sha512-7BM1bwvdF1UUvt+b9smhqdc/eniOnCKxQT/kj3oXtj3LqnTWCAM0qHRHfyzCzhEfWX0zrW7KqXXeE4DlchZBKw==", + "version": "5.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.33.0.tgz", + "integrity": "sha512-/Jta8yMNpXYpRDl8EwF/M8It2A9sFJTubDo0ATZefGXmOqlaBffEw0ZbkbQ7TNDK6q55NPHFshGBPAZvZkE8Pw==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.30.7", - "@typescript-eslint/visitor-keys": "5.30.7" + "@typescript-eslint/types": "5.33.0", + "@typescript-eslint/visitor-keys": "5.33.0" }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -9727,12 +9747,12 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "5.30.7", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.30.7.tgz", - "integrity": "sha512-nD5qAE2aJX/YLyKMvOU5jvJyku4QN5XBVsoTynFrjQZaDgDV6i7QHFiYCx10wvn7hFvfuqIRNBtsgaLe0DbWhw==", + "version": "5.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.33.0.tgz", + "integrity": "sha512-2zB8uEn7hEH2pBeyk3NpzX1p3lF9dKrEbnXq1F7YkpZ6hlyqb2yZujqgRGqXgRBTHWIUG3NGx/WeZk224UKlIA==", "dev": true, "dependencies": { - "@typescript-eslint/utils": "5.30.7", + "@typescript-eslint/utils": "5.33.0", "debug": "^4.3.4", "tsutils": "^3.21.0" }, @@ -9753,9 +9773,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "5.30.7", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.30.7.tgz", - "integrity": "sha512-ocVkETUs82+U+HowkovV6uxf1AnVRKCmDRNUBUUo46/5SQv1owC/EBFkiu4MOHeZqhKz2ktZ3kvJJ1uFqQ8QPg==", + "version": "5.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.33.0.tgz", + "integrity": "sha512-nIMt96JngB4MYFYXpZ/3ZNU4GWPNdBbcB5w2rDOCpXOVUkhtNlG2mmm8uXhubhidRZdwMaMBap7Uk8SZMU/ppw==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -9766,13 +9786,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "5.30.7", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.30.7.tgz", - "integrity": "sha512-tNslqXI1ZdmXXrHER83TJ8OTYl4epUzJC0aj2i4DMDT4iU+UqLT3EJeGQvJ17BMbm31x5scSwo3hPM0nqQ1AEA==", + "version": "5.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.33.0.tgz", + "integrity": "sha512-tqq3MRLlggkJKJUrzM6wltk8NckKyyorCSGMq4eVkyL5sDYzJJcMgZATqmF8fLdsWrW7OjjIZ1m9v81vKcaqwQ==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.30.7", - "@typescript-eslint/visitor-keys": "5.30.7", + "@typescript-eslint/types": "5.33.0", + "@typescript-eslint/visitor-keys": "5.33.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -9808,15 +9828,15 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "5.30.7", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.30.7.tgz", - "integrity": "sha512-Z3pHdbFw+ftZiGUnm1GZhkJgVqsDL5CYW2yj+TB2mfXDFOMqtbzQi2dNJIyPqPbx9mv2kUxS1gU+r2gKlKi1rQ==", + "version": "5.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.33.0.tgz", + "integrity": "sha512-JxOAnXt9oZjXLIiXb5ZIcZXiwVHCkqZgof0O8KPgz7C7y0HS42gi75PdPlqh1Tf109M0fyUw45Ao6JLo7S5AHw==", "dev": true, "dependencies": { "@types/json-schema": "^7.0.9", - "@typescript-eslint/scope-manager": "5.30.7", - "@typescript-eslint/types": "5.30.7", - "@typescript-eslint/typescript-estree": "5.30.7", + "@typescript-eslint/scope-manager": "5.33.0", + "@typescript-eslint/types": "5.33.0", + "@typescript-eslint/typescript-estree": "5.33.0", "eslint-scope": "^5.1.1", "eslint-utils": "^3.0.0" }, @@ -9832,12 +9852,12 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "5.30.7", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.30.7.tgz", - "integrity": "sha512-KrRXf8nnjvcpxDFOKej4xkD7657+PClJs5cJVSG7NNoCNnjEdc46juNAQt7AyuWctuCgs6mVRc1xGctEqrjxWw==", + "version": "5.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.33.0.tgz", + "integrity": "sha512-/XsqCzD4t+Y9p5wd9HZiptuGKBlaZO5showwqODii5C0nZawxWLF+Q6k5wYHBrQv96h6GYKyqqMHCSTqta8Kiw==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.30.7", + "@typescript-eslint/types": "5.33.0", "eslint-visitor-keys": "^3.3.0" }, "engines": { @@ -10336,9 +10356,9 @@ } }, "node_modules/ansi-colors": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.4.tgz", - "integrity": "sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", "dev": true, "engines": { "node": ">=6" @@ -11083,13 +11103,17 @@ } }, "node_modules/babel-plugin-macros": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-2.8.0.tgz", - "integrity": "sha512-SEP5kJpfGYqYKpBrj5XU3ahw5p5GOHJ0U5ssOSQ/WBVdwkD2Dzlce95exQTs3jOVWPPKLBN2rlEWkCK7dSmLvg==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", + "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", "dependencies": { - "@babel/runtime": "^7.7.2", - "cosmiconfig": "^6.0.0", - "resolve": "^1.12.0" + "@babel/runtime": "^7.12.5", + "cosmiconfig": "^7.0.0", + "resolve": "^1.19.0" + }, + "engines": { + "node": ">=10", + "npm": ">=6" } }, "node_modules/babel-plugin-named-exports-order": { @@ -11099,13 +11123,13 @@ "dev": true }, "node_modules/babel-plugin-polyfill-corejs2": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.3.1.tgz", - "integrity": "sha512-v7/T6EQcNfVLfcN2X8Lulb7DjprieyLWJK/zOWH5DUYcAgex9sP3h25Q+DLsX9TloXe3y1O8l2q2Jv9q8UVB9w==", + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.3.2.tgz", + "integrity": "sha512-LPnodUl3lS0/4wN3Rb+m+UK8s7lj2jcLRrjho4gLw+OJs+I4bvGXshINesY5xx/apM+biTnQ9reDI8yj+0M5+Q==", "dev": true, "dependencies": { - "@babel/compat-data": "^7.13.11", - "@babel/helper-define-polyfill-provider": "^0.3.1", + "@babel/compat-data": "^7.17.7", + "@babel/helper-define-polyfill-provider": "^0.3.2", "semver": "^6.1.1" }, "peerDependencies": { @@ -11113,12 +11137,12 @@ } }, "node_modules/babel-plugin-polyfill-corejs3": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.5.2.tgz", - "integrity": "sha512-G3uJih0XWiID451fpeFaYGVuxHEjzKTHtc9uGFEjR6hHrvNzeS/PX+LLLcetJcytsB5m4j+K3o/EpXJNb/5IEQ==", + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.5.3.tgz", + "integrity": "sha512-zKsXDh0XjnrUEW0mxIHLfjBfnXSMr5Q/goMe/fxpQnLm07mcOZiIZHBNWCMx60HmdvjxfXcalac0tfFg0wqxyw==", "dev": true, "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.3.1", + "@babel/helper-define-polyfill-provider": "^0.3.2", "core-js-compat": "^3.21.0" }, "peerDependencies": { @@ -11126,12 +11150,12 @@ } }, "node_modules/babel-plugin-polyfill-regenerator": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.3.1.tgz", - "integrity": "sha512-Y2B06tvgHYt1x0yz17jGkGeeMr5FeKUu+ASJ+N6nB5lQ8Dapfg42i0OVrf8PNGJ3zKL4A23snMi1IRwrqqND7A==", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.4.0.tgz", + "integrity": "sha512-RW1cnryiADFeHmfLS+WW/G431p1PsW5qdRdz0SDRi7TKcUgc7Oh/uXkT7MZ/+tGsT1BkczEAmD5XjUyJ5SWDTw==", "dev": true, "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.3.1" + "@babel/helper-define-polyfill-provider": "^0.3.2" }, "peerDependencies": { "@babel/core": "^7.0.0-0" @@ -11678,9 +11702,9 @@ } }, "node_modules/browserslist": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.2.tgz", - "integrity": "sha512-MonuOgAtUB46uP5CezYbRaYKBNt2LxP0yX+Pmj4LkcDFGkn9Cbpi83d9sCjwQDErXsIJSzY5oKGDbgOlF/LPAA==", + "version": "4.21.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.3.tgz", + "integrity": "sha512-898rgRXLAyRkM1GryrrBHGkqA5hlpkV5MhtZwg9QXeiyLUYs2k00Un05aX5l2/yJIOObYKOpS2JNo8nJDE7fWQ==", "funding": [ { "type": "opencollective", @@ -11692,10 +11716,10 @@ } ], "dependencies": { - "caniuse-lite": "^1.0.30001366", - "electron-to-chromium": "^1.4.188", + "caniuse-lite": "^1.0.30001370", + "electron-to-chromium": "^1.4.202", "node-releases": "^2.0.6", - "update-browserslist-db": "^1.0.4" + "update-browserslist-db": "^1.0.5" }, "bin": { "browserslist": "cli.js" @@ -11774,9 +11798,9 @@ } }, "node_modules/c8": { - "version": "7.11.3", - "resolved": "https://registry.npmjs.org/c8/-/c8-7.11.3.tgz", - "integrity": "sha512-6YBmsaNmqRm9OS3ZbIiL2EZgi1+Xc4O24jL3vMYGE6idixYuGdy76rIfIdltSKDj9DpLNrcXSonUTR1miBD0wA==", + "version": "7.12.0", + "resolved": "https://registry.npmjs.org/c8/-/c8-7.12.0.tgz", + "integrity": "sha512-CtgQrHOkyxr5koX1wEUmN/5cfDa2ckbHRA4Gy5LAL0zaCFtVWJS5++n+w4/sr2GWGerBxgTjpKeDclk/Qk6W/A==", "dev": true, "dependencies": { "@bcoe/v8-coverage": "^0.2.3", @@ -12000,9 +12024,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001367", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001367.tgz", - "integrity": "sha512-XDgbeOHfifWV3GEES2B8rtsrADx4Jf+juKX2SICJcaUhjYBO3bR96kvEIHa15VU6ohtOhBZuPGGYGbXMRn0NCw==", + "version": "1.0.30001375", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001375.tgz", + "integrity": "sha512-kWIMkNzLYxSvnjy0hL8w1NOaWNr2rn39RTAVyIwcw8juu60bZDWiF1/loOYANzjtJmy6qPgNmn38ro5Pygagdw==", "funding": [ { "type": "opencollective", @@ -12144,6 +12168,18 @@ "fsevents": "~2.3.2" } }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/chownr": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", @@ -12163,9 +12199,9 @@ } }, "node_modules/ci-info": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", - "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.3.2.tgz", + "integrity": "sha512-xmDt/QIAdeZ9+nfdPsaBCpMvHNLFiLdjj59qjqn+6iPe6YmHGQ35sBnQ8uslRBXFmXkiZQOJRjvQeoGppoTjjg==", "dev": true }, "node_modules/cipher-base": { @@ -12352,9 +12388,9 @@ } }, "node_modules/cli-spinners": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.6.1.tgz", - "integrity": "sha512-x/5fWmGMnbKQAaNwN+UZlV79qBLM9JFnJuJ03gIi5whrob0xV0ofNVHy9DhwGdsMJQc2OKv0oGmLzvaqvAVv+g==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.7.0.tgz", + "integrity": "sha512-qu3pN8Y3qHNgE2AFweciB1IfMnmZ/fsNTEE+NOFjmGB2F/7rLhnhzppvpCnN4FovtP26k8lHyy9ptEbNwWFLzw==", "dev": true, "engines": { "node": ">=6" @@ -12519,9 +12555,9 @@ } }, "node_modules/commander": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", - "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", + "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", "dev": true, "engines": { "node": ">= 6" @@ -12782,9 +12818,9 @@ } }, "node_modules/core-js": { - "version": "3.23.5", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.23.5.tgz", - "integrity": "sha512-7Vh11tujtAZy82da4duVreQysIoO2EvVrur7y6IzZkH1IHPSekuDi8Vuw1+YKjkbfWLRD7Nc9ICQ/sIUDutcyg==", + "version": "3.24.1", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.24.1.tgz", + "integrity": "sha512-0QTBSYSUZ6Gq21utGzkfITDylE8jWC9Ne1D2MrhvlsZBI1x39OdDIVbzSqtgMndIy6BlHxBXpMGqzZmnztg2rg==", "dev": true, "hasInstallScript": true, "funding": { @@ -12793,12 +12829,12 @@ } }, "node_modules/core-js-compat": { - "version": "3.23.5", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.23.5.tgz", - "integrity": "sha512-fHYozIFIxd+91IIbXJgWd/igXIc8Mf9is0fusswjnGIWVG96y2cwyUdlCkGOw6rMLHKAxg7xtCIVaHsyOUnJIg==", + "version": "3.24.1", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.24.1.tgz", + "integrity": "sha512-XhdNAGeRnTpp8xbD+sR/HFDK9CbeeeqXT6TuofXh3urqEevzkWmLRgrVoykodsw8okqo2pu1BOmuCKrHx63zdw==", "dev": true, "dependencies": { - "browserslist": "^4.21.2", + "browserslist": "^4.21.3", "semver": "7.0.0" }, "funding": { @@ -12816,9 +12852,9 @@ } }, "node_modules/core-js-pure": { - "version": "3.23.5", - "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.23.5.tgz", - "integrity": "sha512-8t78LdpKSuCq4pJYCYk8hl7XEkAX+BP16yRIwL3AanTksxuEf7CM83vRyctmiEL8NDZ3jpUcv56fk9/zG3aIuw==", + "version": "3.24.1", + "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.24.1.tgz", + "integrity": "sha512-r1nJk41QLLPyozHUUPmILCEMtMw24NG4oWK6RbsDdjzQgg9ZvrUsPBj1MnG0wXXp1DCDU6j+wUvEmBSrtRbLXg==", "dev": true, "hasInstallScript": true, "funding": { @@ -12827,24 +12863,24 @@ } }, "node_modules/core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", "dev": true }, "node_modules/cosmiconfig": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz", - "integrity": "sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.1.tgz", + "integrity": "sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ==", "dependencies": { "@types/parse-json": "^4.0.0", - "import-fresh": "^3.1.0", + "import-fresh": "^3.2.1", "parse-json": "^5.0.0", "path-type": "^4.0.0", - "yaml": "^1.7.2" + "yaml": "^1.10.0" }, "engines": { - "node": ">=8" + "node": ">=10" } }, "node_modules/cp-file": { @@ -13144,6 +13180,15 @@ "node": ">=4" } }, + "node_modules/cpy/node_modules/slash": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", + "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/cpy/node_modules/to-regex-range": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", @@ -13481,12 +13526,6 @@ "node": ">=8" } }, - "node_modules/cypress/node_modules/ci-info": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.3.2.tgz", - "integrity": "sha512-xmDt/QIAdeZ9+nfdPsaBCpMvHNLFiLdjj59qjqn+6iPe6YmHGQ35sBnQ8uslRBXFmXkiZQOJRjvQeoGppoTjjg==", - "dev": true - }, "node_modules/cypress/node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -13514,44 +13553,6 @@ "node": ">= 6" } }, - "node_modules/cypress/node_modules/execa": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz", - "integrity": "sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==", - "dev": true, - "dependencies": { - "cross-spawn": "^7.0.0", - "get-stream": "^5.0.0", - "human-signals": "^1.1.1", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.0", - "onetime": "^5.1.0", - "signal-exit": "^3.0.2", - "strip-final-newline": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/cypress/node_modules/get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", - "dev": true, - "dependencies": { - "pump": "^3.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/cypress/node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -13561,42 +13562,6 @@ "node": ">=8" } }, - "node_modules/cypress/node_modules/human-signals": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", - "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==", - "dev": true, - "engines": { - "node": ">=8.12.0" - } - }, - "node_modules/cypress/node_modules/is-ci": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz", - "integrity": "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==", - "dev": true, - "dependencies": { - "ci-info": "^3.2.0" - }, - "bin": { - "is-ci": "bin.js" - } - }, - "node_modules/cypress/node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/cypress/node_modules/semver": { "version": "7.3.7", "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", @@ -13627,27 +13592,6 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "node_modules/cypress/node_modules/tmp": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", - "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", - "dev": true, - "dependencies": { - "rimraf": "^3.0.0" - }, - "engines": { - "node": ">=8.17.0" - } - }, - "node_modules/cypress/node_modules/untildify": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", - "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", @@ -13746,6 +13690,19 @@ "node": ">=0.10.0" } }, + "node_modules/default-browser-id/node_modules/untildify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/untildify/-/untildify-2.1.0.tgz", + "integrity": "sha512-sJjbDp2GodvkB0FZZcn7k6afVisqX5BZD7Yq3xp4nN2O15BBK0cLm3Vwn2vQaF7UDS0UUsrQMkkplmDI5fskig==", + "dev": true, + "optional": true, + "dependencies": { + "os-homedir": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/default-gateway": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-6.0.3.tgz", @@ -13758,6 +13715,50 @@ "node": ">= 10" } }, + "node_modules/default-gateway/node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/default-gateway/node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-gateway/node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "engines": { + "node": ">=10.17.0" + } + }, "node_modules/defaults": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz", @@ -13942,6 +13943,50 @@ "node": ">=12" } }, + "node_modules/detect-package-manager/node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/detect-package-manager/node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/detect-package-manager/node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "engines": { + "node": ">=10.17.0" + } + }, "node_modules/detect-port": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/detect-port/-/detect-port-1.3.0.tgz", @@ -14208,9 +14253,9 @@ "dev": true }, "node_modules/electron-to-chromium": { - "version": "1.4.194", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.194.tgz", - "integrity": "sha512-ola5UH0xAP1oYY0FFUsPvwtucEzCQHucXnT7PQ1zjHJMccZhCDktEugI++JUR3YuIs7Ff7afz+OVEhVAIMhLAQ==" + "version": "1.4.215", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.215.tgz", + "integrity": "sha512-vqZxT8C5mlDZ//hQFhneHmOLnj1LhbzxV0+I1yqHV8SB1Oo4Y5Ne9+qQhwHl7O1s9s9cRuo2l5CoLEHdhMTwZg==" }, "node_modules/elliptic": { "version": "6.5.4", @@ -14310,15 +14355,6 @@ "node": ">=8.6" } }, - "node_modules/enquirer/node_modules/ansi-colors": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", - "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", - "dev": true, - "engines": { - "node": ">=6" - } - }, "node_modules/entities": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", @@ -14758,28 +14794,6 @@ "webpack": "^4.40.0 || ^5.0.0" } }, - "node_modules/esbuild-loader/node_modules/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, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/esbuild-loader/node_modules/webpack-sources": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-2.3.1.tgz", - "integrity": "sha512-y9EI9AO42JjEcrTJFOYmVywVZdKVUfOvDUPsJea5GIr1JOEGFVqwlY2K098fFoIjOkDzHn2AjRvM8dsBZu+gCA==", - "dev": true, - "dependencies": { - "source-list-map": "^2.0.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/esbuild-netbsd-64": { "version": "0.14.54", "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.54.tgz", @@ -14934,13 +14948,14 @@ } }, "node_modules/eslint": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.20.0.tgz", - "integrity": "sha512-d4ixhz5SKCa1D6SCPrivP7yYVi7nyD6A4vs6HIAul9ujBzcEmZVM3/0NN/yu5nKhmO1wjp5xQ46iRfmDGlOviA==", + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.21.0.tgz", + "integrity": "sha512-/XJ1+Qurf1T9G2M5IHrsjp+xrGT73RZf23xA1z5wB1ZzzEAWSZKvRwhWxTFp1rvkvCfwcvAUNAP31bhKTTGfDA==", "dev": true, "dependencies": { "@eslint/eslintrc": "^1.3.0", - "@humanwhocodes/config-array": "^0.9.2", + "@humanwhocodes/config-array": "^0.10.4", + "@humanwhocodes/gitignore-to-minimatch": "^1.0.2", "ajv": "^6.10.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", @@ -14950,14 +14965,17 @@ "eslint-scope": "^7.1.1", "eslint-utils": "^3.0.0", "eslint-visitor-keys": "^3.3.0", - "espree": "^9.3.2", + "espree": "^9.3.3", "esquery": "^1.4.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", "functional-red-black-tree": "^1.0.1", "glob-parent": "^6.0.1", "globals": "^13.15.0", + "globby": "^11.1.0", + "grapheme-splitter": "^1.0.4", "ignore": "^5.2.0", "import-fresh": "^3.0.0", "imurmurhash": "^0.1.4", @@ -15017,9 +15035,9 @@ } }, "node_modules/eslint-import-resolver-typescript": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.2.7.tgz", - "integrity": "sha512-WvcsRy3aPmwVsuS/XVliAJWpIdTlaFXXZPZk3TCbvvF8RtaAkjAhcLL5bl5VEoTmE+XnTHjIbWMzNZcOQpK/DA==", + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.4.0.tgz", + "integrity": "sha512-rBCgiEovwX/HQ8ESWV+XIWZaFiRtDeAXNZdcTATB8UbMuadc9qfGOlIP+vy+c7nsgfEBN4NTwy5qunGNptDP0Q==", "dev": true, "dependencies": { "debug": "^4.3.4", @@ -15216,21 +15234,21 @@ "dev": true }, "node_modules/eslint-plugin-jsx-a11y": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.6.0.tgz", - "integrity": "sha512-kTeLuIzpNhXL2CwLlc8AHI0aFRwWHcg483yepO9VQiHzM9bZwJdzTkzBszbuPrbgGmq2rlX/FaT2fJQsjUSHsw==", + "version": "6.6.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.6.1.tgz", + "integrity": "sha512-sXgFVNHiWffBq23uiS/JaP6eVR622DqwB4yTzKvGZGcPq6/yZ3WmOZfuBks/vHWo9GaFOqC2ZK4i6+C35knx7Q==", "dev": true, "dependencies": { - "@babel/runtime": "^7.18.3", + "@babel/runtime": "^7.18.9", "aria-query": "^4.2.2", "array-includes": "^3.1.5", "ast-types-flow": "^0.0.7", - "axe-core": "^4.4.2", + "axe-core": "^4.4.3", "axobject-query": "^2.2.0", "damerau-levenshtein": "^1.0.8", "emoji-regex": "^9.2.2", "has": "^1.0.3", - "jsx-ast-utils": "^3.3.1", + "jsx-ast-utils": "^3.3.2", "language-tags": "^1.0.5", "minimatch": "^3.1.2", "semver": "^6.3.0" @@ -15497,22 +15515,10 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, - "node_modules/eslint/node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/eslint/node_modules/globals": { - "version": "13.16.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.16.0.tgz", - "integrity": "sha512-A1lrQfpNF+McdPOnnFqY3kSN0AFTy485bTi1bkLk4mVPODIUEcSfhHgRqA+QdXPksrSTTztYXx37NFV+GpGk3Q==", + "version": "13.17.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.17.0.tgz", + "integrity": "sha512-1C+6nQRb1GwGMKm2dH/E7enFAMxGTmGI7/dEdhy/DNelv85w9B72t3uc5frtMNXIbzrarJJ/lTCjcaZwbLJmyw==", "dev": true, "dependencies": { "type-fest": "^0.20.2" @@ -15575,23 +15581,26 @@ } }, "node_modules/espree": { - "version": "9.3.2", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.3.2.tgz", - "integrity": "sha512-D211tC7ZwouTIuY5x9XnS0E9sWNChB7IYKX/Xp5eQj3nFXhqmiUDB9q27y76oFl8jTg3pXcQx/bpxMfs3CIZbA==", + "version": "9.3.3", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.3.3.tgz", + "integrity": "sha512-ORs1Rt/uQTqUKjDdGCyrtYxbazf5umATSf/K4qxjmZHORR6HJk+2s/2Pqe+Kk49HHINC/xNIrGfgh8sZcll0ng==", "dev": true, "dependencies": { - "acorn": "^8.7.1", + "acorn": "^8.8.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^3.3.0" }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, "node_modules/espree/node_modules/acorn": { - "version": "8.7.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.1.tgz", - "integrity": "sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A==", + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.0.tgz", + "integrity": "sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==", "dev": true, "bin": { "acorn": "bin/acorn" @@ -15716,19 +15725,19 @@ "dev": true }, "node_modules/execa": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", - "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz", + "integrity": "sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==", "dev": true, "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.0", - "human-signals": "^2.1.0", + "cross-spawn": "^7.0.0", + "get-stream": "^5.0.0", + "human-signals": "^1.1.1", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.1", - "onetime": "^5.1.2", - "signal-exit": "^3.0.3", + "npm-run-path": "^4.0.0", + "onetime": "^5.1.0", + "signal-exit": "^3.0.2", "strip-final-newline": "^2.0.0" }, "engines": { @@ -16027,6 +16036,18 @@ "node": ">=4" } }, + "node_modules/external-editor/node_modules/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "dependencies": { + "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" + } + }, "node_modules/extglob": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", @@ -16099,21 +16120,6 @@ "@types/yauzl": "^2.9.1" } }, - "node_modules/extract-zip/node_modules/get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", - "dev": true, - "dependencies": { - "pump": "^3.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/extsprintf": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", @@ -16151,6 +16157,18 @@ "node": ">=8.6.0" } }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fast-json-parse": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/fast-json-parse/-/fast-json-parse-1.0.3.tgz", @@ -16170,10 +16188,13 @@ "dev": true }, "node_modules/fastest-levenshtein": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.12.tgz", - "integrity": "sha512-On2N+BpYJ15xIC974QNVuYGMOlEVt4s0EOI3wwMqOmK1fdDY+FN/zltPV8vosq4ad4c/gJ1KHScUn/6AWIgiow==", - "dev": true + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", + "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", + "dev": true, + "engines": { + "node": ">= 4.9.1" + } }, "node_modules/fastq": { "version": "1.13.0", @@ -16184,19 +16205,6 @@ "reusify": "^1.0.4" } }, - "node_modules/fault": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/fault/-/fault-1.0.4.tgz", - "integrity": "sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==", - "dev": true, - "dependencies": { - "format": "^0.2.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, "node_modules/faye-websocket": { "version": "0.11.4", "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", @@ -16808,15 +16816,6 @@ "node": ">= 6" } }, - "node_modules/format": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz", - "integrity": "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==", - "dev": true, - "engines": { - "node": ">=0.4.x" - } - }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -17090,12 +17089,15 @@ } }, "node_modules/get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", "dev": true, + "dependencies": { + "pump": "^3.0.0" + }, "engines": { - "node": ">=10" + "node": ">=8" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -17179,15 +17181,15 @@ } }, "node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "dev": true, "dependencies": { - "is-glob": "^4.0.1" + "is-glob": "^4.0.3" }, "engines": { - "node": ">= 6" + "node": ">=10.13.0" } }, "node_modules/glob-promise": { @@ -17285,15 +17287,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/globby/node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/globrex": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", @@ -17306,6 +17299,12 @@ "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", "dev": true }, + "node_modules/grapheme-splitter": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", + "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", + "dev": true + }, "node_modules/graphql": { "version": "16.5.0", "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.5.0.tgz", @@ -17679,15 +17678,6 @@ "integrity": "sha512-lOhQU7iG3AMcjmb8NIWCa+KwfJw5bY44BoWPtrj5A4iDbSD3ylGf5QcYr0ZyQnhkKQ2GgWNLdF2rfrXtXlF3nQ==", "dev": true }, - "node_modules/highlight.js": { - "version": "10.7.3", - "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", - "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==", - "dev": true, - "engines": { - "node": "*" - } - }, "node_modules/history": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/history/-/history-5.3.0.tgz", @@ -17968,12 +17958,12 @@ "dev": true }, "node_modules/human-signals": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", - "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", + "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==", "dev": true, "engines": { - "node": ">=10.17.0" + "node": ">=8.12.0" } }, "node_modules/iconv-lite": { @@ -18470,21 +18460,21 @@ } }, "node_modules/is-ci": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz", - "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz", + "integrity": "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==", "dev": true, "dependencies": { - "ci-info": "^2.0.0" + "ci-info": "^3.2.0" }, "bin": { "is-ci": "bin.js" } }, "node_modules/is-core-module": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.9.0.tgz", - "integrity": "sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A==", + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.10.0.tgz", + "integrity": "sha512-Erxj2n/LDAZ7H8WNJXd9tw38GYM3dv8rk8Zcs+jJuxYTW7sozH+SS8NtrSjVL1/vpLvWi1hxy96IzjJ3EHTJJg==", "dependencies": { "has": "^1.0.3" }, @@ -18655,15 +18645,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-installed-globally/node_modules/is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/is-interactive": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", @@ -18754,7 +18735,7 @@ "node": ">=6" } }, - "node_modules/is-path-inside": { + "node_modules/is-path-in-cwd/node_modules/is-path-inside": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-2.1.0.tgz", "integrity": "sha512-wiyhTzfDWsvwAW53OBWF5zuvaOGlZ6PwYxAbPVDhpm+gM09xKQGjBq/8uYN12aDvMxnAnq3dxTyoSoRNmg5YFg==", @@ -18766,6 +18747,15 @@ "node": ">=6" } }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/is-plain-obj": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", @@ -19308,6 +19298,12 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/jest-util/node_modules/ci-info": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", + "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", + "dev": true + }, "node_modules/jest-util/node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -19335,6 +19331,18 @@ "node": ">=8" } }, + "node_modules/jest-util/node_modules/is-ci": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz", + "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==", + "dev": true, + "dependencies": { + "ci-info": "^2.0.0" + }, + "bin": { + "is-ci": "bin.js" + } + }, "node_modules/jest-util/node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -19523,13 +19531,13 @@ } }, "node_modules/jsx-ast-utils": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.2.tgz", - "integrity": "sha512-4ZCADZHRkno244xlNnn4AOG6sRQ7iBZ5BbgZ4vW4y5IZw7cVUD1PPeblm1xx/nfmMxPdt/LHsXZW8z/j58+l9Q==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.3.tgz", + "integrity": "sha512-fYQHZTZ8jSfmWZ0iyzfwiU4WDX4HpHbMCZ3gPlWYiCl3BoeOTsqKBqnTVfH2rYT7eP5c3sVbeSPHnnJOaTrWiw==", "dev": true, "dependencies": { "array-includes": "^3.1.5", - "object.assign": "^4.1.2" + "object.assign": "^4.1.3" }, "engines": { "node": ">=4.0" @@ -20005,20 +20013,6 @@ "tslib": "^2.0.3" } }, - "node_modules/lowlight": { - "version": "1.20.0", - "resolved": "https://registry.npmjs.org/lowlight/-/lowlight-1.20.0.tgz", - "integrity": "sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==", - "dev": true, - "dependencies": { - "fault": "^1.0.0", - "highlight.js": "~10.7.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, "node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -21328,14 +21322,14 @@ } }, "node_modules/object.assign": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz", - "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.3.tgz", + "integrity": "sha512-ZFJnX3zltyjcYJL0RoCJuzb+11zWGyaDbjgxZbdV7rFEcHQuYxrZqhow67aA7xpes6LhojyFDaBKAFfogQrikA==", "dev": true, "dependencies": { - "call-bind": "^1.0.0", - "define-properties": "^1.1.3", - "has-symbols": "^1.0.1", + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "has-symbols": "^1.0.3", "object-keys": "^1.1.1" }, "engines": { @@ -22239,22 +22233,6 @@ "webpack": "^4.0.0 || ^5.0.0" } }, - "node_modules/postcss-loader/node_modules/cosmiconfig": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.1.tgz", - "integrity": "sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ==", - "dev": true, - "dependencies": { - "@types/parse-json": "^4.0.0", - "import-fresh": "^3.2.1", - "parse-json": "^5.0.0", - "path-type": "^4.0.0", - "yaml": "^1.10.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/postcss-loader/node_modules/semver": { "version": "7.3.7", "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", @@ -22447,15 +22425,6 @@ "node": ">= 0.8" } }, - "node_modules/prismjs": { - "version": "1.28.0", - "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.28.0.tgz", - "integrity": "sha512-8aaXdYvl1F7iC7Xm1spqSaY/OJBpYW3v+KJ+F17iYxvdc8sfjW194COK5wVhMZX45tGteiBQgdvD/nhxcRwylw==", - "dev": true, - "engines": { - "node": ">=6" - } - }, "node_modules/process": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", @@ -22913,22 +22882,6 @@ "react-dom": ">=16.8" } }, - "node_modules/react-syntax-highlighter": { - "version": "15.5.0", - "resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-15.5.0.tgz", - "integrity": "sha512-+zq2myprEnQmH5yw6Gqc8lD55QHnpKaU8TOcFeC/Lg/MQSs8UknEA0JC4nTZGFAXC2J2Hyj/ijJ7NlabyPi2gg==", - "dev": true, - "dependencies": { - "@babel/runtime": "^7.3.1", - "highlight.js": "^10.4.1", - "lowlight": "^1.17.0", - "prismjs": "^1.27.0", - "refractor": "^3.6.0" - }, - "peerDependencies": { - "react": ">= 0.14.0" - } - }, "node_modules/read-pkg": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", @@ -23112,30 +23065,6 @@ "node": ">=0.10.0" } }, - "node_modules/refractor": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/refractor/-/refractor-3.6.0.tgz", - "integrity": "sha512-MY9W41IOWxxk31o+YvFCNyNzdkc9M20NoZK5vq6jkv4I/uh2zkWcfudj0Q1fovjUQJrNewS9NMzeTtqPf+n5EA==", - "dev": true, - "dependencies": { - "hastscript": "^6.0.0", - "parse-entities": "^2.0.0", - "prismjs": "~1.27.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/refractor/node_modules/prismjs": { - "version": "1.27.0", - "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.27.0.tgz", - "integrity": "sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA==", - "dev": true, - "engines": { - "node": ">=6" - } - }, "node_modules/regenerate": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", @@ -24267,9 +24196,9 @@ "dev": true }, "node_modules/set-cookie-parser": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.5.0.tgz", - "integrity": "sha512-cHMAtSXilfyBePduZEBVPTCftTQWz6ehWJD5YNUg4mqvRosrrjKbo4WS8JkB0/RxonMoohHm7cOGH60mDkRQ9w==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.5.1.tgz", + "integrity": "sha512-1jeBGaKNGdEq4FgIrORu/N570dwoPYio8lSoYLWmX7sQ//0JY08Xh9o5pBcgmHQ/MbsYp/aZnOe1s1lIsbLprQ==", "dev": true }, "node_modules/set-value": { @@ -24393,12 +24322,12 @@ "dev": true }, "node_modules/slash": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", - "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "dev": true, "engines": { - "node": ">=6" + "node": ">=8" } }, "node_modules/slice-ansi": { @@ -25474,9 +25403,9 @@ "dev": true }, "node_modules/synckit": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.1.tgz", - "integrity": "sha512-rJEeygO5PNmcZICmrgnbOd2usi5zWE1ESc0Gn5tTmJlongoU8zCTwMFQtar2UgMSiR68vK9afPQ+uVs2lURSIA==", + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.3.tgz", + "integrity": "sha512-1goXnDYNJlKwCM37f5MTzRwo+8SqutgVtg2d37D6YnHHT4E3IhQMRfKiGdfTZU7LBlI6T8inCQUxnMBFHrbqWw==", "dev": true, "dependencies": { "@pkgr/utils": "^2.3.0", @@ -25690,10 +25619,20 @@ "node": ">=0.10.0" } }, + "node_modules/terser-webpack-plugin/node_modules/webpack-sources": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz", + "integrity": "sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==", + "dev": true, + "dependencies": { + "source-list-map": "^2.0.0", + "source-map": "~0.6.1" + } + }, "node_modules/terser/node_modules/acorn": { - "version": "8.7.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.1.tgz", - "integrity": "sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A==", + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.0.tgz", + "integrity": "sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==", "dev": true, "bin": { "acorn": "bin/acorn" @@ -25809,15 +25748,30 @@ } }, "node_modules/tmp": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", + "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", "dev": true, "dependencies": { - "os-tmpdir": "~1.0.2" + "rimraf": "^3.0.0" }, "engines": { - "node": ">=0.6.0" + "node": ">=8.17.0" + } + }, + "node_modules/tmp/node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/tmpl": { @@ -26125,9 +26079,9 @@ } }, "node_modules/uglify-js": { - "version": "3.16.2", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.16.2.tgz", - "integrity": "sha512-AaQNokTNgExWrkEYA24BTNMSjyqEXPSfhqoS0AxmHkCJ4U+Dyy5AvbGV/sqxuxficEfGGoX3zWw9R7QpLFfEsg==", + "version": "3.16.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.16.3.tgz", + "integrity": "sha512-uVbFqx9vvLhQg0iBaau9Z75AxWJ8tqM9AV890dIZCLApF4rTcyHwmAvLeEdYRs+BzYWu8Iw81F79ah0EfTXbaw==", "dev": true, "optional": true, "bin": { @@ -26462,16 +26416,12 @@ "dev": true }, "node_modules/untildify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/untildify/-/untildify-2.1.0.tgz", - "integrity": "sha512-sJjbDp2GodvkB0FZZcn7k6afVisqX5BZD7Yq3xp4nN2O15BBK0cLm3Vwn2vQaF7UDS0UUsrQMkkplmDI5fskig==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", + "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==", "dev": true, - "optional": true, - "dependencies": { - "os-homedir": "^1.0.0" - }, "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, "node_modules/upath": { @@ -26707,12 +26657,6 @@ "extsprintf": "^1.2.0" } }, - "node_modules/verror/node_modules/core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", - "dev": true - }, "node_modules/vfile": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/vfile/-/vfile-4.2.1.tgz", @@ -27280,9 +27224,9 @@ } }, "node_modules/webpack-dev-server": { - "version": "4.9.3", - "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.9.3.tgz", - "integrity": "sha512-3qp/eoboZG5/6QgiZ3llN8TUzkSpYg1Ko9khWX1h40MIEUNS2mDoIa8aXsPfskER+GbTvs/IJZ1QTBBhhuetSw==", + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.10.0.tgz", + "integrity": "sha512-7dezwAs+k6yXVFZ+MaL8VnE+APobiO3zvpp3rBHe/HmWQ+avwh0Q3d0xxacOiBybZZ3syTZw9HXzpa3YNbAZDQ==", "dev": true, "dependencies": { "@types/bonjour": "^3.5.9", @@ -27459,6 +27403,15 @@ "node": ">= 6" } }, + "node_modules/webpack-log/node_modules/ansi-colors": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.4.tgz", + "integrity": "sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/webpack-log/node_modules/uuid": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", @@ -27483,13 +27436,16 @@ } }, "node_modules/webpack-sources": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz", - "integrity": "sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-2.3.1.tgz", + "integrity": "sha512-y9EI9AO42JjEcrTJFOYmVywVZdKVUfOvDUPsJea5GIr1JOEGFVqwlY2K098fFoIjOkDzHn2AjRvM8dsBZu+gCA==", "dev": true, "dependencies": { - "source-list-map": "^2.0.0", - "source-map": "~0.6.1" + "source-list-map": "^2.0.1", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10.13.0" } }, "node_modules/webpack-sources/node_modules/source-map": { @@ -27520,9 +27476,9 @@ } }, "node_modules/webpack/node_modules/acorn": { - "version": "8.7.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.1.tgz", - "integrity": "sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A==", + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.0.tgz", + "integrity": "sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==", "dev": true, "bin": { "acorn": "bin/acorn" @@ -27914,9 +27870,9 @@ } }, "node_modules/yargs/node_modules/yargs-parser": { - "version": "21.0.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.0.1.tgz", - "integrity": "sha512-9BK1jFpLzJROCI5TzwZL/TU4gqjK5xiHV/RfWLOahrjAko/e4DJkRDZQXfvqAsiZzzYhgAzbgz6lg48jcm4GLg==", + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", "dev": true, "engines": { "node": ">=12" @@ -27979,20 +27935,20 @@ "integrity": "sha512-HSmX4WZPPK3FUxYp7g2T6EyO8j96HlZJlxmKPSh6KAcqwyDrfx7hKjXpAW/0FhFfTJsR0Yt4lAjLI2coMptIHQ==" }, "@babel/core": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.18.9.tgz", - "integrity": "sha512-1LIb1eL8APMy91/IMW+31ckrfBM4yCoLaVzoDhZUKSM4cu1L1nIidyxkCgzPAgrC5WEz36IPEr/eSeSF9pIn+g==", + "version": "7.18.10", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.18.10.tgz", + "integrity": "sha512-JQM6k6ENcBFKVtWvLavlvi/mPcpYZ3+R+2EySDEMSMbp7Mn4FexlbbJVrx2R7Ijhr01T8gyqrOaABWIOgxeUyw==", "requires": { "@ampproject/remapping": "^2.1.0", "@babel/code-frame": "^7.18.6", - "@babel/generator": "^7.18.9", + "@babel/generator": "^7.18.10", "@babel/helper-compilation-targets": "^7.18.9", "@babel/helper-module-transforms": "^7.18.9", "@babel/helpers": "^7.18.9", - "@babel/parser": "^7.18.9", - "@babel/template": "^7.18.6", - "@babel/traverse": "^7.18.9", - "@babel/types": "^7.18.9", + "@babel/parser": "^7.18.10", + "@babel/template": "^7.18.10", + "@babel/traverse": "^7.18.10", + "@babel/types": "^7.18.10", "convert-source-map": "^1.7.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -28001,11 +27957,11 @@ } }, "@babel/generator": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.18.9.tgz", - "integrity": "sha512-wt5Naw6lJrL1/SGkipMiFxJjtyczUWTP38deiP1PO60HsBjDeKk08CGC3S8iVuvf0FmTdgKwU1KIXzSKL1G0Ug==", + "version": "7.18.12", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.18.12.tgz", + "integrity": "sha512-dfQ8ebCN98SvyL7IxNMCUtZQSq5R7kxgN+r8qYTGDmmSion1hX2C0zq2yo1bsCDhXixokv1SAWTZUMYbO/V5zg==", "requires": { - "@babel/types": "^7.18.9", + "@babel/types": "^7.18.10", "@jridgewell/gen-mapping": "^0.3.2", "jsesc": "^2.5.1" }, @@ -28078,15 +28034,13 @@ } }, "@babel/helper-define-polyfill-provider": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.3.1.tgz", - "integrity": "sha512-J9hGMpJQmtWmj46B3kBHmL38UhJGhYX7eqkcq+2gsstyYt341HmPeWspihX43yVRA0mS+8GGk2Gckc7bY/HCmA==", + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.3.2.tgz", + "integrity": "sha512-r9QJJ+uDWrd+94BSPcP6/de67ygLtvVy6cK4luE6MOuDsZIdoaPBnfSpbO/+LTifjPckbKXRuI9BB/Z2/y3iTg==", "dev": true, "requires": { - "@babel/helper-compilation-targets": "^7.13.0", - "@babel/helper-module-imports": "^7.12.13", - "@babel/helper-plugin-utils": "^7.13.0", - "@babel/traverse": "^7.13.0", + "@babel/helper-compilation-targets": "^7.17.7", + "@babel/helper-plugin-utils": "^7.16.7", "debug": "^4.1.1", "lodash.debounce": "^4.0.8", "resolve": "^1.14.2", @@ -28220,6 +28174,11 @@ "@babel/types": "^7.18.6" } }, + "@babel/helper-string-parser": { + "version": "7.18.10", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.18.10.tgz", + "integrity": "sha512-XtIfWmeNY3i4t7t4D2t02q50HvqHybPqW2ki1kosnvWCwuCMeo81Jf0gwr85jy/neUdg5XDdeFE/80DXiO+njw==" + }, "@babel/helper-validator-identifier": { "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.18.6.tgz", @@ -28231,15 +28190,15 @@ "integrity": "sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw==" }, "@babel/helper-wrap-function": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.18.9.tgz", - "integrity": "sha512-cG2ru3TRAL6a60tfQflpEfs4ldiPwF6YW3zfJiRgmoFVIaC1vGnBBgatfec+ZUziPHkHSaXAuEck3Cdkf3eRpQ==", + "version": "7.18.11", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.18.11.tgz", + "integrity": "sha512-oBUlbv+rjZLh2Ks9SKi4aL7eKaAXBWleHzU89mP0G6BMUlRxSckk9tSIkgDGydhgFxHuGSlBQZfnaD47oBEB7w==", "dev": true, "requires": { "@babel/helper-function-name": "^7.18.9", - "@babel/template": "^7.18.6", - "@babel/traverse": "^7.18.9", - "@babel/types": "^7.18.9" + "@babel/template": "^7.18.10", + "@babel/traverse": "^7.18.11", + "@babel/types": "^7.18.10" } }, "@babel/helpers": { @@ -28263,9 +28222,9 @@ } }, "@babel/parser": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.18.9.tgz", - "integrity": "sha512-9uJveS9eY9DJ0t64YbIBZICtJy8a5QrDEVdiLCG97fVLpDTpGX7t8mMSb6OWw6Lrnjqj4O8zwjELX3dhoMgiBg==" + "version": "7.18.11", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.18.11.tgz", + "integrity": "sha512-9JKn5vN+hDt0Hdqn1PiJ2guflwP+B6Ga8qbDuoF0PzzVhrzsKIJo8yGqVk6CmMHiMei9w1C1Bp9IMJSIK+HPIQ==" }, "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { "version": "7.18.6", @@ -28288,14 +28247,14 @@ } }, "@babel/plugin-proposal-async-generator-functions": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.18.6.tgz", - "integrity": "sha512-WAz4R9bvozx4qwf74M+sfqPMKfSqwM0phxPTR6iJIi8robgzXwkEgmeJG1gEKhm6sDqT/U9aV3lfcqybIpev8w==", + "version": "7.18.10", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.18.10.tgz", + "integrity": "sha512-1mFuY2TOsR1hxbjCo4QL+qlIjV07p4H4EUYw2J/WCqsvFV6V9X9z9YhXbWndc/4fw+hYGlDT7egYxliMp5O6Ew==", "dev": true, "requires": { - "@babel/helper-environment-visitor": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/helper-remap-async-to-generator": "^7.18.6", + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-plugin-utils": "^7.18.9", + "@babel/helper-remap-async-to-generator": "^7.18.9", "@babel/plugin-syntax-async-generators": "^7.8.4" } }, @@ -28321,9 +28280,9 @@ } }, "@babel/plugin-proposal-decorators": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.18.9.tgz", - "integrity": "sha512-KD7zDNaD14CRpjQjVbV4EnH9lsKYlcpUrhZH37ei2IY+AlXrfAPy5pTmRUE4X6X1k8EsKXPraykxeaogqQvSGA==", + "version": "7.18.10", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.18.10.tgz", + "integrity": "sha512-wdGTwWF5QtpTY/gbBtQLAiCnoxfD4qMbN87NYZle1dOZ9Os8Y6zXcKrIaOU8W+TIvFUWVGG9tUgNww3CjXRVVw==", "dev": true, "requires": { "@babel/helper-create-class-features-plugin": "^7.18.9", @@ -28344,9 +28303,9 @@ } }, "@babel/plugin-proposal-export-default-from": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-default-from/-/plugin-proposal-export-default-from-7.18.9.tgz", - "integrity": "sha512-1qtsLNCDm5awHLIt+2qAFDi31XC94r4QepMQcOosC7FpY6O+Bgay5f2IyAQt2wvm1TARumpFprnQt5pTIJ9nUg==", + "version": "7.18.10", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-default-from/-/plugin-proposal-export-default-from-7.18.10.tgz", + "integrity": "sha512-5H2N3R2aQFxkV4PIBUR/i7PUSwgTZjouJKzI8eKswfIjT0PhvzkPn0t0wIS5zn6maQuvtT0t1oHtMUz61LOuow==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.18.9", @@ -28900,16 +28859,16 @@ } }, "@babel/plugin-transform-react-jsx": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.18.6.tgz", - "integrity": "sha512-Mz7xMPxoy9kPS/JScj6fJs03TZ/fZ1dJPlMjRAgTaxaS0fUBk8FV/A2rRgfPsVCZqALNwMexD+0Uaf5zlcKPpw==", + "version": "7.18.10", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.18.10.tgz", + "integrity": "sha512-gCy7Iikrpu3IZjYZolFE4M1Sm+nrh1/6za2Ewj77Z+XirT4TsbJcvOFOyF+fRPwU6AKKK136CZxx6L8AbSFG6A==", "dev": true, "requires": { "@babel/helper-annotate-as-pure": "^7.18.6", "@babel/helper-module-imports": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.9", "@babel/plugin-syntax-jsx": "^7.18.6", - "@babel/types": "^7.18.6" + "@babel/types": "^7.18.10" } }, "@babel/plugin-transform-react-jsx-development": { @@ -28997,23 +28956,23 @@ } }, "@babel/plugin-transform-typescript": { - "version": "7.18.8", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.18.8.tgz", - "integrity": "sha512-p2xM8HI83UObjsZGofMV/EdYjamsDm6MoN3hXPYIT0+gxIoopE+B7rPYKAxfrz9K9PK7JafTTjqYC6qipLExYA==", + "version": "7.18.12", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.18.12.tgz", + "integrity": "sha512-2vjjam0cum0miPkenUbQswKowuxs/NjMwIKEq0zwegRxXk12C9YOF9STXnaUptITOtOJHKHpzvvWYOjbm6tc0w==", "dev": true, "requires": { - "@babel/helper-create-class-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6", + "@babel/helper-create-class-features-plugin": "^7.18.9", + "@babel/helper-plugin-utils": "^7.18.9", "@babel/plugin-syntax-typescript": "^7.18.6" } }, "@babel/plugin-transform-unicode-escapes": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.18.6.tgz", - "integrity": "sha512-XNRwQUXYMP7VLuy54cr/KS/WeL3AZeORhrmeZ7iewgu+X2eBqmpaLI/hzqr9ZxCeUoq0ASK4GUzSM0BDhZkLFw==", + "version": "7.18.10", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.18.10.tgz", + "integrity": "sha512-kKAdAI+YzPgGY/ftStBFXTI1LZFju38rYThnfMykS+IXy8BVx+res7s2fxf1l8I35DV2T97ezo6+SGrXz6B3iQ==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-plugin-utils": "^7.18.9" } }, "@babel/plugin-transform-unicode-regex": { @@ -29027,9 +28986,9 @@ } }, "@babel/preset-env": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.18.9.tgz", - "integrity": "sha512-75pt/q95cMIHWssYtyfjVlvI+QEZQThQbKvR9xH+F/Agtw/s4Wfc2V9Bwd/P39VtixB7oWxGdH4GteTTwYJWMg==", + "version": "7.18.10", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.18.10.tgz", + "integrity": "sha512-wVxs1yjFdW3Z/XkNfXKoblxoHgbtUF7/l3PvvP4m02Qz9TZ6uZGxRVYjSQeR87oQmHco9zWitW5J82DJ7sCjvA==", "dev": true, "requires": { "@babel/compat-data": "^7.18.8", @@ -29038,7 +28997,7 @@ "@babel/helper-validator-option": "^7.18.6", "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.18.6", "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.18.9", - "@babel/plugin-proposal-async-generator-functions": "^7.18.6", + "@babel/plugin-proposal-async-generator-functions": "^7.18.10", "@babel/plugin-proposal-class-properties": "^7.18.6", "@babel/plugin-proposal-class-static-block": "^7.18.6", "@babel/plugin-proposal-dynamic-import": "^7.18.6", @@ -29098,13 +29057,13 @@ "@babel/plugin-transform-sticky-regex": "^7.18.6", "@babel/plugin-transform-template-literals": "^7.18.9", "@babel/plugin-transform-typeof-symbol": "^7.18.9", - "@babel/plugin-transform-unicode-escapes": "^7.18.6", + "@babel/plugin-transform-unicode-escapes": "^7.18.10", "@babel/plugin-transform-unicode-regex": "^7.18.6", "@babel/preset-modules": "^0.1.5", - "@babel/types": "^7.18.9", - "babel-plugin-polyfill-corejs2": "^0.3.1", - "babel-plugin-polyfill-corejs3": "^0.5.2", - "babel-plugin-polyfill-regenerator": "^0.3.1", + "@babel/types": "^7.18.10", + "babel-plugin-polyfill-corejs2": "^0.3.2", + "babel-plugin-polyfill-corejs3": "^0.5.3", + "babel-plugin-polyfill-regenerator": "^0.4.0", "core-js-compat": "^3.22.1", "semver": "^6.3.0" } @@ -29190,37 +29149,38 @@ } }, "@babel/template": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.18.6.tgz", - "integrity": "sha512-JoDWzPe+wgBsTTgdnIma3iHNFC7YVJoPssVBDjiHfNlyt4YcunDtcDOUmfVDfCK5MfdsaIoX9PkijPhjH3nYUw==", + "version": "7.18.10", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.18.10.tgz", + "integrity": "sha512-TI+rCtooWHr3QJ27kJxfjutghu44DLnasDMwpDqCXVTal9RLp3RSYNh4NdBrRP2cQAoG9A8juOQl6P6oZG4JxA==", "requires": { "@babel/code-frame": "^7.18.6", - "@babel/parser": "^7.18.6", - "@babel/types": "^7.18.6" + "@babel/parser": "^7.18.10", + "@babel/types": "^7.18.10" } }, "@babel/traverse": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.18.9.tgz", - "integrity": "sha512-LcPAnujXGwBgv3/WHv01pHtb2tihcyW1XuL9wd7jqh1Z8AQkTd+QVjMrMijrln0T7ED3UXLIy36P9Ao7W75rYg==", + "version": "7.18.11", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.18.11.tgz", + "integrity": "sha512-TG9PiM2R/cWCAy6BPJKeHzNbu4lPzOSZpeMfeNErskGpTJx6trEvFaVCbDvpcxwy49BKWmEPwiW8mrysNiDvIQ==", "requires": { "@babel/code-frame": "^7.18.6", - "@babel/generator": "^7.18.9", + "@babel/generator": "^7.18.10", "@babel/helper-environment-visitor": "^7.18.9", "@babel/helper-function-name": "^7.18.9", "@babel/helper-hoist-variables": "^7.18.6", "@babel/helper-split-export-declaration": "^7.18.6", - "@babel/parser": "^7.18.9", - "@babel/types": "^7.18.9", + "@babel/parser": "^7.18.11", + "@babel/types": "^7.18.10", "debug": "^4.1.0", "globals": "^11.1.0" } }, "@babel/types": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.18.9.tgz", - "integrity": "sha512-WwMLAg2MvJmt/rKEVQBBhIVffMmnilX4oe0sRe7iPOHIGsqpruFHHdrfj4O1CMMtgMtCU4oPafZjDPCRgO57Wg==", + "version": "7.18.10", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.18.10.tgz", + "integrity": "sha512-MJvnbEiiNkpjo+LknnmRrqbY1GPUUggjv+wQVjetM/AONoupqRALB7I6jGqNUAZsKcRIEu2J6FRFvsczljjsaQ==", "requires": { + "@babel/helper-string-parser": "^7.18.10", "@babel/helper-validator-identifier": "^7.18.6", "to-fast-properties": "^2.0.0" } @@ -29427,17 +29387,17 @@ "dev": true }, "@emotion/babel-plugin": { - "version": "11.9.2", - "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.9.2.tgz", - "integrity": "sha512-Pr/7HGH6H6yKgnVFNEj2MVlreu3ADqftqjqwUvDy/OJzKFgxKeTQ+eeUf20FOTuHVkDON2iNa25rAXVYtWJCjw==", + "version": "11.10.0", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.10.0.tgz", + "integrity": "sha512-xVnpDAAbtxL1dsuSelU5A7BnY/lftws0wUexNJZTPsvX/1tM4GZJbclgODhvW4E+NH7E5VFcH0bBn30NvniPJA==", "requires": { - "@babel/helper-module-imports": "^7.12.13", - "@babel/plugin-syntax-jsx": "^7.12.13", - "@babel/runtime": "^7.13.10", - "@emotion/hash": "^0.8.0", - "@emotion/memoize": "^0.7.5", - "@emotion/serialize": "^1.0.2", - "babel-plugin-macros": "^2.6.1", + "@babel/helper-module-imports": "^7.16.7", + "@babel/plugin-syntax-jsx": "^7.17.12", + "@babel/runtime": "^7.18.3", + "@emotion/hash": "^0.9.0", + "@emotion/memoize": "^0.8.0", + "@emotion/serialize": "^1.1.0", + "babel-plugin-macros": "^3.1.0", "convert-source-map": "^1.5.0", "escape-string-regexp": "^4.0.0", "find-root": "^1.1.0", @@ -29446,94 +29406,94 @@ } }, "@emotion/cache": { - "version": "11.9.3", - "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.9.3.tgz", - "integrity": "sha512-0dgkI/JKlCXa+lEXviaMtGBL0ynpx4osh7rjOXE71q9bIF8G+XhJgvi+wDu0B0IdCVx37BffiwXlN9I3UuzFvg==", - "requires": { - "@emotion/memoize": "^0.7.4", - "@emotion/sheet": "^1.1.1", - "@emotion/utils": "^1.0.0", - "@emotion/weak-memoize": "^0.2.5", + "version": "11.10.1", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.10.1.tgz", + "integrity": "sha512-uZTj3Yz5D69GE25iFZcIQtibnVCFsc/6+XIozyL3ycgWvEdif2uEw9wlUt6umjLr4Keg9K6xRPHmD8LGi+6p1A==", + "requires": { + "@emotion/memoize": "^0.8.0", + "@emotion/sheet": "^1.2.0", + "@emotion/utils": "^1.2.0", + "@emotion/weak-memoize": "^0.3.0", "stylis": "4.0.13" } }, "@emotion/hash": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz", - "integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==" + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.0.tgz", + "integrity": "sha512-14FtKiHhy2QoPIzdTcvh//8OyBlknNs2nXRwIhG904opCby3l+9Xaf/wuPvICBF0rc1ZCNBd3nKe9cd2mecVkQ==" }, "@emotion/is-prop-valid": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.1.3.tgz", - "integrity": "sha512-RFg04p6C+1uO19uG8N+vqanzKqiM9eeV1LDOG3bmkYmuOj7NbKNlFC/4EZq5gnwAIlcC/jOT24f8Td0iax2SXA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.0.tgz", + "integrity": "sha512-3aDpDprjM0AwaxGE09bOPkNxHpBd+kA6jty3RnaEXdweX1DF1U3VQpPYb0g1IStAuK7SVQ1cy+bNBBKp4W3Fjg==", "dev": true, "requires": { - "@emotion/memoize": "^0.7.4" + "@emotion/memoize": "^0.8.0" } }, "@emotion/memoize": { - "version": "0.7.5", - "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.5.tgz", - "integrity": "sha512-igX9a37DR2ZPGYtV6suZ6whr8pTFtyHL3K/oLUotxpSVO2ASaprmAe2Dkq7tBo7CRY7MMDrAa9nuQP9/YG8FxQ==" + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.0.tgz", + "integrity": "sha512-G/YwXTkv7Den9mXDO7AhLWkE3q+I92B+VqAE+dYG4NGPaHZGvt3G8Q0p9vmE+sq7rTGphUbAvmQ9YpbfMQGGlA==" }, "@emotion/react": { - "version": "11.9.3", - "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.9.3.tgz", - "integrity": "sha512-g9Q1GcTOlzOEjqwuLF/Zd9LC+4FljjPjDfxSM7KmEakm+hsHXk+bYZ2q+/hTJzr0OUNkujo72pXLQvXj6H+GJQ==", - "requires": { - "@babel/runtime": "^7.13.10", - "@emotion/babel-plugin": "^11.7.1", - "@emotion/cache": "^11.9.3", - "@emotion/serialize": "^1.0.4", - "@emotion/utils": "^1.1.0", - "@emotion/weak-memoize": "^0.2.5", + "version": "11.10.0", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.10.0.tgz", + "integrity": "sha512-K6z9zlHxxBXwN8TcpwBKcEsBsOw4JWCCmR+BeeOWgqp8GIU1yA2Odd41bwdAAr0ssbQrbJbVnndvv7oiv1bZeQ==", + "requires": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.10.0", + "@emotion/cache": "^11.10.0", + "@emotion/serialize": "^1.1.0", + "@emotion/utils": "^1.2.0", + "@emotion/weak-memoize": "^0.3.0", "hoist-non-react-statics": "^3.3.1" } }, "@emotion/serialize": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.0.4.tgz", - "integrity": "sha512-1JHamSpH8PIfFwAMryO2bNka+y8+KA5yga5Ocf2d7ZEiJjb7xlLW7aknBGZqJLajuLOvJ+72vN+IBSwPlXD1Pg==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.1.0.tgz", + "integrity": "sha512-F1ZZZW51T/fx+wKbVlwsfchr5q97iW8brAnXmsskz4d0hVB4O3M/SiA3SaeH06x02lSNzkkQv+n3AX3kCXKSFA==", "requires": { - "@emotion/hash": "^0.8.0", - "@emotion/memoize": "^0.7.4", - "@emotion/unitless": "^0.7.5", - "@emotion/utils": "^1.0.0", + "@emotion/hash": "^0.9.0", + "@emotion/memoize": "^0.8.0", + "@emotion/unitless": "^0.8.0", + "@emotion/utils": "^1.2.0", "csstype": "^3.0.2" } }, "@emotion/sheet": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.1.1.tgz", - "integrity": "sha512-J3YPccVRMiTZxYAY0IOq3kd+hUP8idY8Kz6B/Cyo+JuXq52Ek+zbPbSQUrVQp95aJ+lsAW7DPL1P2Z+U1jGkKA==" + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.2.0.tgz", + "integrity": "sha512-OiTkRgpxescko+M51tZsMq7Puu/KP55wMT8BgpcXVG2hqXc0Vo0mfymJ/Uj24Hp0i083ji/o0aLddh08UEjq8w==" }, "@emotion/styled": { - "version": "11.9.3", - "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.9.3.tgz", - "integrity": "sha512-o3sBNwbtoVz9v7WB1/Y/AmXl69YHmei2mrVnK7JgyBJ//Rst5yqPZCecEJlMlJrFeWHp+ki/54uN265V2pEcXA==", + "version": "11.10.0", + "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.10.0.tgz", + "integrity": "sha512-V9oaEH6V4KePeQpgUE83i8ht+4Ri3E8Djp/ZPJ4DQlqWhSKITvgzlR3/YQE2hdfP4Jw3qVRkANJz01LLqK9/TA==", "dev": true, "requires": { - "@babel/runtime": "^7.13.10", - "@emotion/babel-plugin": "^11.7.1", - "@emotion/is-prop-valid": "^1.1.3", - "@emotion/serialize": "^1.0.4", - "@emotion/utils": "^1.1.0" + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.10.0", + "@emotion/is-prop-valid": "^1.2.0", + "@emotion/serialize": "^1.1.0", + "@emotion/utils": "^1.2.0" } }, "@emotion/unitless": { - "version": "0.7.5", - "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz", - "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==" + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.0.tgz", + "integrity": "sha512-VINS5vEYAscRl2ZUDiT3uMPlrFQupiKgHz5AA4bCH1miKBg4qtwkim1qPmJj/4WG6TreYMY111rEFsjupcOKHw==" }, "@emotion/utils": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.1.0.tgz", - "integrity": "sha512-iRLa/Y4Rs5H/f2nimczYmS5kFJEbpiVvgN3XVfZ022IYhuNA1IRSHEizcof88LtCTXtl9S2Cxt32KgaXEu72JQ==" + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.2.0.tgz", + "integrity": "sha512-sn3WH53Kzpw8oQ5mgMmIzzyAaH2ZqFEbozVVBSYp538E06OSE6ytOp7pRAjNQR+Q/orwqdQYJSe2m3hCOeznkw==" }, "@emotion/weak-memoize": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz", - "integrity": "sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA==" + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.0.tgz", + "integrity": "sha512-AHPmaAx+RYfZz0eYu6Gviiagpmiyw98ySSlQvCUhVGDRtDFe4DBS0x1bSjdF3gqUDYOczB+yYvBTtEylYSdRhg==" }, "@esbuild/linux-loong64": { "version": "0.14.54", @@ -29566,9 +29526,9 @@ "dev": true }, "globals": { - "version": "13.16.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.16.0.tgz", - "integrity": "sha512-A1lrQfpNF+McdPOnnFqY3kSN0AFTy485bTi1bkLk4mVPODIUEcSfhHgRqA+QdXPksrSTTztYXx37NFV+GpGk3Q==", + "version": "13.17.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.17.0.tgz", + "integrity": "sha512-1C+6nQRb1GwGMKm2dH/E7enFAMxGTmGI7/dEdhy/DNelv85w9B72t3uc5frtMNXIbzrarJJ/lTCjcaZwbLJmyw==", "dev": true, "requires": { "type-fest": "^0.20.2" @@ -29592,9 +29552,9 @@ "dev": true }, "@humanwhocodes/config-array": { - "version": "0.9.5", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.9.5.tgz", - "integrity": "sha512-ObyMyWxZiCu/yTisA7uzx81s40xR2fD5Cg/2Kq7G02ajkNubJf6BopgDTmDyc3U7sXpNKM8cYOw7s7Tyr+DnCw==", + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.10.4.tgz", + "integrity": "sha512-mXAIHxZT3Vcpg83opl1wGlVZ9xydbfZO3r5YfRSH6Gpp2J/PfdBP0wbDa2sO6/qRbcalpoevVyW6A/fI6LfeMw==", "dev": true, "requires": { "@humanwhocodes/object-schema": "^1.2.1", @@ -29602,6 +29562,12 @@ "minimatch": "^3.0.4" } }, + "@humanwhocodes/gitignore-to-minimatch": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@humanwhocodes/gitignore-to-minimatch/-/gitignore-to-minimatch-1.0.2.tgz", + "integrity": "sha512-rSqmMJDdLFUsyxR6FMtD00nfQKKLFb1kv+qBbOVKqErvloEIJLo5bDTJTQNTYgeyp78JsA7u/NPi5jT1GR/MuA==", + "dev": true + }, "@humanwhocodes/object-schema": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", @@ -29729,12 +29695,6 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, - "slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true - }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -30116,18 +30076,18 @@ } }, "@storybook/addon-actions": { - "version": "6.5.9", - "resolved": "https://registry.npmjs.org/@storybook/addon-actions/-/addon-actions-6.5.9.tgz", - "integrity": "sha512-wDYm3M1bN+zcYZV3Q24M03b/P8DDpvj1oSoY6VLlxDAi56h8qZB/voeIS2I6vWXOB79C5tbwljYNQO0GsufS0g==", + "version": "6.5.10", + "resolved": "https://registry.npmjs.org/@storybook/addon-actions/-/addon-actions-6.5.10.tgz", + "integrity": "sha512-vpCnEu81fmtYzOf0QsRYoDuf9wXgVVl2VysE1dWRebRhIUDU0JurrthTnw322e38D4FzaoNGqZE7wnBYBohzZA==", "dev": true, "requires": { - "@storybook/addons": "6.5.9", - "@storybook/api": "6.5.9", - "@storybook/client-logger": "6.5.9", - "@storybook/components": "6.5.9", - "@storybook/core-events": "6.5.9", + "@storybook/addons": "6.5.10", + "@storybook/api": "6.5.10", + "@storybook/client-logger": "6.5.10", + "@storybook/components": "6.5.10", + "@storybook/core-events": "6.5.10", "@storybook/csf": "0.0.2--canary.4566f4d.1", - "@storybook/theming": "6.5.9", + "@storybook/theming": "6.5.10", "core-js": "^3.8.2", "fast-deep-equal": "^3.1.3", "global": "^4.4.0", @@ -30156,18 +30116,18 @@ } }, "@storybook/addon-backgrounds": { - "version": "6.5.9", - "resolved": "https://registry.npmjs.org/@storybook/addon-backgrounds/-/addon-backgrounds-6.5.9.tgz", - "integrity": "sha512-9k+GiY5aiANLOct34ar29jqgdi5ZpCqpZ86zPH0GsEC6ifH6nzP4trLU0vFUe260XDCvB4g8YaI7JZKPhozERg==", + "version": "6.5.10", + "resolved": "https://registry.npmjs.org/@storybook/addon-backgrounds/-/addon-backgrounds-6.5.10.tgz", + "integrity": "sha512-5uzQda3dh891h7BL8e9Ymk7BI+QgkkzDJXuA4mHjOXfIiD3S3efhJI8amXuBC2ZpIr6zmVit0MqZVyoVve46cQ==", "dev": true, "requires": { - "@storybook/addons": "6.5.9", - "@storybook/api": "6.5.9", - "@storybook/client-logger": "6.5.9", - "@storybook/components": "6.5.9", - "@storybook/core-events": "6.5.9", + "@storybook/addons": "6.5.10", + "@storybook/api": "6.5.10", + "@storybook/client-logger": "6.5.10", + "@storybook/components": "6.5.10", + "@storybook/core-events": "6.5.10", "@storybook/csf": "0.0.2--canary.4566f4d.1", - "@storybook/theming": "6.5.9", + "@storybook/theming": "6.5.10", "core-js": "^3.8.2", "global": "^4.4.0", "memoizerific": "^1.11.3", @@ -30177,49 +30137,49 @@ } }, "@storybook/addon-controls": { - "version": "6.5.9", - "resolved": "https://registry.npmjs.org/@storybook/addon-controls/-/addon-controls-6.5.9.tgz", - "integrity": "sha512-VvjkgK32bGURKyWU2No6Q2B0RQZjLZk8D3neVNCnrWxwrl1G82StegxjRPn/UZm9+MZVPvTvI46nj1VdgOktnw==", + "version": "6.5.10", + "resolved": "https://registry.npmjs.org/@storybook/addon-controls/-/addon-controls-6.5.10.tgz", + "integrity": "sha512-lC2y3XcolmQAJwFurIyGrynAHPWmfNtTCdu3rQBTVGwyxCoNwdOOeC2jV0BRqX2+CW6OHzJr9frNWXPSaZ8c4w==", "dev": true, "requires": { - "@storybook/addons": "6.5.9", - "@storybook/api": "6.5.9", - "@storybook/client-logger": "6.5.9", - "@storybook/components": "6.5.9", - "@storybook/core-common": "6.5.9", + "@storybook/addons": "6.5.10", + "@storybook/api": "6.5.10", + "@storybook/client-logger": "6.5.10", + "@storybook/components": "6.5.10", + "@storybook/core-common": "6.5.10", "@storybook/csf": "0.0.2--canary.4566f4d.1", - "@storybook/node-logger": "6.5.9", - "@storybook/store": "6.5.9", - "@storybook/theming": "6.5.9", + "@storybook/node-logger": "6.5.10", + "@storybook/store": "6.5.10", + "@storybook/theming": "6.5.10", "core-js": "^3.8.2", "lodash": "^4.17.21", "ts-dedent": "^2.0.0" } }, "@storybook/addon-docs": { - "version": "6.5.9", - "resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-6.5.9.tgz", - "integrity": "sha512-9lwOZyiOJFUgGd9ADVfcgpels5o0XOXqGMeVLuzT1160nopbZjNjo/3+YLJ0pyHRPpMJ4rmq2+vxRQR6PVRgPg==", + "version": "6.5.10", + "resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-6.5.10.tgz", + "integrity": "sha512-1kgjo3f0vL6GN8fTwLL05M/q/kDdzvuqwhxPY/v5hubFb3aQZGr2yk9pRBaLAbs4bez0yG0ASXcwhYnrEZUppg==", "dev": true, "requires": { "@babel/plugin-transform-react-jsx": "^7.12.12", "@babel/preset-env": "^7.12.11", "@jest/transform": "^26.6.2", "@mdx-js/react": "^1.6.22", - "@storybook/addons": "6.5.9", - "@storybook/api": "6.5.9", - "@storybook/components": "6.5.9", - "@storybook/core-common": "6.5.9", - "@storybook/core-events": "6.5.9", + "@storybook/addons": "6.5.10", + "@storybook/api": "6.5.10", + "@storybook/components": "6.5.10", + "@storybook/core-common": "6.5.10", + "@storybook/core-events": "6.5.10", "@storybook/csf": "0.0.2--canary.4566f4d.1", - "@storybook/docs-tools": "6.5.9", + "@storybook/docs-tools": "6.5.10", "@storybook/mdx1-csf": "^0.0.1", - "@storybook/node-logger": "6.5.9", - "@storybook/postinstall": "6.5.9", - "@storybook/preview-web": "6.5.9", - "@storybook/source-loader": "6.5.9", - "@storybook/store": "6.5.9", - "@storybook/theming": "6.5.9", + "@storybook/node-logger": "6.5.10", + "@storybook/postinstall": "6.5.10", + "@storybook/preview-web": "6.5.10", + "@storybook/source-loader": "6.5.10", + "@storybook/store": "6.5.10", + "@storybook/theming": "6.5.10", "babel-loader": "^8.0.0", "core-js": "^3.8.2", "fast-deep-equal": "^3.1.3", @@ -30242,44 +30202,44 @@ } }, "@storybook/addon-essentials": { - "version": "6.5.9", - "resolved": "https://registry.npmjs.org/@storybook/addon-essentials/-/addon-essentials-6.5.9.tgz", - "integrity": "sha512-V9ThjKQsde4A2Es20pLFBsn0MWx2KCJuoTcTsANP4JDcbvEmj8UjbDWbs8jAU+yzJT5r+CI6NoWmQudv12ZOgw==", - "dev": true, - "requires": { - "@storybook/addon-actions": "6.5.9", - "@storybook/addon-backgrounds": "6.5.9", - "@storybook/addon-controls": "6.5.9", - "@storybook/addon-docs": "6.5.9", - "@storybook/addon-measure": "6.5.9", - "@storybook/addon-outline": "6.5.9", - "@storybook/addon-toolbars": "6.5.9", - "@storybook/addon-viewport": "6.5.9", - "@storybook/addons": "6.5.9", - "@storybook/api": "6.5.9", - "@storybook/core-common": "6.5.9", - "@storybook/node-logger": "6.5.9", + "version": "6.5.10", + "resolved": "https://registry.npmjs.org/@storybook/addon-essentials/-/addon-essentials-6.5.10.tgz", + "integrity": "sha512-PT2aiR4vgAyB0pl3HNBUa4/a7NDRxASxAazz7zt9ZDirkipDKfxwdcLeRoJzwSngVDWEhuz5/paN5x4eNp4Hww==", + "dev": true, + "requires": { + "@storybook/addon-actions": "6.5.10", + "@storybook/addon-backgrounds": "6.5.10", + "@storybook/addon-controls": "6.5.10", + "@storybook/addon-docs": "6.5.10", + "@storybook/addon-measure": "6.5.10", + "@storybook/addon-outline": "6.5.10", + "@storybook/addon-toolbars": "6.5.10", + "@storybook/addon-viewport": "6.5.10", + "@storybook/addons": "6.5.10", + "@storybook/api": "6.5.10", + "@storybook/core-common": "6.5.10", + "@storybook/node-logger": "6.5.10", "core-js": "^3.8.2", "regenerator-runtime": "^0.13.7", "ts-dedent": "^2.0.0" } }, "@storybook/addon-interactions": { - "version": "6.5.9", - "resolved": "https://registry.npmjs.org/@storybook/addon-interactions/-/addon-interactions-6.5.9.tgz", - "integrity": "sha512-p3xBbrhmYTHvRO8MqAIr2DucgrXt38nJE71rogLNLsJ01rUN4JsLI8OkQAMQbqfIpwC27irMjQxJTp4HSzkFJA==", + "version": "6.5.10", + "resolved": "https://registry.npmjs.org/@storybook/addon-interactions/-/addon-interactions-6.5.10.tgz", + "integrity": "sha512-+O/ZuQjonpFmTdFRqjCimQTx4S4c1+S3dYCn6gD/E4xzqlQn1BQaER3paX/aBUKb3oRaSO9RUQ+uxePM4zBEwA==", "dev": true, "requires": { "@devtools-ds/object-inspector": "^1.1.2", - "@storybook/addons": "6.5.9", - "@storybook/api": "6.5.9", - "@storybook/client-logger": "6.5.9", - "@storybook/components": "6.5.9", - "@storybook/core-common": "6.5.9", - "@storybook/core-events": "6.5.9", + "@storybook/addons": "6.5.10", + "@storybook/api": "6.5.10", + "@storybook/client-logger": "6.5.10", + "@storybook/components": "6.5.10", + "@storybook/core-common": "6.5.10", + "@storybook/core-events": "6.5.10", "@storybook/csf": "0.0.2--canary.4566f4d.1", - "@storybook/instrumenter": "6.5.9", - "@storybook/theming": "6.5.9", + "@storybook/instrumenter": "6.5.10", + "@storybook/theming": "6.5.10", "core-js": "^3.8.2", "global": "^4.4.0", "jest-mock": "^27.0.6", @@ -30288,16 +30248,16 @@ } }, "@storybook/addon-links": { - "version": "6.5.9", - "resolved": "https://registry.npmjs.org/@storybook/addon-links/-/addon-links-6.5.9.tgz", - "integrity": "sha512-4BYC7pkxL3NLRnEgTA9jpIkObQKril+XFj1WtmY/lngF90vvK0Kc/TtvTA2/5tSgrHfxEuPevIdxMIyLJ4ejWQ==", + "version": "6.5.10", + "resolved": "https://registry.npmjs.org/@storybook/addon-links/-/addon-links-6.5.10.tgz", + "integrity": "sha512-r3WzYIPz7WjHiaPObC2Tg6bHuZRBb/Kt/X+Eitw+jTqBel7ksvkO36tn81q8Eyj61qIdNQmUWAaX/0aewT0kLA==", "dev": true, "requires": { - "@storybook/addons": "6.5.9", - "@storybook/client-logger": "6.5.9", - "@storybook/core-events": "6.5.9", + "@storybook/addons": "6.5.10", + "@storybook/client-logger": "6.5.10", + "@storybook/core-events": "6.5.10", "@storybook/csf": "0.0.2--canary.4566f4d.1", - "@storybook/router": "6.5.9", + "@storybook/router": "6.5.10", "@types/qs": "^6.9.5", "core-js": "^3.8.2", "global": "^4.4.0", @@ -30308,32 +30268,32 @@ } }, "@storybook/addon-measure": { - "version": "6.5.9", - "resolved": "https://registry.npmjs.org/@storybook/addon-measure/-/addon-measure-6.5.9.tgz", - "integrity": "sha512-0aA22wD0CIEUccsEbWkckCOXOwr4VffofMH1ToVCOeqBoyLOMB0gxFubESeprqM54CWsYh2DN1uujgD6508cwA==", + "version": "6.5.10", + "resolved": "https://registry.npmjs.org/@storybook/addon-measure/-/addon-measure-6.5.10.tgz", + "integrity": "sha512-ss7L1H5K5hXygDIoVwj+QyVXbve5V67x7CofLiLCgQYuJzfO16+sPGjiTGWMpTb4ijox2uKWnTkpilt5bCjXgw==", "dev": true, "requires": { - "@storybook/addons": "6.5.9", - "@storybook/api": "6.5.9", - "@storybook/client-logger": "6.5.9", - "@storybook/components": "6.5.9", - "@storybook/core-events": "6.5.9", + "@storybook/addons": "6.5.10", + "@storybook/api": "6.5.10", + "@storybook/client-logger": "6.5.10", + "@storybook/components": "6.5.10", + "@storybook/core-events": "6.5.10", "@storybook/csf": "0.0.2--canary.4566f4d.1", "core-js": "^3.8.2", "global": "^4.4.0" } }, "@storybook/addon-outline": { - "version": "6.5.9", - "resolved": "https://registry.npmjs.org/@storybook/addon-outline/-/addon-outline-6.5.9.tgz", - "integrity": "sha512-oJ1DK3BDJr6aTlZc9axfOxV1oDkZO7hOohgUQDaKO1RZrSpyQsx2ViK2X6p/W7JhFJHKh7rv+nGCaVlLz3YIZA==", + "version": "6.5.10", + "resolved": "https://registry.npmjs.org/@storybook/addon-outline/-/addon-outline-6.5.10.tgz", + "integrity": "sha512-AjdaeQ+/iBKmGrAqRW4niwMB6AkgGnYmSzVs5Cf6F/Sb4Dp+vzgLNOwLABD9qs8Ri8dvHl5J4QpVwQKUhYZaOQ==", "dev": true, "requires": { - "@storybook/addons": "6.5.9", - "@storybook/api": "6.5.9", - "@storybook/client-logger": "6.5.9", - "@storybook/components": "6.5.9", - "@storybook/core-events": "6.5.9", + "@storybook/addons": "6.5.10", + "@storybook/api": "6.5.10", + "@storybook/client-logger": "6.5.10", + "@storybook/components": "6.5.10", + "@storybook/core-events": "6.5.10", "@storybook/csf": "0.0.2--canary.4566f4d.1", "core-js": "^3.8.2", "global": "^4.4.0", @@ -30342,32 +30302,32 @@ } }, "@storybook/addon-toolbars": { - "version": "6.5.9", - "resolved": "https://registry.npmjs.org/@storybook/addon-toolbars/-/addon-toolbars-6.5.9.tgz", - "integrity": "sha512-6JFQNHYVZUwp17p5rppc+iQJ2QOIWPTF+ni1GMMThjc84mzXs2+899Sf1aPFTvrFJTklmT+bPX6x4aUTouVa1w==", + "version": "6.5.10", + "resolved": "https://registry.npmjs.org/@storybook/addon-toolbars/-/addon-toolbars-6.5.10.tgz", + "integrity": "sha512-S0Ljc6Wv+bPbx2e0iTveJ6bBDqjsemu+FZD4qDLsHreoI7DAcqyrF5Def1l8xNohixIVpx8dQpYXRtyzNlXekg==", "dev": true, "requires": { - "@storybook/addons": "6.5.9", - "@storybook/api": "6.5.9", - "@storybook/client-logger": "6.5.9", - "@storybook/components": "6.5.9", - "@storybook/theming": "6.5.9", + "@storybook/addons": "6.5.10", + "@storybook/api": "6.5.10", + "@storybook/client-logger": "6.5.10", + "@storybook/components": "6.5.10", + "@storybook/theming": "6.5.10", "core-js": "^3.8.2", "regenerator-runtime": "^0.13.7" } }, "@storybook/addon-viewport": { - "version": "6.5.9", - "resolved": "https://registry.npmjs.org/@storybook/addon-viewport/-/addon-viewport-6.5.9.tgz", - "integrity": "sha512-thKS+iw6M7ueDQQ7M66STZ5rgtJKliAcIX6UCopo0Ffh4RaRYmX6MCjqtvBKk8joyXUvm9SpWQemJD9uBQrjgw==", + "version": "6.5.10", + "resolved": "https://registry.npmjs.org/@storybook/addon-viewport/-/addon-viewport-6.5.10.tgz", + "integrity": "sha512-RFMd+4kZljyuJjR9OJ2bFXHrSG7VTi5FDZYWEU+4W1sBxzC+JhnVnUP+HJH3gUxEFIRQC5neRzwWRE9RUUoALQ==", "dev": true, "requires": { - "@storybook/addons": "6.5.9", - "@storybook/api": "6.5.9", - "@storybook/client-logger": "6.5.9", - "@storybook/components": "6.5.9", - "@storybook/core-events": "6.5.9", - "@storybook/theming": "6.5.9", + "@storybook/addons": "6.5.10", + "@storybook/api": "6.5.10", + "@storybook/client-logger": "6.5.10", + "@storybook/components": "6.5.10", + "@storybook/core-events": "6.5.10", + "@storybook/theming": "6.5.10", "core-js": "^3.8.2", "global": "^4.4.0", "memoizerific": "^1.11.3", @@ -30376,18 +30336,18 @@ } }, "@storybook/addons": { - "version": "6.5.9", - "resolved": "https://registry.npmjs.org/@storybook/addons/-/addons-6.5.9.tgz", - "integrity": "sha512-adwdiXg+mntfPocLc1KXjZXyLgGk7Aac699Fwe+OUYPEC5tW347Rm/kFatcE556d42o5czcRiq3ZSIGWnm9ieQ==", + "version": "6.5.10", + "resolved": "https://registry.npmjs.org/@storybook/addons/-/addons-6.5.10.tgz", + "integrity": "sha512-VD4tBCQ23PkSeDoxuHcKy0RfhIs3oMYjBacOZx7d0bvOzK9WjPyvE2ysDAh7r/ceqnwmWHAScIpE+I1RU7gl+g==", "dev": true, "requires": { - "@storybook/api": "6.5.9", - "@storybook/channels": "6.5.9", - "@storybook/client-logger": "6.5.9", - "@storybook/core-events": "6.5.9", + "@storybook/api": "6.5.10", + "@storybook/channels": "6.5.10", + "@storybook/client-logger": "6.5.10", + "@storybook/core-events": "6.5.10", "@storybook/csf": "0.0.2--canary.4566f4d.1", - "@storybook/router": "6.5.9", - "@storybook/theming": "6.5.9", + "@storybook/router": "6.5.10", + "@storybook/theming": "6.5.10", "@types/webpack-env": "^1.16.0", "core-js": "^3.8.2", "global": "^4.4.0", @@ -30395,18 +30355,18 @@ } }, "@storybook/api": { - "version": "6.5.9", - "resolved": "https://registry.npmjs.org/@storybook/api/-/api-6.5.9.tgz", - "integrity": "sha512-9ylztnty4Y+ALU/ehW3BML9czjCAFsWvrwuCi6UgcwNjswwjSX3VRLhfD1KT3pl16ho//95LgZ0LnSwROCcPOA==", + "version": "6.5.10", + "resolved": "https://registry.npmjs.org/@storybook/api/-/api-6.5.10.tgz", + "integrity": "sha512-AkmgSPNEGdKp4oZA4KQ+RJsacw7GwfvjsVDnCkcXqS9zmSr/RNL0fhpcd60KKkmx/hGKPTDFpK3ZayxDrJ/h4A==", "dev": true, "requires": { - "@storybook/channels": "6.5.9", - "@storybook/client-logger": "6.5.9", - "@storybook/core-events": "6.5.9", + "@storybook/channels": "6.5.10", + "@storybook/client-logger": "6.5.10", + "@storybook/core-events": "6.5.10", "@storybook/csf": "0.0.2--canary.4566f4d.1", - "@storybook/router": "6.5.9", + "@storybook/router": "6.5.10", "@storybook/semver": "^7.3.2", - "@storybook/theming": "6.5.9", + "@storybook/theming": "6.5.10", "core-js": "^3.8.2", "fast-deep-equal": "^3.1.3", "global": "^4.4.0", @@ -30420,28 +30380,28 @@ } }, "@storybook/builder-webpack4": { - "version": "6.5.9", - "resolved": "https://registry.npmjs.org/@storybook/builder-webpack4/-/builder-webpack4-6.5.9.tgz", - "integrity": "sha512-YOeA4++9uRZ8Hog1wC60yjaxBOiI1FRQNtax7b9E7g+kP8UlSCPCGcv4gls9hFmzbzTOPfQTWnToA9Oa6jzRVw==", + "version": "6.5.10", + "resolved": "https://registry.npmjs.org/@storybook/builder-webpack4/-/builder-webpack4-6.5.10.tgz", + "integrity": "sha512-AoKjsCNoQQoZXYwBDxO8s+yVEd5FjBJAaysEuUTHq2fb81jwLrGcEOo6hjw4jqfugZQIzYUEjPazlvubS78zpw==", "dev": true, "requires": { "@babel/core": "^7.12.10", - "@storybook/addons": "6.5.9", - "@storybook/api": "6.5.9", - "@storybook/channel-postmessage": "6.5.9", - "@storybook/channels": "6.5.9", - "@storybook/client-api": "6.5.9", - "@storybook/client-logger": "6.5.9", - "@storybook/components": "6.5.9", - "@storybook/core-common": "6.5.9", - "@storybook/core-events": "6.5.9", - "@storybook/node-logger": "6.5.9", - "@storybook/preview-web": "6.5.9", - "@storybook/router": "6.5.9", + "@storybook/addons": "6.5.10", + "@storybook/api": "6.5.10", + "@storybook/channel-postmessage": "6.5.10", + "@storybook/channels": "6.5.10", + "@storybook/client-api": "6.5.10", + "@storybook/client-logger": "6.5.10", + "@storybook/components": "6.5.10", + "@storybook/core-common": "6.5.10", + "@storybook/core-events": "6.5.10", + "@storybook/node-logger": "6.5.10", + "@storybook/preview-web": "6.5.10", + "@storybook/router": "6.5.10", "@storybook/semver": "^7.3.2", - "@storybook/store": "6.5.9", - "@storybook/theming": "6.5.9", - "@storybook/ui": "6.5.9", + "@storybook/store": "6.5.10", + "@storybook/theming": "6.5.10", + "@storybook/ui": "6.5.10", "@types/node": "^14.0.10 || ^16.0.0", "@types/webpack": "^4.41.26", "autoprefixer": "^9.8.6", @@ -30481,9 +30441,9 @@ "dev": true }, "@types/node": { - "version": "16.11.45", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.45.tgz", - "integrity": "sha512-3rKg/L5x0rofKuuUt5zlXzOnKyIHXmIu5R8A0TuNDMF2062/AOIDBciFIjToLEJ/9F9DzkHNot+BpNsMI1OLdQ==", + "version": "16.11.47", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.47.tgz", + "integrity": "sha512-fpP+jk2zJ4VW66+wAMFoBJlx1bxmBKx4DUFf68UHgdGCOuyUTDlLWqsaNPJh7xhNDykyJ9eIzAygilP/4WoN8g==", "dev": true }, "@webassemblyjs/ast": { @@ -30696,6 +30656,12 @@ "source-map": "~0.6.0" } }, + "commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true + }, "enhanced-resolve": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.5.0.tgz", @@ -31094,6 +31060,16 @@ "dev": true, "requires": {} }, + "webpack-sources": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz", + "integrity": "sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==", + "dev": true, + "requires": { + "source-list-map": "^2.0.0", + "source-map": "~0.6.1" + } + }, "y18n": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", @@ -31109,27 +31085,27 @@ } }, "@storybook/builder-webpack5": { - "version": "6.5.9", - "resolved": "https://registry.npmjs.org/@storybook/builder-webpack5/-/builder-webpack5-6.5.9.tgz", - "integrity": "sha512-NUVZ4Qci6HWPuoH8U/zQkdBO5soGgu7QYrGC/LWU0tRfmmZxkjr7IUU14ppDpGPYgx3r7jkaQI1J/E1YEmSCWQ==", + "version": "6.5.10", + "resolved": "https://registry.npmjs.org/@storybook/builder-webpack5/-/builder-webpack5-6.5.10.tgz", + "integrity": "sha512-Hcsm/TzGRXHndgQCftt+pzI7GQJRqAv8A8ie5b3aFcodhJfK0qzZsQD4Y4ZWxXh1I/xe5t74Kl2qUJ40PX+geA==", "dev": true, "requires": { "@babel/core": "^7.12.10", - "@storybook/addons": "6.5.9", - "@storybook/api": "6.5.9", - "@storybook/channel-postmessage": "6.5.9", - "@storybook/channels": "6.5.9", - "@storybook/client-api": "6.5.9", - "@storybook/client-logger": "6.5.9", - "@storybook/components": "6.5.9", - "@storybook/core-common": "6.5.9", - "@storybook/core-events": "6.5.9", - "@storybook/node-logger": "6.5.9", - "@storybook/preview-web": "6.5.9", - "@storybook/router": "6.5.9", + "@storybook/addons": "6.5.10", + "@storybook/api": "6.5.10", + "@storybook/channel-postmessage": "6.5.10", + "@storybook/channels": "6.5.10", + "@storybook/client-api": "6.5.10", + "@storybook/client-logger": "6.5.10", + "@storybook/components": "6.5.10", + "@storybook/core-common": "6.5.10", + "@storybook/core-events": "6.5.10", + "@storybook/node-logger": "6.5.10", + "@storybook/preview-web": "6.5.10", + "@storybook/router": "6.5.10", "@storybook/semver": "^7.3.2", - "@storybook/store": "6.5.9", - "@storybook/theming": "6.5.9", + "@storybook/store": "6.5.10", + "@storybook/theming": "6.5.10", "@types/node": "^14.0.10 || ^16.0.0", "babel-loader": "^8.0.0", "babel-plugin-named-exports-order": "^0.0.2", @@ -31155,9 +31131,9 @@ }, "dependencies": { "@types/node": { - "version": "16.11.45", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.45.tgz", - "integrity": "sha512-3rKg/L5x0rofKuuUt5zlXzOnKyIHXmIu5R8A0TuNDMF2062/AOIDBciFIjToLEJ/9F9DzkHNot+BpNsMI1OLdQ==", + "version": "16.11.47", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.47.tgz", + "integrity": "sha512-fpP+jk2zJ4VW66+wAMFoBJlx1bxmBKx4DUFf68UHgdGCOuyUTDlLWqsaNPJh7xhNDykyJ9eIzAygilP/4WoN8g==", "dev": true }, "ansi-styles": { @@ -31200,6 +31176,19 @@ "integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==", "dev": true }, + "cosmiconfig": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz", + "integrity": "sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==", + "dev": true, + "requires": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.1.0", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.7.2" + } + }, "css-loader": { "version": "5.2.7", "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-5.2.7.tgz", @@ -31294,9 +31283,9 @@ "dev": true }, "postcss": { - "version": "8.4.14", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.14.tgz", - "integrity": "sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig==", + "version": "8.4.16", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.16.tgz", + "integrity": "sha512-ipHE1XBvKzm5xI7hiHCZJCSugxvsdq2mPnsq5+UF+VHCjiBvtDrlxJfMBToWaP9D5XlgNmcFGqoHmUn0EYEaRQ==", "dev": true, "requires": { "nanoid": "^3.3.4", @@ -31419,14 +31408,14 @@ } }, "@storybook/channel-postmessage": { - "version": "6.5.9", - "resolved": "https://registry.npmjs.org/@storybook/channel-postmessage/-/channel-postmessage-6.5.9.tgz", - "integrity": "sha512-pX/0R8UW7ezBhCrafRaL20OvMRcmESYvQQCDgjqSzJyHkcG51GOhsd6Ge93eJ6QvRMm9+w0Zs93N2VKjVtz0Qw==", + "version": "6.5.10", + "resolved": "https://registry.npmjs.org/@storybook/channel-postmessage/-/channel-postmessage-6.5.10.tgz", + "integrity": "sha512-t9PTA0UzFvYa3IlOfpBOolfrRMPTjUMIeCQ6FNyM0aj5GqLKSvoQzP8NeoRpIrvyf6ljFKKdaMaZ3fiCvh45ag==", "dev": true, "requires": { - "@storybook/channels": "6.5.9", - "@storybook/client-logger": "6.5.9", - "@storybook/core-events": "6.5.9", + "@storybook/channels": "6.5.10", + "@storybook/client-logger": "6.5.10", + "@storybook/core-events": "6.5.10", "core-js": "^3.8.2", "global": "^4.4.0", "qs": "^6.10.0", @@ -31434,22 +31423,22 @@ } }, "@storybook/channel-websocket": { - "version": "6.5.9", - "resolved": "https://registry.npmjs.org/@storybook/channel-websocket/-/channel-websocket-6.5.9.tgz", - "integrity": "sha512-xtHvSNwuOhkgALwVshKWsoFhDmuvcosdYfxcfFGEiYKXIu46tRS5ZXmpmgEC/0JAVkVoFj5nL8bV7IY5np6oaA==", + "version": "6.5.10", + "resolved": "https://registry.npmjs.org/@storybook/channel-websocket/-/channel-websocket-6.5.10.tgz", + "integrity": "sha512-RTXMZbMWCS3xU+4GVIdfnUXsKcwg/WTozy88/5OxaKjGw6KgRedqLAQJKJ6Y5XlnwIcWelirkHj/COwTTXhbPg==", "dev": true, "requires": { - "@storybook/channels": "6.5.9", - "@storybook/client-logger": "6.5.9", + "@storybook/channels": "6.5.10", + "@storybook/client-logger": "6.5.10", "core-js": "^3.8.2", "global": "^4.4.0", "telejson": "^6.0.8" } }, "@storybook/channels": { - "version": "6.5.9", - "resolved": "https://registry.npmjs.org/@storybook/channels/-/channels-6.5.9.tgz", - "integrity": "sha512-FvGA35nV38UPXWOl9ERapFTJaxwSTamQ339s2Ev7E9riyRG+GRkgTWzf5kECJgS1PAYKd/7m/RqKJT9BVv6A5g==", + "version": "6.5.10", + "resolved": "https://registry.npmjs.org/@storybook/channels/-/channels-6.5.10.tgz", + "integrity": "sha512-lo26YZ6kWpHXLhuHJF4P/bICY7jD/rXEZqReKtGOSk1Lv99/xvG6pqmcy3hWLf3v3Dy/8otjRPSR7izFVIIZgQ==", "dev": true, "requires": { "core-js": "^3.8.2", @@ -31458,18 +31447,18 @@ } }, "@storybook/client-api": { - "version": "6.5.9", - "resolved": "https://registry.npmjs.org/@storybook/client-api/-/client-api-6.5.9.tgz", - "integrity": "sha512-pc7JKJoWLesixUKvG2nV36HukUuYoGRyAgD3PpIV7qSBS4JixqZ3VAHFUtqV1UzfOSQTovLSl4a0rIRnpie6gA==", + "version": "6.5.10", + "resolved": "https://registry.npmjs.org/@storybook/client-api/-/client-api-6.5.10.tgz", + "integrity": "sha512-3wBWZl3NvMFgMovgEh+euiARAT2FXzpvTF4Q1gerGMNNDlrGxHnFvSuy4FHg/irtOGLa4yLz43ULFbYtpKw0Lg==", "dev": true, "requires": { - "@storybook/addons": "6.5.9", - "@storybook/channel-postmessage": "6.5.9", - "@storybook/channels": "6.5.9", - "@storybook/client-logger": "6.5.9", - "@storybook/core-events": "6.5.9", + "@storybook/addons": "6.5.10", + "@storybook/channel-postmessage": "6.5.10", + "@storybook/channels": "6.5.10", + "@storybook/client-logger": "6.5.10", + "@storybook/core-events": "6.5.10", "@storybook/csf": "0.0.2--canary.4566f4d.1", - "@storybook/store": "6.5.9", + "@storybook/store": "6.5.10", "@types/qs": "^6.9.5", "@types/webpack-env": "^1.16.0", "core-js": "^3.8.2", @@ -31486,9 +31475,9 @@ } }, "@storybook/client-logger": { - "version": "6.5.9", - "resolved": "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-6.5.9.tgz", - "integrity": "sha512-DOHL6p0uiDd3gV/Sb2FR+Vh6OiPrrf8BrA06uvXWsMRIIvEEvnparxv9EvPg7FlmUX0T3nq7d3juwjx4F8Wbcg==", + "version": "6.5.10", + "resolved": "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-6.5.10.tgz", + "integrity": "sha512-/xA0MHOevXev68hyLMQw8Qo8KczSIdXOxliAgrycMTkDmw5eKeA8TP7B8zP3wGuq/e3MrdD9/8MWhb/IQBNC3w==", "dev": true, "requires": { "core-js": "^3.8.2", @@ -31496,49 +31485,47 @@ } }, "@storybook/components": { - "version": "6.5.9", - "resolved": "https://registry.npmjs.org/@storybook/components/-/components-6.5.9.tgz", - "integrity": "sha512-BhfX980O9zn/1J4FNMeDo8ZvL1m5Ml3T4HRpfYmEBnf8oW5b5BeF6S2K2cwFStZRjWqm1feUcwNpZxCBVMkQnQ==", + "version": "6.5.10", + "resolved": "https://registry.npmjs.org/@storybook/components/-/components-6.5.10.tgz", + "integrity": "sha512-9OhgB8YQfGwOKjo/N96N5mrtJ6qDVVoEM1zuhea32tJUd2eYf0aSWpryA9VnOM0V1q/8DAoCg5rPBMYWMBU5uw==", "dev": true, "requires": { - "@storybook/client-logger": "6.5.9", + "@storybook/client-logger": "6.5.10", "@storybook/csf": "0.0.2--canary.4566f4d.1", - "@storybook/theming": "6.5.9", - "@types/react-syntax-highlighter": "11.0.5", + "@storybook/theming": "6.5.10", "core-js": "^3.8.2", "memoizerific": "^1.11.3", "qs": "^6.10.0", - "react-syntax-highlighter": "^15.4.5", "regenerator-runtime": "^0.13.7", "util-deprecate": "^1.0.2" } }, "@storybook/core": { - "version": "6.5.9", - "resolved": "https://registry.npmjs.org/@storybook/core/-/core-6.5.9.tgz", - "integrity": "sha512-Mt3TTQnjQt2/pa60A+bqDsAOrYpohapdtt4DDZEbS8h0V6u11KyYYh3w7FCySlL+sPEyogj63l5Ec76Jah3l2w==", + "version": "6.5.10", + "resolved": "https://registry.npmjs.org/@storybook/core/-/core-6.5.10.tgz", + "integrity": "sha512-K86yYa0tYlMxADlwQTculYvPROokQau09SCVqpsLg3wJCTvYFL4+SIqcYoyBSbFmHOdnYbJgPydjN33MYLiOZQ==", "dev": true, "requires": { - "@storybook/core-client": "6.5.9", - "@storybook/core-server": "6.5.9" + "@storybook/core-client": "6.5.10", + "@storybook/core-server": "6.5.10" } }, "@storybook/core-client": { - "version": "6.5.9", - "resolved": "https://registry.npmjs.org/@storybook/core-client/-/core-client-6.5.9.tgz", - "integrity": "sha512-LY0QbhShowO+PQx3gao3wdVjpKMH1AaSLmuI95FrcjoMmSXGf96jVLKQp9mJRGeHIsAa93EQBYuCihZycM3Kbg==", + "version": "6.5.10", + "resolved": "https://registry.npmjs.org/@storybook/core-client/-/core-client-6.5.10.tgz", + "integrity": "sha512-THsIjNrOrampTl0Lgfjvfjk1JnktKb4CQLOM80KpQb4cjDqorBjJmErzUkUQ2y3fXvrDmQ/kUREkShET4XEdtA==", "dev": true, "requires": { - "@storybook/addons": "6.5.9", - "@storybook/channel-postmessage": "6.5.9", - "@storybook/channel-websocket": "6.5.9", - "@storybook/client-api": "6.5.9", - "@storybook/client-logger": "6.5.9", - "@storybook/core-events": "6.5.9", + "@storybook/addons": "6.5.10", + "@storybook/channel-postmessage": "6.5.10", + "@storybook/channel-websocket": "6.5.10", + "@storybook/client-api": "6.5.10", + "@storybook/client-logger": "6.5.10", + "@storybook/core-events": "6.5.10", "@storybook/csf": "0.0.2--canary.4566f4d.1", - "@storybook/preview-web": "6.5.9", - "@storybook/store": "6.5.9", - "@storybook/ui": "6.5.9", + "@storybook/preview-web": "6.5.10", + "@storybook/store": "6.5.10", + "@storybook/ui": "6.5.10", "airbnb-js-shims": "^2.2.1", "ansi-to-html": "^0.6.11", "core-js": "^3.8.2", @@ -31552,9 +31539,9 @@ } }, "@storybook/core-common": { - "version": "6.5.9", - "resolved": "https://registry.npmjs.org/@storybook/core-common/-/core-common-6.5.9.tgz", - "integrity": "sha512-NxOK0mrOCo0TWZ7Npc5HU66EKoRHlrtg18/ZixblLDWQMIqY9XCck8K1kJ8QYpYCHla+aHIsYUArFe2vhlEfZA==", + "version": "6.5.10", + "resolved": "https://registry.npmjs.org/@storybook/core-common/-/core-common-6.5.10.tgz", + "integrity": "sha512-Bx+VKkfWdrAmD8T51Sjq/mMhRaiapBHcpG4cU5bc3DMbg+LF2/yrgqv/cjVu+m5gHAzYCac5D7gqzBgvG7Myww==", "dev": true, "requires": { "@babel/core": "^7.12.10", @@ -31579,7 +31566,7 @@ "@babel/preset-react": "^7.12.10", "@babel/preset-typescript": "^7.12.7", "@babel/register": "^7.12.1", - "@storybook/node-logger": "6.5.9", + "@storybook/node-logger": "6.5.10", "@storybook/semver": "^7.3.2", "@types/node": "^14.0.10 || ^16.0.0", "@types/pretty-hrtime": "^1.0.0", @@ -31626,9 +31613,9 @@ } }, "@types/node": { - "version": "16.11.45", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.45.tgz", - "integrity": "sha512-3rKg/L5x0rofKuuUt5zlXzOnKyIHXmIu5R8A0TuNDMF2062/AOIDBciFIjToLEJ/9F9DzkHNot+BpNsMI1OLdQ==", + "version": "16.11.47", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.47.tgz", + "integrity": "sha512-fpP+jk2zJ4VW66+wAMFoBJlx1bxmBKx4DUFf68UHgdGCOuyUTDlLWqsaNPJh7xhNDykyJ9eIzAygilP/4WoN8g==", "dev": true }, "@webassemblyjs/ast": { @@ -31777,32 +31764,6 @@ "color-convert": "^2.0.1" } }, - "babel-plugin-macros": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", - "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", - "dev": true, - "requires": { - "@babel/runtime": "^7.12.5", - "cosmiconfig": "^7.0.0", - "resolve": "^1.19.0" - }, - "dependencies": { - "cosmiconfig": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.1.tgz", - "integrity": "sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ==", - "dev": true, - "requires": { - "@types/parse-json": "^4.0.0", - "import-fresh": "^3.2.1", - "parse-json": "^5.0.0", - "path-type": "^4.0.0", - "yaml": "^1.10.0" - } - } - } - }, "babel-plugin-polyfill-corejs3": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.1.7.tgz", @@ -31913,6 +31874,19 @@ "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "dev": true }, + "cosmiconfig": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz", + "integrity": "sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==", + "dev": true, + "requires": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.1.0", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.7.2" + } + }, "enhanced-resolve": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.5.0.tgz", @@ -32150,12 +32124,6 @@ "randombytes": "^2.1.0" } }, - "slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true - }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -32302,6 +32270,16 @@ } } }, + "webpack-sources": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz", + "integrity": "sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==", + "dev": true, + "requires": { + "source-list-map": "^2.0.0", + "source-map": "~0.6.1" + } + }, "y18n": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", @@ -32317,32 +32295,32 @@ } }, "@storybook/core-events": { - "version": "6.5.9", - "resolved": "https://registry.npmjs.org/@storybook/core-events/-/core-events-6.5.9.tgz", - "integrity": "sha512-tXt7a3ZvJOCeEKpNa/B5rQM5VI7UJLlOh3IHOImWn4HqoBRrZvbourmac+PRZAtXpos0h3c6554Hjapj/Sny5Q==", + "version": "6.5.10", + "resolved": "https://registry.npmjs.org/@storybook/core-events/-/core-events-6.5.10.tgz", + "integrity": "sha512-EVb1gO1172klVIAABLOoigFMx0V88uctY0K/qVCO8n6v+wd2+0Ccn63kl+gTxsAC3WZ8XhXh9q2w5ImHklVECw==", "dev": true, "requires": { "core-js": "^3.8.2" } }, "@storybook/core-server": { - "version": "6.5.9", - "resolved": "https://registry.npmjs.org/@storybook/core-server/-/core-server-6.5.9.tgz", - "integrity": "sha512-YeePGUrd5fQPvGzMhowh124KrcZURFpFXg1VB0Op3ESqCIsInoMZeObci4Gc+binMXC7vcv7aw3EwSLU37qJzQ==", + "version": "6.5.10", + "resolved": "https://registry.npmjs.org/@storybook/core-server/-/core-server-6.5.10.tgz", + "integrity": "sha512-jqwpA0ccA8X5ck4esWBid04+cEIVqirdAcqJeNb9IZAD+bRreO4Im8ilzr7jc5AmQ9fkqHs2NByFKh9TITp8NQ==", "dev": true, "requires": { "@discoveryjs/json-ext": "^0.5.3", - "@storybook/builder-webpack4": "6.5.9", - "@storybook/core-client": "6.5.9", - "@storybook/core-common": "6.5.9", - "@storybook/core-events": "6.5.9", + "@storybook/builder-webpack4": "6.5.10", + "@storybook/core-client": "6.5.10", + "@storybook/core-common": "6.5.10", + "@storybook/core-events": "6.5.10", "@storybook/csf": "0.0.2--canary.4566f4d.1", - "@storybook/csf-tools": "6.5.9", - "@storybook/manager-webpack4": "6.5.9", - "@storybook/node-logger": "6.5.9", + "@storybook/csf-tools": "6.5.10", + "@storybook/manager-webpack4": "6.5.10", + "@storybook/node-logger": "6.5.10", "@storybook/semver": "^7.3.2", - "@storybook/store": "6.5.9", - "@storybook/telemetry": "6.5.9", + "@storybook/store": "6.5.10", + "@storybook/telemetry": "6.5.10", "@types/node": "^14.0.10 || ^16.0.0", "@types/node-fetch": "^2.5.7", "@types/pretty-hrtime": "^1.0.0", @@ -32379,9 +32357,9 @@ }, "dependencies": { "@types/node": { - "version": "16.11.45", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.45.tgz", - "integrity": "sha512-3rKg/L5x0rofKuuUt5zlXzOnKyIHXmIu5R8A0TuNDMF2062/AOIDBciFIjToLEJ/9F9DzkHNot+BpNsMI1OLdQ==", + "version": "16.11.47", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.47.tgz", + "integrity": "sha512-fpP+jk2zJ4VW66+wAMFoBJlx1bxmBKx4DUFf68UHgdGCOuyUTDlLWqsaNPJh7xhNDykyJ9eIzAygilP/4WoN8g==", "dev": true }, "@webassemblyjs/ast": { @@ -32613,12 +32591,6 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, - "commander": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", - "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", - "dev": true - }, "enhanced-resolve": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.5.0.tgz", @@ -32831,12 +32803,6 @@ "randombytes": "^2.1.0" } }, - "slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true - }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -32967,6 +32933,16 @@ } } }, + "webpack-sources": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz", + "integrity": "sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==", + "dev": true, + "requires": { + "source-list-map": "^2.0.0", + "source-map": "~0.6.1" + } + }, "y18n": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", @@ -32991,9 +32967,9 @@ } }, "@storybook/csf-tools": { - "version": "6.5.9", - "resolved": "https://registry.npmjs.org/@storybook/csf-tools/-/csf-tools-6.5.9.tgz", - "integrity": "sha512-RAdhsO2XmEDyWy0qNQvdKMLeIZAuyfD+tYlUwBHRU6DbByDucvwgMOGy5dF97YNJFmyo93EUYJzXjUrJs3U1LQ==", + "version": "6.5.10", + "resolved": "https://registry.npmjs.org/@storybook/csf-tools/-/csf-tools-6.5.10.tgz", + "integrity": "sha512-H77kZQEisu7+skzeIbNZwmE09OqLjwJTeFhLN1pcjxKVa30LEI3pBHcNBxVKqgxl+Yg3KkB7W/ArLO2N+i2ohw==", "dev": true, "requires": { "@babel/core": "^7.12.10", @@ -33013,14 +32989,14 @@ } }, "@storybook/docs-tools": { - "version": "6.5.9", - "resolved": "https://registry.npmjs.org/@storybook/docs-tools/-/docs-tools-6.5.9.tgz", - "integrity": "sha512-UoTaXLvec8x+q+4oYIk/t8DBju9C3ZTGklqOxDIt+0kS3TFAqEgI3JhKXqQOXgN5zDcvLVSxi8dbVAeSxk2ktA==", + "version": "6.5.10", + "resolved": "https://registry.npmjs.org/@storybook/docs-tools/-/docs-tools-6.5.10.tgz", + "integrity": "sha512-/bvYgOO+CxMEcHifkjJg0A60OTGOhcjGxnsB1h0gJuxMrqA/7Qwc108bFmPiX0eiD1BovFkZLJV4O6OY7zP5Vw==", "dev": true, "requires": { "@babel/core": "^7.12.10", "@storybook/csf": "0.0.2--canary.4566f4d.1", - "@storybook/store": "6.5.9", + "@storybook/store": "6.5.10", "core-js": "^3.8.2", "doctrine": "^3.0.0", "lodash": "^4.17.21", @@ -33028,33 +33004,33 @@ } }, "@storybook/instrumenter": { - "version": "6.5.9", - "resolved": "https://registry.npmjs.org/@storybook/instrumenter/-/instrumenter-6.5.9.tgz", - "integrity": "sha512-I2nu/6H0MAy8d+d3LY/G6oYEFyWlc8f2Qs2DhpYh5FiCgIpzvY0DMN05Lf8oaXdKHL3lPF/YLJH17FttekXs1w==", + "version": "6.5.10", + "resolved": "https://registry.npmjs.org/@storybook/instrumenter/-/instrumenter-6.5.10.tgz", + "integrity": "sha512-3yKJW68wTnGYEts2mJQG6M7ZE+fe54fuy5lBBzRtvWnC15uWTxuaiFp2kxH5b+stSCi4m71ws45RNiEafdBgEQ==", "dev": true, "requires": { - "@storybook/addons": "6.5.9", - "@storybook/client-logger": "6.5.9", - "@storybook/core-events": "6.5.9", + "@storybook/addons": "6.5.10", + "@storybook/client-logger": "6.5.10", + "@storybook/core-events": "6.5.10", "core-js": "^3.8.2", "global": "^4.4.0" } }, "@storybook/manager-webpack4": { - "version": "6.5.9", - "resolved": "https://registry.npmjs.org/@storybook/manager-webpack4/-/manager-webpack4-6.5.9.tgz", - "integrity": "sha512-49LZlHqWc7zj9tQfOOANixPYmLxqWTTZceA6DSXnKd9xDiO2Gl23Y+l/CSPXNZGDB8QFAwpimwqyKJj/NLH45A==", + "version": "6.5.10", + "resolved": "https://registry.npmjs.org/@storybook/manager-webpack4/-/manager-webpack4-6.5.10.tgz", + "integrity": "sha512-N/TlNDhuhARuFipR/ZJ/xEVESz23iIbCsZ4VNehLHm8PpiGlQUehk+jMjWmz5XV0bJItwjRclY+CU3GjZKblfQ==", "dev": true, "requires": { "@babel/core": "^7.12.10", "@babel/plugin-transform-template-literals": "^7.12.1", "@babel/preset-react": "^7.12.10", - "@storybook/addons": "6.5.9", - "@storybook/core-client": "6.5.9", - "@storybook/core-common": "6.5.9", - "@storybook/node-logger": "6.5.9", - "@storybook/theming": "6.5.9", - "@storybook/ui": "6.5.9", + "@storybook/addons": "6.5.10", + "@storybook/core-client": "6.5.10", + "@storybook/core-common": "6.5.10", + "@storybook/node-logger": "6.5.10", + "@storybook/theming": "6.5.10", + "@storybook/ui": "6.5.10", "@types/node": "^14.0.10 || ^16.0.0", "@types/webpack": "^4.41.26", "babel-loader": "^8.0.0", @@ -33090,9 +33066,9 @@ "dev": true }, "@types/node": { - "version": "16.11.45", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.45.tgz", - "integrity": "sha512-3rKg/L5x0rofKuuUt5zlXzOnKyIHXmIu5R8A0TuNDMF2062/AOIDBciFIjToLEJ/9F9DzkHNot+BpNsMI1OLdQ==", + "version": "16.11.47", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.47.tgz", + "integrity": "sha512-fpP+jk2zJ4VW66+wAMFoBJlx1bxmBKx4DUFf68UHgdGCOuyUTDlLWqsaNPJh7xhNDykyJ9eIzAygilP/4WoN8g==", "dev": true }, "@webassemblyjs/ast": { @@ -33339,6 +33315,12 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true + }, "enhanced-resolve": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.5.0.tgz", @@ -33745,6 +33727,16 @@ } } }, + "webpack-sources": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz", + "integrity": "sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==", + "dev": true, + "requires": { + "source-list-map": "^2.0.0", + "source-map": "~0.6.1" + } + }, "y18n": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", @@ -33760,20 +33752,20 @@ } }, "@storybook/manager-webpack5": { - "version": "6.5.9", - "resolved": "https://registry.npmjs.org/@storybook/manager-webpack5/-/manager-webpack5-6.5.9.tgz", - "integrity": "sha512-J1GamphSsaZLNBEhn1awgxzOS8KfvzrHtVlAm2VHwW7j1E1DItROFJhGCgduYYuBiN9eqm+KIYrxcr6cRuoolQ==", + "version": "6.5.10", + "resolved": "https://registry.npmjs.org/@storybook/manager-webpack5/-/manager-webpack5-6.5.10.tgz", + "integrity": "sha512-uRo+6e5MiVOtyFVMYIKVqvpDveCjHyzXBfetSYR7rKEZoaDMEnLLiuF7DIH12lzxwmzCJ1gIc4lf5HFiTMNkgw==", "dev": true, "requires": { "@babel/core": "^7.12.10", "@babel/plugin-transform-template-literals": "^7.12.1", "@babel/preset-react": "^7.12.10", - "@storybook/addons": "6.5.9", - "@storybook/core-client": "6.5.9", - "@storybook/core-common": "6.5.9", - "@storybook/node-logger": "6.5.9", - "@storybook/theming": "6.5.9", - "@storybook/ui": "6.5.9", + "@storybook/addons": "6.5.10", + "@storybook/core-client": "6.5.10", + "@storybook/core-common": "6.5.10", + "@storybook/node-logger": "6.5.10", + "@storybook/theming": "6.5.10", + "@storybook/ui": "6.5.10", "@types/node": "^14.0.10 || ^16.0.0", "babel-loader": "^8.0.0", "case-sensitive-paths-webpack-plugin": "^2.3.0", @@ -33800,9 +33792,9 @@ }, "dependencies": { "@types/node": { - "version": "16.11.45", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.45.tgz", - "integrity": "sha512-3rKg/L5x0rofKuuUt5zlXzOnKyIHXmIu5R8A0TuNDMF2062/AOIDBciFIjToLEJ/9F9DzkHNot+BpNsMI1OLdQ==", + "version": "16.11.47", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.47.tgz", + "integrity": "sha512-fpP+jk2zJ4VW66+wAMFoBJlx1bxmBKx4DUFf68UHgdGCOuyUTDlLWqsaNPJh7xhNDykyJ9eIzAygilP/4WoN8g==", "dev": true }, "ansi-styles": { @@ -33905,9 +33897,9 @@ "dev": true }, "postcss": { - "version": "8.4.14", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.14.tgz", - "integrity": "sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig==", + "version": "8.4.16", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.16.tgz", + "integrity": "sha512-ipHE1XBvKzm5xI7hiHCZJCSugxvsdq2mPnsq5+UF+VHCjiBvtDrlxJfMBToWaP9D5XlgNmcFGqoHmUn0EYEaRQ==", "dev": true, "requires": { "nanoid": "^3.3.4", @@ -34051,9 +34043,9 @@ } }, "@storybook/node-logger": { - "version": "6.5.9", - "resolved": "https://registry.npmjs.org/@storybook/node-logger/-/node-logger-6.5.9.tgz", - "integrity": "sha512-nZZNZG2Wtwv6Trxi3FrnIqUmB55xO+X/WQGPT5iKlqNjdRIu/T72mE7addcp4rbuWCQfZUhcDDGpBOwKtBxaGg==", + "version": "6.5.10", + "resolved": "https://registry.npmjs.org/@storybook/node-logger/-/node-logger-6.5.10.tgz", + "integrity": "sha512-bYswXIKV7Stru8vYfkjUMNN8UhF7Qg7NRsUvG5Djt5lLIae1XmUIgnH40mU/nW4X4BSfcR9MKxsSsngvn2WmQg==", "dev": true, "requires": { "@types/npmlog": "^4.1.2", @@ -34115,26 +34107,26 @@ } }, "@storybook/postinstall": { - "version": "6.5.9", - "resolved": "https://registry.npmjs.org/@storybook/postinstall/-/postinstall-6.5.9.tgz", - "integrity": "sha512-KQBupK+FMRrtSt8IL0MzCZ/w9qbd25Yxxp/+ajfWgZTRgsWgVFOqcDyMhS16eNbBp5qKIBCBDXfEF+/mK8HwQQ==", + "version": "6.5.10", + "resolved": "https://registry.npmjs.org/@storybook/postinstall/-/postinstall-6.5.10.tgz", + "integrity": "sha512-xqUdpnFHYkn8MgtV+QztvIsRWa6jQUk7QT1Mu17Y0S7PbslNGsuskRPHenHhACXBJF+TM86R+4BaAhnVYTmElw==", "dev": true, "requires": { "core-js": "^3.8.2" } }, "@storybook/preview-web": { - "version": "6.5.9", - "resolved": "https://registry.npmjs.org/@storybook/preview-web/-/preview-web-6.5.9.tgz", - "integrity": "sha512-4eMrO2HJyZUYyL/j+gUaDvry6iGedshwT5MQqe7J9FaA+Q2pNARQRB1X53f410w7S4sObRmYIAIluWPYdWym9w==", + "version": "6.5.10", + "resolved": "https://registry.npmjs.org/@storybook/preview-web/-/preview-web-6.5.10.tgz", + "integrity": "sha512-sTC/o5gkvALOtcNgtApGKGN9EavvSxRHBeBh+5BQjV2qQ8ap+26RsfUizNBECAa2Jrn4osaDYn9HRhJLFL69WA==", "dev": true, "requires": { - "@storybook/addons": "6.5.9", - "@storybook/channel-postmessage": "6.5.9", - "@storybook/client-logger": "6.5.9", - "@storybook/core-events": "6.5.9", + "@storybook/addons": "6.5.10", + "@storybook/channel-postmessage": "6.5.10", + "@storybook/client-logger": "6.5.10", + "@storybook/core-events": "6.5.10", "@storybook/csf": "0.0.2--canary.4566f4d.1", - "@storybook/store": "6.5.9", + "@storybook/store": "6.5.10", "ansi-to-html": "^0.6.11", "core-js": "^3.8.2", "global": "^4.4.0", @@ -34148,24 +34140,24 @@ } }, "@storybook/react": { - "version": "6.5.9", - "resolved": "https://registry.npmjs.org/@storybook/react/-/react-6.5.9.tgz", - "integrity": "sha512-Rp+QaTQAzxJhwuzJXVd49mnIBLQRlF8llTxPT2YoGHdrGkku/zl/HblQ6H2yzEf15367VyzaAv/BpLsO9Jlfxg==", + "version": "6.5.10", + "resolved": "https://registry.npmjs.org/@storybook/react/-/react-6.5.10.tgz", + "integrity": "sha512-m8S1qQrwA7pDGwdKEvL6LV3YKvSzVUY297Fq+xcTU3irnAy4sHDuFoLqV6Mi1510mErK1r8+rf+0R5rEXB219g==", "dev": true, "requires": { "@babel/preset-flow": "^7.12.1", "@babel/preset-react": "^7.12.10", "@pmmmwh/react-refresh-webpack-plugin": "^0.5.3", - "@storybook/addons": "6.5.9", - "@storybook/client-logger": "6.5.9", - "@storybook/core": "6.5.9", - "@storybook/core-common": "6.5.9", + "@storybook/addons": "6.5.10", + "@storybook/client-logger": "6.5.10", + "@storybook/core": "6.5.10", + "@storybook/core-common": "6.5.10", "@storybook/csf": "0.0.2--canary.4566f4d.1", - "@storybook/docs-tools": "6.5.9", - "@storybook/node-logger": "6.5.9", + "@storybook/docs-tools": "6.5.10", + "@storybook/node-logger": "6.5.10", "@storybook/react-docgen-typescript-plugin": "1.0.2-canary.6.9d540b91e815f8fc2f8829189deb00553559ff63.0", "@storybook/semver": "^7.3.2", - "@storybook/store": "6.5.9", + "@storybook/store": "6.5.10", "@types/estree": "^0.0.51", "@types/node": "^14.14.20 || ^16.0.0", "@types/webpack-env": "^1.16.0", @@ -34191,9 +34183,9 @@ }, "dependencies": { "@types/node": { - "version": "16.11.45", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.45.tgz", - "integrity": "sha512-3rKg/L5x0rofKuuUt5zlXzOnKyIHXmIu5R8A0TuNDMF2062/AOIDBciFIjToLEJ/9F9DzkHNot+BpNsMI1OLdQ==", + "version": "16.11.47", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.47.tgz", + "integrity": "sha512-fpP+jk2zJ4VW66+wAMFoBJlx1bxmBKx4DUFf68UHgdGCOuyUTDlLWqsaNPJh7xhNDykyJ9eIzAygilP/4WoN8g==", "dev": true }, "is-plain-object": { @@ -34305,12 +34297,12 @@ } }, "@storybook/router": { - "version": "6.5.9", - "resolved": "https://registry.npmjs.org/@storybook/router/-/router-6.5.9.tgz", - "integrity": "sha512-G2Xp/2r8vU2O34eelE+G5VbEEVFDeHcCURrVJEROh6dq2asFJAPbzslVXSeCqgOTNLSpRDJ2NcN5BckkNqmqJg==", + "version": "6.5.10", + "resolved": "https://registry.npmjs.org/@storybook/router/-/router-6.5.10.tgz", + "integrity": "sha512-O+vNW/eEpYFF8eCg5jZjNQ6q2DKQVxqDRPCy9pJdEbvavMDZn6AFYgVK+VJe5F4211WW2yncOu922xObCxXJYg==", "dev": true, "requires": { - "@storybook/client-logger": "6.5.9", + "@storybook/client-logger": "6.5.10", "core-js": "^3.8.2", "memoizerific": "^1.11.3", "qs": "^6.10.0", @@ -34367,13 +34359,13 @@ } }, "@storybook/source-loader": { - "version": "6.5.9", - "resolved": "https://registry.npmjs.org/@storybook/source-loader/-/source-loader-6.5.9.tgz", - "integrity": "sha512-H03nFKaP6borfWMTTa9igBA+Jm2ph+FoVJImWC/X+LAmLSJYYSXuqSgmiZ/DZvbjxS4k8vccE2HXogne1IvaRA==", + "version": "6.5.10", + "resolved": "https://registry.npmjs.org/@storybook/source-loader/-/source-loader-6.5.10.tgz", + "integrity": "sha512-1RxxRumpjs8VUUwES9LId+cuNQnixhZAcwCxd6jaKkTZbjiQCtAhXX6DBTjJGV1u/JnCsqEp5b1wB8j/EioNHw==", "dev": true, "requires": { - "@storybook/addons": "6.5.9", - "@storybook/client-logger": "6.5.9", + "@storybook/addons": "6.5.10", + "@storybook/client-logger": "6.5.10", "@storybook/csf": "0.0.2--canary.4566f4d.1", "core-js": "^3.8.2", "estraverse": "^5.2.0", @@ -34393,14 +34385,14 @@ } }, "@storybook/store": { - "version": "6.5.9", - "resolved": "https://registry.npmjs.org/@storybook/store/-/store-6.5.9.tgz", - "integrity": "sha512-80pcDTcCwK6wUA63aWOp13urI77jfipIVee9mpVvbNyfrNN8kGv1BS0z/JHDxuV6rC4g7LG1fb+BurR0yki7BA==", + "version": "6.5.10", + "resolved": "https://registry.npmjs.org/@storybook/store/-/store-6.5.10.tgz", + "integrity": "sha512-RswrSYh2IiKkytFPxP9AvP+hekjrvHK2ILvyDk2ZgduCN4n5ivsekOb+N3M2t+dq1eLuW9or5n2T4OWwAwjxxQ==", "dev": true, "requires": { - "@storybook/addons": "6.5.9", - "@storybook/client-logger": "6.5.9", - "@storybook/core-events": "6.5.9", + "@storybook/addons": "6.5.10", + "@storybook/client-logger": "6.5.10", + "@storybook/core-events": "6.5.10", "@storybook/csf": "0.0.2--canary.4566f4d.1", "core-js": "^3.8.2", "fast-deep-equal": "^3.1.3", @@ -34413,24 +34405,16 @@ "synchronous-promise": "^2.0.15", "ts-dedent": "^2.0.0", "util-deprecate": "^1.0.2" - }, - "dependencies": { - "slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true - } } }, "@storybook/telemetry": { - "version": "6.5.9", - "resolved": "https://registry.npmjs.org/@storybook/telemetry/-/telemetry-6.5.9.tgz", - "integrity": "sha512-JluoHCRhHAr4X0eUNVBSBi1JIBA92404Tu1TPdbN7x6gCZxHXXPTSUTAnspXp/21cTdMhY2x+kfZQ8fmlGK4MQ==", + "version": "6.5.10", + "resolved": "https://registry.npmjs.org/@storybook/telemetry/-/telemetry-6.5.10.tgz", + "integrity": "sha512-+M5HILDFS8nDumLxeSeAwi1MTzIuV6UWzV4yB2wcsEXOBTdplcl9oYqFKtlst78oOIdGtpPYxYfivDlqxC2K4g==", "dev": true, "requires": { - "@storybook/client-logger": "6.5.9", - "@storybook/core-common": "6.5.9", + "@storybook/client-logger": "6.5.10", + "@storybook/core-common": "6.5.10", "chalk": "^4.1.0", "core-js": "^3.8.2", "detect-package-manager": "^2.0.1", @@ -34508,32 +34492,32 @@ } }, "@storybook/theming": { - "version": "6.5.9", - "resolved": "https://registry.npmjs.org/@storybook/theming/-/theming-6.5.9.tgz", - "integrity": "sha512-KM0AMP5jMQPAdaO8tlbFCYqx9uYM/hZXGSVUhznhLYu7bhNAIK7ZVmXxyE/z/khM++8eUHzRoZGiO/cwCkg9Xw==", + "version": "6.5.10", + "resolved": "https://registry.npmjs.org/@storybook/theming/-/theming-6.5.10.tgz", + "integrity": "sha512-BvTQBBcSEwKKcsVmF+Ol6v0RIQUr+bxP7gb10wtfBd23mZTEFA0C1N5FnZr/dDeiBKG1pvf1UKvoYA731y0BsA==", "dev": true, "requires": { - "@storybook/client-logger": "6.5.9", + "@storybook/client-logger": "6.5.10", "core-js": "^3.8.2", "memoizerific": "^1.11.3", "regenerator-runtime": "^0.13.7" } }, "@storybook/ui": { - "version": "6.5.9", - "resolved": "https://registry.npmjs.org/@storybook/ui/-/ui-6.5.9.tgz", - "integrity": "sha512-ryuPxJgtbb0gPXKGgGAUC+Z185xGAd1IvQ0jM5fJ0SisHXI8jteG3RaWhntOehi9qCg+64Vv6eH/cj9QYNHt1Q==", - "dev": true, - "requires": { - "@storybook/addons": "6.5.9", - "@storybook/api": "6.5.9", - "@storybook/channels": "6.5.9", - "@storybook/client-logger": "6.5.9", - "@storybook/components": "6.5.9", - "@storybook/core-events": "6.5.9", - "@storybook/router": "6.5.9", + "version": "6.5.10", + "resolved": "https://registry.npmjs.org/@storybook/ui/-/ui-6.5.10.tgz", + "integrity": "sha512-6iaoaRAiTqB1inTw35vao+5hjcDE0Qa0A3a9ZIeNa6yHvpB1k0lO/N/0PMrRdVvySYpXVD1iry4z4QYdo1rU+w==", + "dev": true, + "requires": { + "@storybook/addons": "6.5.10", + "@storybook/api": "6.5.10", + "@storybook/channels": "6.5.10", + "@storybook/client-logger": "6.5.10", + "@storybook/components": "6.5.10", + "@storybook/core-events": "6.5.10", + "@storybook/router": "6.5.10", "@storybook/semver": "^7.3.2", - "@storybook/theming": "6.5.9", + "@storybook/theming": "6.5.10", "core-js": "^3.8.2", "memoizerific": "^1.11.3", "qs": "^6.10.0", @@ -34552,9 +34536,9 @@ } }, "@testing-library/dom": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-8.16.0.tgz", - "integrity": "sha512-uxF4zmnLHHDlmW4l+0WDjcgLVwCvH+OVLpD8Dfp+Bjfz85prwxWGbwXgJdLtkgjD0qfOzkJF9SmA6YZPsMYX4w==", + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-8.17.1.tgz", + "integrity": "sha512-KnH2MnJUzmFNPW6RIKfd+zf2Wue8mEKX0M3cpX6aKl5ZXrJM1/c/Pc8c2xDNYQCnJO48Sm5ITbMXgqTr3h4jxQ==", "dev": true, "requires": { "@babel/code-frame": "^7.10.4", @@ -34810,9 +34794,9 @@ } }, "@types/express-serve-static-core": { - "version": "4.17.29", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.29.tgz", - "integrity": "sha512-uMd++6dMKS32EOuw1Uli3e3BPgdLIXmezcfHv7N4c1s3gkhikBplORPpMq3fuWkxncZN1reb16d5n8yhQ80x7Q==", + "version": "4.17.30", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.30.tgz", + "integrity": "sha512-gstzbTWro2/nFed1WXtf+TtrpwxH7Ggs4RLYTLbeVgIkUQOI3WG/JKjgeOU1zXDvezllupjrf8OPIdvTbIaVOQ==", "dev": true, "requires": { "@types/node": "*", @@ -34933,9 +34917,9 @@ } }, "@types/mime": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", - "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz", + "integrity": "sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==", "dev": true }, "@types/minimatch": { @@ -34945,9 +34929,9 @@ "dev": true }, "@types/node": { - "version": "18.0.6", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.0.6.tgz", - "integrity": "sha512-/xUq6H2aQm261exT6iZTMifUySEt4GR5KX8eYyY+C4MSNPqSh9oNIP7tz2GLKTlFaiBbgZNxffoR3CVRG+cljw==", + "version": "18.7.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.7.1.tgz", + "integrity": "sha512-GKX1Qnqxo4S+Z/+Z8KKPLpH282LD7jLHWJcVryOflnsnH+BtSDfieR6ObwBMwpnNws0bUK8GI7z0unQf9bARNQ==", "dev": true }, "@types/node-fetch": { @@ -35008,9 +34992,9 @@ "dev": true }, "@types/react": { - "version": "18.0.15", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.15.tgz", - "integrity": "sha512-iz3BtLuIYH1uWdsv6wXYdhozhqj20oD4/Hk2DNXIn1kFsmp9x8d9QB6FnPhfkbhd2PgEONt9Q1x/ebkwjfFLow==", + "version": "18.0.17", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.17.tgz", + "integrity": "sha512-38ETy4tL+rn4uQQi7mB81G7V1g0u2ryquNmsVIOKUAEIDK+3CUjZ6rSRpdvS99dNBnkLFL83qfmtLacGOTIhwQ==", "dev": true, "requires": { "@types/prop-types": "*", @@ -35027,15 +35011,6 @@ "@types/react": "*" } }, - "@types/react-syntax-highlighter": { - "version": "11.0.5", - "resolved": "https://registry.npmjs.org/@types/react-syntax-highlighter/-/react-syntax-highlighter-11.0.5.tgz", - "integrity": "sha512-VIOi9i2Oj5XsmWWoB72p3KlZoEbdRAcechJa8Ztebw7bDl2YmR+odxIqhtJGp1q2EozHs02US+gzxJ9nuf56qg==", - "dev": true, - "requires": { - "@types/react": "*" - } - }, "@types/retry": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", @@ -35058,12 +35033,12 @@ } }, "@types/serve-static": { - "version": "1.13.10", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.10.tgz", - "integrity": "sha512-nCkHGI4w7ZgAdNkrEu0bv+4xNV/XDqW+DydknebMOQwkpDGx8G+HTlj7R7ABI8i8nKxVw0wtKPi1D+lPOkh4YQ==", + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-z5xyF6uh8CbjAu9760KDKsH2FcDxZ2tFCsA4HIMWE6IkiYMXfVoa+4f9KX+FN0ZLsaMw1WNG2ETLA6N+/YA+cg==", "dev": true, "requires": { - "@types/mime": "^1", + "@types/mime": "*", "@types/node": "*" } }, @@ -35220,14 +35195,14 @@ } }, "@typescript-eslint/eslint-plugin": { - "version": "5.30.7", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.30.7.tgz", - "integrity": "sha512-l4L6Do+tfeM2OK0GJsU7TUcM/1oN/N25xHm3Jb4z3OiDU4Lj8dIuxX9LpVMS9riSXQs42D1ieX7b85/r16H9Fw==", + "version": "5.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.33.0.tgz", + "integrity": "sha512-jHvZNSW2WZ31OPJ3enhLrEKvAZNyAFWZ6rx9tUwaessTc4sx9KmgMNhVcqVAl1ETnT5rU5fpXTLmY9YvC1DCNg==", "dev": true, "requires": { - "@typescript-eslint/scope-manager": "5.30.7", - "@typescript-eslint/type-utils": "5.30.7", - "@typescript-eslint/utils": "5.30.7", + "@typescript-eslint/scope-manager": "5.33.0", + "@typescript-eslint/type-utils": "5.33.0", + "@typescript-eslint/utils": "5.33.0", "debug": "^4.3.4", "functional-red-black-tree": "^1.0.1", "ignore": "^5.2.0", @@ -35248,61 +35223,61 @@ } }, "@typescript-eslint/experimental-utils": { - "version": "5.30.7", - "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-5.30.7.tgz", - "integrity": "sha512-r218ZVL0zFBYzEq8/9K2ZhRgsmKUhm8xd3sWChgvTbmP98kHGuY83IUl64SS9fx9OSBM9vMLdzBfox4eDdm/ZQ==", + "version": "5.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-5.33.0.tgz", + "integrity": "sha512-NvRsNe+T90QrSVlgdV9/U8/chfqGmShvKUE7hWZTAUUCF6hZty/R+eMPVGldKcUDq7uRQaK6+V8gv5OwVDqC+g==", "dev": true, "requires": { - "@typescript-eslint/utils": "5.30.7" + "@typescript-eslint/utils": "5.33.0" } }, "@typescript-eslint/parser": { - "version": "5.30.7", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.30.7.tgz", - "integrity": "sha512-Rg5xwznHWWSy7v2o0cdho6n+xLhK2gntImp0rJroVVFkcYFYQ8C8UJTSuTw/3CnExBmPjycjmUJkxVmjXsld6A==", + "version": "5.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.33.0.tgz", + "integrity": "sha512-cgM5cJrWmrDV2KpvlcSkelTBASAs1mgqq+IUGKJvFxWrapHpaRy5EXPQz9YaKF3nZ8KY18ILTiVpUtbIac86/w==", "dev": true, "requires": { - "@typescript-eslint/scope-manager": "5.30.7", - "@typescript-eslint/types": "5.30.7", - "@typescript-eslint/typescript-estree": "5.30.7", + "@typescript-eslint/scope-manager": "5.33.0", + "@typescript-eslint/types": "5.33.0", + "@typescript-eslint/typescript-estree": "5.33.0", "debug": "^4.3.4" } }, "@typescript-eslint/scope-manager": { - "version": "5.30.7", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.30.7.tgz", - "integrity": "sha512-7BM1bwvdF1UUvt+b9smhqdc/eniOnCKxQT/kj3oXtj3LqnTWCAM0qHRHfyzCzhEfWX0zrW7KqXXeE4DlchZBKw==", + "version": "5.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.33.0.tgz", + "integrity": "sha512-/Jta8yMNpXYpRDl8EwF/M8It2A9sFJTubDo0ATZefGXmOqlaBffEw0ZbkbQ7TNDK6q55NPHFshGBPAZvZkE8Pw==", "dev": true, "requires": { - "@typescript-eslint/types": "5.30.7", - "@typescript-eslint/visitor-keys": "5.30.7" + "@typescript-eslint/types": "5.33.0", + "@typescript-eslint/visitor-keys": "5.33.0" } }, "@typescript-eslint/type-utils": { - "version": "5.30.7", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.30.7.tgz", - "integrity": "sha512-nD5qAE2aJX/YLyKMvOU5jvJyku4QN5XBVsoTynFrjQZaDgDV6i7QHFiYCx10wvn7hFvfuqIRNBtsgaLe0DbWhw==", + "version": "5.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.33.0.tgz", + "integrity": "sha512-2zB8uEn7hEH2pBeyk3NpzX1p3lF9dKrEbnXq1F7YkpZ6hlyqb2yZujqgRGqXgRBTHWIUG3NGx/WeZk224UKlIA==", "dev": true, "requires": { - "@typescript-eslint/utils": "5.30.7", + "@typescript-eslint/utils": "5.33.0", "debug": "^4.3.4", "tsutils": "^3.21.0" } }, "@typescript-eslint/types": { - "version": "5.30.7", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.30.7.tgz", - "integrity": "sha512-ocVkETUs82+U+HowkovV6uxf1AnVRKCmDRNUBUUo46/5SQv1owC/EBFkiu4MOHeZqhKz2ktZ3kvJJ1uFqQ8QPg==", + "version": "5.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.33.0.tgz", + "integrity": "sha512-nIMt96JngB4MYFYXpZ/3ZNU4GWPNdBbcB5w2rDOCpXOVUkhtNlG2mmm8uXhubhidRZdwMaMBap7Uk8SZMU/ppw==", "dev": true }, "@typescript-eslint/typescript-estree": { - "version": "5.30.7", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.30.7.tgz", - "integrity": "sha512-tNslqXI1ZdmXXrHER83TJ8OTYl4epUzJC0aj2i4DMDT4iU+UqLT3EJeGQvJ17BMbm31x5scSwo3hPM0nqQ1AEA==", + "version": "5.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.33.0.tgz", + "integrity": "sha512-tqq3MRLlggkJKJUrzM6wltk8NckKyyorCSGMq4eVkyL5sDYzJJcMgZATqmF8fLdsWrW7OjjIZ1m9v81vKcaqwQ==", "dev": true, "requires": { - "@typescript-eslint/types": "5.30.7", - "@typescript-eslint/visitor-keys": "5.30.7", + "@typescript-eslint/types": "5.33.0", + "@typescript-eslint/visitor-keys": "5.33.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -35322,26 +35297,26 @@ } }, "@typescript-eslint/utils": { - "version": "5.30.7", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.30.7.tgz", - "integrity": "sha512-Z3pHdbFw+ftZiGUnm1GZhkJgVqsDL5CYW2yj+TB2mfXDFOMqtbzQi2dNJIyPqPbx9mv2kUxS1gU+r2gKlKi1rQ==", + "version": "5.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.33.0.tgz", + "integrity": "sha512-JxOAnXt9oZjXLIiXb5ZIcZXiwVHCkqZgof0O8KPgz7C7y0HS42gi75PdPlqh1Tf109M0fyUw45Ao6JLo7S5AHw==", "dev": true, "requires": { "@types/json-schema": "^7.0.9", - "@typescript-eslint/scope-manager": "5.30.7", - "@typescript-eslint/types": "5.30.7", - "@typescript-eslint/typescript-estree": "5.30.7", + "@typescript-eslint/scope-manager": "5.33.0", + "@typescript-eslint/types": "5.33.0", + "@typescript-eslint/typescript-estree": "5.33.0", "eslint-scope": "^5.1.1", "eslint-utils": "^3.0.0" } }, "@typescript-eslint/visitor-keys": { - "version": "5.30.7", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.30.7.tgz", - "integrity": "sha512-KrRXf8nnjvcpxDFOKej4xkD7657+PClJs5cJVSG7NNoCNnjEdc46juNAQt7AyuWctuCgs6mVRc1xGctEqrjxWw==", + "version": "5.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.33.0.tgz", + "integrity": "sha512-/XsqCzD4t+Y9p5wd9HZiptuGKBlaZO5showwqODii5C0nZawxWLF+Q6k5wYHBrQv96h6GYKyqqMHCSTqta8Kiw==", "dev": true, "requires": { - "@typescript-eslint/types": "5.30.7", + "@typescript-eslint/types": "5.33.0", "eslint-visitor-keys": "^3.3.0" } }, @@ -35785,9 +35760,9 @@ } }, "ansi-colors": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.4.tgz", - "integrity": "sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", "dev": true }, "ansi-escapes": { @@ -36341,13 +36316,13 @@ } }, "babel-plugin-macros": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-2.8.0.tgz", - "integrity": "sha512-SEP5kJpfGYqYKpBrj5XU3ahw5p5GOHJ0U5ssOSQ/WBVdwkD2Dzlce95exQTs3jOVWPPKLBN2rlEWkCK7dSmLvg==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", + "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", "requires": { - "@babel/runtime": "^7.7.2", - "cosmiconfig": "^6.0.0", - "resolve": "^1.12.0" + "@babel/runtime": "^7.12.5", + "cosmiconfig": "^7.0.0", + "resolve": "^1.19.0" } }, "babel-plugin-named-exports-order": { @@ -36357,33 +36332,33 @@ "dev": true }, "babel-plugin-polyfill-corejs2": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.3.1.tgz", - "integrity": "sha512-v7/T6EQcNfVLfcN2X8Lulb7DjprieyLWJK/zOWH5DUYcAgex9sP3h25Q+DLsX9TloXe3y1O8l2q2Jv9q8UVB9w==", + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.3.2.tgz", + "integrity": "sha512-LPnodUl3lS0/4wN3Rb+m+UK8s7lj2jcLRrjho4gLw+OJs+I4bvGXshINesY5xx/apM+biTnQ9reDI8yj+0M5+Q==", "dev": true, "requires": { - "@babel/compat-data": "^7.13.11", - "@babel/helper-define-polyfill-provider": "^0.3.1", + "@babel/compat-data": "^7.17.7", + "@babel/helper-define-polyfill-provider": "^0.3.2", "semver": "^6.1.1" } }, "babel-plugin-polyfill-corejs3": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.5.2.tgz", - "integrity": "sha512-G3uJih0XWiID451fpeFaYGVuxHEjzKTHtc9uGFEjR6hHrvNzeS/PX+LLLcetJcytsB5m4j+K3o/EpXJNb/5IEQ==", + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.5.3.tgz", + "integrity": "sha512-zKsXDh0XjnrUEW0mxIHLfjBfnXSMr5Q/goMe/fxpQnLm07mcOZiIZHBNWCMx60HmdvjxfXcalac0tfFg0wqxyw==", "dev": true, "requires": { - "@babel/helper-define-polyfill-provider": "^0.3.1", + "@babel/helper-define-polyfill-provider": "^0.3.2", "core-js-compat": "^3.21.0" } }, "babel-plugin-polyfill-regenerator": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.3.1.tgz", - "integrity": "sha512-Y2B06tvgHYt1x0yz17jGkGeeMr5FeKUu+ASJ+N6nB5lQ8Dapfg42i0OVrf8PNGJ3zKL4A23snMi1IRwrqqND7A==", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.4.0.tgz", + "integrity": "sha512-RW1cnryiADFeHmfLS+WW/G431p1PsW5qdRdz0SDRi7TKcUgc7Oh/uXkT7MZ/+tGsT1BkczEAmD5XjUyJ5SWDTw==", "dev": true, "requires": { - "@babel/helper-define-polyfill-provider": "^0.3.1" + "@babel/helper-define-polyfill-provider": "^0.3.2" } }, "babel-plugin-react-docgen": { @@ -36830,14 +36805,14 @@ } }, "browserslist": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.2.tgz", - "integrity": "sha512-MonuOgAtUB46uP5CezYbRaYKBNt2LxP0yX+Pmj4LkcDFGkn9Cbpi83d9sCjwQDErXsIJSzY5oKGDbgOlF/LPAA==", + "version": "4.21.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.3.tgz", + "integrity": "sha512-898rgRXLAyRkM1GryrrBHGkqA5hlpkV5MhtZwg9QXeiyLUYs2k00Un05aX5l2/yJIOObYKOpS2JNo8nJDE7fWQ==", "requires": { - "caniuse-lite": "^1.0.30001366", - "electron-to-chromium": "^1.4.188", + "caniuse-lite": "^1.0.30001370", + "electron-to-chromium": "^1.4.202", "node-releases": "^2.0.6", - "update-browserslist-db": "^1.0.4" + "update-browserslist-db": "^1.0.5" } }, "bser": { @@ -36890,9 +36865,9 @@ "dev": true }, "c8": { - "version": "7.11.3", - "resolved": "https://registry.npmjs.org/c8/-/c8-7.11.3.tgz", - "integrity": "sha512-6YBmsaNmqRm9OS3ZbIiL2EZgi1+Xc4O24jL3vMYGE6idixYuGdy76rIfIdltSKDj9DpLNrcXSonUTR1miBD0wA==", + "version": "7.12.0", + "resolved": "https://registry.npmjs.org/c8/-/c8-7.12.0.tgz", + "integrity": "sha512-CtgQrHOkyxr5koX1wEUmN/5cfDa2ckbHRA4Gy5LAL0zaCFtVWJS5++n+w4/sr2GWGerBxgTjpKeDclk/Qk6W/A==", "dev": true, "requires": { "@bcoe/v8-coverage": "^0.2.3", @@ -37068,9 +37043,9 @@ } }, "caniuse-lite": { - "version": "1.0.30001367", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001367.tgz", - "integrity": "sha512-XDgbeOHfifWV3GEES2B8rtsrADx4Jf+juKX2SICJcaUhjYBO3bR96kvEIHa15VU6ohtOhBZuPGGYGbXMRn0NCw==" + "version": "1.0.30001375", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001375.tgz", + "integrity": "sha512-kWIMkNzLYxSvnjy0hL8w1NOaWNr2rn39RTAVyIwcw8juu60bZDWiF1/loOYANzjtJmy6qPgNmn38ro5Pygagdw==" }, "capture-exit": { "version": "2.0.0", @@ -37160,6 +37135,17 @@ "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" + }, + "dependencies": { + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + } } }, "chownr": { @@ -37175,9 +37161,9 @@ "dev": true }, "ci-info": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", - "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.3.2.tgz", + "integrity": "sha512-xmDt/QIAdeZ9+nfdPsaBCpMvHNLFiLdjj59qjqn+6iPe6YmHGQ35sBnQ8uslRBXFmXkiZQOJRjvQeoGppoTjjg==", "dev": true }, "cipher-base": { @@ -37324,9 +37310,9 @@ } }, "cli-spinners": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.6.1.tgz", - "integrity": "sha512-x/5fWmGMnbKQAaNwN+UZlV79qBLM9JFnJuJ03gIi5whrob0xV0ofNVHy9DhwGdsMJQc2OKv0oGmLzvaqvAVv+g==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.7.0.tgz", + "integrity": "sha512-qu3pN8Y3qHNgE2AFweciB1IfMnmZ/fsNTEE+NOFjmGB2F/7rLhnhzppvpCnN4FovtP26k8lHyy9ptEbNwWFLzw==", "dev": true }, "cli-table3": { @@ -37445,9 +37431,9 @@ "dev": true }, "commander": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", - "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", + "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", "dev": true }, "common-path-prefix": { @@ -37669,18 +37655,18 @@ "dev": true }, "core-js": { - "version": "3.23.5", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.23.5.tgz", - "integrity": "sha512-7Vh11tujtAZy82da4duVreQysIoO2EvVrur7y6IzZkH1IHPSekuDi8Vuw1+YKjkbfWLRD7Nc9ICQ/sIUDutcyg==", + "version": "3.24.1", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.24.1.tgz", + "integrity": "sha512-0QTBSYSUZ6Gq21utGzkfITDylE8jWC9Ne1D2MrhvlsZBI1x39OdDIVbzSqtgMndIy6BlHxBXpMGqzZmnztg2rg==", "dev": true }, "core-js-compat": { - "version": "3.23.5", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.23.5.tgz", - "integrity": "sha512-fHYozIFIxd+91IIbXJgWd/igXIc8Mf9is0fusswjnGIWVG96y2cwyUdlCkGOw6rMLHKAxg7xtCIVaHsyOUnJIg==", + "version": "3.24.1", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.24.1.tgz", + "integrity": "sha512-XhdNAGeRnTpp8xbD+sR/HFDK9CbeeeqXT6TuofXh3urqEevzkWmLRgrVoykodsw8okqo2pu1BOmuCKrHx63zdw==", "dev": true, "requires": { - "browserslist": "^4.21.2", + "browserslist": "^4.21.3", "semver": "7.0.0" }, "dependencies": { @@ -37693,27 +37679,27 @@ } }, "core-js-pure": { - "version": "3.23.5", - "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.23.5.tgz", - "integrity": "sha512-8t78LdpKSuCq4pJYCYk8hl7XEkAX+BP16yRIwL3AanTksxuEf7CM83vRyctmiEL8NDZ3jpUcv56fk9/zG3aIuw==", + "version": "3.24.1", + "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.24.1.tgz", + "integrity": "sha512-r1nJk41QLLPyozHUUPmILCEMtMw24NG4oWK6RbsDdjzQgg9ZvrUsPBj1MnG0wXXp1DCDU6j+wUvEmBSrtRbLXg==", "dev": true }, "core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", "dev": true }, "cosmiconfig": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz", - "integrity": "sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.1.tgz", + "integrity": "sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ==", "requires": { "@types/parse-json": "^4.0.0", - "import-fresh": "^3.1.0", + "import-fresh": "^3.2.1", "parse-json": "^5.0.0", "path-type": "^4.0.0", - "yaml": "^1.7.2" + "yaml": "^1.10.0" } }, "cp-file": { @@ -37959,6 +37945,12 @@ } } }, + "slash": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", + "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", + "dev": true + }, "to-regex-range": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", @@ -38232,12 +38224,6 @@ } } }, - "ci-info": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.3.2.tgz", - "integrity": "sha512-xmDt/QIAdeZ9+nfdPsaBCpMvHNLFiLdjj59qjqn+6iPe6YmHGQ35sBnQ8uslRBXFmXkiZQOJRjvQeoGppoTjjg==", - "dev": true - }, "color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -38259,62 +38245,12 @@ "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", "dev": true }, - "execa": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz", - "integrity": "sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==", - "dev": true, - "requires": { - "cross-spawn": "^7.0.0", - "get-stream": "^5.0.0", - "human-signals": "^1.1.1", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.0", - "onetime": "^5.1.0", - "signal-exit": "^3.0.2", - "strip-final-newline": "^2.0.0" - } - }, - "get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", - "dev": true, - "requires": { - "pump": "^3.0.0" - } - }, "has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, - "human-signals": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", - "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==", - "dev": true - }, - "is-ci": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz", - "integrity": "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==", - "dev": true, - "requires": { - "ci-info": "^3.2.0" - } - }, - "rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - }, "semver": { "version": "7.3.7", "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", @@ -38332,21 +38268,6 @@ "requires": { "has-flag": "^4.0.0" } - }, - "tmp": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", - "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", - "dev": true, - "requires": { - "rimraf": "^3.0.0" - } - }, - "untildify": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", - "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==", - "dev": true } } }, @@ -38420,6 +38341,18 @@ "bplist-parser": "^0.1.0", "meow": "^3.1.0", "untildify": "^2.0.0" + }, + "dependencies": { + "untildify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/untildify/-/untildify-2.1.0.tgz", + "integrity": "sha512-sJjbDp2GodvkB0FZZcn7k6afVisqX5BZD7Yq3xp4nN2O15BBK0cLm3Vwn2vQaF7UDS0UUsrQMkkplmDI5fskig==", + "dev": true, + "optional": true, + "requires": { + "os-homedir": "^1.0.0" + } + } } }, "default-gateway": { @@ -38429,6 +38362,37 @@ "dev": true, "requires": { "execa": "^5.0.0" + }, + "dependencies": { + "execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + } + }, + "get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true + }, + "human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true + } } }, "defaults": { @@ -38573,6 +38537,37 @@ "dev": true, "requires": { "execa": "^5.1.1" + }, + "dependencies": { + "execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + } + }, + "get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true + }, + "human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true + } } }, "detect-port": { @@ -38806,9 +38801,9 @@ "dev": true }, "electron-to-chromium": { - "version": "1.4.194", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.194.tgz", - "integrity": "sha512-ola5UH0xAP1oYY0FFUsPvwtucEzCQHucXnT7PQ1zjHJMccZhCDktEugI++JUR3YuIs7Ff7afz+OVEhVAIMhLAQ==" + "version": "1.4.215", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.215.tgz", + "integrity": "sha512-vqZxT8C5mlDZ//hQFhneHmOLnj1LhbzxV0+I1yqHV8SB1Oo4Y5Ne9+qQhwHl7O1s9s9cRuo2l5CoLEHdhMTwZg==" }, "elliptic": { "version": "6.5.4", @@ -38894,14 +38889,6 @@ "dev": true, "requires": { "ansi-colors": "^4.1.1" - }, - "dependencies": { - "ansi-colors": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", - "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", - "dev": true - } } }, "entities": { @@ -39172,24 +39159,6 @@ "loader-utils": "^2.0.0", "tapable": "^2.2.0", "webpack-sources": "^2.2.0" - }, - "dependencies": { - "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 - }, - "webpack-sources": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-2.3.1.tgz", - "integrity": "sha512-y9EI9AO42JjEcrTJFOYmVywVZdKVUfOvDUPsJea5GIr1JOEGFVqwlY2K098fFoIjOkDzHn2AjRvM8dsBZu+gCA==", - "dev": true, - "requires": { - "source-list-map": "^2.0.1", - "source-map": "^0.6.1" - } - } } }, "esbuild-netbsd-64": { @@ -39273,13 +39242,14 @@ } }, "eslint": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.20.0.tgz", - "integrity": "sha512-d4ixhz5SKCa1D6SCPrivP7yYVi7nyD6A4vs6HIAul9ujBzcEmZVM3/0NN/yu5nKhmO1wjp5xQ46iRfmDGlOviA==", + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.21.0.tgz", + "integrity": "sha512-/XJ1+Qurf1T9G2M5IHrsjp+xrGT73RZf23xA1z5wB1ZzzEAWSZKvRwhWxTFp1rvkvCfwcvAUNAP31bhKTTGfDA==", "dev": true, "requires": { "@eslint/eslintrc": "^1.3.0", - "@humanwhocodes/config-array": "^0.9.2", + "@humanwhocodes/config-array": "^0.10.4", + "@humanwhocodes/gitignore-to-minimatch": "^1.0.2", "ajv": "^6.10.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", @@ -39289,14 +39259,17 @@ "eslint-scope": "^7.1.1", "eslint-utils": "^3.0.0", "eslint-visitor-keys": "^3.3.0", - "espree": "^9.3.2", + "espree": "^9.3.3", "esquery": "^1.4.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", "functional-red-black-tree": "^1.0.1", "glob-parent": "^6.0.1", "globals": "^13.15.0", + "globby": "^11.1.0", + "grapheme-splitter": "^1.0.4", "ignore": "^5.2.0", "import-fresh": "^3.0.0", "imurmurhash": "^0.1.4", @@ -39365,19 +39338,10 @@ "estraverse": "^5.2.0" } }, - "glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "requires": { - "is-glob": "^4.0.3" - } - }, "globals": { - "version": "13.16.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.16.0.tgz", - "integrity": "sha512-A1lrQfpNF+McdPOnnFqY3kSN0AFTy485bTi1bkLk4mVPODIUEcSfhHgRqA+QdXPksrSTTztYXx37NFV+GpGk3Q==", + "version": "13.17.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.17.0.tgz", + "integrity": "sha512-1C+6nQRb1GwGMKm2dH/E7enFAMxGTmGI7/dEdhy/DNelv85w9B72t3uc5frtMNXIbzrarJJ/lTCjcaZwbLJmyw==", "dev": true, "requires": { "type-fest": "^0.20.2" @@ -39452,9 +39416,9 @@ } }, "eslint-import-resolver-typescript": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.2.7.tgz", - "integrity": "sha512-WvcsRy3aPmwVsuS/XVliAJWpIdTlaFXXZPZk3TCbvvF8RtaAkjAhcLL5bl5VEoTmE+XnTHjIbWMzNZcOQpK/DA==", + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.4.0.tgz", + "integrity": "sha512-rBCgiEovwX/HQ8ESWV+XIWZaFiRtDeAXNZdcTATB8UbMuadc9qfGOlIP+vy+c7nsgfEBN4NTwy5qunGNptDP0Q==", "dev": true, "requires": { "debug": "^4.3.4", @@ -39605,21 +39569,21 @@ } }, "eslint-plugin-jsx-a11y": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.6.0.tgz", - "integrity": "sha512-kTeLuIzpNhXL2CwLlc8AHI0aFRwWHcg483yepO9VQiHzM9bZwJdzTkzBszbuPrbgGmq2rlX/FaT2fJQsjUSHsw==", + "version": "6.6.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.6.1.tgz", + "integrity": "sha512-sXgFVNHiWffBq23uiS/JaP6eVR622DqwB4yTzKvGZGcPq6/yZ3WmOZfuBks/vHWo9GaFOqC2ZK4i6+C35knx7Q==", "dev": true, "requires": { - "@babel/runtime": "^7.18.3", + "@babel/runtime": "^7.18.9", "aria-query": "^4.2.2", "array-includes": "^3.1.5", "ast-types-flow": "^0.0.7", - "axe-core": "^4.4.2", + "axe-core": "^4.4.3", "axobject-query": "^2.2.0", "damerau-levenshtein": "^1.0.8", "emoji-regex": "^9.2.2", "has": "^1.0.3", - "jsx-ast-utils": "^3.3.1", + "jsx-ast-utils": "^3.3.2", "language-tags": "^1.0.5", "minimatch": "^3.1.2", "semver": "^6.3.0" @@ -39761,20 +39725,20 @@ "dev": true }, "espree": { - "version": "9.3.2", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.3.2.tgz", - "integrity": "sha512-D211tC7ZwouTIuY5x9XnS0E9sWNChB7IYKX/Xp5eQj3nFXhqmiUDB9q27y76oFl8jTg3pXcQx/bpxMfs3CIZbA==", + "version": "9.3.3", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.3.3.tgz", + "integrity": "sha512-ORs1Rt/uQTqUKjDdGCyrtYxbazf5umATSf/K4qxjmZHORR6HJk+2s/2Pqe+Kk49HHINC/xNIrGfgh8sZcll0ng==", "dev": true, "requires": { - "acorn": "^8.7.1", + "acorn": "^8.8.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^3.3.0" }, "dependencies": { "acorn": { - "version": "8.7.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.1.tgz", - "integrity": "sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A==", + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.0.tgz", + "integrity": "sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==", "dev": true } } @@ -39867,19 +39831,19 @@ "dev": true }, "execa": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", - "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz", + "integrity": "sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==", "dev": true, "requires": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.0", - "human-signals": "^2.1.0", + "cross-spawn": "^7.0.0", + "get-stream": "^5.0.0", + "human-signals": "^1.1.1", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.1", - "onetime": "^5.1.2", - "signal-exit": "^3.0.3", + "npm-run-path": "^4.0.0", + "onetime": "^5.1.0", + "signal-exit": "^3.0.2", "strip-final-newline": "^2.0.0" } }, @@ -40115,6 +40079,17 @@ "chardet": "^0.7.0", "iconv-lite": "^0.4.24", "tmp": "^0.0.33" + }, + "dependencies": { + "tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "requires": { + "os-tmpdir": "~1.0.2" + } + } } }, "extglob": { @@ -40169,17 +40144,6 @@ "debug": "^4.1.1", "get-stream": "^5.1.0", "yauzl": "^2.10.0" - }, - "dependencies": { - "get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", - "dev": true, - "requires": { - "pump": "^3.0.0" - } - } } }, "extsprintf": { @@ -40211,6 +40175,17 @@ "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.4" + }, + "dependencies": { + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + } } }, "fast-json-parse": { @@ -40232,9 +40207,9 @@ "dev": true }, "fastest-levenshtein": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.12.tgz", - "integrity": "sha512-On2N+BpYJ15xIC974QNVuYGMOlEVt4s0EOI3wwMqOmK1fdDY+FN/zltPV8vosq4ad4c/gJ1KHScUn/6AWIgiow==", + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", + "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", "dev": true }, "fastq": { @@ -40246,15 +40221,6 @@ "reusify": "^1.0.4" } }, - "fault": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/fault/-/fault-1.0.4.tgz", - "integrity": "sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==", - "dev": true, - "requires": { - "format": "^0.2.0" - } - }, "faye-websocket": { "version": "0.11.4", "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", @@ -40749,12 +40715,6 @@ "mime-types": "^2.1.12" } }, - "format": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz", - "integrity": "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==", - "dev": true - }, "forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -40983,10 +40943,13 @@ "optional": true }, "get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "dev": true + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "requires": { + "pump": "^3.0.0" + } }, "get-symbol-description": { "version": "1.0.0", @@ -41048,12 +41011,12 @@ } }, "glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "dev": true, "requires": { - "is-glob": "^4.0.1" + "is-glob": "^4.0.3" } }, "glob-promise": { @@ -41122,14 +41085,6 @@ "ignore": "^5.2.0", "merge2": "^1.4.1", "slash": "^3.0.0" - }, - "dependencies": { - "slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true - } } }, "globrex": { @@ -41144,6 +41099,12 @@ "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", "dev": true }, + "grapheme-splitter": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", + "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", + "dev": true + }, "graphql": { "version": "16.5.0", "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.5.0.tgz", @@ -41424,12 +41385,6 @@ "integrity": "sha512-lOhQU7iG3AMcjmb8NIWCa+KwfJw5bY44BoWPtrj5A4iDbSD3ylGf5QcYr0ZyQnhkKQ2GgWNLdF2rfrXtXlF3nQ==", "dev": true }, - "highlight.js": { - "version": "10.7.3", - "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", - "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==", - "dev": true - }, "history": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/history/-/history-5.3.0.tgz", @@ -41654,9 +41609,9 @@ "dev": true }, "human-signals": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", - "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", + "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==", "dev": true }, "iconv-lite": { @@ -42003,18 +41958,18 @@ "dev": true }, "is-ci": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz", - "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz", + "integrity": "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==", "dev": true, "requires": { - "ci-info": "^2.0.0" + "ci-info": "^3.2.0" } }, "is-core-module": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.9.0.tgz", - "integrity": "sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A==", + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.10.0.tgz", + "integrity": "sha512-Erxj2n/LDAZ7H8WNJXd9tw38GYM3dv8rk8Zcs+jJuxYTW7sozH+SS8NtrSjVL1/vpLvWi1hxy96IzjJ3EHTJJg==", "requires": { "has": "^1.0.3" } @@ -42127,14 +42082,6 @@ "requires": { "global-dirs": "^3.0.0", "is-path-inside": "^3.0.2" - }, - "dependencies": { - "is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", - "dev": true - } } }, "is-interactive": { @@ -42195,16 +42142,24 @@ "dev": true, "requires": { "is-path-inside": "^2.1.0" + }, + "dependencies": { + "is-path-inside": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-2.1.0.tgz", + "integrity": "sha512-wiyhTzfDWsvwAW53OBWF5zuvaOGlZ6PwYxAbPVDhpm+gM09xKQGjBq/8uYN12aDvMxnAnq3dxTyoSoRNmg5YFg==", + "dev": true, + "requires": { + "path-is-inside": "^1.0.2" + } + } } }, "is-path-inside": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-2.1.0.tgz", - "integrity": "sha512-wiyhTzfDWsvwAW53OBWF5zuvaOGlZ6PwYxAbPVDhpm+gM09xKQGjBq/8uYN12aDvMxnAnq3dxTyoSoRNmg5YFg==", - "dev": true, - "requires": { - "path-is-inside": "^1.0.2" - } + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true }, "is-plain-obj": { "version": "2.1.0", @@ -42607,6 +42562,12 @@ "supports-color": "^7.1.0" } }, + "ci-info": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", + "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", + "dev": true + }, "color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -42628,6 +42589,15 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, + "is-ci": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz", + "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==", + "dev": true, + "requires": { + "ci-info": "^2.0.0" + } + }, "supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -42779,13 +42749,13 @@ } }, "jsx-ast-utils": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.2.tgz", - "integrity": "sha512-4ZCADZHRkno244xlNnn4AOG6sRQ7iBZ5BbgZ4vW4y5IZw7cVUD1PPeblm1xx/nfmMxPdt/LHsXZW8z/j58+l9Q==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.3.tgz", + "integrity": "sha512-fYQHZTZ8jSfmWZ0iyzfwiU4WDX4HpHbMCZ3gPlWYiCl3BoeOTsqKBqnTVfH2rYT7eP5c3sVbeSPHnnJOaTrWiw==", "dev": true, "requires": { "array-includes": "^3.1.5", - "object.assign": "^4.1.2" + "object.assign": "^4.1.3" } }, "junk": { @@ -43144,16 +43114,6 @@ "tslib": "^2.0.3" } }, - "lowlight": { - "version": "1.20.0", - "resolved": "https://registry.npmjs.org/lowlight/-/lowlight-1.20.0.tgz", - "integrity": "sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==", - "dev": true, - "requires": { - "fault": "^1.0.0", - "highlight.js": "~10.7.0" - } - }, "lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -44207,14 +44167,14 @@ } }, "object.assign": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz", - "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.3.tgz", + "integrity": "sha512-ZFJnX3zltyjcYJL0RoCJuzb+11zWGyaDbjgxZbdV7rFEcHQuYxrZqhow67aA7xpes6LhojyFDaBKAFfogQrikA==", "dev": true, "requires": { - "call-bind": "^1.0.0", - "define-properties": "^1.1.3", - "has-symbols": "^1.0.1", + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "has-symbols": "^1.0.3", "object-keys": "^1.1.1" } }, @@ -44910,19 +44870,6 @@ "semver": "^7.3.4" }, "dependencies": { - "cosmiconfig": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.1.tgz", - "integrity": "sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ==", - "dev": true, - "requires": { - "@types/parse-json": "^4.0.0", - "import-fresh": "^3.2.1", - "parse-json": "^5.0.0", - "path-type": "^4.0.0", - "yaml": "^1.10.0" - } - }, "semver": { "version": "7.3.7", "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", @@ -45059,12 +45006,6 @@ "integrity": "sha512-66hKPCr+72mlfiSjlEB1+45IjXSqvVAIy6mocupoww4tBFE9R9IhwwUGoI4G++Tc9Aq+2rxOt0RFU6gPcrte0A==", "dev": true }, - "prismjs": { - "version": "1.28.0", - "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.28.0.tgz", - "integrity": "sha512-8aaXdYvl1F7iC7Xm1spqSaY/OJBpYW3v+KJ+F17iYxvdc8sfjW194COK5wVhMZX45tGteiBQgdvD/nhxcRwylw==", - "dev": true - }, "process": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", @@ -45409,19 +45350,6 @@ "react-router": "6.3.0" } }, - "react-syntax-highlighter": { - "version": "15.5.0", - "resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-15.5.0.tgz", - "integrity": "sha512-+zq2myprEnQmH5yw6Gqc8lD55QHnpKaU8TOcFeC/Lg/MQSs8UknEA0JC4nTZGFAXC2J2Hyj/ijJ7NlabyPi2gg==", - "dev": true, - "requires": { - "@babel/runtime": "^7.3.1", - "highlight.js": "^10.4.1", - "lowlight": "^1.17.0", - "prismjs": "^1.27.0", - "refractor": "^3.6.0" - } - }, "read-pkg": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", @@ -45560,25 +45488,6 @@ } } }, - "refractor": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/refractor/-/refractor-3.6.0.tgz", - "integrity": "sha512-MY9W41IOWxxk31o+YvFCNyNzdkc9M20NoZK5vq6jkv4I/uh2zkWcfudj0Q1fovjUQJrNewS9NMzeTtqPf+n5EA==", - "dev": true, - "requires": { - "hastscript": "^6.0.0", - "parse-entities": "^2.0.0", - "prismjs": "~1.27.0" - }, - "dependencies": { - "prismjs": { - "version": "1.27.0", - "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.27.0.tgz", - "integrity": "sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA==", - "dev": true - } - } - }, "regenerate": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", @@ -46494,9 +46403,9 @@ "dev": true }, "set-cookie-parser": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.5.0.tgz", - "integrity": "sha512-cHMAtSXilfyBePduZEBVPTCftTQWz6ehWJD5YNUg4mqvRosrrjKbo4WS8JkB0/RxonMoohHm7cOGH60mDkRQ9w==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.5.1.tgz", + "integrity": "sha512-1jeBGaKNGdEq4FgIrORu/N570dwoPYio8lSoYLWmX7sQ//0JY08Xh9o5pBcgmHQ/MbsYp/aZnOe1s1lIsbLprQ==", "dev": true }, "set-value": { @@ -46598,9 +46507,9 @@ "dev": true }, "slash": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", - "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "dev": true }, "slice-ansi": { @@ -47472,9 +47381,9 @@ "dev": true }, "synckit": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.1.tgz", - "integrity": "sha512-rJEeygO5PNmcZICmrgnbOd2usi5zWE1ESc0Gn5tTmJlongoU8zCTwMFQtar2UgMSiR68vK9afPQ+uVs2lURSIA==", + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.3.tgz", + "integrity": "sha512-1goXnDYNJlKwCM37f5MTzRwo+8SqutgVtg2d37D6YnHHT4E3IhQMRfKiGdfTZU7LBlI6T8inCQUxnMBFHrbqWw==", "dev": true, "requires": { "@pkgr/utils": "^2.3.0", @@ -47538,9 +47447,9 @@ }, "dependencies": { "acorn": { - "version": "8.7.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.1.tgz", - "integrity": "sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A==", + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.0.tgz", + "integrity": "sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==", "dev": true }, "commander": { @@ -47641,6 +47550,16 @@ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true + }, + "webpack-sources": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz", + "integrity": "sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==", + "dev": true, + "requires": { + "source-list-map": "^2.0.0", + "source-map": "~0.6.1" + } } } }, @@ -47741,12 +47660,23 @@ } }, "tmp": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", + "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", "dev": true, "requires": { - "os-tmpdir": "~1.0.2" + "rimraf": "^3.0.0" + }, + "dependencies": { + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + } } }, "tmpl": { @@ -47986,9 +47916,9 @@ "dev": true }, "uglify-js": { - "version": "3.16.2", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.16.2.tgz", - "integrity": "sha512-AaQNokTNgExWrkEYA24BTNMSjyqEXPSfhqoS0AxmHkCJ4U+Dyy5AvbGV/sqxuxficEfGGoX3zWw9R7QpLFfEsg==", + "version": "3.16.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.16.3.tgz", + "integrity": "sha512-uVbFqx9vvLhQg0iBaau9Z75AxWJ8tqM9AV890dIZCLApF4rTcyHwmAvLeEdYRs+BzYWu8Iw81F79ah0EfTXbaw==", "dev": true, "optional": true }, @@ -48240,14 +48170,10 @@ } }, "untildify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/untildify/-/untildify-2.1.0.tgz", - "integrity": "sha512-sJjbDp2GodvkB0FZZcn7k6afVisqX5BZD7Yq3xp4nN2O15BBK0cLm3Vwn2vQaF7UDS0UUsrQMkkplmDI5fskig==", - "dev": true, - "optional": true, - "requires": { - "os-homedir": "^1.0.0" - } + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", + "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==", + "dev": true }, "upath": { "version": "1.2.0", @@ -48427,14 +48353,6 @@ "assert-plus": "^1.0.0", "core-util-is": "1.0.2", "extsprintf": "^1.2.0" - }, - "dependencies": { - "core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", - "dev": true - } } }, "vfile": { @@ -48830,9 +48748,9 @@ }, "dependencies": { "acorn": { - "version": "8.7.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.1.tgz", - "integrity": "sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A==", + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.0.tgz", + "integrity": "sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==", "dev": true }, "acorn-import-assertions": { @@ -48957,9 +48875,9 @@ } }, "webpack-dev-server": { - "version": "4.9.3", - "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.9.3.tgz", - "integrity": "sha512-3qp/eoboZG5/6QgiZ3llN8TUzkSpYg1Ko9khWX1h40MIEUNS2mDoIa8aXsPfskER+GbTvs/IJZ1QTBBhhuetSw==", + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.10.0.tgz", + "integrity": "sha512-7dezwAs+k6yXVFZ+MaL8VnE+APobiO3zvpp3rBHe/HmWQ+avwh0Q3d0xxacOiBybZZ3syTZw9HXzpa3YNbAZDQ==", "dev": true, "requires": { "@types/bonjour": "^3.5.9", @@ -49084,6 +49002,12 @@ "uuid": "^3.3.2" }, "dependencies": { + "ansi-colors": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.4.tgz", + "integrity": "sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA==", + "dev": true + }, "uuid": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", @@ -49103,13 +49027,13 @@ } }, "webpack-sources": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz", - "integrity": "sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-2.3.1.tgz", + "integrity": "sha512-y9EI9AO42JjEcrTJFOYmVywVZdKVUfOvDUPsJea5GIr1JOEGFVqwlY2K098fFoIjOkDzHn2AjRvM8dsBZu+gCA==", "dev": true, "requires": { - "source-list-map": "^2.0.0", - "source-map": "~0.6.1" + "source-list-map": "^2.0.1", + "source-map": "^0.6.1" }, "dependencies": { "source-map": { @@ -49352,9 +49276,9 @@ }, "dependencies": { "yargs-parser": { - "version": "21.0.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.0.1.tgz", - "integrity": "sha512-9BK1jFpLzJROCI5TzwZL/TU4gqjK5xiHV/RfWLOahrjAko/e4DJkRDZQXfvqAsiZzzYhgAzbgz6lg48jcm4GLg==", + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", "dev": true } } diff --git a/frontend/src/api/axiosInstance.ts b/frontend/src/api/axiosInstance.ts index e2157c1a4..00cf80571 100644 --- a/frontend/src/api/axiosInstance.ts +++ b/frontend/src/api/axiosInstance.ts @@ -1,28 +1,34 @@ -import axios, { AxiosError, AxiosResponse } from 'axios'; +import axios from 'axios'; +import type { AxiosError, AxiosResponse } from 'axios'; -import { ACCESS_TOKEN_KEY } from '@constants'; +import AccessTokenController from '@auth/accessToken'; const axiosInstance = axios.create({ baseURL: process.env.API_URL, headers: { 'Content-Type': 'application/json', }, + withCredentials: true, }); const handleAxiosError = (error: AxiosError) => { - const { data } = error.response as AxiosResponse<{ message: string }>; + const { data } = error.response as AxiosResponse<{ message: string; code?: number }>; if (data?.message) { - throw new Error(data.message); + console.error(data.message); + // TODO: 커스텀 에러 코드를 만들어서 그에 맞는 message를 담은 error 객체를 return 하도록 해야 함 + return Promise.reject(error); } - throw new Error('뭔가 axios에 에러가 발생했습니다'); + console.error('서버에 에러가 발생했습니다.', `코드: ${error.code}`); + error.message = '알 수 없는 오류가 발생했습니다. 잠시 후 다시 시도해주세요 :('; + return Promise.reject(error); }; axiosInstance.interceptors.response.use(response => response, handleAxiosError); axiosInstance.interceptors.request.use( config => { - const accessToken = window.sessionStorage.getItem(ACCESS_TOKEN_KEY); + const accessToken = AccessTokenController.accessToken; if (!accessToken) return config; if (!config.headers) { diff --git a/frontend/src/api/deleteRefreshToken.ts b/frontend/src/api/deleteRefreshToken.ts new file mode 100644 index 000000000..6afb15afe --- /dev/null +++ b/frontend/src/api/deleteRefreshToken.ts @@ -0,0 +1,10 @@ +import type { AxiosResponse } from 'axios'; + +import { axiosInstance } from '@api'; + +const deleteRefreshToken = async () => { + const response = await axiosInstance.delete<null, AxiosResponse<null>, null>(`/api/auth/logout`); + return response.data; +}; + +export default deleteRefreshToken; diff --git a/frontend/src/api/deleteReview.ts b/frontend/src/api/deleteReview.ts index e9ad17d29..8d3b3544f 100644 --- a/frontend/src/api/deleteReview.ts +++ b/frontend/src/api/deleteReview.ts @@ -1,11 +1,11 @@ import type { AxiosResponse } from 'axios'; -import type { DeleteReviewRequestBody, EmptyObject } from '@custom-types'; +import type { DeleteReviewRequestBody } from '@custom-types'; import { axiosInstance } from '@api'; const deleteReview = async ({ studyId, reviewId }: DeleteReviewRequestBody) => { - const response = await axiosInstance.delete<EmptyObject, AxiosResponse<EmptyObject>, DeleteReviewRequestBody>( + const response = await axiosInstance.delete<null, AxiosResponse<null>, DeleteReviewRequestBody>( `/api/studies/${studyId}/reviews/${reviewId}`, ); return response.data; diff --git a/frontend/src/api/getAccessToken.ts b/frontend/src/api/getAccessToken.ts new file mode 100644 index 000000000..b4a570aa1 --- /dev/null +++ b/frontend/src/api/getAccessToken.ts @@ -0,0 +1,10 @@ +import type { GetTokenResponseData } from '@custom-types'; + +import { axiosInstance } from '@api'; + +const getAccessToken = async () => { + const response = await axiosInstance.get<GetTokenResponseData>(`/api/auth/refresh`); + return response.data; +}; + +export default getAccessToken; diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index 6c89ce17f..ffdbb8464 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -1,12 +1,16 @@ export { default as axiosInstance } from '@api/axiosInstance'; +export { default as deleteRefreshToken } from '@api/deleteRefreshToken'; export { default as deleteReview } from '@api/deleteReview'; +export { default as getAccessToken } from '@api/getAccessToken'; export { default as getMyStudyList } from '@api/getMyStudyList'; export { default as getStudyDetail } from '@api/getStudyDetail'; export { default as getStudyList } from '@api/getStudyList'; export { default as getStudyReviews } from '@api/getStudyReviews'; export { default as getTagList } from '@api/getTagList'; -export { default as putReview } from '@api/putReview'; -export { default as postAccessToken } from '@api/postAccessToken'; +export { default as getUserInformation } from '@api/getUserInformation'; +export { default as getUserRole } from '@api/getUserRole'; export { default as postJoiningStudy } from '@api/postJoiningStudy'; +export { default as postLogin } from '@api/postLogin'; export { default as postNewStudy } from '@api/postNewStudy'; export { default as postReview } from '@api/postReview'; +export { default as putReview } from '@api/putReview'; diff --git a/frontend/src/api/postAccessToken.ts b/frontend/src/api/postAccessToken.ts deleted file mode 100644 index 8abba2a1b..000000000 --- a/frontend/src/api/postAccessToken.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { AxiosResponse } from 'axios'; - -import type { PostTokenRequestParams, PostTokenResponseData } from '@custom-types'; - -import { axiosInstance } from '@api'; - -const postAccessToken = async ({ code }: PostTokenRequestParams) => { - const response = await axiosInstance.post< - PostTokenResponseData, - AxiosResponse<PostTokenResponseData>, - PostTokenRequestParams - >(`/api/login/token?code=${code}`); - return response.data; -}; - -export default postAccessToken; diff --git a/frontend/src/api/postJoiningStudy.ts b/frontend/src/api/postJoiningStudy.ts index ab5f37b55..51f4443d7 100644 --- a/frontend/src/api/postJoiningStudy.ts +++ b/frontend/src/api/postJoiningStudy.ts @@ -1,11 +1,11 @@ import type { AxiosResponse } from 'axios'; -import type { EmptyObject, PostJoiningStudyRequestParams } from '@custom-types'; +import type { PostJoiningStudyRequestParams } from '@custom-types'; import { axiosInstance } from '@api'; const postJoiningStudy = async ({ studyId }: PostJoiningStudyRequestParams) => { - const response = await axiosInstance.post<EmptyObject, AxiosResponse<EmptyObject>, PostJoiningStudyRequestParams>( + const response = await axiosInstance.post<null, AxiosResponse<null>, PostJoiningStudyRequestParams>( `/api/studies/${studyId}`, ); return response.data; diff --git a/frontend/src/api/postLogin.ts b/frontend/src/api/postLogin.ts new file mode 100644 index 000000000..5946ad6b5 --- /dev/null +++ b/frontend/src/api/postLogin.ts @@ -0,0 +1,16 @@ +import type { AxiosResponse } from 'axios'; + +import type { PostLoginRequestParams, PostLoginResponseData } from '@custom-types'; + +import { axiosInstance } from '@api'; + +const postLogin = async ({ code }: PostLoginRequestParams) => { + const response = await axiosInstance.post< + PostLoginResponseData, + AxiosResponse<PostLoginResponseData>, + PostLoginRequestParams + >(`/api/auth/login/token?code=${code}`); + return response.data; +}; + +export default postLogin; diff --git a/frontend/src/api/postNewStudy.ts b/frontend/src/api/postNewStudy.ts index 03a1ace81..dbb6aba1f 100644 --- a/frontend/src/api/postNewStudy.ts +++ b/frontend/src/api/postNewStudy.ts @@ -1,14 +1,11 @@ import type { AxiosResponse } from 'axios'; -import type { EmptyObject, PostNewStudyRequestBody } from '@custom-types'; +import type { PostNewStudyRequestBody } from '@custom-types'; import { axiosInstance } from '@api'; const postNewStudy = async (data: PostNewStudyRequestBody) => { - const response = await axiosInstance.post<EmptyObject, AxiosResponse<EmptyObject>, PostNewStudyRequestBody>( - `/api/studies`, - data, - ); + const response = await axiosInstance.post<null, AxiosResponse<null>, PostNewStudyRequestBody>(`/api/studies`, data); return response.data; }; diff --git a/frontend/src/api/postReview.ts b/frontend/src/api/postReview.ts index 548dc8851..cb1e150f4 100644 --- a/frontend/src/api/postReview.ts +++ b/frontend/src/api/postReview.ts @@ -1,11 +1,11 @@ import type { AxiosResponse } from 'axios'; -import { EmptyObject, PostReviewRequestBody, PostReviewRequestVariables } from '@custom-types'; +import { PostReviewRequestBody, PostReviewRequestVariables } from '@custom-types'; import { axiosInstance } from '@api'; const postReview = async ({ studyId, content }: PostReviewRequestVariables) => { - const response = await axiosInstance.post<EmptyObject, AxiosResponse<EmptyObject>, PostReviewRequestBody>( + const response = await axiosInstance.post<null, AxiosResponse<null>, PostReviewRequestBody>( `/api/studies/${studyId}/reviews`, { content, diff --git a/frontend/src/api/putReview.ts b/frontend/src/api/putReview.ts index d7ee99c50..d3fadeab0 100644 --- a/frontend/src/api/putReview.ts +++ b/frontend/src/api/putReview.ts @@ -1,11 +1,11 @@ import type { AxiosResponse } from 'axios'; -import type { EmptyObject, PutReviewRequestBody, PutReviewRequestVariables } from '@custom-types'; +import type { PutReviewRequestBody, PutReviewRequestVariables } from '@custom-types'; import { axiosInstance } from '@api'; const putReview = async ({ studyId, reviewId, content }: PutReviewRequestVariables) => { - const response = await axiosInstance.put<EmptyObject, AxiosResponse<EmptyObject>, PutReviewRequestBody>( + const response = await axiosInstance.put<null, AxiosResponse<null>, PutReviewRequestBody>( `/api/studies/${studyId}/reviews/${reviewId}`, { content, diff --git a/frontend/src/auth/accessToken.ts b/frontend/src/auth/accessToken.ts new file mode 100644 index 000000000..8f2948fbf --- /dev/null +++ b/frontend/src/auth/accessToken.ts @@ -0,0 +1,63 @@ +import { AxiosError } from 'axios'; + +import { API_ERROR } from '@constants'; + +import { deleteRefreshToken, getAccessToken } from '@api'; + +class AccessTokenController { + private static _accessToken: string | null = null; + private static _tokenExpiredMsTime: number = 20 * 60000; + + static setAccessToken(newAccessToken: string) { + this._accessToken = newAccessToken; + } + + static get accessToken() { + return this._accessToken; + } + + static setTokenExpiredMsTime(newTime: number) { + this._tokenExpiredMsTime = Math.max(Math.floor(newTime * 0.8), newTime - 5 * 60000); + } + + static get tokenExpiredMsTime() { + return this._tokenExpiredMsTime; + } + + static get hasAccessToken() { + return !!this._accessToken; + } + + static removeAccessToken() { + this._accessToken = null; + } + + private static async fetchLogout() { + try { + await deleteRefreshToken(); + AccessTokenController.removeAccessToken(); + } catch (error) { + alert('로그아웃에 실패했습니다. :('); + window.location.reload(); + } + } + + static async fetchAccessTokenWithRefresh() { + try { + const data = await getAccessToken(); + this.setAccessToken(data.accessToken); + this.setTokenExpiredMsTime(data.expiredTime); + + setTimeout(() => { + this.fetchAccessTokenWithRefresh(); + }, this.tokenExpiredMsTime); + } catch (error) { + if (!(error instanceof AxiosError)) return; + if (error.response?.data.code === API_ERROR.EXPIRED_REFRESH_TOKEN.CODE) { + await this.fetchLogout(); + } + } + } +} + +export default AccessTokenController; diff --git a/frontend/src/components/button-group/ButtonGroup.tsx b/frontend/src/components/button-group/ButtonGroup.tsx index 17ba9b149..f2dd1e1ff 100644 --- a/frontend/src/components/button-group/ButtonGroup.tsx +++ b/frontend/src/components/button-group/ButtonGroup.tsx @@ -11,7 +11,11 @@ export type ButtonGroupProps = { type OptionalButtonGroupProps = MakeOptional<ButtonGroupProps, 'variation'>; const ButtonGroup: React.FC<OptionalButtonGroupProps> = ({ className, children, variation = 'flex-end' }) => { - return <S.ButtonGroup variation={variation}>{children}</S.ButtonGroup>; + return ( + <S.ButtonGroup className={className} variation={variation}> + {children} + </S.ButtonGroup> + ); }; export default ButtonGroup; diff --git a/frontend/src/constants.ts b/frontend/src/constants.ts index bc2160521..e7b47faf3 100644 --- a/frontend/src/constants.ts +++ b/frontend/src/constants.ts @@ -8,7 +8,12 @@ export const PATH = { REVIEW: (studyId: string | number = ':studyId') => `/studyroom/${studyId}/reviews`, }; -export const ACCESS_TOKEN_KEY = 'accessToken'; +export const API_ERROR = { + EXPIRED_REFRESH_TOKEN: { + CODE: 4001, + MESSAGE: '오류가 발생했습니다 :(', + }, +}; export const DEFAULT_STUDY_CARD_QUERY_PARAM = { PAGE: 0, @@ -99,9 +104,6 @@ export const MEMBER_COUNT = { }, }; -export const PROFILE_IMAGE_URL = - 'https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80'; - export const REVIEW_LENGTH = { MIN: { VALUE: 1, diff --git a/frontend/src/context/login/LoginProvider.tsx b/frontend/src/context/login/LoginProvider.tsx index f5a186154..379a86e71 100644 --- a/frontend/src/context/login/LoginProvider.tsx +++ b/frontend/src/context/login/LoginProvider.tsx @@ -1,9 +1,9 @@ import { ReactNode, createContext, useEffect, useState } from 'react'; -import { ACCESS_TOKEN_KEY } from '@constants'; - import { noop } from '@utils'; +import AccessTokenController from '@auth/accessToken'; + import { useUserInfo } from '@hooks/useUserInfo'; type LoginProviderProps = { @@ -15,15 +15,13 @@ type ContextType = { setIsLoggedIn: React.Dispatch<React.SetStateAction<boolean>>; }; -const hasAccessToken = !!window.sessionStorage.getItem(ACCESS_TOKEN_KEY); - export const LoginContext = createContext<ContextType>({ isLoggedIn: false, setIsLoggedIn: noop, }); export const LoginProvider = ({ children }: LoginProviderProps) => { - const [isLoggedIn, setIsLoggedIn] = useState(hasAccessToken); + const [isLoggedIn, setIsLoggedIn] = useState(AccessTokenController.hasAccessToken); const { fetchUserInfo } = useUserInfo(); useEffect(() => { diff --git a/frontend/src/context/search/SearchProvider.tsx b/frontend/src/context/search/SearchProvider.tsx index 496ff5416..4b72cd766 100644 --- a/frontend/src/context/search/SearchProvider.tsx +++ b/frontend/src/context/search/SearchProvider.tsx @@ -1,5 +1,7 @@ import { ReactNode, createContext, useState } from 'react'; +import { noop } from '@utils'; + type Props = { children: ReactNode; }; @@ -13,7 +15,7 @@ type ContextType = { export const SearchContext = createContext<ContextType>({ keyword: '', - setKeyword: (_: KeywordType | ((_: KeywordType) => KeywordType)) => {}, + setKeyword: noop, }); export const SearchProvider = ({ children }: Props) => { diff --git a/frontend/src/custom-types/index.ts b/frontend/src/custom-types/index.ts index 95acf5e17..06f0c851f 100644 --- a/frontend/src/custom-types/index.ts +++ b/frontend/src/custom-types/index.ts @@ -119,11 +119,16 @@ export type GetTagListResponseData = { tags: Array<Tag>; }; -export type PostTokenRequestParams = { +export type PostLoginRequestParams = { code: string; }; -export type PostTokenResponseData = { - token: string; +export type PostLoginResponseData = { + accessToken: string; + expiredTime: number; +}; +export type GetTokenResponseData = { + accessToken: string; + expiredTime: number; }; export type GetReviewResponseData = { diff --git a/frontend/src/custom-types/theme.d.ts b/frontend/src/custom-types/theme.d.ts index 6ecc3018c..97d526321 100644 --- a/frontend/src/custom-types/theme.d.ts +++ b/frontend/src/custom-types/theme.d.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-empty-interface */ // import문과 declare의 충돌로 나머지 declare문은 common.d.ts에 작성했다 import { theme } from '@styles/theme'; diff --git a/frontend/src/hooks/useAuth.ts b/frontend/src/hooks/useAuth.ts index da78be269..3b08c2c25 100644 --- a/frontend/src/hooks/useAuth.ts +++ b/frontend/src/hooks/useAuth.ts @@ -1,24 +1,38 @@ +import { AxiosError } from 'axios'; import { useContext } from 'react'; +import { useMutation } from 'react-query'; -import { ACCESS_TOKEN_KEY } from '@constants'; +import { deleteRefreshToken } from '@api'; -import { LoginContext } from '@context/login/LoginProvider'; +import AccessTokenController from '@auth/accessToken'; + +import { useUserInfo } from '@hooks/useUserInfo'; -import { useUserInfo } from './useUserInfo'; +import { LoginContext } from '@context/login/LoginProvider'; export const useAuth = () => { const { isLoggedIn, setIsLoggedIn } = useContext(LoginContext); const { fetchUserInfo } = useUserInfo(); + const { mutate } = useMutation<null, AxiosError, null>(deleteRefreshToken); + const login = (accesssToken: string) => { - window.sessionStorage.setItem(ACCESS_TOKEN_KEY, accesssToken); + AccessTokenController.setAccessToken(accesssToken); setIsLoggedIn(true); fetchUserInfo(); }; const logout = () => { - window.sessionStorage.removeItem(ACCESS_TOKEN_KEY); - setIsLoggedIn(false); + mutate(null, { + onError: () => { + alert('로그아웃에 실패했습니다. 새로고침 해주세요.'); + }, + onSuccess: () => { + AccessTokenController.removeAccessToken(); + setIsLoggedIn(false); + alert('로그아웃 되었습니다.'); + }, + }); }; return { isLoggedIn, login, logout }; diff --git a/frontend/src/hooks/useForm.tsx b/frontend/src/hooks/useForm.tsx index 24a4e811d..91b84b6cf 100644 --- a/frontend/src/hooks/useForm.tsx +++ b/frontend/src/hooks/useForm.tsx @@ -167,10 +167,6 @@ export const useForm: UseForm = () => { const { validate, fieldElement: { name, value }, - min, - max, - minLength, - maxLength, } = field; // Custom Validation diff --git a/frontend/src/hooks/useUserInfo.ts b/frontend/src/hooks/useUserInfo.ts index 510dd9ec8..b0443c97e 100644 --- a/frontend/src/hooks/useUserInfo.ts +++ b/frontend/src/hooks/useUserInfo.ts @@ -1,9 +1,10 @@ +import type { AxiosError } from 'axios'; import { useContext, useEffect } from 'react'; import { useQuery } from 'react-query'; import type { GetUserInformationResponseData } from '@custom-types'; -import getUserInformation from '@api/getUserInformation'; +import { getUserInformation } from '@api'; import { UserInfoContext } from '@context/userInfo/UserInfoProvider'; @@ -14,7 +15,7 @@ export const useUserInfo = () => { refetch: fetchUserInfo, isError, isSuccess, - } = useQuery<GetUserInformationResponseData, Error>('user-info', getUserInformation, { + } = useQuery<GetUserInformationResponseData, AxiosError>('user-info', getUserInformation, { enabled: false, }); diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx index 80dd71072..a806279be 100644 --- a/frontend/src/index.tsx +++ b/frontend/src/index.tsx @@ -1,5 +1,6 @@ +import { AxiosError } from 'axios'; import { createRoot } from 'react-dom/client'; -import { QueryClient, QueryClientProvider } from 'react-query'; +import { QueryCache, QueryClient, QueryClientProvider } from 'react-query'; import { BrowserRouter } from 'react-router-dom'; import { ThemeProvider } from '@emotion/react'; @@ -7,6 +8,8 @@ import { ThemeProvider } from '@emotion/react'; import GlobalStyles from '@styles/Globalstyles'; import { theme } from '@styles/theme'; +import AccessTokenController from '@auth/accessToken'; + import { LoginProvider } from '@context/login/LoginProvider'; import { SearchProvider } from '@context/search/SearchProvider'; import { UserInfoProvider } from '@context/userInfo/UserInfoProvider'; @@ -16,7 +19,7 @@ import App from './App'; if (process.env.NODE_ENV == 'development') { // eslint-disable-next-line @typescript-eslint/no-var-requires const { worker } = require('./mocks/browser'); - // worker.start();j + worker.start(); } const $root = document.getElementById('root'); @@ -28,23 +31,35 @@ if ($root) { refetchOnWindowFocus: false, }, }, + queryCache: new QueryCache({ + onError: error => { + if (!(error instanceof AxiosError)) return; + if (error.response?.status === 401) { + alert(`문제가 발생했습니다. 관리자에게 문의해주세요 :( ${error.message}`); + window.location.reload(); + } + }, + }), + }); + + AccessTokenController.fetchAccessTokenWithRefresh().finally(() => { + root.render( + <ThemeProvider theme={theme}> + <QueryClientProvider client={queryClient}> + <UserInfoProvider> + <LoginProvider> + <SearchProvider> + <GlobalStyles /> + <BrowserRouter> + <App /> + </BrowserRouter> + </SearchProvider> + </LoginProvider> + </UserInfoProvider> + </QueryClientProvider> + </ThemeProvider>, + ); }); - root.render( - <ThemeProvider theme={theme}> - <QueryClientProvider client={queryClient}> - <UserInfoProvider> - <LoginProvider> - <SearchProvider> - <GlobalStyles /> - <BrowserRouter> - <App /> - </BrowserRouter> - </SearchProvider> - </LoginProvider> - </UserInfoProvider> - </QueryClientProvider> - </ThemeProvider>, - ); } else { throw new Error('root element is not exist'); } diff --git a/frontend/src/layout/header/Header.tsx b/frontend/src/layout/header/Header.tsx index edd9b1e8d..08495b7eb 100644 --- a/frontend/src/layout/header/Header.tsx +++ b/frontend/src/layout/header/Header.tsx @@ -1,8 +1,7 @@ import { useContext, useState } from 'react'; import { Link, useNavigate } from 'react-router-dom'; -// TODO: 불필요한 Import , 상수 자체가 필요 없음 -import { PATH, PROFILE_IMAGE_URL } from '@constants'; +import { PATH } from '@constants'; import { useAuth } from '@hooks/useAuth'; import { useUserInfo } from '@hooks/useUserInfo'; @@ -71,7 +70,6 @@ const Header: React.FC<HeaderProps> = ({ className }) => { const [openDropDownBox, setOpenDropDownBox] = useState(false); const navigate = useNavigate(); - const { logout, isLoggedIn } = useAuth(); const { userInfo } = useUserInfo(); @@ -86,6 +84,10 @@ const Header: React.FC<HeaderProps> = ({ className }) => { navigate(PATH.MAIN); }; + const handleLogoutButtonClick = () => { + logout(); + }; + const handleAvatarButtonClick = () => setOpenDropDownBox(prev => !prev); const handleOutsideDropBoxClick = () => setOpenDropDownBox(false); @@ -110,7 +112,7 @@ const Header: React.FC<HeaderProps> = ({ className }) => { </S.AvatarButton> {openDropDownBox && ( <DropDownBox top={'70px'} right={'50px'} onOutOfBoxClick={handleOutsideDropBoxClick}> - <NavButton onClick={logout} ariaLabel="로그아웃"> + <NavButton onClick={handleLogoutButtonClick} ariaLabel="로그아웃"> <MdOutlineLogout /> <span>로그아웃</span> </NavButton> diff --git a/frontend/src/mocks/detailStudyHandlers.ts b/frontend/src/mocks/detailStudyHandlers.ts index 151785066..884b683de 100644 --- a/frontend/src/mocks/detailStudyHandlers.ts +++ b/frontend/src/mocks/detailStudyHandlers.ts @@ -12,7 +12,7 @@ const detailStudyHandlers = [ return res(ctx.status(200), ctx.json(study)); }), rest.post('/api/studies/:studyId', (req, res, ctx) => { - const studyId = req.params.studyId; + // const studyId = req.params.studyId; return res(ctx.status(200)); }), diff --git a/frontend/src/mocks/handlers.ts b/frontend/src/mocks/handlers.ts index 35dbc39ff..4bcdafc33 100644 --- a/frontend/src/mocks/handlers.ts +++ b/frontend/src/mocks/handlers.ts @@ -9,46 +9,14 @@ import { tagHandlers } from '@mocks/tagHandlers'; import { tokenHandlers } from '@mocks/tokenHandlers'; export const handlers = [ - rest.get('/api/studies', (req, res, ctx) => { - const page = req.url.searchParams.get('page'); - const size = req.url.searchParams.get('size'); - - if ((size === null && page !== null) || (size !== null && page === null)) { - return res(ctx.status(400), ctx.json({ message: 'size혹은 page가 없습니다' })); - } - - if (page === null && size === null) { - return res( - ctx.status(200), - ctx.json({ - studies: studyJSON.studies.slice(0, 5), - hasNext: true, // TODO: hasNext 조건 주기 - }), - ); - } - - const sizeNum = Number(size); - const pageNum = Number(page); - const startIndex = pageNum * sizeNum; - const endIndexExclusive = startIndex + sizeNum; - - return res( - ctx.status(200), - ctx.json({ - studies: studyJSON.studies.slice(startIndex, endIndexExclusive), - hasNext: endIndexExclusive < studyJSON.studies.length, - }), - ); - }), - rest.get('/api/studies/search', (req, res, ctx) => { const title = req.url.searchParams.get('title'); const page = req.url.searchParams.get('page'); const size = req.url.searchParams.get('size'); - const generations = req.url.searchParams.getAll('generation'); - const areas = req.url.searchParams.getAll('area'); - const tags = req.url.searchParams.getAll('tag'); + // const generations = req.url.searchParams.getAll('generation'); + // const areas = req.url.searchParams.getAll('area'); + // const tags = req.url.searchParams.getAll('tag'); if ((size === null && page !== null) || (size !== null && page === null)) { return res(ctx.status(400), ctx.json({ message: 'size혹은 page가 없습니다' })); @@ -76,7 +44,7 @@ export const handlers = [ ctx.status(200), ctx.json({ studies: searchedStudies.slice(0, 5), - hasNext: true, // TODO: hasNext 조건 주기 + hasNext: true, }), ); } diff --git a/frontend/src/mocks/memberHandlers.ts b/frontend/src/mocks/memberHandlers.ts index 3fc085ccb..f9a4191a7 100644 --- a/frontend/src/mocks/memberHandlers.ts +++ b/frontend/src/mocks/memberHandlers.ts @@ -13,7 +13,7 @@ export const memberHandlers = [ return res(ctx.status(200), ctx.json(user)); }), rest.get('/api/members/me/role', (req, res, ctx) => { - const studyId = req.url.searchParams.get('study-id'); + // const studyId = req.url.searchParams.get('study-id'); const roles = ['OWNER', 'MEMBER', 'NON_MEMBER']; const selectedRole = roles[Math.floor(Math.random() * roles.length)]; diff --git a/frontend/src/mocks/tokenHandlers.ts b/frontend/src/mocks/tokenHandlers.ts index c7aee602f..87cf074a4 100644 --- a/frontend/src/mocks/tokenHandlers.ts +++ b/frontend/src/mocks/tokenHandlers.ts @@ -1,12 +1,46 @@ import { rest } from 'msw'; +const EXPIRED_TIME = 30 * 60000; + export const tokenHandlers = [ - rest.get('/api/login/token', (req, res, ctx) => { + rest.post('/api/auth/login/token', (req, res, ctx) => { const code = req.url.searchParams.get('code'); if (!code) { return res(ctx.status(400), ctx.json({ message: '잘못된 요청입니다.' })); } - return res(ctx.status(200), ctx.json({ token: 'asddfasdfassdf' })); + return res( + ctx.status(200), + ctx.json({ + accessToken: + 'eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiI1NDAwMjEwNSIsImlhdCI6MTY2MDEyMDczOCwiZXhwIjoxNjYwMTI0MzM4fQ.scUIdy0iHg52NYugHLxMilgh_vbpHNdVIEwLeRRDRRk', + expiredTime: EXPIRED_TIME, + }), + ctx.cookie('refreshToken', 'test-refreshToken!!!sdfsdf', { + // httpOnly: true, + // secure: true, + }), + ); + }), + rest.get('/api/auth/refresh', (req, res, ctx) => { + if (!req.cookies.refreshToken) return res(ctx.status(400), ctx.json({ message: '리프레시 토큰 없음' })); + + // return res(ctx.status(401), ctx.json({ message: '리프레시 토큰 만료', code: 4001 })); + + return res( + ctx.status(200), + ctx.json({ + accessToken: + 'eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiI1NDAwMjEwNSIsImlhdCI6MTY2MDEyMDczOCwiZXhwIjoxNjYwMTI0MzM4fQ.scUIdy0iHg52NYugHLxMilgh_vbpHNdVIEwLeRRDRRk', + expiredTime: EXPIRED_TIME, + }), + ctx.cookie('refreshToken', 'test-refreshToken!!!sdfsdf', { + // httpOnly: true, + // secure: true, + }), + ); + }), + rest.delete('/api/auth/logout', (req, res, ctx) => { + return res(ctx.status(200), ctx.cookie('refreshToken', '')); }), ]; diff --git a/frontend/src/pages/create-study-page/CreateStudyPage.style.tsx b/frontend/src/pages/create-study-page/CreateStudyPage.style.tsx index 272dceace..dd8db391a 100644 --- a/frontend/src/pages/create-study-page/CreateStudyPage.style.tsx +++ b/frontend/src/pages/create-study-page/CreateStudyPage.style.tsx @@ -1,4 +1,3 @@ -import { css } from '@emotion/react'; import styled from '@emotion/styled'; import { mqDown } from '@utils'; @@ -12,6 +11,13 @@ export const CreateStudyPage = styled.div``; export const Form = styled.form``; +export const PageTitle = styled.h1` + margin-bottom: 20px; + + font-size: 32px; + font-weight: 700; +`; + export const DescriptionTab = styled(OriginalDesriptionTab)` margin-bottom: 15px; `; diff --git a/frontend/src/pages/create-study-page/CreateStudyPage.tsx b/frontend/src/pages/create-study-page/CreateStudyPage.tsx index 47f32cb33..39535c07c 100644 --- a/frontend/src/pages/create-study-page/CreateStudyPage.tsx +++ b/frontend/src/pages/create-study-page/CreateStudyPage.tsx @@ -22,7 +22,7 @@ const CreateStudyPage: React.FC = () => { <S.CreateStudyPage> <FormProvider {...formMethods}> <S.Form onSubmit={formMethods.handleSubmit(onSubmit)}> - <h1>스터디 개설하기</h1> + <S.PageTitle>스터디 개설하기</S.PageTitle> <S.Container> <S.Main> <Title /> diff --git a/frontend/src/pages/create-study-page/components/max-member-count/MaxMemberCount.tsx b/frontend/src/pages/create-study-page/components/max-member-count/MaxMemberCount.tsx index 3668fbe63..4c73bc69b 100644 --- a/frontend/src/pages/create-study-page/components/max-member-count/MaxMemberCount.tsx +++ b/frontend/src/pages/create-study-page/components/max-member-count/MaxMemberCount.tsx @@ -16,11 +16,8 @@ const maxMemberCountName = 'max-member-count'; const MaxMemberCount = ({ className }: MaxMemberCountProps) => { const [willSelectMaxMember, setWillSelectMaxMember] = useState<boolean>(true); - const [count, setCount] = useState<number>(1); - const { removeField } = useFormContext(); - - const { register } = useFormContext(); + const { removeField, register } = useFormContext(); const { handleKeyDown } = usePositiveNumberInput(); @@ -52,9 +49,6 @@ const MaxMemberCount = ({ className }: MaxMemberCountProps) => { type="number" id={maxMemberCountName} placeholder="최대 인원" - onChange={value => { - setCount(Number(value)); - }} /> </> )} diff --git a/frontend/src/pages/create-study-page/components/meta-box/MetaBox.style.tsx b/frontend/src/pages/create-study-page/components/meta-box/MetaBox.style.tsx index 04dbf5c28..3864cb1ba 100644 --- a/frontend/src/pages/create-study-page/components/meta-box/MetaBox.style.tsx +++ b/frontend/src/pages/create-study-page/components/meta-box/MetaBox.style.tsx @@ -8,18 +8,20 @@ export const MetaBox = styled.div` border: 1px solid ${theme.colors.secondary.dark}; box-shadow: 0 1px 1px rgb(0 0 0 / 4%); border-radius: 6px; + `} +`; - & > .title { - font-size: 16px; - padding: 8px 12px; - margin: 0; - line-height: 1.4; - - border-bottom: 1px solid ${theme.colors.secondary.dark}; - } +export const Title = styled.h2` + ${({ theme }) => css` + font-size: 16px; + padding: 8px 12px; + margin: 0; + line-height: 1.4; - & > .content { - padding: 8px 12px; - } + border-bottom: 1px solid ${theme.colors.secondary.dark}; `} `; + +export const Content = styled.div` + padding: 8px 12px; +`; diff --git a/frontend/src/pages/create-study-page/components/meta-box/MetaBox.tsx b/frontend/src/pages/create-study-page/components/meta-box/MetaBox.tsx index db783cebd..7a1c88565 100644 --- a/frontend/src/pages/create-study-page/components/meta-box/MetaBox.tsx +++ b/frontend/src/pages/create-study-page/components/meta-box/MetaBox.tsx @@ -3,11 +3,11 @@ import { ReactNode } from 'react'; import * as S from '@create-study-page/components/meta-box/MetaBox.style'; const MetaBoxTitle = ({ className, children }: { className?: string; children: ReactNode }) => { - return <h2 className={className}>{children}</h2>; + return <S.Title className={className}>{children}</S.Title>; }; const MetaBoxContent = ({ className, children }: { className?: string; children: ReactNode }) => { - return <div className={className}>{children}</div>; + return <S.Content className={className}>{children}</S.Content>; }; const MetaBox = ({ className, children }: { className?: string; children: ReactNode }) => { diff --git a/frontend/src/pages/create-study-page/components/period/Peroid.tsx b/frontend/src/pages/create-study-page/components/period/Peroid.tsx index 7d9ab6d48..9b3d159d7 100644 --- a/frontend/src/pages/create-study-page/components/period/Peroid.tsx +++ b/frontend/src/pages/create-study-page/components/period/Peroid.tsx @@ -1,7 +1,5 @@ import { useMemo } from 'react'; -import { css } from '@emotion/react'; - import { getNextYear, getToday } from '@utils'; import { DateYMD } from '@custom-types'; diff --git a/frontend/src/pages/create-study-page/components/publish/Publish.tsx b/frontend/src/pages/create-study-page/components/publish/Publish.tsx index a88ca7e5a..dcc97c767 100644 --- a/frontend/src/pages/create-study-page/components/publish/Publish.tsx +++ b/frontend/src/pages/create-study-page/components/publish/Publish.tsx @@ -1,7 +1,5 @@ import { css } from '@emotion/react'; -import { useFormContext } from '@hooks/useForm'; - import Button from '@components/button/Button'; import MetaBox from '@create-study-page/components/meta-box/MetaBox'; diff --git a/frontend/src/pages/create-study-page/hooks/useGetTagList.ts b/frontend/src/pages/create-study-page/hooks/useGetTagList.ts index b3e987c35..5448dfa04 100644 --- a/frontend/src/pages/create-study-page/hooks/useGetTagList.ts +++ b/frontend/src/pages/create-study-page/hooks/useGetTagList.ts @@ -1,3 +1,4 @@ +import type { AxiosError } from 'axios'; import { useQuery } from 'react-query'; import type { GetTagListResponseData } from '@custom-types'; @@ -5,7 +6,7 @@ import type { GetTagListResponseData } from '@custom-types'; import { getTagList } from '@api'; const useGetTagList = () => { - return useQuery<GetTagListResponseData, Error>('filters', getTagList); + return useQuery<GetTagListResponseData, AxiosError>('filters', getTagList); }; export default useGetTagList; diff --git a/frontend/src/pages/create-study-page/hooks/usePostNewStudy.ts b/frontend/src/pages/create-study-page/hooks/usePostNewStudy.ts index 5d42cb04b..2978117ca 100644 --- a/frontend/src/pages/create-study-page/hooks/usePostNewStudy.ts +++ b/frontend/src/pages/create-study-page/hooks/usePostNewStudy.ts @@ -1,11 +1,12 @@ +import type { AxiosError } from 'axios'; import { useMutation } from 'react-query'; -import type { EmptyObject, PostNewStudyRequestBody } from '@custom-types'; +import type { PostNewStudyRequestBody } from '@custom-types'; import { postNewStudy } from '@api'; const usePostNewStudy = () => { - return useMutation<EmptyObject, Error, PostNewStudyRequestBody>(postNewStudy); + return useMutation<null, AxiosError, PostNewStudyRequestBody>(postNewStudy); }; export default usePostNewStudy; diff --git a/frontend/src/pages/detail-page/components/study-wide-float-box/StudyWideFloatBox.tsx b/frontend/src/pages/detail-page/components/study-wide-float-box/StudyWideFloatBox.tsx index 5abe12a6c..b3cb00b0d 100644 --- a/frontend/src/pages/detail-page/components/study-wide-float-box/StudyWideFloatBox.tsx +++ b/frontend/src/pages/detail-page/components/study-wide-float-box/StudyWideFloatBox.tsx @@ -12,7 +12,6 @@ import Button from '@components/button/Button'; import * as S from '@detail-page/components/study-wide-float-box/StudyWideFloatBox.style'; -// TODO: 스터디에 가입한 사람인지 아닌지 상태도 받아야 함 export type StudyWideFloatBoxProps = Pick< StudyDetail, 'enrollmentEndDate' | 'currentMemberCount' | 'maxMemberCount' | 'recruitmentStatus' diff --git a/frontend/src/pages/detail-page/hooks/useDetailPage.ts b/frontend/src/pages/detail-page/hooks/useDetailPage.ts index 0700aabc8..f81c3b4d8 100644 --- a/frontend/src/pages/detail-page/hooks/useDetailPage.ts +++ b/frontend/src/pages/detail-page/hooks/useDetailPage.ts @@ -1,10 +1,10 @@ +import type { AxiosError } from 'axios'; import { useMutation, useQuery } from 'react-query'; import { useParams } from 'react-router-dom'; -import type { EmptyObject, GetUserRoleResponseData, PostJoiningStudyRequestParams } from '@custom-types'; +import type { GetUserRoleResponseData, PostJoiningStudyRequestParams } from '@custom-types'; -import { postJoiningStudy } from '@api'; -import getUserRole from '@api/getUserRole'; +import { getUserRole, postJoiningStudy } from '@api'; import { useAuth } from '@hooks/useAuth'; @@ -16,8 +16,8 @@ const useDetailPage = () => { const detailQueryResult = useGetDetail(Number(studyId)); - const { mutate } = useMutation<EmptyObject, Error, PostJoiningStudyRequestParams>(postJoiningStudy); - const userRoleQueryResult = useQuery<GetUserRoleResponseData, Error>( + const { mutate } = useMutation<null, AxiosError, PostJoiningStudyRequestParams>(postJoiningStudy); + const userRoleQueryResult = useQuery<GetUserRoleResponseData, AxiosError>( 'my-role', () => getUserRole({ studyId: Number(studyId) }), { diff --git a/frontend/src/pages/detail-page/hooks/useGetDetail.ts b/frontend/src/pages/detail-page/hooks/useGetDetail.ts index 17db86f3e..60769b99d 100644 --- a/frontend/src/pages/detail-page/hooks/useGetDetail.ts +++ b/frontend/src/pages/detail-page/hooks/useGetDetail.ts @@ -1,3 +1,4 @@ +import type { AxiosError } from 'axios'; import { useQuery } from 'react-query'; import { QK_FETCH_STUDY_DETAIL } from '@constants'; @@ -7,7 +8,7 @@ import type { GetStudyDetailResponseData } from '@custom-types'; import { getStudyDetail } from '@api'; const useGetDetail = (studyId: number) => { - return useQuery<GetStudyDetailResponseData, Error>([QK_FETCH_STUDY_DETAIL, studyId], () => + return useQuery<GetStudyDetailResponseData, AxiosError>([QK_FETCH_STUDY_DETAIL, studyId], () => getStudyDetail({ studyId }), ); }; diff --git a/frontend/src/pages/detail-page/hooks/useGetStudyReviews.ts b/frontend/src/pages/detail-page/hooks/useGetStudyReviews.ts index a7c066bc6..cf5007926 100644 --- a/frontend/src/pages/detail-page/hooks/useGetStudyReviews.ts +++ b/frontend/src/pages/detail-page/hooks/useGetStudyReviews.ts @@ -1,3 +1,4 @@ +import type { AxiosError } from 'axios'; import { useQuery } from 'react-query'; import { QK_FETCH_STUDY_REVIEWS } from '@constants'; @@ -8,7 +9,7 @@ import { getStudyReviews } from '@api'; const useGetStudyReviews = (studyId: number, size?: number) => { const queryKey = size ? [QK_FETCH_STUDY_REVIEWS, studyId] : [QK_FETCH_STUDY_REVIEWS, studyId, 'all']; - return useQuery<GetReviewResponseData, Error>(queryKey, () => getStudyReviews({ studyId, size })); + return useQuery<GetReviewResponseData, AxiosError>(queryKey, () => getStudyReviews({ studyId, size })); }; export default useGetStudyReviews; diff --git a/frontend/src/pages/login-redirect-page/hooks/useLoginRedirectPage.ts b/frontend/src/pages/login-redirect-page/hooks/useLoginRedirectPage.ts index 668298b5f..2ed86a4d2 100644 --- a/frontend/src/pages/login-redirect-page/hooks/useLoginRedirectPage.ts +++ b/frontend/src/pages/login-redirect-page/hooks/useLoginRedirectPage.ts @@ -1,12 +1,15 @@ +import type { AxiosError } from 'axios'; import { useEffect } from 'react'; import { useMutation } from 'react-query'; import { useNavigate, useSearchParams } from 'react-router-dom'; import { PATH } from '@constants'; -import { PostTokenRequestParams, PostTokenResponseData } from '@custom-types'; +import { PostLoginRequestParams, PostLoginResponseData } from '@custom-types'; -import { postAccessToken } from '@api'; +import { postLogin } from '@api'; + +import AccessTokenController from '@auth/accessToken'; import { useAuth } from '@hooks/useAuth'; @@ -17,7 +20,7 @@ const useLoginRedirectPage = () => { const { login } = useAuth(); - const { mutate } = useMutation<PostTokenResponseData, Error, PostTokenRequestParams>(postAccessToken); + const { mutate } = useMutation<PostLoginResponseData, AxiosError, PostLoginRequestParams>(postLogin); useEffect(() => { if (!codeParam) { @@ -29,12 +32,18 @@ const useLoginRedirectPage = () => { mutate( { code: codeParam }, { - onError: error => { - alert(error.message ?? '로그인에 실패했습니다.'); + onError: () => { + alert('로그인에 실패했습니다.'); navigate(PATH.MAIN, { replace: true }); }, onSuccess: data => { - login(data.token); + login(data.accessToken); + AccessTokenController.setTokenExpiredMsTime(data.expiredTime); + + setTimeout(() => { + AccessTokenController.fetchAccessTokenWithRefresh(); + }, AccessTokenController.tokenExpiredMsTime); + navigate(PATH.MAIN, { replace: true }); }, }, diff --git a/frontend/src/pages/main-page/components/filter-section/FilterSection.tsx b/frontend/src/pages/main-page/components/filter-section/FilterSection.tsx index 9d4ae93d3..78ddebe08 100644 --- a/frontend/src/pages/main-page/components/filter-section/FilterSection.tsx +++ b/frontend/src/pages/main-page/components/filter-section/FilterSection.tsx @@ -1,3 +1,4 @@ +import type { AxiosError } from 'axios'; import { useRef } from 'react'; import { useQuery } from 'react-query'; @@ -26,7 +27,7 @@ const FilterSection: React.FC<FilterSectionProps> = ({ }) => { const sliderRef = useRef<HTMLElement>(null); - const { data, isLoading, isError, error } = useQuery<GetTagListResponseData, Error>('filters', getTagList); + const { data, isLoading, isError } = useQuery<GetTagListResponseData, AxiosError>('filters', getTagList); const generationTags = filterByCategory(data?.tags, 1); const areaTags = filterByCategory(data?.tags, 2); @@ -63,7 +64,7 @@ const FilterSection: React.FC<FilterSectionProps> = ({ </S.LeftButtonContainer> <S.FilterSection ref={sliderRef}> {isLoading && <div>로딩 중...</div>} - {isError && <div>{error.message}</div>} + {isError && <div>필터 불러오기에 실패했습니다.</div>} <FilterButtonList filters={areaTags} selectedFilters={selectedFilters} diff --git a/frontend/src/pages/main-page/hooks/useGetInfiniteStudyList.ts b/frontend/src/pages/main-page/hooks/useGetInfiniteStudyList.ts index 5df417cc8..71836df58 100644 --- a/frontend/src/pages/main-page/hooks/useGetInfiniteStudyList.ts +++ b/frontend/src/pages/main-page/hooks/useGetInfiniteStudyList.ts @@ -1,3 +1,4 @@ +import type { AxiosError } from 'axios'; import { useInfiniteQuery } from 'react-query'; import { DEFAULT_STUDY_CARD_QUERY_PARAM } from '@constants'; @@ -33,7 +34,7 @@ const getStudyListWithPage = }; const useGetInfiniteStudyList = ({ title, selectedFilters }: UseGetInfiniteStudyListParams) => { - return useInfiniteQuery<GetStudyListResponseDataWithPage, Error>( + return useInfiniteQuery<GetStudyListResponseDataWithPage, AxiosError>( ['infinite-scroll-searched-study-list', title, selectedFilters], getStudyListWithPage(title, selectedFilters), { diff --git a/frontend/src/pages/my-study-page/MyStudyPage.tsx b/frontend/src/pages/my-study-page/MyStudyPage.tsx index 0f623ecde..70650cf74 100644 --- a/frontend/src/pages/my-study-page/MyStudyPage.tsx +++ b/frontend/src/pages/my-study-page/MyStudyPage.tsx @@ -1,37 +1,11 @@ -import { useMemo } from 'react'; - import { css } from '@emotion/react'; -import type { MyStudy, StudyStatus } from '@custom-types'; - import Divider from '@components/divider/Divider'; import Wrapper from '@components/wrapper/Wrapper'; import * as S from '@my-study-page/MyStudyPage.style'; import MyStudyCardListSection from '@my-study-page/components/my-study-card-list-section/MyStudyCardListSection'; -import useGetMyStudy from '@my-study-page/hooks/useGetMyStudy'; - -const filterStudiesByStatus = (studies: Array<MyStudy>, status: StudyStatus) => { - return studies.filter(({ studyStatus }) => studyStatus === status); -}; - -const useMyStudyPage = () => { - const myStudyQueryResult = useGetMyStudy(); - - const filteredStudies: Record<string, Array<MyStudy>> = useMemo(() => { - const studies = myStudyQueryResult.data?.studies ?? []; - return { - prepare: filterStudiesByStatus(studies, 'PREPARE'), - inProgress: filterStudiesByStatus(studies, 'IN_PROGRESS'), - done: filterStudiesByStatus(studies, 'DONE'), - }; - }, [myStudyQueryResult.data]); - - return { - myStudyQueryResult, - studies: filteredStudies, - }; -}; +import { useMyStudyPage } from '@my-study-page/hooks/useMyStudyPage'; const MyStudyPage: React.FC = () => { const { myStudyQueryResult, studies } = useMyStudyPage(); diff --git a/frontend/src/pages/my-study-page/hooks/useGetMyStudy.ts b/frontend/src/pages/my-study-page/hooks/useGetMyStudy.ts index 101dde955..98a08122d 100644 --- a/frontend/src/pages/my-study-page/hooks/useGetMyStudy.ts +++ b/frontend/src/pages/my-study-page/hooks/useGetMyStudy.ts @@ -1,3 +1,4 @@ +import type { AxiosError } from 'axios'; import { useQuery } from 'react-query'; import type { GetMyStudyResponseData } from '@custom-types'; @@ -5,7 +6,7 @@ import type { GetMyStudyResponseData } from '@custom-types'; import { getMyStudyList } from '@api'; const useGetMyStudy = () => { - return useQuery<GetMyStudyResponseData, Error>('my-studies', getMyStudyList); + return useQuery<GetMyStudyResponseData, AxiosError>('my-studies', getMyStudyList); }; export default useGetMyStudy; diff --git a/frontend/src/pages/my-study-page/hooks/useMyStudyPage.ts b/frontend/src/pages/my-study-page/hooks/useMyStudyPage.ts new file mode 100644 index 000000000..2cfe77052 --- /dev/null +++ b/frontend/src/pages/my-study-page/hooks/useMyStudyPage.ts @@ -0,0 +1,27 @@ +import { useMemo } from 'react'; + +import type { MyStudy, StudyStatus } from '@custom-types'; + +import useGetMyStudy from '@my-study-page/hooks/useGetMyStudy'; + +const filterStudiesByStatus = (studies: Array<MyStudy>, status: StudyStatus) => { + return studies.filter(({ studyStatus }) => studyStatus === status); +}; + +export const useMyStudyPage = () => { + const myStudyQueryResult = useGetMyStudy(); + + const filteredStudies: Record<string, Array<MyStudy>> = useMemo(() => { + const studies = myStudyQueryResult.data?.studies ?? []; + return { + prepare: filterStudiesByStatus(studies, 'PREPARE'), + inProgress: filterStudiesByStatus(studies, 'IN_PROGRESS'), + done: filterStudiesByStatus(studies, 'DONE'), + }; + }, [myStudyQueryResult.data]); + + return { + myStudyQueryResult, + studies: filteredStudies, + }; +}; diff --git a/frontend/src/pages/study-room-page/components/review-tab-panel/components/reivew-form/ReviewForm.tsx b/frontend/src/pages/study-room-page/components/review-tab-panel/components/reivew-form/ReviewForm.tsx index f9e71ae5b..1c24a661d 100644 --- a/frontend/src/pages/study-room-page/components/review-tab-panel/components/reivew-form/ReviewForm.tsx +++ b/frontend/src/pages/study-room-page/components/review-tab-panel/components/reivew-form/ReviewForm.tsx @@ -1,8 +1,9 @@ +import type { AxiosError } from 'axios'; import { useMutation } from 'react-query'; import { REVIEW_LENGTH } from '@constants'; -import type { EmptyObject, Member, PostReviewRequestVariables, StudyId } from '@custom-types'; +import type { Member, PostReviewRequestVariables, StudyId } from '@custom-types'; import { postReview } from '@api'; @@ -26,7 +27,7 @@ export type ReviewFormProps = { const ReviewForm: React.FC<ReviewFormProps> = ({ studyId, author, onPostSuccess, onPostError }) => { const { count, setCount, maxCount } = useLetterCount(REVIEW_LENGTH.MAX.VALUE); const { register, handleSubmit, reset } = useForm(); - const { mutateAsync } = useMutation<EmptyObject, Error, PostReviewRequestVariables>(postReview); + const { mutateAsync } = useMutation<null, AxiosError, PostReviewRequestVariables>(postReview); const onSubmit = async (_: React.FormEvent<HTMLFormElement>, submitResult: UseFormSubmitResult) => { if (!submitResult.values) { diff --git a/frontend/src/pages/study-room-page/components/review-tab-panel/components/review-comment/ReviewComment.tsx b/frontend/src/pages/study-room-page/components/review-tab-panel/components/review-comment/ReviewComment.tsx index 6439f9622..cb74ca92f 100644 --- a/frontend/src/pages/study-room-page/components/review-tab-panel/components/review-comment/ReviewComment.tsx +++ b/frontend/src/pages/study-room-page/components/review-tab-panel/components/review-comment/ReviewComment.tsx @@ -59,9 +59,9 @@ const ReviewComment: React.FC<ReviewCommentProps> = ({ id, studyId, author, date </S.UsernameContainer> </S.UserInfo> {isMyComment && ( - <S.DropDown isOpen={isOpen}> + <S.DropDown> <KebabMenu onClick={handleDropDownClick} /> - <S.DropDownMenu> + <S.DropDownMenu isOpen={isOpen}> <li> <button onClick={handleEditReviewBtnClick}>수정</button> </li> diff --git a/frontend/src/pages/study-room-page/components/review-tab-panel/components/review-comment/useReviewComment.ts b/frontend/src/pages/study-room-page/components/review-tab-panel/components/review-comment/useReviewComment.ts index 4a4c1b58c..0179a4290 100644 --- a/frontend/src/pages/study-room-page/components/review-tab-panel/components/review-comment/useReviewComment.ts +++ b/frontend/src/pages/study-room-page/components/review-tab-panel/components/review-comment/useReviewComment.ts @@ -1,16 +1,17 @@ +import type { AxiosError } from 'axios'; import { useCallback, useEffect, useState } from 'react'; import { useMutation, useQueryClient } from 'react-query'; import { QK_FETCH_STUDY_REVIEWS } from '@constants'; -import type { DeleteReviewRequestBody, EmptyObject, ReviewId, StudyId } from '@custom-types'; +import type { DeleteReviewRequestBody, ReviewId, StudyId } from '@custom-types'; import { deleteReview } from '@api'; const useReviewComment = (id: ReviewId, studyId: StudyId) => { const [isOpen, setIsOpen] = useState<boolean>(false); const [isEditing, setIsEditing] = useState<boolean>(false); - const { mutateAsync } = useMutation<EmptyObject, Error, DeleteReviewRequestBody>(deleteReview); + const { mutateAsync } = useMutation<null, AxiosError, DeleteReviewRequestBody>(deleteReview); const queryClient = useQueryClient(); const refetch = () => { queryClient.refetchQueries([QK_FETCH_STUDY_REVIEWS, studyId]); diff --git a/frontend/src/pages/study-room-page/components/review-tab-panel/components/review-edit-form/ReviewEditForm.tsx b/frontend/src/pages/study-room-page/components/review-tab-panel/components/review-edit-form/ReviewEditForm.tsx index a83b0aa42..3d27821f7 100644 --- a/frontend/src/pages/study-room-page/components/review-tab-panel/components/review-edit-form/ReviewEditForm.tsx +++ b/frontend/src/pages/study-room-page/components/review-tab-panel/components/review-edit-form/ReviewEditForm.tsx @@ -1,10 +1,11 @@ +import type { AxiosError } from 'axios'; import { useMutation } from 'react-query'; import { REVIEW_LENGTH } from '@constants'; import { changeDateSeperator } from '@utils'; -import type { DateYMD, EmptyObject, Member, PutReviewRequestVariables, ReviewId, StudyId } from '@custom-types'; +import type { DateYMD, Member, PutReviewRequestVariables, ReviewId, StudyId } from '@custom-types'; import { putReview } from '@api'; @@ -41,7 +42,7 @@ const ReviewEditForm: React.FC<ReviewEditFormProps> = ({ }) => { const { count, setCount, maxCount } = useLetterCount(REVIEW_LENGTH.MAX.VALUE, originalContent.length); const { register, handleSubmit } = useForm(); - const { mutateAsync } = useMutation<EmptyObject, Error, PutReviewRequestVariables>(putReview); + const { mutateAsync } = useMutation<null, AxiosError, PutReviewRequestVariables>(putReview); const onSubmit = async (_: React.FormEvent<HTMLFormElement>, submitResult: UseFormSubmitResult) => { if (!submitResult.values) { diff --git a/frontend/src/pages/study-room-page/hooks/useStudyRoomPage.tsx b/frontend/src/pages/study-room-page/hooks/useStudyRoomPage.tsx index 2da5b5fbe..60f3ff660 100644 --- a/frontend/src/pages/study-room-page/hooks/useStudyRoomPage.tsx +++ b/frontend/src/pages/study-room-page/hooks/useStudyRoomPage.tsx @@ -1,10 +1,11 @@ +import type { AxiosError } from 'axios'; import { useState } from 'react'; import { useQuery } from 'react-query'; import { useParams } from 'react-router-dom'; -import { GetUserRoleResponseData } from '@custom-types'; +import type { GetUserRoleResponseData } from '@custom-types'; -import getUserRole from '@api/getUserRole'; +import { getUserRole } from '@api'; import ReviewTabPanel from '@study-room-page/components/review-tab-panel/ReviewTabPanel'; @@ -17,7 +18,7 @@ export type Tabs = Array<Tab>; const useStudyRoomPage = () => { const { studyId } = useParams() as { studyId: string }; - const userRoleQueryResult = useQuery<GetUserRoleResponseData, Error>('my-role', () => + const userRoleQueryResult = useQuery<GetUserRoleResponseData, AxiosError>('my-role', () => getUserRole({ studyId: Number(studyId) }), ); diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index 617077cbf..e2546b315 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -1,7 +1,6 @@ { "compilerOptions": { "target": "esnext", - "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, "esModuleInterop": true, @@ -26,6 +25,8 @@ "@constants": ["constants.ts"], "@api": ["api/index.ts"], "@api/*": ["api/*"], + "@auth": ["auth/index.ts"], + "@auth/*": ["auth/*"], "@context/*": ["context/*"], "@main-page/*": ["pages/main-page/*"], "@detail-page/*": ["pages/detail-page/*"], @@ -41,8 +42,7 @@ "@pages": ["pages/index.ts"], "@pages/*": ["pages/*"] }, - "typeRoots": ["src/custom-types"], - "types": ["cypress", "@testing-library/cypress"] //https://docs.cypress.io/guides/tooling/typescript-support#Configure-tsconfig-json + "typeRoots": ["src/custom-types"] }, "include": ["src", "node_modules/cypress", "cypress/**/*.cy.ts", "cypress/**/*.cy.tsx"] } diff --git a/frontend/webpack/webpack.common.js b/frontend/webpack/webpack.common.js index 57e9401f7..89d379890 100644 --- a/frontend/webpack/webpack.common.js +++ b/frontend/webpack/webpack.common.js @@ -51,6 +51,7 @@ module.exports = { '@utils': resolve(__dirname, '../src/utils'), '@constants': resolve(__dirname, '../src/constants.ts'), '@api': resolve(__dirname, '../src/api'), + '@auth': resolve(__dirname, '../src/auth'), '@context': resolve(__dirname, '../src/context'), '@main-page': resolve(__dirname, '../src/pages/main-page'), '@detail-page': resolve(__dirname, '../src/pages/detail-page'), From 3eef23d3a5dd35d1e226190f9569ca19001f9b1e Mon Sep 17 00:00:00 2001 From: TaeYoon <uni613@naver.com> Date: Fri, 12 Aug 2022 14:18:22 +0900 Subject: [PATCH 09/51] =?UTF-8?q?fix:=20login=20api=20=EA=B2=BD=EB=A1=9C?= =?UTF-8?q?=20=EC=88=98=EC=A0=95=20(#249)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/api/postLogin.ts | 2 +- frontend/src/mocks/tokenHandlers.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/api/postLogin.ts b/frontend/src/api/postLogin.ts index 5946ad6b5..d1417c5e3 100644 --- a/frontend/src/api/postLogin.ts +++ b/frontend/src/api/postLogin.ts @@ -9,7 +9,7 @@ const postLogin = async ({ code }: PostLoginRequestParams) => { PostLoginResponseData, AxiosResponse<PostLoginResponseData>, PostLoginRequestParams - >(`/api/auth/login/token?code=${code}`); + >(`/api/auth/login?code=${code}`); return response.data; }; diff --git a/frontend/src/mocks/tokenHandlers.ts b/frontend/src/mocks/tokenHandlers.ts index 87cf074a4..c6d4af11d 100644 --- a/frontend/src/mocks/tokenHandlers.ts +++ b/frontend/src/mocks/tokenHandlers.ts @@ -3,7 +3,7 @@ import { rest } from 'msw'; const EXPIRED_TIME = 30 * 60000; export const tokenHandlers = [ - rest.post('/api/auth/login/token', (req, res, ctx) => { + rest.post('/api/auth/login', (req, res, ctx) => { const code = req.url.searchParams.get('code'); if (!code) { From ebbd7805262f4089568e8bf0743a7c6b56d56477 Mon Sep 17 00:00:00 2001 From: Donggyu <a29661498@gmail.com> Date: Fri, 12 Aug 2022 14:45:21 +0900 Subject: [PATCH 10/51] =?UTF-8?q?fix:=20sameSite("None")=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20(#250)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/woowacourse/moamoa/auth/controller/AuthController.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/backend/src/main/java/com/woowacourse/moamoa/auth/controller/AuthController.java b/backend/src/main/java/com/woowacourse/moamoa/auth/controller/AuthController.java index ef362e67e..17938a0c0 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/auth/controller/AuthController.java +++ b/backend/src/main/java/com/woowacourse/moamoa/auth/controller/AuthController.java @@ -49,6 +49,7 @@ private ResponseCookie putTokenInCookie(final TokensResponse tokenResponse) { return ResponseCookie.from(REFRESH_TOKEN, tokenResponse.getRefreshToken()) .maxAge(REFRESH_TOKEN_EXPIRATION) .path("/") + .sameSite("None") .secure(true) .httpOnly(true) .build(); @@ -58,6 +59,7 @@ private ResponseCookie removeCookie() { return ResponseCookie.from(REFRESH_TOKEN, null) .maxAge(0) .path("/") + .sameSite("None") .secure(true) .httpOnly(true) .build(); From 2ac0d2077325cf85693884b192bc01610dd110df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=A7=84=ED=98=81?= <wilgur513@naver.com> Date: Fri, 12 Aug 2022 14:51:48 +0900 Subject: [PATCH 11/51] =?UTF-8?q?[BE]=20issue231:=20=EC=BB=A4=EB=AE=A4?= =?UTF-8?q?=EB=8B=88=ED=8B=B0=20CRUD=20(#235)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * test: 커뮤니티 생성 인수 테스트 중 RestDoc 적용 * test: 스터디 조회 Rest docs 생성 * test: 커뮤니티 게시글 단건 조회 인수 테스트 추가 * test: 스터디 커뮤니티 게시글 응답 검증 추가중 * test: 게시글 작성 및 단건 조회 인수테스트 최종 작성 Co-authored-by: jaejae-yoo <wotj102@gmail.com> * test: 잘못된 토큰으로 커뮤니티 글을 생성하는 경우 컨트롤러 테스트 작성중 Co-authored-by: wilgur513 <wilgur513@naver.com> * test: WebMvcTest를 위한 Service 객체 자동 목킹 추가" Co-authored-by: jaejae-yoo <wotj102@gmail.com> * test: 스터디 커뮤니티 작성 study ID 형식이 잘못된 경우 400에러 반환 테스트 작성 Co-authored-by: wilgur513 <wilgur513@naver.com> * test: 커뮤니티 게시글 작성 통합 테스트 작성 * feat: 스터디 커뮤니티 게시글 작성 * test: 요청 바디 잘못된 형식 테스트 추가 * test: 커뮤니티 제목 및 글 공백,null,길이 검증 * feat: 사용자 또는 스터디가 없는 경우 예외 발생 * test: 스터디에 작성된 사용자인지 검증 테스트 * feat: 스터디 참가자만 게시글 작성 가능 * feat: 스터디 커뮤니티 게시글 작성 기능 * feat: 잘못된 게시글 단건 조회 시 401, 400 반환 * test: 스터디 게시글 단건 조회 테스트 * feat: 단건 조회를 위한 Service 메서드 추가 * feat: 스터디 커뮤니티 게시물 단건 조회 * test: 단건 조회 인수 테스트 * feat: 게시글 조회 시 예외 상황 처리 * feat: 스터디 커뮤니티 게시글 단건 조회 * feat: 스터디 커뮤니티 게시글 삭제 * test: 스터디 전체 게시글 인수 테스트 * test: 커뮤니티 게시글 목록 조회 인수 테스트 작성 * test: 스터디 커뮤니티 400 에러 확인 * feat: 게시글 목록 조회 시 400 에러 처리 * test: 스터디 글 목록 조회 통합 테스트 작성중 * feat: 스터디 커뮤니티 게시글 목록 조회 * feat: 스터디 커뮤니티 게시글 전체 조회 * test: 스터디 커뮤니티 게시글 수정 인수 테스트 * feat: 커뮤니티 게시글을 수정한다 * refactor: 커뮤니티 게시글 관련 도메인 구조 변경 * refactor: 스터디 커뮤니티 게시글 도메인으로 검증 이동 * feat: 스터디 게시글을 볼 수 없는 경우 예외 메시지 추가 * style: 불푤요한 코드 제거 * style: 중복된 코드 제거 Co-authored-by: jaejae-yoo <wotj102@gmail.com> --- .../moamoa/auth/config/AuthConfig.java | 12 +- .../auth/config/AuthRequestMatchConfig.java | 18 +- .../auth/config/AuthenticatedMember.java | 11 + .../AuthenticatedMemberResolver.java | 47 +++ .../controller/AuthenticationInterceptor.java | 3 +- .../PageableVerificationArgumentResolver.java | 5 + .../CommunityArticleController.java | 81 +++++ .../community/domain/CommunityArticle.java | 104 ++++++ .../CommunityArticleRepository.java | 7 + .../community/query/CommunityArticleDao.java | 80 +++++ .../query/data/CommunityArticleData.java | 27 ++ .../service/CommunityArticleService.java | 111 ++++++ .../exception/ArticleNotFoundException.java | 10 + .../exception/NotArticleAuthorException.java | 10 + .../exception/NotRelatedArticleException.java | 10 + .../exception/UneditableArticleException.java | 10 + .../exception/UnviewableArticleException.java | 10 + .../service/request/ArticleRequest.java | 24 ++ .../service/response/ArticleResponse.java | 31 ++ .../response/ArticleSummariesResponse.java | 28 ++ .../response/ArticleSummaryResponse.java | 39 ++ .../service/response/AuthorResponse.java | 25 ++ .../domain/repository/MemberRepository.java | 2 + .../moamoa/study/domain/Participants.java | 10 +- .../moamoa/study/domain/Study.java | 14 +- .../acceptance/AcceptanceTest.java | 1 + .../acceptance/fixture/TagFixtures.java | 7 - .../acceptance/steps/StudyRelatedSteps.java | 30 ++ .../community/CommunityAcceptanceTest.java | 333 ++++++++++++++++++ .../com/woowacourse/moamoa/WebMVCTest.java | 57 +-- .../auth/controller/AuthControllerTest.java | 6 + .../AuthenticationArgumentResolverTest.java | 8 + .../AuthenticationInterceptorTest.java | 5 + .../AuthenticationRequestMatcherTest.java | 5 + .../moamoa/auth/service/AuthServiceTest.java | 6 +- .../moamoa/common/CategoryAndTagsSaver.java | 30 ++ .../MockedServiceObjectsBeanRegister.java | 53 +++ .../moamoa/common/RepositoryTest.java | 2 +- .../CommunityArticleControllerTest.java | 103 ++++++ ...eletingCommunityArticleControllerTest.java | 113 ++++++ ...GettingCommunityArticleControllerTest.java | 121 +++++++ ...mmunityArticleSummariesControllerTest.java | 130 +++++++ ...pdatingCommunityArticleControllerTest.java | 118 +++++++ .../domain/CommunityArticleTest.java | 102 ++++++ ...gCommunityArticleControllerWebMvcTest.java | 162 +++++++++ ...gCommunityArticleControllerWebMvcTest.java | 42 +++ ...gCommunityArticleControllerWebMvcTest.java | 99 ++++++ ...gCommunityArticleControllerWebMvcTest.java | 139 ++++++++ .../{ => moamoa}/fixtures/AuthFixtures.java | 2 +- .../fixtures/CategoryFixtures.java | 2 +- .../{ => moamoa}/fixtures/MemberFixtures.java | 2 +- .../{ => moamoa}/fixtures/ReviewFixtures.java | 22 +- .../{ => moamoa}/fixtures/StudyFixtures.java | 56 ++- .../{ => moamoa}/fixtures/TagFixtures.java | 16 +- .../member/webmvc/MemberWebMvcTest.java | 6 + .../moamoa/study/domain/StudyTest.java | 18 + backend/src/test/resources/application.yml | 2 +- backend/src/test/resources/data.sql | 9 - backend/src/test/resources/schema.sql | 14 + 59 files changed, 2409 insertions(+), 141 deletions(-) create mode 100644 backend/src/main/java/com/woowacourse/moamoa/auth/config/AuthenticatedMember.java create mode 100644 backend/src/main/java/com/woowacourse/moamoa/auth/controller/AuthenticatedMemberResolver.java create mode 100644 backend/src/main/java/com/woowacourse/moamoa/community/controller/CommunityArticleController.java create mode 100644 backend/src/main/java/com/woowacourse/moamoa/community/domain/CommunityArticle.java create mode 100644 backend/src/main/java/com/woowacourse/moamoa/community/domain/repository/CommunityArticleRepository.java create mode 100644 backend/src/main/java/com/woowacourse/moamoa/community/query/CommunityArticleDao.java create mode 100644 backend/src/main/java/com/woowacourse/moamoa/community/query/data/CommunityArticleData.java create mode 100644 backend/src/main/java/com/woowacourse/moamoa/community/service/CommunityArticleService.java create mode 100644 backend/src/main/java/com/woowacourse/moamoa/community/service/exception/ArticleNotFoundException.java create mode 100644 backend/src/main/java/com/woowacourse/moamoa/community/service/exception/NotArticleAuthorException.java create mode 100644 backend/src/main/java/com/woowacourse/moamoa/community/service/exception/NotRelatedArticleException.java create mode 100644 backend/src/main/java/com/woowacourse/moamoa/community/service/exception/UneditableArticleException.java create mode 100644 backend/src/main/java/com/woowacourse/moamoa/community/service/exception/UnviewableArticleException.java create mode 100644 backend/src/main/java/com/woowacourse/moamoa/community/service/request/ArticleRequest.java create mode 100644 backend/src/main/java/com/woowacourse/moamoa/community/service/response/ArticleResponse.java create mode 100644 backend/src/main/java/com/woowacourse/moamoa/community/service/response/ArticleSummariesResponse.java create mode 100644 backend/src/main/java/com/woowacourse/moamoa/community/service/response/ArticleSummaryResponse.java create mode 100644 backend/src/main/java/com/woowacourse/moamoa/community/service/response/AuthorResponse.java create mode 100644 backend/src/test/java/com/woowacourse/acceptance/test/community/CommunityAcceptanceTest.java create mode 100644 backend/src/test/java/com/woowacourse/moamoa/common/CategoryAndTagsSaver.java create mode 100644 backend/src/test/java/com/woowacourse/moamoa/common/MockedServiceObjectsBeanRegister.java create mode 100644 backend/src/test/java/com/woowacourse/moamoa/community/controller/CommunityArticleControllerTest.java create mode 100644 backend/src/test/java/com/woowacourse/moamoa/community/controller/DeletingCommunityArticleControllerTest.java create mode 100644 backend/src/test/java/com/woowacourse/moamoa/community/controller/GettingCommunityArticleControllerTest.java create mode 100644 backend/src/test/java/com/woowacourse/moamoa/community/controller/GettingCommunityArticleSummariesControllerTest.java create mode 100644 backend/src/test/java/com/woowacourse/moamoa/community/controller/UpdatingCommunityArticleControllerTest.java create mode 100644 backend/src/test/java/com/woowacourse/moamoa/community/domain/CommunityArticleTest.java create mode 100644 backend/src/test/java/com/woowacourse/moamoa/community/webmvc/CreatingCommunityArticleControllerWebMvcTest.java create mode 100644 backend/src/test/java/com/woowacourse/moamoa/community/webmvc/DeletingCommunityArticleControllerWebMvcTest.java create mode 100644 backend/src/test/java/com/woowacourse/moamoa/community/webmvc/GettingCommunityArticleControllerWebMvcTest.java create mode 100644 backend/src/test/java/com/woowacourse/moamoa/community/webmvc/UpdatingCommunityArticleControllerWebMvcTest.java rename backend/src/test/java/com/woowacourse/{ => moamoa}/fixtures/AuthFixtures.java (71%) rename backend/src/test/java/com/woowacourse/{ => moamoa}/fixtures/CategoryFixtures.java (96%) rename backend/src/test/java/com/woowacourse/{ => moamoa}/fixtures/MemberFixtures.java (98%) rename backend/src/test/java/com/woowacourse/{ => moamoa}/fixtures/ReviewFixtures.java (76%) rename backend/src/test/java/com/woowacourse/{ => moamoa}/fixtures/StudyFixtures.java (76%) rename backend/src/test/java/com/woowacourse/{ => moamoa}/fixtures/TagFixtures.java (80%) delete mode 100644 backend/src/test/resources/data.sql diff --git a/backend/src/main/java/com/woowacourse/moamoa/auth/config/AuthConfig.java b/backend/src/main/java/com/woowacourse/moamoa/auth/config/AuthConfig.java index 516878da7..bf5946c87 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/auth/config/AuthConfig.java +++ b/backend/src/main/java/com/woowacourse/moamoa/auth/config/AuthConfig.java @@ -1,10 +1,12 @@ package com.woowacourse.moamoa.auth.config; +import com.woowacourse.moamoa.auth.controller.AuthenticatedMemberResolver; import com.woowacourse.moamoa.auth.controller.AuthenticationArgumentResolver; import com.woowacourse.moamoa.auth.controller.AuthenticationInterceptor; import java.util.List; +import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.client.RestTemplate; @@ -13,21 +15,17 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration +@RequiredArgsConstructor public class AuthConfig implements WebMvcConfigurer { private final AuthenticationInterceptor authenticationInterceptor; private final AuthenticationArgumentResolver authenticationArgumentResolver; - - public AuthConfig( - final AuthenticationInterceptor authenticationInterceptor, - final AuthenticationArgumentResolver authenticationArgumentResolver) { - this.authenticationInterceptor = authenticationInterceptor; - this.authenticationArgumentResolver = authenticationArgumentResolver; - } + private final AuthenticatedMemberResolver authenticatedMemberResolver; @Override public void addArgumentResolvers(final List<HandlerMethodArgumentResolver> resolvers) { resolvers.add(authenticationArgumentResolver); + resolvers.add(authenticatedMemberResolver); } @Override diff --git a/backend/src/main/java/com/woowacourse/moamoa/auth/config/AuthRequestMatchConfig.java b/backend/src/main/java/com/woowacourse/moamoa/auth/config/AuthRequestMatchConfig.java index ca6abeb60..14a07c730 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/auth/config/AuthRequestMatchConfig.java +++ b/backend/src/main/java/com/woowacourse/moamoa/auth/config/AuthRequestMatchConfig.java @@ -1,15 +1,11 @@ package com.woowacourse.moamoa.auth.config; -import static org.springframework.http.HttpMethod.DELETE; -import static org.springframework.http.HttpMethod.GET; -import static org.springframework.http.HttpMethod.POST; -import static org.springframework.http.HttpMethod.PUT; - import com.woowacourse.moamoa.auth.controller.matcher.AuthenticationRequestMatcher; import com.woowacourse.moamoa.auth.controller.matcher.AuthenticationRequestMatcherBuilder; import lombok.AllArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; @Configuration @AllArgsConstructor @@ -18,10 +14,14 @@ public class AuthRequestMatchConfig { @Bean public AuthenticationRequestMatcher authenticationRequestMatcher() { return new AuthenticationRequestMatcherBuilder() - .addUpAuthenticationPath(POST, "/api/studies", "/api/studies/\\d+/reviews", "/api/studies/\\d+/reviews/\\d+") - .addUpAuthenticationPath(GET, "/api/my/studies", "/api/members/me", "/api/members/me/role") - .addUpAuthenticationPath(PUT, "/api/studies/\\d+/reviews/\\d+") - .addUpAuthenticationPath(DELETE, "/api/studies/\\d+/reviews/\\d+") + .addUpAuthenticationPath(HttpMethod.POST, "/api/studies", "/api/studies/\\d+/reviews", + "/api/studies/\\d+/reviews/\\d+", "/api/studies/\\w+/community/articles", + "/api/studies") + .addUpAuthenticationPath(HttpMethod.GET, "/api/my/studies", "/api/members/me", "/api/members/me/role", + "/api/studies/\\w+/community/articles/\\w+", "/api/studies/\\w+/community/articles") + .addUpAuthenticationPath(HttpMethod.PUT, "/api/studies/\\d+/reviews/\\d+") + .addUpAuthenticationPath(HttpMethod.DELETE, "/api/studies/\\d+/reviews/\\d+", + "/api/studies/\\w+/community/articles/\\w+") .build(); } } diff --git a/backend/src/main/java/com/woowacourse/moamoa/auth/config/AuthenticatedMember.java b/backend/src/main/java/com/woowacourse/moamoa/auth/config/AuthenticatedMember.java new file mode 100644 index 000000000..8eac7f120 --- /dev/null +++ b/backend/src/main/java/com/woowacourse/moamoa/auth/config/AuthenticatedMember.java @@ -0,0 +1,11 @@ +package com.woowacourse.moamoa.auth.config; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +public @interface AuthenticatedMember { +} diff --git a/backend/src/main/java/com/woowacourse/moamoa/auth/controller/AuthenticatedMemberResolver.java b/backend/src/main/java/com/woowacourse/moamoa/auth/controller/AuthenticatedMemberResolver.java new file mode 100644 index 000000000..caf2be9eb --- /dev/null +++ b/backend/src/main/java/com/woowacourse/moamoa/auth/controller/AuthenticatedMemberResolver.java @@ -0,0 +1,47 @@ +package com.woowacourse.moamoa.auth.controller; + +import com.woowacourse.moamoa.auth.config.AuthenticatedMember; +import com.woowacourse.moamoa.auth.config.AuthenticationExtractor; +import com.woowacourse.moamoa.auth.config.AuthenticationPrincipal; +import com.woowacourse.moamoa.auth.infrastructure.TokenProvider; +import com.woowacourse.moamoa.common.exception.UnauthorizedException; +import com.woowacourse.moamoa.member.domain.Member; +import com.woowacourse.moamoa.member.domain.repository.MemberRepository; +import com.woowacourse.moamoa.member.service.exception.MemberNotFoundException; +import javax.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import org.springframework.core.MethodParameter; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; + +@Component +@RequiredArgsConstructor +public class AuthenticatedMemberResolver implements HandlerMethodArgumentResolver { + + private final MemberRepository memberRepository; + private final TokenProvider tokenProvider; + + @Override + public boolean supportsParameter(final MethodParameter parameter) { + return parameter.hasParameterAnnotation(AuthenticatedMember.class); + } + + @Override + public Object resolveArgument(final MethodParameter parameter, final ModelAndViewContainer mavContainer, + final NativeWebRequest webRequest, final WebDataBinderFactory binderFactory) { + final HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class); + + final String token = AuthenticationExtractor.extract(request); + if (token == null) { + throw new UnauthorizedException("인증 타입이 올바르지 않습니다."); + } + + final Long githubId = Long.valueOf(tokenProvider.getPayload(token)); + + final Member member = memberRepository.findByGithubId(githubId).orElseThrow(MemberNotFoundException::new); + return member.getId(); + } +} diff --git a/backend/src/main/java/com/woowacourse/moamoa/auth/controller/AuthenticationInterceptor.java b/backend/src/main/java/com/woowacourse/moamoa/auth/controller/AuthenticationInterceptor.java index 455647aad..51398e34c 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/auth/controller/AuthenticationInterceptor.java +++ b/backend/src/main/java/com/woowacourse/moamoa/auth/controller/AuthenticationInterceptor.java @@ -22,8 +22,7 @@ public class AuthenticationInterceptor implements HandlerInterceptor { private final AuthenticationRequestMatcher authenticationRequestMatcher; @Override - public boolean preHandle(final HttpServletRequest request, final HttpServletResponse response, - final Object handler) { + public boolean preHandle(final HttpServletRequest request, final HttpServletResponse response, final Object handler) { if (isPreflight(request)) { return true; diff --git a/backend/src/main/java/com/woowacourse/moamoa/common/config/PageableVerificationArgumentResolver.java b/backend/src/main/java/com/woowacourse/moamoa/common/config/PageableVerificationArgumentResolver.java index 0ff567ab6..7ca9cdd4b 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/common/config/PageableVerificationArgumentResolver.java +++ b/backend/src/main/java/com/woowacourse/moamoa/common/config/PageableVerificationArgumentResolver.java @@ -55,11 +55,16 @@ private boolean isInvalidSize(final String size) { } private boolean isNumeric(final String text) { + if (text.isBlank()) { + return false; + } + for (char character : text.toCharArray()) { if (!Character.isDigit(character)) { return false; } } + return true; } } diff --git a/backend/src/main/java/com/woowacourse/moamoa/community/controller/CommunityArticleController.java b/backend/src/main/java/com/woowacourse/moamoa/community/controller/CommunityArticleController.java new file mode 100644 index 000000000..ca9fe0ef9 --- /dev/null +++ b/backend/src/main/java/com/woowacourse/moamoa/community/controller/CommunityArticleController.java @@ -0,0 +1,81 @@ +package com.woowacourse.moamoa.community.controller; + +import com.woowacourse.moamoa.auth.config.AuthenticatedMember; +import com.woowacourse.moamoa.auth.config.AuthenticationPrincipal; +import com.woowacourse.moamoa.community.domain.CommunityArticle; +import com.woowacourse.moamoa.community.service.CommunityArticleService; +import com.woowacourse.moamoa.community.service.request.ArticleRequest; +import com.woowacourse.moamoa.community.service.response.ArticleResponse; +import com.woowacourse.moamoa.community.service.response.ArticleSummariesResponse; +import java.net.URI; +import javax.validation.Valid; +import org.springframework.data.domain.Pageable; +import org.springframework.data.web.PageableDefault; +import org.springframework.http.HttpHeaders; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("api/studies/{study-id}/community/articles") +public class CommunityArticleController { + + private final CommunityArticleService communityArticleService; + + public CommunityArticleController(final CommunityArticleService communityArticleService) { + this.communityArticleService = communityArticleService; + } + + @PostMapping + public ResponseEntity<Void> createArticle(@AuthenticatedMember final Long id, + @PathVariable("study-id") final Long studyId, + @Valid @RequestBody final ArticleRequest request + ) { + final CommunityArticle article = communityArticleService.createArticle(id, studyId, request); + final URI location = URI.create("/api/studies/" + studyId + "/community/articles/" + article.getId()); + return ResponseEntity.created(location).header("Access-Control-Allow-Headers", HttpHeaders.LOCATION).build(); + } + + @GetMapping("/{article-id}") + public ResponseEntity<ArticleResponse> getArticle(@AuthenticatedMember final Long id, + @PathVariable("study-id") final Long studyId, + @PathVariable("article-id") final Long articleId + ) { + ArticleResponse response = communityArticleService.getArticle(id, studyId, articleId); + return ResponseEntity.ok().body(response); + } + + @DeleteMapping("{article-id}") + public ResponseEntity<Void> deleteArticle(@AuthenticatedMember final Long id, + @PathVariable("study-id") final Long studyId, + @PathVariable("article-id") final Long articleId + ) { + communityArticleService.deleteArticle(id, studyId, articleId); + return ResponseEntity.noContent().build(); + } + + @GetMapping + public ResponseEntity<ArticleSummariesResponse> getArticles(@AuthenticatedMember final Long id, + @PathVariable("study-id") final Long studyId, + @PageableDefault final Pageable pageable + ) { + ArticleSummariesResponse response = communityArticleService.getArticles(id, studyId, pageable); + return ResponseEntity.ok().body(response); + } + + @PutMapping("/{article-id}") + public ResponseEntity<Void> updateArticle(@AuthenticatedMember final Long id, + @PathVariable("study-id") final Long studyId, + @PathVariable("article-id") final Long articleId, + @Valid @RequestBody final ArticleRequest request + ) { + communityArticleService.updateArticle(id, studyId, articleId, request); + return ResponseEntity.noContent().build(); + } +} diff --git a/backend/src/main/java/com/woowacourse/moamoa/community/domain/CommunityArticle.java b/backend/src/main/java/com/woowacourse/moamoa/community/domain/CommunityArticle.java new file mode 100644 index 000000000..4adb36eb4 --- /dev/null +++ b/backend/src/main/java/com/woowacourse/moamoa/community/domain/CommunityArticle.java @@ -0,0 +1,104 @@ +package com.woowacourse.moamoa.community.domain; + +import static javax.persistence.GenerationType.IDENTITY; + +import com.woowacourse.moamoa.common.entity.BaseEntity; +import com.woowacourse.moamoa.community.service.request.ArticleRequest; +import com.woowacourse.moamoa.member.domain.Member; +import com.woowacourse.moamoa.member.service.exception.NotParticipatedMemberException; +import com.woowacourse.moamoa.study.domain.Study; +import java.util.Objects; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.GeneratedValue; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.Table; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +@Table(name = "article") +public class CommunityArticle extends BaseEntity { + + @Id + @GeneratedValue(strategy = IDENTITY) + private Long id; + + private String title; + + private String content; + + @Column(name = "author_id") + private Long authorId; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "study_id") + private Study study; + + private CommunityArticle(final String title, final String content, final Long authorId, final Study study) { + this(null, title, content, authorId, study); + } + + public CommunityArticle(final Long id, final String title, final String content, final Long authorId, + final Study study) { + this.id = id; + this.title = title; + this.content = content; + this.authorId = authorId; + this.study = study; + } + + public static CommunityArticle write(final Member member, final Study study, final ArticleRequest request) { + if (!study.isParticipant(member.getId())) { + throw new NotParticipatedMemberException(); + } + + return new CommunityArticle(request.getTitle(), request.getContent(), member.getId(), study); + } + + public void update(final String title, final String content) { + this.title = title; + this.content = content; + } + + public boolean isViewableBy(final Long studyId, final Long memberId) { + return isBelongTo(studyId) && study.isParticipant(memberId); + } + + public boolean isEditableBy(final Long studyId, final Long memberId) { + return isViewableBy(studyId, memberId) && isAuthor(memberId); + } + + private boolean isBelongTo(final Long studyId) { + return this.study.getId().equals(studyId); + } + + private boolean isAuthor(final Long memberId) { + return this.authorId.equals(memberId); + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final CommunityArticle that = (CommunityArticle) o; + return Objects.equals(getId(), that.getId()) && Objects.equals(getTitle(), that.getTitle()) + && Objects.equals(getContent(), that.getContent()) && Objects.equals(getAuthorId(), + that.getAuthorId()) && Objects.equals(getStudy().getId(), that.getStudy().getId()); + } + + @Override + public int hashCode() { + return Objects.hash(getId(), getTitle(), getContent(), getAuthorId(), getStudy().getId()); + } +} diff --git a/backend/src/main/java/com/woowacourse/moamoa/community/domain/repository/CommunityArticleRepository.java b/backend/src/main/java/com/woowacourse/moamoa/community/domain/repository/CommunityArticleRepository.java new file mode 100644 index 000000000..d7b1d0dd5 --- /dev/null +++ b/backend/src/main/java/com/woowacourse/moamoa/community/domain/repository/CommunityArticleRepository.java @@ -0,0 +1,7 @@ +package com.woowacourse.moamoa.community.domain.repository; + +import com.woowacourse.moamoa.community.domain.CommunityArticle; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface CommunityArticleRepository extends JpaRepository<CommunityArticle, Long> { +} diff --git a/backend/src/main/java/com/woowacourse/moamoa/community/query/CommunityArticleDao.java b/backend/src/main/java/com/woowacourse/moamoa/community/query/CommunityArticleDao.java new file mode 100644 index 000000000..c8b5c4e49 --- /dev/null +++ b/backend/src/main/java/com/woowacourse/moamoa/community/query/CommunityArticleDao.java @@ -0,0 +1,80 @@ +package com.woowacourse.moamoa.community.query; + +import com.woowacourse.moamoa.community.query.data.CommunityArticleData; +import com.woowacourse.moamoa.member.query.data.MemberData; +import java.time.LocalDate; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.stereotype.Repository; + +@Repository +public class CommunityArticleDao { + + public static final RowMapper<CommunityArticleData> ROW_MAPPER = (rs, rn) -> { + final long id = rs.getLong("article.id"); + final String title = rs.getString("article.title"); + final String content = rs.getString("article.content"); + final LocalDate createdDate = rs.getObject("article.created_date", LocalDate.class); + final LocalDate lastModifiedDate = rs.getObject("article.last_modified_date", LocalDate.class); + + final long githubId = rs.getLong("member.github_id"); + final String username = rs.getString("member.username"); + final String imageUrl = rs.getString("member.image_url"); + final String profileUrl = rs.getString("member.profile_url"); + MemberData memberData = new MemberData(githubId, username, imageUrl, profileUrl); + + return new CommunityArticleData(id, memberData, title, content, createdDate, lastModifiedDate); + }; + private final NamedParameterJdbcTemplate namedParameterJdbcTemplate; + + + public CommunityArticleDao(final NamedParameterJdbcTemplate namedParameterJdbcTemplate) { + this.namedParameterJdbcTemplate = namedParameterJdbcTemplate; + } + + public Optional<CommunityArticleData> getById(final Long articleId) { + final String sql = "SELECT article.id, article.title, article.content, " + + "article.created_date, article.last_modified_date, " + + "member.github_id, member.username, member.image_url, member.profile_url " + + "FROM article JOIN member ON article.author_id = member.id " + + "WHERE article.id = :articleId"; + final Map<String, Long> params = Map.of("articleId", articleId); + return namedParameterJdbcTemplate.query(sql, params, ROW_MAPPER).stream().findAny(); + } + + public Page<CommunityArticleData> getAllByStudyId(final Long studyId, final Pageable pageable) { + final List<CommunityArticleData> content = getContent(studyId, pageable); + final int totalCount = getTotalCount(studyId); + return new PageImpl<>(content, pageable, totalCount); + } + + private List<CommunityArticleData> getContent(final Long studyId, final Pageable pageable) { + final String sql = "SELECT article.id, article.title, article.content, " + + "article.created_date, article.last_modified_date, " + + "member.github_id, member.username, member.image_url, member.profile_url " + + "FROM article JOIN member ON article.author_id = member.id " + + "WHERE article.study_id = :studyId " + + "ORDER BY created_date DESC, id DESC " + + "LIMIT :size OFFSET :offset"; + + final Map<String, Object> params = Map.of( + "studyId", studyId, + "size", pageable.getPageSize(), + "offset", pageable.getOffset() + ); + + return namedParameterJdbcTemplate.query(sql, params, ROW_MAPPER); + } + + private int getTotalCount(final Long studyId) { + final String sql = "SELECT count(article.id) FROM article WHERE article.study_id = :studyId"; + final Map<String, Long> param = Map.of("studyId", studyId); + return namedParameterJdbcTemplate.queryForObject(sql, param, (rs, rn) -> rs.getInt(1)); + } +} diff --git a/backend/src/main/java/com/woowacourse/moamoa/community/query/data/CommunityArticleData.java b/backend/src/main/java/com/woowacourse/moamoa/community/query/data/CommunityArticleData.java new file mode 100644 index 000000000..8814fb211 --- /dev/null +++ b/backend/src/main/java/com/woowacourse/moamoa/community/query/data/CommunityArticleData.java @@ -0,0 +1,27 @@ +package com.woowacourse.moamoa.community.query.data; + +import com.woowacourse.moamoa.member.query.data.MemberData; +import java.time.LocalDate; +import lombok.Getter; + +@Getter +public class CommunityArticleData { + + private final Long id; + private final MemberData memberData; + private final String title; + private final String content; + private final LocalDate createdDate; + private final LocalDate lastModifiedDate; + + public CommunityArticleData(final Long id, final MemberData memberData, final String title, final String content, + final LocalDate createdDate, + final LocalDate lastModifiedDate) { + this.id = id; + this.memberData = memberData; + this.title = title; + this.content = content; + this.createdDate = createdDate; + this.lastModifiedDate = lastModifiedDate; + } +} diff --git a/backend/src/main/java/com/woowacourse/moamoa/community/service/CommunityArticleService.java b/backend/src/main/java/com/woowacourse/moamoa/community/service/CommunityArticleService.java new file mode 100644 index 000000000..b4b272591 --- /dev/null +++ b/backend/src/main/java/com/woowacourse/moamoa/community/service/CommunityArticleService.java @@ -0,0 +1,111 @@ +package com.woowacourse.moamoa.community.service; + +import com.woowacourse.moamoa.community.domain.CommunityArticle; +import com.woowacourse.moamoa.community.domain.repository.CommunityArticleRepository; +import com.woowacourse.moamoa.community.query.CommunityArticleDao; +import com.woowacourse.moamoa.community.query.data.CommunityArticleData; +import com.woowacourse.moamoa.community.service.exception.ArticleNotFoundException; +import com.woowacourse.moamoa.community.service.exception.NotArticleAuthorException; +import com.woowacourse.moamoa.community.service.exception.NotRelatedArticleException; +import com.woowacourse.moamoa.community.service.exception.UneditableArticleException; +import com.woowacourse.moamoa.community.service.exception.UnviewableArticleException; +import com.woowacourse.moamoa.community.service.request.ArticleRequest; +import com.woowacourse.moamoa.community.service.response.ArticleResponse; +import com.woowacourse.moamoa.community.service.response.ArticleSummariesResponse; +import com.woowacourse.moamoa.community.service.response.ArticleSummaryResponse; +import com.woowacourse.moamoa.member.domain.Member; +import com.woowacourse.moamoa.member.domain.repository.MemberRepository; +import com.woowacourse.moamoa.member.service.exception.MemberNotFoundException; +import com.woowacourse.moamoa.member.service.exception.NotParticipatedMemberException; +import com.woowacourse.moamoa.study.domain.Study; +import com.woowacourse.moamoa.study.domain.repository.StudyRepository; +import com.woowacourse.moamoa.study.service.exception.StudyNotFoundException; +import java.util.List; +import java.util.stream.Collectors; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional(readOnly = true) +public class CommunityArticleService { + + private final MemberRepository memberRepository; + private final StudyRepository studyRepository; + private final CommunityArticleRepository communityArticleRepository; + private final CommunityArticleDao communityArticleDao; + + public CommunityArticleService(final MemberRepository memberRepository, + final StudyRepository studyRepository, + final CommunityArticleRepository communityArticleRepository, + final CommunityArticleDao communityArticleDao) { + this.memberRepository = memberRepository; + this.studyRepository = studyRepository; + this.communityArticleRepository = communityArticleRepository; + this.communityArticleDao = communityArticleDao; + } + + @Transactional + public CommunityArticle createArticle(final Long memberId, final Long studyId, + final ArticleRequest request) { + final Member member = memberRepository.findById(memberId).orElseThrow(MemberNotFoundException::new); + final Study study = studyRepository.findById(studyId).orElseThrow(StudyNotFoundException::new); + + return communityArticleRepository.save(CommunityArticle.write(member, study, request)); + } + + public ArticleResponse getArticle(final Long memberId, final Long studyId, final Long articleId) { + final CommunityArticle article = communityArticleRepository.findById(articleId) + .orElseThrow(() -> new ArticleNotFoundException(articleId)); + + if (!article.isViewableBy(studyId, memberId)) { + throw new UnviewableArticleException(studyId, memberId); + } + + final CommunityArticleData data = communityArticleDao.getById(articleId) + .orElseThrow(() -> new ArticleNotFoundException(articleId)); + return new ArticleResponse(data); + } + + @Transactional + public void deleteArticle(final Long memberId, final Long studyId, final Long articleId) { + final CommunityArticle article = communityArticleRepository.findById(articleId) + .orElseThrow(() -> new ArticleNotFoundException(articleId)); + + if (!article.isEditableBy(studyId, memberId)) { + throw new UneditableArticleException(); + } + + communityArticleRepository.deleteById(articleId); + } + + public ArticleSummariesResponse getArticles(final Long memberId, final Long studyId, final Pageable pageable) { + final Study study = studyRepository.findById(studyId).orElseThrow(StudyNotFoundException::new); + + if (!study.isParticipant(memberId)) { + throw new UnviewableArticleException(studyId, memberId); + } + + final Page<CommunityArticleData> page = communityArticleDao.getAllByStudyId(studyId, pageable); + + final List<ArticleSummaryResponse> articles = page.getContent().stream() + .map(ArticleSummaryResponse::new) + .collect(Collectors.toList()); + + return new ArticleSummariesResponse(articles, page.getNumber(), page.getTotalPages() - 1, page.getTotalElements()); + } + + @Transactional + public void updateArticle(final Long memberId, final Long studyId, final Long articleId, + final ArticleRequest request) { + final CommunityArticle article = communityArticleRepository.findById(articleId) + .orElseThrow(() -> new ArticleNotFoundException(articleId)); + + if (!article.isEditableBy(studyId, memberId)) { + throw new UneditableArticleException(); + } + + article.update(request.getTitle(), request.getContent()); + } +} diff --git a/backend/src/main/java/com/woowacourse/moamoa/community/service/exception/ArticleNotFoundException.java b/backend/src/main/java/com/woowacourse/moamoa/community/service/exception/ArticleNotFoundException.java new file mode 100644 index 000000000..5977414b7 --- /dev/null +++ b/backend/src/main/java/com/woowacourse/moamoa/community/service/exception/ArticleNotFoundException.java @@ -0,0 +1,10 @@ +package com.woowacourse.moamoa.community.service.exception; + +import com.woowacourse.moamoa.common.exception.NotFoundException; + +public class ArticleNotFoundException extends NotFoundException { + + public ArticleNotFoundException(long articleId) { + super(articleId + "의 식별자를 가진 게시글이 존재하지 않습니다."); + } +} diff --git a/backend/src/main/java/com/woowacourse/moamoa/community/service/exception/NotArticleAuthorException.java b/backend/src/main/java/com/woowacourse/moamoa/community/service/exception/NotArticleAuthorException.java new file mode 100644 index 000000000..c66e9115b --- /dev/null +++ b/backend/src/main/java/com/woowacourse/moamoa/community/service/exception/NotArticleAuthorException.java @@ -0,0 +1,10 @@ +package com.woowacourse.moamoa.community.service.exception; + +import com.woowacourse.moamoa.common.exception.BadRequestException; + +public class NotArticleAuthorException extends BadRequestException { + + public NotArticleAuthorException(final Long articleId, final Long authorId) { + super("게시글[" + articleId + "]은 작성자[" + authorId + "]가 아닙니다."); + } +} diff --git a/backend/src/main/java/com/woowacourse/moamoa/community/service/exception/NotRelatedArticleException.java b/backend/src/main/java/com/woowacourse/moamoa/community/service/exception/NotRelatedArticleException.java new file mode 100644 index 000000000..f66eee223 --- /dev/null +++ b/backend/src/main/java/com/woowacourse/moamoa/community/service/exception/NotRelatedArticleException.java @@ -0,0 +1,10 @@ +package com.woowacourse.moamoa.community.service.exception; + +import com.woowacourse.moamoa.common.exception.BadRequestException; + +public class NotRelatedArticleException extends BadRequestException { + + public NotRelatedArticleException(final Long studyId, final Long articleId) { + super("스터디[" + studyId + "] 에 작성된 게시글[" + articleId + "] 가 없습니다."); + } +} diff --git a/backend/src/main/java/com/woowacourse/moamoa/community/service/exception/UneditableArticleException.java b/backend/src/main/java/com/woowacourse/moamoa/community/service/exception/UneditableArticleException.java new file mode 100644 index 000000000..31d2cc696 --- /dev/null +++ b/backend/src/main/java/com/woowacourse/moamoa/community/service/exception/UneditableArticleException.java @@ -0,0 +1,10 @@ +package com.woowacourse.moamoa.community.service.exception; + +import com.woowacourse.moamoa.common.exception.BadRequestException; + +public class UneditableArticleException extends BadRequestException { + + public UneditableArticleException() { + super(""); + } +} diff --git a/backend/src/main/java/com/woowacourse/moamoa/community/service/exception/UnviewableArticleException.java b/backend/src/main/java/com/woowacourse/moamoa/community/service/exception/UnviewableArticleException.java new file mode 100644 index 000000000..875e9155f --- /dev/null +++ b/backend/src/main/java/com/woowacourse/moamoa/community/service/exception/UnviewableArticleException.java @@ -0,0 +1,10 @@ +package com.woowacourse.moamoa.community.service.exception; + +import com.woowacourse.moamoa.common.exception.BadRequestException; + +public class UnviewableArticleException extends BadRequestException { + + public UnviewableArticleException(final Long studyId, final Long articleId) { + super("스터디[" + studyId + "] 에 작성된 게시글[" + articleId + "]을 볼 수 없습니다."); + } +} diff --git a/backend/src/main/java/com/woowacourse/moamoa/community/service/request/ArticleRequest.java b/backend/src/main/java/com/woowacourse/moamoa/community/service/request/ArticleRequest.java new file mode 100644 index 000000000..97f7424f8 --- /dev/null +++ b/backend/src/main/java/com/woowacourse/moamoa/community/service/request/ArticleRequest.java @@ -0,0 +1,24 @@ +package com.woowacourse.moamoa.community.service.request; + +import javax.validation.constraints.NotBlank; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.validator.constraints.Length; + +@Getter +@NoArgsConstructor +public class ArticleRequest { + + @NotBlank(message = "내용을 입력해 주세요.") + @Length(max = 30) + private String title; + + @NotBlank(message = "내용을 입력해 주세요.") + @Length(max = 5000) + private String content; + + public ArticleRequest(final String title, final String content) { + this.title = title; + this.content = content; + } +} diff --git a/backend/src/main/java/com/woowacourse/moamoa/community/service/response/ArticleResponse.java b/backend/src/main/java/com/woowacourse/moamoa/community/service/response/ArticleResponse.java new file mode 100644 index 000000000..409c7f593 --- /dev/null +++ b/backend/src/main/java/com/woowacourse/moamoa/community/service/response/ArticleResponse.java @@ -0,0 +1,31 @@ +package com.woowacourse.moamoa.community.service.response; + +import com.woowacourse.moamoa.community.query.data.CommunityArticleData; +import java.time.LocalDate; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@Builder +@EqualsAndHashCode +@AllArgsConstructor +@NoArgsConstructor +@Getter +@ToString +public class ArticleResponse { + + private Long id; + private AuthorResponse author; + private String title; + private String content; + private LocalDate createdDate; + private LocalDate lastModifiedDate; + + public ArticleResponse(CommunityArticleData data) { + this(data.getId(), new AuthorResponse(data.getMemberData()), data.getTitle(), data.getContent(), + data.getCreatedDate(), data.getLastModifiedDate()); + } +} diff --git a/backend/src/main/java/com/woowacourse/moamoa/community/service/response/ArticleSummariesResponse.java b/backend/src/main/java/com/woowacourse/moamoa/community/service/response/ArticleSummariesResponse.java new file mode 100644 index 000000000..df05e4b65 --- /dev/null +++ b/backend/src/main/java/com/woowacourse/moamoa/community/service/response/ArticleSummariesResponse.java @@ -0,0 +1,28 @@ +package com.woowacourse.moamoa.community.service.response; + +import java.util.List; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@NoArgsConstructor +@Getter +@EqualsAndHashCode +@ToString +public class ArticleSummariesResponse { + + private List<ArticleSummaryResponse> articles; + private long currentPage; + private long lastPage; + private long totalCount; + + public ArticleSummariesResponse( + final List<ArticleSummaryResponse> articles, final long currentPage, final long lastPage, + final long totalCount) { + this.articles = articles; + this.currentPage = currentPage; + this.lastPage = lastPage; + this.totalCount = totalCount; + } +} diff --git a/backend/src/main/java/com/woowacourse/moamoa/community/service/response/ArticleSummaryResponse.java b/backend/src/main/java/com/woowacourse/moamoa/community/service/response/ArticleSummaryResponse.java new file mode 100644 index 000000000..1f5bc7543 --- /dev/null +++ b/backend/src/main/java/com/woowacourse/moamoa/community/service/response/ArticleSummaryResponse.java @@ -0,0 +1,39 @@ +package com.woowacourse.moamoa.community.service.response; + +import com.woowacourse.moamoa.community.query.data.CommunityArticleData; +import java.time.LocalDate; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@NoArgsConstructor +@Getter +@ToString +@EqualsAndHashCode +public class ArticleSummaryResponse { + + private long id; + private AuthorResponse author; + private String title; + private LocalDate createdDate; + private LocalDate lastModifiedDate; + + public ArticleSummaryResponse(CommunityArticleData data) { + this.id = data.getId(); + this.author = new AuthorResponse(data.getMemberData()); + this.title = data.getTitle(); + this.createdDate = data.getCreatedDate(); + this.lastModifiedDate = data.getLastModifiedDate(); + } + + public ArticleSummaryResponse(final long id, final AuthorResponse author, final String title, + final LocalDate createdDate, + final LocalDate lastModifiedDate) { + this.id = id; + this.author = author; + this.title = title; + this.createdDate = createdDate; + this.lastModifiedDate = lastModifiedDate; + } +} diff --git a/backend/src/main/java/com/woowacourse/moamoa/community/service/response/AuthorResponse.java b/backend/src/main/java/com/woowacourse/moamoa/community/service/response/AuthorResponse.java new file mode 100644 index 000000000..e83a5149a --- /dev/null +++ b/backend/src/main/java/com/woowacourse/moamoa/community/service/response/AuthorResponse.java @@ -0,0 +1,25 @@ +package com.woowacourse.moamoa.community.service.response; + +import com.woowacourse.moamoa.member.query.data.MemberData; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@AllArgsConstructor +@Getter +@EqualsAndHashCode +@NoArgsConstructor +@ToString +public class AuthorResponse { + + private long id; + private String username; + private String imageUrl; + private String profileUrl; + + public AuthorResponse(MemberData memberData) { + this(memberData.getGithubId(), memberData.getUsername(), memberData.getImageUrl(), memberData.getProfileUrl()); + } +} diff --git a/backend/src/main/java/com/woowacourse/moamoa/member/domain/repository/MemberRepository.java b/backend/src/main/java/com/woowacourse/moamoa/member/domain/repository/MemberRepository.java index f76d3dc2d..bc4fe708a 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/member/domain/repository/MemberRepository.java +++ b/backend/src/main/java/com/woowacourse/moamoa/member/domain/repository/MemberRepository.java @@ -11,4 +11,6 @@ public interface MemberRepository { Optional<Member> findByGithubId(Long githubId); List<Member> findAllById(Iterable<Long> ids); + + Optional<Member> findById(Long id); } diff --git a/backend/src/main/java/com/woowacourse/moamoa/study/domain/Participants.java b/backend/src/main/java/com/woowacourse/moamoa/study/domain/Participants.java index e01c511e2..b364f303e 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/study/domain/Participants.java +++ b/backend/src/main/java/com/woowacourse/moamoa/study/domain/Participants.java @@ -41,7 +41,7 @@ public Participants(Long ownerId, final Set<Long> participants) { } void participate(Long memberId) { - if (isAlreadyParticipated(memberId)) { + if (isParticipation(memberId)) { throw new FailureParticipationException(); } @@ -49,12 +49,12 @@ void participate(Long memberId) { size = size + 1; } - boolean isAlreadyParticipated(Long memberId) { - return participants.contains(new Participant(memberId)) || ownerId.equals(memberId); + boolean isParticipation(Long memberId) { + return participants.contains(new Participant(memberId)) || isOwner(memberId); } - public boolean isParticipate(Long memberId) { - return participants.contains(new Participant(memberId)); + boolean isOwner(Long memberId) { + return ownerId.equals(memberId); } int getSize() { diff --git a/backend/src/main/java/com/woowacourse/moamoa/study/domain/Study.java b/backend/src/main/java/com/woowacourse/moamoa/study/domain/Study.java index 7f2c49778..5d01c1ffc 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/study/domain/Study.java +++ b/backend/src/main/java/com/woowacourse/moamoa/study/domain/Study.java @@ -5,10 +5,8 @@ import com.woowacourse.moamoa.study.domain.exception.InvalidPeriodException; import com.woowacourse.moamoa.study.service.exception.FailureParticipationException; -import com.woowacourse.moamoa.study.service.response.MyRoleResponse; import java.time.LocalDate; import java.time.LocalDateTime; -import java.util.Objects; import javax.persistence.Column; import javax.persistence.Embedded; import javax.persistence.Entity; @@ -50,7 +48,7 @@ public Study(final Content content, final Participants participants, final Recru this(null, content, participants, recruitPlanner, studyPlanner, attachedTags, createdAt); } - private Study(final Long id, final Content content, final Participants participants, + public Study(final Long id, final Content content, final Participants participants, final RecruitPlanner recruitPlanner, final StudyPlanner studyPlanner, final AttachedTags attachedTags, final LocalDateTime createdAt ) { @@ -84,8 +82,12 @@ private boolean isRecruitedOrStartStudyBeforeCreatedAt(final RecruitPlanner recr recruitPlanner.isRecruitedBeforeThan(createdAt.toLocalDate()); } + public boolean isParticipant(final Long memberId) { + return participants.isParticipation(memberId); + } + public boolean isWritableReviews(final Long memberId) { - return participants.isAlreadyParticipated(memberId) && !studyPlanner.isPreparing(); + return participants.isParticipation(memberId) && !studyPlanner.isPreparing(); } public void participate(final Long memberId) { @@ -119,10 +121,10 @@ private boolean isFullOfCapacity() { } public MemberRole getRole(final Long memberId) { - if (Objects.equals(participants.getOwnerId(), memberId)) { + if (participants.isOwner(memberId)) { return MemberRole.OWNER; } - if (participants.isParticipate(memberId)) { + if (participants.isParticipation(memberId)) { return MemberRole.MEMBER; } return MemberRole.NON_MEMBER; diff --git a/backend/src/test/java/com/woowacourse/acceptance/AcceptanceTest.java b/backend/src/test/java/com/woowacourse/acceptance/AcceptanceTest.java index e0fdd177b..a80fc3d64 100644 --- a/backend/src/test/java/com/woowacourse/acceptance/AcceptanceTest.java +++ b/backend/src/test/java/com/woowacourse/acceptance/AcceptanceTest.java @@ -96,6 +96,7 @@ void mockingGithubServer() { @AfterEach void tearDown() { jdbcTemplate.update("SET REFERENTIAL_INTEGRITY FALSE"); + jdbcTemplate.update("TRUNCATE TABLE article"); jdbcTemplate.update("TRUNCATE TABLE member"); jdbcTemplate.update("TRUNCATE TABLE study_tag"); jdbcTemplate.update("TRUNCATE TABLE study_member"); diff --git a/backend/src/test/java/com/woowacourse/acceptance/fixture/TagFixtures.java b/backend/src/test/java/com/woowacourse/acceptance/fixture/TagFixtures.java index 63026e661..188dc167c 100644 --- a/backend/src/test/java/com/woowacourse/acceptance/fixture/TagFixtures.java +++ b/backend/src/test/java/com/woowacourse/acceptance/fixture/TagFixtures.java @@ -1,12 +1,5 @@ package com.woowacourse.acceptance.fixture; -import static com.woowacourse.fixtures.CategoryFixtures.AREA_응답; -import static com.woowacourse.fixtures.CategoryFixtures.GENERATION_응답; -import static com.woowacourse.fixtures.CategoryFixtures.SUBJECT_응답; - -import com.woowacourse.moamoa.tag.query.response.TagData; -import com.woowacourse.moamoa.tag.query.response.TagSummaryData; - public class TagFixtures { public static final Long 자바_태그_ID = 1L; diff --git a/backend/src/test/java/com/woowacourse/acceptance/steps/StudyRelatedSteps.java b/backend/src/test/java/com/woowacourse/acceptance/steps/StudyRelatedSteps.java index f9759364d..9ffbcd5fe 100644 --- a/backend/src/test/java/com/woowacourse/acceptance/steps/StudyRelatedSteps.java +++ b/backend/src/test/java/com/woowacourse/acceptance/steps/StudyRelatedSteps.java @@ -3,13 +3,24 @@ import static org.springframework.http.HttpHeaders.AUTHORIZATION; import static org.springframework.http.HttpHeaders.CONTENT_TYPE; import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; +import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; +import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; +import static org.springframework.restdocs.headers.HeaderDocumentation.responseHeaders; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; +import static org.springframework.restdocs.restassured3.RestAssuredRestDocumentation.document; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.woowacourse.moamoa.community.service.request.ArticleRequest; import com.woowacourse.moamoa.review.service.request.WriteReviewRequest; import io.restassured.RestAssured; import org.junit.jupiter.api.Assertions; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; +import org.springframework.restdocs.payload.JsonFieldType; public class StudyRelatedSteps extends Steps { @@ -48,4 +59,23 @@ public class StudyRelatedSteps extends Steps { return -1; } } + + public long 게시글을_작성한다(final String title, final String content) { + try { + final String location = RestAssured.given().log().all() + .header(org.apache.http.HttpHeaders.AUTHORIZATION, token) + .header(org.apache.http.HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) + .body(objectMapper.writeValueAsString(new ArticleRequest(title, content))) + .pathParam("study-id", studyId) + .when().log().all() + .post("/api/studies/{study-id}/community/articles") + .then().log().all() + .statusCode(HttpStatus.CREATED.value()) + .extract().header(HttpHeaders.LOCATION); + return Long.parseLong(location.replaceAll("/api/studies/" + studyId + "/community/articles/", "")); + } catch (Exception e) { + Assertions.fail("게시글 작성 실패"); + return -1; + } + } } diff --git a/backend/src/test/java/com/woowacourse/acceptance/test/community/CommunityAcceptanceTest.java b/backend/src/test/java/com/woowacourse/acceptance/test/community/CommunityAcceptanceTest.java new file mode 100644 index 000000000..353ebb568 --- /dev/null +++ b/backend/src/test/java/com/woowacourse/acceptance/test/community/CommunityAcceptanceTest.java @@ -0,0 +1,333 @@ +package com.woowacourse.acceptance.test.community; + +import static com.woowacourse.acceptance.fixture.MemberFixtures.그린론_깃허브_ID; +import static com.woowacourse.acceptance.fixture.MemberFixtures.그린론_이름; +import static com.woowacourse.acceptance.fixture.MemberFixtures.그린론_이미지_URL; +import static com.woowacourse.acceptance.fixture.MemberFixtures.그린론_프로필_URL; +import static com.woowacourse.acceptance.fixture.MemberFixtures.베루스_깃허브_ID; +import static com.woowacourse.acceptance.fixture.MemberFixtures.베루스_이름; +import static com.woowacourse.acceptance.fixture.MemberFixtures.베루스_이미지_URL; +import static com.woowacourse.acceptance.fixture.MemberFixtures.베루스_프로필_URL; +import static com.woowacourse.acceptance.steps.LoginSteps.그린론이; +import static com.woowacourse.acceptance.steps.LoginSteps.베루스가; +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; +import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; +import static org.springframework.restdocs.headers.HeaderDocumentation.responseHeaders; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; +import static org.springframework.restdocs.request.RequestDocumentation.requestParameters; +import static org.springframework.restdocs.restassured3.RestAssuredRestDocumentation.document; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.woowacourse.acceptance.AcceptanceTest; +import com.woowacourse.moamoa.community.service.request.ArticleRequest; +import com.woowacourse.moamoa.community.service.response.ArticleResponse; +import com.woowacourse.moamoa.community.service.response.ArticleSummariesResponse; +import com.woowacourse.moamoa.community.service.response.ArticleSummaryResponse; +import com.woowacourse.moamoa.community.service.response.AuthorResponse; +import io.restassured.RestAssured; +import java.time.LocalDate; +import java.util.List; +import org.apache.http.HttpHeaders; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.restdocs.payload.JsonFieldType; + +public class CommunityAcceptanceTest extends AcceptanceTest { + + @DisplayName("커뮤니티에 글을 작성한다.") + @Test + void writeArticleToCommunity() throws Exception { + // arrange + long 스터디_ID = 그린론이().로그인하고().자바_스터디를().시작일자는(LocalDate.now()).생성한다(); + String 토큰 = 그린론이().로그인한다(); + ArticleRequest request = new ArticleRequest("게시글 제목", "게시글 내용"); + + // act + final String location = RestAssured.given(spec).log().all() + .header(HttpHeaders.AUTHORIZATION, 토큰) + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) + .body(objectMapper.writeValueAsString(request)) + .pathParam("study-id", 스터디_ID) + .when().log().all() + .filter(document("write/article", + requestHeaders( + headerWithName(HttpHeaders.AUTHORIZATION).description("JWT 토큰") + ), + pathParameters( + parameterWithName("study-id").description("스터디 식별 ID") + ), + requestFields( + fieldWithPath("title").type(JsonFieldType.STRING).description("게시글 제목"), + fieldWithPath("content").type(JsonFieldType.STRING).description("게시글 내용") + ), + responseHeaders( + headerWithName(HttpHeaders.LOCATION).description("생성된 게시글 url"), + headerWithName("Access-Control-Allow-Headers").description("접근 가능한 헤더") + )) + ) + .post("/api/studies/{study-id}/community/articles") + .then().log().all() + .statusCode(HttpStatus.CREATED.value()) + .extract() + .header(HttpHeaders.LOCATION); + + // assert + Long articleId = Long.valueOf(location.split("/")[6]); + + final ArticleResponse actualResponse = RestAssured + .given(spec).log().all() + .header(HttpHeaders.AUTHORIZATION, 토큰) + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) + .pathParam("study-id", 스터디_ID) + .pathParam("article-id", articleId) + .filter(document("get/article", + requestHeaders( + headerWithName(HttpHeaders.AUTHORIZATION).description("JWT 토큰") + ), + pathParameters( + parameterWithName("study-id").description("스터디 식별 ID"), + parameterWithName("article-id").description("게시글 식별 ID") + ), + responseFields( + fieldWithPath("id").type(JsonFieldType.NUMBER).description("게시글 식별 ID"), + fieldWithPath("author").type(JsonFieldType.OBJECT).description("작성자"), + fieldWithPath("author.id").type(JsonFieldType.NUMBER).description("작성자 github ID"), + fieldWithPath("author.username").type(JsonFieldType.STRING) + .description("작성자 github 사용자 이름"), + fieldWithPath("author.imageUrl").type(JsonFieldType.STRING) + .description("작성자 github 이미지 URL"), + fieldWithPath("author.profileUrl").type(JsonFieldType.STRING) + .description("작성자 github 프로필 URL"), + fieldWithPath("title").type(JsonFieldType.STRING).description("게시글 제목"), + fieldWithPath("content").type(JsonFieldType.STRING).description("게시글 내용"), + fieldWithPath("createdDate").type(JsonFieldType.STRING).description("게시글 작성일"), + fieldWithPath("lastModifiedDate").type(JsonFieldType.STRING).description("게시글 수정일") + ) + )) + .when().log().all() + .get("/api/studies/{study-id}/community/articles/{article-id}") + .then().log().all() + .statusCode(HttpStatus.OK.value()) + .extract().as(ArticleResponse.class); + + final ArticleResponse expectedResponse = ArticleResponse.builder() + .id(articleId) + .author(new AuthorResponse(그린론_깃허브_ID, 그린론_이름, 그린론_이미지_URL, 그린론_프로필_URL)) + .title("게시글 제목") + .content("게시글 내용") + .createdDate(LocalDate.now()) + .lastModifiedDate(LocalDate.now()) + .build(); + + assertThat(actualResponse).isEqualTo(expectedResponse); + } + + @DisplayName("스터디 커뮤니티 게시글을 삭제한다.") + @Test + void deleteCommunityArticle() { + // arrange + long 스터디_ID = 그린론이().로그인하고().자바_스터디를().시작일자는(LocalDate.now()).생성한다(); + long 게시글_ID = 그린론이().로그인하고().스터디에(스터디_ID).게시글을_작성한다("게시글 제목", "게시글 내용"); + String 토큰 = 그린론이().로그인한다(); + + // act + RestAssured.given(spec).log().all() + .header(HttpHeaders.AUTHORIZATION, 토큰) + .pathParam("study-id", 스터디_ID) + .pathParam("article-id", 게시글_ID) + .filter(document("delete/article", + requestHeaders( + headerWithName(HttpHeaders.AUTHORIZATION).description("JWT 토큰") + ), + pathParameters( + parameterWithName("study-id").description("스터디 식별 번호"), + parameterWithName("article-id").description("게시글 식별 번호") + ) + )) + .when().log().all() + .delete("/api/studies/{study-id}/community/articles/{article-id}") + .then().log().all() + .statusCode(HttpStatus.NO_CONTENT.value()); + + // assert + RestAssured + .given(spec).log().all() + .header(HttpHeaders.AUTHORIZATION, 토큰) + .pathParam("study-id", 스터디_ID) + .pathParam("article-id", 게시글_ID) + .when().log().all() + .get("/api/studies/{study-id}/community/articles/{article-id}") + .then().log().all() + .statusCode(HttpStatus.NOT_FOUND.value()); + } + + @DisplayName("스터디 커뮤니티 전체 게시글을 조회한다.") + @Test + void getStudyCommunityArticles() { + // arrange + long 자바_스터디_ID = 그린론이().로그인하고().자바_스터디를().시작일자는(LocalDate.now()).생성한다(); + 그린론이().로그인하고().스터디에(자바_스터디_ID).게시글을_작성한다("자바 게시글 제목1", "자바 게시글 내용1"); + + 베루스가().로그인하고().스터디에(자바_스터디_ID).참여한다(); + long 자바_게시글2_ID = 베루스가().로그인하고().스터디에(자바_스터디_ID).게시글을_작성한다("자바 게시글 제목2", "자바 게시글 내용2"); + long 자바_게시글3_ID = 베루스가().로그인하고().스터디에(자바_스터디_ID).게시글을_작성한다("자바 게시글 제목3", "자바 게시글 내용3"); + long 자바_게시글4_ID = 베루스가().로그인하고().스터디에(자바_스터디_ID).게시글을_작성한다("자바 게시글 제목4", "자바 게시글 내용4"); + + long 리액트_스터디_ID = 베루스가().로그인하고().리액트_스터디를().시작일자는(LocalDate.now()).생성한다(); + 베루스가().로그인하고().스터디에(리액트_스터디_ID).게시글을_작성한다("리액트 게시글 제목", "리액트 게시글 내용"); + + String 토큰 = 그린론이().로그인한다(); + + // act + final ArticleSummariesResponse response = RestAssured.given(spec).log().all() + .header(HttpHeaders.AUTHORIZATION, 토큰) + .pathParam("study-id", 자바_스터디_ID) + .queryParam("page", 0) + .queryParam("size", 3) + .filter(document("get/articles", + requestHeaders( + headerWithName(HttpHeaders.AUTHORIZATION).description("Jwt 토큰") + ), + pathParameters( + parameterWithName("study-id").description("스터디 ID") + ), + requestParameters( + parameterWithName("page").description("페이지"), + parameterWithName("size").description("사이즈") + ), + responseFields( + fieldWithPath("articles").type(JsonFieldType.ARRAY).description("게시물 목록"), + fieldWithPath("articles[].id").type(JsonFieldType.NUMBER).description("게시글 식별 ID"), + fieldWithPath("articles[].author").type(JsonFieldType.OBJECT).description("작성자"), + fieldWithPath("articles[].author.id").type(JsonFieldType.NUMBER) + .description("작성자 github ID"), + fieldWithPath("articles[].author.username").type(JsonFieldType.STRING) + .description("작성자 github 사용자 이름"), + fieldWithPath("articles[].author.imageUrl").type(JsonFieldType.STRING) + .description("작성자 github 이미지 URL"), + fieldWithPath("articles[].author.profileUrl").type(JsonFieldType.STRING) + .description("작성자 github 프로필 URL"), + fieldWithPath("articles[].title").type(JsonFieldType.STRING).description("게시글 제목"), + fieldWithPath("articles[].createdDate").type(JsonFieldType.STRING) + .description("게시글 작성일"), + fieldWithPath("articles[].lastModifiedDate").type(JsonFieldType.STRING) + .description("게시글 수정일"), + fieldWithPath("currentPage").type(JsonFieldType.NUMBER).description("현재 페이지 번호"), + fieldWithPath("lastPage").type(JsonFieldType.NUMBER).description("마지막 페이지 번호"), + fieldWithPath("totalCount").type(JsonFieldType.NUMBER).description("게시글 전체 갯수") + ) + )) + .when().log().all() + .get("/api/studies/{study-id}/community/articles") + .then().log().all() + .statusCode(HttpStatus.OK.value()) + .extract().as(ArticleSummariesResponse.class); + + // assert + AuthorResponse 베루스 = new AuthorResponse(베루스_깃허브_ID, 베루스_이름, 베루스_이미지_URL, 베루스_프로필_URL); + + List<ArticleSummaryResponse> articles = List.of( + new ArticleSummaryResponse(자바_게시글4_ID, 베루스, "자바 게시글 제목4", LocalDate.now(), LocalDate.now()), + new ArticleSummaryResponse(자바_게시글3_ID, 베루스, "자바 게시글 제목3", LocalDate.now(), LocalDate.now()), + new ArticleSummaryResponse(자바_게시글2_ID, 베루스, "자바 게시글 제목2", LocalDate.now(), LocalDate.now()) + ); + + assertThat(response).isEqualTo(new ArticleSummariesResponse(articles, 0, 1, 4)); + } + + @DisplayName("스터디 커뮤니티 전체 게시글을 기본 페이징 정보로 조회한다.") + @Test + void getStudyCommunityArticlesByDefaultPageable() { + // arrange + long 스터디_ID = 그린론이().로그인하고().자바_스터디를().시작일자는(LocalDate.now()).생성한다(); + long 게시글1_ID = 그린론이().로그인하고().스터디에(스터디_ID).게시글을_작성한다("자바 게시글 제목1", "자바 게시글 내용1"); + long 게시글2_ID = 그린론이().로그인하고().스터디에(스터디_ID).게시글을_작성한다("자바 게시글 제목2", "자바 게시글 내용2"); + long 게시글3_ID = 그린론이().로그인하고().스터디에(스터디_ID).게시글을_작성한다("자바 게시글 제목3", "자바 게시글 내용3"); + long 게시글4_ID = 그린론이().로그인하고().스터디에(스터디_ID).게시글을_작성한다("자바 게시글 제목4", "자바 게시글 내용4"); + + String 토큰 = 그린론이().로그인한다(); + + // act + final ArticleSummariesResponse response = RestAssured.given(spec).log().all() + .header(HttpHeaders.AUTHORIZATION, 토큰) + .pathParam("study-id", 스터디_ID) + .when().log().all() + .get("/api/studies/{study-id}/community/articles") + .then().log().all() + .statusCode(HttpStatus.OK.value()) + .extract().as(ArticleSummariesResponse.class); + + // assert + AuthorResponse 그린론 = new AuthorResponse(그린론_깃허브_ID, 그린론_이름, 그린론_이미지_URL, 그린론_프로필_URL); + + List<ArticleSummaryResponse> articles = List.of( + new ArticleSummaryResponse(게시글4_ID, 그린론, "자바 게시글 제목4", LocalDate.now(), LocalDate.now()), + new ArticleSummaryResponse(게시글3_ID, 그린론, "자바 게시글 제목3", LocalDate.now(), LocalDate.now()), + new ArticleSummaryResponse(게시글2_ID, 그린론, "자바 게시글 제목2", LocalDate.now(), LocalDate.now()), + new ArticleSummaryResponse(게시글1_ID, 그린론, "자바 게시글 제목1", LocalDate.now(), LocalDate.now()) + ); + + assertThat(response).isEqualTo(new ArticleSummariesResponse(articles, 0, 0, 4)); + } + + @DisplayName("커뮤니티 글을 수정한다.") + @Test + void updateArticleToCommunity() throws JsonProcessingException { + // arrange + long 스터디_ID = 그린론이().로그인하고().자바_스터디를().시작일자는(LocalDate.now()).생성한다(); + long 게시글_ID = 그린론이().로그인하고().스터디에(스터디_ID).게시글을_작성한다("게시글 제목", "게시글 내용"); + String 토큰 = 그린론이().로그인한다(); + + final ArticleRequest request = new ArticleRequest("게시글 제목 수정", "게시글 내용 수정"); + + // act + RestAssured.given(spec).log().all() + .header(HttpHeaders.AUTHORIZATION, 토큰) + .pathParam("study-id", 스터디_ID) + .pathParam("article-id", 게시글_ID) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .body(objectMapper.writeValueAsString(request)) + .filter(document("update/article", + requestHeaders( + headerWithName(HttpHeaders.AUTHORIZATION).description("JWT 토큰") + ), + pathParameters( + parameterWithName("study-id").description("스터디 식별 번호"), + parameterWithName("article-id").description("게시글 식별 번호") + ), + requestFields( + fieldWithPath("title").type(JsonFieldType.STRING).description("게시글 수정 제목"), + fieldWithPath("content").type(JsonFieldType.STRING).description("게시글 내용 수정") + ) + )) + .when().log().all() + .put("/api/studies/{study-id}/community/articles/{article-id}") + .then().log().all() + .statusCode(HttpStatus.NO_CONTENT.value()); + + // assert + final ArticleResponse response = RestAssured + .given().log().all() + .header(HttpHeaders.AUTHORIZATION, 토큰) + .pathParam("study-id", 스터디_ID) + .pathParam("article-id", 게시글_ID) + .when().log().all() + .get("/api/studies/{study-id}/community/articles/{article-id}") + .then().log().all() + .statusCode(HttpStatus.OK.value()) + .extract() + .as(ArticleResponse.class); + + final AuthorResponse authorResponse = new AuthorResponse(그린론_깃허브_ID, 그린론_이름, 그린론_이미지_URL, 그린론_프로필_URL); + + assertThat(response).isEqualTo(new ArticleResponse(스터디_ID, authorResponse, "게시글 제목 수정", + "게시글 내용 수정", LocalDate.now(), LocalDate.now())); + } +} diff --git a/backend/src/test/java/com/woowacourse/moamoa/WebMVCTest.java b/backend/src/test/java/com/woowacourse/moamoa/WebMVCTest.java index f12467f10..1b88f01ea 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/WebMVCTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/WebMVCTest.java @@ -1,40 +1,33 @@ package com.woowacourse.moamoa; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + import com.fasterxml.jackson.databind.ObjectMapper; import com.woowacourse.moamoa.auth.config.AuthRequestMatchConfig; -import com.woowacourse.moamoa.auth.controller.AuthController; import com.woowacourse.moamoa.auth.controller.AuthenticationInterceptor; -import com.woowacourse.moamoa.auth.controller.matcher.AuthenticationRequestMatcher; import com.woowacourse.moamoa.auth.infrastructure.JwtTokenProvider; import com.woowacourse.moamoa.auth.infrastructure.TokenProvider; -import com.woowacourse.moamoa.auth.service.AuthService; -import com.woowacourse.moamoa.member.controller.MemberController; -import com.woowacourse.moamoa.member.service.MemberService; -import com.woowacourse.moamoa.review.controller.ReviewController; -import com.woowacourse.moamoa.review.controller.SearchingReviewController; -import com.woowacourse.moamoa.review.service.ReviewService; -import com.woowacourse.moamoa.review.service.SearchingReviewService; -import com.woowacourse.moamoa.study.controller.MyStudyController; -import com.woowacourse.moamoa.study.service.MyStudyService; +import com.woowacourse.moamoa.common.MockedServiceObjectsBeanRegister; +import com.woowacourse.moamoa.member.domain.Member; +import com.woowacourse.moamoa.member.domain.repository.MemberRepository; +import java.util.Optional; import javax.servlet.http.HttpServletRequest; +import org.junit.jupiter.api.BeforeEach; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.ComponentScan.Filter; +import org.springframework.context.annotation.FilterType; import org.springframework.context.annotation.Import; import org.springframework.test.web.servlet.MockMvc; +import org.springframework.web.bind.annotation.RestController; import org.springframework.web.context.request.NativeWebRequest; -@WebMvcTest({ - MyStudyController.class, - ReviewController.class, - SearchingReviewController.class, - MemberController.class, - MyStudyController.class, - AuthController.class -}) -@Import({JwtTokenProvider.class, AuthRequestMatchConfig.class}) +@WebMvcTest(includeFilters = @Filter(type = FilterType.ANNOTATION, classes = RestController.class)) +@Import({JwtTokenProvider.class, AuthRequestMatchConfig.class, MockedServiceObjectsBeanRegister.class}) public abstract class WebMVCTest { @Autowired @@ -53,23 +46,11 @@ public abstract class WebMVCTest { protected ObjectMapper objectMapper; @MockBean - protected ReviewService reviewService; - - @MockBean - protected SearchingReviewService searchingReviewService; - - @MockBean - protected MemberService memberService; - - @MockBean - protected AuthService authService; - - @MockBean - protected MyStudyService myStudyService; + private MemberRepository memberRepository; - @MockBean - protected HttpServletRequest httpServletRequest; - - @MockBean - protected NativeWebRequest nativeWebRequest; + @BeforeEach + void setUp() { + when(memberRepository.findByGithubId(any())) + .thenReturn(Optional.of(new Member(1L, 1L, "username", "image", "profile"))); + } } diff --git a/backend/src/test/java/com/woowacourse/moamoa/auth/controller/AuthControllerTest.java b/backend/src/test/java/com/woowacourse/moamoa/auth/controller/AuthControllerTest.java index 59cfd4788..1248762bf 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/auth/controller/AuthControllerTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/auth/controller/AuthControllerTest.java @@ -7,12 +7,18 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import com.woowacourse.moamoa.WebMVCTest; +import com.woowacourse.moamoa.auth.service.AuthService; import com.woowacourse.moamoa.auth.service.response.TokensResponse; + import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.springframework.boot.test.mock.mockito.MockBean; public class AuthControllerTest extends WebMVCTest { + @MockBean + AuthService authService; + @DisplayName("Authorization 요청과 응답 형식을 확인한다.") @Test void getJwtToken() throws Exception { diff --git a/backend/src/test/java/com/woowacourse/moamoa/auth/controller/AuthenticationArgumentResolverTest.java b/backend/src/test/java/com/woowacourse/moamoa/auth/controller/AuthenticationArgumentResolverTest.java index 1bb654cd9..30dabdc4b 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/auth/controller/AuthenticationArgumentResolverTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/auth/controller/AuthenticationArgumentResolverTest.java @@ -5,6 +5,7 @@ import static org.mockito.BDDMockito.given; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.HttpHeaders; import java.util.Collections; @@ -16,9 +17,16 @@ import com.woowacourse.moamoa.WebMVCTest; import com.woowacourse.moamoa.common.exception.UnauthorizedException; +import org.springframework.web.context.request.NativeWebRequest; class AuthenticationArgumentResolverTest extends WebMVCTest { + @MockBean + protected HttpServletRequest httpServletRequest; + + @MockBean + protected NativeWebRequest nativeWebRequest; + @Autowired private AuthenticationArgumentResolver authenticationArgumentResolver; diff --git a/backend/src/test/java/com/woowacourse/moamoa/auth/controller/AuthenticationInterceptorTest.java b/backend/src/test/java/com/woowacourse/moamoa/auth/controller/AuthenticationInterceptorTest.java index fa4fb8ab5..1844a824a 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/auth/controller/AuthenticationInterceptorTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/auth/controller/AuthenticationInterceptorTest.java @@ -7,8 +7,10 @@ import java.util.Collections; import java.util.List; +import javax.servlet.http.HttpServletRequest; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; @@ -17,6 +19,9 @@ class AuthenticationInterceptorTest extends WebMVCTest { + @MockBean + protected HttpServletRequest httpServletRequest; + @DisplayName("Preflight 요청인지 확인한다.") @Test void isPreflightRequest() { diff --git a/backend/src/test/java/com/woowacourse/moamoa/auth/controller/matcher/AuthenticationRequestMatcherTest.java b/backend/src/test/java/com/woowacourse/moamoa/auth/controller/matcher/AuthenticationRequestMatcherTest.java index d34a88c26..2a9f2cb4e 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/auth/controller/matcher/AuthenticationRequestMatcherTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/auth/controller/matcher/AuthenticationRequestMatcherTest.java @@ -5,13 +5,18 @@ import com.woowacourse.moamoa.WebMVCTest; +import javax.servlet.http.HttpServletRequest; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.MockBean; class AuthenticationRequestMatcherTest extends WebMVCTest { + @MockBean + protected HttpServletRequest httpServletRequest; + @Autowired private AuthenticationRequestMatcher authenticationRequestMatcher; diff --git a/backend/src/test/java/com/woowacourse/moamoa/auth/service/AuthServiceTest.java b/backend/src/test/java/com/woowacourse/moamoa/auth/service/AuthServiceTest.java index 4c2c0de24..3f84fa269 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/auth/service/AuthServiceTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/auth/service/AuthServiceTest.java @@ -16,12 +16,15 @@ import com.woowacourse.moamoa.member.domain.repository.MemberRepository; import com.woowacourse.moamoa.member.query.MemberDao; import com.woowacourse.moamoa.member.service.MemberService; + import java.util.Optional; + import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.mockito.Mockito; @RepositoryTest class AuthServiceTest { @@ -76,7 +79,6 @@ public void updateRefreshToken() { final String refreshToken = token.getRefreshToken(); final AccessTokenResponse accessTokenResponse = authService.refreshToken(1L, refreshToken); - assertThat(refreshToken).isNotBlank(); assertThat(accessTokenResponse.getAccessToken()).isEqualTo("recreationAccessToken"); } diff --git a/backend/src/test/java/com/woowacourse/moamoa/common/CategoryAndTagsSaver.java b/backend/src/test/java/com/woowacourse/moamoa/common/CategoryAndTagsSaver.java new file mode 100644 index 000000000..5e976fe21 --- /dev/null +++ b/backend/src/test/java/com/woowacourse/moamoa/common/CategoryAndTagsSaver.java @@ -0,0 +1,30 @@ +package com.woowacourse.moamoa.common; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Component; + +@Component +public class CategoryAndTagsSaver implements ApplicationRunner { + + private final JdbcTemplate jdbcTemplate; + + public CategoryAndTagsSaver(final JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + @Override + public void run(final ApplicationArguments args) { + jdbcTemplate.update("INSERT INTO category(id, name) VALUES (1, 'generation')"); + jdbcTemplate.update("INSERT INTO category(id, name) VALUES (2, 'area')"); + jdbcTemplate.update("INSERT INTO category(id, name) VALUES (3, 'subject')"); + + jdbcTemplate.update("INSERT INTO tag(id, name, description, category_id) VALUES (1, 'Java', '자바', 3)"); + jdbcTemplate.update("INSERT INTO tag(id, name, description, category_id) VALUES (2, '4기', '우테코4기', 1)"); + jdbcTemplate.update("INSERT INTO tag(id, name, description, category_id) VALUES (3, 'BE', '백엔드', 2)"); + jdbcTemplate.update("INSERT INTO tag(id, name, description, category_id) VALUES (4, 'FE', '프론트엔드', 2)"); + jdbcTemplate.update("INSERT INTO tag(id, name, description, category_id) VALUES (5, 'React', '리액트', 3)"); + } +} diff --git a/backend/src/test/java/com/woowacourse/moamoa/common/MockedServiceObjectsBeanRegister.java b/backend/src/test/java/com/woowacourse/moamoa/common/MockedServiceObjectsBeanRegister.java new file mode 100644 index 000000000..9b873dce0 --- /dev/null +++ b/backend/src/test/java/com/woowacourse/moamoa/common/MockedServiceObjectsBeanRegister.java @@ -0,0 +1,53 @@ +package com.woowacourse.moamoa.common; + +import static org.mockito.Mockito.mock; +import static org.springframework.beans.factory.support.BeanDefinitionBuilder.rootBeanDefinition; + +import java.util.Set; +import java.util.stream.Collectors; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.support.AbstractBeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor; +import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider; +import org.springframework.core.type.filter.AnnotationTypeFilter; +import org.springframework.stereotype.Service; + +public class MockedServiceObjectsBeanRegister implements BeanDefinitionRegistryPostProcessor { + + @Override + public void postProcessBeanDefinitionRegistry(final BeanDefinitionRegistry registry) throws BeansException { + for (String className : getServiceLayerObjectClassNames()) { + try { + Class<?> cls = Class.forName(className); + registry.registerBeanDefinition(beanName(cls), beanDefinition(cls)); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + } + } + + private Set<String> getServiceLayerObjectClassNames() { + ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(false); + provider.addIncludeFilter(new AnnotationTypeFilter(Service.class)); + return provider.findCandidateComponents("com.woowacourse.moamoa") + .stream() + .map(BeanDefinition::getBeanClassName) + .collect(Collectors.toSet()); + } + + private String beanName(final Class<?> cls) { + final String clsName = cls.getSimpleName(); + return clsName.toLowerCase().charAt(0) + clsName.substring(1); + } + + private <T> AbstractBeanDefinition beanDefinition(final Class<T> cls) { + return rootBeanDefinition(cls, () -> mock(cls)).getBeanDefinition(); + } + + @Override + public void postProcessBeanFactory(final ConfigurableListableBeanFactory beanFactory) throws BeansException { + } +} diff --git a/backend/src/test/java/com/woowacourse/moamoa/common/RepositoryTest.java b/backend/src/test/java/com/woowacourse/moamoa/common/RepositoryTest.java index be19d1dd6..448be3cd7 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/common/RepositoryTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/common/RepositoryTest.java @@ -15,6 +15,6 @@ @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @DataJpaTest(includeFilters = @Filter(type = FilterType.ANNOTATION, classes = Repository.class)) -@Import(JpaAuditingConfig.class) +@Import({JpaAuditingConfig.class, CategoryAndTagsSaver.class}) public @interface RepositoryTest { } diff --git a/backend/src/test/java/com/woowacourse/moamoa/community/controller/CommunityArticleControllerTest.java b/backend/src/test/java/com/woowacourse/moamoa/community/controller/CommunityArticleControllerTest.java new file mode 100644 index 000000000..05a187dd4 --- /dev/null +++ b/backend/src/test/java/com/woowacourse/moamoa/community/controller/CommunityArticleControllerTest.java @@ -0,0 +1,103 @@ +package com.woowacourse.moamoa.community.controller; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.woowacourse.moamoa.common.RepositoryTest; +import com.woowacourse.moamoa.common.utils.DateTimeSystem; +import com.woowacourse.moamoa.community.domain.CommunityArticle; +import com.woowacourse.moamoa.community.domain.repository.CommunityArticleRepository; +import com.woowacourse.moamoa.community.query.CommunityArticleDao; +import com.woowacourse.moamoa.community.service.CommunityArticleService; +import com.woowacourse.moamoa.community.service.request.ArticleRequest; +import com.woowacourse.moamoa.member.domain.Member; +import com.woowacourse.moamoa.member.domain.repository.MemberRepository; +import com.woowacourse.moamoa.member.service.exception.MemberNotFoundException; +import com.woowacourse.moamoa.study.domain.Study; +import com.woowacourse.moamoa.study.domain.repository.StudyRepository; +import com.woowacourse.moamoa.study.service.StudyService; +import com.woowacourse.moamoa.study.service.exception.StudyNotFoundException; +import com.woowacourse.moamoa.study.service.request.CreatingStudyRequestBuilder; +import java.time.LocalDate; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +@RepositoryTest +public class CommunityArticleControllerTest { + + CreatingStudyRequestBuilder javaStudyRequest = new CreatingStudyRequestBuilder() + .title("java 스터디").excerpt("자바 설명").thumbnail("java image").description("자바 소개"); + + @Autowired + private StudyRepository studyRepository; + + @Autowired + private MemberRepository memberRepository; + + @Autowired + private CommunityArticleRepository communityArticleRepository; + + @Autowired + private CommunityArticleDao communityArticleDao; + + private StudyService studyService; + private CommunityArticleController sut; + + @BeforeEach + void setUp() { + studyService = new StudyService(studyRepository, memberRepository, new DateTimeSystem()); + sut = new CommunityArticleController(new CommunityArticleService(memberRepository, studyRepository, + communityArticleRepository, communityArticleDao)); + } + + @DisplayName("커뮤니티 게시글을 작성한다.") + @Test + void createCommunityArticle() { + // arrange + Member member = memberRepository.save(new Member(1L, "username", "imageUrl", "profileUrl")); + Study study = studyService + .createStudy(member.getGithubId(), javaStudyRequest.startDate(LocalDate.now()).build()); + + ArticleRequest request = new ArticleRequest("게시글 제목", "게시글 내용"); + + // act + ResponseEntity<Void> response = sut.createArticle(member.getId(), study.getId(), request); + + // assert + String location = response.getHeaders().getLocation().getPath(); + Long articleId = Long.valueOf(location.replaceAll("/api/studies/\\d+/community/articles/", "")); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED); + assertThat(location).matches("/api/studies/\\d+/community/articles/\\d+"); + assertThat(communityArticleRepository.findById(articleId).get()) + .isEqualTo(new CommunityArticle(articleId, "게시글 제목", "게시글 내용", member.getId(), study)); + } + + @DisplayName("사용자가 없는 경우 게시글 작성 시 예외가 발생한다.") + @Test + void throwExceptionWhenCreateByNotFoundMember() { + // arrange + Member member = memberRepository.save(new Member(1L, "username", "imageUrl", "profileUrl")); + Study study = studyService + .createStudy(member.getGithubId(), javaStudyRequest.startDate(LocalDate.now()).build()); + + // act & assert + assertThatThrownBy(() -> sut.createArticle(member.getId() + 1, study.getId(), new ArticleRequest("제목", "내용"))) + .isInstanceOf(MemberNotFoundException.class); + } + + @DisplayName("스터디가 없는 경우 게시글 작성 시 예외가 발생한다.") + @Test + void throwExceptionWhenWriteToNotFoundStudy() { + // arrange + Member member = memberRepository.save(new Member(1L, "username", "imageUrl", "profileUrl")); + + // act & assert + assertThatThrownBy(() -> sut.createArticle(member.getId(), 1L, new ArticleRequest("제목", "내용"))) + .isInstanceOf(StudyNotFoundException.class); + } +} diff --git a/backend/src/test/java/com/woowacourse/moamoa/community/controller/DeletingCommunityArticleControllerTest.java b/backend/src/test/java/com/woowacourse/moamoa/community/controller/DeletingCommunityArticleControllerTest.java new file mode 100644 index 000000000..3778aaa87 --- /dev/null +++ b/backend/src/test/java/com/woowacourse/moamoa/community/controller/DeletingCommunityArticleControllerTest.java @@ -0,0 +1,113 @@ +package com.woowacourse.moamoa.community.controller; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.woowacourse.moamoa.common.RepositoryTest; +import com.woowacourse.moamoa.common.utils.DateTimeSystem; +import com.woowacourse.moamoa.community.domain.CommunityArticle; +import com.woowacourse.moamoa.community.domain.repository.CommunityArticleRepository; +import com.woowacourse.moamoa.community.query.CommunityArticleDao; +import com.woowacourse.moamoa.community.service.CommunityArticleService; +import com.woowacourse.moamoa.community.service.exception.ArticleNotFoundException; +import com.woowacourse.moamoa.community.service.exception.NotArticleAuthorException; +import com.woowacourse.moamoa.community.service.exception.NotRelatedArticleException; +import com.woowacourse.moamoa.community.service.exception.UneditableArticleException; +import com.woowacourse.moamoa.community.service.request.ArticleRequest; +import com.woowacourse.moamoa.member.domain.Member; +import com.woowacourse.moamoa.member.domain.repository.MemberRepository; +import com.woowacourse.moamoa.member.service.exception.MemberNotFoundException; +import com.woowacourse.moamoa.member.service.exception.NotParticipatedMemberException; +import com.woowacourse.moamoa.study.domain.Study; +import com.woowacourse.moamoa.study.domain.repository.StudyRepository; +import com.woowacourse.moamoa.study.service.StudyService; +import com.woowacourse.moamoa.study.service.exception.StudyNotFoundException; +import com.woowacourse.moamoa.study.service.request.CreatingStudyRequestBuilder; +import java.time.LocalDate; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +@RepositoryTest +public class DeletingCommunityArticleControllerTest { + + CreatingStudyRequestBuilder javaStudyRequest = new CreatingStudyRequestBuilder() + .title("java 스터디").excerpt("자바 설명").thumbnail("java image").description("자바 소개"); + + @Autowired + private StudyRepository studyRepository; + + @Autowired + private MemberRepository memberRepository; + + @Autowired + private CommunityArticleRepository communityArticleRepository; + + @Autowired + private CommunityArticleDao communityArticleDao; + + private StudyService studyService; + private CommunityArticleController sut; + private CommunityArticleService communityArticleService; + + @BeforeEach + void setUp() { + studyService = new StudyService(studyRepository, memberRepository, new DateTimeSystem()); + communityArticleService = new CommunityArticleService(memberRepository, studyRepository, + communityArticleRepository, communityArticleDao); + sut = new CommunityArticleController(communityArticleService); + } + + @DisplayName("스터디 커뮤니티 게시글을 삭제한다.") + @Test + void deleteCommunityArticle() { + // arrange + Member member = memberRepository.save(new Member(1L, "username", "imageUrl", "profileUrl")); + + Study study = studyService + .createStudy(member.getGithubId(), javaStudyRequest.startDate(LocalDate.now()).build()); + + ArticleRequest request = new ArticleRequest("게시글 제목", "게시글 내용"); + CommunityArticle article = communityArticleService.createArticle(member.getId(), study.getId(), + request); + + //act + sut.deleteArticle(member.getId(), study.getId(), article.getId()); + + //assert + assertThat(communityArticleRepository.existsById(article.getId())).isFalse(); + } + + @DisplayName("게시글이 없는 경우 조회 시 예외가 발생한다.") + @Test + void throwExceptionWhenGettingToNotFoundArticle() { + // arrange + Member member = memberRepository.save(new Member(1L, "username", "imageUrl", "profileUrl")); + Study study = studyService + .createStudy(member.getGithubId(), javaStudyRequest.startDate(LocalDate.now()).build()); + + // act & assert + assertThatThrownBy(() -> sut.deleteArticle(member.getId(), study.getId(), 1L)) + .isInstanceOf(ArticleNotFoundException.class); + } + + @DisplayName("게시글을 삭제할 수 없는 경우 예외가 발생한다.") + @Test + void throwExceptionWhenDeletingByNotParticipant() { + // arrange + Member member = memberRepository.save(new Member(1L, "username", "imageUrl", "profileUrl")); + Member other = memberRepository.save(new Member(2L, "username2", "imageUrl", "profileUrl")); + + Study study = studyService + .createStudy(member.getGithubId(), javaStudyRequest.startDate(LocalDate.now()).build()); + + ArticleRequest request = new ArticleRequest("게시글 제목", "게시글 내용"); + final CommunityArticle article = communityArticleService.createArticle(member.getId(), study.getId(), + request); + + // act & assert + assertThatThrownBy(() -> sut.deleteArticle(other.getId(), study.getId(), article.getId())) + .isInstanceOf(UneditableArticleException.class); + } +} diff --git a/backend/src/test/java/com/woowacourse/moamoa/community/controller/GettingCommunityArticleControllerTest.java b/backend/src/test/java/com/woowacourse/moamoa/community/controller/GettingCommunityArticleControllerTest.java new file mode 100644 index 000000000..24e9bfffd --- /dev/null +++ b/backend/src/test/java/com/woowacourse/moamoa/community/controller/GettingCommunityArticleControllerTest.java @@ -0,0 +1,121 @@ +package com.woowacourse.moamoa.community.controller; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.woowacourse.moamoa.common.RepositoryTest; +import com.woowacourse.moamoa.common.utils.DateTimeSystem; +import com.woowacourse.moamoa.community.domain.CommunityArticle; +import com.woowacourse.moamoa.community.domain.repository.CommunityArticleRepository; +import com.woowacourse.moamoa.community.query.CommunityArticleDao; +import com.woowacourse.moamoa.community.service.CommunityArticleService; +import com.woowacourse.moamoa.community.service.exception.ArticleNotFoundException; +import com.woowacourse.moamoa.community.service.exception.NotRelatedArticleException; +import com.woowacourse.moamoa.community.service.exception.UnviewableArticleException; +import com.woowacourse.moamoa.community.service.request.ArticleRequest; +import com.woowacourse.moamoa.community.service.response.ArticleResponse; +import com.woowacourse.moamoa.community.service.response.AuthorResponse; +import com.woowacourse.moamoa.member.domain.Member; +import com.woowacourse.moamoa.member.domain.repository.MemberRepository; +import com.woowacourse.moamoa.member.service.exception.MemberNotFoundException; +import com.woowacourse.moamoa.member.service.exception.NotParticipatedMemberException; +import com.woowacourse.moamoa.study.domain.Study; +import com.woowacourse.moamoa.study.domain.repository.StudyRepository; +import com.woowacourse.moamoa.study.service.StudyService; +import com.woowacourse.moamoa.study.service.exception.StudyNotFoundException; +import com.woowacourse.moamoa.study.service.request.CreatingStudyRequestBuilder; +import java.time.LocalDate; +import javax.persistence.EntityManager; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +@RepositoryTest +public class GettingCommunityArticleControllerTest { + + CreatingStudyRequestBuilder javaStudyRequest = new CreatingStudyRequestBuilder() + .title("java 스터디").excerpt("자바 설명").thumbnail("java image").description("자바 소개"); + + @Autowired + private StudyRepository studyRepository; + + @Autowired + private MemberRepository memberRepository; + + @Autowired + private CommunityArticleRepository communityArticleRepository; + + @Autowired + private CommunityArticleDao communityArticleDao; + + private StudyService studyService; + private CommunityArticleController sut; + private CommunityArticleService communityArticleService; + + @BeforeEach + void setUp() { + studyService = new StudyService(studyRepository, memberRepository, new DateTimeSystem()); + communityArticleService = new CommunityArticleService(memberRepository, studyRepository, + communityArticleRepository, communityArticleDao); + sut = new CommunityArticleController(communityArticleService); + } + + @DisplayName("스터디 게시글을 단건 조회한다.") + @Test + void getStudyCommunityArticle() { + // arrange + Member member = memberRepository.save(new Member(1L, "username", "imageUrl", "profileUrl")); + Study study = studyService + .createStudy(member.getGithubId(), javaStudyRequest.startDate(LocalDate.now()).build()); + + ArticleRequest request = new ArticleRequest("게시글 제목", "게시글 내용"); + final CommunityArticle article = communityArticleService.createArticle(member.getId(), study.getId(), + request); + + //act + final ResponseEntity<ArticleResponse> response = sut.getArticle(member.getId(), study.getId(), + article.getId()); + + //assert + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getBody()).isEqualTo(new ArticleResponse(article.getId(), + new AuthorResponse(member.getGithubId(), member.getUsername(), member.getImageUrl(), + member.getProfileUrl()), + request.getTitle(), request.getContent(), LocalDate.now(), LocalDate.now())); + } + + @DisplayName("게시글이 없는 경우 조회 시 예외가 발생한다.") + @Test + void throwExceptionWhenGettingToNotFoundArticle() { + // arrange + Member member = memberRepository.save(new Member(1L, "username", "imageUrl", "profileUrl")); + Study study = studyService + .createStudy(member.getGithubId(), javaStudyRequest.startDate(LocalDate.now()).build()); + + // act & assert + assertThatThrownBy(() -> sut.getArticle(member.getId(), study.getId(), 1L)) + .isInstanceOf(ArticleNotFoundException.class); + } + + @DisplayName("스터디를 조회할 수 없는 경우 예외가 발생한다.") + @Test + void throwExceptionWhenGettingByNotParticipant() { + // arrange + Member member = memberRepository.save(new Member(1L, "username", "imageUrl", "profileUrl")); + Member other = memberRepository.save(new Member(2L, "username2", "imageUrl", "profileUrl")); + + Study study = studyService + .createStudy(member.getGithubId(), javaStudyRequest.startDate(LocalDate.now()).build()); + + ArticleRequest request = new ArticleRequest("게시글 제목", "게시글 내용"); + final CommunityArticle article = communityArticleService.createArticle(member.getId(), study.getId(), + request); + + // act & assert + assertThatThrownBy(() -> sut.getArticle(other.getId(), study.getId(), article.getId())) + .isInstanceOf(UnviewableArticleException.class); + } +} diff --git a/backend/src/test/java/com/woowacourse/moamoa/community/controller/GettingCommunityArticleSummariesControllerTest.java b/backend/src/test/java/com/woowacourse/moamoa/community/controller/GettingCommunityArticleSummariesControllerTest.java new file mode 100644 index 000000000..54ba3455a --- /dev/null +++ b/backend/src/test/java/com/woowacourse/moamoa/community/controller/GettingCommunityArticleSummariesControllerTest.java @@ -0,0 +1,130 @@ +package com.woowacourse.moamoa.community.controller; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.woowacourse.moamoa.common.RepositoryTest; +import com.woowacourse.moamoa.common.utils.DateTimeSystem; +import com.woowacourse.moamoa.community.domain.CommunityArticle; +import com.woowacourse.moamoa.community.domain.repository.CommunityArticleRepository; +import com.woowacourse.moamoa.community.query.CommunityArticleDao; +import com.woowacourse.moamoa.community.service.CommunityArticleService; +import com.woowacourse.moamoa.community.service.exception.ArticleNotFoundException; +import com.woowacourse.moamoa.community.service.exception.UnviewableArticleException; +import com.woowacourse.moamoa.community.service.request.ArticleRequest; +import com.woowacourse.moamoa.community.service.response.ArticleSummariesResponse; +import com.woowacourse.moamoa.community.service.response.ArticleSummaryResponse; +import com.woowacourse.moamoa.community.service.response.AuthorResponse; +import com.woowacourse.moamoa.member.domain.Member; +import com.woowacourse.moamoa.member.domain.repository.MemberRepository; +import com.woowacourse.moamoa.member.service.exception.MemberNotFoundException; +import com.woowacourse.moamoa.member.service.exception.NotParticipatedMemberException; +import com.woowacourse.moamoa.study.domain.Study; +import com.woowacourse.moamoa.study.domain.repository.StudyRepository; +import com.woowacourse.moamoa.study.service.StudyService; +import com.woowacourse.moamoa.study.service.exception.StudyNotFoundException; +import com.woowacourse.moamoa.study.service.request.CreatingStudyRequestBuilder; +import java.lang.reflect.Field; +import java.time.LocalDate; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.PageRequest; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +@RepositoryTest +public class GettingCommunityArticleSummariesControllerTest { + + CreatingStudyRequestBuilder javaStudyRequest = new CreatingStudyRequestBuilder() + .title("java 스터디").excerpt("자바 설명").thumbnail("java image").description("자바 소개"); + + private CommunityArticleService communityArticleService; + + private StudyService studyService; + + @Autowired + private MemberRepository memberRepository; + + @Autowired + private StudyRepository studyRepository; + + @Autowired + private CommunityArticleRepository communityArticleRepository; + + @Autowired + private CommunityArticleDao communityArticleDao; + + private CommunityArticleController sut; + + @BeforeEach + void setUp() { + communityArticleService = new CommunityArticleService(memberRepository, studyRepository, + communityArticleRepository, communityArticleDao); + studyService = new StudyService(studyRepository, memberRepository, new DateTimeSystem()); + sut = new CommunityArticleController(communityArticleService); + } + + @DisplayName("스터디 커뮤니티 글 목록을 조회한다.") + @Test + void getCommunityArticles() { + // arrange + Member 그린론 = memberRepository.save(new Member(1L, "그린론", "http://image", "http://profile")); + + Study study = studyService.createStudy(그린론.getGithubId(), javaStudyRequest.startDate(LocalDate.now()).build()); + + communityArticleService.createArticle(그린론.getId(), study.getId(), new ArticleRequest("제목1", "내용1")); + communityArticleService.createArticle(그린론.getId(), study.getId(), new ArticleRequest("제목2", "내용2")); + CommunityArticle article3 = communityArticleService + .createArticle(그린론.getId(), study.getId(), new ArticleRequest("제목3", "내용3")); + CommunityArticle article4 = communityArticleService + .createArticle(그린론.getId(), study.getId(), new ArticleRequest("제목4", "내용4")); + CommunityArticle article5 = communityArticleService + .createArticle(그린론.getId(), study.getId(), new ArticleRequest("제목5", "내용5")); + + // act + ResponseEntity<ArticleSummariesResponse> response = sut.getArticles(그린론.getId(), study.getId(), PageRequest.of(0, 3)); + + // assert + AuthorResponse author = new AuthorResponse(1L, "그린론", "http://image", "http://profile"); + + List<ArticleSummaryResponse> articles = List.of( + new ArticleSummaryResponse(article5.getId(), author, "제목5", LocalDate.now(), LocalDate.now()), + new ArticleSummaryResponse(article4.getId(), author, "제목4", LocalDate.now(), LocalDate.now()), + new ArticleSummaryResponse(article3.getId(), author, "제목3", LocalDate.now(), LocalDate.now()) + ); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getBody()).isEqualTo( + new ArticleSummariesResponse(articles, 0, 1, 5) + ); + } + + @DisplayName("스터디가 없는 경우 게시글 목록 조회 시 예외가 발생한다.") + @Test + void throwExceptionWhenWriteToNotFoundStudy() { + // arrange + Member member = memberRepository.save(new Member(1L, "username", "imageUrl", "profileUrl")); + + // act & assert + assertThatThrownBy(() -> sut.getArticles(member.getId(), 1L, PageRequest.of(0, 3))) + .isInstanceOf(StudyNotFoundException.class); + } + + @DisplayName("스터디에 참여하지 않은 사용자가 스터디 커뮤니티 게시글 목록을 조회한 경우 예외가 발생한다.") + @Test + void throwExceptionWhenGettingByNotParticipant() { + // arrange + Member member = memberRepository.save(new Member(1L, "username", "imageUrl", "profileUrl")); + Member other = memberRepository.save(new Member(2L, "username2", "imageUrl", "profileUrl")); + + Study study = studyService + .createStudy(member.getGithubId(), javaStudyRequest.startDate(LocalDate.now()).build()); + + // act & assert + assertThatThrownBy(() -> sut.getArticles(other.getId(), study.getId(), PageRequest.of(0, 3))) + .isInstanceOf(UnviewableArticleException.class); + } +} diff --git a/backend/src/test/java/com/woowacourse/moamoa/community/controller/UpdatingCommunityArticleControllerTest.java b/backend/src/test/java/com/woowacourse/moamoa/community/controller/UpdatingCommunityArticleControllerTest.java new file mode 100644 index 000000000..ea0d3cacd --- /dev/null +++ b/backend/src/test/java/com/woowacourse/moamoa/community/controller/UpdatingCommunityArticleControllerTest.java @@ -0,0 +1,118 @@ +package com.woowacourse.moamoa.community.controller; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.woowacourse.moamoa.common.RepositoryTest; +import com.woowacourse.moamoa.common.utils.DateTimeSystem; +import com.woowacourse.moamoa.community.domain.CommunityArticle; +import com.woowacourse.moamoa.community.domain.repository.CommunityArticleRepository; +import com.woowacourse.moamoa.community.query.CommunityArticleDao; +import com.woowacourse.moamoa.community.service.CommunityArticleService; +import com.woowacourse.moamoa.community.service.exception.ArticleNotFoundException; +import com.woowacourse.moamoa.community.service.exception.NotArticleAuthorException; +import com.woowacourse.moamoa.community.service.exception.NotRelatedArticleException; +import com.woowacourse.moamoa.community.service.exception.UneditableArticleException; +import com.woowacourse.moamoa.community.service.request.ArticleRequest; +import com.woowacourse.moamoa.member.domain.Member; +import com.woowacourse.moamoa.member.domain.repository.MemberRepository; +import com.woowacourse.moamoa.member.service.exception.NotParticipatedMemberException; +import com.woowacourse.moamoa.study.domain.Study; +import com.woowacourse.moamoa.study.domain.repository.StudyRepository; +import com.woowacourse.moamoa.study.service.StudyService; +import com.woowacourse.moamoa.study.service.exception.StudyNotFoundException; +import com.woowacourse.moamoa.study.service.request.CreatingStudyRequestBuilder; +import java.time.LocalDate; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +@RepositoryTest +public class UpdatingCommunityArticleControllerTest { + + CreatingStudyRequestBuilder javaStudyBuilder = new CreatingStudyRequestBuilder() + .title("java 스터디").excerpt("자바 설명").thumbnail("java image").description("자바 소개"); + + @Autowired + private StudyRepository studyRepository; + + @Autowired + private MemberRepository memberRepository; + + @Autowired + private CommunityArticleRepository communityArticleRepository; + + @Autowired + private CommunityArticleDao communityArticleDao; + + private StudyService studyService; + private CommunityArticleController sut; + private CommunityArticleService communityArticleService; + + @BeforeEach + void setUp() { + studyService = new StudyService(studyRepository, memberRepository, new DateTimeSystem()); + communityArticleService = new CommunityArticleService(memberRepository, studyRepository, + communityArticleRepository, communityArticleDao); + sut = new CommunityArticleController(communityArticleService); + } + + @DisplayName("게시글을 수정한다.") + @Test + void updateArticle() { + // arrange + Member member = memberRepository.save(new Member(1L, "username", "image", "profile")); + Study study = studyService + .createStudy(member.getGithubId(), javaStudyBuilder.startDate(LocalDate.now()).build()); + CommunityArticle article = communityArticleService + .createArticle(member.getId(), study.getId(), new ArticleRequest("제목", "내용")); + + // act + final ResponseEntity<Void> response = sut.updateArticle(member.getId(), study.getId(), article.getId(), + new ArticleRequest("제목 수정", "내용 수정")); + + // assert + CommunityArticle actualArticle = communityArticleRepository.findById(article.getId()).orElseThrow(); + CommunityArticle expectArticle = new CommunityArticle(article.getId(), "제목 수정", "내용 수정", member.getId(), study); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NO_CONTENT); + assertThat(actualArticle).isEqualTo(expectArticle); + } + + @DisplayName("게시글이 없는 경우 수정 시 예외가 발생한다.") + @Test + void throwExceptionWhenUpdateToNotFoundArticle() { + // arrange + Member member = memberRepository.save(new Member(1L, "username", "imageUrl", "profileUrl")); + Study study = studyService + .createStudy(member.getGithubId(), javaStudyBuilder.startDate(LocalDate.now()).build()); + + // act & assert + assertThatThrownBy( + () -> sut.updateArticle(member.getId(), study.getId(), 1L, new ArticleRequest("제목 수정", "내용 수정"))) + .isInstanceOf(ArticleNotFoundException.class); + } + + @DisplayName("게시글을 수정할 수 없는 경우 예외가 발생한다.") + @Test + void throwExceptionWhenUpdateByNotParticipant() { + // arrange + Member member = memberRepository.save(new Member(1L, "username", "imageUrl", "profileUrl")); + Member other = memberRepository.save(new Member(2L, "username2", "imageUrl", "profileUrl")); + + Study study = studyService + .createStudy(member.getGithubId(), javaStudyBuilder.startDate(LocalDate.now()).build()); + + ArticleRequest request = new ArticleRequest("게시글 제목", "게시글 내용"); + final CommunityArticle article = communityArticleService.createArticle(member.getId(), study.getId(), + request); + + // act & assert + assertThatThrownBy(() -> sut + .updateArticle(other.getId(), study.getId(), article.getId(), new ArticleRequest("제목 수정", "내용 수정"))) + .isInstanceOf(UneditableArticleException.class); + } +} diff --git a/backend/src/test/java/com/woowacourse/moamoa/community/domain/CommunityArticleTest.java b/backend/src/test/java/com/woowacourse/moamoa/community/domain/CommunityArticleTest.java new file mode 100644 index 000000000..cab877996 --- /dev/null +++ b/backend/src/test/java/com/woowacourse/moamoa/community/domain/CommunityArticleTest.java @@ -0,0 +1,102 @@ +package com.woowacourse.moamoa.community.domain; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.woowacourse.moamoa.community.service.request.ArticleRequest; +import com.woowacourse.moamoa.member.domain.Member; +import com.woowacourse.moamoa.member.service.exception.NotParticipatedMemberException; +import com.woowacourse.moamoa.study.domain.AttachedTags; +import com.woowacourse.moamoa.study.domain.Content; +import com.woowacourse.moamoa.study.domain.Participants; +import com.woowacourse.moamoa.study.domain.RecruitPlanner; +import com.woowacourse.moamoa.study.domain.RecruitStatus; +import com.woowacourse.moamoa.study.domain.Study; +import com.woowacourse.moamoa.study.domain.StudyPlanner; +import com.woowacourse.moamoa.study.domain.StudyStatus; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +class CommunityArticleTest { + + @DisplayName("스터디에 참여한 참가자만 게시글을 작성할 수 있다.") + @Test + void writeCommunityArticleByParticipant() { + final Member owner = createMember(1L); + final Member another = createMember(2L); + final Study study = createStudy(1L, owner); + + assertThatThrownBy(() -> CommunityArticle.write(another, study, new ArticleRequest("제목", "내용"))) + .isInstanceOf(NotParticipatedMemberException.class); + } + + @DisplayName("스터디 참여자는 게시글을 조회할 수 있다.") + @ParameterizedTest + @CsvSource({"1,true", "2,true", "3,false"}) + void getArticle(Long viewerId, boolean expected) { + final Member owner = createMember(1L); + final Member participant = createMember(2L); + final Study study = createStudy(1L, owner); + study.participate(participant.getId()); + + final CommunityArticle communityArticle = CommunityArticle.write(owner, study, new ArticleRequest("제목", "내용")); + + assertThat(communityArticle.isViewableBy(study.getId(), viewerId)).isEqualTo(expected); + } + + @DisplayName("스터디에 속해 있는 게시글이 맞을 경우 조회할 수 있다.") + @ParameterizedTest + @CsvSource({"1,1,true", "1,2,false"}) + void deleteArticle(Long studyId, Long wantToViewStudyId, boolean expected) { + final Member member = createMember(1L); + final Study study = createStudy(studyId, member); + final CommunityArticle communityArticle = CommunityArticle.write(member, study, new ArticleRequest("제목", "내용")); + + assertThat(communityArticle.isViewableBy(wantToViewStudyId, member.getId())).isEqualTo(expected); + } + + @DisplayName("스터디에 참여했고, 작성자인 경우 스터디 게시글을 수정,삭제할 수 있다.") + @ParameterizedTest + @CsvSource({"1,true", "2,false", "3,false"}) + void updateArticle(Long editorId, boolean expected) { + final Member owner = createMember(1L); + final Member participant = createMember(2L); + final Study study = createStudy(1L, owner); + study.participate(participant.getId()); + final CommunityArticle communityArticle = CommunityArticle.write(owner, study, new ArticleRequest("제목", "내용")); + + assertThat(communityArticle.isEditableBy(study.getId(), editorId)).isEqualTo(expected); + } + + @DisplayName("스터디에 속해 있지 않은 게시글인 경우 수정,삭제할 수 없다.") + @Test + void editArticleByInvalidStudyId() { + final Member member = createMember(1L); + final Study study = createStudy(1L, member); + final CommunityArticle communityArticle = CommunityArticle.write(member, study, new ArticleRequest("제목", "내용")); + + assertThat(communityArticle.isViewableBy(2L, member.getId())).isFalse(); + } + + private Study createStudy(final long id, final Member owner) { + return new Study(id, + new Content("제목", "한 줄 소개", "http://image", "설명"), + Participants.createBy(owner.getId()), + new RecruitPlanner(10, RecruitStatus.RECRUITMENT_START, null), + new StudyPlanner(LocalDate.now(), null, StudyStatus.IN_PROGRESS), + new AttachedTags(List.of()), + LocalDateTime.now() + ); + } + + private Member createMember(final long id) { + return new Member(id, id, "username" + id, "image", "profile"); + } +} diff --git a/backend/src/test/java/com/woowacourse/moamoa/community/webmvc/CreatingCommunityArticleControllerWebMvcTest.java b/backend/src/test/java/com/woowacourse/moamoa/community/webmvc/CreatingCommunityArticleControllerWebMvcTest.java new file mode 100644 index 000000000..9b9f80539 --- /dev/null +++ b/backend/src/test/java/com/woowacourse/moamoa/community/webmvc/CreatingCommunityArticleControllerWebMvcTest.java @@ -0,0 +1,162 @@ +package com.woowacourse.moamoa.community.webmvc; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.woowacourse.moamoa.WebMVCTest; +import com.woowacourse.moamoa.community.service.CommunityArticleService; +import com.woowacourse.moamoa.community.service.request.ArticleRequest; +import com.woowacourse.moamoa.member.service.exception.NotParticipatedMemberException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.ValueSource; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; + +public class CreatingCommunityArticleControllerWebMvcTest extends WebMVCTest { + + @MockBean + private CommunityArticleService communityArticleService; + + @DisplayName("잘못된 토큰으로 커뮤니티 글을 생성할 경우 401을 반환한다.") + @ParameterizedTest + @ValueSource(strings = {"", "Bearer InvalidToken", "Invalid"}) + void unauthorizedByInvalidToken(String token) throws Exception { + mockMvc.perform( + post("/api/studies/{study-id}/community/articles", 1L) + .header(HttpHeaders.AUTHORIZATION, token) + ) + .andExpect(status().isUnauthorized()) + .andDo(print()); + } + + @DisplayName("스터디 ID가 잘못된 형식인 경우 400에러를 반환한다.") + @Test + void badRequestByInvalidStudyIdFormat() throws Exception { + final String token = "Bearer" + tokenProvider.createToken(1L).getAccessToken(); + + mockMvc.perform( + post("/api/studies/{study-id}/community/articles", "one") + .header(HttpHeaders.AUTHORIZATION, token) + ) + .andExpect(status().isBadRequest()) + .andDo(print()); + } + + @DisplayName("제목은 null 또는 Empty 일 수 없다.") + @ParameterizedTest + @NullAndEmptySource + void badRequestByNullOrEmptyTitle(String title) throws Exception { + final String token = "Bearer" + tokenProvider.createToken(1L).getAccessToken(); + + mockMvc.perform( + post("/api/studies/{study-id}/community/articles", "1") + .header(HttpHeaders.AUTHORIZATION, token) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(new ArticleRequest(title, "content"))) + ) + .andExpect(status().isBadRequest()) + .andDo(print()); + } + + @DisplayName("내용은 null 또는 Empty 일 수 없다.") + @ParameterizedTest + @NullAndEmptySource + void badRequestByNullOrEmptyContent(String content) throws Exception { + final String token = "Bearer" + tokenProvider.createToken(1L).getAccessToken(); + + mockMvc.perform( + post("/api/studies/{study-id}/community/articles", "1") + .header(HttpHeaders.AUTHORIZATION, token) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(new ArticleRequest("title", content))) + ) + .andExpect(status().isBadRequest()) + .andDo(print()); + } + + @DisplayName("제목은 공백일 수 없다.") + @ParameterizedTest + @ValueSource(strings = {" ", "\t", "\n"}) + void badRequestByBlankTitle(String title) throws Exception { + final String token = "Bearer" + tokenProvider.createToken(1L).getAccessToken(); + + mockMvc.perform( + post("/api/studies/{study-id}/community/articles", "1") + .header(HttpHeaders.AUTHORIZATION, token) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(new ArticleRequest(title, "content"))) + ) + .andExpect(status().isBadRequest()) + .andDo(print()); + } + + @DisplayName("내용은 공백일 수 없다.") + @ParameterizedTest + @ValueSource(strings = {" ", "\t", "\n"}) + void badRequestByBlankContent(String content) throws Exception { + final String token = "Bearer" + tokenProvider.createToken(1L).getAccessToken(); + + mockMvc.perform( + post("/api/studies/{study-id}/community/articles", "1") + .header(HttpHeaders.AUTHORIZATION, token) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(new ArticleRequest("title", content))) + ) + .andExpect(status().isBadRequest()) + .andDo(print()); + } + + @DisplayName("제목은 30자 이하여야 한다.") + @Test + void badRequestByInvalidLengthTitle() throws Exception { + final String token = "Bearer" + tokenProvider.createToken(1L).getAccessToken(); + + mockMvc.perform( + post("/api/studies/{study-id}/community/articles", "1") + .header(HttpHeaders.AUTHORIZATION, token) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(new ArticleRequest("a".repeat(31), "cotent"))) + ) + .andExpect(status().isBadRequest()) + .andDo(print()); + } + + @DisplayName("내용은 5000자 이하여야 한다.") + @Test + void badRequestByInvalidLengthContent() throws Exception { + final String token = "Bearer" + tokenProvider.createToken(1L).getAccessToken(); + + mockMvc.perform( + post("/api/studies/{study-id}/community/articles", "1") + .header(HttpHeaders.AUTHORIZATION, token) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(new ArticleRequest("a".repeat(5001), "cotent"))) + ) + .andExpect(status().isBadRequest()) + .andDo(print()); + } + + @DisplayName("스터디에 참여한 참가자가 아닌 경우, NotParticipatedMemberException이 발생하고 401을 반환한다.") + @Test + void unauthorizedByNotParticipant() throws Exception { + when(communityArticleService.createArticle(any(), any(), any())).thenThrow(NotParticipatedMemberException.class); + + final String token = "Bearer" + tokenProvider.createToken(1L).getAccessToken(); + + mockMvc.perform( + post("/api/studies/{study-id}/community/articles", "1") + .header(HttpHeaders.AUTHORIZATION, token) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(new ArticleRequest("title", "content"))) + ) + .andExpect(status().isUnauthorized()) + .andDo(print()); + } +} diff --git a/backend/src/test/java/com/woowacourse/moamoa/community/webmvc/DeletingCommunityArticleControllerWebMvcTest.java b/backend/src/test/java/com/woowacourse/moamoa/community/webmvc/DeletingCommunityArticleControllerWebMvcTest.java new file mode 100644 index 000000000..6104ad81b --- /dev/null +++ b/backend/src/test/java/com/woowacourse/moamoa/community/webmvc/DeletingCommunityArticleControllerWebMvcTest.java @@ -0,0 +1,42 @@ +package com.woowacourse.moamoa.community.webmvc; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.woowacourse.moamoa.WebMVCTest; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; +import org.springframework.http.HttpHeaders; + +public class DeletingCommunityArticleControllerWebMvcTest extends WebMVCTest { + + @DisplayName("잘못된 토큰으로 커뮤니티 글을 생성할 경우 401을 반환한다.") + @ParameterizedTest + @ValueSource(strings = {"", "Bearer InvalidToken", "Invalid"}) + void unauthorizedByInvalidToken(String token) throws Exception { + mockMvc.perform( + delete("/api/studies/{study-id}/community/articles/{article-id}", 1L, 1L) + .header(HttpHeaders.AUTHORIZATION, token) + ) + .andExpect(status().isUnauthorized()) + .andDo(print()); + } + + @DisplayName("스터디 ID 또는 게시글 ID가 잘못된 형식인 경우 400에러를 반환한다.") + @ParameterizedTest + @CsvSource({"one, 1", "1, one"}) + void badRequestByInvalidIdFormat(String studyId, String articleId) throws Exception { + final String token = "Bearer" + tokenProvider.createToken(1L).getAccessToken(); + + mockMvc.perform( + delete("/api/studies/{study-id}/community/articles/{article-id}", studyId, articleId) + .header(HttpHeaders.AUTHORIZATION, token) + ) + .andExpect(status().isBadRequest()) + .andDo(print()); + } +} diff --git a/backend/src/test/java/com/woowacourse/moamoa/community/webmvc/GettingCommunityArticleControllerWebMvcTest.java b/backend/src/test/java/com/woowacourse/moamoa/community/webmvc/GettingCommunityArticleControllerWebMvcTest.java new file mode 100644 index 000000000..710b1294d --- /dev/null +++ b/backend/src/test/java/com/woowacourse/moamoa/community/webmvc/GettingCommunityArticleControllerWebMvcTest.java @@ -0,0 +1,99 @@ +package com.woowacourse.moamoa.community.webmvc; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.woowacourse.moamoa.WebMVCTest; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; +import org.springframework.http.HttpHeaders; + +public class GettingCommunityArticleControllerWebMvcTest extends WebMVCTest { + + @DisplayName("잘못된 토큰으로 커뮤니티 글을 조회할 경우 401을 반환한다.") + @ParameterizedTest + @ValueSource(strings = {"", "Bearer InvalidToken", "Invalid"}) + void unauthorizedGetArticleByInvalidToken(String token) throws Exception { + mockMvc.perform( + get("/api/studies/{study-id}/community/articles/{article-id}", 1L, 1L) + .header(HttpHeaders.AUTHORIZATION, token) + ) + .andExpect(status().isUnauthorized()) + .andDo(print()); + } + + @DisplayName("스터디 ID 또는 게시글 ID가 잘못된 형식인 경우 400에러를 반환한다.") + @ParameterizedTest + @CsvSource({"one, 1", "1, one"}) + void badRequestByInvalidIdFormat(String studyId, String articleId) throws Exception { + final String token = "Bearer" + tokenProvider.createToken(1L).getAccessToken(); + + mockMvc.perform( + get("/api/studies/{study-id}/community/articles/{article-id}", studyId, articleId) + .header(HttpHeaders.AUTHORIZATION, token) + ) + .andExpect(status().isBadRequest()) + .andDo(print()); + } + + @DisplayName("잘못된 토큰으로 커뮤니티 글 목록을 조회할 경우 401을 반환한다.") + @ParameterizedTest + @ValueSource(strings = {"", "Bearer InvalidToken", "Invalid"}) + void unauthorizedGetArticleListByInvalidToken(String token) throws Exception { + mockMvc.perform( + get("/api/studies/{study-id}/community/articles?page=0&size=3", 1L) + .header(HttpHeaders.AUTHORIZATION, token) + ) + .andExpect(status().isUnauthorized()) + .andDo(print()); + } + + @DisplayName("스터디 ID가 잘못된 형식인 경우 400에러를 반환한다.") + @Test + void badRequestByInvalidIdFormat() throws Exception { + final String token = "Bearer" + tokenProvider.createToken(1L).getAccessToken(); + + mockMvc.perform( + get("/api/studies/{study-id}/community/articles", "one") + .header(HttpHeaders.AUTHORIZATION, token) + ) + .andExpect(status().isBadRequest()) + .andDo(print()); + } + + @DisplayName("잘못된 형식의 파라미터를 전달한 경우 400 에러를 반환한다.") + @ParameterizedTest + @CsvSource({"one,1", "1,one", "-1,3", "1,-1", "1,0"}) + void badRequestByInvalidFormatParam(String page, String size) throws Exception { + final String token = "Bearer" + tokenProvider.createToken(1L).getAccessToken(); + + mockMvc.perform( + get("/api/studies/{study-id}/community/articles", "1") + .header(HttpHeaders.AUTHORIZATION, token) + .param("page", page) + .param("size", size) + ) + .andExpect(status().isBadRequest()) + .andDo(print()); + } + + @DisplayName("페이지 정보 없이 게시글 목록 조회 시 400 에러를 반환한다.") + @Test + void badRequestByEmptyPage() throws Exception { + final String token = "Bearer" + tokenProvider.createToken(1L).getAccessToken(); + + mockMvc.perform( + get("/api/studies/{study-id}/community/articles", "1") + .header(HttpHeaders.AUTHORIZATION, token) + .param("page", "") + .param("size", "5") + ) + .andExpect(status().isBadRequest()) + .andDo(print()); + } +} diff --git a/backend/src/test/java/com/woowacourse/moamoa/community/webmvc/UpdatingCommunityArticleControllerWebMvcTest.java b/backend/src/test/java/com/woowacourse/moamoa/community/webmvc/UpdatingCommunityArticleControllerWebMvcTest.java new file mode 100644 index 000000000..336b9d17a --- /dev/null +++ b/backend/src/test/java/com/woowacourse/moamoa/community/webmvc/UpdatingCommunityArticleControllerWebMvcTest.java @@ -0,0 +1,139 @@ +package com.woowacourse.moamoa.community.webmvc; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.woowacourse.moamoa.WebMVCTest; +import com.woowacourse.moamoa.community.service.request.ArticleRequest; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.ValueSource; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; + +public class UpdatingCommunityArticleControllerWebMvcTest extends WebMVCTest { + + @DisplayName("잘못된 토큰으로 커뮤니티 글을 수정할 경우 401을 반환한다.") + @ParameterizedTest + @ValueSource(strings = {"", "Bearer InvalidToken", "Invalid"}) + void unauthorizedByInvalidToken(String token) throws Exception { + mockMvc.perform( + put("/api/studies/{study-id}/community/articles/{article-id}", 1L, 1L) + .header(HttpHeaders.AUTHORIZATION, token) + ) + .andExpect(status().isUnauthorized()) + .andDo(print()); + } + + @DisplayName("스터디 ID 또는 게시글 ID가 잘못된 형식인 경우 400에러를 반환한다.") + @ParameterizedTest + @CsvSource({"one, 1", "1, one"}) + void badRequestByInvalidIdFormat(String studyId, String articleId) throws Exception { + final String token = "Bearer" + tokenProvider.createToken(1L).getAccessToken(); + + mockMvc.perform( + put("/api/studies/{study-id}/community/articles/{article-id}", studyId, articleId) + .header(HttpHeaders.AUTHORIZATION, token) + ) + .andExpect(status().isBadRequest()) + .andDo(print()); + } + + @DisplayName("제목은 null 또는 Empty 일 수 없다.") + @ParameterizedTest + @NullAndEmptySource + void badRequestByNullOrEmptyTitle(String title) throws Exception { + final String token = "Bearer" + tokenProvider.createToken(1L).getAccessToken(); + + mockMvc.perform( + put("/api/studies/{study-id}/community/articles/{article-id}", "1", "1") + .header(HttpHeaders.AUTHORIZATION, token) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(new ArticleRequest(title, "content"))) + ) + .andExpect(status().isBadRequest()) + .andDo(print()); + } + + @DisplayName("내용은 null 또는 Empty 일 수 없다.") + @ParameterizedTest + @NullAndEmptySource + void badRequestByNullOrEmptyContent(String content) throws Exception { + final String token = "Bearer" + tokenProvider.createToken(1L).getAccessToken(); + + mockMvc.perform( + put("/api/studies/{study-id}/community/articles/{article-id}", "1", "1") + .header(HttpHeaders.AUTHORIZATION, token) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(new ArticleRequest("title", content))) + ) + .andExpect(status().isBadRequest()) + .andDo(print()); + } + + @DisplayName("제목은 공백일 수 없다.") + @ParameterizedTest + @ValueSource(strings = {" ", "\t", "\n"}) + void badRequestByBlankTitle(String title) throws Exception { + final String token = "Bearer" + tokenProvider.createToken(1L).getAccessToken(); + + mockMvc.perform( + put("/api/studies/{study-id}/community/articles/{article-id}", "1", "1") + .header(HttpHeaders.AUTHORIZATION, token) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(new ArticleRequest(title, "content"))) + ) + .andExpect(status().isBadRequest()) + .andDo(print()); + } + + @DisplayName("내용은 공백일 수 없다.") + @ParameterizedTest + @ValueSource(strings = {" ", "\t", "\n"}) + void badRequestByBlankContent(String content) throws Exception { + final String token = "Bearer" + tokenProvider.createToken(1L).getAccessToken(); + + mockMvc.perform( + put("/api/studies/{study-id}/community/articles/{article-id}", "1", "1") + .header(HttpHeaders.AUTHORIZATION, token) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(new ArticleRequest("title", content))) + ) + .andExpect(status().isBadRequest()) + .andDo(print()); + } + + @DisplayName("제목은 30자 이하여야 한다.") + @Test + void badRequestByInvalidLengthTitle() throws Exception { + final String token = "Bearer" + tokenProvider.createToken(1L).getAccessToken(); + + mockMvc.perform( + put("/api/studies/{study-id}/community/articles/{article-id}", "1", "1") + .header(HttpHeaders.AUTHORIZATION, token) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(new ArticleRequest("a".repeat(31), "cotent"))) + ) + .andExpect(status().isBadRequest()) + .andDo(print()); + } + + @DisplayName("내용은 5000자 이하여야 한다.") + @Test + void badRequestByInvalidLengthContent() throws Exception { + final String token = "Bearer" + tokenProvider.createToken(1L).getAccessToken(); + + mockMvc.perform( + put("/api/studies/{study-id}/community/articles/{article-id}", "1", "1") + .header(HttpHeaders.AUTHORIZATION, token) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(new ArticleRequest("a".repeat(5001), "cotent"))) + ) + .andExpect(status().isBadRequest()) + .andDo(print()); + } +} diff --git a/backend/src/test/java/com/woowacourse/fixtures/AuthFixtures.java b/backend/src/test/java/com/woowacourse/moamoa/fixtures/AuthFixtures.java similarity index 71% rename from backend/src/test/java/com/woowacourse/fixtures/AuthFixtures.java rename to backend/src/test/java/com/woowacourse/moamoa/fixtures/AuthFixtures.java index cd9ce534f..de99710a8 100644 --- a/backend/src/test/java/com/woowacourse/fixtures/AuthFixtures.java +++ b/backend/src/test/java/com/woowacourse/moamoa/fixtures/AuthFixtures.java @@ -1,4 +1,4 @@ -package com.woowacourse.fixtures; +package com.woowacourse.moamoa.fixtures; public class AuthFixtures { diff --git a/backend/src/test/java/com/woowacourse/fixtures/CategoryFixtures.java b/backend/src/test/java/com/woowacourse/moamoa/fixtures/CategoryFixtures.java similarity index 96% rename from backend/src/test/java/com/woowacourse/fixtures/CategoryFixtures.java rename to backend/src/test/java/com/woowacourse/moamoa/fixtures/CategoryFixtures.java index ab91e1805..e681a8c59 100644 --- a/backend/src/test/java/com/woowacourse/fixtures/CategoryFixtures.java +++ b/backend/src/test/java/com/woowacourse/moamoa/fixtures/CategoryFixtures.java @@ -1,4 +1,4 @@ -package com.woowacourse.fixtures; +package com.woowacourse.moamoa.fixtures; import com.woowacourse.moamoa.tag.domain.Category; import com.woowacourse.moamoa.tag.domain.CategoryName; diff --git a/backend/src/test/java/com/woowacourse/fixtures/MemberFixtures.java b/backend/src/test/java/com/woowacourse/moamoa/fixtures/MemberFixtures.java similarity index 98% rename from backend/src/test/java/com/woowacourse/fixtures/MemberFixtures.java rename to backend/src/test/java/com/woowacourse/moamoa/fixtures/MemberFixtures.java index 76f835837..62cced407 100644 --- a/backend/src/test/java/com/woowacourse/fixtures/MemberFixtures.java +++ b/backend/src/test/java/com/woowacourse/moamoa/fixtures/MemberFixtures.java @@ -1,4 +1,4 @@ -package com.woowacourse.fixtures; +package com.woowacourse.moamoa.fixtures; import com.woowacourse.moamoa.member.domain.Member; import com.woowacourse.moamoa.member.query.data.MemberData; diff --git a/backend/src/test/java/com/woowacourse/fixtures/ReviewFixtures.java b/backend/src/test/java/com/woowacourse/moamoa/fixtures/ReviewFixtures.java similarity index 76% rename from backend/src/test/java/com/woowacourse/fixtures/ReviewFixtures.java rename to backend/src/test/java/com/woowacourse/moamoa/fixtures/ReviewFixtures.java index dce9d1d24..c99f56bd7 100644 --- a/backend/src/test/java/com/woowacourse/fixtures/ReviewFixtures.java +++ b/backend/src/test/java/com/woowacourse/moamoa/fixtures/ReviewFixtures.java @@ -1,14 +1,14 @@ -package com.woowacourse.fixtures; - -import static com.woowacourse.fixtures.MemberFixtures.그린론_아이디; -import static com.woowacourse.fixtures.MemberFixtures.그린론_응답; -import static com.woowacourse.fixtures.MemberFixtures.디우_아이디; -import static com.woowacourse.fixtures.MemberFixtures.디우_응답; -import static com.woowacourse.fixtures.MemberFixtures.베루스_아이디; -import static com.woowacourse.fixtures.MemberFixtures.베루스_응답; -import static com.woowacourse.fixtures.MemberFixtures.짱구_아이디; -import static com.woowacourse.fixtures.MemberFixtures.짱구_응답; -import static com.woowacourse.fixtures.StudyFixtures.자바_스터디_아이디; +package com.woowacourse.moamoa.fixtures; + +import static com.woowacourse.moamoa.fixtures.MemberFixtures.그린론_아이디; +import static com.woowacourse.moamoa.fixtures.MemberFixtures.그린론_응답; +import static com.woowacourse.moamoa.fixtures.MemberFixtures.디우_아이디; +import static com.woowacourse.moamoa.fixtures.MemberFixtures.디우_응답; +import static com.woowacourse.moamoa.fixtures.MemberFixtures.베루스_아이디; +import static com.woowacourse.moamoa.fixtures.MemberFixtures.베루스_응답; +import static com.woowacourse.moamoa.fixtures.MemberFixtures.짱구_아이디; +import static com.woowacourse.moamoa.fixtures.MemberFixtures.짱구_응답; +import static com.woowacourse.moamoa.fixtures.StudyFixtures.자바_스터디_아이디; import com.woowacourse.moamoa.review.domain.AssociatedStudy; import com.woowacourse.moamoa.review.domain.Review; diff --git a/backend/src/test/java/com/woowacourse/fixtures/StudyFixtures.java b/backend/src/test/java/com/woowacourse/moamoa/fixtures/StudyFixtures.java similarity index 76% rename from backend/src/test/java/com/woowacourse/fixtures/StudyFixtures.java rename to backend/src/test/java/com/woowacourse/moamoa/fixtures/StudyFixtures.java index ddfe83778..064a04102 100644 --- a/backend/src/test/java/com/woowacourse/fixtures/StudyFixtures.java +++ b/backend/src/test/java/com/woowacourse/moamoa/fixtures/StudyFixtures.java @@ -1,19 +1,4 @@ -package com.woowacourse.fixtures; - -import static com.woowacourse.fixtures.MemberFixtures.그린론_아이디; -import static com.woowacourse.fixtures.MemberFixtures.디우_아이디; -import static com.woowacourse.fixtures.MemberFixtures.베루스_아이디; -import static com.woowacourse.fixtures.MemberFixtures.짱구_아이디; -import static com.woowacourse.fixtures.TagFixtures.BE_태그_아이디; -import static com.woowacourse.fixtures.TagFixtures.BE_태그_요약; -import static com.woowacourse.fixtures.TagFixtures.FE_태그_아이디; -import static com.woowacourse.fixtures.TagFixtures.FE_태그_요약; -import static com.woowacourse.fixtures.TagFixtures.리액트_태그_아이디; -import static com.woowacourse.fixtures.TagFixtures.리액트_태그_요약; -import static com.woowacourse.fixtures.TagFixtures.우테코4기_태그_아이디; -import static com.woowacourse.fixtures.TagFixtures.우테코4기_태그_요약; -import static com.woowacourse.fixtures.TagFixtures.자바_태그_아이디; -import static com.woowacourse.fixtures.TagFixtures.자바_태그_요약; +package com.woowacourse.moamoa.fixtures; import com.woowacourse.moamoa.study.domain.AttachedTag; import com.woowacourse.moamoa.study.domain.AttachedTags; @@ -35,55 +20,65 @@ public class StudyFixtures { /* 자바 스터디 */ public static final Long 자바_스터디_아이디 = 1L; public static final Content 자바_스터디_내용 = new Content("신짱구의 자바의 정석", "자바 스터디 요약", "자바 스터디 썸네일", "자바 스터디 설명입니다."); - public static final Participants 자바_스터디_참가자들 = new Participants(짱구_아이디, Set.of(그린론_아이디, 디우_아이디)); + public static final Participants 자바_스터디_참가자들 = new Participants( + MemberFixtures.짱구_아이디, Set.of(MemberFixtures.그린론_아이디, MemberFixtures.디우_아이디)); public static final RecruitPlanner 자바_스터디_모집계획 = new RecruitPlanner(10, RecruitStatus.RECRUITMENT_START, LocalDate.now()); public static final StudyPlanner 자바_스터디_계획 = new StudyPlanner(LocalDate.now().plusMonths(1), LocalDate.now().plusMonths(2), StudyStatus.IN_PROGRESS); - public static final AttachedTags 자바_스터디_태그 = new AttachedTags(List.of(new AttachedTag(자바_태그_아이디), new AttachedTag(우테코4기_태그_아이디), new AttachedTag(BE_태그_아이디))); + public static final AttachedTags 자바_스터디_태그 = new AttachedTags(List.of(new AttachedTag(TagFixtures.자바_태그_아이디), new AttachedTag( + TagFixtures.우테코4기_태그_아이디), new AttachedTag(TagFixtures.BE_태그_아이디))); public static final Study 자바_스터디 = new Study(자바_스터디_내용, 자바_스터디_참가자들, 자바_스터디_모집계획, 자바_스터디_계획, 자바_스터디_태그, LocalDateTime.now()); public static final StudyResponse 자바_스터디_응답 = new StudyResponse(자바_스터디_아이디, 자바_스터디_내용.getTitle(), 자바_스터디_내용.getExcerpt(), - 자바_스터디_내용.getThumbnail(), 자바_스터디_모집계획.getRecruitStatus().name(), List.of(자바_태그_요약, 우테코4기_태그_요약, BE_태그_요약)); + 자바_스터디_내용.getThumbnail(), 자바_스터디_모집계획.getRecruitStatus().name(), List.of( + TagFixtures.자바_태그_요약, TagFixtures.우테코4기_태그_요약, TagFixtures.BE_태그_요약)); /* 리액트 스터디 */ public static final Long 리액트_스터디_아이디 = 2L; public static final Content 리액트_스터디_내용 = new Content("디우의 이것이 리액트다.", "리액트 스터디 요약", "리액트 스터디 썸네일", "리액트 스터디 설명입니다."); - public static final Participants 리액트_스터디_참가자들 = new Participants(디우_아이디, Set.of(짱구_아이디, 그린론_아이디, 베루스_아이디)); + public static final Participants 리액트_스터디_참가자들 = new Participants( + MemberFixtures.디우_아이디, Set.of(MemberFixtures.짱구_아이디, MemberFixtures.그린론_아이디, MemberFixtures.베루스_아이디)); public static final RecruitPlanner 리액트_스터디_모집계획 = new RecruitPlanner(5, RecruitStatus.RECRUITMENT_START, LocalDate.now()); public static final StudyPlanner 리액트_스터디_계획 = new StudyPlanner(LocalDate.now().plusMonths(1), LocalDate.now().plusMonths(2), StudyStatus.PREPARE); - public static final AttachedTags 리액트_스터디_태그 = new AttachedTags(List.of(new AttachedTag(우테코4기_태그_아이디), new AttachedTag(FE_태그_아이디), new AttachedTag(리액트_태그_아이디))); + public static final AttachedTags 리액트_스터디_태그 = new AttachedTags(List.of(new AttachedTag(TagFixtures.우테코4기_태그_아이디), new AttachedTag( + TagFixtures.FE_태그_아이디), new AttachedTag(TagFixtures.리액트_태그_아이디))); public static final Study 리액트_스터디 = new Study(리액트_스터디_내용, 리액트_스터디_참가자들, 리액트_스터디_모집계획, 리액트_스터디_계획, 리액트_스터디_태그, LocalDateTime.now()); public static final StudyResponse 리액트_스터디_응답 = new StudyResponse(리액트_스터디_아이디, 리액트_스터디_내용.getTitle(), 리액트_스터디_내용.getExcerpt(), - 리액트_스터디_내용.getThumbnail(), 리액트_스터디_모집계획.getRecruitStatus().name(), List.of(우테코4기_태그_요약, FE_태그_요약, 리액트_태그_요약)); + 리액트_스터디_내용.getThumbnail(), 리액트_스터디_모집계획.getRecruitStatus().name(), List.of(TagFixtures.우테코4기_태그_요약, TagFixtures.FE_태그_요약, TagFixtures.리액트_태그_요약)); /* 자바스크립트스크립트 스터디 */ public static final Long 자바스크립트_스터디_아이디 = 3L; public static final Content 자바스크립트_스터디_내용 = new Content("그린론의 모던 자바스크립트 인 액션", "자바스크립트 스터디 요약", "자바스크립트 스터디 썸네일", "자바스크립트 스터디 설명입니다."); - public static final Participants 자바스크립트_스터디_참가자들 = new Participants(그린론_아이디, Set.of(디우_아이디, 베루스_아이디)); + public static final Participants 자바스크립트_스터디_참가자들 = new Participants( + MemberFixtures.그린론_아이디, Set.of(MemberFixtures.디우_아이디, MemberFixtures.베루스_아이디)); public static final RecruitPlanner 자바스크립트_스터디_모집계획 = new RecruitPlanner(20, RecruitStatus.RECRUITMENT_START, LocalDate.now()); public static final StudyPlanner 자바스크립트_스터디_계획 = new StudyPlanner(LocalDate.now().plusMonths(1), LocalDate.now().plusMonths(2), StudyStatus.PREPARE); - public static final AttachedTags 자바스크립트_스터디_태그 = new AttachedTags(List.of(new AttachedTag(우테코4기_태그_아이디), new AttachedTag(FE_태그_아이디))); + public static final AttachedTags 자바스크립트_스터디_태그 = new AttachedTags(List.of(new AttachedTag(TagFixtures.우테코4기_태그_아이디), new AttachedTag( + TagFixtures.FE_태그_아이디))); public static final Study 자바스크립트_스터디 = new Study(자바스크립트_스터디_내용, 자바스크립트_스터디_참가자들, 자바스크립트_스터디_모집계획, 자바스크립트_스터디_계획, 자바스크립트_스터디_태그, LocalDateTime.now()); public static final StudyResponse 자바스크립트_스터디_응답 = new StudyResponse(자바스크립트_스터디_아이디, 자바스크립트_스터디_내용.getTitle(), 자바스크립트_스터디_내용.getExcerpt(), - 자바스크립트_스터디_내용.getThumbnail(), 자바스크립트_스터디_모집계획.getRecruitStatus().name(), List.of(우테코4기_태그_요약, FE_태그_요약)); + 자바스크립트_스터디_내용.getThumbnail(), 자바스크립트_스터디_모집계획.getRecruitStatus().name(), List.of(TagFixtures.우테코4기_태그_요약, TagFixtures.FE_태그_요약)); /* HTTP 스터디 */ public static final Long HTTP_스터디_아이디 = 4L; public static final Content HTTP_스터디_내용 = new Content("디우의 HTTP", "HTTP 스터디 요약", "HTTP 스터디 썸네일", "HTTP 스터디 설명입니다."); - public static final Participants HTTP_스터디_참가자들 = new Participants(디우_아이디, Set.of(베루스_아이디, 짱구_아이디)); + public static final Participants HTTP_스터디_참가자들 = new Participants( + MemberFixtures.디우_아이디, Set.of(MemberFixtures.베루스_아이디, MemberFixtures.짱구_아이디)); public static final RecruitPlanner HTTP_스터디_모집계획 = new RecruitPlanner(4, RecruitStatus.RECRUITMENT_END, LocalDate.now()); public static final StudyPlanner HTTP_스터디_계획 = new StudyPlanner(LocalDate.now().plusMonths(1), LocalDate.now().plusMonths(2), StudyStatus.PREPARE); - public static final AttachedTags HTTP_스터디_태그 = new AttachedTags(List.of(new AttachedTag(우테코4기_태그_아이디), new AttachedTag(BE_태그_아이디))); + public static final AttachedTags HTTP_스터디_태그 = new AttachedTags(List.of(new AttachedTag(TagFixtures.우테코4기_태그_아이디), new AttachedTag( + TagFixtures.BE_태그_아이디))); public static final Study HTTP_스터디 = new Study(HTTP_스터디_내용, HTTP_스터디_참가자들, HTTP_스터디_모집계획, HTTP_스터디_계획, HTTP_스터디_태그, LocalDateTime.now()); public static final StudyResponse HTTP_스터디_응답 = new StudyResponse(HTTP_스터디_아이디, HTTP_스터디_내용.getTitle(), HTTP_스터디_내용.getExcerpt(), - HTTP_스터디_내용.getThumbnail(), HTTP_스터디_모집계획.getRecruitStatus().name(), List.of(우테코4기_태그_요약, BE_태그_요약)); + HTTP_스터디_내용.getThumbnail(), HTTP_스터디_모집계획.getRecruitStatus().name(), List.of(TagFixtures.우테코4기_태그_요약, TagFixtures.BE_태그_요약)); /* 알고리즘 스터디 */ public static final Long 알고리즘_스터디_아이디 = 5L; public static final Content 알고리즘_스터디_내용 = new Content("알고리즘 주도 개발 1타 강사 베루스", "알고리즘 스터디 요약", "알고리즘 스터디 썸네일", "알고리즘 스터디 설명입니다."); - public static final Participants 알고리즘_스터디_참가자들 = new Participants(베루스_아이디, Set.of(그린론_아이디, 디우_아이디)); + public static final Participants 알고리즘_스터디_참가자들 = new Participants( + MemberFixtures.베루스_아이디, Set.of(MemberFixtures.그린론_아이디, MemberFixtures.디우_아이디)); public static final RecruitPlanner 알고리즘_스터디_모집계획 = new RecruitPlanner(10, RecruitStatus.RECRUITMENT_END, LocalDate.now()); public static final StudyPlanner 알고리즘_스터디_계획 = new StudyPlanner(LocalDate.now().plusMonths(1), LocalDate.now().plusMonths(2), StudyStatus.PREPARE); public static final AttachedTags 알고리즘_스터디_태그 = new AttachedTags(List.of()); @@ -95,7 +90,8 @@ public class StudyFixtures { /* 리눅스 스터디 */ public static final Long 리눅스_스터디_아이디 = 6L; public static final Content 리눅스_스터디_내용 = new Content("벨우스의 린우스", "리눅스 스터디 요약", "리눅스 스터디 썸네일", "리눅스 스터디 설명입니다."); - public static final Participants 리눅스_스터디_참가자들 = new Participants(베루스_아이디, Set.of(그린론_아이디, 디우_아이디)); + public static final Participants 리눅스_스터디_참가자들 = new Participants( + MemberFixtures.베루스_아이디, Set.of(MemberFixtures.그린론_아이디, MemberFixtures.디우_아이디)); public static final RecruitPlanner 리눅스_스터디_모집계획 = new RecruitPlanner(10, RecruitStatus.RECRUITMENT_START, LocalDate.now()); public static final StudyPlanner 리눅스_스터디_계획 = new StudyPlanner(LocalDate.now().plusMonths(1), LocalDate.now().plusMonths(2), StudyStatus.PREPARE); public static final AttachedTags 리눅스_스터디_태그 = new AttachedTags(List.of()); diff --git a/backend/src/test/java/com/woowacourse/fixtures/TagFixtures.java b/backend/src/test/java/com/woowacourse/moamoa/fixtures/TagFixtures.java similarity index 80% rename from backend/src/test/java/com/woowacourse/fixtures/TagFixtures.java rename to backend/src/test/java/com/woowacourse/moamoa/fixtures/TagFixtures.java index 5cdee01c5..2abc6866b 100644 --- a/backend/src/test/java/com/woowacourse/fixtures/TagFixtures.java +++ b/backend/src/test/java/com/woowacourse/moamoa/fixtures/TagFixtures.java @@ -1,8 +1,4 @@ -package com.woowacourse.fixtures; - -import static com.woowacourse.fixtures.CategoryFixtures.AREA_응답; -import static com.woowacourse.fixtures.CategoryFixtures.GENERATION_응답; -import static com.woowacourse.fixtures.CategoryFixtures.SUBJECT_응답; +package com.woowacourse.moamoa.fixtures; import com.woowacourse.moamoa.tag.query.response.TagData; import com.woowacourse.moamoa.tag.query.response.TagSummaryData; @@ -12,30 +8,30 @@ public class TagFixtures { public static final Long 자바_태그_아이디 = 1L; public static final String 자바_태그명 = "Java"; public static final String 자바_태그_설명 = "자바"; - public static final TagData 자바_태그 = new TagData(자바_태그_아이디, 자바_태그명, 자바_태그_설명, SUBJECT_응답); + public static final TagData 자바_태그 = new TagData(자바_태그_아이디, 자바_태그명, 자바_태그_설명, CategoryFixtures.SUBJECT_응답); public static final TagSummaryData 자바_태그_요약 = new TagSummaryData(자바_태그_아이디, 자바_태그명); public static final Long 우테코4기_태그_아이디 = 2L; public static final String 우테코4기_태그명 = "4기"; public static final String 우테코4기_태그_설명 = "우테코4기"; - public static final TagData 우테코4기_태그 = new TagData(우테코4기_태그_아이디, 우테코4기_태그명, 우테코4기_태그_설명, GENERATION_응답); + public static final TagData 우테코4기_태그 = new TagData(우테코4기_태그_아이디, 우테코4기_태그명, 우테코4기_태그_설명, CategoryFixtures.GENERATION_응답); public static final TagSummaryData 우테코4기_태그_요약 = new TagSummaryData(우테코4기_태그_아이디, 우테코4기_태그명); public static final Long BE_태그_아이디 = 3L; public static final String BE_태그명 = "BE"; public static final String BE_태그_설명 = "백엔드"; - public static final TagData BE_태그 = new TagData(BE_태그_아이디, BE_태그명, BE_태그_설명, AREA_응답); + public static final TagData BE_태그 = new TagData(BE_태그_아이디, BE_태그명, BE_태그_설명, CategoryFixtures.AREA_응답); public static final TagSummaryData BE_태그_요약 = new TagSummaryData(BE_태그_아이디, BE_태그명); public static final Long FE_태그_아이디 = 4L; public static final String FE_태그명 = "FE"; public static final String FE_태그_설명 = "프론트엔드"; - public static final TagData FE_태그 = new TagData(FE_태그_아이디, FE_태그명, FE_태그_설명, AREA_응답); + public static final TagData FE_태그 = new TagData(FE_태그_아이디, FE_태그명, FE_태그_설명, CategoryFixtures.AREA_응답); public static final TagSummaryData FE_태그_요약 = new TagSummaryData(FE_태그_아이디, FE_태그명); public static final Long 리액트_태그_아이디 = 5L; public static final String 리액트_태그명 = "React"; public static final String 리액트_태그_설명 = "리액트"; - public static final TagData 리액트_태그 = new TagData(리액트_태그_아이디, 리액트_태그명, 리액트_태그_설명, SUBJECT_응답); + public static final TagData 리액트_태그 = new TagData(리액트_태그_아이디, 리액트_태그명, 리액트_태그_설명, CategoryFixtures.SUBJECT_응답); public static final TagSummaryData 리액트_태그_요약 = new TagSummaryData(리액트_태그_아이디, 리액트_태그명); } diff --git a/backend/src/test/java/com/woowacourse/moamoa/member/webmvc/MemberWebMvcTest.java b/backend/src/test/java/com/woowacourse/moamoa/member/webmvc/MemberWebMvcTest.java index eac8b078c..7debac1c1 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/member/webmvc/MemberWebMvcTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/member/webmvc/MemberWebMvcTest.java @@ -6,6 +6,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import com.woowacourse.moamoa.member.service.MemberService; import org.apache.http.HttpHeaders; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -14,9 +15,14 @@ import com.woowacourse.moamoa.WebMVCTest; import com.woowacourse.moamoa.member.service.exception.MemberNotFoundException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.MockBean; public class MemberWebMvcTest extends WebMVCTest { + @MockBean + MemberService memberService; + @DisplayName("잘못된 토큰 사용시 401 에러 반환") @ParameterizedTest @ValueSource(strings = {"bearer invalid.token", "invalid.token"}) diff --git a/backend/src/test/java/com/woowacourse/moamoa/study/domain/StudyTest.java b/backend/src/test/java/com/woowacourse/moamoa/study/domain/StudyTest.java index 6b2f92ceb..83fb56881 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/study/domain/StudyTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/study/domain/StudyTest.java @@ -12,6 +12,7 @@ import static java.time.LocalDateTime.now; +import com.woowacourse.moamoa.member.domain.Member; import com.woowacourse.moamoa.study.domain.exception.InvalidPeriodException; import com.woowacourse.moamoa.study.service.exception.FailureParticipationException; @@ -327,4 +328,21 @@ public void autoCloseEnrollment() { // then assertThat(sut.getRecruitPlanner().isCloseEnrollment()).isTrue(); } + + @DisplayName("참여자는 방장, 참가자만 가능하다.") + @ParameterizedTest + @CsvSource({"1, 2, 1, true", "1, 2, 2, true", "1, 2, 3, false"}) + void checkIsParticipant(Long ownerId, Long participantId, Long targetMemberId, boolean expected) { + // arrange + final Content content = new Content("title", "excerpt", "thumbnail", "description"); + final Participants participants = Participants.createBy(ownerId); + final RecruitPlanner recruitPlanner = new RecruitPlanner(2, RECRUITMENT_START, LocalDate.now().minusDays(1)); + final StudyPlanner studyPlanner = new StudyPlanner(LocalDate.now(), LocalDate.now().plusDays(5), PREPARE); + final Study sut = new Study(content, participants, recruitPlanner, studyPlanner, AttachedTags.empty(), now().minusDays(2)); + + sut.participate(participantId); + + // act + assertThat(sut.isParticipant(targetMemberId)).isEqualTo(expected); + } } diff --git a/backend/src/test/resources/application.yml b/backend/src/test/resources/application.yml index e8edf3b70..1030aead9 100644 --- a/backend/src/test/resources/application.yml +++ b/backend/src/test/resources/application.yml @@ -7,7 +7,7 @@ spring: ddl-auto: none properties: hibernate: - dialect: org.hibernate.dialect.MySQL8Dialect + dialect: org.hibernate.dialect.H2Dialect show_sql: true format_sql: true datasource: diff --git a/backend/src/test/resources/data.sql b/backend/src/test/resources/data.sql deleted file mode 100644 index beee99bd8..000000000 --- a/backend/src/test/resources/data.sql +++ /dev/null @@ -1,9 +0,0 @@ -INSERT INTO category(id, name) VALUES (1, 'generation'); -INSERT INTO category(id, name) VALUES (2, 'area'); -INSERT INTO category(id, name) VALUES (3, 'subject'); - -INSERT INTO tag(id, name, description, category_id) VALUES (1, 'Java', '자바', 3); -INSERT INTO tag(id, name, description, category_id) VALUES (2, '4기', '우테코4기', 1); -INSERT INTO tag(id, name, description, category_id) VALUES (3, 'BE', '백엔드', 2); -INSERT INTO tag(id, name, description, category_id) VALUES (4, 'FE', '프론트엔드', 2); -INSERT INTO tag(id, name, description, category_id) VALUES (5, 'React', '리액트', 3); diff --git a/backend/src/test/resources/schema.sql b/backend/src/test/resources/schema.sql index 6d6ff5c7f..225b95efd 100644 --- a/backend/src/test/resources/schema.sql +++ b/backend/src/test/resources/schema.sql @@ -1,3 +1,4 @@ +DROP TABLE IF EXISTS article; DROP TABLE IF EXISTS study_tag; DROP TABLE IF EXISTS study_member; DROP TABLE IF EXISTS tag; @@ -81,6 +82,19 @@ CREATE TABLE study_member FOREIGN KEY (member_id) REFERENCES member (id) ); +CREATE TABLE article +( + id BIGINT PRIMARY KEY AUTO_INCREMENT, + title VARCHAR(255) NOT NULL, + content MEDIUMTEXT NOT NULL, + author_id BIGINT, + study_id BIGINT, + created_date DATETIME not null, + last_modified_date DATETIME not null, + FOREIGN KEY (author_id) REFERENCES member (id), + FOREIGN KEY (study_id) REFERENCES study (id) +); + CREATE TABLE token ( id BIGINT PRIMARY KEY AUTO_INCREMENT, From e96d8ff1e4a3c741912244820af3bed76717b450 Mon Sep 17 00:00:00 2001 From: SeungCheol Shin <47477359+sc0116@users.noreply.github.com> Date: Fri, 12 Aug 2022 15:57:34 +0900 Subject: [PATCH 12/51] =?UTF-8?q?[BE]=20issue227:=20=EC=8A=A4=ED=84=B0?= =?UTF-8?q?=EB=94=94=EC=9B=90=EC=9D=84=20=EC=9C=84=ED=95=9C=20=EB=A7=81?= =?UTF-8?q?=ED=81=AC=EA=B3=B5=EC=9C=A0=20CRUD=20(#237)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * test: 링크 공유글 생성 관련 인수 테스트 추가 * test: 링크 공유글 생성 관련 인수 테스트 수정 * feat: 링크 공유 생성 기능 구현 * test: 링크 공유글 수정 관련 인수 테스트 추가 * feat: 존재하지 않는 링크 공유글인 경우 예외 처리 구현 * feat: 내가 작성한 링크 공유글이 아닌 경우 예외 처리 및 기능 구현 * test: 링크 공유글 삭제 관련 인수 테스트 추가 * feat: 존재하지 않는 링크 공유글 삭제 시 예외 처리 구현 * feat: 내가 작성하지 않은 링크 공유글 삭제 시 예외 처리 및 기능 구현 * test: 링크 공유 전체 조회 관련 인수 테스트 추가 * feat: 링크 전체 조회 쿼리 작성 * feat: 링크 전체 조회 기능 구현 * test: 깨지는 테스트 수정 * refactor: 링크 공유 리팩토링 * feat: 링크 공유글 수정, 삭제 시 해당 스터디에 속해있는지 검증 * feat: 링크 공유글 수정, 삭제 시 작성자가 해당 스터디에 참여하고 있는지 검증 * test: 링크 공유글 수정, 삭제 인수 테스트 수정 * refactor: 링크 공유글 설명 최대 글자수 50자에서 25자로 변경 * refactor: 피드백 반영 * refactor: REST Docs 전체 조회 response-fields 추가 * refactor: BaseEntity LocalDate -> LocalDateTime으로 변경 * refactor: 피드백 반영 * refactor: 커뮤니티 관련 REST Docs 설정 추가 --- backend/src/docs/asciidoc/index.adoc | 33 +++ .../auth/config/AuthRequestMatchConfig.java | 32 ++- .../moamoa/common/entity/BaseEntity.java | 6 +- .../NotParticipatedMemberException.java | 2 +- .../controller/ReferenceRoomController.java | 56 ++++ .../SearchingReferenceRoomController.java | 31 +++ .../moamoa/referenceroom/domain/Author.java | 18 ++ .../moamoa/referenceroom/domain/Link.java | 90 +++++++ .../domain/repository/JpaLinkRepository.java | 7 + .../domain/repository/LinkRepository.java | 11 + .../moamoa/referenceroom/query/LinkDao.java | 64 +++++ .../referenceroom/query/data/LinkData.java | 23 ++ .../service/ReferenceRoomService.java | 77 ++++++ .../SearchingReferenceRoomService.java | 41 +++ .../exception/LinkNotFoundException.java | 10 + .../exception/NotCreatingLinkException.java | 10 + .../exception/NotLinkAuthorException.java | 10 + .../NotParticipatedMemberException.java | 10 + .../exception/NotRelatedLinkException.java | 10 + .../service/request/CreatingLinkRequest.java | 27 ++ .../service/request/EditingLinkRequest.java | 27 ++ .../service/response/AuthorResponse.java | 31 +++ .../service/response/LinkResponse.java | 38 +++ .../service/response/LinksResponse.java | 28 ++ .../moamoa/review/domain/AssociatedStudy.java | 2 + .../moamoa/review/query/ReviewDao.java | 2 +- .../moamoa/review/service/ReviewService.java | 2 +- .../moamoa/study/domain/Participants.java | 4 +- .../moamoa/study/domain/Study.java | 12 +- .../acceptance/AcceptanceTest.java | 1 + .../acceptance/steps/StudyRelatedSteps.java | 37 ++- .../community/CommunityAcceptanceTest.java | 10 +- .../ReferenceRoomAcceptanceTest.java | 242 ++++++++++++++++++ .../test/review/ReviewsAcceptanceTest.java | 7 +- .../com/woowacourse/moamoa/WebMVCTest.java | 16 +- .../AuthenticationArgumentResolverTest.java | 19 +- .../AuthenticationInterceptorTest.java | 11 +- .../AuthenticationRequestMatcherTest.java | 6 - .../ReferenceRoomControllerTest.java | 127 +++++++++ .../SearchingReferenceRoomControllerTest.java | 154 +++++++++++ .../moamoa/referenceroom/domain/LinkTest.java | 85 ++++++ .../referenceroom/query/LinkDaoTest.java | 122 +++++++++ .../webmvc/ReferenceRoomWebMvcTest.java | 115 +++++++++ .../SearchingReviewControllerTest.java | 1 - .../moamoa/study/domain/StudyTest.java | 9 +- backend/src/test/resources/schema.sql | 14 + 46 files changed, 1610 insertions(+), 80 deletions(-) create mode 100644 backend/src/main/java/com/woowacourse/moamoa/referenceroom/controller/ReferenceRoomController.java create mode 100644 backend/src/main/java/com/woowacourse/moamoa/referenceroom/controller/SearchingReferenceRoomController.java create mode 100644 backend/src/main/java/com/woowacourse/moamoa/referenceroom/domain/Author.java create mode 100644 backend/src/main/java/com/woowacourse/moamoa/referenceroom/domain/Link.java create mode 100644 backend/src/main/java/com/woowacourse/moamoa/referenceroom/domain/repository/JpaLinkRepository.java create mode 100644 backend/src/main/java/com/woowacourse/moamoa/referenceroom/domain/repository/LinkRepository.java create mode 100644 backend/src/main/java/com/woowacourse/moamoa/referenceroom/query/LinkDao.java create mode 100644 backend/src/main/java/com/woowacourse/moamoa/referenceroom/query/data/LinkData.java create mode 100644 backend/src/main/java/com/woowacourse/moamoa/referenceroom/service/ReferenceRoomService.java create mode 100644 backend/src/main/java/com/woowacourse/moamoa/referenceroom/service/SearchingReferenceRoomService.java create mode 100644 backend/src/main/java/com/woowacourse/moamoa/referenceroom/service/exception/LinkNotFoundException.java create mode 100644 backend/src/main/java/com/woowacourse/moamoa/referenceroom/service/exception/NotCreatingLinkException.java create mode 100644 backend/src/main/java/com/woowacourse/moamoa/referenceroom/service/exception/NotLinkAuthorException.java create mode 100644 backend/src/main/java/com/woowacourse/moamoa/referenceroom/service/exception/NotParticipatedMemberException.java create mode 100644 backend/src/main/java/com/woowacourse/moamoa/referenceroom/service/exception/NotRelatedLinkException.java create mode 100644 backend/src/main/java/com/woowacourse/moamoa/referenceroom/service/request/CreatingLinkRequest.java create mode 100644 backend/src/main/java/com/woowacourse/moamoa/referenceroom/service/request/EditingLinkRequest.java create mode 100644 backend/src/main/java/com/woowacourse/moamoa/referenceroom/service/response/AuthorResponse.java create mode 100644 backend/src/main/java/com/woowacourse/moamoa/referenceroom/service/response/LinkResponse.java create mode 100644 backend/src/main/java/com/woowacourse/moamoa/referenceroom/service/response/LinksResponse.java create mode 100644 backend/src/test/java/com/woowacourse/acceptance/test/referenceroom/ReferenceRoomAcceptanceTest.java create mode 100644 backend/src/test/java/com/woowacourse/moamoa/referenceroom/controller/ReferenceRoomControllerTest.java create mode 100644 backend/src/test/java/com/woowacourse/moamoa/referenceroom/controller/SearchingReferenceRoomControllerTest.java create mode 100644 backend/src/test/java/com/woowacourse/moamoa/referenceroom/domain/LinkTest.java create mode 100644 backend/src/test/java/com/woowacourse/moamoa/referenceroom/query/LinkDaoTest.java create mode 100644 backend/src/test/java/com/woowacourse/moamoa/referenceroom/webmvc/ReferenceRoomWebMvcTest.java diff --git a/backend/src/docs/asciidoc/index.adoc b/backend/src/docs/asciidoc/index.adoc index 05dd77482..8c0633fc0 100644 --- a/backend/src/docs/asciidoc/index.adoc +++ b/backend/src/docs/asciidoc/index.adoc @@ -51,6 +51,39 @@ operation::studies/details[snippets='http-request,http-response'] === 참여한 스터디 목록 조회 operation::studies/myStudy[snippets='http-request,http-response'] +[[Comunity]] +== 커뮤니티 + +=== 커뮤니티 생성 +operation::article/create[snippets='http-request,request-headers,request-fields,http-response'] + +=== 커뮤니티 단건 조회 +operation::article/get[snippets='http-request,request-headers,http-response,response-fields'] + +=== 커뮤니티 전체 조회 +operation::article/list[snippets='http-request,request-headers,http-response,response-fields'] + +=== 커뮤니티 수정 +operation::article/update[snippets='http-request,request-headers,http-response'] + +=== 커뮤니티 삭제 +operation::article/delete[snippets='http-request,request-headers,http-response'] + +[[Reference-Room]] +== 링크 공유 + +=== 링크 공유글 생성 +operation::reference-room/create[snippets='http-request,request-headers,request-fields,http-response'] + +=== 링크 공유글 전체 조회 +operation::reference-room/list[snippets='http-request,request-headers,http-response,response-fields'] + +=== 링크 공유글 수정 +operation::reference-room/update[snippets='http-request,request-headers,http-response'] + +=== 링크 공유글 삭제 +operation::reference-room/delete[snippets='http-request,request-headers,http-response'] + [[Tag]] == 태그 diff --git a/backend/src/main/java/com/woowacourse/moamoa/auth/config/AuthRequestMatchConfig.java b/backend/src/main/java/com/woowacourse/moamoa/auth/config/AuthRequestMatchConfig.java index 14a07c730..5d037fc64 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/auth/config/AuthRequestMatchConfig.java +++ b/backend/src/main/java/com/woowacourse/moamoa/auth/config/AuthRequestMatchConfig.java @@ -14,14 +14,30 @@ public class AuthRequestMatchConfig { @Bean public AuthenticationRequestMatcher authenticationRequestMatcher() { return new AuthenticationRequestMatcherBuilder() - .addUpAuthenticationPath(HttpMethod.POST, "/api/studies", "/api/studies/\\d+/reviews", - "/api/studies/\\d+/reviews/\\d+", "/api/studies/\\w+/community/articles", - "/api/studies") - .addUpAuthenticationPath(HttpMethod.GET, "/api/my/studies", "/api/members/me", "/api/members/me/role", - "/api/studies/\\w+/community/articles/\\w+", "/api/studies/\\w+/community/articles") - .addUpAuthenticationPath(HttpMethod.PUT, "/api/studies/\\d+/reviews/\\d+") - .addUpAuthenticationPath(HttpMethod.DELETE, "/api/studies/\\d+/reviews/\\d+", - "/api/studies/\\w+/community/articles/\\w+") + .addUpAuthenticationPath(HttpMethod.POST, + "/api/studies", + "/api/studies/\\d+/reviews", + "/api/studies/\\d+/reviews/\\d+", + "/api/studies/\\w+/community/articles", + "/api/studies" + ) + .addUpAuthenticationPath(HttpMethod.GET, + "/api/my/studies", + "/api/members/me", + "/api/members/me/role", + "/api/studies/\\w+/community/articles/\\w+", + "/api/studies/\\w+/community/articles", + "/api/studies/\\d+/reference-room/links" + ) + .addUpAuthenticationPath(HttpMethod.PUT, + "/api/studies/\\d+/reviews/\\d+", + "/api/studies/\\d+/reference-room/links/\\d+" + ) + .addUpAuthenticationPath(HttpMethod.DELETE, + "/api/studies/\\d+/reviews/\\d+", + "/api/studies/\\w+/community/articles/\\w+", + "/api/studies/\\d+/reference-room/links/\\d+" + ) .build(); } } diff --git a/backend/src/main/java/com/woowacourse/moamoa/common/entity/BaseEntity.java b/backend/src/main/java/com/woowacourse/moamoa/common/entity/BaseEntity.java index bef1425ef..b3a3da8c3 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/common/entity/BaseEntity.java +++ b/backend/src/main/java/com/woowacourse/moamoa/common/entity/BaseEntity.java @@ -1,6 +1,6 @@ package com.woowacourse.moamoa.common.entity; -import java.util.Date; +import java.time.LocalDateTime; import javax.persistence.Column; import javax.persistence.EntityListeners; import javax.persistence.MappedSuperclass; @@ -16,9 +16,9 @@ public class BaseEntity { @CreatedDate @Column(updatable = false, nullable = false) - private Date createdDate; + private LocalDateTime createdDate; @LastModifiedDate @Column(nullable = false) - private Date lastModifiedDate; + private LocalDateTime lastModifiedDate; } diff --git a/backend/src/main/java/com/woowacourse/moamoa/member/service/exception/NotParticipatedMemberException.java b/backend/src/main/java/com/woowacourse/moamoa/member/service/exception/NotParticipatedMemberException.java index 21f0007f8..c630ed034 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/member/service/exception/NotParticipatedMemberException.java +++ b/backend/src/main/java/com/woowacourse/moamoa/member/service/exception/NotParticipatedMemberException.java @@ -5,6 +5,6 @@ public class NotParticipatedMemberException extends UnauthorizedException { public NotParticipatedMemberException() { - super("스터디에 참여한 회원만 후기를 작성할 수 있습니다."); + super("스터디에 참여한 회원만 작성할 수 있습니다."); } } diff --git a/backend/src/main/java/com/woowacourse/moamoa/referenceroom/controller/ReferenceRoomController.java b/backend/src/main/java/com/woowacourse/moamoa/referenceroom/controller/ReferenceRoomController.java new file mode 100644 index 000000000..48887ba41 --- /dev/null +++ b/backend/src/main/java/com/woowacourse/moamoa/referenceroom/controller/ReferenceRoomController.java @@ -0,0 +1,56 @@ +package com.woowacourse.moamoa.referenceroom.controller; + +import com.woowacourse.moamoa.auth.config.AuthenticationPrincipal; +import com.woowacourse.moamoa.referenceroom.service.ReferenceRoomService; +import com.woowacourse.moamoa.referenceroom.service.request.CreatingLinkRequest; +import com.woowacourse.moamoa.referenceroom.service.request.EditingLinkRequest; +import java.net.URI; +import javax.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/studies/{study-id}/reference-room/links") +@RequiredArgsConstructor +public class ReferenceRoomController { + + private final ReferenceRoomService referenceRoomService; + + @PostMapping + public ResponseEntity<Void> createLink( + @AuthenticationPrincipal final Long githubId, + @PathVariable("study-id") final Long studyId, + @Valid @RequestBody final CreatingLinkRequest creatingLinkRequest + ) { + final Long id = referenceRoomService.createLink(githubId, studyId, creatingLinkRequest).getId(); + return ResponseEntity.created(URI.create("/api/studies/" + studyId + "/reference-room/links/" + id)).build(); + } + + @PutMapping("/{link-id}") + public ResponseEntity<Void> updateLink( + @AuthenticationPrincipal final Long githubId, + @PathVariable("study-id") final Long studyId, + @PathVariable("link-id") final Long linkId, + @Valid @RequestBody final EditingLinkRequest editingLinkRequest + ) { + referenceRoomService.updateLink(githubId, studyId, linkId, editingLinkRequest); + return ResponseEntity.noContent().build(); + } + + @DeleteMapping("/{link-id}") + public ResponseEntity<Void> deleteLink( + @AuthenticationPrincipal final Long githubId, + @PathVariable("study-id") final Long studyId, + @PathVariable("link-id") final Long linkId + ) { + referenceRoomService.deleteLink(githubId, studyId, linkId); + return ResponseEntity.noContent().build(); + } +} diff --git a/backend/src/main/java/com/woowacourse/moamoa/referenceroom/controller/SearchingReferenceRoomController.java b/backend/src/main/java/com/woowacourse/moamoa/referenceroom/controller/SearchingReferenceRoomController.java new file mode 100644 index 000000000..d7554e63b --- /dev/null +++ b/backend/src/main/java/com/woowacourse/moamoa/referenceroom/controller/SearchingReferenceRoomController.java @@ -0,0 +1,31 @@ +package com.woowacourse.moamoa.referenceroom.controller; + +import com.woowacourse.moamoa.auth.config.AuthenticationPrincipal; +import com.woowacourse.moamoa.referenceroom.service.SearchingReferenceRoomService; +import com.woowacourse.moamoa.referenceroom.service.response.LinksResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.data.web.PageableDefault; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/studies/{study-id}/reference-room/links") +@RequiredArgsConstructor +public class SearchingReferenceRoomController { + + private final SearchingReferenceRoomService searchingReferenceRoomService; + + @GetMapping + public ResponseEntity<LinksResponse> getLinks( + @AuthenticationPrincipal final Long githubId, + @PathVariable("study-id") final Long studyId, + @PageableDefault(size = 9) final Pageable pageable + ) { + final LinksResponse linksResponse = searchingReferenceRoomService.getLinks(githubId, studyId, pageable); + return ResponseEntity.ok().body(linksResponse); + } +} diff --git a/backend/src/main/java/com/woowacourse/moamoa/referenceroom/domain/Author.java b/backend/src/main/java/com/woowacourse/moamoa/referenceroom/domain/Author.java new file mode 100644 index 000000000..017ba56ec --- /dev/null +++ b/backend/src/main/java/com/woowacourse/moamoa/referenceroom/domain/Author.java @@ -0,0 +1,18 @@ +package com.woowacourse.moamoa.referenceroom.domain; + +import javax.persistence.Column; +import javax.persistence.Embeddable; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +@Embeddable +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@EqualsAndHashCode +public class Author { + + @Column(name = "member_id", nullable = false) + private Long memberId; +} diff --git a/backend/src/main/java/com/woowacourse/moamoa/referenceroom/domain/Link.java b/backend/src/main/java/com/woowacourse/moamoa/referenceroom/domain/Link.java new file mode 100644 index 000000000..216ee411f --- /dev/null +++ b/backend/src/main/java/com/woowacourse/moamoa/referenceroom/domain/Link.java @@ -0,0 +1,90 @@ +package com.woowacourse.moamoa.referenceroom.domain; + +import com.woowacourse.moamoa.common.entity.BaseEntity; +import com.woowacourse.moamoa.referenceroom.service.exception.NotLinkAuthorException; +import com.woowacourse.moamoa.referenceroom.service.exception.NotRelatedLinkException; +import com.woowacourse.moamoa.review.domain.AssociatedStudy; +import javax.persistence.Column; +import javax.persistence.Embedded; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.Where; + +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Where(clause = "deleted = false") +public class Link extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Embedded + private AssociatedStudy associatedStudy; + + @Embedded + private Author author; + + @Column(nullable = false) + private String linkUrl; + + private String description; + + @Column(nullable = false) + private boolean deleted; + + public Link( + final AssociatedStudy associatedStudy, final Author author, final String linkUrl, final String description + ) { + this(null, associatedStudy, author, linkUrl, description, false); + } + + public void update(final Link updatedLink) { + validateBelongToStudy(updatedLink.associatedStudy); + validateAuthor(updatedLink.author); + + linkUrl = updatedLink.linkUrl; + description = updatedLink.description; + } + + public void delete(final AssociatedStudy associatedStudy, final Author author) { + validateBelongToStudy(associatedStudy); + validateAuthor(author); + + deleted = true; + } + + private void validateBelongToStudy(final AssociatedStudy associatedStudy) { + if (!this.associatedStudy.equals(associatedStudy)) { + throw new NotRelatedLinkException(); + } + } + + private void validateAuthor(final Author author) { + if (!this.author.equals(author)) { + throw new NotLinkAuthorException(); + } + } + + public Long getId() { + return id; + } + + public String getLinkUrl() { + return linkUrl; + } + + public String getDescription() { + return description; + } + + public boolean isDeleted() { + return deleted; + } +} diff --git a/backend/src/main/java/com/woowacourse/moamoa/referenceroom/domain/repository/JpaLinkRepository.java b/backend/src/main/java/com/woowacourse/moamoa/referenceroom/domain/repository/JpaLinkRepository.java new file mode 100644 index 000000000..256156e0d --- /dev/null +++ b/backend/src/main/java/com/woowacourse/moamoa/referenceroom/domain/repository/JpaLinkRepository.java @@ -0,0 +1,7 @@ +package com.woowacourse.moamoa.referenceroom.domain.repository; + +import com.woowacourse.moamoa.referenceroom.domain.Link; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface JpaLinkRepository extends JpaRepository<Link, Long>, LinkRepository { +} diff --git a/backend/src/main/java/com/woowacourse/moamoa/referenceroom/domain/repository/LinkRepository.java b/backend/src/main/java/com/woowacourse/moamoa/referenceroom/domain/repository/LinkRepository.java new file mode 100644 index 000000000..bb21d63c7 --- /dev/null +++ b/backend/src/main/java/com/woowacourse/moamoa/referenceroom/domain/repository/LinkRepository.java @@ -0,0 +1,11 @@ +package com.woowacourse.moamoa.referenceroom.domain.repository; + +import com.woowacourse.moamoa.referenceroom.domain.Link; +import java.util.Optional; + +public interface LinkRepository { + + Link save(Link link); + + Optional<Link> findById(Long id); +} diff --git a/backend/src/main/java/com/woowacourse/moamoa/referenceroom/query/LinkDao.java b/backend/src/main/java/com/woowacourse/moamoa/referenceroom/query/LinkDao.java new file mode 100644 index 000000000..f4a1c09a0 --- /dev/null +++ b/backend/src/main/java/com/woowacourse/moamoa/referenceroom/query/LinkDao.java @@ -0,0 +1,64 @@ +package com.woowacourse.moamoa.referenceroom.query; + +import com.woowacourse.moamoa.member.query.data.MemberData; +import com.woowacourse.moamoa.referenceroom.query.data.LinkData; +import java.time.LocalDate; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.data.domain.SliceImpl; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.stereotype.Repository; + +@Repository +@RequiredArgsConstructor +public class LinkDao { + + private final NamedParameterJdbcTemplate namedParameterJdbcTemplate; + + public Slice<LinkData> findAllByStudyId(final Long studyId, final Pageable pageable) { + final String sql = "SELECT link.id, link.link_url, link.description, link.created_date, link.last_modified_date, " + + "member.github_id, member.username, member.image_url, member.profile_url " + + "FROM link " + + "JOIN member ON link.member_id = member.id " + + "WHERE link.deleted = false " + + "AND link.study_id = :studyId " + + "ORDER BY link.created_date DESC, link.id DESC"; + final MapSqlParameterSource params = new MapSqlParameterSource("studyId", studyId); + + final List<LinkData> linkData = namedParameterJdbcTemplate.query(sql, params, rowMapper()); + return new SliceImpl<>(getCurrentPageLinks(linkData, pageable), pageable, hasNext(linkData, pageable)); + } + + private List<LinkData> getCurrentPageLinks(final List<LinkData> linkData, final Pageable pageable) { + if (hasNext(linkData, pageable)) { + return linkData.subList(0, linkData.size() - 1); + } + return linkData; + } + + private boolean hasNext(final List<LinkData> linkData, final Pageable pageable) { + return linkData.size() > pageable.getPageSize(); + } + + private RowMapper<LinkData> rowMapper() { + return (rs, rn) -> { + final Long id = rs.getLong("id"); + final String linkUrl = rs.getString("link_url"); + final String description = rs.getString("description"); + final LocalDate createdDate = rs.getObject("created_date", LocalDate.class); + final LocalDate lastModifiedDate = rs.getObject("last_modified_date", LocalDate.class); + + final Long githubId = rs.getLong("github_id"); + final String username = rs.getString("username"); + final String imageUrl = rs.getString("image_url"); + final String profileUrl = rs.getString("profile_url"); + final MemberData memberData = new MemberData(githubId, username, imageUrl, profileUrl); + + return new LinkData(id, memberData, linkUrl, description, createdDate, lastModifiedDate); + }; + } +} diff --git a/backend/src/main/java/com/woowacourse/moamoa/referenceroom/query/data/LinkData.java b/backend/src/main/java/com/woowacourse/moamoa/referenceroom/query/data/LinkData.java new file mode 100644 index 000000000..e0d9dfbd6 --- /dev/null +++ b/backend/src/main/java/com/woowacourse/moamoa/referenceroom/query/data/LinkData.java @@ -0,0 +1,23 @@ +package com.woowacourse.moamoa.referenceroom.query.data; + +import com.woowacourse.moamoa.member.query.data.MemberData; +import java.time.LocalDate; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Getter +@EqualsAndHashCode +public class LinkData { + + private Long id; + private MemberData memberData; + private String linkUrl; + private String description; + private LocalDate createdDate; + private LocalDate lastModifiedDate; +} diff --git a/backend/src/main/java/com/woowacourse/moamoa/referenceroom/service/ReferenceRoomService.java b/backend/src/main/java/com/woowacourse/moamoa/referenceroom/service/ReferenceRoomService.java new file mode 100644 index 000000000..f890aa702 --- /dev/null +++ b/backend/src/main/java/com/woowacourse/moamoa/referenceroom/service/ReferenceRoomService.java @@ -0,0 +1,77 @@ +package com.woowacourse.moamoa.referenceroom.service; + +import com.woowacourse.moamoa.member.domain.Member; +import com.woowacourse.moamoa.member.domain.repository.MemberRepository; +import com.woowacourse.moamoa.member.service.exception.MemberNotFoundException; +import com.woowacourse.moamoa.referenceroom.domain.Author; +import com.woowacourse.moamoa.referenceroom.domain.Link; +import com.woowacourse.moamoa.referenceroom.domain.repository.LinkRepository; +import com.woowacourse.moamoa.referenceroom.service.exception.LinkNotFoundException; +import com.woowacourse.moamoa.referenceroom.service.exception.NotCreatingLinkException; +import com.woowacourse.moamoa.referenceroom.service.exception.NotParticipatedMemberException; +import com.woowacourse.moamoa.referenceroom.service.request.CreatingLinkRequest; +import com.woowacourse.moamoa.referenceroom.service.request.EditingLinkRequest; +import com.woowacourse.moamoa.review.domain.AssociatedStudy; +import com.woowacourse.moamoa.study.domain.Study; +import com.woowacourse.moamoa.study.domain.repository.StudyRepository; +import com.woowacourse.moamoa.study.service.exception.StudyNotFoundException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional +@RequiredArgsConstructor +public class ReferenceRoomService { + + private final MemberRepository memberRepository; + private final StudyRepository studyRepository; + private final LinkRepository linkRepository; + + public Link createLink(final Long githubId, final Long studyId, final CreatingLinkRequest creatingLinkRequest) { + final Member member = memberRepository.findByGithubId(githubId) + .orElseThrow(MemberNotFoundException::new); + final Study study = studyRepository.findById(studyId) + .orElseThrow(StudyNotFoundException::new); + + if (!study.isReviewWritable(member.getId())) { + throw new NotCreatingLinkException(); + } + + final Link link = creatingLinkRequest.toLink(studyId, member.getId()); + return linkRepository.save(link); + } + + public void updateLink( + final Long githubId, final Long studyId, final Long linkId, final EditingLinkRequest editingLinkRequest + ) { + final Member member = memberRepository.findByGithubId(githubId) + .orElseThrow(MemberNotFoundException::new); + final Study study = studyRepository.findById(studyId) + .orElseThrow(StudyNotFoundException::new); + final Link link = linkRepository.findById(linkId) + .orElseThrow(LinkNotFoundException::new); + + if (!study.isParticipant(member.getId())) { + throw new NotParticipatedMemberException(); + } + + final Link updatedLink = editingLinkRequest.toLink(studyId, member.getId()); + link.update(updatedLink); + } + + public void deleteLink(final Long githubId, final Long studyId, final Long linkId) { + final Member member = memberRepository.findByGithubId(githubId) + .orElseThrow(MemberNotFoundException::new); + final Study study = studyRepository.findById(studyId) + .orElseThrow(StudyNotFoundException::new); + final Link link = linkRepository.findById(linkId) + .orElseThrow(LinkNotFoundException::new); + + if (!study.isParticipant(member.getId())) { + throw new NotParticipatedMemberException(); + } + + link.delete(new AssociatedStudy(studyId), new Author(member.getId())); + } +} diff --git a/backend/src/main/java/com/woowacourse/moamoa/referenceroom/service/SearchingReferenceRoomService.java b/backend/src/main/java/com/woowacourse/moamoa/referenceroom/service/SearchingReferenceRoomService.java new file mode 100644 index 000000000..55ad52d34 --- /dev/null +++ b/backend/src/main/java/com/woowacourse/moamoa/referenceroom/service/SearchingReferenceRoomService.java @@ -0,0 +1,41 @@ +package com.woowacourse.moamoa.referenceroom.service; + +import com.woowacourse.moamoa.member.domain.Member; +import com.woowacourse.moamoa.member.domain.repository.MemberRepository; +import com.woowacourse.moamoa.member.service.exception.MemberNotFoundException; +import com.woowacourse.moamoa.referenceroom.query.LinkDao; +import com.woowacourse.moamoa.referenceroom.query.data.LinkData; +import com.woowacourse.moamoa.referenceroom.service.exception.NotParticipatedMemberException; +import com.woowacourse.moamoa.referenceroom.service.response.LinksResponse; +import com.woowacourse.moamoa.study.domain.Study; +import com.woowacourse.moamoa.study.domain.repository.StudyRepository; +import com.woowacourse.moamoa.study.service.exception.StudyNotFoundException; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional(readOnly = true) +@RequiredArgsConstructor +public class SearchingReferenceRoomService { + + private final LinkDao linkDao; + private final MemberRepository memberRepository; + private final StudyRepository studyRepository; + + public LinksResponse getLinks(final Long githubId, final Long studyId, final Pageable pageable) { + final Member member = memberRepository.findByGithubId(githubId) + .orElseThrow(MemberNotFoundException::new); + final Study study = studyRepository.findById(studyId) + .orElseThrow(StudyNotFoundException::new); + + if (!study.isParticipant(member.getId())) { + throw new NotParticipatedMemberException(); + } + + final Slice<LinkData> linkData = linkDao.findAllByStudyId(studyId, pageable); + return new LinksResponse(linkData.getContent(), linkData.hasNext()); + } +} diff --git a/backend/src/main/java/com/woowacourse/moamoa/referenceroom/service/exception/LinkNotFoundException.java b/backend/src/main/java/com/woowacourse/moamoa/referenceroom/service/exception/LinkNotFoundException.java new file mode 100644 index 000000000..74ccc2868 --- /dev/null +++ b/backend/src/main/java/com/woowacourse/moamoa/referenceroom/service/exception/LinkNotFoundException.java @@ -0,0 +1,10 @@ +package com.woowacourse.moamoa.referenceroom.service.exception; + +import com.woowacourse.moamoa.common.exception.NotFoundException; + +public class LinkNotFoundException extends NotFoundException { + + public LinkNotFoundException() { + super("링크 공유글을 찾을 수 없습니다."); + } +} diff --git a/backend/src/main/java/com/woowacourse/moamoa/referenceroom/service/exception/NotCreatingLinkException.java b/backend/src/main/java/com/woowacourse/moamoa/referenceroom/service/exception/NotCreatingLinkException.java new file mode 100644 index 000000000..4865c198a --- /dev/null +++ b/backend/src/main/java/com/woowacourse/moamoa/referenceroom/service/exception/NotCreatingLinkException.java @@ -0,0 +1,10 @@ +package com.woowacourse.moamoa.referenceroom.service.exception; + +import com.woowacourse.moamoa.common.exception.BadRequestException; + +public class NotCreatingLinkException extends BadRequestException { + + public NotCreatingLinkException() { + super("링크 공유를 할 수 없습니다."); + } +} diff --git a/backend/src/main/java/com/woowacourse/moamoa/referenceroom/service/exception/NotLinkAuthorException.java b/backend/src/main/java/com/woowacourse/moamoa/referenceroom/service/exception/NotLinkAuthorException.java new file mode 100644 index 000000000..1110c5ebd --- /dev/null +++ b/backend/src/main/java/com/woowacourse/moamoa/referenceroom/service/exception/NotLinkAuthorException.java @@ -0,0 +1,10 @@ +package com.woowacourse.moamoa.referenceroom.service.exception; + +import com.woowacourse.moamoa.common.exception.BadRequestException; + +public class NotLinkAuthorException extends BadRequestException { + + public NotLinkAuthorException() { + super("내가 작성한 링크 공유글이 아닙니다."); + } +} diff --git a/backend/src/main/java/com/woowacourse/moamoa/referenceroom/service/exception/NotParticipatedMemberException.java b/backend/src/main/java/com/woowacourse/moamoa/referenceroom/service/exception/NotParticipatedMemberException.java new file mode 100644 index 000000000..6bc75ccb3 --- /dev/null +++ b/backend/src/main/java/com/woowacourse/moamoa/referenceroom/service/exception/NotParticipatedMemberException.java @@ -0,0 +1,10 @@ +package com.woowacourse.moamoa.referenceroom.service.exception; + +import com.woowacourse.moamoa.common.exception.BadRequestException; + +public class NotParticipatedMemberException extends BadRequestException { + + public NotParticipatedMemberException() { + super("스터디에 참여하지 않은 회원입니다."); + } +} diff --git a/backend/src/main/java/com/woowacourse/moamoa/referenceroom/service/exception/NotRelatedLinkException.java b/backend/src/main/java/com/woowacourse/moamoa/referenceroom/service/exception/NotRelatedLinkException.java new file mode 100644 index 000000000..af18716ad --- /dev/null +++ b/backend/src/main/java/com/woowacourse/moamoa/referenceroom/service/exception/NotRelatedLinkException.java @@ -0,0 +1,10 @@ +package com.woowacourse.moamoa.referenceroom.service.exception; + +import com.woowacourse.moamoa.common.exception.BadRequestException; + +public class NotRelatedLinkException extends BadRequestException { + + public NotRelatedLinkException() { + super("해당 스터디에 작성된 링크 공유글이 아닙니다."); + } +} diff --git a/backend/src/main/java/com/woowacourse/moamoa/referenceroom/service/request/CreatingLinkRequest.java b/backend/src/main/java/com/woowacourse/moamoa/referenceroom/service/request/CreatingLinkRequest.java new file mode 100644 index 000000000..c20d3736c --- /dev/null +++ b/backend/src/main/java/com/woowacourse/moamoa/referenceroom/service/request/CreatingLinkRequest.java @@ -0,0 +1,27 @@ +package com.woowacourse.moamoa.referenceroom.service.request; + +import com.woowacourse.moamoa.referenceroom.domain.Author; +import com.woowacourse.moamoa.referenceroom.domain.Link; +import com.woowacourse.moamoa.review.domain.AssociatedStudy; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Size; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@NoArgsConstructor +@AllArgsConstructor +@Getter +public class CreatingLinkRequest { + + @NotBlank(message = "공유할 링크 URL을 입력해 주세요.") + @Size(max = 500, message = "링크 URL은 500자를 초과할 수 없습니다.") + private String linkUrl; + + @Size(max = 25, message = "설명은 25자를 초과할 수 없습니다.") + private String description; + + public Link toLink(final Long studyId, final Long memberId) { + return new Link(new AssociatedStudy(studyId), new Author(memberId), linkUrl, description); + } +} diff --git a/backend/src/main/java/com/woowacourse/moamoa/referenceroom/service/request/EditingLinkRequest.java b/backend/src/main/java/com/woowacourse/moamoa/referenceroom/service/request/EditingLinkRequest.java new file mode 100644 index 000000000..9e2a3550d --- /dev/null +++ b/backend/src/main/java/com/woowacourse/moamoa/referenceroom/service/request/EditingLinkRequest.java @@ -0,0 +1,27 @@ +package com.woowacourse.moamoa.referenceroom.service.request; + +import com.woowacourse.moamoa.referenceroom.domain.Author; +import com.woowacourse.moamoa.referenceroom.domain.Link; +import com.woowacourse.moamoa.review.domain.AssociatedStudy; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Size; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@NoArgsConstructor +@AllArgsConstructor +@Getter +public class EditingLinkRequest { + + @NotBlank(message = "공유할 링크 URL을 입력해 주세요.") + @Size(max = 500, message = "링크 URL은 500자를 초과할 수 없습니다.") + private String linkUrl; + + @Size(max = 25, message = "설명은 25자를 초과할 수 없습니다.") + private String description; + + public Link toLink(final Long studyId, final Long memberId) { + return new Link(new AssociatedStudy(studyId), new Author(memberId), linkUrl, description); + } +} diff --git a/backend/src/main/java/com/woowacourse/moamoa/referenceroom/service/response/AuthorResponse.java b/backend/src/main/java/com/woowacourse/moamoa/referenceroom/service/response/AuthorResponse.java new file mode 100644 index 000000000..7083d0344 --- /dev/null +++ b/backend/src/main/java/com/woowacourse/moamoa/referenceroom/service/response/AuthorResponse.java @@ -0,0 +1,31 @@ +package com.woowacourse.moamoa.referenceroom.service.response; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.woowacourse.moamoa.member.query.data.MemberData; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Getter +@EqualsAndHashCode +@ToString +public class AuthorResponse { + + @JsonProperty("id") + private Long githubId; + + private String username; + + private String imageUrl; + + private String profileUrl; + + public AuthorResponse(final MemberData memberData) { + this(memberData.getGithubId(), memberData.getUsername(), memberData.getImageUrl(), memberData.getProfileUrl()); + } +} diff --git a/backend/src/main/java/com/woowacourse/moamoa/referenceroom/service/response/LinkResponse.java b/backend/src/main/java/com/woowacourse/moamoa/referenceroom/service/response/LinkResponse.java new file mode 100644 index 000000000..9d618b060 --- /dev/null +++ b/backend/src/main/java/com/woowacourse/moamoa/referenceroom/service/response/LinkResponse.java @@ -0,0 +1,38 @@ +package com.woowacourse.moamoa.referenceroom.service.response; + +import com.woowacourse.moamoa.referenceroom.query.data.LinkData; +import java.time.LocalDate; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Getter +@EqualsAndHashCode +@ToString +public class LinkResponse { + + private Long id; + + private AuthorResponse author; + + private String linkUrl; + + private String description; + + private LocalDate createdDate; + + private LocalDate lastModifiedDate; + + public LinkResponse(final LinkData linkData) { + this( + linkData.getId(), new AuthorResponse(linkData.getMemberData()), + linkData.getLinkUrl(), linkData.getDescription(), + linkData.getCreatedDate(), linkData.getLastModifiedDate() + ); + } +} diff --git a/backend/src/main/java/com/woowacourse/moamoa/referenceroom/service/response/LinksResponse.java b/backend/src/main/java/com/woowacourse/moamoa/referenceroom/service/response/LinksResponse.java new file mode 100644 index 000000000..641d272dc --- /dev/null +++ b/backend/src/main/java/com/woowacourse/moamoa/referenceroom/service/response/LinksResponse.java @@ -0,0 +1,28 @@ +package com.woowacourse.moamoa.referenceroom.service.response; + +import com.woowacourse.moamoa.referenceroom.query.data.LinkData; +import java.util.List; +import java.util.stream.Collectors; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +public class LinksResponse { + + private List<LinkResponse> links; + + private boolean hasNext; + + public LinksResponse(final List<LinkData> linkData, final boolean hasNext) { + this.links = getLinkResponses(linkData); + this.hasNext = hasNext; + } + + private List<LinkResponse> getLinkResponses(final List<LinkData> linkData) { + return linkData.stream() + .map(LinkResponse::new) + .collect(Collectors.toList()); + } +} diff --git a/backend/src/main/java/com/woowacourse/moamoa/review/domain/AssociatedStudy.java b/backend/src/main/java/com/woowacourse/moamoa/review/domain/AssociatedStudy.java index 2819aaa57..6d5a37844 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/review/domain/AssociatedStudy.java +++ b/backend/src/main/java/com/woowacourse/moamoa/review/domain/AssociatedStudy.java @@ -5,6 +5,7 @@ import javax.persistence.Column; import javax.persistence.Embeddable; import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; @@ -12,6 +13,7 @@ @NoArgsConstructor(access = PROTECTED) @AllArgsConstructor @Getter +@EqualsAndHashCode public class AssociatedStudy { @Column(name = "study_id", nullable = false) diff --git a/backend/src/main/java/com/woowacourse/moamoa/review/query/ReviewDao.java b/backend/src/main/java/com/woowacourse/moamoa/review/query/ReviewDao.java index fed75f097..59490d118 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/review/query/ReviewDao.java +++ b/backend/src/main/java/com/woowacourse/moamoa/review/query/ReviewDao.java @@ -22,7 +22,7 @@ public List<ReviewData> findAllByStudyId(final Long studyId) { + "FROM review JOIN member ON review.member_id = member.id " + "WHERE review.deleted = false " + "AND review.study_id = :studyId " - + "ORDER BY review.created_date DESC "; + + "ORDER BY review.created_date DESC, review.id DESC"; return namedParameterJdbcTemplate.query(sql, Map.of("studyId", studyId), rowMapper()); } diff --git a/backend/src/main/java/com/woowacourse/moamoa/review/service/ReviewService.java b/backend/src/main/java/com/woowacourse/moamoa/review/service/ReviewService.java index f99a09294..09739da63 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/review/service/ReviewService.java +++ b/backend/src/main/java/com/woowacourse/moamoa/review/service/ReviewService.java @@ -32,7 +32,7 @@ public Long writeReview(final Long githubId, final Long studyId, final WriteRevi final Member member = memberRepository.findByGithubId(githubId) .orElseThrow(MemberNotFoundException::new); - if (!study.isWritableReviews(member.getId())) { + if (!study.isReviewWritable(member.getId())) { throw new WritingReviewBadRequestException(); } diff --git a/backend/src/main/java/com/woowacourse/moamoa/study/domain/Participants.java b/backend/src/main/java/com/woowacourse/moamoa/study/domain/Participants.java index b364f303e..4130fc450 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/study/domain/Participants.java +++ b/backend/src/main/java/com/woowacourse/moamoa/study/domain/Participants.java @@ -49,7 +49,7 @@ void participate(Long memberId) { size = size + 1; } - boolean isParticipation(Long memberId) { + public boolean isParticipation(final Long memberId) { return participants.contains(new Participant(memberId)) || isOwner(memberId); } @@ -81,7 +81,7 @@ public boolean equals(final Object o) { } @Override - public int hashCode() { + public int hashCode() { return Objects.hash(size, getParticipants()); } diff --git a/backend/src/main/java/com/woowacourse/moamoa/study/domain/Study.java b/backend/src/main/java/com/woowacourse/moamoa/study/domain/Study.java index 5d01c1ffc..8d42f4f8e 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/study/domain/Study.java +++ b/backend/src/main/java/com/woowacourse/moamoa/study/domain/Study.java @@ -49,8 +49,8 @@ public Study(final Content content, final Participants participants, final Recru } public Study(final Long id, final Content content, final Participants participants, - final RecruitPlanner recruitPlanner, final StudyPlanner studyPlanner, final AttachedTags attachedTags, - final LocalDateTime createdAt + final RecruitPlanner recruitPlanner, final StudyPlanner studyPlanner, final AttachedTags attachedTags, + final LocalDateTime createdAt ) { if (isRecruitingAfterEndStudy(recruitPlanner, studyPlanner) || isRecruitedOrStartStudyBeforeCreatedAt(recruitPlanner, studyPlanner, createdAt)) { @@ -82,12 +82,12 @@ private boolean isRecruitedOrStartStudyBeforeCreatedAt(final RecruitPlanner recr recruitPlanner.isRecruitedBeforeThan(createdAt.toLocalDate()); } - public boolean isParticipant(final Long memberId) { - return participants.isParticipation(memberId); + public boolean isReviewWritable(final Long memberId) { + return participants.isParticipation(memberId) && !studyPlanner.isPreparing(); } - public boolean isWritableReviews(final Long memberId) { - return participants.isParticipation(memberId) && !studyPlanner.isPreparing(); + public boolean isParticipant(final Long memberId) { + return participants.isParticipation(memberId); } public void participate(final Long memberId) { diff --git a/backend/src/test/java/com/woowacourse/acceptance/AcceptanceTest.java b/backend/src/test/java/com/woowacourse/acceptance/AcceptanceTest.java index a80fc3d64..5bce1bd77 100644 --- a/backend/src/test/java/com/woowacourse/acceptance/AcceptanceTest.java +++ b/backend/src/test/java/com/woowacourse/acceptance/AcceptanceTest.java @@ -102,6 +102,7 @@ void tearDown() { jdbcTemplate.update("TRUNCATE TABLE study_member"); jdbcTemplate.update("TRUNCATE TABLE review"); jdbcTemplate.update("TRUNCATE TABLE study"); + jdbcTemplate.update("TRUNCATE TABLE link"); jdbcTemplate.update("SET REFERENTIAL_INTEGRITY TRUE"); jdbcTemplate.update("ALTER TABLE member AUTO_INCREMENT = 1"); jdbcTemplate.update("ALTER TABLE study AUTO_INCREMENT = 1"); diff --git a/backend/src/test/java/com/woowacourse/acceptance/steps/StudyRelatedSteps.java b/backend/src/test/java/com/woowacourse/acceptance/steps/StudyRelatedSteps.java index 9ffbcd5fe..d8aced7dc 100644 --- a/backend/src/test/java/com/woowacourse/acceptance/steps/StudyRelatedSteps.java +++ b/backend/src/test/java/com/woowacourse/acceptance/steps/StudyRelatedSteps.java @@ -3,24 +3,15 @@ import static org.springframework.http.HttpHeaders.AUTHORIZATION; import static org.springframework.http.HttpHeaders.CONTENT_TYPE; import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; -import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; -import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; -import static org.springframework.restdocs.headers.HeaderDocumentation.responseHeaders; -import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; -import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; -import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; -import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; -import static org.springframework.restdocs.restassured3.RestAssuredRestDocumentation.document; -import com.fasterxml.jackson.core.JsonProcessingException; import com.woowacourse.moamoa.community.service.request.ArticleRequest; +import com.woowacourse.moamoa.referenceroom.service.request.CreatingLinkRequest; import com.woowacourse.moamoa.review.service.request.WriteReviewRequest; import io.restassured.RestAssured; import org.junit.jupiter.api.Assertions; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; -import org.springframework.restdocs.payload.JsonFieldType; public class StudyRelatedSteps extends Steps { @@ -42,7 +33,7 @@ public class StudyRelatedSteps extends Steps { .statusCode(HttpStatus.OK.value()); } - public long 리뷰를_작성한다(String content) { + public Long 리뷰를_작성한다(String content) { try { final String location = RestAssured.given().log().all() .header(HttpHeaders.AUTHORIZATION, token) @@ -56,11 +47,29 @@ public class StudyRelatedSteps extends Steps { return Long.parseLong(location.replaceAll("/api/studies/" + studyId + "/reviews/", "")); } catch (Exception e) { Assertions.fail("리뷰 작성 실패"); - return -1; + return null; } } - public long 게시글을_작성한다(final String title, final String content) { + public Long 링크를_공유한다(final CreatingLinkRequest request) { + try { + final String location = RestAssured.given().log().all() + .header(AUTHORIZATION, token) + .header(CONTENT_TYPE, APPLICATION_JSON_VALUE) + .pathParams("study-id", studyId) + .body(objectMapper.writeValueAsString(request)) + .when().post("/api/studies/{study-id}/reference-room/links") + .then().log().all() + .statusCode(HttpStatus.CREATED.value()) + .extract().header(HttpHeaders.LOCATION); + return Long.parseLong(location.replaceAll("/api/studies/" + studyId + "/reference-room/links/", "")); + } catch (Exception e) { + Assertions.fail("링크 공유 작성 실패"); + return null; + } + } + + public Long 게시글을_작성한다(final String title, final String content) { try { final String location = RestAssured.given().log().all() .header(org.apache.http.HttpHeaders.AUTHORIZATION, token) @@ -75,7 +84,7 @@ public class StudyRelatedSteps extends Steps { return Long.parseLong(location.replaceAll("/api/studies/" + studyId + "/community/articles/", "")); } catch (Exception e) { Assertions.fail("게시글 작성 실패"); - return -1; + return null; } } } diff --git a/backend/src/test/java/com/woowacourse/acceptance/test/community/CommunityAcceptanceTest.java b/backend/src/test/java/com/woowacourse/acceptance/test/community/CommunityAcceptanceTest.java index 353ebb568..c1104bd40 100644 --- a/backend/src/test/java/com/woowacourse/acceptance/test/community/CommunityAcceptanceTest.java +++ b/backend/src/test/java/com/woowacourse/acceptance/test/community/CommunityAcceptanceTest.java @@ -56,7 +56,7 @@ void writeArticleToCommunity() throws Exception { .body(objectMapper.writeValueAsString(request)) .pathParam("study-id", 스터디_ID) .when().log().all() - .filter(document("write/article", + .filter(document("article/create", requestHeaders( headerWithName(HttpHeaders.AUTHORIZATION).description("JWT 토큰") ), @@ -87,7 +87,7 @@ void writeArticleToCommunity() throws Exception { .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) .pathParam("study-id", 스터디_ID) .pathParam("article-id", articleId) - .filter(document("get/article", + .filter(document("article/get", requestHeaders( headerWithName(HttpHeaders.AUTHORIZATION).description("JWT 토큰") ), @@ -142,7 +142,7 @@ void deleteCommunityArticle() { .header(HttpHeaders.AUTHORIZATION, 토큰) .pathParam("study-id", 스터디_ID) .pathParam("article-id", 게시글_ID) - .filter(document("delete/article", + .filter(document("article/delete", requestHeaders( headerWithName(HttpHeaders.AUTHORIZATION).description("JWT 토큰") ), @@ -191,7 +191,7 @@ void getStudyCommunityArticles() { .pathParam("study-id", 자바_스터디_ID) .queryParam("page", 0) .queryParam("size", 3) - .filter(document("get/articles", + .filter(document("article/list", requestHeaders( headerWithName(HttpHeaders.AUTHORIZATION).description("Jwt 토큰") ), @@ -294,7 +294,7 @@ void updateArticleToCommunity() throws JsonProcessingException { .pathParam("article-id", 게시글_ID) .contentType(MediaType.APPLICATION_JSON_VALUE) .body(objectMapper.writeValueAsString(request)) - .filter(document("update/article", + .filter(document("article/update", requestHeaders( headerWithName(HttpHeaders.AUTHORIZATION).description("JWT 토큰") ), diff --git a/backend/src/test/java/com/woowacourse/acceptance/test/referenceroom/ReferenceRoomAcceptanceTest.java b/backend/src/test/java/com/woowacourse/acceptance/test/referenceroom/ReferenceRoomAcceptanceTest.java new file mode 100644 index 000000000..b578bf2a2 --- /dev/null +++ b/backend/src/test/java/com/woowacourse/acceptance/test/referenceroom/ReferenceRoomAcceptanceTest.java @@ -0,0 +1,242 @@ +package com.woowacourse.acceptance.test.referenceroom; + +import static com.woowacourse.acceptance.fixture.MemberFixtures.그린론_깃허브_ID; +import static com.woowacourse.acceptance.fixture.MemberFixtures.그린론_이름; +import static com.woowacourse.acceptance.fixture.MemberFixtures.그린론_이미지_URL; +import static com.woowacourse.acceptance.fixture.MemberFixtures.그린론_프로필_URL; +import static com.woowacourse.acceptance.fixture.MemberFixtures.디우_깃허브_ID; +import static com.woowacourse.acceptance.fixture.MemberFixtures.디우_이름; +import static com.woowacourse.acceptance.fixture.MemberFixtures.디우_이미지_URL; +import static com.woowacourse.acceptance.fixture.MemberFixtures.디우_프로필_URL; +import static com.woowacourse.acceptance.fixture.MemberFixtures.베루스_깃허브_ID; +import static com.woowacourse.acceptance.fixture.MemberFixtures.베루스_이름; +import static com.woowacourse.acceptance.fixture.MemberFixtures.베루스_이미지_URL; +import static com.woowacourse.acceptance.fixture.MemberFixtures.베루스_프로필_URL; +import static com.woowacourse.acceptance.fixture.MemberFixtures.짱구_깃허브_ID; +import static com.woowacourse.acceptance.fixture.MemberFixtures.짱구_이름; +import static com.woowacourse.acceptance.fixture.MemberFixtures.짱구_이미지_URL; +import static com.woowacourse.acceptance.fixture.MemberFixtures.짱구_프로필_URL; +import static com.woowacourse.acceptance.steps.LoginSteps.그린론이; +import static com.woowacourse.acceptance.steps.LoginSteps.디우가; +import static com.woowacourse.acceptance.steps.LoginSteps.베루스가; +import static com.woowacourse.acceptance.steps.LoginSteps.짱구가; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; +import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +import static org.springframework.restdocs.restassured3.RestAssuredRestDocumentation.document; + +import com.woowacourse.acceptance.AcceptanceTest; +import com.woowacourse.moamoa.referenceroom.service.request.CreatingLinkRequest; +import com.woowacourse.moamoa.referenceroom.service.request.EditingLinkRequest; +import com.woowacourse.moamoa.referenceroom.service.response.AuthorResponse; +import com.woowacourse.moamoa.referenceroom.service.response.LinkResponse; +import com.woowacourse.moamoa.referenceroom.service.response.LinksResponse; +import io.restassured.RestAssured; +import java.time.LocalDate; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.restdocs.payload.JsonFieldType; + +@DisplayName("링크 모음 인수 테스트") +public class ReferenceRoomAcceptanceTest extends AcceptanceTest { + + @DisplayName("참여한 스터디의 링크 공유실에 정상적으로 글을 작성한다.") + @Test + void shareLink() { + final LocalDate 지금 = LocalDate.now(); + final Long javaStudyId = 짱구가().로그인하고().자바_스터디를().시작일자는(지금).생성한다(); + + final String jwtToken = 짱구가().로그인한다(); + final CreatingLinkRequest request = + new CreatingLinkRequest("https://github.com/sc0116", "링크에 대한 간단한 소개입니다."); + + RestAssured.given(spec).log().all() + .filter(document("reference-room/create", + requestHeaders( + headerWithName("Authorization").description("Bearer Token") + ), + requestFields( + fieldWithPath("linkUrl").type(JsonFieldType.STRING).description("링크공유 url"), + fieldWithPath("description").type(JsonFieldType.STRING).description("링크공유 설명") + ))) + .header(HttpHeaders.AUTHORIZATION, jwtToken) + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) + .pathParam("study-id", javaStudyId) + .body(request) + .when().log().all() + .post("/api/studies/{study-id}/reference-room/links") + .then().log().all() + .statusCode(HttpStatus.CREATED.value()); + } + + @DisplayName("스터디에 전체 링크공유 목록을 조회할 수 있다.") + @Test + void getAllLink() { + final LocalDate 지금 = LocalDate.now(); + final Long 자바_스터디_ID = 짱구가().로그인하고().자바_스터디를().시작일자는(지금).생성한다(); + + 그린론이().로그인하고().스터디에(자바_스터디_ID).참여한다(); + 디우가().로그인하고().스터디에(자바_스터디_ID).참여한다(); + 베루스가().로그인하고().스터디에(자바_스터디_ID).참여한다(); + + final CreatingLinkRequest request1 = new CreatingLinkRequest("https://github.com/sc0116", "짱구 링크."); + final CreatingLinkRequest request2 = new CreatingLinkRequest("https://github.com/jaejae-yoo", "그린론 링크."); + final CreatingLinkRequest request3 = new CreatingLinkRequest("https://github.com/tco0427", "디우 링크."); + final CreatingLinkRequest request4 = new CreatingLinkRequest("https://github.com/wilgur513", "베루스 링크."); + + final Long 짱구_링크공유_ID = 짱구가().로그인하고().스터디에(자바_스터디_ID).링크를_공유한다(request1); + final Long 그린론_링크공유_ID = 그린론이().로그인하고().스터디에(자바_스터디_ID).링크를_공유한다(request2); + final Long 디우_링크공유_ID = 디우가().로그인하고().스터디에(자바_스터디_ID).링크를_공유한다(request3); + final Long 베루스_링크공유_ID = 베루스가().로그인하고().스터디에(자바_스터디_ID).링크를_공유한다(request4); + final Long 짱구_링크공유_ID2 = 짱구가().로그인하고().스터디에(자바_스터디_ID).링크를_공유한다(request1); + final Long 짱구_링크공유_ID3 = 짱구가().로그인하고().스터디에(자바_스터디_ID).링크를_공유한다(request1); + + final String token = 짱구가().로그인한다(); + final LinksResponse linksResponse = RestAssured.given(spec).log().all() + .filter(document("reference-room/list", + requestHeaders( + headerWithName("Authorization").description("Bearer Token") + ), + responseFields( + fieldWithPath("links[].id").type(JsonFieldType.NUMBER).description("링크공유 ID"), + fieldWithPath("links[].author.id").type(JsonFieldType.NUMBER).description("링크공유 작성자 ID"), + fieldWithPath("links[].author.username").type(JsonFieldType.STRING).description("링크공유 작성자 유저네임"), + fieldWithPath("links[].author.imageUrl").type(JsonFieldType.STRING).description("링크공유 작성자 이미지 URL"), + fieldWithPath("links[].author.profileUrl").type(JsonFieldType.STRING).description("링크공유 작성자 프로필 URL"), + fieldWithPath("links[].linkUrl").type(JsonFieldType.STRING).description("링크공유 URL"), + fieldWithPath("links[].description").type(JsonFieldType.STRING).description("링크공유 설명"), + fieldWithPath("links[].createdDate").type(JsonFieldType.STRING).description("링크공유 생성일자"), + fieldWithPath("links[].lastModifiedDate").type(JsonFieldType.STRING).description("링크공유 수정일자"), + fieldWithPath("hasNext").type(JsonFieldType.BOOLEAN).description("데이터가 더 존재하는지 여부") + ))) + .header(HttpHeaders.AUTHORIZATION, token) + .pathParam("study-id", 자바_스터디_ID) + .param("page", 1) + .param("size", 5) + .when().log().all() + .get("/api/studies/{study-id}/reference-room/links") + .then().statusCode(HttpStatus.OK.value()) + .extract().as(LinksResponse.class); + + final LocalDate 리뷰_생성일 = LocalDate.now(); + final LocalDate 리뷰_수정일 = LocalDate.now(); + + final AuthorResponse 짱구 = new AuthorResponse(짱구_깃허브_ID, 짱구_이름, 짱구_이미지_URL, 짱구_프로필_URL); + final LinkResponse 짱구_응답 + = new LinkResponse(짱구_링크공유_ID, 짱구, request1.getLinkUrl(), request1.getDescription(), 리뷰_생성일, 리뷰_수정일); + + final AuthorResponse 그린론 = new AuthorResponse(그린론_깃허브_ID, 그린론_이름, 그린론_이미지_URL, 그린론_프로필_URL); + final LinkResponse 그린론_응답 + = new LinkResponse(그린론_링크공유_ID, 그린론, request2.getLinkUrl(), request2.getDescription(), 리뷰_생성일, 리뷰_수정일); + + final AuthorResponse 디우 = new AuthorResponse(디우_깃허브_ID, 디우_이름, 디우_이미지_URL, 디우_프로필_URL); + final LinkResponse 디우_응답 + = new LinkResponse(디우_링크공유_ID, 디우, request3.getLinkUrl(), request3.getDescription(), 리뷰_생성일, 리뷰_수정일); + + final AuthorResponse 베루스 = new AuthorResponse(베루스_깃허브_ID, 베루스_이름, 베루스_이미지_URL, 베루스_프로필_URL); + final LinkResponse 베루스_응답 + = new LinkResponse(베루스_링크공유_ID, 베루스, request4.getLinkUrl(), request4.getDescription(), 리뷰_생성일, 리뷰_수정일); + + final LinkResponse 짱구_응답2 + = new LinkResponse(짱구_링크공유_ID2, 짱구, request1.getLinkUrl(), request1.getDescription(), 리뷰_생성일, 리뷰_수정일); + + final LinkResponse 짱구_응답3 + = new LinkResponse(짱구_링크공유_ID3, 짱구, request1.getLinkUrl(), request1.getDescription(), 리뷰_생성일, 리뷰_수정일); + + assertAll( + () -> assertThat(linksResponse.isHasNext()).isTrue(), + () -> assertThat(linksResponse.getLinks()) + .containsExactly(짱구_응답3, 짱구_응답2, 베루스_응답, 디우_응답, 그린론_응답) + ); + } + + @DisplayName("작성한 링크 공유글을 수정할 수 있다.") + @Test + void updateLink() { + final CreatingLinkRequest creatingLinkRequest = new CreatingLinkRequest("https://github.com/sc0116", + "링크 설명입니다."); + final LocalDate 지금 = LocalDate.now(); + final Long 자바_스터디_ID = 짱구가().로그인하고().자바_스터디를().시작일자는(지금).생성한다(); + final Long 짱구_링크공유_ID = 짱구가().로그인하고().스터디에(자바_스터디_ID).링크를_공유한다(creatingLinkRequest); + final String token = 짱구가().로그인한다(); + + final EditingLinkRequest editingLinkRequest = new EditingLinkRequest("https://github.com/woowacourse", + "수정된 링크 설명입니다."); + + RestAssured.given(spec).log().all() + .filter(document("reference-room/update", + requestHeaders( + headerWithName("Authorization").description("Bearer Token") + ))) + .header(HttpHeaders.AUTHORIZATION, token) + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) + .header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE) + .pathParam("study-id", 자바_스터디_ID) + .pathParam("link-id", 짱구_링크공유_ID) + .body(editingLinkRequest) + .when().log().all() + .put("/api/studies/{study-id}/reference-room/links/{link-id}") + .then().statusCode(HttpStatus.NO_CONTENT.value()); + + final LinksResponse response = RestAssured.given().log().all() + .header(HttpHeaders.AUTHORIZATION, token) + .pathParam("study-id", 자바_스터디_ID) + .when().log().all() + .get("/api/studies/{study-id}/reference-room/links") + .then() + .statusCode(HttpStatus.OK.value()) + .extract().as(LinksResponse.class); + + final LocalDate 링크_생성일 = 지금; + final LocalDate 링크_수정일 = 지금; + + final AuthorResponse 짱구 = new AuthorResponse(짱구_깃허브_ID, 짱구_이름, 짱구_이미지_URL, 짱구_프로필_URL); + final LinkResponse 짱구_링크 = new LinkResponse(짱구_링크공유_ID, 짱구, editingLinkRequest.getLinkUrl(), + editingLinkRequest.getDescription(), 링크_생성일, 링크_수정일); + + assertThat(response.getLinks()).containsExactlyInAnyOrder(짱구_링크); + assertThat(response.isHasNext()).isFalse(); + } + + @DisplayName("작성한 링크 공유글을 삭제할 수 있다.") + @Test + void deleteLink() { + final CreatingLinkRequest creatingLinkRequest = new CreatingLinkRequest("https://github.com/sc0116", + "링크 설명입니다."); + final LocalDate 지금 = LocalDate.now(); + final Long 자바_스터디_ID = 짱구가().로그인하고().자바_스터디를().시작일자는(지금).생성한다(); + final Long 짱구_링크공유_ID = 짱구가().로그인하고().스터디에(자바_스터디_ID).링크를_공유한다(creatingLinkRequest); + final String token = 짱구가().로그인한다(); + + RestAssured.given(spec).log().all() + .filter(document("reference-room/delete", + requestHeaders( + headerWithName("Authorization").description("Bearer Token") + ))) + .header(HttpHeaders.AUTHORIZATION, token) + .pathParam("study-id", 자바_스터디_ID) + .pathParam("link-id", 짱구_링크공유_ID) + .when().log().all() + .delete("/api/studies/{study-id}/reference-room/links/{link-id}") + .then().statusCode(HttpStatus.NO_CONTENT.value()); + + final LinksResponse response = RestAssured.given().log().all() + .header(HttpHeaders.AUTHORIZATION, token) + .pathParam("study-id", 자바_스터디_ID) + .when().log().all() + .get("/api/studies/{study-id}/reference-room/links") + .then() + .statusCode(HttpStatus.OK.value()) + .extract().as(LinksResponse.class); + + assertThat(response.getLinks()).isEmpty(); + assertThat(response.isHasNext()).isFalse(); + } +} diff --git a/backend/src/test/java/com/woowacourse/acceptance/test/review/ReviewsAcceptanceTest.java b/backend/src/test/java/com/woowacourse/acceptance/test/review/ReviewsAcceptanceTest.java index eebf81d2d..6092f6ed0 100644 --- a/backend/src/test/java/com/woowacourse/acceptance/test/review/ReviewsAcceptanceTest.java +++ b/backend/src/test/java/com/woowacourse/acceptance/test/review/ReviewsAcceptanceTest.java @@ -32,7 +32,6 @@ import io.restassured.RestAssured; import java.time.LocalDate; import java.util.List; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.http.HttpHeaders; @@ -149,6 +148,12 @@ public void getReviewsBySize() { final LocalDate 리뷰_생성일 = 지금; final LocalDate 리뷰_수정일 = 지금; + final WriterResponse 짱구 = new WriterResponse(짱구_깃허브_ID, 짱구_이름, 짱구_이미지_URL, 짱구_프로필_URL); + final ReviewResponse 짱구_리뷰 = new ReviewResponse(짱구_리뷰_ID, 짱구, 리뷰_생성일, 리뷰_수정일, "리뷰 내용1"); + + final WriterResponse 그린론 = new WriterResponse(그린론_깃허브_ID, 그린론_이름, 그린론_이미지_URL, 그린론_프로필_URL); + final ReviewResponse 그린론_리뷰 = new ReviewResponse(그린론_리뷰_ID, 그린론, 리뷰_생성일, 리뷰_생성일, "리뷰 내용2"); + final WriterResponse 디우 = new WriterResponse(디우_깃허브_ID, 디우_이름, 디우_이미지_URL, 디우_프로필_URL); final ReviewResponse 디우_리뷰 = new ReviewResponse(디우_리뷰_ID, 디우, 리뷰_생성일, 리뷰_수정일, "리뷰 내용3"); diff --git a/backend/src/test/java/com/woowacourse/moamoa/WebMVCTest.java b/backend/src/test/java/com/woowacourse/moamoa/WebMVCTest.java index 1b88f01ea..4b20a2754 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/WebMVCTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/WebMVCTest.java @@ -4,15 +4,15 @@ import static org.mockito.Mockito.when; import com.fasterxml.jackson.databind.ObjectMapper; - import com.woowacourse.moamoa.auth.config.AuthRequestMatchConfig; import com.woowacourse.moamoa.auth.controller.AuthenticationInterceptor; import com.woowacourse.moamoa.auth.infrastructure.JwtTokenProvider; import com.woowacourse.moamoa.auth.infrastructure.TokenProvider; import com.woowacourse.moamoa.common.MockedServiceObjectsBeanRegister; - import com.woowacourse.moamoa.member.domain.Member; import com.woowacourse.moamoa.member.domain.repository.MemberRepository; +import com.woowacourse.moamoa.referenceroom.service.ReferenceRoomService; +import com.woowacourse.moamoa.referenceroom.service.SearchingReferenceRoomService; import java.util.Optional; import javax.servlet.http.HttpServletRequest; import org.junit.jupiter.api.BeforeEach; @@ -48,6 +48,18 @@ public abstract class WebMVCTest { @MockBean private MemberRepository memberRepository; + @MockBean + protected ReferenceRoomService referenceRoomService; + + @MockBean + protected SearchingReferenceRoomService searchingReferenceRoomService; + + @MockBean + protected HttpServletRequest httpServletRequest; + + @MockBean + protected NativeWebRequest nativeWebRequest; + @BeforeEach void setUp() { when(memberRepository.findByGithubId(any())) diff --git a/backend/src/test/java/com/woowacourse/moamoa/auth/controller/AuthenticationArgumentResolverTest.java b/backend/src/test/java/com/woowacourse/moamoa/auth/controller/AuthenticationArgumentResolverTest.java index 30dabdc4b..857f995f4 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/auth/controller/AuthenticationArgumentResolverTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/auth/controller/AuthenticationArgumentResolverTest.java @@ -4,29 +4,18 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.BDDMockito.given; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.http.HttpHeaders; - +import com.woowacourse.moamoa.WebMVCTest; +import com.woowacourse.moamoa.common.exception.UnauthorizedException; import java.util.Collections; import java.util.List; import javax.servlet.http.HttpServletRequest; - import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; - -import com.woowacourse.moamoa.WebMVCTest; -import com.woowacourse.moamoa.common.exception.UnauthorizedException; -import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpHeaders; class AuthenticationArgumentResolverTest extends WebMVCTest { - @MockBean - protected HttpServletRequest httpServletRequest; - - @MockBean - protected NativeWebRequest nativeWebRequest; - @Autowired private AuthenticationArgumentResolver authenticationArgumentResolver; diff --git a/backend/src/test/java/com/woowacourse/moamoa/auth/controller/AuthenticationInterceptorTest.java b/backend/src/test/java/com/woowacourse/moamoa/auth/controller/AuthenticationInterceptorTest.java index 1844a824a..bb42c718c 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/auth/controller/AuthenticationInterceptorTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/auth/controller/AuthenticationInterceptorTest.java @@ -4,24 +4,17 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.BDDMockito.given; +import com.woowacourse.moamoa.WebMVCTest; +import com.woowacourse.moamoa.common.exception.UnauthorizedException; import java.util.Collections; import java.util.List; - -import javax.servlet.http.HttpServletRequest; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; -import com.woowacourse.moamoa.WebMVCTest; -import com.woowacourse.moamoa.common.exception.UnauthorizedException; - class AuthenticationInterceptorTest extends WebMVCTest { - @MockBean - protected HttpServletRequest httpServletRequest; - @DisplayName("Preflight 요청인지 확인한다.") @Test void isPreflightRequest() { diff --git a/backend/src/test/java/com/woowacourse/moamoa/auth/controller/matcher/AuthenticationRequestMatcherTest.java b/backend/src/test/java/com/woowacourse/moamoa/auth/controller/matcher/AuthenticationRequestMatcherTest.java index 2a9f2cb4e..991bab0c6 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/auth/controller/matcher/AuthenticationRequestMatcherTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/auth/controller/matcher/AuthenticationRequestMatcherTest.java @@ -4,19 +4,13 @@ import static org.mockito.BDDMockito.given; import com.woowacourse.moamoa.WebMVCTest; - -import javax.servlet.http.HttpServletRequest; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.mock.mockito.MockBean; class AuthenticationRequestMatcherTest extends WebMVCTest { - @MockBean - protected HttpServletRequest httpServletRequest; - @Autowired private AuthenticationRequestMatcher authenticationRequestMatcher; diff --git a/backend/src/test/java/com/woowacourse/moamoa/referenceroom/controller/ReferenceRoomControllerTest.java b/backend/src/test/java/com/woowacourse/moamoa/referenceroom/controller/ReferenceRoomControllerTest.java new file mode 100644 index 000000000..f99fd2090 --- /dev/null +++ b/backend/src/test/java/com/woowacourse/moamoa/referenceroom/controller/ReferenceRoomControllerTest.java @@ -0,0 +1,127 @@ +package com.woowacourse.moamoa.referenceroom.controller; + +import static com.woowacourse.moamoa.fixtures.MemberFixtures.디우; +import static com.woowacourse.moamoa.fixtures.MemberFixtures.디우_깃허브_아이디; +import static com.woowacourse.moamoa.fixtures.MemberFixtures.베루스; +import static com.woowacourse.moamoa.fixtures.MemberFixtures.베루스_깃허브_아이디; +import static com.woowacourse.moamoa.fixtures.MemberFixtures.짱구; +import static com.woowacourse.moamoa.fixtures.MemberFixtures.짱구_깃허브_아이디; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.woowacourse.moamoa.common.RepositoryTest; +import com.woowacourse.moamoa.common.utils.DateTimeSystem; +import com.woowacourse.moamoa.member.domain.repository.MemberRepository; +import com.woowacourse.moamoa.referenceroom.domain.repository.LinkRepository; +import com.woowacourse.moamoa.referenceroom.service.ReferenceRoomService; +import com.woowacourse.moamoa.referenceroom.service.exception.LinkNotFoundException; +import com.woowacourse.moamoa.referenceroom.service.exception.NotCreatingLinkException; +import com.woowacourse.moamoa.referenceroom.service.exception.NotParticipatedMemberException; +import com.woowacourse.moamoa.referenceroom.service.request.CreatingLinkRequest; +import com.woowacourse.moamoa.referenceroom.service.request.EditingLinkRequest; +import com.woowacourse.moamoa.study.domain.repository.StudyRepository; +import com.woowacourse.moamoa.study.service.StudyService; +import com.woowacourse.moamoa.study.service.request.CreatingStudyRequest; +import java.time.LocalDate; +import javax.persistence.EntityManager; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +@RepositoryTest +public class ReferenceRoomControllerTest { + + @Autowired + private MemberRepository memberRepository; + + @Autowired + private StudyRepository studyRepository; + + @Autowired + private LinkRepository linkRepository; + + @Autowired + private EntityManager entityManager; + + private ReferenceRoomController sut; + + private Long jjangguId; + private Long verusId; + private Long dwooId; + private Long javaStudyId; + private Long linkId; + + @BeforeEach + void setUp() { + sut = new ReferenceRoomController(new ReferenceRoomService(memberRepository, studyRepository, linkRepository)); + + // 사용자 추가 + jjangguId = memberRepository.save(짱구).getId(); + verusId = memberRepository.save(베루스).getId(); + dwooId = memberRepository.save(디우).getId(); + + // 스터디 생성 + final StudyService studyService = new StudyService(studyRepository, memberRepository, new DateTimeSystem()); + final LocalDate startDate = LocalDate.now(); + final CreatingStudyRequest javaStudyRequest = CreatingStudyRequest.builder() + .title("java 스터디").excerpt("자바 설명").thumbnail("java image").description("자바 소개") + .startDate(startDate) + .build(); + + javaStudyId = studyService.createStudy(짱구_깃허브_아이디, javaStudyRequest).getId(); + studyService.participateStudy(베루스_깃허브_아이디, javaStudyId); + + // 링크 공유 생성 + final ReferenceRoomService referenceRoomService = + new ReferenceRoomService(memberRepository, studyRepository, linkRepository); + final CreatingLinkRequest creatingLinkRequest = + new CreatingLinkRequest("https://github.com/sc0116", "링크 설명입니다."); + + linkId = referenceRoomService.createLink(짱구_깃허브_아이디, javaStudyId, creatingLinkRequest).getId(); + + entityManager.flush(); + entityManager.clear(); + } + + @DisplayName("스터디에 참여하지 않은 회원은 링크 공유를 할 수 없다.") + @Test + void createByNotParticipatedMember() { + final CreatingLinkRequest creatingLinkRequest = + new CreatingLinkRequest("https://github.com/sc0116", "링크 설명입니다."); + + assertThatThrownBy(() -> sut.createLink(디우_깃허브_아이디, javaStudyId, creatingLinkRequest)) + .isInstanceOf(NotCreatingLinkException.class); + } + + @DisplayName("존재하지 않는 링크 공유글을 수정할 수 없다.") + @Test + void updateByInvalidLinkId() { + final EditingLinkRequest editingLinkRequest = new EditingLinkRequest("www.naver.com", "수정"); + + assertThatThrownBy(() -> sut.updateLink(짱구_깃허브_아이디, javaStudyId, -1L, editingLinkRequest)) + .isInstanceOf(LinkNotFoundException.class); + } + + @DisplayName("스터디에 참여하지 않은 경우 링크 공유글을 수정할 수 없다.") + @Test + void updateByNotParticipatedMember() { + final EditingLinkRequest editingLinkRequest = new EditingLinkRequest("https://github.com", "수정된 링크 설명입니다."); + + assertThatThrownBy(() -> sut.updateLink(디우_깃허브_아이디, javaStudyId, linkId, editingLinkRequest)) + .isInstanceOf(NotParticipatedMemberException.class); + } + + @DisplayName("존재하지 않는 링크 공유글을 삭제할 수 없다.") + @Test + void deleteByInvalidLinkId() { + assertThatThrownBy(() -> sut.deleteLink(짱구_깃허브_아이디, javaStudyId, -1L)) + .isInstanceOf(LinkNotFoundException.class); + } + + @DisplayName("스터디에 참여하지 않은 경우 링크 공유글을 삭제할 수 없다.") + @Test + void deleteByNotParticipatedMember() { + assertThatThrownBy(() -> sut.deleteLink(디우_깃허브_아이디, javaStudyId, linkId)) + .isInstanceOf(NotParticipatedMemberException.class); + } +} diff --git a/backend/src/test/java/com/woowacourse/moamoa/referenceroom/controller/SearchingReferenceRoomControllerTest.java b/backend/src/test/java/com/woowacourse/moamoa/referenceroom/controller/SearchingReferenceRoomControllerTest.java new file mode 100644 index 000000000..73fda2126 --- /dev/null +++ b/backend/src/test/java/com/woowacourse/moamoa/referenceroom/controller/SearchingReferenceRoomControllerTest.java @@ -0,0 +1,154 @@ +package com.woowacourse.moamoa.referenceroom.controller; + +import static com.woowacourse.moamoa.fixtures.MemberFixtures.그린론; +import static com.woowacourse.moamoa.fixtures.MemberFixtures.그린론_깃허브_아이디; +import static com.woowacourse.moamoa.fixtures.MemberFixtures.그린론_응답; +import static com.woowacourse.moamoa.fixtures.MemberFixtures.디우; +import static com.woowacourse.moamoa.fixtures.MemberFixtures.디우_깃허브_아이디; +import static com.woowacourse.moamoa.fixtures.MemberFixtures.디우_응답; +import static com.woowacourse.moamoa.fixtures.MemberFixtures.베루스; +import static com.woowacourse.moamoa.fixtures.MemberFixtures.베루스_깃허브_아이디; +import static com.woowacourse.moamoa.fixtures.MemberFixtures.베루스_응답; +import static com.woowacourse.moamoa.fixtures.MemberFixtures.병민; +import static com.woowacourse.moamoa.fixtures.MemberFixtures.병민_깃허브_아이디; +import static com.woowacourse.moamoa.fixtures.MemberFixtures.짱구; +import static com.woowacourse.moamoa.fixtures.MemberFixtures.짱구_깃허브_아이디; +import static com.woowacourse.moamoa.fixtures.MemberFixtures.짱구_응답; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; + +import com.woowacourse.moamoa.common.RepositoryTest; +import com.woowacourse.moamoa.common.utils.DateTimeSystem; +import com.woowacourse.moamoa.member.domain.Member; +import com.woowacourse.moamoa.member.domain.repository.MemberRepository; +import com.woowacourse.moamoa.referenceroom.domain.Link; +import com.woowacourse.moamoa.referenceroom.domain.repository.LinkRepository; +import com.woowacourse.moamoa.referenceroom.query.LinkDao; +import com.woowacourse.moamoa.referenceroom.query.data.LinkData; +import com.woowacourse.moamoa.referenceroom.service.ReferenceRoomService; +import com.woowacourse.moamoa.referenceroom.service.SearchingReferenceRoomService; +import com.woowacourse.moamoa.referenceroom.service.exception.NotParticipatedMemberException; +import com.woowacourse.moamoa.referenceroom.service.request.CreatingLinkRequest; +import com.woowacourse.moamoa.referenceroom.service.response.LinkResponse; +import com.woowacourse.moamoa.referenceroom.service.response.LinksResponse; +import com.woowacourse.moamoa.study.domain.Study; +import com.woowacourse.moamoa.study.domain.repository.StudyRepository; +import com.woowacourse.moamoa.study.service.StudyService; +import com.woowacourse.moamoa.study.service.request.CreatingStudyRequest; +import java.time.LocalDate; +import java.util.List; +import javax.persistence.EntityManager; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.PageRequest; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +@RepositoryTest +public class SearchingReferenceRoomControllerTest { + + @Autowired + private LinkDao linkDao; + + @Autowired + private MemberRepository memberRepository; + + @Autowired + private StudyRepository studyRepository; + + @Autowired + private LinkRepository linkRepository; + + @Autowired + private EntityManager entityManager; + + private SearchingReferenceRoomController sut; + + private Study javaStudy; + + private List<LinkResponse> linkResponses; + + @BeforeEach + void setUp() { + sut = new SearchingReferenceRoomController( + new SearchingReferenceRoomService(linkDao, memberRepository, studyRepository)); + + // 사용자 추가 + final Member jjanggu = memberRepository.save(짱구); + final Member greenlawn = memberRepository.save(그린론); + final Member dwoo = memberRepository.save(디우); + final Member verus = memberRepository.save(베루스); + memberRepository.save(병민); + + // 스터디 생성 + StudyService studyService = new StudyService(studyRepository, memberRepository, new DateTimeSystem()); + + final LocalDate startDate = LocalDate.now(); + CreatingStudyRequest javaStudyRequest = CreatingStudyRequest.builder() + .title("java 스터디").excerpt("자바 설명").thumbnail("java image").description("자바 소개") + .startDate(startDate) + .build(); + + javaStudy = studyService.createStudy(짱구_깃허브_아이디, javaStudyRequest); + + studyService.participateStudy(그린론_깃허브_아이디, javaStudy.getId()); + studyService.participateStudy(디우_깃허브_아이디, javaStudy.getId()); + studyService.participateStudy(베루스_깃허브_아이디, javaStudy.getId()); + + // 링크 공유 추가 + final ReferenceRoomService linkService = new ReferenceRoomService(memberRepository, studyRepository, + linkRepository); + + final CreatingLinkRequest request1 = new CreatingLinkRequest("https://github.com/sc0116", "짱구 링크."); + final CreatingLinkRequest request2 = new CreatingLinkRequest("https://github.com/jaejae-yoo", "그린론 링크."); + final CreatingLinkRequest request3 = new CreatingLinkRequest("https://github.com/tco0427", "디우 링크."); + final CreatingLinkRequest request4 = new CreatingLinkRequest("https://github.com/wilgur513", "베루스 링크."); + + final Link link1 = linkService.createLink(짱구_깃허브_아이디, javaStudy.getId(), request1); + final Link link2 = linkService.createLink(그린론_깃허브_아이디, javaStudy.getId(), request2); + final Link link3 = linkService.createLink(디우_깃허브_아이디, javaStudy.getId(), request3); + final Link link4 = linkService.createLink(베루스_깃허브_아이디, javaStudy.getId(), request4); + + entityManager.flush(); + entityManager.clear(); + + final LinkResponse 링크1 = new LinkResponse( + new LinkData(link1.getId(), 짱구_응답, link1.getLinkUrl(), link1.getDescription(), + link1.getCreatedDate().toLocalDate(), link1.getLastModifiedDate().toLocalDate())); + final LinkResponse 링크2 = new LinkResponse( + new LinkData(link2.getId(), 그린론_응답, link2.getLinkUrl(), link2.getDescription(), + link2.getCreatedDate().toLocalDate(), link2.getLastModifiedDate().toLocalDate())); + final LinkResponse 링크3 = new LinkResponse( + new LinkData(link3.getId(), 디우_응답, link3.getLinkUrl(), link3.getDescription(), + link3.getCreatedDate().toLocalDate(), link3.getLastModifiedDate().toLocalDate())); + final LinkResponse 링크4 = new LinkResponse( + new LinkData(link4.getId(), 베루스_응답, link4.getLinkUrl(), link4.getDescription(), + link4.getCreatedDate().toLocalDate(), link4.getLastModifiedDate().toLocalDate())); + + linkResponses = List.of(링크1, 링크2, 링크3, 링크4); + } + + @DisplayName("링크 공유글 전체 목록 조회를 할 수 있다.") + @Test + void getLinks() { + final ResponseEntity<LinksResponse> links = sut.getLinks(짱구_깃허브_아이디, javaStudy.getId(), PageRequest.of(0, 5)); + + assertAll( + () -> assertThat(links.getStatusCode()).isEqualTo(HttpStatus.OK), + () -> assertThat(links.getBody()).isNotNull(), + () -> assertThat(links.getBody().isHasNext()).isFalse(), + () -> assertThat(links.getBody().getLinks()) + .containsExactlyInAnyOrderElementsOf(linkResponses) + ); + } + + @DisplayName("스터디에 참여하지 않은 회원은 링크 공유글을 조회할 수 없다.") + @Test + void getLinksByNotParticipatedMember() { + assertThatThrownBy(() -> sut.getLinks(병민_깃허브_아이디, javaStudy.getId(), PageRequest.of(0, 5))) + .isInstanceOf(NotParticipatedMemberException.class); + } +} diff --git a/backend/src/test/java/com/woowacourse/moamoa/referenceroom/domain/LinkTest.java b/backend/src/test/java/com/woowacourse/moamoa/referenceroom/domain/LinkTest.java new file mode 100644 index 000000000..8130e0438 --- /dev/null +++ b/backend/src/test/java/com/woowacourse/moamoa/referenceroom/domain/LinkTest.java @@ -0,0 +1,85 @@ +package com.woowacourse.moamoa.referenceroom.domain; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; + +import com.woowacourse.moamoa.referenceroom.service.exception.NotLinkAuthorException; +import com.woowacourse.moamoa.referenceroom.service.exception.NotRelatedLinkException; +import com.woowacourse.moamoa.review.domain.AssociatedStudy; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +public class LinkTest { + + @DisplayName("링크 공유를 수정한다.") + @Test + void update() { + final Link link = new Link(new AssociatedStudy(1L), new Author(1L), "link", "설명"); + final Link updatedLink = new Link(new AssociatedStudy(1L), new Author(1L), "updated link", "수정된 설명"); + + link.update(updatedLink); + + assertAll( + () -> assertThat(link.getLinkUrl()).isEqualTo("updated link"), + () -> assertThat(link.getDescription()).isEqualTo("수정된 설명") + ); + } + + @DisplayName("작성자가 아니면 수정할 수 없다.") + @Test + void updateByNotAuthor() { + final Author author = new Author(1L); + final Author nonAuthor = new Author(2L); + + final Link link = new Link(new AssociatedStudy(1L), author, "link", "설명"); + final Link updatedLink = new Link(new AssociatedStudy(1L), nonAuthor, "updated link", "수정된 설명"); + + assertThatThrownBy(() -> link.update(updatedLink)) + .isInstanceOf(NotLinkAuthorException.class); + } + + @DisplayName("스터디에 속하지 않은 링크 공유글을 수정할 수 없다.") + @Test + void updateByNotBelongToStudy() { + final AssociatedStudy relatedStudy = new AssociatedStudy(1L); + final AssociatedStudy unrelatedStudy = new AssociatedStudy(2L); + + final Link link = new Link(relatedStudy, new Author(1L), "link", "설명"); + final Link updatedLink = new Link(unrelatedStudy, new Author(1L), "updated link", "수정된 설명"); + + assertThatThrownBy(() -> link.update(updatedLink)) + .isInstanceOf(NotRelatedLinkException.class); + } + + @DisplayName("링크 공유를 삭제한다.") + @Test + void delete() { + final Author author = new Author(1L); + final Link link = new Link(new AssociatedStudy(1L), author, "link", "설명"); + + link.delete(new AssociatedStudy(1L), author); + + assertThat(link.isDeleted()).isTrue(); + } + + @DisplayName("작성자가 아니면 삭제할 수 없다.") + @Test + void deleteByNotAuthor() { + final Link link = new Link(new AssociatedStudy(1L), new Author(1L), "link", "설명"); + final Author nonAuthor = new Author(2L); + + assertThatThrownBy(() -> link.delete(new AssociatedStudy(1L), nonAuthor)) + .isInstanceOf(NotLinkAuthorException.class); + } + + @DisplayName("스터디에 속하지 않은 링크 공유글을 삭제할 수 없다.") + @Test + void deleteByNotBelongToStudy() { + final Link link = new Link(new AssociatedStudy(1L), new Author(1L), "link", "설명"); + final AssociatedStudy unrelatedStudy = new AssociatedStudy(2L); + + assertThatThrownBy(() -> link.delete(unrelatedStudy, new Author(1L))) + .isInstanceOf(NotRelatedLinkException.class); + } +} diff --git a/backend/src/test/java/com/woowacourse/moamoa/referenceroom/query/LinkDaoTest.java b/backend/src/test/java/com/woowacourse/moamoa/referenceroom/query/LinkDaoTest.java new file mode 100644 index 000000000..1e4e5eef0 --- /dev/null +++ b/backend/src/test/java/com/woowacourse/moamoa/referenceroom/query/LinkDaoTest.java @@ -0,0 +1,122 @@ +package com.woowacourse.moamoa.referenceroom.query; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +import com.woowacourse.moamoa.common.RepositoryTest; +import com.woowacourse.moamoa.common.utils.DateTimeSystem; +import com.woowacourse.moamoa.member.domain.Member; +import com.woowacourse.moamoa.member.domain.repository.MemberRepository; +import com.woowacourse.moamoa.member.query.data.MemberData; +import com.woowacourse.moamoa.referenceroom.domain.Link; +import com.woowacourse.moamoa.referenceroom.domain.repository.LinkRepository; +import com.woowacourse.moamoa.referenceroom.query.data.LinkData; +import com.woowacourse.moamoa.referenceroom.service.ReferenceRoomService; +import com.woowacourse.moamoa.referenceroom.service.request.CreatingLinkRequest; +import com.woowacourse.moamoa.study.domain.Study; +import com.woowacourse.moamoa.study.domain.repository.StudyRepository; +import com.woowacourse.moamoa.study.service.StudyService; +import com.woowacourse.moamoa.study.service.request.CreatingStudyRequest; +import java.time.LocalDate; +import java.util.List; +import javax.persistence.EntityManager; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Slice; +import org.springframework.jdbc.core.JdbcTemplate; + +@RepositoryTest +public class LinkDaoTest { + + private static final MemberData JJANGGU = new MemberData(1L, "jjanggu", "https://image", "github.com"); + private static final MemberData GREENLAWN = new MemberData(2L, "greenlawn", "https://image", "github.com"); + private static final MemberData DWOO = new MemberData(3L, "dwoo", "https://image", "github.com"); + private static final MemberData VERUS = new MemberData(4L, "verus", "https://image", "github.com"); + + @Autowired + private MemberRepository memberRepository; + + @Autowired + private StudyRepository studyRepository; + + @Autowired + private LinkRepository linkRepository; + + @Autowired + private JdbcTemplate jdbcTemplate; + + @Autowired + private EntityManager entityManager; + + @Autowired + private LinkDao sut; + + private Study javaStudy; + + private List<LinkData> linkData; + + @BeforeEach + void setUp() { + // 사용자 추가 + final Member jjanggu = memberRepository.save(toMember(JJANGGU)); + final Member greenlawn = memberRepository.save(toMember(GREENLAWN)); + final Member dwoo = memberRepository.save(toMember(DWOO)); + final Member verus = memberRepository.save(toMember(VERUS)); + + // 스터디 생성 + StudyService createStudyService = new StudyService(studyRepository, memberRepository, new DateTimeSystem()); + + final LocalDate startDate = LocalDate.now(); + CreatingStudyRequest javaStudyRequest = CreatingStudyRequest.builder() + .title("java 스터디").excerpt("자바 설명").thumbnail("java image").description("자바 소개") + .startDate(startDate) + .build(); + + javaStudy = createStudyService.createStudy(JJANGGU.getGithubId(), javaStudyRequest); + createStudyService.participateStudy(GREENLAWN.getGithubId(), javaStudy.getId()); + createStudyService.participateStudy(DWOO.getGithubId(), javaStudy.getId()); + createStudyService.participateStudy(VERUS.getGithubId(), javaStudy.getId()); + + // 링크 공유 추가 + final ReferenceRoomService linkService = new ReferenceRoomService(memberRepository, studyRepository, linkRepository); + + final CreatingLinkRequest request1 = new CreatingLinkRequest("https://github.com/sc0116", "짱구 링크."); + final CreatingLinkRequest request2 = new CreatingLinkRequest("https://github.com/jaejae-yoo", "그린론 링크."); + final CreatingLinkRequest request3 = new CreatingLinkRequest("https://github.com/tco0427", "디우 링크."); + final CreatingLinkRequest request4 = new CreatingLinkRequest("https://github.com/wilgur513", "베루스 링크."); + + final Link link1 = linkService.createLink(JJANGGU.getGithubId(), javaStudy.getId(), request1); + final Link link2 = linkService.createLink(GREENLAWN.getGithubId(), javaStudy.getId(), request2); + final Link link3 = linkService.createLink(DWOO.getGithubId(), javaStudy.getId(), request3); + final Link link4 = linkService.createLink(VERUS.getGithubId(), javaStudy.getId(), request4); + + entityManager.flush(); + entityManager.clear(); + + final LinkData 링크1 = new LinkData(link1.getId(), JJANGGU, link1.getLinkUrl(), link1.getDescription(), link1.getCreatedDate().toLocalDate(), link1.getLastModifiedDate().toLocalDate()); + final LinkData 링크2 = new LinkData(link2.getId(), GREENLAWN, link2.getLinkUrl(), link2.getDescription(), link2.getCreatedDate().toLocalDate(), link2.getLastModifiedDate().toLocalDate()); + final LinkData 링크3 = new LinkData(link3.getId(), DWOO, link3.getLinkUrl(), link3.getDescription(), link3.getCreatedDate().toLocalDate(), link3.getLastModifiedDate().toLocalDate()); + final LinkData 링크4 = new LinkData(link4.getId(), VERUS, link4.getLinkUrl(), link4.getDescription(), link4.getCreatedDate().toLocalDate(), link4.getLastModifiedDate().toLocalDate()); + + linkData = List.of(링크1, 링크2, 링크3, 링크4); + } + + private Member toMember(final MemberData data) { + return new Member(data.getGithubId(), data.getUsername(), data.getImageUrl(), data.getProfileUrl()); + } + + @DisplayName("스터디 ID로 링크 공유글을 조회한다.") + @Test + void getLinks() { + final Slice<LinkData> links = sut.findAllByStudyId(javaStudy.getId(), PageRequest.of(0, 5)); + + assertAll( + () -> assertThat(links.hasNext()).isFalse(), + () -> assertThat(links.getContent()) + .containsExactlyInAnyOrderElementsOf(linkData) + ); + } +} diff --git a/backend/src/test/java/com/woowacourse/moamoa/referenceroom/webmvc/ReferenceRoomWebMvcTest.java b/backend/src/test/java/com/woowacourse/moamoa/referenceroom/webmvc/ReferenceRoomWebMvcTest.java new file mode 100644 index 000000000..6eb4809e3 --- /dev/null +++ b/backend/src/test/java/com/woowacourse/moamoa/referenceroom/webmvc/ReferenceRoomWebMvcTest.java @@ -0,0 +1,115 @@ +package com.woowacourse.moamoa.referenceroom.webmvc; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.woowacourse.moamoa.WebMVCTest; +import com.woowacourse.moamoa.referenceroom.service.request.CreatingLinkRequest; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; + +public class ReferenceRoomWebMvcTest extends WebMVCTest { + + @DisplayName("필수 데이터인 링크 URL이 null인 경우 400을 반환한다.") + @Test + void requestByNullLinkUrl() throws Exception { + final String token = "Bearer " + tokenProvider.createToken(1L).getAccessToken(); + final String content = objectMapper.writeValueAsString(new CreatingLinkRequest(null, "설명")); + + mockMvc.perform(post("/api/studies/1/reference-room/links") + .header(HttpHeaders.AUTHORIZATION, token) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .content(content)) + .andDo(print()) + .andExpect(status().isBadRequest()); + } + + @DisplayName("필수 데이터인 링크 URL이 공백인 경우 400을 반환한다.") + @Test + void requestByBlankLinkUrl() throws Exception { + final String token = "Bearer " + tokenProvider.createToken(1L).getAccessToken(); + final String content = objectMapper.writeValueAsString(new CreatingLinkRequest("", "설명")); + + mockMvc.perform(post("/api/studies/1/reference-room/links") + .header(HttpHeaders.AUTHORIZATION, token) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .content(content)) + .andDo(print()) + .andExpect(status().isBadRequest()); + } + + @DisplayName("링크 공유 설명이 25글자 이상인 경우 400을 반환한다.") + @Test + void requestBy25LengthExceededDescription() throws Exception { + final String token = "Bearer " + tokenProvider.createToken(1L).getAccessToken(); + final String content = objectMapper.writeValueAsString(new CreatingLinkRequest("링크", + "일이삼사오육칠팔구십" + + "일이삼사오육칠팔이십" + + "일이삼사오육칠팔삼십" + + "일이삼사오육칠팔사십" + + "일이삼사오육앗싸오십일")); + + mockMvc.perform(post("/api/studies/1/reference-room/links") + .header(HttpHeaders.AUTHORIZATION, token) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .content(content)) + .andDo(print()) + .andExpect(status().isBadRequest()); + } + + @DisplayName("정상적이지 않은 스터디 id인 경우 400을 반환한다.") + @Test + void requestByInvalidStudyId() throws Exception { + final String token = "Bearer " + tokenProvider.createToken(1L).getAccessToken(); + final String content = objectMapper.writeValueAsString(new CreatingLinkRequest("링크", "설명")); + + mockMvc.perform(post("/api/studies/one/reference-room/links") + .header(HttpHeaders.AUTHORIZATION, token) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .content(content)) + .andDo(print()) + .andExpect(status().isBadRequest()); + } + + @DisplayName("page 파라미터 형식이 잘못된 경우 400을 반환한다.") + @Test + void requestByInvalidPageParameter() throws Exception { + final String token = tokenProvider.createToken(1L).getAccessToken(); + + mockMvc.perform(get("/api/studies/1/reference-room/links") + .header(HttpHeaders.AUTHORIZATION, "Bearer " + token) + .param("page", "one")) + .andDo(print()) + .andExpect(status().isBadRequest()); + } + + @DisplayName("size 파라미터 형식이 잘못된 경우 400을 반환한다.") + @Test + void requestByInvalidSizeParameter() throws Exception { + final String token = tokenProvider.createToken(1L).getAccessToken(); + + mockMvc.perform(get("/api/studies/1/reference-room/links") + .header(HttpHeaders.AUTHORIZATION, "Bearer " + token) + .param("size", "one")) + .andDo(print()) + .andExpect(status().isBadRequest()); + } + + @DisplayName("유효하지 않은 토큰으로 호출하는 경우 401을 반환한다.") + @Test + void requestByInvalidToken() throws Exception { + final String invalidToken = "Bearer Invalid Token"; + final String content = objectMapper.writeValueAsString(new CreatingLinkRequest("링크", "설명")); + + mockMvc.perform(post("/api/studies/one/reference-room/links") + .header(HttpHeaders.AUTHORIZATION, invalidToken) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .content(content)) + .andDo(print()) + .andExpect(status().isUnauthorized()); + } +} diff --git a/backend/src/test/java/com/woowacourse/moamoa/review/controller/SearchingReviewControllerTest.java b/backend/src/test/java/com/woowacourse/moamoa/review/controller/SearchingReviewControllerTest.java index 8296decd0..916ac6c94 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/review/controller/SearchingReviewControllerTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/review/controller/SearchingReviewControllerTest.java @@ -113,7 +113,6 @@ void setUp() { javaReviews = List.of(리뷰_내용4, 리뷰_내용3, 리뷰_내용2, 리뷰_내용1); entityManager.flush(); - entityManager.clear(); } private static Member toMember(MemberData memberData) { diff --git a/backend/src/test/java/com/woowacourse/moamoa/study/domain/StudyTest.java b/backend/src/test/java/com/woowacourse/moamoa/study/domain/StudyTest.java index 83fb56881..a18b1d389 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/study/domain/StudyTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/study/domain/StudyTest.java @@ -12,7 +12,6 @@ import static java.time.LocalDateTime.now; -import com.woowacourse.moamoa.member.domain.Member; import com.woowacourse.moamoa.study.domain.exception.InvalidPeriodException; import com.woowacourse.moamoa.study.service.exception.FailureParticipationException; @@ -197,7 +196,7 @@ void writeReviewAfterStartDate( final Study sut = new Study(content, participants, recruitPlanner, studyPlanner, AttachedTags.empty(), createdAt); - assertThat(sut.isWritableReviews(1L)).isEqualTo(isWritable); + assertThat(sut.isReviewWritable(1L)).isEqualTo(isWritable); } private static Stream<Arguments> provideStudyPeriod() { @@ -234,7 +233,7 @@ void writeReviewByOwner() { final StudyPlanner studyPlanner = new StudyPlanner(LocalDate.now(), LocalDate.now(), IN_PROGRESS); final Study sut = new Study(content, participants, recruitPlanner, studyPlanner, AttachedTags.empty(), now()); - assertThat(sut.isWritableReviews(1L)).isTrue(); + assertThat(sut.isReviewWritable(1L)).isTrue(); } @DisplayName("스터디에 참여한 사용자는 리뷰를 작성할 수 있다.") @@ -248,7 +247,7 @@ void writeReviewByParticipant() { sut.participate(2L); - assertThat(sut.isWritableReviews(2L)).isTrue(); + assertThat(sut.isReviewWritable(2L)).isTrue(); } @DisplayName("스터디에 참여하지 않은 사용자는 리뷰를 작성할 수 없다.") @@ -260,7 +259,7 @@ void writeReviewByNotParticipant() { final StudyPlanner studyPlanner = new StudyPlanner(LocalDate.now(), LocalDate.now(), IN_PROGRESS); final Study sut = new Study(content, participants, recruitPlanner, studyPlanner, AttachedTags.empty(), now()); - assertThat(sut.isWritableReviews(2L)).isFalse(); + assertThat(sut.isReviewWritable(2L)).isFalse(); } @DisplayName("스터디에서 나의 역할을 조회한다.") diff --git a/backend/src/test/resources/schema.sql b/backend/src/test/resources/schema.sql index 225b95efd..fcbcc4936 100644 --- a/backend/src/test/resources/schema.sql +++ b/backend/src/test/resources/schema.sql @@ -49,6 +49,20 @@ CREATE TABLE review FOREIGN KEY (member_id) REFERENCES member (id) ); +CREATE TABLE link +( + id BIGINT PRIMARY KEY AUTO_INCREMENT, + study_id BIGINT NOT NULL, + member_id BIGINT NOT NULL, + link_url MEDIUMTEXT NOT NULL, + description MEDIUMTEXT, + created_date DATETIME NOT NULL, + last_modified_date DATETIME NOT NULL, + deleted boolean NOT NULL, + FOREIGN KEY (study_id) REFERENCES study (id), + FOREIGN KEY (member_id) REFERENCES member (id) +); + CREATE TABLE category ( id BIGINT PRIMARY KEY, From 2b0320b12c7f1052803b85e14af010498d6f71de Mon Sep 17 00:00:00 2001 From: SeungCheol Shin <47477359+sc0116@users.noreply.github.com> Date: Fri, 12 Aug 2022 16:24:10 +0900 Subject: [PATCH 13/51] =?UTF-8?q?[BE]=20issue252:=20=EB=A7=81=ED=81=AC=20?= =?UTF-8?q?=EA=B3=B5=EC=9C=A0=20=EC=84=A4=EB=AA=85=EB=9E=80=20=EC=B5=9C?= =?UTF-8?q?=EB=8C=80=20=EA=B8=80=EC=9E=90=EC=88=98=2040=EC=9E=90=EB=A1=9C?= =?UTF-8?q?=20=EB=B3=80=EA=B2=BD=20(#253)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../referenceroom/service/request/CreatingLinkRequest.java | 2 +- .../referenceroom/service/request/EditingLinkRequest.java | 2 +- .../moamoa/referenceroom/webmvc/ReferenceRoomWebMvcTest.java | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/backend/src/main/java/com/woowacourse/moamoa/referenceroom/service/request/CreatingLinkRequest.java b/backend/src/main/java/com/woowacourse/moamoa/referenceroom/service/request/CreatingLinkRequest.java index c20d3736c..df4fd1be1 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/referenceroom/service/request/CreatingLinkRequest.java +++ b/backend/src/main/java/com/woowacourse/moamoa/referenceroom/service/request/CreatingLinkRequest.java @@ -18,7 +18,7 @@ public class CreatingLinkRequest { @Size(max = 500, message = "링크 URL은 500자를 초과할 수 없습니다.") private String linkUrl; - @Size(max = 25, message = "설명은 25자를 초과할 수 없습니다.") + @Size(max = 40, message = "설명은 40자를 초과할 수 없습니다.") private String description; public Link toLink(final Long studyId, final Long memberId) { diff --git a/backend/src/main/java/com/woowacourse/moamoa/referenceroom/service/request/EditingLinkRequest.java b/backend/src/main/java/com/woowacourse/moamoa/referenceroom/service/request/EditingLinkRequest.java index 9e2a3550d..26c304289 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/referenceroom/service/request/EditingLinkRequest.java +++ b/backend/src/main/java/com/woowacourse/moamoa/referenceroom/service/request/EditingLinkRequest.java @@ -18,7 +18,7 @@ public class EditingLinkRequest { @Size(max = 500, message = "링크 URL은 500자를 초과할 수 없습니다.") private String linkUrl; - @Size(max = 25, message = "설명은 25자를 초과할 수 없습니다.") + @Size(max = 40, message = "설명은 40자를 초과할 수 없습니다.") private String description; public Link toLink(final Long studyId, final Long memberId) { diff --git a/backend/src/test/java/com/woowacourse/moamoa/referenceroom/webmvc/ReferenceRoomWebMvcTest.java b/backend/src/test/java/com/woowacourse/moamoa/referenceroom/webmvc/ReferenceRoomWebMvcTest.java index 6eb4809e3..8386a42e4 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/referenceroom/webmvc/ReferenceRoomWebMvcTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/referenceroom/webmvc/ReferenceRoomWebMvcTest.java @@ -42,9 +42,9 @@ void requestByBlankLinkUrl() throws Exception { .andExpect(status().isBadRequest()); } - @DisplayName("링크 공유 설명이 25글자 이상인 경우 400을 반환한다.") + @DisplayName("링크 공유 설명이 40글자 이상인 경우 400을 반환한다.") @Test - void requestBy25LengthExceededDescription() throws Exception { + void requestBy40LengthExceededDescription() throws Exception { final String token = "Bearer " + tokenProvider.createToken(1L).getAccessToken(); final String content = objectMapper.writeValueAsString(new CreatingLinkRequest("링크", "일이삼사오육칠팔구십" From e16a944d85650d72ff8a20c16a703be046ae0f91 Mon Sep 17 00:00:00 2001 From: Donggyu <a29661498@gmail.com> Date: Fri, 12 Aug 2022 21:28:57 +0900 Subject: [PATCH 14/51] [BE] issue229 : refresh token (#257) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: Refresh Token 추가 * feat: 만료 토큰 예외 처리 및 액세스 토큰 재발급 구현 * feat: 토큰 재발급 요청 처리 구현 * feat: 리프래시 토큰 쿠키에 담기로 변경 * feat: RefreshToken을 통해 AccessToken 재발급 구현 * test: 테스트 수정 * feat: RefreshToken 저장 로직 추가 * feat: DB 검증 코드 추가 * fix: STRING 수정 * feat: index.adoc 수정 * docs: index.adoc 수정 * feat: Origin 설정 * feat: 로그아웃 기능 구현 * feat: 4001 코드 추가 & expiredTime 추가 * feat: 피드백 반영 및 application.yml 수정 * feat: 쿠키 sameSite 제거 및 Transactional 명시 * refactor: 테스트 코드 수정 * refactor: response DTO 네이밍 변경 * fix: INTERNAL_SERVER_ERROR -> UNAUTHORIZED 로 수정 * refactor: 메소드 AuthAcceptanceTest 로 내림 * fix: exposedHeaders "Set-Cookie" 추가 * Merge branch 'develop' of https://github.com/woowacourse-teams/2022-moamoa into feat/229-refresh-token # Conflicts: # backend/src/main/java/com/woowacourse/moamoa/auth/config/AuthRequestMatchConfig.java # backend/src/main/java/com/woowacourse/moamoa/auth/controller/AuthController.java # backend/src/main/java/com/woowacourse/moamoa/common/config/WebConfig.java # backend/src/test/java/com/woowacourse/moamoa/auth/controller/AuthControllerTest.java # backend/src/test/java/com/woowacourse/moamoa/auth/service/AuthServiceTest.java # backend/src/test/resources/schema.sql --- .../java/com/woowacourse/moamoa/common/config/WebConfig.java | 1 + .../moamoa/auth/controller/AuthControllerTest.java | 3 +-- .../com/woowacourse/moamoa/auth/service/AuthServiceTest.java | 5 +---- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/backend/src/main/java/com/woowacourse/moamoa/common/config/WebConfig.java b/backend/src/main/java/com/woowacourse/moamoa/common/config/WebConfig.java index 12a44b543..1e815d2c7 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/common/config/WebConfig.java +++ b/backend/src/main/java/com/woowacourse/moamoa/common/config/WebConfig.java @@ -27,6 +27,7 @@ public void addCorsMappings(final CorsRegistry registry) { .allowedOrigins("https://dev.moamoa.space", "https://moamoa.space") .allowedMethods(ALLOW_METHODS) .exposedHeaders(HttpHeaders.LOCATION) + .exposedHeaders("Set-Cookie") .allowCredentials(true); } diff --git a/backend/src/test/java/com/woowacourse/moamoa/auth/controller/AuthControllerTest.java b/backend/src/test/java/com/woowacourse/moamoa/auth/controller/AuthControllerTest.java index 1248762bf..f4b14e128 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/auth/controller/AuthControllerTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/auth/controller/AuthControllerTest.java @@ -7,9 +7,8 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import com.woowacourse.moamoa.WebMVCTest; -import com.woowacourse.moamoa.auth.service.AuthService; import com.woowacourse.moamoa.auth.service.response.TokensResponse; - +import com.woowacourse.moamoa.auth.service.AuthService; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.boot.test.mock.mockito.MockBean; diff --git a/backend/src/test/java/com/woowacourse/moamoa/auth/service/AuthServiceTest.java b/backend/src/test/java/com/woowacourse/moamoa/auth/service/AuthServiceTest.java index 3f84fa269..28106b3b5 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/auth/service/AuthServiceTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/auth/service/AuthServiceTest.java @@ -16,15 +16,12 @@ import com.woowacourse.moamoa.member.domain.repository.MemberRepository; import com.woowacourse.moamoa.member.query.MemberDao; import com.woowacourse.moamoa.member.service.MemberService; - import java.util.Optional; - import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.mock.mockito.MockBean; import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; @RepositoryTest class AuthServiceTest { From ebf97881c80897d5ecd641c413b11abb8e1c63cc Mon Sep 17 00:00:00 2001 From: TaeYoon <uni613@naver.com> Date: Sat, 13 Aug 2022 13:16:10 +0900 Subject: [PATCH 15/51] =?UTF-8?q?feat:=20accessToken=20=EC=84=B8=EC=85=98?= =?UTF-8?q?=20=EC=8A=A4=ED=86=A0=EB=A6=AC=EC=A7=80=EC=97=90=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5=20(#262)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/auth/accessToken.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/frontend/src/auth/accessToken.ts b/frontend/src/auth/accessToken.ts index 8f2948fbf..f4b64207a 100644 --- a/frontend/src/auth/accessToken.ts +++ b/frontend/src/auth/accessToken.ts @@ -5,15 +5,15 @@ import { API_ERROR } from '@constants'; import { deleteRefreshToken, getAccessToken } from '@api'; class AccessTokenController { - private static _accessToken: string | null = null; + private static ACCESS_TOKEN_KEY = 'accessToken'; private static _tokenExpiredMsTime: number = 20 * 60000; static setAccessToken(newAccessToken: string) { - this._accessToken = newAccessToken; + window.sessionStorage.setItem(this.ACCESS_TOKEN_KEY, newAccessToken); } static get accessToken() { - return this._accessToken; + return window.sessionStorage.getItem(this.ACCESS_TOKEN_KEY); } static setTokenExpiredMsTime(newTime: number) { @@ -25,17 +25,17 @@ class AccessTokenController { } static get hasAccessToken() { - return !!this._accessToken; + return !!this.accessToken; } static removeAccessToken() { - this._accessToken = null; + window.sessionStorage.removeItem(this.ACCESS_TOKEN_KEY); } private static async fetchLogout() { try { await deleteRefreshToken(); - AccessTokenController.removeAccessToken(); + this.removeAccessToken(); } catch (error) { alert('로그아웃에 실패했습니다. :('); window.location.reload(); From 77e67caa18da400281ecd44250a76b68213f217d Mon Sep 17 00:00:00 2001 From: airman5573 <68623798+airman5573@users.noreply.github.com> Date: Sat, 13 Aug 2022 14:35:27 +0900 Subject: [PATCH 16/51] =?UTF-8?q?[FE]=20issue256:=20atomic=20css=20?= =?UTF-8?q?=EB=B0=A9=EC=8B=9D=EC=9D=84=20css=20props=EC=97=90=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9=ED=95=98=EB=8A=94=20=EC=9C=A0=ED=8B=B8=20=ED=95=A8?= =?UTF-8?q?=EC=88=98=20=EC=83=9D=EC=84=B1=20(#259)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: tailwind 같은 atomic css util함수 생성 * feat: top right bottom left 추가 * refactor: css prop -> tw * refactor: 사용하지 않는 module import 제거 --- frontend/src/App.tsx | 14 +- .../markdown-render/MarkdownRender.tsx | 9 +- frontend/src/layout/footer/Footer.tsx | 12 +- .../components/category/Category.stories.tsx | 8 +- .../components/category/Category.tsx | 22 +- .../components/meta-box/MetaBox.stories.tsx | 8 +- .../components/publish/Publish.tsx | 8 +- .../components/subject/Subject.tsx | 10 +- .../StudyWideFloatBox.tsx | 22 +- .../pages/study-room-page/StudyRoomPage.tsx | 12 +- frontend/src/utils/tw.ts | 270 ++++++++++++++++++ frontend/tailwind.config.js | 29 ++ 12 files changed, 324 insertions(+), 100 deletions(-) create mode 100644 frontend/src/utils/tw.ts create mode 100644 frontend/tailwind.config.js diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 96b6198af..12fd499c1 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -9,10 +9,10 @@ import { } from '@pages'; import { Navigate, Route, Routes } from 'react-router-dom'; -import { css } from '@emotion/react'; - import { PATH } from '@constants'; +import tw from '@utils/tw'; + import { useAuth } from '@hooks/useAuth'; import { Footer, Header, Main } from '@layout'; @@ -22,15 +22,7 @@ const App = () => { return ( <div> - <Header - css={css` - position: fixed; - top: 0; - left: 0; - right: 0; - z-index: 2; - `} - /> + <Header css={tw`fixed top-0 left-0 right-0 z-2`} /> <Main> <Routes> <Route path={PATH.MAIN} element={<MainPage />} /> diff --git a/frontend/src/components/markdown-render/MarkdownRender.tsx b/frontend/src/components/markdown-render/MarkdownRender.tsx index 0217ade9a..6db275d87 100644 --- a/frontend/src/components/markdown-render/MarkdownRender.tsx +++ b/frontend/src/components/markdown-render/MarkdownRender.tsx @@ -4,6 +4,8 @@ import { useEffect, useRef } from 'react'; import { css } from '@emotion/react'; +import tw from '@utils/tw'; + import markdown from '@styles/markdown'; type MarkdownRenderProps = { @@ -20,12 +22,7 @@ const MarkdownRender = ({ markdownContent }: MarkdownRenderProps) => { }, [contentRef, markdownContent]); return ( - <div - css={css` - overflow-y: scroll; - height: 100%; - `} - > + <div css={tw`overflow-y-scroll h-full`}> <div css={css` ${markdown} diff --git a/frontend/src/layout/footer/Footer.tsx b/frontend/src/layout/footer/Footer.tsx index 175ed2a10..2c3711ca9 100644 --- a/frontend/src/layout/footer/Footer.tsx +++ b/frontend/src/layout/footer/Footer.tsx @@ -1,4 +1,4 @@ -import { css } from '@emotion/react'; +import tw from '@utils/tw'; import * as S from '@layout/footer/Footer.style'; @@ -7,15 +7,7 @@ export type FooterProps = { }; const Footer: React.FC<FooterProps> = ({ marginBottom }) => { - return ( - <S.Footer - css={css` - margin-bottom: ${marginBottom}; - `} - > - 그린론 디우 베루스 병민 짱구 태태 - </S.Footer> - ); + return <S.Footer css={tw`mb-[${marginBottom}]`}>그린론 디우 베루스 병민 짱구 태태</S.Footer>; }; export default Footer; diff --git a/frontend/src/pages/create-study-page/components/category/Category.stories.tsx b/frontend/src/pages/create-study-page/components/category/Category.stories.tsx index 9b7e9d74e..55f63cd93 100644 --- a/frontend/src/pages/create-study-page/components/category/Category.stories.tsx +++ b/frontend/src/pages/create-study-page/components/category/Category.stories.tsx @@ -1,6 +1,6 @@ import type { Story } from '@storybook/react'; -import { css } from '@emotion/react'; +import tw from '@utils/tw'; import Category from '@create-study-page/components/category/Category'; import type { CategoryProps } from '@create-study-page/components/category/Category'; @@ -11,11 +11,7 @@ export default { }; const Template: Story<CategoryProps> = props => ( - <div - css={css` - max-width: 400px; - `} - > + <div css={tw`max-w-[400px]`}> <Category {...props} /> </div> ); diff --git a/frontend/src/pages/create-study-page/components/category/Category.tsx b/frontend/src/pages/create-study-page/components/category/Category.tsx index 6c6673021..2800cd531 100644 --- a/frontend/src/pages/create-study-page/components/category/Category.tsx +++ b/frontend/src/pages/create-study-page/components/category/Category.tsx @@ -1,4 +1,4 @@ -import { css } from '@emotion/react'; +import tw from '@utils/tw'; import type { Tag } from '@custom-types'; @@ -58,27 +58,11 @@ const Category = ({ className }: CategoryProps) => { <S.Area> <S.Label>영역 :</S.Label> <S.AreaCheckboxContainer> - <Checkbox - css={css` - margin-right: 4px; - `} - type="checkbox" - id="area-fe" - data-tagid={areaFE.id} - {...register('area-fe')} - /> + <Checkbox css={tw`mr-4`} type="checkbox" id="area-fe" data-tagid={areaFE.id} {...register('area-fe')} /> <label htmlFor="area-fe">FE</label> </S.AreaCheckboxContainer> <S.AreaCheckboxContainer> - <Checkbox - css={css` - margin-right: 4px; - `} - type="checkbox" - id="area-be" - data-tagid={areaBE.id} - {...register('area-be')} - /> + <Checkbox css={tw`mr-4`} type="checkbox" id="area-be" data-tagid={areaBE.id} {...register('area-be')} /> <label htmlFor="area-be">BE</label> </S.AreaCheckboxContainer> </S.Area> diff --git a/frontend/src/pages/create-study-page/components/meta-box/MetaBox.stories.tsx b/frontend/src/pages/create-study-page/components/meta-box/MetaBox.stories.tsx index f1615e1c6..e8d0f17f0 100644 --- a/frontend/src/pages/create-study-page/components/meta-box/MetaBox.stories.tsx +++ b/frontend/src/pages/create-study-page/components/meta-box/MetaBox.stories.tsx @@ -1,6 +1,6 @@ import type { Story } from '@storybook/react'; -import { css } from '@emotion/react'; +import tw from '@utils/tw'; import MetaBox from '@create-study-page/components/meta-box/MetaBox'; @@ -10,11 +10,7 @@ export default { }; const Template: Story = () => ( - <MetaBox - css={css` - max-width: 300px; - `} - > + <MetaBox css={tw`max-w-[300px]`}> <MetaBox.Title>스터디 인원</MetaBox.Title> <MetaBox.Content>Content입니다</MetaBox.Content> </MetaBox> diff --git a/frontend/src/pages/create-study-page/components/publish/Publish.tsx b/frontend/src/pages/create-study-page/components/publish/Publish.tsx index dcc97c767..035c9ed95 100644 --- a/frontend/src/pages/create-study-page/components/publish/Publish.tsx +++ b/frontend/src/pages/create-study-page/components/publish/Publish.tsx @@ -1,4 +1,4 @@ -import { css } from '@emotion/react'; +import tw from '@utils/tw'; import Button from '@components/button/Button'; @@ -17,11 +17,7 @@ const Publish = ({ className, onPublishButtonClick: handlePublishButtonClick }: <MetaBox.Title>스터디 개설</MetaBox.Title> <MetaBox.Content> <Button - css={css` - border-radius: 6px; - font-size: 16px; - padding: 12px 10px; - `} + css={tw`rounded-[6px] text-16 py-12 px-10`} fluid={true} onClick={handlePublishButtonClick} outline={true} diff --git a/frontend/src/pages/create-study-page/components/subject/Subject.tsx b/frontend/src/pages/create-study-page/components/subject/Subject.tsx index 15d5f3a7c..2fc79173c 100644 --- a/frontend/src/pages/create-study-page/components/subject/Subject.tsx +++ b/frontend/src/pages/create-study-page/components/subject/Subject.tsx @@ -1,4 +1,4 @@ -import { css } from '@emotion/react'; +import tw from '@utils/tw'; import { useFormContext } from '@hooks/useForm'; @@ -24,13 +24,7 @@ const Subject = ({ className }: SubjectProps) => { const subjects = tags.filter(({ category }) => category.name === 'subject'); return ( - <S.Select - id="subject-list" - css={css` - width: 100%; - `} - {...register('subject')} - > + <S.Select id="subject-list" css={tw`w-full`} {...register('subject')}> {subjects.map(({ id, name, description }) => ( <option selected={name === 'Etc'} key={id} value={id}> {description} diff --git a/frontend/src/pages/detail-page/components/study-wide-float-box/StudyWideFloatBox.tsx b/frontend/src/pages/detail-page/components/study-wide-float-box/StudyWideFloatBox.tsx index b3cb00b0d..19d6328a9 100644 --- a/frontend/src/pages/detail-page/components/study-wide-float-box/StudyWideFloatBox.tsx +++ b/frontend/src/pages/detail-page/components/study-wide-float-box/StudyWideFloatBox.tsx @@ -1,10 +1,9 @@ import { Link } from 'react-router-dom'; -import { css } from '@emotion/react'; - import { PATH } from '@constants'; import { yyyymmddTommdd } from '@utils'; +import tw from '@utils/tw'; import type { StudyDetail, UserRole } from '@custom-types'; @@ -56,14 +55,7 @@ const StudyWideFloatBox: React.FC<StudyWideFloatBoxProps> = ({ if (userRole === 'MEMBER' || userRole === 'OWNER') { return ( <Link to={PATH.STUDY_ROOM(studyId)}> - <Button - css={css` - height: 100%; - padding: 0 20px; - `} - fluid={true} - type="button" - > + <Button css={tw`h-full py-0 px-20`} fluid={true} type="button"> 이동하기 </Button> </Link> @@ -71,15 +63,7 @@ const StudyWideFloatBox: React.FC<StudyWideFloatBoxProps> = ({ } return ( - <Button - css={css` - height: 100%; - padding: 0 20px; - `} - fluid={true} - disabled={!isOpen} - onClick={handleRegisterButtonClick} - > + <Button css={tw`h-full py-0 px-20`} fluid={true} disabled={!isOpen} onClick={handleRegisterButtonClick}> {isOpen ? '가입하기' : '모집 마감'} </Button> ); diff --git a/frontend/src/pages/study-room-page/StudyRoomPage.tsx b/frontend/src/pages/study-room-page/StudyRoomPage.tsx index d3f072085..0c7d088fd 100644 --- a/frontend/src/pages/study-room-page/StudyRoomPage.tsx +++ b/frontend/src/pages/study-room-page/StudyRoomPage.tsx @@ -1,9 +1,9 @@ import { Navigate } from 'react-router-dom'; -import { css } from '@emotion/react'; - import { PATH } from '@constants'; +import tw from '@utils/tw'; + import Wrapper from '@components/wrapper/Wrapper'; import * as S from '@study-room-page/StudyRoomPage.style'; @@ -28,13 +28,7 @@ const StudyRoomPage: React.FC = () => { <Wrapper> <S.Container> <SideMenu - css={css` - position: sticky; - top: 100px; - left: 0; - - align-self: flex-start; - `} + css={tw`sticky top-100 left-0 self-start`} activeTabId={activeTab.id} tabs={tabs} onTabButtonClick={handleTabButtonClick} diff --git a/frontend/src/utils/tw.ts b/frontend/src/utils/tw.ts new file mode 100644 index 000000000..2bcc1a374 --- /dev/null +++ b/frontend/src/utils/tw.ts @@ -0,0 +1,270 @@ +// quick style sheet +import { css } from '@emotion/react'; + +const arr0_to_100 = [...Array(101).keys()]; +const arr0_to_300 = [...Array(301).keys()]; + +const layout = { + block: 'display: block', + 'inline-block': 'display: inline-block', + inline: 'display: inline', + flex: 'display: flex', + 'inline-flex': 'display: inline-flex', + table: 'display: table', + 'inline-table': 'display: inline-table', + 'table-caption': 'display: table-caption', + 'table-cell': 'display: table-cell', + 'table-column': 'display: table-column', + 'table-column-group': 'display: table-column-group', + 'table-footer-group': 'display: table-footer-group', + 'table-header-group': 'display: table-header-group', + 'table-row-group': 'display: table-row-group', + 'table-row': 'display: table-row', + 'flow-root': 'display: flow-root', + grid: 'display: grid', + 'inline-grid': 'display: inline-grid', + contents: 'display: contents', + 'list-item': 'display: list-item', + hidden: 'display: none', +}; + +const flex = { + 'flex-1': 'flex: 1 1 0%', + 'flex-auto': 'flex: 1 1 auto', + 'flex-initial': 'flex: 0 1 auto', + 'flex-none': 'flex: none', +}; + +const justifyContent = { + 'justify-start': 'justify-content: flex-start', + 'justify-end': 'justify-content: flex-end', + 'justify-center': 'justify-content: center', + 'justify-between': 'justify-content: space-between', + 'justify-around': 'justify-content: space-around', + 'justify-evenly': 'justify-content: space-evenly', +}; + +const alignItems = { + 'items-start': 'align-items: flex-start', + 'items-end': 'align-items: flex-end', + 'items-center': 'align-items: center', + 'items-baseline': 'align-items: baseline', + 'items-stretch': 'align-items: stretch', +}; + +const alignSelf = { + 'self-auto': 'align-self: auto', + 'self-start': 'align-self: flex-start', + 'self-end': 'align-self: flex-end', + 'self-center': 'align-self: center', + 'self-stretch': 'align-self: stretch', + 'self-baseline': 'align-self: baseline', +}; + +const margin = arr0_to_100.reduce((acc, cur) => { + acc[`mt-${cur}`] = `margin-top: ${cur}px`; + acc[`mr-${cur}`] = `margin-right: ${cur}px`; + acc[`mb-${cur}`] = `margin-bottom: ${cur}px`; + acc[`ml-${cur}`] = `margin-left: ${cur}px`; + return acc; +}, {} as Record<string, string>); + +const padding = arr0_to_100.reduce((acc, cur) => { + acc[`p-${cur}`] = `padding-top: ${cur}px; padding-right: ${cur}px; padding-bottom: ${cur}px; padding-left: ${cur}px`; + acc[`py-${cur}`] = `padding-top: ${cur}px; padding-bottom: ${cur}px`; + acc[`px-${cur}`] = `padding-left: ${cur}px; padding-right: ${cur}px`; + + acc[`pt-${cur}`] = `padding-top: ${cur}px`; + acc[`pr-${cur}`] = `padding-right: ${cur}px`; + acc[`pb-${cur}`] = `padding-bottom: ${cur}px`; + acc[`pl-${cur}`] = `padding-left: ${cur}px`; + return acc; +}, {} as Record<string, string>); + +const widthPx = arr0_to_300.reduce((acc, cur) => { + acc[`w-${cur}`] = `width: ${cur}px`; + acc[`max-w-${cur}`] = `max-width: ${cur}px`; + return acc; +}, {} as Record<string, string>); + +const width = { + 'w-full': 'width: 100%', +}; + +const heightPx = arr0_to_300.reduce((acc, cur) => { + acc[`h-${cur}px`] = `height: ${cur}px`; + acc[`max-h-${cur}px`] = `max-height: ${cur}px`; + return acc; +}, {} as Record<string, string>); + +const height = { + 'h-full': 'height: 100%', +}; + +const position = { + static: 'position: static', + fixed: 'position: fixed', + absolute: 'position: absolute', + relative: 'position: relative', + sticky: 'position: sticky', +}; + +const trbl = arr0_to_100.reduce((acc, cur) => { + acc[`top-${cur}`] = `top: ${cur}px`; + acc[`right-${cur}`] = `right: ${cur}px`; + acc[`bottom-${cur}`] = `bottom: ${cur}px`; + acc[`left-${cur}`] = `left: ${cur}px`; + return acc; +}, {} as Record<string, string>); + +const zIndex = arr0_to_100.reduce((acc, cur) => { + acc[`z-${cur}`] = `z-index: ${cur}`; + return acc; +}, {} as Record<string, string>); + +const overflow = { + 'overflow-auto': 'overflow: auto', + 'overflow-hidden': 'overflow: hidden', + 'overflow-clip': 'overflow: clip', + 'overflow-visible': 'overflow: visible', + 'overflow-scroll': 'overflow: scroll', + 'overflow-x-auto': 'overflow-x: auto', + 'overflow-y-auto': 'overflow-y: auto', + 'overflow-x-hidden': 'overflow-x: hidden', + 'overflow-y-hidden': 'overflow-y: hidden', + 'overflow-x-clip': 'overflow-x: clip', + 'overflow-y-clip': 'overflow-y: clip', + 'overflow-x-visible': 'overflow-x: visible', + 'overflow-y-visible': 'overflow-y: visible', + 'overflow-x-scroll': 'overflow-x: scroll', + 'overflow-y-scroll': 'overflow-y: scroll', +}; + +const textAlign = { + 'text-left': 'text-align: left', + 'text-center': 'text-align: center', + 'text-right': 'text-align: right', + 'text-justify': 'text-align: justify', + 'text-start': 'text-align: start', + 'text-end': 'text-align: end', +}; + +const borderRadius = arr0_to_100.reduce((acc, cur) => { + acc[`rounded-${cur}`] = `border-radius: ${cur}px`; + acc[`rounded-t-${cur}`] = `border-top-left-radius: ${cur}px; border-top-right-radius: ${cur}px`; + acc[`rounded-r-${cur}`] = `border-top-right-radius: ${cur}px; border-bottom-right-radius: ${cur}px`; + acc[`rounded-b-${cur}`] = `border-bottom-right-radius: ${cur}px; border-bottom-left-radius: ${cur}px`; + acc[`rounded-l-${cur}`] = `border-top-left-radius: ${cur}px; border-bottom-left-radius: ${cur}px`; + + return acc; +}, {} as Record<string, string>); + +const fontSize = arr0_to_100.reduce((acc, cur) => { + acc[`text-${cur}`] = `font-size: ${cur}px`; + return acc; +}, {} as Record<string, string>); + +const totalStyles = { + ...layout, + ...flex, + ...justifyContent, + ...alignItems, + ...alignSelf, + ...margin, + ...padding, + ...widthPx, + ...width, + ...heightPx, + ...height, + ...position, + ...trbl, + ...zIndex, + ...overflow, + ...textAlign, + ...borderRadius, + ...fontSize, +}; + +const cssPropertyForArbitararyValue = { + // margin + mt: ['margin-top'], + mr: ['margin-right'], + mb: ['margin-bottom'], + ml: ['margin-left'], + m: ['margin-top', 'margin-right', 'margin-bottom', 'margin-left'], + my: ['margin-top', 'margin-bottom'], + mx: ['margin-left', 'margin-right'], + + // padding + pt: ['padding-top'], + pr: ['padding-right'], + pb: ['padding-bottom'], + pl: ['padding-left'], + p: ['padding-top', 'padding-right', 'padding-bottom', 'padding-left'], + py: ['padding-top', 'padding-bottom'], + px: ['padding-left', 'padding-right'], + + // size + h: ['height'], + w: ['width'], + 'max-w': ['max-width'], + 'min-w': ['min-width'], + + // border + rounded: ['border-radius'], + + // typo + text: ['font-size'], +}; + +type styleKey = keyof typeof totalStyles; +type cssShortPropertyKey = keyof typeof cssPropertyForArbitararyValue; + +const getArbitararyValue = (str: string) => { + return str.substring(str.indexOf('[') + 1, str.indexOf(']')); +}; + +// TODO: Test 코드 만들자 +const combine = (template: TemplateStringsArray, args: Array<string>): string => { + const result = []; + for (let i = 0; i < template.length - 1; i += 1) { + result.push(template[i]); + result.push(args[i]); + } + result.push(template[template.length - 1]); + return result.join(''); +}; + +// TODO: Test 코드 만들다 +const tw = (template: TemplateStringsArray, ...args: Array<string>) => { + let templateStr = template[0]; + if (template.length > 1) templateStr = combine(template, args); + + const styles = templateStr.split(' '); + const styleText = styles + .map(s => { + const arbitararyValue = getArbitararyValue(s); + + if (arbitararyValue) { + // py-[12px]에서 ['py-', '[12px]']로 짜르고 마지막 요소를 빼낸다 + const shortCssProperty = s.split('-').slice(0, -1)[0]; + const fullCssProperties = cssPropertyForArbitararyValue[shortCssProperty as cssShortPropertyKey]; + const cssTexts = fullCssProperties.map(fullCssProp => { + return `${fullCssProp}: ${arbitararyValue}`; + }); + return cssTexts.join(';'); + } + if (!(s in totalStyles)) { + throw new Error('올바른 스타일을 입력해 주세요'); + } + return totalStyles[s as styleKey]; + }) + .join(';'); + + // 마지막 ;도 중요하다! + return css` + ${styleText}; + `; +}; + +export default tw; diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js new file mode 100644 index 000000000..282e204f5 --- /dev/null +++ b/frontend/tailwind.config.js @@ -0,0 +1,29 @@ +/** @type {import('tailwindcss').Config} */ + +const arr0_to_100 = [...Array(101).keys()]; +const px0_50 = { ...Array.from(Array(51)).map((_, i) => `${i}px`) }; +const px0_100 = { ...Array.from(Array(101)).map((_, i) => `${i}px`) }; +const px0_500 = { ...Array.from(Array(501)).map((_, i) => `${i}px`) }; + +module.exports = { + content: ['src/components/*.tsx', 'src/pages/*.tsx'], + theme: { + extend: { + borderWidth: px0_50, + fontSize: px0_100, + lineHeight: px0_100, + width: px0_500, + maxWidth: px0_500, + minWidth: px0_500, + maxHeight: px0_500, + minHeight: px0_500, + spacing: px0_100, + zIndex: arr0_to_100, + top: arr0_to_100, + right: arr0_to_100, + bottom: arr0_to_100, + left: arr0_to_100, + }, + }, + plugins: [], +}; From 3194f22cd3c1462feebf3f851e1275f933d391f8 Mon Sep 17 00:00:00 2001 From: Donggyu <a29661498@gmail.com> Date: Sun, 14 Aug 2022 19:45:31 +0900 Subject: [PATCH 17/51] =?UTF-8?q?feat:=20Interceptor=20refresh=20=EC=9A=94?= =?UTF-8?q?=EC=B2=AD=20=EC=A0=9C=EC=99=B8=20(#267)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../moamoa/auth/controller/AuthenticationInterceptor.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/backend/src/main/java/com/woowacourse/moamoa/auth/controller/AuthenticationInterceptor.java b/backend/src/main/java/com/woowacourse/moamoa/auth/controller/AuthenticationInterceptor.java index 51398e34c..92b4d728b 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/auth/controller/AuthenticationInterceptor.java +++ b/backend/src/main/java/com/woowacourse/moamoa/auth/controller/AuthenticationInterceptor.java @@ -2,7 +2,6 @@ import com.woowacourse.moamoa.auth.config.AuthenticationExtractor; import com.woowacourse.moamoa.auth.controller.matcher.AuthenticationRequestMatcher; -import com.woowacourse.moamoa.auth.controller.matcher.AuthenticationRequestMatcherBuilder; import com.woowacourse.moamoa.auth.infrastructure.TokenProvider; import com.woowacourse.moamoa.common.exception.UnauthorizedException; @@ -30,7 +29,7 @@ public boolean preHandle(final HttpServletRequest request, final HttpServletResp if (authenticationRequestMatcher.isRequiredAuth(request)) { final String token = AuthenticationExtractor.extract(request); - validateToken(token); + validateToken(token, request.getRequestURI()); request.setAttribute("payload", tokenProvider.getPayload(token)); } @@ -42,7 +41,10 @@ private boolean isPreflight(HttpServletRequest request) { return HttpMethod.OPTIONS.matches(request.getMethod()); } - private void validateToken(String token) { + private void validateToken(String token, String requestURI) { + if (requestURI.equals("/api/auth/refresh") && token != null) { + return; + } if (token == null || !tokenProvider.validateToken(token)) { throw new UnauthorizedException("유효하지 않은 토큰입니다."); } From 2423233dc9a00229f2c697a079cc5a9c7175c6b0 Mon Sep 17 00:00:00 2001 From: jaejae-yoo <wotj102@gmail.com> Date: Tue, 16 Aug 2022 16:13:04 +0900 Subject: [PATCH 18/51] =?UTF-8?q?feat:=20sonarqube=20=EC=A0=81=EC=9A=A9?= =?UTF-8?q?=EC=9D=84=20=EC=9C=84=ED=95=9C=20build.gradle=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20Co-authored-by:=20sc0116=20<ssc6839@gmail.com>?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/build.gradle | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/backend/build.gradle b/backend/build.gradle index 0b175bac7..f30e04f4c 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -1,4 +1,6 @@ plugins { + id 'org.sonarqube' version '3.3' + id 'jacoco' id 'org.springframework.boot' version '2.6.9' id 'io.spring.dependency-management' version '1.0.11.RELEASE' id 'java' @@ -39,6 +41,8 @@ dependencies { asciidoctorExtensions 'org.springframework.restdocs:spring-restdocs-asciidoctor' testImplementation 'org.springframework.restdocs:spring-restdocs-restassured' + + implementation 'org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:3.3' } ext { @@ -50,6 +54,19 @@ tasks.named('test') { useJUnitPlatform() systemProperties = System.getProperties() finalizedBy 'asciidoctor' + finalizedBy 'jacocoTestReport' +} + +jacoco { + toolVersion = "0.8.7" +} + +jacocoTestReport { + reports { + xml.enabled true + csv.enabled false + html.enabled false + } } asciidoctor.doFirst { @@ -79,3 +96,17 @@ task moveDocument(type: Copy) { bootJar { dependsOn moveDocument } + +sonarqube { + properties { + property "sonar.host.url", "https://dev.moamoa.space" + property "sonar.projectKey", "moamoa" + property "sonar.sources", "src" + property "sonar.language", "java" + property "sonar.sourceEncoding", "UTF-8" + property "sonar.profile", "Sonar way" + property "sonar.java.binaries", "${buildDir}/classes" + property "sonar.test.inclusions", "**/*Test.java" + property "sonar.coverage.jacoco.xmlReportPaths", "${buildDir}/reports/jacoco/test/jacocoTestReport.xml" + } +} \ No newline at end of file From 9d2360c06588bcc427fe4b71f1b4e7cf3f025c61 Mon Sep 17 00:00:00 2001 From: Donggyu <a29661498@gmail.com> Date: Tue, 16 Aug 2022 16:31:48 +0900 Subject: [PATCH 19/51] =?UTF-8?q?[BE]=20issue241:=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EB=A6=AC=ED=8C=A9=ED=86=A0?= =?UTF-8?q?=EB=A7=81=20(#242)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: ReviewDaoTest 리팩토링 * refactor: createdDate 수정 및 schema 변경 * refactor: ReviewControllerTest 리팩토링 * refactor: MemberData Fixture 사용 * refactor: MyStudyControllerTest 리팩토링 * refactor: MyStudyControllerTest 리팩토링 완료 * refactor: MyStudyDaoTest * refactor: MyStudyDaoTest 수정 중 * refactor: MyStudyDaoTest 완료 * fix: 깨지는 테스트 수정 * fix: id 값은 알 수 없으므로 null만 검사하도록 수정 * refactor: MemberFixtures에서 Member 상수 -> 메서드로 변경 * refactor: ReviewFixtures에서 Review 상수 -> 메서드로 변경 * refactor: StudyDetailsDaoTest 리팩토링 * refactor: MyStudyServiceTest 리팩토링 * refactor: 불필요한 StudyFixtures 제거 * refactor: TagDaoTest 수정 * refactor: ReviewDaoTest 리팩토링 * refactor: 사용하지 않는 Fixture 제거 * fix: ReviewDaoTest 수정 * feat: 스터디 최대 인원이 한명인 경우 바로 모집 종료가 된다. * fix: 테스트 코드 통과하도록 수정 * refactor: 린론 피드백 반영 * refactor: DTO 검증문 추가 * fix: 테스트 수정 * refactor: 픽스쳐 추가 Co-authored-by: SeungCheol <ssc6839@gmail.com> --- .../moamoa/study/domain/AttachedTags.java | 2 + .../moamoa/study/domain/Participants.java | 2 +- .../moamoa/study/domain/RecruitPlanner.java | 8 +- .../moamoa/study/domain/StudyPlanner.java | 2 + .../moamoa/study/service/StudyService.java | 1 + .../service/request/CreatingStudyRequest.java | 13 +- .../test/review/ReviewsAcceptanceTest.java | 2 +- .../test/tag/TagAcceptanceTest.java | 3 - .../moamoa/fixtures/AuthFixtures.java | 6 - .../moamoa/fixtures/MemberFixtures.java | 32 ++- .../moamoa/fixtures/ReviewFixtures.java | 67 +++--- .../moamoa/fixtures/StudyFixtures.java | 217 +++++++++++++----- .../repository/MemberRepositoryTest.java | 1 - .../ReferenceRoomControllerTest.java | 12 +- .../SearchingReferenceRoomControllerTest.java | 16 +- .../referenceroom/query/LinkDaoTest.java | 10 +- .../controller/ReviewControllerTest.java | 31 ++- .../SearchingReviewControllerTest.java | 47 ++-- .../moamoa/review/query/ReviewDaoTest.java | 133 +++++------ .../controller/MyStudyControllerTest.java | 197 +++++++++------- .../SearchingStudyControllerTest.java | 32 +-- .../study/controller/StudyControllerTest.java | 30 +++ .../moamoa/study/query/MyStudyDaoTest.java | 150 +++++++----- .../study/query/StudyDetailsDaoTest.java | 126 +++++----- .../study/query/StudySummaryDaoTest.java | 8 - .../study/service/MyStudyServiceTest.java | 161 +++++++------ .../SearchingTagControllerTest.java | 2 - .../moamoa/tag/query/TagDaoTest.java | 86 ++++--- backend/src/test/resources/schema.sql | 4 +- 29 files changed, 794 insertions(+), 607 deletions(-) delete mode 100644 backend/src/test/java/com/woowacourse/moamoa/fixtures/AuthFixtures.java diff --git a/backend/src/main/java/com/woowacourse/moamoa/study/domain/AttachedTags.java b/backend/src/main/java/com/woowacourse/moamoa/study/domain/AttachedTags.java index 4cbfa74e4..c9c0fc2af 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/study/domain/AttachedTags.java +++ b/backend/src/main/java/com/woowacourse/moamoa/study/domain/AttachedTags.java @@ -7,7 +7,9 @@ import javax.persistence.ElementCollection; import javax.persistence.Embeddable; import javax.persistence.JoinColumn; +import lombok.Getter; +@Getter @Embeddable public class AttachedTags { diff --git a/backend/src/main/java/com/woowacourse/moamoa/study/domain/Participants.java b/backend/src/main/java/com/woowacourse/moamoa/study/domain/Participants.java index 4130fc450..3603be43d 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/study/domain/Participants.java +++ b/backend/src/main/java/com/woowacourse/moamoa/study/domain/Participants.java @@ -57,7 +57,7 @@ boolean isOwner(Long memberId) { return ownerId.equals(memberId); } - int getSize() { + public int getSize() { return size; } diff --git a/backend/src/main/java/com/woowacourse/moamoa/study/domain/RecruitPlanner.java b/backend/src/main/java/com/woowacourse/moamoa/study/domain/RecruitPlanner.java index 9c310b39f..e3e5b84e9 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/study/domain/RecruitPlanner.java +++ b/backend/src/main/java/com/woowacourse/moamoa/study/domain/RecruitPlanner.java @@ -9,8 +9,10 @@ import javax.persistence.Column; import javax.persistence.Embeddable; import javax.persistence.Enumerated; +import lombok.Getter; import lombok.NoArgsConstructor; +@Getter @Embeddable @NoArgsConstructor(access = PROTECTED) public class RecruitPlanner { @@ -55,7 +57,7 @@ void closeRecruiting() { recruitStatus = RECRUITMENT_END; } - LocalDate getEnrollmentEndDate() { + public LocalDate getEnrollmentEndDate() { return enrollmentEndDate; } @@ -73,8 +75,4 @@ int getCapacity() { boolean hasCapacity() { return max != null; } - - public RecruitStatus getRecruitStatus() { - return recruitStatus; - } } diff --git a/backend/src/main/java/com/woowacourse/moamoa/study/domain/StudyPlanner.java b/backend/src/main/java/com/woowacourse/moamoa/study/domain/StudyPlanner.java index 2116707da..f3148c08d 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/study/domain/StudyPlanner.java +++ b/backend/src/main/java/com/woowacourse/moamoa/study/domain/StudyPlanner.java @@ -12,8 +12,10 @@ import javax.persistence.Embeddable; import javax.persistence.Enumerated; import lombok.EqualsAndHashCode; +import lombok.Getter; import lombok.NoArgsConstructor; +@Getter @Embeddable @EqualsAndHashCode @NoArgsConstructor(access = PROTECTED) diff --git a/backend/src/main/java/com/woowacourse/moamoa/study/service/StudyService.java b/backend/src/main/java/com/woowacourse/moamoa/study/service/StudyService.java index 6b3434306..23fe93b6a 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/study/service/StudyService.java +++ b/backend/src/main/java/com/woowacourse/moamoa/study/service/StudyService.java @@ -42,6 +42,7 @@ public Study createStudy(final Long githubId, final CreatingStudyRequest request final Participants participants = request.mapToParticipants(owner.getId()); final RecruitPlanner recruitPlanner = request.mapToRecruitPlan(); + final StudyPlanner studyPlanner = request.mapToStudyPlanner(createdAt.toLocalDate()); final AttachedTags attachedTags = request.mapToAttachedTags(); final Content content = request.mapToContent(); diff --git a/backend/src/main/java/com/woowacourse/moamoa/study/service/request/CreatingStudyRequest.java b/backend/src/main/java/com/woowacourse/moamoa/study/service/request/CreatingStudyRequest.java index 74e62242d..b1266a0c2 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/study/service/request/CreatingStudyRequest.java +++ b/backend/src/main/java/com/woowacourse/moamoa/study/service/request/CreatingStudyRequest.java @@ -1,5 +1,7 @@ package com.woowacourse.moamoa.study.service.request; +import static com.woowacourse.moamoa.study.domain.RecruitStatus.RECRUITMENT_END; +import static com.woowacourse.moamoa.study.domain.RecruitStatus.RECRUITMENT_START; import static com.woowacourse.moamoa.study.domain.StudyStatus.IN_PROGRESS; import static com.woowacourse.moamoa.study.domain.StudyStatus.PREPARE; @@ -7,9 +9,8 @@ import com.woowacourse.moamoa.study.domain.AttachedTags; import com.woowacourse.moamoa.study.domain.Content; import com.woowacourse.moamoa.study.domain.Participants; -import com.woowacourse.moamoa.study.domain.StudyPlanner; import com.woowacourse.moamoa.study.domain.RecruitPlanner; -import com.woowacourse.moamoa.study.domain.RecruitStatus; +import com.woowacourse.moamoa.study.domain.StudyPlanner; import java.time.LocalDate; import java.util.List; import java.util.stream.Collectors; @@ -64,7 +65,7 @@ public String getEndDate() { } public String getMaxMemberCount() { - return maxMemberCount == null ? "" : String.valueOf(maxMemberCount); + return maxMemberCount == null ? null : String.valueOf(maxMemberCount); } public String getEnrollmentEndDate() { @@ -87,7 +88,11 @@ public Participants mapToParticipants(Long ownerId) { } public RecruitPlanner mapToRecruitPlan() { - return new RecruitPlanner(maxMemberCount, RecruitStatus.RECRUITMENT_START, enrollmentEndDate); + if (maxMemberCount != null && maxMemberCount == 1) { + return new RecruitPlanner(maxMemberCount, RECRUITMENT_END, enrollmentEndDate); + } + + return new RecruitPlanner(maxMemberCount, RECRUITMENT_START, enrollmentEndDate); } public AttachedTags mapToAttachedTags() { diff --git a/backend/src/test/java/com/woowacourse/acceptance/test/review/ReviewsAcceptanceTest.java b/backend/src/test/java/com/woowacourse/acceptance/test/review/ReviewsAcceptanceTest.java index 6092f6ed0..dbf4cec3d 100644 --- a/backend/src/test/java/com/woowacourse/acceptance/test/review/ReviewsAcceptanceTest.java +++ b/backend/src/test/java/com/woowacourse/acceptance/test/review/ReviewsAcceptanceTest.java @@ -161,7 +161,7 @@ public void getReviewsBySize() { final ReviewResponse 베루스_리뷰 = new ReviewResponse(베루스_리뷰_ID, 베루스, 리뷰_생성일, 리뷰_수정일, "리뷰 내용4"); assertThat(reviewsResponse.getTotalCount()).isEqualTo(4); - assertThat(reviewsResponse.getReviews()).containsExactly(베루스_리뷰, 디우_리뷰); + assertThat(reviewsResponse.getReviews()).containsExactlyInAnyOrder(디우_리뷰, 베루스_리뷰); } @DisplayName("자신이 참여한 스터디에 작성한 리뷰를 삭제할 수 있다.") diff --git a/backend/src/test/java/com/woowacourse/acceptance/test/tag/TagAcceptanceTest.java b/backend/src/test/java/com/woowacourse/acceptance/test/tag/TagAcceptanceTest.java index a7bb614ca..deb6b889c 100644 --- a/backend/src/test/java/com/woowacourse/acceptance/test/tag/TagAcceptanceTest.java +++ b/backend/src/test/java/com/woowacourse/acceptance/test/tag/TagAcceptanceTest.java @@ -8,12 +8,9 @@ import com.woowacourse.acceptance.AcceptanceTest; import io.restassured.RestAssured; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; -import org.springframework.jdbc.core.JdbcTemplate; public class TagAcceptanceTest extends AcceptanceTest { diff --git a/backend/src/test/java/com/woowacourse/moamoa/fixtures/AuthFixtures.java b/backend/src/test/java/com/woowacourse/moamoa/fixtures/AuthFixtures.java deleted file mode 100644 index de99710a8..000000000 --- a/backend/src/test/java/com/woowacourse/moamoa/fixtures/AuthFixtures.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.woowacourse.moamoa.fixtures; - -public class AuthFixtures { - - public static final String JWT_토큰 = "header.payload.signature"; -} diff --git a/backend/src/test/java/com/woowacourse/moamoa/fixtures/MemberFixtures.java b/backend/src/test/java/com/woowacourse/moamoa/fixtures/MemberFixtures.java index 62cced407..712372137 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/fixtures/MemberFixtures.java +++ b/backend/src/test/java/com/woowacourse/moamoa/fixtures/MemberFixtures.java @@ -11,7 +11,6 @@ public class MemberFixtures { public static final String 짱구_유저네임 = "jjanggu"; public static final String 짱구_이미지 = "https://jjanggu.png"; public static final String 짱구_프로필 = "https://jjanggu.com"; - public static final Member 짱구 = new Member(짱구_깃허브_아이디, 짱구_유저네임, 짱구_이미지, 짱구_프로필); public static final MemberData 짱구_응답 = new MemberData(짱구_깃허브_아이디, 짱구_유저네임, 짱구_이미지, 짱구_프로필); /* 그린론 */ @@ -20,7 +19,6 @@ public class MemberFixtures { public static final String 그린론_유저네임 = "greenlawn"; public static final String 그린론_이미지 = "https://greenlawn.png"; public static final String 그린론_프로필 = "https://greenlawn.com"; - public static final Member 그린론 = new Member(그린론_깃허브_아이디, 그린론_유저네임, 그린론_이미지, 그린론_프로필); public static final MemberData 그린론_응답 = new MemberData(그린론_깃허브_아이디, 그린론_유저네임, 그린론_이미지, 그린론_프로필); /* 디우 */ @@ -29,7 +27,6 @@ public class MemberFixtures { public static final String 디우_유저네임 = "dwoo"; public static final String 디우_이미지 = "https://dwoo.png"; public static final String 디우_프로필 = "https://dwoo.com"; - public static final Member 디우 = new Member(디우_깃허브_아이디, 디우_유저네임, 디우_이미지, 디우_프로필); public static final MemberData 디우_응답 = new MemberData(디우_깃허브_아이디, 디우_유저네임, 디우_이미지, 디우_프로필); /* 베루스 */ @@ -38,24 +35,43 @@ public class MemberFixtures { public static final String 베루스_유저네임 = "verus"; public static final String 베루스_이미지 = "https://verus.png"; public static final String 베루스_프로필 = "https://verus.com"; - public static final Member 베루스 = new Member(베루스_깃허브_아이디, 베루스_유저네임, 베루스_이미지, 베루스_프로필); public static final MemberData 베루스_응답 = new MemberData(베루스_깃허브_아이디, 베루스_유저네임, 베루스_이미지, 베루스_프로필); /* 병민 */ - public static final Long 병민_아이디 = 5L; public static final Long 병민_깃허브_아이디 = 5L; public static final String 병민_유저네임 = "airman"; public static final String 병민_이미지 = "https://airman.png"; public static final String 병민_프로필 = "https://airman.com"; - public static final Member 병민 = new Member(병민_깃허브_아이디, 병민_유저네임, 병민_이미지, 병민_프로필); public static final MemberData 병민_응답 = new MemberData(병민_깃허브_아이디, 병민_유저네임, 병민_이미지, 병민_프로필); /* 태태 */ - public static final Long 태태_아이디 = 6L; public static final Long 태태_깃허브_아이디 = 6L; public static final String 태태_유저네임 = "nannoo"; public static final String 태태_이미지 = "https://nannoo.png"; public static final String 태태_프로필 = "https://nannoo.com"; - public static final Member 태태 = new Member(태태_깃허브_아이디, 태태_유저네임, 태태_이미지, 태태_프로필); public static final MemberData 태태_응답 = new MemberData(태태_깃허브_아이디, 태태_유저네임, 태태_이미지, 태태_프로필); + + public static Member 짱구() { + return new Member(짱구_깃허브_아이디, 짱구_유저네임, 짱구_이미지, 짱구_프로필); + } + + public static Member 그린론() { + return new Member(그린론_깃허브_아이디, 그린론_유저네임, 그린론_이미지, 그린론_프로필); + } + + public static Member 디우() { + return new Member(디우_깃허브_아이디, 디우_유저네임, 디우_이미지, 디우_프로필); + } + + public static Member 베루스() { + return new Member(베루스_깃허브_아이디, 베루스_유저네임, 베루스_이미지, 베루스_프로필); + } + + public static Member 병민() { + return new Member(병민_깃허브_아이디, 병민_유저네임, 병민_이미지, 병민_프로필); + } + + public static Member 태태() { + return new Member(태태_깃허브_아이디, 태태_유저네임, 태태_이미지, 태태_프로필); + } } diff --git a/backend/src/test/java/com/woowacourse/moamoa/fixtures/ReviewFixtures.java b/backend/src/test/java/com/woowacourse/moamoa/fixtures/ReviewFixtures.java index c99f56bd7..cdd020efd 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/fixtures/ReviewFixtures.java +++ b/backend/src/test/java/com/woowacourse/moamoa/fixtures/ReviewFixtures.java @@ -1,47 +1,50 @@ package com.woowacourse.moamoa.fixtures; -import static com.woowacourse.moamoa.fixtures.MemberFixtures.그린론_아이디; -import static com.woowacourse.moamoa.fixtures.MemberFixtures.그린론_응답; -import static com.woowacourse.moamoa.fixtures.MemberFixtures.디우_아이디; -import static com.woowacourse.moamoa.fixtures.MemberFixtures.디우_응답; -import static com.woowacourse.moamoa.fixtures.MemberFixtures.베루스_아이디; -import static com.woowacourse.moamoa.fixtures.MemberFixtures.베루스_응답; -import static com.woowacourse.moamoa.fixtures.MemberFixtures.짱구_아이디; -import static com.woowacourse.moamoa.fixtures.MemberFixtures.짱구_응답; -import static com.woowacourse.moamoa.fixtures.StudyFixtures.자바_스터디_아이디; - import com.woowacourse.moamoa.review.domain.AssociatedStudy; import com.woowacourse.moamoa.review.domain.Review; import com.woowacourse.moamoa.review.domain.Reviewer; -import com.woowacourse.moamoa.review.query.data.ReviewData; -import java.time.LocalDate; public class ReviewFixtures { /* 자바 스터디 리뷰 */ - public static final Long 자바_리뷰1_아이디 = 1L; public static final String 자바_리뷰1_내용 = "자바 스터디 첫 번째 리뷰입니다."; - public static final Review 자바_리뷰1 = new Review(new AssociatedStudy(자바_스터디_아이디), new Reviewer(짱구_아이디), 자바_리뷰1_내용); - public static final ReviewData 자바_리뷰1_데이터 = new ReviewData(자바_리뷰1_아이디, 짱구_응답, - LocalDate.of(2022, 10, 9), LocalDate.of(2022, 10, 9), 자바_리뷰1_내용); - - public static final Long 자바_리뷰2_아이디 = 2L; public static final String 자바_리뷰2_내용 = "자바 스터디 두 번째 리뷰입니다."; - public static final Review 자바_리뷰2 = new Review(new AssociatedStudy(자바_스터디_아이디), new Reviewer(베루스_아이디), 자바_리뷰2_내용); - public static final ReviewData 자바_리뷰2_데이터 = new ReviewData(자바_리뷰2_아이디, 베루스_응답, - LocalDate.of(2022, 10, 9), LocalDate.of(2022, 10, 10), 자바_리뷰2_내용); - - public static final Long 자바_리뷰3_아이디 = 3L; public static final String 자바_리뷰3_내용 = "자바 스터디 세 번째 리뷰입니다."; - public static final Review 자바_리뷰3 = new Review(new AssociatedStudy(자바_스터디_아이디), new Reviewer(그린론_아이디), 자바_리뷰3_내용); - public static final ReviewData 자바_리뷰3_데이터 = new ReviewData(자바_리뷰3_아이디, 그린론_응답, - LocalDate.of(2022, 10, 10), LocalDate.of(2022, 10, 10), 자바_리뷰3_내용); - - public static final Long 자바_리뷰4_아이디 = 4L; public static final String 자바_리뷰4_내용 = "자바 스터디 네 번째 리뷰입니다."; - public static final Review 자바_리뷰4 = new Review(new AssociatedStudy(자바_스터디_아이디), new Reviewer(디우_아이디), 자바_리뷰4_내용); - public static final ReviewData 자바_리뷰4_데이터 = new ReviewData(자바_리뷰4_아이디, 디우_응답, - LocalDate.of(2022, 10, 14), LocalDate.of(2022, 10, 15), 자바_리뷰4_내용); - public static final int 자바_리뷰_총_개수 = 4; + public static final String 리액트_리뷰1_내용 = "리액트 스터디 첫 번째 리뷰입니다."; + public static final String 리액트_리뷰2_내용 = "리액트 스터디 두 번째 리뷰입니다."; + public static final String 리액트_리뷰3_내용 = "리액트 스터디 세 번째 리뷰입니다."; + + public static Review 자바_리뷰1(final Long studyId, final Long memberId) { + return 리뷰(studyId, memberId, 자바_리뷰1_내용); + } + + public static Review 자바_리뷰2(final Long studyId, final Long memberId) { + return 리뷰(studyId, memberId, 자바_리뷰2_내용); + } + + public static Review 자바_리뷰3(final Long studyId, final Long memberId) { + return 리뷰(studyId, memberId, 자바_리뷰3_내용); + } + + public static Review 자바_리뷰4(final Long studyId, final Long memberId) { + return 리뷰(studyId, memberId, 자바_리뷰4_내용); + } + + public static Review 리액트_리뷰1(final Long studyId, final Long memberId) { + return 리뷰(studyId, memberId, 리액트_리뷰1_내용); + } + + public static Review 리액트_리뷰2(final Long studyId, final Long memberId) { + return 리뷰(studyId, memberId, 리액트_리뷰2_내용); + } + + public static Review 리액트_리뷰3(final Long studyId, final Long memberId) { + return 리뷰(studyId, memberId, 리액트_리뷰3_내용); + } + + private static Review 리뷰(final Long studyId, final Long reviewerId, final String content) { + return new Review(new AssociatedStudy(studyId), new Reviewer(reviewerId), content); + } } diff --git a/backend/src/test/java/com/woowacourse/moamoa/fixtures/StudyFixtures.java b/backend/src/test/java/com/woowacourse/moamoa/fixtures/StudyFixtures.java index 064a04102..5c4190028 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/fixtures/StudyFixtures.java +++ b/backend/src/test/java/com/woowacourse/moamoa/fixtures/StudyFixtures.java @@ -1,15 +1,44 @@ package com.woowacourse.moamoa.fixtures; +import static com.woowacourse.moamoa.fixtures.MemberFixtures.그린론_깃허브_아이디; +import static com.woowacourse.moamoa.fixtures.MemberFixtures.그린론_아이디; +import static com.woowacourse.moamoa.fixtures.MemberFixtures.그린론_유저네임; +import static com.woowacourse.moamoa.fixtures.MemberFixtures.그린론_이미지; +import static com.woowacourse.moamoa.fixtures.MemberFixtures.그린론_프로필; +import static com.woowacourse.moamoa.fixtures.MemberFixtures.디우_깃허브_아이디; +import static com.woowacourse.moamoa.fixtures.MemberFixtures.디우_아이디; +import static com.woowacourse.moamoa.fixtures.MemberFixtures.디우_유저네임; +import static com.woowacourse.moamoa.fixtures.MemberFixtures.디우_이미지; +import static com.woowacourse.moamoa.fixtures.MemberFixtures.디우_프로필; +import static com.woowacourse.moamoa.fixtures.MemberFixtures.베루스_깃허브_아이디; +import static com.woowacourse.moamoa.fixtures.MemberFixtures.베루스_아이디; +import static com.woowacourse.moamoa.fixtures.MemberFixtures.베루스_유저네임; +import static com.woowacourse.moamoa.fixtures.MemberFixtures.베루스_이미지; +import static com.woowacourse.moamoa.fixtures.MemberFixtures.베루스_프로필; +import static com.woowacourse.moamoa.fixtures.MemberFixtures.짱구_깃허브_아이디; +import static com.woowacourse.moamoa.fixtures.MemberFixtures.짱구_아이디; +import static com.woowacourse.moamoa.fixtures.MemberFixtures.짱구_유저네임; +import static com.woowacourse.moamoa.fixtures.MemberFixtures.짱구_이미지; +import static com.woowacourse.moamoa.fixtures.MemberFixtures.짱구_프로필; +import static com.woowacourse.moamoa.fixtures.TagFixtures.BE_태그_아이디; +import static com.woowacourse.moamoa.fixtures.TagFixtures.FE_태그_아이디; +import static com.woowacourse.moamoa.fixtures.TagFixtures.리액트_태그_아이디; +import static com.woowacourse.moamoa.fixtures.TagFixtures.우테코4기_태그_아이디; +import static com.woowacourse.moamoa.fixtures.TagFixtures.자바_태그_아이디; +import static com.woowacourse.moamoa.study.domain.RecruitStatus.RECRUITMENT_END; +import static com.woowacourse.moamoa.study.domain.RecruitStatus.RECRUITMENT_START; +import static com.woowacourse.moamoa.study.domain.StudyStatus.IN_PROGRESS; +import static com.woowacourse.moamoa.study.domain.StudyStatus.PREPARE; + +import com.woowacourse.moamoa.member.domain.Member; import com.woowacourse.moamoa.study.domain.AttachedTag; import com.woowacourse.moamoa.study.domain.AttachedTags; import com.woowacourse.moamoa.study.domain.Content; import com.woowacourse.moamoa.study.domain.Participants; import com.woowacourse.moamoa.study.domain.RecruitPlanner; -import com.woowacourse.moamoa.study.domain.RecruitStatus; import com.woowacourse.moamoa.study.domain.Study; import com.woowacourse.moamoa.study.domain.StudyPlanner; -import com.woowacourse.moamoa.study.domain.StudyStatus; -import com.woowacourse.moamoa.study.service.StudyResponse; +import com.woowacourse.moamoa.study.service.request.CreatingStudyRequest; import java.time.LocalDate; import java.time.LocalDateTime; import java.util.List; @@ -18,85 +47,155 @@ public class StudyFixtures { /* 자바 스터디 */ - public static final Long 자바_스터디_아이디 = 1L; + public static final Member 자바_스터디장 = new Member(짱구_아이디, 짱구_깃허브_아이디, 짱구_유저네임, 짱구_이미지, 짱구_프로필); public static final Content 자바_스터디_내용 = new Content("신짱구의 자바의 정석", "자바 스터디 요약", "자바 스터디 썸네일", "자바 스터디 설명입니다."); - public static final Participants 자바_스터디_참가자들 = new Participants( - MemberFixtures.짱구_아이디, Set.of(MemberFixtures.그린론_아이디, MemberFixtures.디우_아이디)); - public static final RecruitPlanner 자바_스터디_모집계획 = new RecruitPlanner(10, RecruitStatus.RECRUITMENT_START, LocalDate.now()); - public static final StudyPlanner 자바_스터디_계획 = new StudyPlanner(LocalDate.now().plusMonths(1), LocalDate.now().plusMonths(2), StudyStatus.IN_PROGRESS); - public static final AttachedTags 자바_스터디_태그 = new AttachedTags(List.of(new AttachedTag(TagFixtures.자바_태그_아이디), new AttachedTag( - TagFixtures.우테코4기_태그_아이디), new AttachedTag(TagFixtures.BE_태그_아이디))); + public static final Participants 자바_스터디_참가자들 = new Participants(짱구_아이디, Set.of(그린론_아이디, 디우_아이디)); + public static final RecruitPlanner 자바_스터디_모집계획 = new RecruitPlanner(10, RECRUITMENT_START, LocalDate.now()); + public static final StudyPlanner 자바_스터디_계획 = new StudyPlanner(LocalDate.now().plusMonths(1), LocalDate.now().plusMonths(2), IN_PROGRESS); + public static final AttachedTags 자바_스터디_태그 = new AttachedTags(List.of(new AttachedTag(자바_태그_아이디), new AttachedTag(우테코4기_태그_아이디), new AttachedTag(BE_태그_아이디))); public static final Study 자바_스터디 = new Study(자바_스터디_내용, 자바_스터디_참가자들, 자바_스터디_모집계획, 자바_스터디_계획, 자바_스터디_태그, LocalDateTime.now()); - public static final StudyResponse 자바_스터디_응답 = new StudyResponse(자바_스터디_아이디, 자바_스터디_내용.getTitle(), 자바_스터디_내용.getExcerpt(), - 자바_스터디_내용.getThumbnail(), 자바_스터디_모집계획.getRecruitStatus().name(), List.of( - TagFixtures.자바_태그_요약, TagFixtures.우테코4기_태그_요약, TagFixtures.BE_태그_요약)); + + public static Study 자바_스터디(final Long ownerId, final Set<Long> participants) { + return new Study(자바_스터디_내용, new Participants(ownerId, participants), 자바_스터디_모집계획, + 자바_스터디_계획, 자바_스터디_태그, LocalDateTime.now()); + } /* 리액트 스터디 */ - public static final Long 리액트_스터디_아이디 = 2L; + public static final Member 리액트_스터디장 = new Member(디우_아이디, 디우_깃허브_아이디, 디우_유저네임, 디우_이미지, 디우_프로필); public static final Content 리액트_스터디_내용 = new Content("디우의 이것이 리액트다.", "리액트 스터디 요약", "리액트 스터디 썸네일", "리액트 스터디 설명입니다."); - public static final Participants 리액트_스터디_참가자들 = new Participants( - MemberFixtures.디우_아이디, Set.of(MemberFixtures.짱구_아이디, MemberFixtures.그린론_아이디, MemberFixtures.베루스_아이디)); - public static final RecruitPlanner 리액트_스터디_모집계획 = new RecruitPlanner(5, RecruitStatus.RECRUITMENT_START, LocalDate.now()); - public static final StudyPlanner 리액트_스터디_계획 = new StudyPlanner(LocalDate.now().plusMonths(1), LocalDate.now().plusMonths(2), StudyStatus.PREPARE); - public static final AttachedTags 리액트_스터디_태그 = new AttachedTags(List.of(new AttachedTag(TagFixtures.우테코4기_태그_아이디), new AttachedTag( - TagFixtures.FE_태그_아이디), new AttachedTag(TagFixtures.리액트_태그_아이디))); + public static final Participants 리액트_스터디_참가자들 = new Participants(디우_아이디, Set.of(짱구_아이디, 그린론_아이디, 베루스_아이디)); + public static final RecruitPlanner 리액트_스터디_모집계획 = new RecruitPlanner(5, RECRUITMENT_START, LocalDate.now()); + public static final StudyPlanner 리액트_스터디_계획 = new StudyPlanner(LocalDate.now().plusMonths(1), LocalDate.now().plusMonths(2), PREPARE); + public static final AttachedTags 리액트_스터디_태그 = new AttachedTags(List.of(new AttachedTag(우테코4기_태그_아이디), new AttachedTag(FE_태그_아이디), new AttachedTag(리액트_태그_아이디))); public static final Study 리액트_스터디 = new Study(리액트_스터디_내용, 리액트_스터디_참가자들, 리액트_스터디_모집계획, 리액트_스터디_계획, 리액트_스터디_태그, LocalDateTime.now()); - public static final StudyResponse 리액트_스터디_응답 = new StudyResponse(리액트_스터디_아이디, 리액트_스터디_내용.getTitle(), 리액트_스터디_내용.getExcerpt(), - 리액트_스터디_내용.getThumbnail(), 리액트_스터디_모집계획.getRecruitStatus().name(), List.of(TagFixtures.우테코4기_태그_요약, TagFixtures.FE_태그_요약, TagFixtures.리액트_태그_요약)); - + + public static Study 리액트_스터디(final Long ownerId, final Set<Long> participants) { + return new Study(리액트_스터디_내용, new Participants(ownerId, participants), 리액트_스터디_모집계획, + 리액트_스터디_계획, 리액트_스터디_태그, LocalDateTime.now()); + } + /* 자바스크립트스크립트 스터디 */ - public static final Long 자바스크립트_스터디_아이디 = 3L; + public static final Member 자바스크립트_스터디장 = new Member(그린론_아이디, 그린론_깃허브_아이디, 그린론_유저네임, 그린론_이미지, 그린론_프로필); public static final Content 자바스크립트_스터디_내용 = new Content("그린론의 모던 자바스크립트 인 액션", "자바스크립트 스터디 요약", "자바스크립트 스터디 썸네일", "자바스크립트 스터디 설명입니다."); - public static final Participants 자바스크립트_스터디_참가자들 = new Participants( - MemberFixtures.그린론_아이디, Set.of(MemberFixtures.디우_아이디, MemberFixtures.베루스_아이디)); - public static final RecruitPlanner 자바스크립트_스터디_모집계획 = new RecruitPlanner(20, RecruitStatus.RECRUITMENT_START, LocalDate.now()); - public static final StudyPlanner 자바스크립트_스터디_계획 = new StudyPlanner(LocalDate.now().plusMonths(1), LocalDate.now().plusMonths(2), StudyStatus.PREPARE); - public static final AttachedTags 자바스크립트_스터디_태그 = new AttachedTags(List.of(new AttachedTag(TagFixtures.우테코4기_태그_아이디), new AttachedTag( - TagFixtures.FE_태그_아이디))); + public static final Participants 자바스크립트_스터디_참가자들 = new Participants(그린론_아이디, Set.of(디우_아이디, 베루스_아이디)); + public static final RecruitPlanner 자바스크립트_스터디_모집계획 = new RecruitPlanner(20, RECRUITMENT_START, LocalDate.now()); + public static final StudyPlanner 자바스크립트_스터디_계획 = new StudyPlanner(LocalDate.now().plusMonths(1), LocalDate.now().plusMonths(2), PREPARE); + public static final AttachedTags 자바스크립트_스터디_태그 = new AttachedTags(List.of(new AttachedTag(우테코4기_태그_아이디), new AttachedTag(FE_태그_아이디))); public static final Study 자바스크립트_스터디 = new Study(자바스크립트_스터디_내용, 자바스크립트_스터디_참가자들, 자바스크립트_스터디_모집계획, 자바스크립트_스터디_계획, 자바스크립트_스터디_태그, LocalDateTime.now()); - public static final StudyResponse 자바스크립트_스터디_응답 = new StudyResponse(자바스크립트_스터디_아이디, 자바스크립트_스터디_내용.getTitle(), 자바스크립트_스터디_내용.getExcerpt(), - 자바스크립트_스터디_내용.getThumbnail(), 자바스크립트_스터디_모집계획.getRecruitStatus().name(), List.of(TagFixtures.우테코4기_태그_요약, TagFixtures.FE_태그_요약)); + + public static Study 자바스크립트_스터디(final Long ownerId, final Set<Long> participants) { + return new Study(자바스크립트_스터디_내용, new Participants(ownerId, participants), 자바스크립트_스터디_모집계획, + 자바스크립트_스터디_계획, 자바스크립트_스터디_태그, LocalDateTime.now()); + } /* HTTP 스터디 */ - public static final Long HTTP_스터디_아이디 = 4L; + public static final Member HTTP_스터디장 = new Member(디우_아이디, 디우_깃허브_아이디, 디우_유저네임, 디우_이미지, 디우_프로필); public static final Content HTTP_스터디_내용 = new Content("디우의 HTTP", "HTTP 스터디 요약", "HTTP 스터디 썸네일", "HTTP 스터디 설명입니다."); - public static final Participants HTTP_스터디_참가자들 = new Participants( - MemberFixtures.디우_아이디, Set.of(MemberFixtures.베루스_아이디, MemberFixtures.짱구_아이디)); - public static final RecruitPlanner HTTP_스터디_모집계획 = new RecruitPlanner(4, RecruitStatus.RECRUITMENT_END, LocalDate.now()); - public static final StudyPlanner HTTP_스터디_계획 = new StudyPlanner(LocalDate.now().plusMonths(1), LocalDate.now().plusMonths(2), StudyStatus.PREPARE); - public static final AttachedTags HTTP_스터디_태그 = new AttachedTags(List.of(new AttachedTag(TagFixtures.우테코4기_태그_아이디), new AttachedTag( - TagFixtures.BE_태그_아이디))); + public static final Participants HTTP_스터디_참가자들 = new Participants(디우_아이디, Set.of(베루스_아이디, 짱구_아이디)); + public static final RecruitPlanner HTTP_스터디_모집계획 = new RecruitPlanner(4, RECRUITMENT_END, LocalDate.now()); + public static final StudyPlanner HTTP_스터디_계획 = new StudyPlanner(LocalDate.now().plusMonths(1), LocalDate.now().plusMonths(2), PREPARE); + public static final AttachedTags HTTP_스터디_태그 = new AttachedTags(List.of(new AttachedTag(우테코4기_태그_아이디), new AttachedTag(BE_태그_아이디))); public static final Study HTTP_스터디 = new Study(HTTP_스터디_내용, HTTP_스터디_참가자들, HTTP_스터디_모집계획, HTTP_스터디_계획, HTTP_스터디_태그, LocalDateTime.now()); - public static final StudyResponse HTTP_스터디_응답 = new StudyResponse(HTTP_스터디_아이디, HTTP_스터디_내용.getTitle(), HTTP_스터디_내용.getExcerpt(), - HTTP_스터디_내용.getThumbnail(), HTTP_스터디_모집계획.getRecruitStatus().name(), List.of(TagFixtures.우테코4기_태그_요약, TagFixtures.BE_태그_요약)); - - /* 알고리즘 스터디 */ - public static final Long 알고리즘_스터디_아이디 = 5L; + + public static Study HTTP_스터디(final Long ownerId, final Set<Long> participants) { + return new Study(HTTP_스터디_내용, new Participants(ownerId, participants), HTTP_스터디_모집계획, + HTTP_스터디_계획, HTTP_스터디_태그, LocalDateTime.now()); + } + + /* 알고리즘 스터디 (모집 기간과 스터디 종료일자가 없음) */ + public static final Member 알고리즘_스터디장 = new Member(베루스_아이디, 베루스_깃허브_아이디, 베루스_유저네임, 베루스_이미지, 베루스_프로필); public static final Content 알고리즘_스터디_내용 = new Content("알고리즘 주도 개발 1타 강사 베루스", "알고리즘 스터디 요약", "알고리즘 스터디 썸네일", "알고리즘 스터디 설명입니다."); - public static final Participants 알고리즘_스터디_참가자들 = new Participants( - MemberFixtures.베루스_아이디, Set.of(MemberFixtures.그린론_아이디, MemberFixtures.디우_아이디)); - public static final RecruitPlanner 알고리즘_스터디_모집계획 = new RecruitPlanner(10, RecruitStatus.RECRUITMENT_END, LocalDate.now()); - public static final StudyPlanner 알고리즘_스터디_계획 = new StudyPlanner(LocalDate.now().plusMonths(1), LocalDate.now().plusMonths(2), StudyStatus.PREPARE); + public static final Participants 알고리즘_스터디_참가자들 = new Participants(베루스_아이디, Set.of(그린론_아이디, 디우_아이디)); + public static final RecruitPlanner 알고리즘_스터디_모집계획 = new RecruitPlanner(null, RECRUITMENT_END, null); + public static final StudyPlanner 알고리즘_스터디_계획 = new StudyPlanner(LocalDate.now().plusMonths(1), null, PREPARE); public static final AttachedTags 알고리즘_스터디_태그 = new AttachedTags(List.of()); public static final Study 알고리즘_스터디 = new Study(알고리즘_스터디_내용, 알고리즘_스터디_참가자들, 알고리즘_스터디_모집계획, 알고리즘_스터디_계획, 알고리즘_스터디_태그, LocalDateTime.now()); - public static final StudyResponse 알고리즘_스터디_응답 = new StudyResponse(알고리즘_스터디_아이디, 알고리즘_스터디_내용.getTitle(), 알고리즘_스터디_내용.getExcerpt(), - 알고리즘_스터디_내용.getThumbnail(), 알고리즘_스터디_모집계획.getRecruitStatus().name(), List.of()); - - /* 리눅스 스터디 */ - public static final Long 리눅스_스터디_아이디 = 6L; + + public static Study 알고리즘_스터디(final Long ownerId, final Set<Long> participants) { + return new Study(알고리즘_스터디_내용, new Participants(ownerId, participants), 알고리즘_스터디_모집계획, + 알고리즘_스터디_계획, 알고리즘_스터디_태그, LocalDateTime.now()); + } + + /* 리눅스 스터디 (최대 인원 없음) */ + public static final Member 리눅스_스터디장 = new Member(베루스_아이디, 베루스_깃허브_아이디, 베루스_유저네임, 베루스_이미지, 베루스_프로필); public static final Content 리눅스_스터디_내용 = new Content("벨우스의 린우스", "리눅스 스터디 요약", "리눅스 스터디 썸네일", "리눅스 스터디 설명입니다."); - public static final Participants 리눅스_스터디_참가자들 = new Participants( - MemberFixtures.베루스_아이디, Set.of(MemberFixtures.그린론_아이디, MemberFixtures.디우_아이디)); - public static final RecruitPlanner 리눅스_스터디_모집계획 = new RecruitPlanner(10, RecruitStatus.RECRUITMENT_START, LocalDate.now()); - public static final StudyPlanner 리눅스_스터디_계획 = new StudyPlanner(LocalDate.now().plusMonths(1), LocalDate.now().plusMonths(2), StudyStatus.PREPARE); + public static final Participants 리눅스_스터디_참가자들 = new Participants(베루스_아이디, Set.of(그린론_아이디, 디우_아이디)); + public static final RecruitPlanner 리눅스_스터디_모집계획 = new RecruitPlanner(null, RECRUITMENT_START, LocalDate.now()); + public static final StudyPlanner 리눅스_스터디_계획 = new StudyPlanner(LocalDate.now().plusMonths(1), LocalDate.now().plusMonths(2), PREPARE); public static final AttachedTags 리눅스_스터디_태그 = new AttachedTags(List.of()); public static final Study 리눅스_스터디 = new Study(리눅스_스터디_내용, 리눅스_스터디_참가자들, 리눅스_스터디_모집계획, 리눅스_스터디_계획, 리눅스_스터디_태그, LocalDateTime.now()); - public static final StudyResponse 리눅스_스터디_응답 = new StudyResponse(리눅스_스터디_아이디, 리눅스_스터디_내용.getTitle(), 리눅스_스터디_내용.getExcerpt(), - 리눅스_스터디_내용.getThumbnail(), 리눅스_스터디_모집계획.getRecruitStatus().name(), List.of()); + + public static Study 리눅스_스터디(final Long ownerId, final Set<Long> participants) { + return new Study(리눅스_스터디_내용, new Participants(ownerId, participants), 리눅스_스터디_모집계획, + 리눅스_스터디_계획, 리눅스_스터디_태그, LocalDateTime.now()); + } + + /* OS 스터디 */ + public static final Member OS_스터디장 = new Member(디우_아이디, 디우_깃허브_아이디, 디우_유저네임, 디우_이미지, 디우_프로필); + public static final Content OS_스터디_내용 = new Content("디우의 OS 스터디", "OS 스터디 요약", "OS 스터디 썸네일", "OS 스터디 설명입니다."); + public static final Participants OS_스터디_참가자들 = new Participants(디우_아이디, Set.of(그린론_아이디, 짱구_아이디, 베루스_아이디)); + public static final RecruitPlanner OS_스터디_모집계획 = new RecruitPlanner(10, RECRUITMENT_START, LocalDate.now()); + public static final StudyPlanner OS_스터디_계획 = new StudyPlanner(LocalDate.now().plusMonths(1), LocalDate.now().plusMonths(2), PREPARE); + public static final AttachedTags OS_스터디_태그 = new AttachedTags(List.of(new AttachedTag(우테코4기_태그_아이디), new AttachedTag(BE_태그_아이디))); + + public static Study OS_스터디(final Long ownerId, final Set<Long> participants) { + return new Study(OS_스터디_내용, new Participants(ownerId, participants), OS_스터디_모집계획, + OS_스터디_계획, OS_스터디_태그, LocalDateTime.now()); + } + + public static CreatingStudyRequest 자바_스터디_신청서(LocalDate now) { + return CreatingStudyRequest.builder() + .title("java 스터디").excerpt("자바 설명").thumbnail("java image").description("자바 소개") + .startDate(now) + .build(); + } + + public static CreatingStudyRequest 자바_스터디_신청서(List<Long> tagIds, int maxMemberCount, LocalDate now) { + return CreatingStudyRequest.builder() + .title("Java 스터디").excerpt("자바 설명").thumbnail("java thumbnail").description("그린론의 우당탕탕 자바 스터디입니다.") + .startDate(now).tagIds(tagIds).maxMemberCount(maxMemberCount) + .build(); + } + + public static CreatingStudyRequest 리액트_스터디_신청서(LocalDate now) { + return CreatingStudyRequest.builder() + .title("react 스터디").excerpt("리액트 설명").thumbnail("react image").description("리액트 소개") + .startDate(now) + .build(); + } + + public static CreatingStudyRequest 리액트_스터디_신청서(List<Long> tagIds, int maxMemberCount, LocalDate now) { + return CreatingStudyRequest.builder() + .title("React 스터디").excerpt("리액트 설명").thumbnail("react thumbnail").description("디우의 뤼액트 스터디입니다.") + .startDate(LocalDate.now()).endDate(now).enrollmentEndDate(LocalDate.now()) + .tagIds(tagIds).maxMemberCount(maxMemberCount) + .build(); + } + + public static CreatingStudyRequest 자바스크립트_스터디_신청서(List<Long> tagIds, LocalDate now) { + return CreatingStudyRequest.builder() + .title("javaScript 스터디").excerpt("자바스크립트 설명").thumbnail("javascript thumbnail").description("자바스크립트 설명") + .startDate(now).tagIds(tagIds) + .build(); + } + + public static CreatingStudyRequest HTTP_스터디_신청서(List<Long> tagIds, LocalDate now) { + return CreatingStudyRequest.builder() + .title("HTTP 스터디").excerpt("HTTP 설명").thumbnail("http thumbnail").description("HTTP 설명") + .startDate(now).tagIds(tagIds) + .build(); + } + + public static CreatingStudyRequest 알고리즘_스터디_신청서(List<Long> tagIds, LocalDate now) { + return CreatingStudyRequest.builder() + .title("알고리즘 스터디").excerpt("알고리즘 설명").thumbnail("algorithm thumbnail").description("알고리즘 설명") + .startDate(now).tagIds(tagIds) + .build(); + } } diff --git a/backend/src/test/java/com/woowacourse/moamoa/member/domain/repository/MemberRepositoryTest.java b/backend/src/test/java/com/woowacourse/moamoa/member/domain/repository/MemberRepositoryTest.java index 9ae24152a..34659c744 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/member/domain/repository/MemberRepositoryTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/member/domain/repository/MemberRepositoryTest.java @@ -12,7 +12,6 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.jdbc.core.JdbcTemplate; @RepositoryTest class MemberRepositoryTest { diff --git a/backend/src/test/java/com/woowacourse/moamoa/referenceroom/controller/ReferenceRoomControllerTest.java b/backend/src/test/java/com/woowacourse/moamoa/referenceroom/controller/ReferenceRoomControllerTest.java index f99fd2090..59651a7d9 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/referenceroom/controller/ReferenceRoomControllerTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/referenceroom/controller/ReferenceRoomControllerTest.java @@ -6,6 +6,7 @@ import static com.woowacourse.moamoa.fixtures.MemberFixtures.베루스_깃허브_아이디; import static com.woowacourse.moamoa.fixtures.MemberFixtures.짱구; import static com.woowacourse.moamoa.fixtures.MemberFixtures.짱구_깃허브_아이디; +import static com.woowacourse.moamoa.fixtures.StudyFixtures.자바_스터디_신청서; import static org.assertj.core.api.Assertions.assertThatThrownBy; import com.woowacourse.moamoa.common.RepositoryTest; @@ -56,17 +57,14 @@ void setUp() { sut = new ReferenceRoomController(new ReferenceRoomService(memberRepository, studyRepository, linkRepository)); // 사용자 추가 - jjangguId = memberRepository.save(짱구).getId(); - verusId = memberRepository.save(베루스).getId(); - dwooId = memberRepository.save(디우).getId(); + jjangguId = memberRepository.save(짱구()).getId(); + verusId = memberRepository.save(베루스()).getId(); + dwooId = memberRepository.save(디우()).getId(); // 스터디 생성 final StudyService studyService = new StudyService(studyRepository, memberRepository, new DateTimeSystem()); final LocalDate startDate = LocalDate.now(); - final CreatingStudyRequest javaStudyRequest = CreatingStudyRequest.builder() - .title("java 스터디").excerpt("자바 설명").thumbnail("java image").description("자바 소개") - .startDate(startDate) - .build(); + final CreatingStudyRequest javaStudyRequest = 자바_스터디_신청서(startDate); javaStudyId = studyService.createStudy(짱구_깃허브_아이디, javaStudyRequest).getId(); studyService.participateStudy(베루스_깃허브_아이디, javaStudyId); diff --git a/backend/src/test/java/com/woowacourse/moamoa/referenceroom/controller/SearchingReferenceRoomControllerTest.java b/backend/src/test/java/com/woowacourse/moamoa/referenceroom/controller/SearchingReferenceRoomControllerTest.java index 73fda2126..6a4ef8766 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/referenceroom/controller/SearchingReferenceRoomControllerTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/referenceroom/controller/SearchingReferenceRoomControllerTest.java @@ -14,6 +14,7 @@ import static com.woowacourse.moamoa.fixtures.MemberFixtures.짱구; import static com.woowacourse.moamoa.fixtures.MemberFixtures.짱구_깃허브_아이디; import static com.woowacourse.moamoa.fixtures.MemberFixtures.짱구_응답; +import static com.woowacourse.moamoa.fixtures.StudyFixtures.자바_스터디_신청서; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.assertAll; @@ -77,20 +78,17 @@ void setUp() { new SearchingReferenceRoomService(linkDao, memberRepository, studyRepository)); // 사용자 추가 - final Member jjanggu = memberRepository.save(짱구); - final Member greenlawn = memberRepository.save(그린론); - final Member dwoo = memberRepository.save(디우); - final Member verus = memberRepository.save(베루스); - memberRepository.save(병민); + final Member jjanggu = memberRepository.save(짱구()); + final Member greenlawn = memberRepository.save(그린론()); + final Member dwoo = memberRepository.save(디우()); + final Member verus = memberRepository.save(베루스()); + memberRepository.save(병민()); // 스터디 생성 StudyService studyService = new StudyService(studyRepository, memberRepository, new DateTimeSystem()); final LocalDate startDate = LocalDate.now(); - CreatingStudyRequest javaStudyRequest = CreatingStudyRequest.builder() - .title("java 스터디").excerpt("자바 설명").thumbnail("java image").description("자바 소개") - .startDate(startDate) - .build(); + CreatingStudyRequest javaStudyRequest = 자바_스터디_신청서(startDate); javaStudy = studyService.createStudy(짱구_깃허브_아이디, javaStudyRequest); diff --git a/backend/src/test/java/com/woowacourse/moamoa/referenceroom/query/LinkDaoTest.java b/backend/src/test/java/com/woowacourse/moamoa/referenceroom/query/LinkDaoTest.java index 1e4e5eef0..655b683a3 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/referenceroom/query/LinkDaoTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/referenceroom/query/LinkDaoTest.java @@ -1,5 +1,6 @@ package com.woowacourse.moamoa.referenceroom.query; +import static com.woowacourse.moamoa.fixtures.StudyFixtures.자바_스터디_신청서; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertAll; @@ -26,7 +27,6 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Slice; -import org.springframework.jdbc.core.JdbcTemplate; @RepositoryTest public class LinkDaoTest { @@ -45,9 +45,6 @@ public class LinkDaoTest { @Autowired private LinkRepository linkRepository; - @Autowired - private JdbcTemplate jdbcTemplate; - @Autowired private EntityManager entityManager; @@ -70,10 +67,7 @@ void setUp() { StudyService createStudyService = new StudyService(studyRepository, memberRepository, new DateTimeSystem()); final LocalDate startDate = LocalDate.now(); - CreatingStudyRequest javaStudyRequest = CreatingStudyRequest.builder() - .title("java 스터디").excerpt("자바 설명").thumbnail("java image").description("자바 소개") - .startDate(startDate) - .build(); + CreatingStudyRequest javaStudyRequest = 자바_스터디_신청서(startDate); javaStudy = createStudyService.createStudy(JJANGGU.getGithubId(), javaStudyRequest); createStudyService.participateStudy(GREENLAWN.getGithubId(), javaStudy.getId()); diff --git a/backend/src/test/java/com/woowacourse/moamoa/review/controller/ReviewControllerTest.java b/backend/src/test/java/com/woowacourse/moamoa/review/controller/ReviewControllerTest.java index 9413b0054..ff15c2af3 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/review/controller/ReviewControllerTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/review/controller/ReviewControllerTest.java @@ -1,12 +1,16 @@ package com.woowacourse.moamoa.review.controller; +import static com.woowacourse.moamoa.fixtures.MemberFixtures.그린론; +import static com.woowacourse.moamoa.fixtures.MemberFixtures.그린론_깃허브_아이디; +import static com.woowacourse.moamoa.fixtures.MemberFixtures.그린론_응답; +import static com.woowacourse.moamoa.fixtures.MemberFixtures.짱구; +import static com.woowacourse.moamoa.fixtures.MemberFixtures.짱구_응답; import static org.assertj.core.api.Assertions.assertThatThrownBy; import com.woowacourse.moamoa.common.RepositoryTest; import com.woowacourse.moamoa.common.utils.DateTimeSystem; import com.woowacourse.moamoa.member.domain.Member; import com.woowacourse.moamoa.member.domain.repository.MemberRepository; -import com.woowacourse.moamoa.member.query.data.MemberData; import com.woowacourse.moamoa.review.domain.repository.ReviewRepository; import com.woowacourse.moamoa.review.service.ReviewService; import com.woowacourse.moamoa.review.service.exception.UnwrittenReviewException; @@ -24,14 +28,10 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.jdbc.core.JdbcTemplate; @RepositoryTest public class ReviewControllerTest { - private static final MemberData 짱구 = new MemberData(1L, "jjanggu", "https://image", "github.com"); - private static final MemberData 베루스 = new MemberData(4L, "verus", "https://image", "github.com"); - @Autowired private MemberRepository memberRepository; @@ -53,8 +53,8 @@ void setUp() { sut = new ReviewController(new ReviewService(reviewRepository, memberRepository, studyRepository)); // 사용자 추가 - final Member jjanggu = memberRepository.save(toMember(짱구)); - final Member verus = memberRepository.save(toMember(베루스)); + final Member jjanggu = memberRepository.save(짱구()); + final Member greelawn = memberRepository.save(그린론()); // 스터디 생성 StudyService studyService = new StudyService(studyRepository, memberRepository, new DateTimeSystem()); @@ -67,7 +67,7 @@ void setUp() { Study javaStudy = studyService.createStudy(1L, javaStudyRequest); - studyService.participateStudy(verus.getGithubId(), javaStudy.getId()); + studyService.participateStudy(greelawn.getGithubId(), javaStudy.getId()); // 리뷰 추가 ReviewService reviewService = new ReviewService(reviewRepository, memberRepository, studyRepository); @@ -75,35 +75,30 @@ void setUp() { 짱구_리뷰 = reviewService .writeReview(jjanggu.getGithubId(), javaStudy.getId(), new WriteReviewRequest("리뷰 내용1")); final Long javaReviewId4 = reviewService - .writeReview(verus.getGithubId(), javaStudy.getId(), new WriteReviewRequest("리뷰 내용4")); + .writeReview(greelawn.getGithubId(), javaStudy.getId(), new WriteReviewRequest("리뷰 내용4")); - final ReviewResponse 리뷰_내용1 = new ReviewResponse(짱구_리뷰, new WriterResponse(짱구), LocalDate.now(), + final ReviewResponse 리뷰_내용1 = new ReviewResponse(짱구_리뷰, new WriterResponse(짱구_응답), LocalDate.now(), LocalDate.now(), "리뷰 내용1"); - final ReviewResponse 리뷰_내용4 = new ReviewResponse(javaReviewId4, new WriterResponse(베루스), LocalDate.now(), + final ReviewResponse 리뷰_내용4 = new ReviewResponse(javaReviewId4, new WriterResponse(그린론_응답), LocalDate.now(), LocalDate.now(), "리뷰 내용4"); entityManager.flush(); entityManager.clear(); } - private static Member toMember(MemberData memberData) { - return new Member(memberData.getGithubId(), memberData.getUsername(), memberData.getImageUrl(), - memberData.getProfileUrl()); - } - @DisplayName("내가 작성하지 않은 리뷰를 수정할 수 없다.") @Test void notUpdate() { final EditingReviewRequest request = new EditingReviewRequest("수정한 리뷰 내용입니다."); - assertThatThrownBy(() -> sut.updateReview(베루스.getGithubId(), 짱구_리뷰, request)) + assertThatThrownBy(() -> sut.updateReview(그린론_깃허브_아이디, 짱구_리뷰, request)) .isInstanceOf(UnwrittenReviewException.class); } @DisplayName("내가 작성하지 않은 리뷰를 삭제할 수 없다.") @Test void notDelete() { - assertThatThrownBy(() -> sut.deleteReview(베루스.getGithubId(), 짱구_리뷰)) + assertThatThrownBy(() -> sut.deleteReview(그린론_깃허브_아이디, 짱구_리뷰)) .isInstanceOf(UnwrittenReviewException.class); } } diff --git a/backend/src/test/java/com/woowacourse/moamoa/review/controller/SearchingReviewControllerTest.java b/backend/src/test/java/com/woowacourse/moamoa/review/controller/SearchingReviewControllerTest.java index 916ac6c94..5b0c92ecc 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/review/controller/SearchingReviewControllerTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/review/controller/SearchingReviewControllerTest.java @@ -1,12 +1,21 @@ package com.woowacourse.moamoa.review.controller; +import static com.woowacourse.moamoa.fixtures.MemberFixtures.그린론; +import static com.woowacourse.moamoa.fixtures.MemberFixtures.그린론_응답; +import static com.woowacourse.moamoa.fixtures.MemberFixtures.디우; +import static com.woowacourse.moamoa.fixtures.MemberFixtures.디우_응답; +import static com.woowacourse.moamoa.fixtures.MemberFixtures.베루스; +import static com.woowacourse.moamoa.fixtures.MemberFixtures.베루스_응답; +import static com.woowacourse.moamoa.fixtures.MemberFixtures.짱구; +import static com.woowacourse.moamoa.fixtures.MemberFixtures.짱구_응답; +import static com.woowacourse.moamoa.fixtures.StudyFixtures.리액트_스터디_신청서; +import static com.woowacourse.moamoa.fixtures.StudyFixtures.자바_스터디_신청서; import static org.assertj.core.api.Assertions.assertThat; import com.woowacourse.moamoa.common.RepositoryTest; import com.woowacourse.moamoa.common.utils.DateTimeSystem; import com.woowacourse.moamoa.member.domain.Member; import com.woowacourse.moamoa.member.domain.repository.MemberRepository; -import com.woowacourse.moamoa.member.query.data.MemberData; import com.woowacourse.moamoa.review.domain.repository.ReviewRepository; import com.woowacourse.moamoa.review.query.ReviewDao; import com.woowacourse.moamoa.review.service.ReviewService; @@ -33,11 +42,6 @@ @RepositoryTest class SearchingReviewControllerTest { - private static final MemberData JJANGGU = new MemberData(1L, "jjanggu", "https://image", "github.com"); - private static final MemberData GREENLAWN = new MemberData(2L, "greenlawn", "https://image", "github.com"); - private static final MemberData DWOO = new MemberData(3L, "dwoo", "https://image", "github.com"); - private static final MemberData VERUS = new MemberData(4L, "verus", "https://image", "github.com"); - @Autowired private MemberRepository memberRepository; @@ -64,23 +68,17 @@ void setUp() { sut = new SearchingReviewController(new SearchingReviewService(reviewDao)); // 사용자 추가 - final Member jjanggu = memberRepository.save(toMember(JJANGGU)); - final Member greenlawn = memberRepository.save(toMember(GREENLAWN)); - final Member dwoo = memberRepository.save(toMember(DWOO)); - final Member verus = memberRepository.save(toMember(VERUS)); + final Member jjanggu = memberRepository.save(짱구()); + final Member greenlawn = memberRepository.save(그린론()); + final Member dwoo = memberRepository.save(디우()); + final Member verus = memberRepository.save(베루스()); // 스터디 생성 StudyService studyService = new StudyService(studyRepository, memberRepository, new DateTimeSystem()); final LocalDate startDate = LocalDate.now(); - CreatingStudyRequest javaStudyRequest = CreatingStudyRequest.builder() - .title("java 스터디").excerpt("자바 설명").thumbnail("java image").description("자바 소개") - .startDate(startDate) - .build(); - CreatingStudyRequest reactStudyRequest = CreatingStudyRequest.builder() - .title("react 스터디").excerpt("리액트 설명").thumbnail("react image").description("리액트 소개") - .startDate(startDate) - .build(); + CreatingStudyRequest javaStudyRequest = 자바_스터디_신청서(startDate); + CreatingStudyRequest reactStudyRequest = 리액트_스터디_신청서(startDate); javaStudy = studyService.createStudy(1L, javaStudyRequest); final Study reactStudy = studyService.createStudy(1L, reactStudyRequest); @@ -102,24 +100,19 @@ void setUp() { .writeReview(verus.getGithubId(), javaStudy.getId(), new WriteReviewRequest("리뷰 내용4")); reviewService.writeReview(jjanggu.getGithubId(), reactStudy.getId(), new WriteReviewRequest("리뷰 내용5")); - final ReviewResponse 리뷰_내용1 = new ReviewResponse(javaReviewId1, new WriterResponse(JJANGGU), LocalDate.now(), + final ReviewResponse 리뷰_내용1 = new ReviewResponse(javaReviewId1, new WriterResponse(짱구_응답), LocalDate.now(), LocalDate.now(), "리뷰 내용1"); - final ReviewResponse 리뷰_내용2 = new ReviewResponse(javaReviewId2, new WriterResponse(GREENLAWN), LocalDate.now(), + final ReviewResponse 리뷰_내용2 = new ReviewResponse(javaReviewId2, new WriterResponse(그린론_응답), LocalDate.now(), LocalDate.now(), "리뷰 내용2"); - final ReviewResponse 리뷰_내용3 = new ReviewResponse(javaReviewId3, new WriterResponse(DWOO), LocalDate.now(), + final ReviewResponse 리뷰_내용3 = new ReviewResponse(javaReviewId3, new WriterResponse(디우_응답), LocalDate.now(), LocalDate.now(), "리뷰 내용3"); - final ReviewResponse 리뷰_내용4 = new ReviewResponse(javaReviewId4, new WriterResponse(VERUS), LocalDate.now(), + final ReviewResponse 리뷰_내용4 = new ReviewResponse(javaReviewId4, new WriterResponse(베루스_응답), LocalDate.now(), LocalDate.now(), "리뷰 내용4"); javaReviews = List.of(리뷰_내용4, 리뷰_내용3, 리뷰_내용2, 리뷰_내용1); entityManager.flush(); } - private static Member toMember(MemberData memberData) { - return new Member(memberData.getGithubId(), memberData.getUsername(), memberData.getImageUrl(), - memberData.getProfileUrl()); - } - @DisplayName("스터디의 전체 후기를 조회할 수 있다.") @Test public void getAllReviews() { diff --git a/backend/src/test/java/com/woowacourse/moamoa/review/query/ReviewDaoTest.java b/backend/src/test/java/com/woowacourse/moamoa/review/query/ReviewDaoTest.java index c3bf342b2..72f3dea96 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/review/query/ReviewDaoTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/review/query/ReviewDaoTest.java @@ -1,15 +1,38 @@ package com.woowacourse.moamoa.review.query; +import static com.woowacourse.moamoa.fixtures.MemberFixtures.그린론; +import static com.woowacourse.moamoa.fixtures.MemberFixtures.디우; +import static com.woowacourse.moamoa.fixtures.MemberFixtures.베루스; +import static com.woowacourse.moamoa.fixtures.MemberFixtures.짱구; +import static com.woowacourse.moamoa.fixtures.ReviewFixtures.리액트_리뷰1; +import static com.woowacourse.moamoa.fixtures.ReviewFixtures.리액트_리뷰1_내용; +import static com.woowacourse.moamoa.fixtures.ReviewFixtures.리액트_리뷰2; +import static com.woowacourse.moamoa.fixtures.ReviewFixtures.리액트_리뷰2_내용; +import static com.woowacourse.moamoa.fixtures.ReviewFixtures.리액트_리뷰3; +import static com.woowacourse.moamoa.fixtures.ReviewFixtures.리액트_리뷰3_내용; +import static com.woowacourse.moamoa.fixtures.ReviewFixtures.자바_리뷰1; +import static com.woowacourse.moamoa.fixtures.ReviewFixtures.자바_리뷰1_내용; +import static com.woowacourse.moamoa.fixtures.ReviewFixtures.자바_리뷰2; +import static com.woowacourse.moamoa.fixtures.ReviewFixtures.자바_리뷰2_내용; +import static com.woowacourse.moamoa.fixtures.ReviewFixtures.자바_리뷰3; +import static com.woowacourse.moamoa.fixtures.ReviewFixtures.자바_리뷰3_내용; +import static com.woowacourse.moamoa.fixtures.ReviewFixtures.자바_리뷰4; +import static com.woowacourse.moamoa.fixtures.ReviewFixtures.자바_리뷰4_내용; +import static com.woowacourse.moamoa.fixtures.StudyFixtures.리액트_스터디_신청서; +import static com.woowacourse.moamoa.fixtures.StudyFixtures.자바_스터디_신청서; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.tuple; import com.woowacourse.moamoa.common.RepositoryTest; +import com.woowacourse.moamoa.common.utils.DateTimeSystem; import com.woowacourse.moamoa.member.domain.Member; import com.woowacourse.moamoa.member.domain.repository.MemberRepository; import com.woowacourse.moamoa.member.query.data.MemberData; +import com.woowacourse.moamoa.review.domain.Review; +import com.woowacourse.moamoa.review.domain.repository.ReviewRepository; import com.woowacourse.moamoa.review.query.data.ReviewData; import com.woowacourse.moamoa.study.domain.Study; import com.woowacourse.moamoa.study.domain.repository.StudyRepository; -import com.woowacourse.moamoa.common.utils.DateTimeSystem; import com.woowacourse.moamoa.study.service.StudyService; import com.woowacourse.moamoa.study.service.request.CreatingStudyRequest; import java.time.LocalDate; @@ -19,16 +42,10 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.jdbc.core.JdbcTemplate; @RepositoryTest class ReviewDaoTest { - private static final MemberData JJANGGU = new MemberData(1L, "jjanggu", "https://image", "github.com"); - private static final MemberData GREENLAWN = new MemberData(2L, "greenlawn", "https://image", "github.com"); - private static final MemberData DWOO = new MemberData(3L, "dwoo", "https://image", "github.com"); - private static final MemberData VERUS = new MemberData(4L, "verus", "https://image", "github.com"); - @Autowired private MemberRepository memberRepository; @@ -36,7 +53,7 @@ class ReviewDaoTest { private StudyRepository studyRepository; @Autowired - private JdbcTemplate jdbcTemplate; + private ReviewRepository reviewRepository; @Autowired private EntityManager entityManager; @@ -52,89 +69,75 @@ class ReviewDaoTest { private List<ReviewData> reactReviews; + private Member 짱구; + private Member 그린론; + private Member 디우; + private Member 베루스; + @BeforeEach void setUp() { // 사용자 추가 - final Member jjanggu = memberRepository.save(toMember(JJANGGU)); - final Member greenlawn = memberRepository.save(toMember(GREENLAWN)); - final Member dwoo = memberRepository.save(toMember(DWOO)); - final Member verus = memberRepository.save(toMember(VERUS)); + 짱구 = memberRepository.save(짱구()); + 그린론 = memberRepository.save(그린론()); + 디우 = memberRepository.save(디우()); + 베루스 = memberRepository.save(베루스()); // 스터디 생성 StudyService createStudyService = new StudyService(studyRepository, memberRepository, new DateTimeSystem()); final LocalDate startDate = LocalDate.now(); - CreatingStudyRequest javaStudyRequest = CreatingStudyRequest.builder() - .title("java 스터디").excerpt("자바 설명").thumbnail("java image").description("자바 소개") - .startDate(startDate) - .build(); - CreatingStudyRequest reactStudyRequest = CreatingStudyRequest.builder() - .title("react 스터디").excerpt("리액트 설명").thumbnail("react image").description("리액트 소개") - .startDate(startDate) - .build(); + CreatingStudyRequest javaStudyRequest = 자바_스터디_신청서(startDate); + CreatingStudyRequest reactStudyRequest = 리액트_스터디_신청서(startDate); javaStudy = createStudyService.createStudy(1L, javaStudyRequest); reactStudy = createStudyService.createStudy(1L, reactStudyRequest); - final LocalDate createdDate = startDate.plusDays(1); - final LocalDate lastModifiedDate = startDate.plusDays(2); + // 리뷰 추가 + final Review firstJavaReview = reviewRepository.save(자바_리뷰1(javaStudy.getId(), 짱구.getId())); + final Review secondJavaReview = reviewRepository.save(자바_리뷰2(javaStudy.getId(), 그린론.getId())); + final Review thirdJavaReview = reviewRepository.save(자바_리뷰3(javaStudy.getId(), 디우.getId())); + final Review forthJavaReview = reviewRepository.save(자바_리뷰4(javaStudy.getId(), 베루스.getId())); + + final Review firstReactReview = reviewRepository.save(리액트_리뷰1(reactStudy.getId(), 짱구.getId())); + final Review secondReactReview = reviewRepository.save(리액트_리뷰2(reactStudy.getId(), 그린론.getId())); + final Review thirdReactReview = reviewRepository.save(리액트_리뷰3(reactStudy.getId(), 디우.getId())); entityManager.flush(); - entityManager.clear(); - // 리뷰 추가 - jdbcTemplate.update("INSERT INTO review(id, study_id, member_id, content, created_date, last_modified_date, deleted) " - + "VALUES (1, " + javaStudy.getId() + ", " + jjanggu.getId() + ", '리뷰 내용1', '" - + createdDate.toString() + "T11:23:30.123456', '" + lastModifiedDate.toString()+ "T11:45:20.456123', false)"); - jdbcTemplate.update("INSERT INTO review(id, study_id, member_id, content, created_date, last_modified_date, deleted) " - + "VALUES (2, " + javaStudy.getId() + ", " + greenlawn.getId() + ", '리뷰 내용2', '" - + createdDate.toString() + "T11:23:30.123456', '" + lastModifiedDate.toString()+ "T11:45:20.456123', false)"); - jdbcTemplate.update("INSERT INTO review(id, study_id, member_id, content, created_date, last_modified_date, deleted) " - + "VALUES (3, " + javaStudy.getId() + ", " + dwoo.getId()+ ", '리뷰 내용3', '" - + createdDate.toString() + "T11:23:30.123456', '" + lastModifiedDate.toString()+ "T11:45:20.456123', false)"); - jdbcTemplate.update("INSERT INTO review(id, study_id, member_id, content, created_date, last_modified_date, deleted) " - + "VALUES (4, " + javaStudy.getId() + ", " + verus.getId() + ", '리뷰 내용4', '" - + createdDate.toString() + "T11:23:30.123456', '" + lastModifiedDate.toString()+ "T11:45:20.456123', false)"); - jdbcTemplate.update("INSERT INTO review(id, study_id, member_id, content, created_date, last_modified_date, deleted) " - + "VALUES (5, " + reactStudy.getId() + ", " + jjanggu.getId() + ", '리뷰 내용5', '" - + createdDate.toString() + "T11:23:30.123456', '" + lastModifiedDate.toString()+ "T11:45:20.456123', false)"); - jdbcTemplate.update("INSERT INTO review(id, study_id, member_id, content, created_date, last_modified_date, deleted) " - + "VALUES (6, " + reactStudy.getId() + ", " + greenlawn.getId()+ ", '리뷰 내용6', '" - + createdDate.toString() + "T11:23:30.123456', '" + lastModifiedDate.toString()+ "T11:45:20.456123', false)"); - jdbcTemplate.update("INSERT INTO review(id, study_id, member_id, content, created_date, last_modified_date, deleted) " - + "VALUES (7, " + reactStudy.getId() + ", " + dwoo.getId()+ ", '리뷰 내용7', '" - + createdDate.toString() + "T11:23:30.123456', '" + lastModifiedDate.toString()+ "T11:45:20.456123', false)"); - - final ReviewData 리뷰_내용1 = new ReviewData(1L, JJANGGU, createdDate, lastModifiedDate, "리뷰 내용1"); - final ReviewData 리뷰_내용2 = new ReviewData(2L, GREENLAWN, createdDate, lastModifiedDate, "리뷰 내용2"); - final ReviewData 리뷰_내용3 = new ReviewData(3L, DWOO, createdDate, lastModifiedDate, "리뷰 내용3"); - final ReviewData 리뷰_내용4 = new ReviewData(4L, VERUS, createdDate, lastModifiedDate, "리뷰 내용4"); javaReviews = List.of( - 리뷰_내용4, - 리뷰_내용3, - 리뷰_내용2, - 리뷰_내용1 + new ReviewData(forthJavaReview.getId(), new MemberData(짱구.getGithubId(), 짱구.getUsername(), 짱구.getImageUrl(), 짱구.getProfileUrl()), + firstJavaReview.getCreatedDate().toLocalDate(), firstJavaReview.getLastModifiedDate().toLocalDate(), 자바_리뷰1_내용), + new ReviewData(thirdJavaReview.getId(), new MemberData(그린론.getGithubId(), 그린론.getUsername(), 그린론.getImageUrl(), 그린론.getProfileUrl()), + secondJavaReview.getCreatedDate().toLocalDate(), secondJavaReview.getLastModifiedDate().toLocalDate(), 자바_리뷰2_내용), + new ReviewData(secondJavaReview.getId(), new MemberData(디우.getGithubId(), 디우.getUsername(), 디우.getImageUrl(), 디우.getProfileUrl()), + thirdJavaReview.getCreatedDate().toLocalDate(), thirdJavaReview.getLastModifiedDate().toLocalDate(), 자바_리뷰3_내용), + new ReviewData(firstJavaReview.getId(), new MemberData(베루스.getGithubId(), 베루스.getUsername(), 베루스.getImageUrl(), 베루스.getProfileUrl()), + forthJavaReview.getCreatedDate().toLocalDate(), forthJavaReview.getLastModifiedDate().toLocalDate(), 자바_리뷰4_내용) ); - reactReviews = List.of( - new ReviewData(5L, JJANGGU, createdDate, lastModifiedDate, "리뷰 내용5"), - new ReviewData(6L, GREENLAWN, createdDate, lastModifiedDate, "리뷰 내용6"), - new ReviewData(7L, DWOO, createdDate, lastModifiedDate, "리뷰 내용7") + new ReviewData(firstReactReview.getId(), new MemberData(짱구.getGithubId(), 짱구.getUsername(), 짱구.getImageUrl(), 짱구.getProfileUrl()), + firstReactReview.getCreatedDate().toLocalDate(), firstReactReview.getLastModifiedDate().toLocalDate(), 리액트_리뷰1_내용), + new ReviewData(secondReactReview.getId(), new MemberData(그린론.getGithubId(), 그린론.getUsername(), 그린론.getImageUrl(), 그린론.getProfileUrl()), + secondReactReview.getCreatedDate().toLocalDate(), secondReactReview.getLastModifiedDate().toLocalDate(), 리액트_리뷰2_내용), + new ReviewData(thirdReactReview.getId(), new MemberData(디우.getGithubId(), 디우.getUsername(), 디우.getImageUrl(), 디우.getProfileUrl()), + thirdReactReview.getCreatedDate().toLocalDate(), thirdReactReview.getLastModifiedDate().toLocalDate(), 리액트_리뷰3_내용) ); } - private static Member toMember(MemberData memberData) { - return new Member(memberData.getGithubId(), memberData.getUsername(), memberData.getImageUrl(), - memberData.getProfileUrl()); - } - @DisplayName("스터디 Id로 작성된 후기를 조회한다.") @Test void findAllReviewsByStudyId() { List<ReviewData> reviews = sut.findAllByStudyId(javaStudy.getId()); assertThat(reviews).isNotEmpty(); - assertThat(reviews).hasSize(4); - assertThat(reviews).containsExactlyInAnyOrderElementsOf(javaReviews); + assertThat(reviews).hasSize(4) + .filteredOn(review -> review.getId() != null) + .extracting("member.githubId", "content") + .containsExactlyInAnyOrder( + tuple(javaReviews.get(0).getMember().getGithubId(), javaReviews.get(0).getContent()), + tuple(javaReviews.get(1).getMember().getGithubId(), javaReviews.get(1).getContent()), + tuple(javaReviews.get(2).getMember().getGithubId(), javaReviews.get(2).getContent()), + tuple(javaReviews.get(3).getMember().getGithubId(), javaReviews.get(3).getContent()) + ); } } diff --git a/backend/src/test/java/com/woowacourse/moamoa/study/controller/MyStudyControllerTest.java b/backend/src/test/java/com/woowacourse/moamoa/study/controller/MyStudyControllerTest.java index 6d93be425..6e39e9fc2 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/study/controller/MyStudyControllerTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/study/controller/MyStudyControllerTest.java @@ -1,30 +1,75 @@ package com.woowacourse.moamoa.study.controller; -import static com.woowacourse.moamoa.study.domain.StudyStatus.*; +import static com.woowacourse.moamoa.fixtures.MemberFixtures.그린론; +import static com.woowacourse.moamoa.fixtures.MemberFixtures.그린론_깃허브_아이디; +import static com.woowacourse.moamoa.fixtures.MemberFixtures.디우; +import static com.woowacourse.moamoa.fixtures.MemberFixtures.베루스; +import static com.woowacourse.moamoa.fixtures.MemberFixtures.짱구; +import static com.woowacourse.moamoa.fixtures.StudyFixtures.HTTP_스터디; +import static com.woowacourse.moamoa.fixtures.StudyFixtures.리눅스_스터디; +import static com.woowacourse.moamoa.fixtures.StudyFixtures.리눅스_스터디_계획; +import static com.woowacourse.moamoa.fixtures.StudyFixtures.리눅스_스터디_내용; +import static com.woowacourse.moamoa.fixtures.StudyFixtures.리눅스_스터디_모집계획; +import static com.woowacourse.moamoa.fixtures.StudyFixtures.리눅스_스터디_참가자들; +import static com.woowacourse.moamoa.fixtures.StudyFixtures.리눅스_스터디장; +import static com.woowacourse.moamoa.fixtures.StudyFixtures.리액트_스터디; +import static com.woowacourse.moamoa.fixtures.StudyFixtures.리액트_스터디_계획; +import static com.woowacourse.moamoa.fixtures.StudyFixtures.리액트_스터디_내용; +import static com.woowacourse.moamoa.fixtures.StudyFixtures.리액트_스터디_모집계획; +import static com.woowacourse.moamoa.fixtures.StudyFixtures.리액트_스터디_참가자들; +import static com.woowacourse.moamoa.fixtures.StudyFixtures.리액트_스터디장; +import static com.woowacourse.moamoa.fixtures.StudyFixtures.알고리즘_스터디; +import static com.woowacourse.moamoa.fixtures.StudyFixtures.알고리즘_스터디_계획; +import static com.woowacourse.moamoa.fixtures.StudyFixtures.알고리즘_스터디_내용; +import static com.woowacourse.moamoa.fixtures.StudyFixtures.알고리즘_스터디_모집계획; +import static com.woowacourse.moamoa.fixtures.StudyFixtures.알고리즘_스터디_참가자들; +import static com.woowacourse.moamoa.fixtures.StudyFixtures.알고리즘_스터디장; +import static com.woowacourse.moamoa.fixtures.StudyFixtures.자바_스터디; +import static com.woowacourse.moamoa.fixtures.StudyFixtures.자바_스터디_계획; +import static com.woowacourse.moamoa.fixtures.StudyFixtures.자바_스터디_내용; +import static com.woowacourse.moamoa.fixtures.StudyFixtures.자바_스터디_모집계획; +import static com.woowacourse.moamoa.fixtures.StudyFixtures.자바_스터디_참가자들; +import static com.woowacourse.moamoa.fixtures.StudyFixtures.자바_스터디장; +import static com.woowacourse.moamoa.fixtures.StudyFixtures.자바스크립트_스터디; +import static com.woowacourse.moamoa.fixtures.StudyFixtures.자바스크립트_스터디_계획; +import static com.woowacourse.moamoa.fixtures.StudyFixtures.자바스크립트_스터디_내용; +import static com.woowacourse.moamoa.fixtures.StudyFixtures.자바스크립트_스터디_모집계획; +import static com.woowacourse.moamoa.fixtures.StudyFixtures.자바스크립트_스터디_참가자들; +import static com.woowacourse.moamoa.fixtures.StudyFixtures.자바스크립트_스터디장; +import static com.woowacourse.moamoa.fixtures.TagFixtures.BE_태그_아이디; +import static com.woowacourse.moamoa.fixtures.TagFixtures.BE_태그명; +import static com.woowacourse.moamoa.fixtures.TagFixtures.FE_태그_아이디; +import static com.woowacourse.moamoa.fixtures.TagFixtures.FE_태그명; +import static com.woowacourse.moamoa.fixtures.TagFixtures.리액트_태그_아이디; +import static com.woowacourse.moamoa.fixtures.TagFixtures.리액트_태그명; +import static com.woowacourse.moamoa.fixtures.TagFixtures.우테코4기_태그_아이디; +import static com.woowacourse.moamoa.fixtures.TagFixtures.우테코4기_태그명; +import static com.woowacourse.moamoa.fixtures.TagFixtures.자바_태그_아이디; +import static com.woowacourse.moamoa.fixtures.TagFixtures.자바_태그명; +import static java.util.stream.Collectors.toList; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.tuple; import com.woowacourse.moamoa.common.RepositoryTest; +import com.woowacourse.moamoa.member.domain.Member; import com.woowacourse.moamoa.member.domain.repository.MemberRepository; import com.woowacourse.moamoa.member.query.data.MemberData; +import com.woowacourse.moamoa.study.domain.Study; import com.woowacourse.moamoa.study.domain.repository.StudyRepository; import com.woowacourse.moamoa.study.query.MyStudyDao; -import com.woowacourse.moamoa.study.service.response.MyStudyResponse; import com.woowacourse.moamoa.study.service.MyStudyService; import com.woowacourse.moamoa.study.service.response.MyStudiesResponse; +import com.woowacourse.moamoa.study.service.response.MyStudyResponse; import com.woowacourse.moamoa.tag.query.response.TagSummaryData; - -import java.time.LocalDateTime; -import java.util.stream.Collectors; import java.util.List; - +import java.util.Set; +import javax.persistence.EntityManager; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.jdbc.core.JdbcTemplate; @RepositoryTest class MyStudyControllerTest { @@ -39,130 +84,116 @@ class MyStudyControllerTest { private StudyRepository studyRepository; @Autowired - private JdbcTemplate jdbcTemplate; + private EntityManager entityManager; private MyStudyController myStudyController; + private Member 짱구; + private Member 그린론; + private Member 디우; + private Member 베루스; + + private Study 자바_스터디; + private Study 리액트_스터디; + private Study 자바스크립트_스터디; + private Study HTTP_스터디; + private Study 알고리즘_스터디; + private Study 리눅스_스터디; + @BeforeEach void setUp() { myStudyController = new MyStudyController(new MyStudyService(myStudyDao, memberRepository, studyRepository)); - jdbcTemplate.update("INSERT INTO member(id, github_id, username, image_url, profile_url) VALUES (1, 1, 'jjanggu', 'https://image', 'github.com')"); - jdbcTemplate.update("INSERT INTO member(id, github_id, username, image_url, profile_url) VALUES (2, 2, 'greenlawn', 'https://image', 'github.com')"); - jdbcTemplate.update("INSERT INTO member(id, github_id, username, image_url, profile_url) VALUES (3, 3, 'dwoo', 'https://image', 'github.com')"); - jdbcTemplate.update("INSERT INTO member(id, github_id, username, image_url, profile_url) VALUES (4, 4, 'verus', 'https://image', 'github.com')"); - - final LocalDateTime now = LocalDateTime.now(); - - jdbcTemplate.update("INSERT INTO study(id, title, excerpt, thumbnail, recruitment_status, study_status, description, current_member_count, max_member_count, created_at, start_date, owner_id) " - + "VALUES (1, 'Java 스터디', '자바 설명', 'java thumbnail', 'RECRUITMENT_START', 'PREPARE', '그린론의 우당탕탕 자바 스터디입니다.', 3, 10, '" - + now + "', '2021-12-08', 2)"); - jdbcTemplate.update("INSERT INTO study(id, title, excerpt, thumbnail, recruitment_status, study_status, description, current_member_count, max_member_count, created_at, enrollment_end_date, start_date, end_date, owner_id) " - + "VALUES (2, 'React 스터디', '리액트 설명', 'react thumbnail', 'RECRUITMENT_START', 'PREPARE', '디우의 뤼액트 스터디입니다.', 4, 5, '" - + now + "', '2021-11-09', '2021-11-10', '2021-12-08', 3)"); - jdbcTemplate.update("INSERT INTO study(id, title, excerpt, thumbnail, recruitment_status, study_status, description, current_member_count, max_member_count, created_at, start_date, owner_id) " - + "VALUES (3, 'javaScript 스터디', '자바스크립트 설명', 'javascript thumbnail', 'RECRUITMENT_START', 'PREPARE', '그린론의 자바스크립트 접해보기', 3, 20, '" - + now + "', '2022-08-03', 2)"); - jdbcTemplate.update("INSERT INTO study(id, title, excerpt, thumbnail, recruitment_status, study_status, description, max_member_count, created_at, start_date, owner_id) " - + "VALUES (4, 'HTTP 스터디', 'HTTP 설명', 'http thumbnail', 'RECRUITMENT_END', 'PREPARE', '디우의 HTTP 정복하기', 5, '" - + now + "', '2022-08-03', 3)"); - jdbcTemplate.update("INSERT INTO study(id, title, excerpt, thumbnail, recruitment_status, study_status, description, current_member_count, created_at, owner_id, start_date) " - + "VALUES (5, '알고리즘 스터디', '알고리즘 설명', 'algorithm thumbnail', 'RECRUITMENT_END', 'PREPARE', '알고리즘을 TDD로 풀자의 베루스입니다.', 1, '" - + now + "', 4, '2021-12-06')"); - jdbcTemplate.update("INSERT INTO study(id, title, excerpt, thumbnail, recruitment_status, study_status, description, current_member_count, created_at, owner_id, start_date, enrollment_end_date, end_date) " - + "VALUES (6, 'Linux 스터디', '리눅스 설명', 'linux thumbnail', 'RECRUITMENT_END', 'PREPARE', 'Linux를 공부하자의 베루스입니다.', 1, '" - + now + "', 4, '2021-12-06', '2021-12-07', '2022-01-07')"); - - jdbcTemplate.update("INSERT INTO study_tag(study_id, tag_id) VALUES (1, 1)"); - jdbcTemplate.update("INSERT INTO study_tag(study_id, tag_id) VALUES (1, 2)"); - jdbcTemplate.update("INSERT INTO study_tag(study_id, tag_id) VALUES (1, 3)"); - - jdbcTemplate.update("INSERT INTO study_tag(study_id, tag_id) VALUES (2, 2)"); - jdbcTemplate.update("INSERT INTO study_tag(study_id, tag_id) VALUES (2, 4)"); - jdbcTemplate.update("INSERT INTO study_tag(study_id, tag_id) VALUES (2, 5)"); - - jdbcTemplate.update("INSERT INTO study_tag(study_id, tag_id) VALUES (3, 2)"); - jdbcTemplate.update("INSERT INTO study_tag(study_id, tag_id) VALUES (3, 4)"); - - jdbcTemplate.update("INSERT INTO study_tag(study_id, tag_id) VALUES (4, 2)"); - jdbcTemplate.update("INSERT INTO study_tag(study_id, tag_id) VALUES (4, 3)"); - - jdbcTemplate.update("INSERT INTO study_member(study_id, member_id) VALUES (1, 3)"); - jdbcTemplate.update("INSERT INTO study_member(study_id, member_id) VALUES (1, 4)"); - - jdbcTemplate.update("INSERT INTO study_member(study_id, member_id) VALUES (2, 1)"); - jdbcTemplate.update("INSERT INTO study_member(study_id, member_id) VALUES (2, 2)"); - jdbcTemplate.update("INSERT INTO study_member(study_id, member_id) VALUES (2, 4)"); - - jdbcTemplate.update("INSERT INTO study_member(study_id, member_id) VALUES (3, 3)"); - jdbcTemplate.update("INSERT INTO study_member(study_id, member_id) VALUES (3, 4)"); + 짱구 = memberRepository.save(짱구()); + 그린론 = memberRepository.save(그린론()); + 디우 = memberRepository.save(디우()); + 베루스 = memberRepository.save(베루스()); + + 자바_스터디 = studyRepository.save(자바_스터디(짱구.getId(), Set.of(그린론.getId(), 디우.getId()))); + 리액트_스터디 = studyRepository.save(리액트_스터디(디우.getId(), Set.of(짱구.getId(), 그린론.getId(), 베루스.getId()))); + 자바스크립트_스터디 = studyRepository.save(자바스크립트_스터디(그린론.getId(), Set.of(디우.getId(), 베루스.getId()))); + HTTP_스터디 = studyRepository.save(HTTP_스터디(디우.getId(), Set.of(베루스.getId(), 짱구.getId()))); + 알고리즘_스터디 = studyRepository.save(알고리즘_스터디(베루스.getId(), Set.of(그린론.getId(), 디우.getId()))); + 리눅스_스터디 = studyRepository.save(리눅스_스터디(베루스.getId(), Set.of(그린론.getId(), 디우.getId()))); + + entityManager.flush(); } @DisplayName("내가 참여한 스터디를 조회한다.") @Test void getMyStudies() { - final ResponseEntity<MyStudiesResponse> myStudies = myStudyController.getMyStudies(4L); + final ResponseEntity<MyStudiesResponse> myStudies = myStudyController.getMyStudies(그린론_깃허브_아이디); assertThat(myStudies.getStatusCode()).isEqualTo(HttpStatus.OK); assertThat(myStudies.getBody()).isNotNull(); assertThat(myStudies.getBody().getStudies()) .hasSize(5) - .extracting("id", "title", "studyStatus", "currentMemberCount", "maxMemberCount") + .filteredOn(myStudy -> myStudy.getId() != null) + .extracting("title", "studyStatus", "currentMemberCount", "maxMemberCount") .containsExactlyElementsOf(List.of( - tuple(1L, "Java 스터디", PREPARE, 3, 10), - tuple(2L, "React 스터디", PREPARE, 4, 5), - tuple(3L, "javaScript 스터디", PREPARE, 3, 20), - tuple(5L, "알고리즘 스터디", PREPARE, 1, null), - tuple(6L, "Linux 스터디", PREPARE, 1, null)) - ); + tuple(자바_스터디_내용.getTitle(), 자바_스터디_계획.getStudyStatus(), 자바_스터디_참가자들.getSize(), + 자바_스터디_모집계획.getMax()), + tuple(리액트_스터디_내용.getTitle(), 리액트_스터디_계획.getStudyStatus(), 리액트_스터디_참가자들.getSize(), + 리액트_스터디_모집계획.getMax()), + tuple(자바스크립트_스터디_내용.getTitle(), 자바스크립트_스터디_계획.getStudyStatus(), 자바스크립트_스터디_참가자들.getSize(), + 자바스크립트_스터디_모집계획.getMax()), + tuple(알고리즘_스터디_내용.getTitle(), 알고리즘_스터디_계획.getStudyStatus(), 알고리즘_스터디_참가자들.getSize(), + 알고리즘_스터디_모집계획.getMax()), + tuple(리눅스_스터디_내용.getTitle(), 리눅스_스터디_계획.getStudyStatus(), 리눅스_스터디_참가자들.getSize(), + 리눅스_스터디_모집계획.getMax()) + )); final List<MemberData> owners = myStudies.getBody() .getStudies() .stream() .map(MyStudyResponse::getOwner) - .collect(Collectors.toList()); + .collect(toList()); assertThat(owners) .hasSize(5) .extracting("githubId", "username", "imageUrl", "profileUrl") - .containsExactlyElementsOf(List.of( - tuple(2L, "greenlawn", "https://image", "github.com"), - tuple(3L, "dwoo", "https://image", "github.com"), - tuple(2L, "greenlawn", "https://image", "github.com"), - tuple(4L, "verus", "https://image", "github.com"), - tuple(4L, "verus", "https://image", "github.com") - )); + .containsExactlyInAnyOrder( + tuple(자바_스터디장.getGithubId(), 자바_스터디장.getUsername(), 자바_스터디장.getImageUrl(), 자바_스터디장.getProfileUrl()), + tuple(리액트_스터디장.getGithubId(), 리액트_스터디장.getUsername(), 리액트_스터디장.getImageUrl(), 리액트_스터디장.getProfileUrl()), + tuple(자바스크립트_스터디장.getGithubId(), 자바스크립트_스터디장.getUsername(), 자바스크립트_스터디장.getImageUrl(), 자바스크립트_스터디장.getProfileUrl()), + tuple(알고리즘_스터디장.getGithubId(), 알고리즘_스터디장.getUsername(), 알고리즘_스터디장.getImageUrl(), 알고리즘_스터디장.getProfileUrl()), + tuple(리눅스_스터디장.getGithubId(), 리눅스_스터디장.getUsername(), 리눅스_스터디장.getImageUrl(), 리눅스_스터디장.getProfileUrl()) + ); final List<List<TagSummaryData>> tags = myStudies.getBody() .getStudies() .stream() .map(MyStudyResponse::getTags) - .collect(Collectors.toList()); + .collect(toList()); assertThat(tags.get(0)) .hasSize(3) .extracting("id", "name") .containsExactlyElementsOf(List.of( - tuple(1L, "Java"), - tuple(2L, "4기"), - tuple(3L, "BE")) + tuple(자바_태그_아이디, 자바_태그명), + tuple(우테코4기_태그_아이디, 우테코4기_태그명), + tuple(BE_태그_아이디, BE_태그명)) ); assertThat(tags.get(1)) .hasSize(3) .extracting("id", "name") - .contains(tuple(5L, "React"), - tuple(2L, "4기"), - tuple(4L, "FE")); + .contains( + tuple(리액트_태그_아이디, 리액트_태그명), + tuple(우테코4기_태그_아이디, 우테코4기_태그명), + tuple(FE_태그_아이디, FE_태그명) + ); assertThat(tags.get(2)) .hasSize(2) .extracting("id", "name") - .contains(tuple(2L, "4기"), - tuple(4L, "FE")); + .contains( + tuple(우테코4기_태그_아이디, 우테코4기_태그명), + tuple(FE_태그_아이디, FE_태그명) + ); - assertThat(tags.get(3).size()).isZero(); + assertThat(tags.get(3).size()).isZero(); assertThat(tags.get(4).size()).isZero(); } } diff --git a/backend/src/test/java/com/woowacourse/moamoa/study/controller/SearchingStudyControllerTest.java b/backend/src/test/java/com/woowacourse/moamoa/study/controller/SearchingStudyControllerTest.java index 8d53effb9..de29f5ba4 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/study/controller/SearchingStudyControllerTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/study/controller/SearchingStudyControllerTest.java @@ -1,5 +1,10 @@ package com.woowacourse.moamoa.study.controller; +import static com.woowacourse.moamoa.fixtures.StudyFixtures.HTTP_스터디_신청서; +import static com.woowacourse.moamoa.fixtures.StudyFixtures.리액트_스터디_신청서; +import static com.woowacourse.moamoa.fixtures.StudyFixtures.알고리즘_스터디_신청서; +import static com.woowacourse.moamoa.fixtures.StudyFixtures.자바_스터디_신청서; +import static com.woowacourse.moamoa.fixtures.StudyFixtures.자바스크립트_스터디_신청서; import static java.util.Collections.emptyList; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.tuple; @@ -32,7 +37,6 @@ import org.springframework.data.domain.PageRequest; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.jdbc.core.JdbcTemplate; @RepositoryTest public class SearchingStudyControllerTest { @@ -80,35 +84,19 @@ void initDataBase() { StudyService studyService = new StudyService(studyRepository, memberRepository, new DateTimeSystem()); - CreatingStudyRequest javaStudyRequest = CreatingStudyRequest.builder() - .title("Java 스터디").excerpt("자바 설명").thumbnail("java thumbnail").description("그린론의 우당탕탕 자바 스터디입니다.") - .startDate(LocalDate.now()).tagIds(List.of(1L, 2L, 3L)).maxMemberCount(10) - .build(); + CreatingStudyRequest javaStudyRequest = 자바_스터디_신청서(List.of(1L, 2L, 3L), 10, LocalDate.now()); javaStudyId = studyService.createStudy(jjanggu.getGithubId(), javaStudyRequest).getId(); - CreatingStudyRequest reactStudyRequest = CreatingStudyRequest.builder() - .title("React 스터디").excerpt("리액트 설명").thumbnail("react thumbnail").description("디우의 뤼액트 스터디입니다.") - .startDate(LocalDate.now()).endDate(LocalDate.now()).enrollmentEndDate(LocalDate.now()) - .tagIds(List.of(2L, 4L, 5L)).maxMemberCount(5) - .build(); + CreatingStudyRequest reactStudyRequest = 리액트_스터디_신청서(List.of(2L, 4L, 5L), 5, LocalDate.now()); reactStudyId = studyService.createStudy(dwoo.getGithubId(), reactStudyRequest).getId(); - CreatingStudyRequest javaScriptStudyRequest = CreatingStudyRequest.builder() - .title("javaScript 스터디").excerpt("자바스크립트 설명").thumbnail("javascript thumbnail").description("자바스크립트 설명") - .startDate(LocalDate.now()).tagIds(List.of(2L, 4L)) - .build(); + CreatingStudyRequest javaScriptStudyRequest = 자바스크립트_스터디_신청서(List.of(2L, 4L), LocalDate.now()); javaScriptId = studyService.createStudy(jjanggu.getGithubId(), javaScriptStudyRequest).getId(); - CreatingStudyRequest httpStudyRequest = CreatingStudyRequest.builder() - .title("HTTP 스터디").excerpt("HTTP 설명").thumbnail("http thumbnail").description("HTTP 설명") - .startDate(LocalDate.now()).tagIds(List.of(2L, 3L)) - .build(); + CreatingStudyRequest httpStudyRequest = HTTP_스터디_신청서(List.of(2L, 3L), LocalDate.now()); httpStudyId = studyService.createStudy(jjanggu.getGithubId(), httpStudyRequest).getId(); - CreatingStudyRequest algorithmStudyRequest = CreatingStudyRequest.builder() - .title("알고리즘 스터디").excerpt("알고리즘 설명").thumbnail("algorithm thumbnail").description("알고리즘 설명") - .startDate(LocalDate.now()).tagIds(List.of()) - .build(); + CreatingStudyRequest algorithmStudyRequest = 알고리즘_스터디_신청서(List.of(), LocalDate.now()); algorithmStudyId = studyService.createStudy(jjanggu.getGithubId(), algorithmStudyRequest).getId(); CreatingStudyRequest linuxStudyRequest = CreatingStudyRequest.builder() diff --git a/backend/src/test/java/com/woowacourse/moamoa/study/controller/StudyControllerTest.java b/backend/src/test/java/com/woowacourse/moamoa/study/controller/StudyControllerTest.java index 502528013..053af15c9 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/study/controller/StudyControllerTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/study/controller/StudyControllerTest.java @@ -1,5 +1,6 @@ package com.woowacourse.moamoa.study.controller; +import static com.woowacourse.moamoa.study.domain.RecruitStatus.RECRUITMENT_END; import static com.woowacourse.moamoa.study.domain.StudyStatus.PREPARE; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -188,6 +189,35 @@ public void participateStudy() { assertThat(response.getStatusCode()).isEqualTo(OK); } + @DisplayName("최대인원이 한 명인 경우 바로 모집 종료가 되어야 한다.") + @Test + public void createdStudyWithMaxSizeOne() { + // given + StudyController studyController = new StudyController(new StudyService(studyRepository, memberRepository, + new DateTimeSystem())); + final CreatingStudyRequest creatingStudyRequest = CreatingStudyRequest.builder() + .title("Java") + .excerpt("java excerpt") + .thumbnail("java image") + .description("자바 스터디 상세설명 입니다.") + .startDate(LocalDate.now().plusDays(1)) + .endDate(LocalDate.now().plusDays(4)) + .enrollmentEndDate(LocalDate.now().plusDays(2)) + .maxMemberCount(1) + .tagIds(List.of(1L, 2L)) + .build(); + + final ResponseEntity<Void> createdResponse = studyController.createStudy(1L, creatingStudyRequest); + + // when + final String location = createdResponse.getHeaders().getLocation().getPath(); + final long studyId = getStudyIdBy(location); + final Study study = studyRepository.findById(studyId).orElseThrow(); + + // then + assertThat(study.getRecruitPlanner().getRecruitStatus()).isEqualTo(RECRUITMENT_END); + } + private long getStudyIdBy(final String location) { final String[] splitLocation = location.split("/"); return Long.parseLong(splitLocation[3]); diff --git a/backend/src/test/java/com/woowacourse/moamoa/study/query/MyStudyDaoTest.java b/backend/src/test/java/com/woowacourse/moamoa/study/query/MyStudyDaoTest.java index e027cbe9c..8887bae55 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/study/query/MyStudyDaoTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/study/query/MyStudyDaoTest.java @@ -1,25 +1,54 @@ package com.woowacourse.moamoa.study.query; -import static com.woowacourse.moamoa.study.domain.StudyStatus.PREPARE; +import static com.woowacourse.moamoa.fixtures.MemberFixtures.그린론; +import static com.woowacourse.moamoa.fixtures.MemberFixtures.디우; +import static com.woowacourse.moamoa.fixtures.MemberFixtures.베루스; +import static com.woowacourse.moamoa.fixtures.MemberFixtures.짱구; +import static com.woowacourse.moamoa.fixtures.StudyFixtures.HTTP_스터디; +import static com.woowacourse.moamoa.fixtures.StudyFixtures.리눅스_스터디; +import static com.woowacourse.moamoa.fixtures.StudyFixtures.리눅스_스터디_계획; +import static com.woowacourse.moamoa.fixtures.StudyFixtures.리눅스_스터디_내용; +import static com.woowacourse.moamoa.fixtures.StudyFixtures.리눅스_스터디_모집계획; +import static com.woowacourse.moamoa.fixtures.StudyFixtures.리눅스_스터디_참가자들; +import static com.woowacourse.moamoa.fixtures.StudyFixtures.리액트_스터디; +import static com.woowacourse.moamoa.fixtures.StudyFixtures.리액트_스터디_계획; +import static com.woowacourse.moamoa.fixtures.StudyFixtures.리액트_스터디_내용; +import static com.woowacourse.moamoa.fixtures.StudyFixtures.리액트_스터디_모집계획; +import static com.woowacourse.moamoa.fixtures.StudyFixtures.리액트_스터디_참가자들; +import static com.woowacourse.moamoa.fixtures.StudyFixtures.알고리즘_스터디; +import static com.woowacourse.moamoa.fixtures.StudyFixtures.알고리즘_스터디_계획; +import static com.woowacourse.moamoa.fixtures.StudyFixtures.알고리즘_스터디_내용; +import static com.woowacourse.moamoa.fixtures.StudyFixtures.알고리즘_스터디_모집계획; +import static com.woowacourse.moamoa.fixtures.StudyFixtures.알고리즘_스터디_참가자들; +import static com.woowacourse.moamoa.fixtures.StudyFixtures.자바_스터디; +import static com.woowacourse.moamoa.fixtures.StudyFixtures.자바_스터디_계획; +import static com.woowacourse.moamoa.fixtures.StudyFixtures.자바_스터디_내용; +import static com.woowacourse.moamoa.fixtures.StudyFixtures.자바_스터디_모집계획; +import static com.woowacourse.moamoa.fixtures.StudyFixtures.자바_스터디_참가자들; +import static com.woowacourse.moamoa.fixtures.StudyFixtures.자바스크립트_스터디; +import static com.woowacourse.moamoa.fixtures.StudyFixtures.자바스크립트_스터디_계획; +import static com.woowacourse.moamoa.fixtures.StudyFixtures.자바스크립트_스터디_내용; +import static com.woowacourse.moamoa.fixtures.StudyFixtures.자바스크립트_스터디_모집계획; +import static com.woowacourse.moamoa.fixtures.StudyFixtures.자바스크립트_스터디_참가자들; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.tuple; +import com.woowacourse.moamoa.common.RepositoryTest; +import com.woowacourse.moamoa.member.domain.Member; +import com.woowacourse.moamoa.member.domain.repository.MemberRepository; import com.woowacourse.moamoa.member.query.data.MemberData; +import com.woowacourse.moamoa.study.domain.Study; +import com.woowacourse.moamoa.study.domain.repository.StudyRepository; +import com.woowacourse.moamoa.study.query.data.MyStudySummaryData; import com.woowacourse.moamoa.tag.query.response.TagSummaryData; import java.util.List; - -import com.woowacourse.moamoa.common.RepositoryTest; -import com.woowacourse.moamoa.study.query.data.MyStudySummaryData; - -import java.time.LocalDateTime; - import java.util.Map; +import java.util.Set; +import javax.persistence.EntityManager; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; - import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.jdbc.core.JdbcTemplate; @RepositoryTest class MyStudyDaoTest { @@ -28,72 +57,67 @@ class MyStudyDaoTest { private MyStudyDao myStudyDao; @Autowired - private JdbcTemplate jdbcTemplate; + private MemberRepository memberRepository; + + @Autowired + private StudyRepository studyRepository; + + @Autowired + private EntityManager entityManager; + + private Member 짱구; + private Member 그린론; + private Member 디우; + private Member 베루스; + + private Study 자바_스터디; + private Study 리액트_스터디; + private Study 자바스크립트_스터디; + private Study HTTP_스터디; + private Study 알고리즘_스터디; + private Study 리눅스_스터디; @BeforeEach void initDataBase() { - jdbcTemplate.update("INSERT INTO member(id, github_id, username, image_url, profile_url) VALUES (1, 1, 'jjanggu', 'https://image', 'github.com')"); - jdbcTemplate.update("INSERT INTO member(id, github_id, username, image_url, profile_url) VALUES (2, 2, 'greenlawn', 'https://image', 'github.com')"); - jdbcTemplate.update("INSERT INTO member(id, github_id, username, image_url, profile_url) VALUES (3, 3, 'dwoo', 'https://image', 'github.com')"); - jdbcTemplate.update("INSERT INTO member(id, github_id, username, image_url, profile_url) VALUES (4, 4, 'verus', 'https://image', 'github.com')"); - - final LocalDateTime now = LocalDateTime.now(); - - jdbcTemplate.update("INSERT INTO study(id, title, excerpt, thumbnail, recruitment_status, study_status, description, current_member_count, max_member_count, created_at, start_date, owner_id) " - + "VALUES (1, 'Java 스터디', '자바 설명', 'java thumbnail', 'RECRUITMENT_START', 'PREPARE', '그린론의 우당탕탕 자바 스터디입니다.', 3, 10, '" + now + "', '2021-12-08', 2)"); - jdbcTemplate.update("INSERT INTO study(id, title, excerpt, thumbnail, recruitment_status, study_status, description, current_member_count, max_member_count, created_at, enrollment_end_date, start_date, end_date, owner_id) " - + "VALUES (2, 'React 스터디', '리액트 설명', 'react thumbnail', 'RECRUITMENT_START', 'PREPARE', '디우의 뤼액트 스터디입니다.', 4, 5, '" + now + "', '2021-11-09', '2021-11-10', '2021-12-08', 3)"); - jdbcTemplate.update("INSERT INTO study(id, title, excerpt, thumbnail, recruitment_status, study_status, description, current_member_count, max_member_count, created_at, start_date, owner_id) " - + "VALUES (3, 'javaScript 스터디', '자바스크립트 설명', 'javascript thumbnail', 'RECRUITMENT_START', 'PREPARE', '그린론의 자바스크립트 접해보기', 3, 20, '" + now + "', '2022-08-03', 2)"); - jdbcTemplate.update("INSERT INTO study(id, title, excerpt, thumbnail, recruitment_status, study_status, description, max_member_count, created_at, start_date, owner_id) " - + "VALUES (4, 'HTTP 스터디', 'HTTP 설명', 'http thumbnail', 'RECRUITMENT_END', 'PREPARE', '디우의 HTTP 정복하기', 5, '" + now + "', '2022-08-03', 3)"); - jdbcTemplate.update("INSERT INTO study(id, title, excerpt, thumbnail, recruitment_status, study_status, description, current_member_count, created_at, owner_id, start_date) " - + "VALUES (5, '알고리즘 스터디', '알고리즘 설명', 'algorithm thumbnail', 'RECRUITMENT_END', 'PREPARE', '알고리즘을 TDD로 풀자의 베루스입니다.', 1, '" + now + "', 4, '2021-12-06')"); - jdbcTemplate.update("INSERT INTO study(id, title, excerpt, thumbnail, recruitment_status, study_status, description, current_member_count, created_at, owner_id, start_date, enrollment_end_date, end_date) " - + "VALUES (6, 'Linux 스터디', '리눅스 설명', 'linux thumbnail', 'RECRUITMENT_END', 'PREPARE', 'Linux를 공부하자의 베루스입니다.', 1, '" + now + "', 4, '2021-12-06', '2021-12-07', '2022-01-07')"); - jdbcTemplate.update("INSERT INTO study(id, title, excerpt, thumbnail, recruitment_status, study_status, description, current_member_count, created_at, owner_id, start_date, enrollment_end_date, end_date) " - + "VALUES (7, 'OS 스터디', 'OS 설명', 'os thumbnail', 'RECRUITMENT_END', 'PREPARE', 'OS를 공부하자의 그린론입니다.', 1, '" + now + "', 2, '2021-12-06', '2021-12-07', '2022-01-07')"); - - - jdbcTemplate.update("INSERT INTO study_tag(study_id, tag_id) VALUES (1, 1)"); - jdbcTemplate.update("INSERT INTO study_tag(study_id, tag_id) VALUES (1, 2)"); - jdbcTemplate.update("INSERT INTO study_tag(study_id, tag_id) VALUES (1, 3)"); - - jdbcTemplate.update("INSERT INTO study_tag(study_id, tag_id) VALUES (2, 2)"); - jdbcTemplate.update("INSERT INTO study_tag(study_id, tag_id) VALUES (2, 4)"); - jdbcTemplate.update("INSERT INTO study_tag(study_id, tag_id) VALUES (2, 5)"); - - jdbcTemplate.update("INSERT INTO study_tag(study_id, tag_id) VALUES (3, 2)"); - jdbcTemplate.update("INSERT INTO study_tag(study_id, tag_id) VALUES (3, 4)"); - - jdbcTemplate.update("INSERT INTO study_tag(study_id, tag_id) VALUES (4, 2)"); - jdbcTemplate.update("INSERT INTO study_tag(study_id, tag_id) VALUES (4, 3)"); - - jdbcTemplate.update("INSERT INTO study_member(study_id, member_id) VALUES (1, 3)"); - jdbcTemplate.update("INSERT INTO study_member(study_id, member_id) VALUES (1, 4)"); - - jdbcTemplate.update("INSERT INTO study_member(study_id, member_id) VALUES (2, 1)"); - jdbcTemplate.update("INSERT INTO study_member(study_id, member_id) VALUES (2, 2)"); - jdbcTemplate.update("INSERT INTO study_member(study_id, member_id) VALUES (2, 4)"); - - jdbcTemplate.update("INSERT INTO study_member(study_id, member_id) VALUES (3, 3)"); - jdbcTemplate.update("INSERT INTO study_member(study_id, member_id) VALUES (3, 4)"); + 짱구 = memberRepository.save(짱구()); + 그린론 = memberRepository.save(그린론()); + 디우 = memberRepository.save(디우()); + 베루스 = memberRepository.save(베루스()); + + 자바_스터디 = studyRepository.save(자바_스터디(짱구.getId(), Set.of(그린론.getId(), 디우.getId()))); + 리액트_스터디 = studyRepository.save(리액트_스터디(디우.getId(), Set.of(짱구.getId(), 그린론.getId(), 베루스.getId()))); + 자바스크립트_스터디 = studyRepository.save(자바스크립트_스터디(그린론.getId(), Set.of(디우.getId(), 베루스.getId()))); + HTTP_스터디 = studyRepository.save(HTTP_스터디(디우.getId(), Set.of(베루스.getId(), 짱구.getId()))); + 알고리즘_스터디 = studyRepository.save(알고리즘_스터디(베루스.getId(), Set.of(그린론.getId(), 디우.getId()))); + 리눅스_스터디 = studyRepository.save(리눅스_스터디(베루스.getId(), Set.of(그린론.getId(), 디우.getId()))); + + entityManager.flush(); } @DisplayName("내가 참여한 스터디 목록을 조회한다.") @Test void getMyStudies() { - final List<MyStudySummaryData> studySummaryData = myStudyDao.findMyStudyByMemberId(2L); + final List<MyStudySummaryData> studySummaryData = myStudyDao.findMyStudyByMemberId(그린론.getId()); assertThat(studySummaryData) - .hasSize(4) + .hasSize(5) .filteredOn(myStudySummaryData -> myStudySummaryData.getId() != null) .extracting("title", "studyStatus", "currentMemberCount", "maxMemberCount", "startDate", "endDate") - .contains( - tuple("Java 스터디", PREPARE, 3, 10, "2021-12-08", null), - tuple("javaScript 스터디" ,PREPARE, 3, 20, "2022-08-03", null), - tuple("React 스터디", PREPARE, 4, 5, "2021-11-10", "2021-12-08"), - tuple("OS 스터디", PREPARE, 1, null, "2021-12-06", "2022-01-07") + .containsExactlyInAnyOrder( + tuple(자바_스터디_내용.getTitle(), 자바_스터디_계획.getStudyStatus(), 자바_스터디_참가자들.getSize(), + 자바_스터디_모집계획.getMax(), 자바_스터디_계획.getStartDate().toString(), + 자바_스터디_계획.getEndDate().toString()), + tuple(리액트_스터디_내용.getTitle(), 리액트_스터디_계획.getStudyStatus(), 리액트_스터디_참가자들.getSize(), + 리액트_스터디_모집계획.getMax(), 리액트_스터디_계획.getStartDate().toString(), + 자바_스터디_계획.getEndDate().toString()), + tuple(자바스크립트_스터디_내용.getTitle(), 자바스크립트_스터디_계획.getStudyStatus(), 자바스크립트_스터디_참가자들.getSize(), + 자바스크립트_스터디_모집계획.getMax(), 자바스크립트_스터디_계획.getStartDate().toString(), + 자바스크립트_스터디_계획.getEndDate().toString()), + tuple(알고리즘_스터디_내용.getTitle(), 알고리즘_스터디_계획.getStudyStatus(), 알고리즘_스터디_참가자들.getSize(), + 알고리즘_스터디_모집계획.getMax(), 알고리즘_스터디_계획.getStartDate().toString(), null), + tuple(리눅스_스터디_내용.getTitle(), 리눅스_스터디_계획.getStudyStatus(), 리눅스_스터디_참가자들.getSize(), + 리눅스_스터디_모집계획.getMax(), 리눅스_스터디_계획.getStartDate().toString(), + 리눅스_스터디_계획.getEndDate().toString()) ); } diff --git a/backend/src/test/java/com/woowacourse/moamoa/study/query/StudyDetailsDaoTest.java b/backend/src/test/java/com/woowacourse/moamoa/study/query/StudyDetailsDaoTest.java index c0cf96dd5..267a6b47a 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/study/query/StudyDetailsDaoTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/study/query/StudyDetailsDaoTest.java @@ -1,18 +1,30 @@ package com.woowacourse.moamoa.study.query; +import static com.woowacourse.moamoa.fixtures.MemberFixtures.그린론; +import static com.woowacourse.moamoa.fixtures.MemberFixtures.디우; +import static com.woowacourse.moamoa.fixtures.MemberFixtures.베루스; +import static com.woowacourse.moamoa.fixtures.MemberFixtures.짱구; +import static com.woowacourse.moamoa.fixtures.StudyFixtures.HTTP_스터디; +import static com.woowacourse.moamoa.fixtures.StudyFixtures.리눅스_스터디; +import static com.woowacourse.moamoa.fixtures.StudyFixtures.리액트_스터디; +import static com.woowacourse.moamoa.fixtures.StudyFixtures.알고리즘_스터디; +import static com.woowacourse.moamoa.fixtures.StudyFixtures.자바_스터디; +import static com.woowacourse.moamoa.fixtures.StudyFixtures.자바스크립트_스터디; import static org.assertj.core.api.Assertions.assertThat; import com.woowacourse.moamoa.common.RepositoryTest; +import com.woowacourse.moamoa.member.domain.Member; +import com.woowacourse.moamoa.member.domain.repository.MemberRepository; import com.woowacourse.moamoa.member.query.data.MemberData; +import com.woowacourse.moamoa.study.domain.Study; +import com.woowacourse.moamoa.study.domain.repository.StudyRepository; import com.woowacourse.moamoa.study.query.data.StudyDetailsData; -import com.woowacourse.moamoa.study.service.exception.StudyNotFoundException; -import java.time.LocalDate; -import java.time.LocalDateTime; +import java.util.Set; +import javax.persistence.EntityManager; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.jdbc.core.JdbcTemplate; @RepositoryTest class StudyDetailsDaoTest { @@ -21,53 +33,41 @@ class StudyDetailsDaoTest { private StudyDetailsDao sut; @Autowired - private JdbcTemplate jdbcTemplate; + private StudyRepository studyRepository; + + @Autowired + private MemberRepository memberRepository; + + @Autowired + private EntityManager entityManager; + + private Member 짱구; + private Member 그린론; + private Member 디우; + private Member 베루스; + + private Study 자바_스터디; + private Study 리액트_스터디; + private Study 자바스크립트_스터디; + private Study HTTP_스터디; + private Study 알고리즘_스터디; + private Study 리눅스_스터디; @BeforeEach void initDataBase() { - jdbcTemplate.update("INSERT INTO member(id, github_id, username, image_url, profile_url) VALUES (1, 1, 'jjanggu', 'https://image', 'github.com')"); - jdbcTemplate.update("INSERT INTO member(id, github_id, username, image_url, profile_url) VALUES (2, 2, 'greenlawn', 'https://image', 'github.com')"); - jdbcTemplate.update("INSERT INTO member(id, github_id, username, image_url, profile_url) VALUES (3, 3, 'dwoo', 'https://image', 'github.com')"); - jdbcTemplate.update("INSERT INTO member(id, github_id, username, image_url, profile_url) VALUES (4, 4, 'verus', 'https://image', 'github.com')"); - - final LocalDateTime now = LocalDateTime.now(); - - jdbcTemplate.update("INSERT INTO study(id, title, excerpt, thumbnail, recruitment_status, study_status, description, current_member_count, max_member_count, created_at, start_date, owner_id) " - + "VALUES (1, 'Java 스터디', '자바 설명', 'java thumbnail', 'RECRUITMENT_START', 'PREPARE', '그린론의 우당탕탕 자바 스터디입니다.', 3, 10, '" + now + "', '2021-12-08', 2)"); - jdbcTemplate.update("INSERT INTO study(id, title, excerpt, thumbnail, recruitment_status, study_status, description, current_member_count, max_member_count, created_at, enrollment_end_date, start_date, end_date, owner_id) " - + "VALUES (2, 'React 스터디', '리액트 설명', 'react thumbnail', 'RECRUITMENT_START', 'PREPARE', '디우의 뤼액트 스터디입니다.', 4, 5, '" + now + "', '2021-11-09', '2021-11-10', '2021-12-08', 3)"); - jdbcTemplate.update("INSERT INTO study(id, title, excerpt, thumbnail, recruitment_status, study_status, description, current_member_count, max_member_count, created_at, start_date, owner_id) " - + "VALUES (3, 'javaScript 스터디', '자바스크립트 설명', 'javascript thumbnail', 'RECRUITMENT_START', 'PREPARE', '그린론의 자바스크립트 접해보기', 3, 20, '" + now + "', '2022-08-03', 2)"); - jdbcTemplate.update("INSERT INTO study(id, title, excerpt, thumbnail, recruitment_status, study_status, description, max_member_count, created_at, start_date, owner_id) " - + "VALUES (4, 'HTTP 스터디', 'HTTP 설명', 'http thumbnail', 'RECRUITMENT_END', 'PREPARE', '디우의 HTTP 정복하기', 5, '" + now + "', '2022-08-03', 3)"); - jdbcTemplate.update("INSERT INTO study(id, title, excerpt, thumbnail, recruitment_status, study_status, description, current_member_count, created_at, owner_id, start_date) " - + "VALUES (5, '알고리즘 스터디', '알고리즘 설명', 'algorithm thumbnail', 'RECRUITMENT_END', 'PREPARE', '알고리즘을 TDD로 풀자의 베루스입니다.', 1, '" + now + "', 4, '2021-12-06')"); - jdbcTemplate.update("INSERT INTO study(id, title, excerpt, thumbnail, recruitment_status, study_status, description, current_member_count, created_at, owner_id, start_date, enrollment_end_date, end_date) " - + "VALUES (6, 'Linux 스터디', '리눅스 설명', 'linux thumbnail', 'RECRUITMENT_END', 'PREPARE', 'Linux를 공부하자의 베루스입니다.', 1, '" + now + "', 4, '2021-12-06', '2021-12-07', '2022-01-07')"); - - jdbcTemplate.update("INSERT INTO study_tag(study_id, tag_id) VALUES (1, 1)"); - jdbcTemplate.update("INSERT INTO study_tag(study_id, tag_id) VALUES (1, 2)"); - jdbcTemplate.update("INSERT INTO study_tag(study_id, tag_id) VALUES (1, 3)"); - - jdbcTemplate.update("INSERT INTO study_tag(study_id, tag_id) VALUES (2, 2)"); - jdbcTemplate.update("INSERT INTO study_tag(study_id, tag_id) VALUES (2, 4)"); - jdbcTemplate.update("INSERT INTO study_tag(study_id, tag_id) VALUES (2, 5)"); - - jdbcTemplate.update("INSERT INTO study_tag(study_id, tag_id) VALUES (3, 2)"); - jdbcTemplate.update("INSERT INTO study_tag(study_id, tag_id) VALUES (3, 4)"); - - jdbcTemplate.update("INSERT INTO study_tag(study_id, tag_id) VALUES (4, 2)"); - jdbcTemplate.update("INSERT INTO study_tag(study_id, tag_id) VALUES (4, 3)"); - - jdbcTemplate.update("INSERT INTO study_member(study_id, member_id) VALUES (1, 3)"); - jdbcTemplate.update("INSERT INTO study_member(study_id, member_id) VALUES (1, 4)"); - - jdbcTemplate.update("INSERT INTO study_member(study_id, member_id) VALUES (2, 1)"); - jdbcTemplate.update("INSERT INTO study_member(study_id, member_id) VALUES (2, 2)"); - jdbcTemplate.update("INSERT INTO study_member(study_id, member_id) VALUES (2, 4)"); - - jdbcTemplate.update("INSERT INTO study_member(study_id, member_id) VALUES (3, 3)"); - jdbcTemplate.update("INSERT INTO study_member(study_id, member_id) VALUES (3, 4)"); + 짱구 = memberRepository.save(짱구()); + 그린론 = memberRepository.save(그린론()); + 디우 = memberRepository.save(디우()); + 베루스 = memberRepository.save(베루스()); + + 자바_스터디 = studyRepository.save(자바_스터디(짱구.getId(), Set.of(그린론.getId(), 디우.getId()))); + 리액트_스터디 = studyRepository.save(리액트_스터디(디우.getId(), Set.of(짱구.getId(), 그린론.getId(), 베루스.getId()))); + 자바스크립트_스터디 = studyRepository.save(자바스크립트_스터디(그린론.getId(), Set.of(디우.getId(), 베루스.getId()))); + HTTP_스터디 = studyRepository.save(HTTP_스터디(디우.getId(), Set.of(베루스.getId(), 짱구.getId()))); + 알고리즘_스터디 = studyRepository.save(알고리즘_스터디(베루스.getId(), Set.of(그린론.getId(), 디우.getId()))); + 리눅스_스터디 = studyRepository.save(리눅스_스터디(베루스.getId(), Set.of(그린론.getId(), 디우.getId()))); + + entityManager.flush(); } @@ -75,17 +75,19 @@ void initDataBase() { @Test void getNotHasEnrollmentEndDateAndEndDateStudyDetails() { // 알고리즘 스터디는 모집 기간과 스터디 종료일자가 없음 - final StudyDetailsData actual = sut.findBy(5L).orElseThrow(); + final StudyDetailsData actual = sut.findBy(알고리즘_스터디.getId()).orElseThrow(); StudyDetailsData expect = StudyDetailsData.builder() // Study Content - .id(5L).title("알고리즘 스터디").excerpt("알고리즘 설명").thumbnail("algorithm thumbnail") - .status("RECRUITMENT_END").description("알고리즘을 TDD로 풀자의 베루스입니다.").createdDate(actual.getCreatedDate()) + .id(알고리즘_스터디.getId()).title(알고리즘_스터디.getContent().getTitle()) + .excerpt(알고리즘_스터디.getContent().getExcerpt()).thumbnail(알고리즘_스터디.getContent().getThumbnail()) + .status(알고리즘_스터디.getRecruitPlanner().getRecruitStatus().toString()) + .description(알고리즘_스터디.getContent().getDescription()).createdDate(actual.getCreatedDate()) // Study Participants - .currentMemberCount(1) - .owner(new MemberData(4L, "verus", "https://image", "github.com")) + .currentMemberCount(알고리즘_스터디.getParticipants().getSize()) + .owner(new MemberData(베루스.getGithubId(), 베루스.getUsername(), 베루스.getImageUrl(), 베루스.getProfileUrl())) // Study Period - .startDate(LocalDate.of(2021, 12, 6)) + .startDate(알고리즘_스터디.getStudyPlanner().getStartDate()) .build(); assertStudyContent(actual, expect); @@ -97,19 +99,19 @@ void getNotHasEnrollmentEndDateAndEndDateStudyDetails() { @Test void getNotHasMaxMemberCountStudyDetails() { // Linux 스터디는 최대 인원 정보가 없음 - final StudyDetailsData actual = sut.findBy(6L).orElseThrow(); + final StudyDetailsData actual = sut.findBy(리눅스_스터디.getId()).orElseThrow(); StudyDetailsData expect = StudyDetailsData.builder() // Study Content - .id(6L).title("Linux 스터디").excerpt("리눅스 설명").thumbnail("linux thumbnail") - .status("RECRUITMENT_END").description("Linux를 공부하자의 베루스입니다.").createdDate(actual.getCreatedDate()) + .id(리눅스_스터디.getId()).title(리눅스_스터디.getContent().getTitle()).excerpt(리눅스_스터디.getContent().getExcerpt()).thumbnail(리눅스_스터디.getContent().getThumbnail()) + .status(리눅스_스터디.getRecruitPlanner().getRecruitStatus().toString()).description(리눅스_스터디.getContent().getDescription()).createdDate(actual.getCreatedDate()) // Study Participant - .currentMemberCount(1) - .owner(new MemberData(4L, "verus", "https://image", "github.com")) + .currentMemberCount(리눅스_스터디.getParticipants().getSize()) + .owner(new MemberData(베루스.getGithubId(), 베루스.getUsername(), 베루스.getImageUrl(), 베루스.getProfileUrl())) // Study Period - .startDate(LocalDate.of(2021, 12, 6)) - .enrollmentEndDate(LocalDate.of(2021, 12, 7)) - .endDate(LocalDate.of(2022, 1, 7)) + .startDate(리눅스_스터디.getStudyPlanner().getStartDate()) + .enrollmentEndDate(리눅스_스터디.getRecruitPlanner().getEnrollmentEndDate()) + .endDate(리눅스_스터디.getStudyPlanner().getEndDate()) .build(); assertStudyContent(actual, expect); diff --git a/backend/src/test/java/com/woowacourse/moamoa/study/query/StudySummaryDaoTest.java b/backend/src/test/java/com/woowacourse/moamoa/study/query/StudySummaryDaoTest.java index a4d9a7ed2..47cdfcb84 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/study/query/StudySummaryDaoTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/study/query/StudySummaryDaoTest.java @@ -20,7 +20,6 @@ import com.woowacourse.moamoa.study.domain.StudyPlanner; import com.woowacourse.moamoa.study.domain.repository.StudyRepository; import com.woowacourse.moamoa.study.query.data.StudySummaryData; -import com.woowacourse.moamoa.tag.query.TagDao; import java.time.LocalDate; import java.time.LocalDateTime; import java.util.List; @@ -38,7 +37,6 @@ import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; -import org.springframework.jdbc.core.JdbcTemplate; @RepositoryTest public class StudySummaryDaoTest { @@ -52,15 +50,9 @@ public class StudySummaryDaoTest { @Autowired private StudyRepository studyRepository; - @Autowired - private TagDao tagDao; - @Autowired private EntityManager em; - @Autowired - private JdbcTemplate jdbcTemplate; - private Member jjanggu; private Member greenlawn; private Member dwoo; diff --git a/backend/src/test/java/com/woowacourse/moamoa/study/service/MyStudyServiceTest.java b/backend/src/test/java/com/woowacourse/moamoa/study/service/MyStudyServiceTest.java index 5f08aa9d6..8eb3a890b 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/study/service/MyStudyServiceTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/study/service/MyStudyServiceTest.java @@ -1,38 +1,44 @@ package com.woowacourse.moamoa.study.service; -import static com.woowacourse.moamoa.study.domain.StudyStatus.PREPARE; +import static com.woowacourse.moamoa.fixtures.MemberFixtures.그린론; +import static com.woowacourse.moamoa.fixtures.MemberFixtures.디우; +import static com.woowacourse.moamoa.fixtures.MemberFixtures.베루스; +import static com.woowacourse.moamoa.fixtures.MemberFixtures.짱구; +import static com.woowacourse.moamoa.fixtures.StudyFixtures.HTTP_스터디; +import static com.woowacourse.moamoa.fixtures.StudyFixtures.OS_스터디; +import static com.woowacourse.moamoa.fixtures.StudyFixtures.리눅스_스터디; +import static com.woowacourse.moamoa.fixtures.StudyFixtures.리액트_스터디; +import static com.woowacourse.moamoa.fixtures.StudyFixtures.알고리즘_스터디; +import static com.woowacourse.moamoa.fixtures.StudyFixtures.자바_스터디; +import static com.woowacourse.moamoa.fixtures.StudyFixtures.자바스크립트_스터디; import static java.util.stream.Collectors.toList; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.tuple; -import java.util.List; -import java.time.LocalDateTime; - -import com.woowacourse.moamoa.study.service.exception.StudyNotFoundException; import com.woowacourse.moamoa.common.RepositoryTest; +import com.woowacourse.moamoa.member.domain.Member; import com.woowacourse.moamoa.member.domain.repository.MemberRepository; import com.woowacourse.moamoa.member.query.data.MemberData; -import com.woowacourse.moamoa.study.domain.repository.StudyRepository; import com.woowacourse.moamoa.member.service.exception.MemberNotFoundException; +import com.woowacourse.moamoa.study.domain.Study; +import com.woowacourse.moamoa.study.domain.repository.StudyRepository; import com.woowacourse.moamoa.study.query.MyStudyDao; +import com.woowacourse.moamoa.study.service.exception.StudyNotFoundException; import com.woowacourse.moamoa.study.service.response.MyStudiesResponse; import com.woowacourse.moamoa.study.service.response.MyStudyResponse; import com.woowacourse.moamoa.tag.query.response.TagSummaryData; - +import java.util.List; +import java.util.Set; +import javax.persistence.EntityManager; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; - import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.jdbc.core.JdbcTemplate; @RepositoryTest class MyStudyServiceTest { - @Autowired - private JdbcTemplate jdbcTemplate; - @Autowired private MyStudyDao myStudyDao; @@ -42,65 +48,48 @@ class MyStudyServiceTest { @Autowired private StudyRepository studyRepository; + @Autowired + private EntityManager entityManager; + private MyStudyService myStudyService; + private Member 짱구; + private Member 그린론; + private Member 디우; + private Member 베루스; + + private Study 자바_스터디; + private Study 리액트_스터디; + private Study 자바스크립트_스터디; + private Study HTTP_스터디; + private Study 알고리즘_스터디; + private Study 리눅스_스터디; + private Study OS_스터디; + @BeforeEach void setUp() { myStudyService = new MyStudyService(myStudyDao, memberRepository, studyRepository); - jdbcTemplate.update("INSERT INTO member(id, github_id, username, image_url, profile_url) VALUES (1, 1, 'jjanggu', 'https://image', 'github.com')"); - jdbcTemplate.update("INSERT INTO member(id, github_id, username, image_url, profile_url) VALUES (2, 2, 'greenlawn', 'https://image', 'github.com')"); - jdbcTemplate.update("INSERT INTO member(id, github_id, username, image_url, profile_url) VALUES (3, 3, 'dwoo', 'https://image', 'github.com')"); - jdbcTemplate.update("INSERT INTO member(id, github_id, username, image_url, profile_url) VALUES (4, 4, 'verus', 'https://image', 'github.com')"); - - final LocalDateTime now = LocalDateTime.now(); - - jdbcTemplate.update("INSERT INTO study(id, title, excerpt, thumbnail, recruitment_status, study_status, description, current_member_count, max_member_count, created_at, start_date, owner_id) " - + "VALUES (1, 'Java 스터디', '자바 설명', 'java thumbnail', 'RECRUITMENT_START', 'PREPARE', '그린론의 우당탕탕 자바 스터디입니다.', 3, 10, '" + now + "', '2021-12-08', 2)"); - jdbcTemplate.update("INSERT INTO study(id, title, excerpt, thumbnail, recruitment_status, study_status, description, current_member_count, max_member_count, created_at, enrollment_end_date, start_date, end_date, owner_id) " - + "VALUES (2, 'React 스터디', '리액트 설명', 'react thumbnail', 'RECRUITMENT_START', 'PREPARE', '디우의 뤼액트 스터디입니다.', 4, 5, '" + now + "', '2021-11-09', '2021-11-10', '2021-12-08', 3)"); - jdbcTemplate.update("INSERT INTO study(id, title, excerpt, thumbnail, recruitment_status, study_status, description, current_member_count, max_member_count, created_at, start_date, owner_id) " - + "VALUES (3, 'javaScript 스터디', '자바스크립트 설명', 'javascript thumbnail', 'RECRUITMENT_START', 'PREPARE', '그린론의 자바스크립트 접해보기', 3, 20, '" + now + "', '2022-08-03', 2)"); - jdbcTemplate.update("INSERT INTO study(id, title, excerpt, thumbnail, recruitment_status, study_status, description, max_member_count, created_at, start_date, owner_id) " - + "VALUES (4, 'HTTP 스터디', 'HTTP 설명', 'http thumbnail', 'RECRUITMENT_END', 'PREPARE', '디우의 HTTP 정복하기', 5, '" + now + "', '2022-08-03', 3)"); - jdbcTemplate.update("INSERT INTO study(id, title, excerpt, thumbnail, recruitment_status, study_status, description, current_member_count, created_at, owner_id, start_date) " - + "VALUES (5, '알고리즘 스터디', '알고리즘 설명', 'algorithm thumbnail', 'RECRUITMENT_END', 'PREPARE', '알고리즘을 TDD로 풀자의 베루스입니다.', 1, '" + now + "', 4, '2021-12-06')"); - jdbcTemplate.update("INSERT INTO study(id, title, excerpt, thumbnail, recruitment_status, study_status, description, current_member_count, created_at, owner_id, start_date, enrollment_end_date, end_date) " - + "VALUES (6, 'Linux 스터디', '리눅스 설명', 'linux thumbnail', 'RECRUITMENT_END', 'PREPARE', 'Linux를 공부하자의 베루스입니다.', 1, '" + now + "', 4, '2021-12-06', '2021-12-07', '2022-01-07')"); - jdbcTemplate.update("INSERT INTO study(id, title, excerpt, thumbnail, recruitment_status, study_status, description, current_member_count, max_member_count, created_at, owner_id, start_date, enrollment_end_date, end_date) " - + "VALUES (7, 'OS 스터디', 'OS 설명', 'os thumbnail', 'RECRUITMENT_END', 'PREPARE', 'OS를 공부하자의 베루스입니다.', 1, 6, '" + now + "', 4, '2021-12-06', '2021-12-07', '2022-01-07')"); - - jdbcTemplate.update("INSERT INTO study_tag(study_id, tag_id) VALUES (1, 1)"); - jdbcTemplate.update("INSERT INTO study_tag(study_id, tag_id) VALUES (1, 2)"); - jdbcTemplate.update("INSERT INTO study_tag(study_id, tag_id) VALUES (1, 3)"); - - jdbcTemplate.update("INSERT INTO study_tag(study_id, tag_id) VALUES (2, 2)"); - jdbcTemplate.update("INSERT INTO study_tag(study_id, tag_id) VALUES (2, 4)"); - jdbcTemplate.update("INSERT INTO study_tag(study_id, tag_id) VALUES (2, 5)"); - - jdbcTemplate.update("INSERT INTO study_tag(study_id, tag_id) VALUES (3, 2)"); - jdbcTemplate.update("INSERT INTO study_tag(study_id, tag_id) VALUES (3, 4)"); - - jdbcTemplate.update("INSERT INTO study_tag(study_id, tag_id) VALUES (4, 2)"); - jdbcTemplate.update("INSERT INTO study_tag(study_id, tag_id) VALUES (4, 3)"); - - jdbcTemplate.update("INSERT INTO study_member(study_id, member_id) VALUES (1, 3)"); - jdbcTemplate.update("INSERT INTO study_member(study_id, member_id) VALUES (1, 4)"); - - jdbcTemplate.update("INSERT INTO study_member(study_id, member_id) VALUES (2, 2)"); - jdbcTemplate.update("INSERT INTO study_member(study_id, member_id) VALUES (2, 4)"); - - jdbcTemplate.update("INSERT INTO study_member(study_id, member_id) VALUES (3, 3)"); - jdbcTemplate.update("INSERT INTO study_member(study_id, member_id) VALUES (3, 4)"); - - jdbcTemplate.update("INSERT INTO study_member(study_id, member_id) VALUES (7, 1)"); - jdbcTemplate.update("INSERT INTO study_member(study_id, member_id) VALUES (7, 2)"); + 짱구 = memberRepository.save(짱구()); + 그린론 = memberRepository.save(그린론()); + 디우 = memberRepository.save(디우()); + 베루스 = memberRepository.save(베루스()); + + 자바_스터디 = studyRepository.save(자바_스터디(짱구.getId(), Set.of(그린론.getId(), 디우.getId()))); + 리액트_스터디 = studyRepository.save(리액트_스터디(디우.getId(), Set.of(짱구.getId(), 그린론.getId(), 베루스.getId()))); + 자바스크립트_스터디 = studyRepository.save(자바스크립트_스터디(그린론.getId(), Set.of(디우.getId(), 베루스.getId()))); + HTTP_스터디 = studyRepository.save(HTTP_스터디(디우.getId(), Set.of(베루스.getId(), 짱구.getId()))); + 알고리즘_스터디 = studyRepository.save(알고리즘_스터디(베루스.getId(), Set.of(그린론.getId(), 디우.getId()))); + 리눅스_스터디 = studyRepository.save(리눅스_스터디(베루스.getId(), Set.of(그린론.getId(), 디우.getId()))); + OS_스터디 = studyRepository.save(OS_스터디(디우.getId(), Set.of(그린론.getId(), 짱구.getId(), 베루스.getId()))); + + entityManager.flush(); } @DisplayName("내가 참여한 스터디를 조회한다.") @Test void findMyStudies() { - final MyStudiesResponse myStudiesResponse = myStudyService.getStudies(2L); + final MyStudiesResponse myStudiesResponse = myStudyService.getStudies(짱구.getGithubId()); final List<MemberData> owners = myStudiesResponse.getStudies() .stream() @@ -114,25 +103,29 @@ void findMyStudies() { final List<MyStudyResponse> studies = myStudiesResponse.getStudies(); + for (MyStudyResponse study : studies) { + System.out.println("study.getTitle() = " + study.getTitle()); + } + assertThat(studies) .hasSize(4) .filteredOn(study -> study.getId() != null) .extracting("title", "studyStatus", "currentMemberCount", "maxMemberCount") .contains( - tuple("Java 스터디", PREPARE, 3, 10), - tuple("javaScript 스터디" ,PREPARE, 3, 20), - tuple("React 스터디", PREPARE, 4, 5), - tuple("OS 스터디", PREPARE, 1, 6) + tuple(자바_스터디.getContent().getTitle(), 자바_스터디.getStudyPlanner().getStudyStatus(), 자바_스터디.getParticipants().getSize(), 자바_스터디.getRecruitPlanner().getMax()), + tuple(리액트_스터디.getContent().getTitle(), 리액트_스터디.getStudyPlanner().getStudyStatus(), 리액트_스터디.getParticipants().getSize(), 리액트_스터디.getRecruitPlanner().getMax()), + tuple(HTTP_스터디.getContent().getTitle(), HTTP_스터디.getStudyPlanner().getStudyStatus(), HTTP_스터디.getParticipants().getSize(), HTTP_스터디.getRecruitPlanner().getMax()), + tuple(OS_스터디.getContent().getTitle(), OS_스터디.getStudyPlanner().getStudyStatus(), OS_스터디.getParticipants().getSize(), OS_스터디.getRecruitPlanner().getMax()) ); assertThat(owners) .hasSize(4) .extracting("githubId", "username", "imageUrl", "profileUrl") .contains( - tuple(2L, "greenlawn", "https://image", "github.com"), - tuple(2L, "greenlawn", "https://image", "github.com"), - tuple(3L, "dwoo", "https://image", "github.com"), - tuple(4L, "verus", "https://image", "github.com") + tuple(짱구.getGithubId(), 짱구.getUsername(), 짱구.getImageUrl(), 짱구.getProfileUrl()), + tuple(디우.getGithubId(), 디우.getUsername(), 디우.getImageUrl(), 디우.getProfileUrl()), + tuple(디우.getGithubId(), 디우.getUsername(), 디우.getImageUrl(), 디우.getProfileUrl()), + tuple(디우.getGithubId(), 디우.getUsername(), 디우.getImageUrl(), 디우.getProfileUrl()) ); assertThat(tags).hasSize(4); @@ -141,7 +134,7 @@ void findMyStudies() { @DisplayName("태그가 없는 스터디를 조회한다.") @Test void findMyStudiesWithoutTags() { - final MyStudiesResponse myStudiesResponse = myStudyService.getStudies(1L); + final MyStudiesResponse myStudiesResponse = myStudyService.getStudies(디우.getGithubId()); final List<MemberData> owners = myStudiesResponse.getStudies() .stream() @@ -156,21 +149,41 @@ void findMyStudiesWithoutTags() { final List<MyStudyResponse> studies = myStudiesResponse.getStudies(); assertThat(studies) - .hasSize(1) + .hasSize(7) .filteredOn(study -> study.getId() != null) .extracting("title", "studyStatus", "currentMemberCount", "maxMemberCount") .contains( - tuple("OS 스터디", PREPARE, 1, 6) + tuple(자바_스터디.getContent().getTitle(), 자바_스터디.getStudyPlanner().getStudyStatus(), + 자바_스터디.getParticipants().getSize(), 자바_스터디.getRecruitPlanner().getMax()), + tuple(리액트_스터디.getContent().getTitle(), 리액트_스터디.getStudyPlanner().getStudyStatus(), + 리액트_스터디.getParticipants().getSize(), 리액트_스터디.getRecruitPlanner().getMax()), + tuple(자바스크립트_스터디.getContent().getTitle(), 자바스크립트_스터디.getStudyPlanner().getStudyStatus(), + 자바스크립트_스터디.getParticipants().getSize(), 자바스크립트_스터디.getRecruitPlanner().getMax()), + tuple(HTTP_스터디.getContent().getTitle(), HTTP_스터디.getStudyPlanner().getStudyStatus(), + HTTP_스터디.getParticipants().getSize(), HTTP_스터디.getRecruitPlanner().getMax()), + tuple(알고리즘_스터디.getContent().getTitle(), 알고리즘_스터디.getStudyPlanner().getStudyStatus(), + 알고리즘_스터디.getParticipants().getSize(), 알고리즘_스터디.getRecruitPlanner().getMax()), + tuple(리눅스_스터디.getContent().getTitle(), 리눅스_스터디.getStudyPlanner().getStudyStatus(), + 리눅스_스터디.getParticipants().getSize(), 리눅스_스터디.getRecruitPlanner().getMax()), + tuple(OS_스터디.getContent().getTitle(), OS_스터디.getStudyPlanner().getStudyStatus(), + OS_스터디.getParticipants().getSize(), OS_스터디.getRecruitPlanner().getMax()) ); assertThat(owners) - .hasSize(1) + .hasSize(7) .extracting("githubId", "username", "imageUrl", "profileUrl") .contains( - tuple(4L, "verus", "https://image", "github.com") + tuple(짱구.getGithubId(), 짱구.getUsername(), 짱구.getImageUrl(), 짱구.getProfileUrl()), + tuple(디우.getGithubId(), 디우.getUsername(), 디우.getImageUrl(), 디우.getProfileUrl()), + tuple(그린론.getGithubId(), 그린론.getUsername(), 그린론.getImageUrl(), 그린론.getProfileUrl()), + tuple(디우.getGithubId(), 디우.getUsername(), 디우.getImageUrl(), 디우.getProfileUrl()), + tuple(베루스.getGithubId(), 베루스.getUsername(), 베루스.getImageUrl(), 베루스.getProfileUrl()), + tuple(베루스.getGithubId(), 베루스.getUsername(), 베루스.getImageUrl(), 베루스.getProfileUrl()), + tuple(디우.getGithubId(), 디우.getUsername(), 디우.getImageUrl(), 디우.getProfileUrl()) ); - assertThat(tags.get(0).size()).isZero(); + assertThat(tags.get(4).size()).isZero(); + assertThat(tags.get(5).size()).isZero(); } @DisplayName("존재하지 않은 내가 참여한 스터디 조회 시 예외 발생") diff --git a/backend/src/test/java/com/woowacourse/moamoa/tag/controller/SearchingTagControllerTest.java b/backend/src/test/java/com/woowacourse/moamoa/tag/controller/SearchingTagControllerTest.java index 881333ec0..30b072d36 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/tag/controller/SearchingTagControllerTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/tag/controller/SearchingTagControllerTest.java @@ -8,14 +8,12 @@ import com.woowacourse.moamoa.tag.query.request.CategoryIdRequest; import com.woowacourse.moamoa.tag.service.SearchingTagService; import com.woowacourse.moamoa.tag.service.response.TagsResponse; -import java.util.Optional; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.jdbc.core.JdbcTemplate; @RepositoryTest class SearchingTagControllerTest { diff --git a/backend/src/test/java/com/woowacourse/moamoa/tag/query/TagDaoTest.java b/backend/src/test/java/com/woowacourse/moamoa/tag/query/TagDaoTest.java index 91e0518ea..a42514f09 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/tag/query/TagDaoTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/tag/query/TagDaoTest.java @@ -1,18 +1,32 @@ package com.woowacourse.moamoa.tag.query; +import static com.woowacourse.moamoa.fixtures.MemberFixtures.그린론; +import static com.woowacourse.moamoa.fixtures.MemberFixtures.디우; +import static com.woowacourse.moamoa.fixtures.MemberFixtures.베루스; +import static com.woowacourse.moamoa.fixtures.MemberFixtures.짱구; +import static com.woowacourse.moamoa.fixtures.StudyFixtures.HTTP_스터디; +import static com.woowacourse.moamoa.fixtures.StudyFixtures.리눅스_스터디; +import static com.woowacourse.moamoa.fixtures.StudyFixtures.리액트_스터디; +import static com.woowacourse.moamoa.fixtures.StudyFixtures.알고리즘_스터디; +import static com.woowacourse.moamoa.fixtures.StudyFixtures.자바_스터디; +import static com.woowacourse.moamoa.fixtures.StudyFixtures.자바스크립트_스터디; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.tuple; import com.woowacourse.moamoa.common.RepositoryTest; +import com.woowacourse.moamoa.member.domain.Member; +import com.woowacourse.moamoa.member.domain.repository.MemberRepository; +import com.woowacourse.moamoa.study.domain.Study; +import com.woowacourse.moamoa.study.domain.repository.StudyRepository; import com.woowacourse.moamoa.tag.query.request.CategoryIdRequest; import com.woowacourse.moamoa.tag.query.response.TagData; -import java.time.LocalDateTime; import java.util.List; +import java.util.Set; +import javax.persistence.EntityManager; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.jdbc.core.JdbcTemplate; @RepositoryTest class TagDaoTest { @@ -21,43 +35,41 @@ class TagDaoTest { private TagDao tagDao; @Autowired - private JdbcTemplate jdbcTemplate; + private MemberRepository memberRepository; + + @Autowired + private StudyRepository studyRepository; + + @Autowired + private EntityManager entityManager; + + private Member 짱구; + private Member 그린론; + private Member 디우; + private Member 베루스; + + private Study 자바_스터디; + private Study 리액트_스터디; + private Study 자바스크립트_스터디; + private Study HTTP_스터디; + private Study 알고리즘_스터디; + private Study 리눅스_스터디; @BeforeEach void initDataBase() { - jdbcTemplate.update("INSERT INTO member(id, github_id, username, image_url, profile_url) VALUES (1, 1, 'jjanggu', 'https://image', 'github.com')"); - jdbcTemplate.update("INSERT INTO member(id, github_id, username, image_url, profile_url) VALUES (2, 2, 'greenlawn', 'https://image', 'github.com')"); - jdbcTemplate.update("INSERT INTO member(id, github_id, username, image_url, profile_url) VALUES (3, 3, 'dwoo', 'https://image', 'github.com')"); - jdbcTemplate.update("INSERT INTO member(id, github_id, username, image_url, profile_url) VALUES (4, 4, 'verus', 'https://image', 'github.com')"); - - final LocalDateTime now = LocalDateTime.now(); - - jdbcTemplate.update("INSERT INTO study(id, title, excerpt, thumbnail, recruitment_status, study_status, description, current_member_count, max_member_count, created_at, start_date, owner_id) " - + "VALUES (1, 'Java 스터디', '자바 설명', 'java thumbnail', 'RECRUITMENT_START', 'PREPARE', '그린론의 우당탕탕 자바 스터디입니다.', 3, 10, '" + now + "', '2021-12-08', 2)"); - jdbcTemplate.update("INSERT INTO study(id, title, excerpt, thumbnail, recruitment_status, study_status, description, current_member_count, max_member_count, created_at, enrollment_end_date, start_date, end_date, owner_id) " - + "VALUES (2, 'React 스터디', '리액트 설명', 'react thumbnail', 'RECRUITMENT_START', 'PREPARE', '디우의 뤼액트 스터디입니다.', 4, 5, '" + now + "', '2021-11-09', '2021-11-10', '2021-12-08', 3)"); - jdbcTemplate.update("INSERT INTO study(id, title, excerpt, thumbnail, recruitment_status, study_status, description, current_member_count, max_member_count, created_at, start_date, owner_id) " - + "VALUES (3, 'javaScript 스터디', '자바스크립트 설명', 'javascript thumbnail', 'RECRUITMENT_START', 'PREPARE', '그린론의 자바스크립트 접해보기', 3, 20, '" + now + "', '2022-08-03', 2)"); - jdbcTemplate.update("INSERT INTO study(id, title, excerpt, thumbnail, recruitment_status, study_status, description, max_member_count, created_at, start_date, owner_id) " - + "VALUES (4, 'HTTP 스터디', 'HTTP 설명', 'http thumbnail', 'RECRUITMENT_END', 'PREPARE', '디우의 HTTP 정복하기', 5, '" + now + "', '2022-08-03', 3)"); - jdbcTemplate.update("INSERT INTO study(id, title, excerpt, thumbnail, recruitment_status, study_status, description, current_member_count, created_at, owner_id, start_date) " - + "VALUES (5, '알고리즘 스터디', '알고리즘 설명', 'algorithm thumbnail', 'RECRUITMENT_END', 'PREPARE', '알고리즘을 TDD로 풀자의 베루스입니다.', 1, '" + now + "', 4, '2021-12-06')"); - jdbcTemplate.update("INSERT INTO study(id, title, excerpt, thumbnail, recruitment_status, study_status, description, current_member_count, created_at, owner_id, start_date, enrollment_end_date, end_date) " - + "VALUES (6, 'Linux 스터디', '리눅스 설명', 'linux thumbnail', 'RECRUITMENT_END', 'PREPARE', 'Linux를 공부하자의 베루스입니다.', 1, '" + now + "', 4, '2021-12-06', '2021-12-07', '2022-01-07')"); - - jdbcTemplate.update("INSERT INTO study_tag(study_id, tag_id) VALUES (1, 1)"); - jdbcTemplate.update("INSERT INTO study_tag(study_id, tag_id) VALUES (1, 2)"); - jdbcTemplate.update("INSERT INTO study_tag(study_id, tag_id) VALUES (1, 3)"); - - jdbcTemplate.update("INSERT INTO study_tag(study_id, tag_id) VALUES (2, 2)"); - jdbcTemplate.update("INSERT INTO study_tag(study_id, tag_id) VALUES (2, 4)"); - jdbcTemplate.update("INSERT INTO study_tag(study_id, tag_id) VALUES (2, 5)"); - - jdbcTemplate.update("INSERT INTO study_tag(study_id, tag_id) VALUES (3, 2)"); - jdbcTemplate.update("INSERT INTO study_tag(study_id, tag_id) VALUES (3, 4)"); - - jdbcTemplate.update("INSERT INTO study_tag(study_id, tag_id) VALUES (4, 2)"); - jdbcTemplate.update("INSERT INTO study_tag(study_id, tag_id) VALUES (4, 3)"); + 짱구 = memberRepository.save(짱구()); + 그린론 = memberRepository.save(그린론()); + 디우 = memberRepository.save(디우()); + 베루스 = memberRepository.save(베루스()); + + 자바_스터디 = studyRepository.save(자바_스터디(짱구.getId(), Set.of(그린론.getId(), 디우.getId()))); + 리액트_스터디 = studyRepository.save(리액트_스터디(디우.getId(), Set.of(짱구.getId(), 그린론.getId(), 베루스.getId()))); + 자바스크립트_스터디 = studyRepository.save(자바스크립트_스터디(그린론.getId(), Set.of(디우.getId(), 베루스.getId()))); + HTTP_스터디 = studyRepository.save(HTTP_스터디(디우.getId(), Set.of(베루스.getId(), 짱구.getId()))); + 알고리즘_스터디 = studyRepository.save(알고리즘_스터디(베루스.getId(), Set.of(그린론.getId(), 디우.getId()))); + 리눅스_스터디 = studyRepository.save(리눅스_스터디(베루스.getId(), Set.of(그린론.getId(), 디우.getId()))); + + entityManager.flush(); } @DisplayName("필터 없이 조회시 태그 목록 전체를 조회한다.") @@ -121,7 +133,7 @@ void findAllByCategoryAndName() { @Test void getAttachedTagsByStudyId() { // Java 스터디에 부착된 태그 : Java, 4기, BE - final List<TagData> attachedTags = tagDao.findTagsByStudyId(1L); + final List<TagData> attachedTags = tagDao.findTagsByStudyId(자바_스터디.getId()); assertThat(attachedTags) .hasSize(3) diff --git a/backend/src/test/resources/schema.sql b/backend/src/test/resources/schema.sql index fcbcc4936..32d9512ad 100644 --- a/backend/src/test/resources/schema.sql +++ b/backend/src/test/resources/schema.sql @@ -42,8 +42,8 @@ CREATE TABLE review study_id BIGINT NOT NULL, member_id BIGINT NOT NULL, content MEDIUMTEXT, - created_date DATETIME not null, - last_modified_date DATETIME not null, + created_date DATE not null, + last_modified_date DATE not null, deleted boolean not null, FOREIGN KEY (study_id) REFERENCES study (id), FOREIGN KEY (member_id) REFERENCES member (id) From faaa88cd8fde424982ef73ed9def6dc6edae7c6f Mon Sep 17 00:00:00 2001 From: jaejae-yoo <wotj102@gmail.com> Date: Tue, 16 Aug 2022 16:32:16 +0900 Subject: [PATCH 20/51] =?UTF-8?q?feat:=20sonarqube=20=EC=A0=81=EC=9A=A9?= =?UTF-8?q?=EC=9D=84=20=EC=9C=84=ED=95=9C=20build.gradle=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: sc0116 <ssc6839@gmail.com> --- backend/build.gradle | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/build.gradle b/backend/build.gradle index f30e04f4c..32d0176f4 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -99,7 +99,6 @@ bootJar { sonarqube { properties { - property "sonar.host.url", "https://dev.moamoa.space" property "sonar.projectKey", "moamoa" property "sonar.sources", "src" property "sonar.language", "java" From 8d42f7719edc97623f13d7d69525e4cb1ead3f16 Mon Sep 17 00:00:00 2001 From: jaeseo yoo <wotj102@gmail.com> Date: Tue, 16 Aug 2022 16:54:46 +0900 Subject: [PATCH 21/51] Create sonarqube.yml --- .github/workflows/sonarqube.yml | 48 +++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 .github/workflows/sonarqube.yml diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml new file mode 100644 index 000000000..bdc5fb62a --- /dev/null +++ b/.github/workflows/sonarqube.yml @@ -0,0 +1,48 @@ +name: moamoa-sonarqube + +on: + pull_request: + branches: + - develop + types: [opened, synchronize] + +defaults: + run: + working-directory: ./backend/ + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout source code + uses: actions/checkout@v3 + + analysis: + runs-on: ubuntu-latest + env: + SONARQUBE_PROJECT_KEY: moamoa + SONARQUBE_URL: ${{ secrets.SONARQUBE_URL }} + SONARQUBE_TOKEN : ${{ secrets.SONARQUBE_TOKEN }} + PR_NUMBER: ${{ github.event.pull_request.number }} + steps: + - name: Checkout source code + uses: actions/checkout@v3 + - name: gradlew permission change + run: sudo chmod 755 gradlew + - name: Sonaqube Analysis + run: ./gradlew test sonarqube + -Dsonar.host.url=${{ env.SONARQUBE_URL }} + -Dsonar.projectKey=${{ env.SONARQUBE_PROJECT_KEY }} + -Dsonar.projectName=${{ env.SONARQUBE_PROJECT_KEY }}-${{ env.PR_NUMBER }} + -Dsonar.login=${{ env.SONARQUBE_TOKEN }} + - name: Comment Sonarqube URL + uses: actions/github-script@v4 + with: + script: | + const { SONARQUBE_PROJECT_KEY, SONARQUBE_URL, PR_NUMBER } = process.env + github.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: `📊 ${ SONARQUBE_PROJECT_KEY }-${ PR_NUMBER } 분석 결과 확인하기 [링크](${SONARQUBE_URL})` + }) From 72650c5dab56cbaab85617cfa33cb03de64b78ad Mon Sep 17 00:00:00 2001 From: Donggyu <a29661498@gmail.com> Date: Tue, 16 Aug 2022 17:03:37 +0900 Subject: [PATCH 22/51] =?UTF-8?q?chore:=20=EC=B6=A9=EB=8F=8C=20=ED=95=B4?= =?UTF-8?q?=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/utils/tw.ts | 270 ++++++++++++++++++++++++++++++++++++ frontend/tailwind.config.js | 29 ++++ 2 files changed, 299 insertions(+) create mode 100644 frontend/src/utils/tw.ts create mode 100644 frontend/tailwind.config.js diff --git a/frontend/src/utils/tw.ts b/frontend/src/utils/tw.ts new file mode 100644 index 000000000..2bcc1a374 --- /dev/null +++ b/frontend/src/utils/tw.ts @@ -0,0 +1,270 @@ +// quick style sheet +import { css } from '@emotion/react'; + +const arr0_to_100 = [...Array(101).keys()]; +const arr0_to_300 = [...Array(301).keys()]; + +const layout = { + block: 'display: block', + 'inline-block': 'display: inline-block', + inline: 'display: inline', + flex: 'display: flex', + 'inline-flex': 'display: inline-flex', + table: 'display: table', + 'inline-table': 'display: inline-table', + 'table-caption': 'display: table-caption', + 'table-cell': 'display: table-cell', + 'table-column': 'display: table-column', + 'table-column-group': 'display: table-column-group', + 'table-footer-group': 'display: table-footer-group', + 'table-header-group': 'display: table-header-group', + 'table-row-group': 'display: table-row-group', + 'table-row': 'display: table-row', + 'flow-root': 'display: flow-root', + grid: 'display: grid', + 'inline-grid': 'display: inline-grid', + contents: 'display: contents', + 'list-item': 'display: list-item', + hidden: 'display: none', +}; + +const flex = { + 'flex-1': 'flex: 1 1 0%', + 'flex-auto': 'flex: 1 1 auto', + 'flex-initial': 'flex: 0 1 auto', + 'flex-none': 'flex: none', +}; + +const justifyContent = { + 'justify-start': 'justify-content: flex-start', + 'justify-end': 'justify-content: flex-end', + 'justify-center': 'justify-content: center', + 'justify-between': 'justify-content: space-between', + 'justify-around': 'justify-content: space-around', + 'justify-evenly': 'justify-content: space-evenly', +}; + +const alignItems = { + 'items-start': 'align-items: flex-start', + 'items-end': 'align-items: flex-end', + 'items-center': 'align-items: center', + 'items-baseline': 'align-items: baseline', + 'items-stretch': 'align-items: stretch', +}; + +const alignSelf = { + 'self-auto': 'align-self: auto', + 'self-start': 'align-self: flex-start', + 'self-end': 'align-self: flex-end', + 'self-center': 'align-self: center', + 'self-stretch': 'align-self: stretch', + 'self-baseline': 'align-self: baseline', +}; + +const margin = arr0_to_100.reduce((acc, cur) => { + acc[`mt-${cur}`] = `margin-top: ${cur}px`; + acc[`mr-${cur}`] = `margin-right: ${cur}px`; + acc[`mb-${cur}`] = `margin-bottom: ${cur}px`; + acc[`ml-${cur}`] = `margin-left: ${cur}px`; + return acc; +}, {} as Record<string, string>); + +const padding = arr0_to_100.reduce((acc, cur) => { + acc[`p-${cur}`] = `padding-top: ${cur}px; padding-right: ${cur}px; padding-bottom: ${cur}px; padding-left: ${cur}px`; + acc[`py-${cur}`] = `padding-top: ${cur}px; padding-bottom: ${cur}px`; + acc[`px-${cur}`] = `padding-left: ${cur}px; padding-right: ${cur}px`; + + acc[`pt-${cur}`] = `padding-top: ${cur}px`; + acc[`pr-${cur}`] = `padding-right: ${cur}px`; + acc[`pb-${cur}`] = `padding-bottom: ${cur}px`; + acc[`pl-${cur}`] = `padding-left: ${cur}px`; + return acc; +}, {} as Record<string, string>); + +const widthPx = arr0_to_300.reduce((acc, cur) => { + acc[`w-${cur}`] = `width: ${cur}px`; + acc[`max-w-${cur}`] = `max-width: ${cur}px`; + return acc; +}, {} as Record<string, string>); + +const width = { + 'w-full': 'width: 100%', +}; + +const heightPx = arr0_to_300.reduce((acc, cur) => { + acc[`h-${cur}px`] = `height: ${cur}px`; + acc[`max-h-${cur}px`] = `max-height: ${cur}px`; + return acc; +}, {} as Record<string, string>); + +const height = { + 'h-full': 'height: 100%', +}; + +const position = { + static: 'position: static', + fixed: 'position: fixed', + absolute: 'position: absolute', + relative: 'position: relative', + sticky: 'position: sticky', +}; + +const trbl = arr0_to_100.reduce((acc, cur) => { + acc[`top-${cur}`] = `top: ${cur}px`; + acc[`right-${cur}`] = `right: ${cur}px`; + acc[`bottom-${cur}`] = `bottom: ${cur}px`; + acc[`left-${cur}`] = `left: ${cur}px`; + return acc; +}, {} as Record<string, string>); + +const zIndex = arr0_to_100.reduce((acc, cur) => { + acc[`z-${cur}`] = `z-index: ${cur}`; + return acc; +}, {} as Record<string, string>); + +const overflow = { + 'overflow-auto': 'overflow: auto', + 'overflow-hidden': 'overflow: hidden', + 'overflow-clip': 'overflow: clip', + 'overflow-visible': 'overflow: visible', + 'overflow-scroll': 'overflow: scroll', + 'overflow-x-auto': 'overflow-x: auto', + 'overflow-y-auto': 'overflow-y: auto', + 'overflow-x-hidden': 'overflow-x: hidden', + 'overflow-y-hidden': 'overflow-y: hidden', + 'overflow-x-clip': 'overflow-x: clip', + 'overflow-y-clip': 'overflow-y: clip', + 'overflow-x-visible': 'overflow-x: visible', + 'overflow-y-visible': 'overflow-y: visible', + 'overflow-x-scroll': 'overflow-x: scroll', + 'overflow-y-scroll': 'overflow-y: scroll', +}; + +const textAlign = { + 'text-left': 'text-align: left', + 'text-center': 'text-align: center', + 'text-right': 'text-align: right', + 'text-justify': 'text-align: justify', + 'text-start': 'text-align: start', + 'text-end': 'text-align: end', +}; + +const borderRadius = arr0_to_100.reduce((acc, cur) => { + acc[`rounded-${cur}`] = `border-radius: ${cur}px`; + acc[`rounded-t-${cur}`] = `border-top-left-radius: ${cur}px; border-top-right-radius: ${cur}px`; + acc[`rounded-r-${cur}`] = `border-top-right-radius: ${cur}px; border-bottom-right-radius: ${cur}px`; + acc[`rounded-b-${cur}`] = `border-bottom-right-radius: ${cur}px; border-bottom-left-radius: ${cur}px`; + acc[`rounded-l-${cur}`] = `border-top-left-radius: ${cur}px; border-bottom-left-radius: ${cur}px`; + + return acc; +}, {} as Record<string, string>); + +const fontSize = arr0_to_100.reduce((acc, cur) => { + acc[`text-${cur}`] = `font-size: ${cur}px`; + return acc; +}, {} as Record<string, string>); + +const totalStyles = { + ...layout, + ...flex, + ...justifyContent, + ...alignItems, + ...alignSelf, + ...margin, + ...padding, + ...widthPx, + ...width, + ...heightPx, + ...height, + ...position, + ...trbl, + ...zIndex, + ...overflow, + ...textAlign, + ...borderRadius, + ...fontSize, +}; + +const cssPropertyForArbitararyValue = { + // margin + mt: ['margin-top'], + mr: ['margin-right'], + mb: ['margin-bottom'], + ml: ['margin-left'], + m: ['margin-top', 'margin-right', 'margin-bottom', 'margin-left'], + my: ['margin-top', 'margin-bottom'], + mx: ['margin-left', 'margin-right'], + + // padding + pt: ['padding-top'], + pr: ['padding-right'], + pb: ['padding-bottom'], + pl: ['padding-left'], + p: ['padding-top', 'padding-right', 'padding-bottom', 'padding-left'], + py: ['padding-top', 'padding-bottom'], + px: ['padding-left', 'padding-right'], + + // size + h: ['height'], + w: ['width'], + 'max-w': ['max-width'], + 'min-w': ['min-width'], + + // border + rounded: ['border-radius'], + + // typo + text: ['font-size'], +}; + +type styleKey = keyof typeof totalStyles; +type cssShortPropertyKey = keyof typeof cssPropertyForArbitararyValue; + +const getArbitararyValue = (str: string) => { + return str.substring(str.indexOf('[') + 1, str.indexOf(']')); +}; + +// TODO: Test 코드 만들자 +const combine = (template: TemplateStringsArray, args: Array<string>): string => { + const result = []; + for (let i = 0; i < template.length - 1; i += 1) { + result.push(template[i]); + result.push(args[i]); + } + result.push(template[template.length - 1]); + return result.join(''); +}; + +// TODO: Test 코드 만들다 +const tw = (template: TemplateStringsArray, ...args: Array<string>) => { + let templateStr = template[0]; + if (template.length > 1) templateStr = combine(template, args); + + const styles = templateStr.split(' '); + const styleText = styles + .map(s => { + const arbitararyValue = getArbitararyValue(s); + + if (arbitararyValue) { + // py-[12px]에서 ['py-', '[12px]']로 짜르고 마지막 요소를 빼낸다 + const shortCssProperty = s.split('-').slice(0, -1)[0]; + const fullCssProperties = cssPropertyForArbitararyValue[shortCssProperty as cssShortPropertyKey]; + const cssTexts = fullCssProperties.map(fullCssProp => { + return `${fullCssProp}: ${arbitararyValue}`; + }); + return cssTexts.join(';'); + } + if (!(s in totalStyles)) { + throw new Error('올바른 스타일을 입력해 주세요'); + } + return totalStyles[s as styleKey]; + }) + .join(';'); + + // 마지막 ;도 중요하다! + return css` + ${styleText}; + `; +}; + +export default tw; diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js new file mode 100644 index 000000000..282e204f5 --- /dev/null +++ b/frontend/tailwind.config.js @@ -0,0 +1,29 @@ +/** @type {import('tailwindcss').Config} */ + +const arr0_to_100 = [...Array(101).keys()]; +const px0_50 = { ...Array.from(Array(51)).map((_, i) => `${i}px`) }; +const px0_100 = { ...Array.from(Array(101)).map((_, i) => `${i}px`) }; +const px0_500 = { ...Array.from(Array(501)).map((_, i) => `${i}px`) }; + +module.exports = { + content: ['src/components/*.tsx', 'src/pages/*.tsx'], + theme: { + extend: { + borderWidth: px0_50, + fontSize: px0_100, + lineHeight: px0_100, + width: px0_500, + maxWidth: px0_500, + minWidth: px0_500, + maxHeight: px0_500, + minHeight: px0_500, + spacing: px0_100, + zIndex: arr0_to_100, + top: arr0_to_100, + right: arr0_to_100, + bottom: arr0_to_100, + left: arr0_to_100, + }, + }, + plugins: [], +}; From 12359ca40746db5d48aa3d3a4be8ffbb847e5f1e Mon Sep 17 00:00:00 2001 From: jaeseo yoo <wotj102@gmail.com> Date: Tue, 16 Aug 2022 17:35:24 +0900 Subject: [PATCH 23/51] Update sonarqube.yml --- .github/workflows/sonarqube.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml index bdc5fb62a..28c8461a0 100644 --- a/.github/workflows/sonarqube.yml +++ b/.github/workflows/sonarqube.yml @@ -8,7 +8,7 @@ on: defaults: run: - working-directory: ./backend/ + working-directory: ./backend jobs: build: @@ -24,17 +24,21 @@ jobs: SONARQUBE_URL: ${{ secrets.SONARQUBE_URL }} SONARQUBE_TOKEN : ${{ secrets.SONARQUBE_TOKEN }} PR_NUMBER: ${{ github.event.pull_request.number }} + steps: - name: Checkout source code uses: actions/checkout@v3 + - name: gradlew permission change run: sudo chmod 755 gradlew + - name: Sonaqube Analysis run: ./gradlew test sonarqube -Dsonar.host.url=${{ env.SONARQUBE_URL }} -Dsonar.projectKey=${{ env.SONARQUBE_PROJECT_KEY }} -Dsonar.projectName=${{ env.SONARQUBE_PROJECT_KEY }}-${{ env.PR_NUMBER }} -Dsonar.login=${{ env.SONARQUBE_TOKEN }} + - name: Comment Sonarqube URL uses: actions/github-script@v4 with: From e7eaaeb500d4d54324af47cd8d8af3dfbf4ef63a Mon Sep 17 00:00:00 2001 From: jaeseo yoo <wotj102@gmail.com> Date: Tue, 16 Aug 2022 17:44:31 +0900 Subject: [PATCH 24/51] Update sonarqube.yml --- .github/workflows/sonarqube.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml index 28c8461a0..931eecc20 100644 --- a/.github/workflows/sonarqube.yml +++ b/.github/workflows/sonarqube.yml @@ -33,7 +33,7 @@ jobs: run: sudo chmod 755 gradlew - name: Sonaqube Analysis - run: ./gradlew test sonarqube + run: ./gradlew build sonarqube -Dsonar.host.url=${{ env.SONARQUBE_URL }} -Dsonar.projectKey=${{ env.SONARQUBE_PROJECT_KEY }} -Dsonar.projectName=${{ env.SONARQUBE_PROJECT_KEY }}-${{ env.PR_NUMBER }} From 64c7f5e153d14c60c6a7df8c24ce93bff0bb3e9e Mon Sep 17 00:00:00 2001 From: jaeseo yoo <wotj102@gmail.com> Date: Tue, 16 Aug 2022 19:26:51 +0900 Subject: [PATCH 25/51] Update sonarqube.yml --- .github/workflows/sonarqube.yml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml index 931eecc20..3e8c5251b 100644 --- a/.github/workflows/sonarqube.yml +++ b/.github/workflows/sonarqube.yml @@ -24,6 +24,10 @@ jobs: SONARQUBE_URL: ${{ secrets.SONARQUBE_URL }} SONARQUBE_TOKEN : ${{ secrets.SONARQUBE_TOKEN }} PR_NUMBER: ${{ github.event.pull_request.number }} + client-id: ${{ secrets.CLIENT_ID }} + client-secret: ${{ secrets.CLIENT_SECRET }} + jwt-secret-key: ${{ secrets.JWT_SECRET_KEY }} + jwt-expire-length: ${{ secrets.JWT_EXPIRE_LENGTH }} steps: - name: Checkout source code @@ -38,7 +42,11 @@ jobs: -Dsonar.projectKey=${{ env.SONARQUBE_PROJECT_KEY }} -Dsonar.projectName=${{ env.SONARQUBE_PROJECT_KEY }}-${{ env.PR_NUMBER }} -Dsonar.login=${{ env.SONARQUBE_TOKEN }} - + -Doauth2.github.client-id=${{ env.client-id }} + -Doauth2.github.client-secret=${{ env.client-secret }} + -Dsecurity.jwt.token.secret-key=${{ env.jwt-secret-key }} + -Dsecurity.jwt.token.expire-length=${{ env.jwt-expire-length }} + - name: Comment Sonarqube URL uses: actions/github-script@v4 with: From fcf273cf30e8d6b811ea9a358a2eb250a8392f69 Mon Sep 17 00:00:00 2001 From: jaeseo yoo <wotj102@gmail.com> Date: Wed, 17 Aug 2022 18:09:47 +0900 Subject: [PATCH 26/51] [BE] issue277: SonarQube Pull Request Decoration (#283) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * style: EOL 추가 * refactor: sonarqube secret 변경 * Update sonarqube.yml * chore: sonarqube build.gradle 수정 * fix: develop 브랜치 제거 * fix: sonar secret key 설정 * Update sonarqube.yml * Update sonarqube.yml * Update sonarqube.yml * Update sonarqube.yml * Update sonarqube.yml * Update build.gradle * Update build.gradle * Update build.gradle * Update sonarqube.yml * Update sonarqube.yml * refactor: 사용자 정보 조회 컨트롤러에 HTTP 메서드 지정 * Update backend.yml * Delete sonarqube.yml * Update frontend.yml * Update deploy-backend-dev.yml * Update deploy-frontend-dev.yml * Update deploy-backend-dev.yml --- .github/workflows/backend.yml | 28 +++++++-- .github/workflows/deploy-backend-dev.yml | 9 +++ .github/workflows/deploy-frontend-dev.yml | 4 +- .github/workflows/frontend.yml | 2 + .github/workflows/sonarqube.yml | 60 ------------------- backend/build.gradle | 14 ++--- .../member/controller/MemberController.java | 3 +- 7 files changed, 45 insertions(+), 75 deletions(-) delete mode 100644 .github/workflows/sonarqube.yml diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index 7b9d87e09..63725ef8d 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -5,9 +5,12 @@ on: branches: - main pull_request: + paths: + - 'backend/**' branches: - main - develop + types: [opened, synchronize] defaults: run: @@ -15,7 +18,17 @@ defaults: jobs: build: - runs-on: ubuntu-latest + runs-on: ubuntu-latest + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SONAR_PROJECT_KEY: moamoa + SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }} + SONAR_TOKEN : ${{ secrets.SONAR_TOKEN }} + PR_NUMBER: ${{ github.event.pull_request.number }} + client-id: ${{ secrets.CLIENT_ID }} + client-secret: ${{ secrets.CLIENT_SECRET }} + jwt-secret-key: ${{ secrets.JWT_SECRET_KEY }} + jwt-expire-length: ${{ secrets.JWT_EXPIRE_LENGTH }} steps: - name: Checkout uses: actions/checkout@v3 @@ -30,9 +43,12 @@ jobs: run: ./gradlew build --exclude-task test --exclude-task asciidoctor - name: Test - env: - client-id: ${{ secrets.CLIENT_ID }} - client-secret: ${{ secrets.CLIENT_SECRET }} - jwt-secret-key: ${{ secrets.JWT_SECRET_KEY }} - jwt-expire-length: ${{ secrets.JWT_EXPIRE_LENGTH }} run: ./gradlew test -Doauth2.github.client-id=${{ env.client-id }} -Doauth2.github.client-secret=${{ env.client-secret }} -Dsecurity.jwt.token.secret-key=${{ env.jwt-secret-key }} -Dsecurity.jwt.token.expire-length=${{ env.jwt-expire-length }} + + - name: SonarQube + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }} + run: ./gradlew sonarqube --info + diff --git a/.github/workflows/deploy-backend-dev.yml b/.github/workflows/deploy-backend-dev.yml index f4abe2a0a..e1d58d0de 100644 --- a/.github/workflows/deploy-backend-dev.yml +++ b/.github/workflows/deploy-backend-dev.yml @@ -10,6 +10,8 @@ name: Deploy Backend Dev on: push: branches : develop + paths: + - 'backend/**' defaults: run: @@ -39,6 +41,13 @@ jobs: jwt-expire-length: ${{ secrets.JWT_EXPIRE_LENGTH }} run: ./gradlew test -Doauth2.github.client-id=${{ env.client-id }} -Doauth2.github.client-secret=${{ env.client-secret }} -Dsecurity.jwt.token.secret-key=${{ env.jwt-secret-key }} -Dsecurity.jwt.token.expire-length=${{ env.jwt-expire-length }} + - name: SonarQube + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }} + run: ./gradlew sonarqube --info + - name : Deploy run: | curl ${{ secrets.DEPLOY_REQUEST_URL }} diff --git a/.github/workflows/deploy-frontend-dev.yml b/.github/workflows/deploy-frontend-dev.yml index 1ca6d9834..96680b793 100644 --- a/.github/workflows/deploy-frontend-dev.yml +++ b/.github/workflows/deploy-frontend-dev.yml @@ -3,7 +3,9 @@ name: Deploy Frontend Dev on: push: branches: ["develop"] - + paths: + - 'frontend/**' + defaults: run: working-directory: frontend diff --git a/.github/workflows/frontend.yml b/.github/workflows/frontend.yml index c71af185b..4290423c2 100644 --- a/.github/workflows/frontend.yml +++ b/.github/workflows/frontend.yml @@ -3,6 +3,8 @@ name: frontend on: pull_request: branches: ["main", "develop"] + paths: + - 'frontend/**' defaults: run: diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml deleted file mode 100644 index 3e8c5251b..000000000 --- a/.github/workflows/sonarqube.yml +++ /dev/null @@ -1,60 +0,0 @@ -name: moamoa-sonarqube - -on: - pull_request: - branches: - - develop - types: [opened, synchronize] - -defaults: - run: - working-directory: ./backend - -jobs: - build: - runs-on: ubuntu-latest - steps: - - name: Checkout source code - uses: actions/checkout@v3 - - analysis: - runs-on: ubuntu-latest - env: - SONARQUBE_PROJECT_KEY: moamoa - SONARQUBE_URL: ${{ secrets.SONARQUBE_URL }} - SONARQUBE_TOKEN : ${{ secrets.SONARQUBE_TOKEN }} - PR_NUMBER: ${{ github.event.pull_request.number }} - client-id: ${{ secrets.CLIENT_ID }} - client-secret: ${{ secrets.CLIENT_SECRET }} - jwt-secret-key: ${{ secrets.JWT_SECRET_KEY }} - jwt-expire-length: ${{ secrets.JWT_EXPIRE_LENGTH }} - - steps: - - name: Checkout source code - uses: actions/checkout@v3 - - - name: gradlew permission change - run: sudo chmod 755 gradlew - - - name: Sonaqube Analysis - run: ./gradlew build sonarqube - -Dsonar.host.url=${{ env.SONARQUBE_URL }} - -Dsonar.projectKey=${{ env.SONARQUBE_PROJECT_KEY }} - -Dsonar.projectName=${{ env.SONARQUBE_PROJECT_KEY }}-${{ env.PR_NUMBER }} - -Dsonar.login=${{ env.SONARQUBE_TOKEN }} - -Doauth2.github.client-id=${{ env.client-id }} - -Doauth2.github.client-secret=${{ env.client-secret }} - -Dsecurity.jwt.token.secret-key=${{ env.jwt-secret-key }} - -Dsecurity.jwt.token.expire-length=${{ env.jwt-expire-length }} - - - name: Comment Sonarqube URL - uses: actions/github-script@v4 - with: - script: | - const { SONARQUBE_PROJECT_KEY, SONARQUBE_URL, PR_NUMBER } = process.env - github.issues.createComment({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - body: `📊 ${ SONARQUBE_PROJECT_KEY }-${ PR_NUMBER } 분석 결과 확인하기 [링크](${SONARQUBE_URL})` - }) diff --git a/backend/build.gradle b/backend/build.gradle index 32d0176f4..f6b75b240 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -1,10 +1,10 @@ plugins { - id 'org.sonarqube' version '3.3' - id 'jacoco' id 'org.springframework.boot' version '2.6.9' id 'io.spring.dependency-management' version '1.0.11.RELEASE' id 'java' id 'org.asciidoctor.jvm.convert' version "3.3.2" + id "org.sonarqube" version "3.4.0.2513" + id 'jacoco' } group = 'com.woowacourse' @@ -64,8 +64,8 @@ jacoco { jacocoTestReport { reports { xml.enabled true - csv.enabled false - html.enabled false + csv.enabled true + html.enabled true } } @@ -99,13 +99,13 @@ bootJar { sonarqube { properties { - property "sonar.projectKey", "moamoa" + property "sonar.projectKey", "woowacourse-teams_2022-moamoa_AYKqauCybW85TfLm8Krq" property "sonar.sources", "src" property "sonar.language", "java" property "sonar.sourceEncoding", "UTF-8" - property "sonar.profile", "Sonar way" + property "sonar.profile", "moamoa" property "sonar.java.binaries", "${buildDir}/classes" property "sonar.test.inclusions", "**/*Test.java" property "sonar.coverage.jacoco.xmlReportPaths", "${buildDir}/reports/jacoco/test/jacocoTestReport.xml" } -} \ No newline at end of file +} diff --git a/backend/src/main/java/com/woowacourse/moamoa/member/controller/MemberController.java b/backend/src/main/java/com/woowacourse/moamoa/member/controller/MemberController.java index 2432bda40..2d44222bf 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/member/controller/MemberController.java +++ b/backend/src/main/java/com/woowacourse/moamoa/member/controller/MemberController.java @@ -5,6 +5,7 @@ import com.woowacourse.moamoa.member.service.MemberService; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -14,7 +15,7 @@ public class MemberController { private final MemberService memberService; - @RequestMapping("/api/members/me") + @GetMapping("/api/members/me") public ResponseEntity<MemberResponse> getCurrentMember( @AuthenticationPrincipal Long githubId ) { From e9ef7313d959b9eb22298b1094c965246c8eb5d2 Mon Sep 17 00:00:00 2001 From: Donggyu <a29661498@gmail.com> Date: Wed, 17 Aug 2022 19:33:30 +0900 Subject: [PATCH 27/51] =?UTF-8?q?[BE]=20issue245:=20=EC=8A=A4=ED=84=B0?= =?UTF-8?q?=EB=94=94=20=EA=B0=80=EC=9E=85=EB=82=A0=EC=A7=9C=20=EB=B0=8F=20?= =?UTF-8?q?=EC=8A=A4=ED=84=B0=EB=94=94=20=EA=B0=9C=EC=88=98=20(#246)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 임시 커밋 * feat: 스터디 단건조회시 가입 개수도 함께 조회 * feat: 스터디 단건조회시 가입날짜도 포함 * refactor: createdDate -> participationDate * refactor: 메소드 분리를 통해 가독성 증가 * refactor: 불필요한 where 절 제거 * fix: test 코드 수정 * refactor: 메소드명 수정 * fix: 테스트 깨짐 해결 * refactor: MemberFullData -> ParticipatingMemberData --- .../moamoa/member/query/MemberDao.java | 62 +++++++++++++++---- .../query/data/ParticipatingMemberData.java | 31 ++++++++++ .../moamoa/study/domain/Participant.java | 9 +++ .../moamoa/study/domain/Study.java | 2 +- .../study/service/SearchingStudyService.java | 5 +- .../service/response/StudyDetailResponse.java | 5 +- .../GettingStudyDetailsAcceptanceTest.java | 2 + .../SearchingStudyControllerTest.java | 21 ++++++- backend/src/test/resources/schema.sql | 1 + 9 files changed, 118 insertions(+), 20 deletions(-) create mode 100644 backend/src/main/java/com/woowacourse/moamoa/member/query/data/ParticipatingMemberData.java diff --git a/backend/src/main/java/com/woowacourse/moamoa/member/query/MemberDao.java b/backend/src/main/java/com/woowacourse/moamoa/member/query/MemberDao.java index ff492988b..b66d449fd 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/member/query/MemberDao.java +++ b/backend/src/main/java/com/woowacourse/moamoa/member/query/MemberDao.java @@ -1,10 +1,10 @@ package com.woowacourse.moamoa.member.query; import com.woowacourse.moamoa.member.query.data.MemberData; - +import com.woowacourse.moamoa.member.query.data.ParticipatingMemberData; +import java.time.LocalDate; import java.util.List; import java.util.Map; - import java.util.Optional; import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.jdbc.core.RowMapper; @@ -14,13 +14,9 @@ @Repository public class MemberDao { - private static final RowMapper<MemberData> ROW_MAPPER = (rs, rn) -> { - Long githubId = rs.getLong("github_id"); - String username = rs.getString("username"); - String imageUrl = rs.getString("image_url"); - String profileUrl = rs.getString("profile_url"); - return new MemberData(githubId, username, imageUrl, profileUrl); - }; + private static final RowMapper<ParticipatingMemberData> MEMBER_FULL_DATA_ROW_MAPPER = createMemberFullDataRowMapper(); + + private static final RowMapper<MemberData> MEMBER_DATA_ROW_MAPPER = createMemberDataRowMapper(); private final NamedParameterJdbcTemplate jdbcTemplate; @@ -28,11 +24,29 @@ public MemberDao(final NamedParameterJdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } - public List<MemberData> findMembersByStudyId(final Long studyId) { - final String sql = "SELECT github_id, username, image_url, profile_url " + public List<ParticipatingMemberData> findMembersByStudyId(final Long studyId) { + final String sql = "SELECT member.id, github_id, username, image_url, profile_url, " + + "study_member.participation_date as participation_date, " + + countStudyNumber() + "FROM member JOIN study_member ON member.id = study_member.member_id " + + "JOIN study ON study_member.study_id = study.id " + "WHERE study_member.study_id = :id"; - return jdbcTemplate.query(sql, Map.of("id", studyId), ROW_MAPPER); + + return jdbcTemplate.query(sql, Map.of("id", studyId), MEMBER_FULL_DATA_ROW_MAPPER); + } + + private String countStudyNumber() { + return countParticipationStudy() + " + " + countOwnerStudy(); + } + + private String countParticipationStudy() { + return "((SELECT count(case when (study_member.member_id = member.id) then 1 end) " + + "FROM study JOIN study_member ON study.id = study_member.study_id) "; + } + + private String countOwnerStudy() { + return "(SELECT count(case when (study.owner_id = member.id) then 1 end) " + + "FROM study)) as number_of_study "; } public Optional<MemberData> findByGithubId(final Long githubId) { @@ -40,10 +54,32 @@ public Optional<MemberData> findByGithubId(final Long githubId) { final String sql = "SELECT github_id, username, image_url, profile_url " + "FROM member " + "WHERE member.github_id = :id"; - final MemberData data = jdbcTemplate.queryForObject(sql, Map.of("id", githubId), ROW_MAPPER); + final MemberData data = jdbcTemplate.queryForObject(sql, Map.of("id", githubId), MEMBER_DATA_ROW_MAPPER); return Optional.of(data); } catch (EmptyResultDataAccessException e) { return Optional.empty(); } } + + private static RowMapper<ParticipatingMemberData> createMemberFullDataRowMapper() { + return (resultSet, resultNumber) -> { + Long githubId = resultSet.getLong("github_id"); + String username = resultSet.getString("username"); + String imageUrl = resultSet.getString("image_url"); + String profileUrl = resultSet.getString("profile_url"); + LocalDate participationDate = resultSet.getDate("participation_date").toLocalDate(); + int numberOfStudy = resultSet.getInt("number_of_study"); + return new ParticipatingMemberData(githubId, username, imageUrl, profileUrl, participationDate, numberOfStudy); + }; + } + + private static RowMapper<MemberData> createMemberDataRowMapper() { + return (resultSet, resultNumber) -> { + Long githubId = resultSet.getLong("github_id"); + String username = resultSet.getString("username"); + String imageUrl = resultSet.getString("image_url"); + String profileUrl = resultSet.getString("profile_url"); + return new MemberData(githubId, username, imageUrl, profileUrl); + }; + } } diff --git a/backend/src/main/java/com/woowacourse/moamoa/member/query/data/ParticipatingMemberData.java b/backend/src/main/java/com/woowacourse/moamoa/member/query/data/ParticipatingMemberData.java new file mode 100644 index 000000000..c24302118 --- /dev/null +++ b/backend/src/main/java/com/woowacourse/moamoa/member/query/data/ParticipatingMemberData.java @@ -0,0 +1,31 @@ +package com.woowacourse.moamoa.member.query.data; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.time.LocalDate; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@Getter +@NoArgsConstructor(access = AccessLevel.PRIVATE) +@AllArgsConstructor +@EqualsAndHashCode +@ToString +public class ParticipatingMemberData { + + @JsonProperty("id") + private Long githubId; + + private String username; + + private String imageUrl; + + private String profileUrl; + + private LocalDate participationDate; + + private int numberOfStudy; +} diff --git a/backend/src/main/java/com/woowacourse/moamoa/study/domain/Participant.java b/backend/src/main/java/com/woowacourse/moamoa/study/domain/Participant.java index 50b602d1e..721d32915 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/study/domain/Participant.java +++ b/backend/src/main/java/com/woowacourse/moamoa/study/domain/Participant.java @@ -2,6 +2,7 @@ import static lombok.AccessLevel.PROTECTED; +import java.time.LocalDate; import javax.persistence.Column; import javax.persistence.Embeddable; import lombok.AllArgsConstructor; @@ -20,4 +21,12 @@ public class Participant { @Column(name = "member_id", nullable = false) private Long memberId; + + @Column(updatable = false, nullable = false) + private LocalDate participationDate; + + public Participant(final Long memberId) { + this.memberId = memberId; + this.participationDate = LocalDate.now(); + } } diff --git a/backend/src/main/java/com/woowacourse/moamoa/study/domain/Study.java b/backend/src/main/java/com/woowacourse/moamoa/study/domain/Study.java index 8d42f4f8e..b9a12accb 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/study/domain/Study.java +++ b/backend/src/main/java/com/woowacourse/moamoa/study/domain/Study.java @@ -16,8 +16,8 @@ import lombok.NoArgsConstructor; @Entity -@NoArgsConstructor(access = PROTECTED) @Getter +@NoArgsConstructor(access = PROTECTED) public class Study { @Id diff --git a/backend/src/main/java/com/woowacourse/moamoa/study/service/SearchingStudyService.java b/backend/src/main/java/com/woowacourse/moamoa/study/service/SearchingStudyService.java index 5c983fa14..22c94339b 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/study/service/SearchingStudyService.java +++ b/backend/src/main/java/com/woowacourse/moamoa/study/service/SearchingStudyService.java @@ -1,7 +1,7 @@ package com.woowacourse.moamoa.study.service; import com.woowacourse.moamoa.member.query.MemberDao; -import com.woowacourse.moamoa.member.query.data.MemberData; +import com.woowacourse.moamoa.member.query.data.ParticipatingMemberData; import com.woowacourse.moamoa.study.query.SearchingTags; import com.woowacourse.moamoa.study.query.StudyDetailsDao; import com.woowacourse.moamoa.study.query.StudySummaryDao; @@ -56,7 +56,8 @@ public StudiesResponse getStudies(final String title, final SearchingTags search public StudyDetailResponse getStudyDetails(final Long studyId) { final StudyDetailsData content = studyDetailsDao.findBy(studyId) .orElseThrow(StudyNotFoundException::new); - final List<MemberData> participants = memberDao.findMembersByStudyId(studyId); + final List<ParticipatingMemberData> participants = memberDao.findMembersByStudyId(studyId); + final List<TagData> attachedTags = tagDao.findTagsByStudyId(studyId); return new StudyDetailResponse(content, participants, attachedTags); } diff --git a/backend/src/main/java/com/woowacourse/moamoa/study/service/response/StudyDetailResponse.java b/backend/src/main/java/com/woowacourse/moamoa/study/service/response/StudyDetailResponse.java index bc2ed337f..255bc81b4 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/study/service/response/StudyDetailResponse.java +++ b/backend/src/main/java/com/woowacourse/moamoa/study/service/response/StudyDetailResponse.java @@ -1,6 +1,7 @@ package com.woowacourse.moamoa.study.service.response; import com.woowacourse.moamoa.member.query.data.MemberData; +import com.woowacourse.moamoa.member.query.data.ParticipatingMemberData; import com.woowacourse.moamoa.study.query.data.StudyDetailsData; import com.woowacourse.moamoa.tag.query.response.TagData; import java.time.LocalDate; @@ -29,11 +30,11 @@ public class StudyDetailResponse { private LocalDate startDate; private LocalDate endDate; private MemberData owner; - private List<MemberData> members; + private List<ParticipatingMemberData> members; private List<TagData> tags; public StudyDetailResponse(final StudyDetailsData study, - final List<MemberData> participants, + final List<ParticipatingMemberData> participants, final List<TagData> attachedTags) { this.id = study.getId(); this.title = study.getTitle(); diff --git a/backend/src/test/java/com/woowacourse/acceptance/test/study/GettingStudyDetailsAcceptanceTest.java b/backend/src/test/java/com/woowacourse/acceptance/test/study/GettingStudyDetailsAcceptanceTest.java index 730a78de2..de18993ee 100644 --- a/backend/src/test/java/com/woowacourse/acceptance/test/study/GettingStudyDetailsAcceptanceTest.java +++ b/backend/src/test/java/com/woowacourse/acceptance/test/study/GettingStudyDetailsAcceptanceTest.java @@ -83,6 +83,8 @@ public void getStudyDetails() { .body("members.username", contains(짱구_이름, 그린론_이름, 베루스_이름)) .body("members.imageUrl", contains(짱구_이미지_URL, 그린론_이미지_URL, 베루스_이미지_URL)) .body("members.profileUrl", contains(짱구_프로필_URL, 그린론_프로필_URL, 베루스_프로필_URL)) + .body("members.participationDate", not(empty())) + .body("members.numberOfStudy", contains(1, 1, 1)) .body("tags.id", not(empty())) .body("tags.name", contains("4기", "FE", "React")); } diff --git a/backend/src/test/java/com/woowacourse/moamoa/study/controller/SearchingStudyControllerTest.java b/backend/src/test/java/com/woowacourse/moamoa/study/controller/SearchingStudyControllerTest.java index de29f5ba4..ad6f22e0a 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/study/controller/SearchingStudyControllerTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/study/controller/SearchingStudyControllerTest.java @@ -235,7 +235,7 @@ void searchBySameAndDifferentKindFilters() { @DisplayName("스터디 상세 정보를 조회할 수 있다.") @Test - public void mentgetStudyDetails() { + public void getStudyDetails() { StudyDetailsData expect = StudyDetailsData.builder() // Study Content .id(javaStudyId).title("Java 스터디").excerpt("자바 설명").thumbnail("java thumbnail") @@ -309,7 +309,6 @@ public void getStudyDetailsWithOptional() { @DisplayName("스터디 참여자와 부착된 태그가 없는 스터디의 세부사항 조회") @Test void getNotHasParticipantsAndAttachedTagsStudyDetails() { - final StudyDetailsData expect = StudyDetailsData.builder() // Study Content .id(linuxStudyId).title("Linux 스터디").excerpt("리눅스 설명").thumbnail("linux thumbnail") @@ -337,6 +336,24 @@ void getNotHasParticipantsAndAttachedTagsStudyDetails() { assertAttachedTags(actual.getTags(), expectAttachedTags); } + @DisplayName("스터디 디테일 정보 조회 시 스터디원들이 가입한 스터디의 수와 가입날짜도 함께 조회한다.") + @Test + public void findStudyDetailsWithNumberOfStudy() { + final ResponseEntity<StudyDetailResponse> response = sut.getStudyDetails(javaStudyId); + + final StudyDetailResponse responseBody = response.getBody(); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(responseBody.getMembers()) + .filteredOn(member -> member.getParticipationDate() != null) + .hasSize(2) + .extracting("githubId", "username", "imageUrl", "profileUrl", "numberOfStudy") + .containsExactlyInAnyOrder( + tuple(dwoo.getGithubId(), dwoo.getUsername(), dwoo.getImageUrl(), dwoo.getProfileUrl(), 3), + tuple(verus.getGithubId(), verus.getUsername(), verus.getImageUrl(), verus.getProfileUrl(), 4) + ); + } + private void assertStudyContent(final StudyDetailResponse actual, final StudyDetailsData expect) { assertThat(actual.getId()).isEqualTo(expect.getId()); assertThat(actual.getTitle()).isEqualTo(expect.getTitle()); diff --git a/backend/src/test/resources/schema.sql b/backend/src/test/resources/schema.sql index 32d9512ad..c0f61e824 100644 --- a/backend/src/test/resources/schema.sql +++ b/backend/src/test/resources/schema.sql @@ -92,6 +92,7 @@ CREATE TABLE study_member id BIGINT PRIMARY KEY AUTO_INCREMENT, study_id BIGINT, member_id BIGINT, + participation_date DATE not null, FOREIGN KEY (study_id) REFERENCES study (id), FOREIGN KEY (member_id) REFERENCES member (id) ); From 77cf4050bf658c051950d78713dd979ae263a7c1 Mon Sep 17 00:00:00 2001 From: TaeYoon <uni613@naver.com> Date: Wed, 17 Aug 2022 20:05:31 +0900 Subject: [PATCH 28/51] =?UTF-8?q?[FE]=20issue243:=20=EB=A7=81=ED=81=AC=20?= =?UTF-8?q?=EB=AA=A8=EC=9D=8C=20CRUD=20+=20=EB=A7=81=ED=81=AC=20=EB=AF=B8?= =?UTF-8?q?=EB=A6=AC=EB=B3=B4=EA=B8=B0=20=EA=B5=AC=ED=98=84=20(#274)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: 스토리북 오류 수정 babel-loader -> esbuild-loader로 바꾸면서 생긴 오류 * feat: UserDescription 컴포넌트 구현 * feat: Image 컴포넌트 -> CenterImage 컴포넌트 * feat: LinkPreview 컴포넌트 구현 * feat: LinkPreview 컴포넌트 수정 * chore: storybook main.js 수정 css props 지원 * feat: DropDownBox 컴포넌트 구현 * feat: DropDownBox 컴포넌트 적용 * refactor: eslint 적용 * feat: LinkItem 컴포넌트 구현 * refactor: icon svg 컴포넌트 분리 * feat: LinkForm 컴포넌트 구현 * refactor: Input.style.js 공통 컴포넌트 폴더로 이동 * feat: LinkForm 컴포넌트 로직 수정 useForm 적용 * feat: url 정규식 검증 로직 추가 * feat: LinkRoomTabPanel 컴포넌트 구현 * style: 스터디 방 페이지 반응형 UI 적용 * feat: Modal 컴포넌트 구현 react portal 사용 * feat: 링크 모음탭에 모달 적용 * feat: 링크 모킹 서버 구현 * feat: 링크 조회 무한 스크롤 구현 * feat: 링크 등록 기능 구현 * feat: 링크 삭제 기능 구현 * feat: 링크 수정 기능 구현 * refactor: eslint, prettier 적용 * feat: 메타 태그 추가 * feat: 내 링크만 수정/삭제 가능하도록 수정 + 링크 폼에 프로필 추가 * chore: api 환경변수 추가 * feat: 링크 미리보기 구현 링크 미리보기 서버 배포 필요 * feat: 도메인네임만 보여주도록 수정 https://www.moamoa.space -> moamoa.space * test: 테스트 수정 * feat: 링크 미리보기 서버와 연결 * fix: domainName이 유효하지 않은 url인 오류 수정 domainName이 올바른 url 형태가 아닐 때 에러가 발생하여 앱이 멈추는 오류 해결 * refactor: lint, type 수정 * chore: api url 변경 * refactor: AxiosError 타입 수정 * refactor: Noop 타입 분리 * refactor: linkHandler 수정 * refactor: CenterImage src 타입 수정 * refactor: useForm onChange 함수 분리 * refactor: list -> 복수형 컴포넌트는 list를 사용하지만, 데이터의 경우 list 대신 복수형 단어를 사용하도록 수정 - 예시: linkList -> links / studyList -> studies * refactor: 상대경로 -> 절대경로 * refactor: api 폴더 구조 변경 api 아래 도메인 폴더를 만들고 index.ts 파일에 해당 도메인 관련 api타입, api 함수, 리액트 쿼리 커스텀 훅을 포함 * feat: 링크 클릭시 새 탭 열리게 수정 * refactor: lint, ts 에러 수정 * refactor: z-index 위치 수정 * refactor: css props에 tw 함수 적용 --- frontend/.storybook/main.js | 37 +- frontend/.storybook/preview-body.html | 2 + .../e2e/CreateStudyFormValidation.cy.tsx | 26 +- frontend/env/.env.dev | 3 +- frontend/env/.env.local | 1 + frontend/env/.env.prod | 3 +- frontend/package-lock.json | 377 +++++++++++++----- frontend/public/index.html | 20 +- frontend/public/open-graph-image.png | Bin 0 -> 31293 bytes frontend/src/api/auth/index.ts | 43 ++ frontend/src/api/axiosInstance.ts | 6 +- frontend/src/api/deleteRefreshToken.ts | 10 - frontend/src/api/deleteReview.ts | 14 - frontend/src/api/getAccessToken.ts | 10 - frontend/src/api/getMyStudyList.ts | 10 - frontend/src/api/getStudyDetail.ts | 10 - frontend/src/api/getStudyList.ts | 18 - frontend/src/api/getStudyReviews.ts | 11 - frontend/src/api/getTagList.ts | 10 - frontend/src/api/getUserInformation.ts | 10 - frontend/src/api/getUserRole.ts | 10 - frontend/src/api/index.ts | 16 - frontend/src/api/link-preview/index.ts | 68 ++++ frontend/src/api/link/index.ts | 54 +++ frontend/src/api/links/index.ts | 55 +++ frontend/src/api/member/index.ts | 41 ++ frontend/src/api/my-studies/index.ts | 20 + frontend/src/api/my-study/index.ts | 20 + frontend/src/api/postJoiningStudy.ts | 14 - frontend/src/api/postLogin.ts | 16 - frontend/src/api/postNewStudy.ts | 12 - frontend/src/api/postReview.ts | 17 - frontend/src/api/putReview.ts | 17 - frontend/src/api/review/index.ts | 64 +++ frontend/src/api/reviews/index.ts | 29 ++ frontend/src/api/studies/index.ts | 66 +++ frontend/src/api/study/index.ts | 46 +++ frontend/src/api/tags/index.ts | 20 + frontend/src/auth/accessToken.ts | 6 +- .../components/arrow-button/ArrowButton.tsx | 37 +- .../src/components/avatar/Avatar.style.tsx | 8 - frontend/src/components/avatar/Avatar.tsx | 3 +- frontend/src/components/card/Card.style.tsx | 10 - frontend/src/components/card/Card.tsx | 3 +- .../center-image/CenterImage.style.tsx | 9 + .../components/center-image/CenterImage.tsx | 19 + frontend/src/components/checkbox/Checkbox.tsx | 2 +- .../drop-down-box/DropDownBox.stories.tsx | 37 ++ .../drop-down-box/DropDownBox.style.tsx | 16 +- .../components/drop-down-box/DropDownBox.tsx | 33 ++ frontend/src/components/image/Image.tsx | 17 - .../infinite-scroll/InfiniteScroll.tsx | 4 +- .../components/input/Input.style.tsx | 6 +- .../src/components/modal/Modal.stories.tsx | 41 ++ frontend/src/components/modal/Modal.style.tsx | 25 ++ frontend/src/components/modal/Modal.tsx | 23 ++ frontend/src/components/svg/index.tsx | 199 +++++++++ frontend/src/constants.ts | 56 ++- frontend/src/custom-types/common.d.ts | 1 + frontend/src/custom-types/index.ts | 106 +---- frontend/src/hooks/useAuth.ts | 6 +- frontend/src/hooks/useForm.tsx | 2 +- frontend/src/hooks/useUserInfo.ts | 15 +- frontend/src/layout/header/Header.style.tsx | 6 +- frontend/src/layout/header/Header.tsx | 89 ++--- .../drop-down-box/DropDownBox.stories.tsx | 34 -- .../components/drop-down-box/DropDownBox.tsx | 20 - .../layout/header/components/logo/Logo.tsx | 4 +- .../components/search-bar/SearchBar.tsx | 21 +- frontend/src/mocks/detailStudyHandlers.ts | 4 +- frontend/src/mocks/handlers.ts | 2 + frontend/src/mocks/linkHandlers.ts | 86 ++++ frontend/src/mocks/links.json | 43 ++ frontend/src/mocks/memberHandlers.ts | 9 +- frontend/src/mocks/reviewHandler.ts | 9 +- .../components/category/Category.tsx | 5 +- .../EnrollmentEndDate.style.tsx | 2 +- .../components/excerpt/Excerpt.tsx | 5 +- .../max-member-count/MaxMemberCount.style.tsx | 2 +- .../components/period/Period.style.tsx | 2 +- .../components/select/Select.style.tsx | 2 +- .../components/subject/Subject.tsx | 5 +- .../components/textarea/Textarea.style.tsx | 2 +- .../components/title/Title.style.tsx | 2 +- .../components/title/Title.tsx | 5 +- .../hooks/useCreateStudyPage.ts | 9 +- .../create-study-page/hooks/useGetTagList.ts | 12 - .../hooks/usePostNewStudy.ts | 12 - .../StudyMemberSection.stories.tsx | 10 +- .../StudyMemberSection.tsx | 24 +- .../StudyReviewSection.tsx | 3 +- .../pages/detail-page/hooks/useDetailPage.ts | 23 +- .../pages/detail-page/hooks/useGetDetail.ts | 16 - .../detail-page/hooks/useGetStudyReviews.ts | 15 - .../hooks/useLoginRedirectPage.ts | 8 +- frontend/src/pages/main-page/MainPage.tsx | 4 +- .../CreateNewStudyButton.style.tsx | 4 + .../CreateNewStudyButton.tsx | 18 +- .../filter-section/FilterSection.style.tsx | 6 +- .../filter-section/FilterSection.tsx | 8 +- .../FilterButtonList.style.tsx | 2 +- .../filter-button-list/FilterButtonList.tsx | 4 +- .../hooks/useGetInfiniteStudyList.ts | 49 --- .../src/pages/main-page/hooks/useMainPage.ts | 8 +- .../components/my-study-card/MyStudyCard.tsx | 43 +- .../my-study-page/hooks/useGetMyStudy.ts | 12 - .../my-study-page/hooks/useMyStudyPage.ts | 4 +- .../pages/study-room-page/StudyRoomPage.tsx | 2 +- .../LinkRoomTabPanel.stories.tsx | 13 + .../LinkRoomTabPanel.style.tsx | 63 +++ .../link-room-tab-panel/LinkRoomTabPanel.tsx | 76 ++++ .../link-edit-form/LinkEditForm.style.tsx | 11 + .../link-edit-form/LinkEditForm.tsx | 136 +++++++ .../components/link-form/LinkForm.stories.tsx | 14 + .../components/link-form/LinkForm.style.tsx | 74 ++++ .../components/link-form/LinkForm.tsx | 128 ++++++ .../components/link-item/LinkItem.stories.tsx | 24 ++ .../components/link-item/LinkItem.style.tsx | 64 +++ .../components/link-item/LinkItem.tsx | 104 +++++ .../components/link-item/hooks/useLinkItem.ts | 78 ++++ .../link-preview/LinkPreview.stories.tsx | 31 ++ .../link-preview/LinkPreview.style.tsx | 78 ++++ .../components/link-preview/LinkPreview.tsx | 33 ++ .../UserDescription.stories.tsx | 31 ++ .../UserDescription.style.tsx | 23 ++ .../user-description/UserDescription.tsx | 22 + .../hooks/useLinkRoomTabPanel.ts | 43 ++ .../review-tab-panel/ReviewTabPanel.tsx | 4 +- .../reivew-form/ReviewForm.stories.tsx | 10 + .../components/reivew-form/ReviewForm.tsx | 17 +- .../review-comment/ReviewComment.style.tsx | 41 +- .../review-comment/ReviewComment.tsx | 34 +- .../review-comment/useReviewComment.ts | 39 +- .../review-edit-form/ReviewEditForm.tsx | 19 +- .../components/side-menu/SideMenu.stories.tsx | 2 +- .../components/side-menu/SideMenu.style.tsx | 29 ++ .../components/side-menu/SideMenu.tsx | 23 +- .../tab-button/TabButton.stories.tsx | 14 - .../components/tab-button/TabButton.style.tsx | 2 +- .../hooks/useStudyRoomPage.tsx | 15 +- frontend/src/styles/Globalstyles.tsx | 3 +- frontend/src/utils/tw.ts | 1 + frontend/tsconfig.json | 2 +- frontend/webpack/webpack.common.js | 13 + frontend/webpack/webpack.dev.js | 1 + frontend/webpack/webpack.local.js | 1 + frontend/webpack/webpack.prod.js | 1 + 147 files changed, 2770 insertions(+), 1092 deletions(-) create mode 100644 frontend/.storybook/preview-body.html create mode 100644 frontend/public/open-graph-image.png create mode 100644 frontend/src/api/auth/index.ts delete mode 100644 frontend/src/api/deleteRefreshToken.ts delete mode 100644 frontend/src/api/deleteReview.ts delete mode 100644 frontend/src/api/getAccessToken.ts delete mode 100644 frontend/src/api/getMyStudyList.ts delete mode 100644 frontend/src/api/getStudyDetail.ts delete mode 100644 frontend/src/api/getStudyList.ts delete mode 100644 frontend/src/api/getStudyReviews.ts delete mode 100644 frontend/src/api/getTagList.ts delete mode 100644 frontend/src/api/getUserInformation.ts delete mode 100644 frontend/src/api/getUserRole.ts delete mode 100644 frontend/src/api/index.ts create mode 100644 frontend/src/api/link-preview/index.ts create mode 100644 frontend/src/api/link/index.ts create mode 100644 frontend/src/api/links/index.ts create mode 100644 frontend/src/api/member/index.ts create mode 100644 frontend/src/api/my-studies/index.ts create mode 100644 frontend/src/api/my-study/index.ts delete mode 100644 frontend/src/api/postJoiningStudy.ts delete mode 100644 frontend/src/api/postLogin.ts delete mode 100644 frontend/src/api/postNewStudy.ts delete mode 100644 frontend/src/api/postReview.ts delete mode 100644 frontend/src/api/putReview.ts create mode 100644 frontend/src/api/review/index.ts create mode 100644 frontend/src/api/reviews/index.ts create mode 100644 frontend/src/api/studies/index.ts create mode 100644 frontend/src/api/study/index.ts create mode 100644 frontend/src/api/tags/index.ts create mode 100644 frontend/src/components/center-image/CenterImage.style.tsx create mode 100644 frontend/src/components/center-image/CenterImage.tsx create mode 100644 frontend/src/components/drop-down-box/DropDownBox.stories.tsx rename frontend/src/{layout/header => }/components/drop-down-box/DropDownBox.style.tsx (74%) create mode 100644 frontend/src/components/drop-down-box/DropDownBox.tsx delete mode 100644 frontend/src/components/image/Image.tsx rename frontend/src/{pages/create-study-page => }/components/input/Input.style.tsx (81%) create mode 100644 frontend/src/components/modal/Modal.stories.tsx create mode 100644 frontend/src/components/modal/Modal.style.tsx create mode 100644 frontend/src/components/modal/Modal.tsx create mode 100644 frontend/src/components/svg/index.tsx delete mode 100644 frontend/src/layout/header/components/drop-down-box/DropDownBox.stories.tsx delete mode 100644 frontend/src/layout/header/components/drop-down-box/DropDownBox.tsx create mode 100644 frontend/src/mocks/linkHandlers.ts create mode 100644 frontend/src/mocks/links.json delete mode 100644 frontend/src/pages/create-study-page/hooks/useGetTagList.ts delete mode 100644 frontend/src/pages/create-study-page/hooks/usePostNewStudy.ts delete mode 100644 frontend/src/pages/detail-page/hooks/useGetDetail.ts delete mode 100644 frontend/src/pages/detail-page/hooks/useGetStudyReviews.ts delete mode 100644 frontend/src/pages/main-page/hooks/useGetInfiniteStudyList.ts delete mode 100644 frontend/src/pages/my-study-page/hooks/useGetMyStudy.ts create mode 100644 frontend/src/pages/study-room-page/components/link-room-tab-panel/LinkRoomTabPanel.stories.tsx create mode 100644 frontend/src/pages/study-room-page/components/link-room-tab-panel/LinkRoomTabPanel.style.tsx create mode 100644 frontend/src/pages/study-room-page/components/link-room-tab-panel/LinkRoomTabPanel.tsx create mode 100644 frontend/src/pages/study-room-page/components/link-room-tab-panel/components/link-edit-form/LinkEditForm.style.tsx create mode 100644 frontend/src/pages/study-room-page/components/link-room-tab-panel/components/link-edit-form/LinkEditForm.tsx create mode 100644 frontend/src/pages/study-room-page/components/link-room-tab-panel/components/link-form/LinkForm.stories.tsx create mode 100644 frontend/src/pages/study-room-page/components/link-room-tab-panel/components/link-form/LinkForm.style.tsx create mode 100644 frontend/src/pages/study-room-page/components/link-room-tab-panel/components/link-form/LinkForm.tsx create mode 100644 frontend/src/pages/study-room-page/components/link-room-tab-panel/components/link-item/LinkItem.stories.tsx create mode 100644 frontend/src/pages/study-room-page/components/link-room-tab-panel/components/link-item/LinkItem.style.tsx create mode 100644 frontend/src/pages/study-room-page/components/link-room-tab-panel/components/link-item/LinkItem.tsx create mode 100644 frontend/src/pages/study-room-page/components/link-room-tab-panel/components/link-item/hooks/useLinkItem.ts create mode 100644 frontend/src/pages/study-room-page/components/link-room-tab-panel/components/link-preview/LinkPreview.stories.tsx create mode 100644 frontend/src/pages/study-room-page/components/link-room-tab-panel/components/link-preview/LinkPreview.style.tsx create mode 100644 frontend/src/pages/study-room-page/components/link-room-tab-panel/components/link-preview/LinkPreview.tsx create mode 100644 frontend/src/pages/study-room-page/components/link-room-tab-panel/components/user-description/UserDescription.stories.tsx create mode 100644 frontend/src/pages/study-room-page/components/link-room-tab-panel/components/user-description/UserDescription.style.tsx create mode 100644 frontend/src/pages/study-room-page/components/link-room-tab-panel/components/user-description/UserDescription.tsx create mode 100644 frontend/src/pages/study-room-page/components/link-room-tab-panel/hooks/useLinkRoomTabPanel.ts diff --git a/frontend/.storybook/main.js b/frontend/.storybook/main.js index 6cc0326f2..4db72d588 100644 --- a/frontend/.storybook/main.js +++ b/frontend/.storybook/main.js @@ -1,4 +1,5 @@ const { resolve } = require('path'); +const { ESBuildMinifyPlugin } = require('esbuild-loader'); module.exports = { stories: ['../src/**/*.stories.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'], @@ -34,27 +35,27 @@ module.exports = { '@mocks': resolve(__dirname, '../src/mocks'), }; - config.module.rules[0].use[0].options.presets = [ - require.resolve('@babel/preset-env'), - [ - require.resolve('@babel/preset-react'), - { - runtime: 'automatic', - importSource: '@emotion/react', + config.module.rules = [ + ...config.module.rules, + { + test: /\.(ts|tsx)$/, + exclude: /node_modules/, + loader: 'esbuild-loader', + options: { + loader: 'tsx', + target: 'es2022', }, - ], + }, + { + test: /\.(png|jpg|jpeg)$/i, + type: 'asset/resource', + }, ]; - config.module.rules[0].use[0].options.plugins = [ - [ - require.resolve('@emotion/babel-plugin'), - { - sourceMap: true, - autoLabel: 'dev-only', - labelFormat: '[local]', - cssPropOptimization: true, - }, - ], + config.optimization.minimizer = [ + new ESBuildMinifyPlugin({ + target: 'es2020', + }), ]; return config; diff --git a/frontend/.storybook/preview-body.html b/frontend/.storybook/preview-body.html new file mode 100644 index 000000000..7104d21a6 --- /dev/null +++ b/frontend/.storybook/preview-body.html @@ -0,0 +1,2 @@ +<div id="modal"></div> +<!-- 스토리북에서 Portal 사용을 위해 필요--> diff --git a/frontend/cypress/e2e/CreateStudyFormValidation.cy.tsx b/frontend/cypress/e2e/CreateStudyFormValidation.cy.tsx index f7cc5b86d..743984a1a 100644 --- a/frontend/cypress/e2e/CreateStudyFormValidation.cy.tsx +++ b/frontend/cypress/e2e/CreateStudyFormValidation.cy.tsx @@ -24,18 +24,20 @@ describe('스터디 개설 페이지 폼 유효성 테스트', () => { const minText = 'a'; const maxText30 = 'abcdefghijklmnopqrstuvwxyzabcd'; - const redOutlineCss = 'rgb(255, 0, 0) solid 2px'; + const redBorderCss = '1px solid rgb(255, 0, 0)'; + const border = 'border'; + it(`스터디 제목은 최소 ${TITLE_LENGTH.MIN.VALUE}글자에서 최대 ${TITLE_LENGTH.MAX.VALUE}글자까지 입력할 수 있다.`, () => { cy.get(`@${studyTitle}`).type(minText); - cy.get(`@${studyTitle}`).should('not.have.css', 'outline', redOutlineCss); + cy.get(`@${studyTitle}`).should('not.have.css', border, redBorderCss); cy.get(`@${studyTitle}`).clear().type(maxText30); - cy.get(`@${studyTitle}`).should('not.have.css', 'outline', redOutlineCss); + cy.get(`@${studyTitle}`).should('not.have.css', border, redBorderCss); }); it(`스터디 제목이 ${TITLE_LENGTH.MIN.VALUE}글자 미만이면 입력창 테두리가 빨갛게 된다.`, () => { cy.get(`@${studyTitle}`).type(minText).clear(); - cy.get(`@${studyTitle}`).should('have.css', 'outline', redOutlineCss); + cy.get(`@${studyTitle}`).should('have.css', border, redBorderCss); }); it(`스터디 제목이 ${TITLE_LENGTH.MAX.VALUE}글자를 초과하면 초과한 글자는 입력되지 않는다.`, () => { @@ -46,37 +48,37 @@ describe('스터디 개설 페이지 폼 유효성 테스트', () => { it(`스터디 제목이 ${TITLE_LENGTH.MIN.VALUE}글자 미만이면 개설하기 버튼을 눌렀을 때 입력창 테두리가 빨갛게 된다.`, () => { cy.get(`@${studyTitle}`).type(minText).clear(); cy.get(`@${submitButton}`).click(); - cy.get(`@${studyTitle}`).should('have.css', 'outline', redOutlineCss); + cy.get(`@${studyTitle}`).should('have.css', border, redBorderCss); }); it(`스터디 소개글은 최소 ${DESCRIPTION_LENGTH.MIN.VALUE}글자를 입력할 수 있다.`, () => { cy.get(`@${description}`).type(minText); - cy.get(`@${description}`).should('not.have.css', 'outline', redOutlineCss); + cy.get(`@${description}`).should('not.have.css', border, redBorderCss); }); it(`스터디 소개글이 ${DESCRIPTION_LENGTH.MIN.VALUE}글자 미만이면 입력창 테두리가 빨갛게 된다.`, () => { cy.get(`@${description}`).type(minText).clear(); - cy.get(`@${description}`).should('have.css', 'outline', redOutlineCss); + cy.get(`@${description}`).should('have.css', border, redBorderCss); }); it(`스터디 소개글이 ${DESCRIPTION_LENGTH.MIN.VALUE}글자 미만이면 개설하기 버튼을 눌렀을 때 입력창 테두리가 빨갛게 된다.`, () => { cy.get(`@${description}`).type(minText).clear(); cy.get(`@${submitButton}`).click(); - cy.get(`@${description}`).should('have.css', 'outline', redOutlineCss); + cy.get(`@${description}`).should('have.css', border, redBorderCss); }); const maxText50 = maxText30 + '12345678901234567890'; it(`스터디 한줄 소개는 최소 ${EXCERPT_LENGTH.MIN.VALUE}글자에서 최대 ${EXCERPT_LENGTH.MAX.VALUE}글자까지 입력할 수 있다.`, () => { cy.get(`@${excerpt}`).type(minText); - cy.get(`@${excerpt}`).should('not.have.css', 'outline', redOutlineCss); + cy.get(`@${excerpt}`).should('not.have.css', border, redBorderCss); cy.get(`@${excerpt}`).clear().type(maxText50); - cy.get(`@${excerpt}`).should('not.have.css', 'outline', redOutlineCss); + cy.get(`@${excerpt}`).should('not.have.css', border, redBorderCss); }); it(`스터디 한줄 소개가 ${EXCERPT_LENGTH.MIN.VALUE}글자 미만이면 입력창 테두리가 빨갛게 된다.`, () => { cy.get(`@${excerpt}`).type(minText).clear(); - cy.get(`@${excerpt}`).should('have.css', 'outline', redOutlineCss); + cy.get(`@${excerpt}`).should('have.css', border, redBorderCss); }); it(`스터디 한줄 소개가 ${EXCERPT_LENGTH.MAX.VALUE}글자를 초과하면 초과한 글자는 입력되지 않는다.`, () => { @@ -87,7 +89,7 @@ describe('스터디 개설 페이지 폼 유효성 테스트', () => { it(`스터디 소개글이 ${EXCERPT_LENGTH.MIN.VALUE}글자 미만이면 개설하기 버튼을 눌렀을 때 입력창 테두리가 빨갛게 된다.`, () => { cy.get(`@${excerpt}`).type(minText).clear(); cy.get(`@${submitButton}`).click(); - cy.get(`@${excerpt}`).should('have.css', 'outline', redOutlineCss); + cy.get(`@${excerpt}`).should('have.css', border, redBorderCss); }); it('스터디 시작 날짜는 필수로 입력해야 한다.', () => { diff --git a/frontend/env/.env.dev b/frontend/env/.env.dev index 479996cc2..1696a7c11 100644 --- a/frontend/env/.env.dev +++ b/frontend/env/.env.dev @@ -1,2 +1,3 @@ -API_URL="https://dev.moamoa.ne.kr" +API_URL="https://api.dev.moamoa.space" CLIENT_ID="cb83d95cd5644436b090" +LINK_PREVIEW_API_URL="https://api.og.moamoa.space" diff --git a/frontend/env/.env.local b/frontend/env/.env.local index 5d27bfa26..7a5cfb9cb 100644 --- a/frontend/env/.env.local +++ b/frontend/env/.env.local @@ -1,2 +1,3 @@ API_URL="" CLIENT_ID="cb83d95cd5644436b090" +LINK_PREVIEW_API_URL="https://api.og.moamoa.space" diff --git a/frontend/env/.env.prod b/frontend/env/.env.prod index b70cc7869..b93b57072 100644 --- a/frontend/env/.env.prod +++ b/frontend/env/.env.prod @@ -1,2 +1,3 @@ -API_URL="https://moamoa.ne.kr" +API_URL="https://api.moamoa.space" CLIENT_ID="cb83d95cd5644436b090" +LINK_PREVIEW_API_URL="https://api.og.moamoa.space" diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 9a162488e..3248b053c 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -68,6 +68,7 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", "integrity": "sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==", + "dev": true, "dependencies": { "@jridgewell/gen-mapping": "^0.1.0", "@jridgewell/trace-mapping": "^0.3.9" @@ -80,6 +81,7 @@ "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz", "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==", + "dev": true, "dependencies": { "@babel/highlight": "^7.18.6" }, @@ -91,6 +93,7 @@ "version": "7.18.8", "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.18.8.tgz", "integrity": "sha512-HSmX4WZPPK3FUxYp7g2T6EyO8j96HlZJlxmKPSh6KAcqwyDrfx7hKjXpAW/0FhFfTJsR0Yt4lAjLI2coMptIHQ==", + "dev": true, "engines": { "node": ">=6.9.0" } @@ -99,6 +102,7 @@ "version": "7.18.10", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.18.10.tgz", "integrity": "sha512-JQM6k6ENcBFKVtWvLavlvi/mPcpYZ3+R+2EySDEMSMbp7Mn4FexlbbJVrx2R7Ijhr01T8gyqrOaABWIOgxeUyw==", + "dev": true, "dependencies": { "@ampproject/remapping": "^2.1.0", "@babel/code-frame": "^7.18.6", @@ -128,6 +132,7 @@ "version": "7.18.12", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.18.12.tgz", "integrity": "sha512-dfQ8ebCN98SvyL7IxNMCUtZQSq5R7kxgN+r8qYTGDmmSion1hX2C0zq2yo1bsCDhXixokv1SAWTZUMYbO/V5zg==", + "dev": true, "dependencies": { "@babel/types": "^7.18.10", "@jridgewell/gen-mapping": "^0.3.2", @@ -141,6 +146,7 @@ "version": "0.3.2", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", + "dev": true, "dependencies": { "@jridgewell/set-array": "^1.0.1", "@jridgewell/sourcemap-codec": "^1.4.10", @@ -179,6 +185,7 @@ "version": "7.18.9", "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.18.9.tgz", "integrity": "sha512-tzLCyVmqUiFlcFoAPLA/gL9TeYrF61VLNtb+hvkuVaB5SUjW7jcfrglBIX1vUIoT7CLP3bBlIMeyEsIl2eFQNg==", + "dev": true, "dependencies": { "@babel/compat-data": "^7.18.8", "@babel/helper-validator-option": "^7.18.6", @@ -250,6 +257,7 @@ "version": "7.18.9", "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz", "integrity": "sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==", + "dev": true, "engines": { "node": ">=6.9.0" } @@ -270,6 +278,7 @@ "version": "7.18.9", "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.18.9.tgz", "integrity": "sha512-fJgWlZt7nxGksJS9a0XdSaI4XvpExnNIgRP+rVefWh5U7BL8pPuir6SJUmFKRfjWQ51OtWSzwOxhaH/EBWWc0A==", + "dev": true, "dependencies": { "@babel/template": "^7.18.6", "@babel/types": "^7.18.9" @@ -282,6 +291,7 @@ "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz", "integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==", + "dev": true, "dependencies": { "@babel/types": "^7.18.6" }, @@ -305,6 +315,7 @@ "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz", "integrity": "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==", + "dev": true, "dependencies": { "@babel/types": "^7.18.6" }, @@ -316,6 +327,7 @@ "version": "7.18.9", "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.18.9.tgz", "integrity": "sha512-KYNqY0ICwfv19b31XzvmI/mfcylOzbLtowkw+mfvGPAQ3kfCnMLYbED3YecL5tPd8nAYFQFAd6JHp2LxZk/J1g==", + "dev": true, "dependencies": { "@babel/helper-environment-visitor": "^7.18.9", "@babel/helper-module-imports": "^7.18.6", @@ -346,6 +358,7 @@ "version": "7.18.9", "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.18.9.tgz", "integrity": "sha512-aBXPT3bmtLryXaoJLyYPXPlSD4p1ld9aYeR+sJNOZjJJGiOpb+fKfh3NkcCu7J54nUJwCERPBExCCpyCOHnu/w==", + "dev": true, "engines": { "node": ">=6.9.0" } @@ -388,6 +401,7 @@ "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.18.6.tgz", "integrity": "sha512-iNpIgTgyAvDQpDj76POqg+YEt8fPxx3yaNBg3S30dxNKm2SWfYhD0TGrK/Eu9wHpUW63VQU894TsTg+GLbUa1g==", + "dev": true, "dependencies": { "@babel/types": "^7.18.6" }, @@ -411,6 +425,7 @@ "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz", "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==", + "dev": true, "dependencies": { "@babel/types": "^7.18.6" }, @@ -422,6 +437,7 @@ "version": "7.18.10", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.18.10.tgz", "integrity": "sha512-XtIfWmeNY3i4t7t4D2t02q50HvqHybPqW2ki1kosnvWCwuCMeo81Jf0gwr85jy/neUdg5XDdeFE/80DXiO+njw==", + "dev": true, "engines": { "node": ">=6.9.0" } @@ -430,6 +446,7 @@ "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.18.6.tgz", "integrity": "sha512-MmetCkz9ej86nJQV+sFCxoGGrUbU3q02kgLciwkrt9QqEB7cP39oKEY0PakknEO0Gu20SskMRi+AYZ3b1TpN9g==", + "dev": true, "engines": { "node": ">=6.9.0" } @@ -438,6 +455,7 @@ "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz", "integrity": "sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw==", + "dev": true, "engines": { "node": ">=6.9.0" } @@ -461,6 +479,7 @@ "version": "7.18.9", "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.18.9.tgz", "integrity": "sha512-Jf5a+rbrLoR4eNdUmnFu8cN5eNJT6qdTdOg5IHIzq87WwyRw9PwguLFOWYgktN/60IP4fgDUawJvs7PjQIzELQ==", + "dev": true, "dependencies": { "@babel/template": "^7.18.6", "@babel/traverse": "^7.18.9", @@ -474,6 +493,7 @@ "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", + "dev": true, "dependencies": { "@babel/helper-validator-identifier": "^7.18.6", "chalk": "^2.0.0", @@ -487,6 +507,7 @@ "version": "7.18.11", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.18.11.tgz", "integrity": "sha512-9JKn5vN+hDt0Hdqn1PiJ2guflwP+B6Ga8qbDuoF0PzzVhrzsKIJo8yGqVk6CmMHiMei9w1C1Bp9IMJSIK+HPIQ==", + "dev": true, "bin": { "parser": "bin/babel-parser.js" }, @@ -949,6 +970,7 @@ "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.18.6.tgz", "integrity": "sha512-6mmljtAedFGTWu2p/8WIORGwy+61PLgOMPOdazc7YoJ9ZCWUyFy3A6CpPkRKLKD1ToAesxX8KGEViAiLo9N+7Q==", + "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.18.6" }, @@ -1888,6 +1910,7 @@ "version": "7.18.10", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.18.10.tgz", "integrity": "sha512-TI+rCtooWHr3QJ27kJxfjutghu44DLnasDMwpDqCXVTal9RLp3RSYNh4NdBrRP2cQAoG9A8juOQl6P6oZG4JxA==", + "dev": true, "dependencies": { "@babel/code-frame": "^7.18.6", "@babel/parser": "^7.18.10", @@ -1901,6 +1924,7 @@ "version": "7.18.11", "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.18.11.tgz", "integrity": "sha512-TG9PiM2R/cWCAy6BPJKeHzNbu4lPzOSZpeMfeNErskGpTJx6trEvFaVCbDvpcxwy49BKWmEPwiW8mrysNiDvIQ==", + "dev": true, "dependencies": { "@babel/code-frame": "^7.18.6", "@babel/generator": "^7.18.10", @@ -1921,6 +1945,7 @@ "version": "7.18.10", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.18.10.tgz", "integrity": "sha512-MJvnbEiiNkpjo+LknnmRrqbY1GPUUggjv+wQVjetM/AONoupqRALB7I6jGqNUAZsKcRIEu2J6FRFvsczljjsaQ==", + "dev": true, "dependencies": { "@babel/helper-string-parser": "^7.18.10", "@babel/helper-validator-identifier": "^7.18.6", @@ -2158,6 +2183,7 @@ "version": "11.10.0", "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.10.0.tgz", "integrity": "sha512-xVnpDAAbtxL1dsuSelU5A7BnY/lftws0wUexNJZTPsvX/1tM4GZJbclgODhvW4E+NH7E5VFcH0bBn30NvniPJA==", + "dev": true, "dependencies": { "@babel/helper-module-imports": "^7.16.7", "@babel/plugin-syntax-jsx": "^7.17.12", @@ -2180,6 +2206,7 @@ "version": "11.10.1", "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.10.1.tgz", "integrity": "sha512-uZTj3Yz5D69GE25iFZcIQtibnVCFsc/6+XIozyL3ycgWvEdif2uEw9wlUt6umjLr4Keg9K6xRPHmD8LGi+6p1A==", + "dev": true, "dependencies": { "@emotion/memoize": "^0.8.0", "@emotion/sheet": "^1.2.0", @@ -2191,7 +2218,8 @@ "node_modules/@emotion/hash": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.0.tgz", - "integrity": "sha512-14FtKiHhy2QoPIzdTcvh//8OyBlknNs2nXRwIhG904opCby3l+9Xaf/wuPvICBF0rc1ZCNBd3nKe9cd2mecVkQ==" + "integrity": "sha512-14FtKiHhy2QoPIzdTcvh//8OyBlknNs2nXRwIhG904opCby3l+9Xaf/wuPvICBF0rc1ZCNBd3nKe9cd2mecVkQ==", + "dev": true }, "node_modules/@emotion/is-prop-valid": { "version": "1.2.0", @@ -2205,12 +2233,14 @@ "node_modules/@emotion/memoize": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.0.tgz", - "integrity": "sha512-G/YwXTkv7Den9mXDO7AhLWkE3q+I92B+VqAE+dYG4NGPaHZGvt3G8Q0p9vmE+sq7rTGphUbAvmQ9YpbfMQGGlA==" + "integrity": "sha512-G/YwXTkv7Den9mXDO7AhLWkE3q+I92B+VqAE+dYG4NGPaHZGvt3G8Q0p9vmE+sq7rTGphUbAvmQ9YpbfMQGGlA==", + "dev": true }, "node_modules/@emotion/react": { "version": "11.10.0", "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.10.0.tgz", "integrity": "sha512-K6z9zlHxxBXwN8TcpwBKcEsBsOw4JWCCmR+BeeOWgqp8GIU1yA2Odd41bwdAAr0ssbQrbJbVnndvv7oiv1bZeQ==", + "dev": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.10.0", @@ -2237,6 +2267,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.1.0.tgz", "integrity": "sha512-F1ZZZW51T/fx+wKbVlwsfchr5q97iW8brAnXmsskz4d0hVB4O3M/SiA3SaeH06x02lSNzkkQv+n3AX3kCXKSFA==", + "dev": true, "dependencies": { "@emotion/hash": "^0.9.0", "@emotion/memoize": "^0.8.0", @@ -2248,7 +2279,8 @@ "node_modules/@emotion/sheet": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.2.0.tgz", - "integrity": "sha512-OiTkRgpxescko+M51tZsMq7Puu/KP55wMT8BgpcXVG2hqXc0Vo0mfymJ/Uj24Hp0i083ji/o0aLddh08UEjq8w==" + "integrity": "sha512-OiTkRgpxescko+M51tZsMq7Puu/KP55wMT8BgpcXVG2hqXc0Vo0mfymJ/Uj24Hp0i083ji/o0aLddh08UEjq8w==", + "dev": true }, "node_modules/@emotion/styled": { "version": "11.10.0", @@ -2279,17 +2311,20 @@ "node_modules/@emotion/unitless": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.0.tgz", - "integrity": "sha512-VINS5vEYAscRl2ZUDiT3uMPlrFQupiKgHz5AA4bCH1miKBg4qtwkim1qPmJj/4WG6TreYMY111rEFsjupcOKHw==" + "integrity": "sha512-VINS5vEYAscRl2ZUDiT3uMPlrFQupiKgHz5AA4bCH1miKBg4qtwkim1qPmJj/4WG6TreYMY111rEFsjupcOKHw==", + "dev": true }, "node_modules/@emotion/utils": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.2.0.tgz", - "integrity": "sha512-sn3WH53Kzpw8oQ5mgMmIzzyAaH2ZqFEbozVVBSYp538E06OSE6ytOp7pRAjNQR+Q/orwqdQYJSe2m3hCOeznkw==" + "integrity": "sha512-sn3WH53Kzpw8oQ5mgMmIzzyAaH2ZqFEbozVVBSYp538E06OSE6ytOp7pRAjNQR+Q/orwqdQYJSe2m3hCOeznkw==", + "dev": true }, "node_modules/@emotion/weak-memoize": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.0.tgz", - "integrity": "sha512-AHPmaAx+RYfZz0eYu6Gviiagpmiyw98ySSlQvCUhVGDRtDFe4DBS0x1bSjdF3gqUDYOczB+yYvBTtEylYSdRhg==" + "integrity": "sha512-AHPmaAx+RYfZz0eYu6Gviiagpmiyw98ySSlQvCUhVGDRtDFe4DBS0x1bSjdF3gqUDYOczB+yYvBTtEylYSdRhg==", + "dev": true }, "node_modules/@esbuild/linux-loong64": { "version": "0.14.54", @@ -2668,6 +2703,7 @@ "version": "0.1.1", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz", "integrity": "sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==", + "dev": true, "dependencies": { "@jridgewell/set-array": "^1.0.0", "@jridgewell/sourcemap-codec": "^1.4.10" @@ -2680,6 +2716,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "dev": true, "engines": { "node": ">=6.0.0" } @@ -2688,6 +2725,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true, "engines": { "node": ">=6.0.0" } @@ -2719,12 +2757,14 @@ "node_modules/@jridgewell/sourcemap-codec": { "version": "1.4.14", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", - "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==" + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", + "dev": true }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.14", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.14.tgz", "integrity": "sha512-bJWEfQ9lPTvm3SneWwRFVLzrh6nhjwqw7TUFFBEMzwvg7t7PCDenf2lDwqo4NQXzdpgBXyFgDWnQA+2vkruksQ==", + "dev": true, "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" @@ -9397,7 +9437,8 @@ "node_modules/@types/parse-json": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==" + "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==", + "dev": true }, "node_modules/@types/parse5": { "version": "5.0.3", @@ -10416,6 +10457,7 @@ "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, "dependencies": { "color-convert": "^1.9.0" }, @@ -11106,6 +11148,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", + "dev": true, "dependencies": { "@babel/runtime": "^7.12.5", "cosmiconfig": "^7.0.0", @@ -11705,6 +11748,7 @@ "version": "4.21.3", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.3.tgz", "integrity": "sha512-898rgRXLAyRkM1GryrrBHGkqA5hlpkV5MhtZwg9QXeiyLUYs2k00Un05aX5l2/yJIOObYKOpS2JNo8nJDE7fWQ==", + "dev": true, "funding": [ { "type": "opencollective", @@ -11967,6 +12011,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, "engines": { "node": ">=6" } @@ -12027,6 +12072,7 @@ "version": "1.0.30001375", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001375.tgz", "integrity": "sha512-kWIMkNzLYxSvnjy0hL8w1NOaWNr2rn39RTAVyIwcw8juu60bZDWiF1/loOYANzjtJmy6qPgNmn38ro5Pygagdw==", + "dev": true, "funding": [ { "type": "opencollective", @@ -12079,6 +12125,7 @@ "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, "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", @@ -12092,6 +12139,7 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, "engines": { "node": ">=0.8.0" } @@ -12509,6 +12557,7 @@ "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, "dependencies": { "color-name": "1.1.3" } @@ -12516,7 +12565,8 @@ "node_modules/color-name": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true }, "node_modules/color-support": { "version": "1.1.3", @@ -12757,6 +12807,7 @@ "version": "1.8.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.8.0.tgz", "integrity": "sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA==", + "dev": true, "dependencies": { "safe-buffer": "~5.1.1" } @@ -12872,6 +12923,7 @@ "version": "7.0.1", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.1.tgz", "integrity": "sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ==", + "dev": true, "dependencies": { "@types/parse-json": "^4.0.0", "import-fresh": "^3.2.1", @@ -13399,7 +13451,8 @@ "node_modules/csstype": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.0.tgz", - "integrity": "sha512-uX1KG+x9h5hIJsaKR9xHUeUraxf8IODOwq9JLNPq6BwB04a/xgpq3rcx47l5BZu5zBPlgD342tdke3Hom/nJRA==" + "integrity": "sha512-uX1KG+x9h5hIJsaKR9xHUeUraxf8IODOwq9JLNPq6BwB04a/xgpq3rcx47l5BZu5zBPlgD342tdke3Hom/nJRA==", + "dev": true }, "node_modules/currently-unhandled": { "version": "0.4.1", @@ -13620,6 +13673,7 @@ "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, "dependencies": { "ms": "2.1.2" }, @@ -14255,7 +14309,8 @@ "node_modules/electron-to-chromium": { "version": "1.4.215", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.215.tgz", - "integrity": "sha512-vqZxT8C5mlDZ//hQFhneHmOLnj1LhbzxV0+I1yqHV8SB1Oo4Y5Ne9+qQhwHl7O1s9s9cRuo2l5CoLEHdhMTwZg==" + "integrity": "sha512-vqZxT8C5mlDZ//hQFhneHmOLnj1LhbzxV0+I1yqHV8SB1Oo4Y5Ne9+qQhwHl7O1s9s9cRuo2l5CoLEHdhMTwZg==", + "dev": true }, "node_modules/elliptic": { "version": "6.5.4", @@ -14392,6 +14447,7 @@ "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, "dependencies": { "is-arrayish": "^0.2.1" } @@ -14894,6 +14950,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true, "engines": { "node": ">=6" } @@ -14908,6 +14965,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, "engines": { "node": ">=10" }, @@ -16469,7 +16527,8 @@ "node_modules/find-root": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", - "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==" + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", + "dev": true }, "node_modules/find-up": { "version": "5.0.0", @@ -16983,7 +17042,8 @@ "node_modules/function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true }, "node_modules/function.prototype.name": { "version": "1.1.5", @@ -17042,6 +17102,7 @@ "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, "engines": { "node": ">=6.9.0" } @@ -17242,6 +17303,7 @@ "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, "engines": { "node": ">=4" } @@ -17354,6 +17416,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, "dependencies": { "function-bind": "^1.1.1" }, @@ -17374,6 +17437,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, "engines": { "node": ">=4" } @@ -17701,6 +17765,7 @@ "version": "3.3.2", "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "dev": true, "dependencies": { "react-is": "^16.7.0" } @@ -18029,6 +18094,7 @@ "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" @@ -18044,6 +18110,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, "engines": { "node": ">=4" } @@ -18382,7 +18449,8 @@ "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true }, "node_modules/is-bigint": { "version": "1.0.4", @@ -18475,6 +18543,7 @@ "version": "2.10.0", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.10.0.tgz", "integrity": "sha512-Erxj2n/LDAZ7H8WNJXd9tw38GYM3dv8rk8Zcs+jJuxYTW7sozH+SS8NtrSjVL1/vpLvWi1hxy96IzjJ3EHTJJg==", + "dev": true, "dependencies": { "has": "^1.0.3" }, @@ -19450,6 +19519,7 @@ "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true, "bin": { "jsesc": "bin/jsesc" }, @@ -19466,7 +19536,8 @@ "node_modules/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", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true }, "node_modules/json-schema": { "version": "0.4.0", @@ -19496,6 +19567,7 @@ "version": "2.2.1", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==", + "dev": true, "bin": { "json5": "lib/cli.js" }, @@ -19646,7 +19718,8 @@ "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true }, "node_modules/listr2": { "version": "3.14.0", @@ -20717,7 +20790,8 @@ "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true }, "node_modules/msw": { "version": "0.43.1", @@ -21108,7 +21182,8 @@ "node_modules/node-releases": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.6.tgz", - "integrity": "sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==" + "integrity": "sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==", + "dev": true }, "node_modules/normalize-package-data": { "version": "2.5.0", @@ -21901,6 +21976,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, "dependencies": { "callsites": "^3.0.0" }, @@ -21943,6 +22019,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", @@ -22037,7 +22114,8 @@ "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true }, "node_modules/path-to-regexp": { "version": "0.1.7", @@ -22049,6 +22127,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, "engines": { "node": ">=8" } @@ -22812,7 +22891,8 @@ "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true }, "node_modules/react-merge-refs": { "version": "1.1.0", @@ -23468,6 +23548,7 @@ "version": "1.22.1", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", + "dev": true, "dependencies": { "is-core-module": "^2.9.0", "path-parse": "^1.0.7", @@ -23645,7 +23726,8 @@ "node_modules/safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true }, "node_modules/safe-regex": { "version": "1.1.0", @@ -24010,6 +24092,7 @@ "version": "6.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true, "bin": { "semver": "bin/semver.js" } @@ -24598,6 +24681,7 @@ "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "dev": true, "engines": { "node": ">=0.10.0" } @@ -25354,12 +25438,14 @@ "node_modules/stylis": { "version": "4.0.13", "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.0.13.tgz", - "integrity": "sha512-xGPXiFVl4YED9Jh7Euv2V220mriG9u4B2TA6Ybjc1catrstKD2PpIdU3U0RKpkVBC2EhmL/F0sPCr9vrFTNRag==" + "integrity": "sha512-xGPXiFVl4YED9Jh7Euv2V220mriG9u4B2TA6Ybjc1catrstKD2PpIdU3U0RKpkVBC2EhmL/F0sPCr9vrFTNRag==", + "dev": true }, "node_modules/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, "dependencies": { "has-flag": "^3.0.0" }, @@ -25371,6 +25457,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, "engines": { "node": ">= 0.4" }, @@ -25790,6 +25877,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true, "engines": { "node": ">=4" } @@ -26439,6 +26527,7 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.5.tgz", "integrity": "sha512-dteFFpCyvuDdr9S/ff1ISkKt/9YZxKjI9WlRR99c180GaztJtRa/fn18FdxGVKVsnPY7/a/FDN68mcvUmP4U7Q==", + "dev": true, "funding": [ { "type": "opencollective", @@ -26463,7 +26552,8 @@ "node_modules/update-browserslist-db/node_modules/picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true }, "node_modules/uri-js": { "version": "4.4.1", @@ -27838,6 +27928,7 @@ "version": "1.10.2", "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true, "engines": { "node": ">= 6" } @@ -27916,6 +28007,7 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", "integrity": "sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==", + "dev": true, "requires": { "@jridgewell/gen-mapping": "^0.1.0", "@jridgewell/trace-mapping": "^0.3.9" @@ -27925,6 +28017,7 @@ "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz", "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==", + "dev": true, "requires": { "@babel/highlight": "^7.18.6" } @@ -27932,12 +28025,14 @@ "@babel/compat-data": { "version": "7.18.8", "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.18.8.tgz", - "integrity": "sha512-HSmX4WZPPK3FUxYp7g2T6EyO8j96HlZJlxmKPSh6KAcqwyDrfx7hKjXpAW/0FhFfTJsR0Yt4lAjLI2coMptIHQ==" + "integrity": "sha512-HSmX4WZPPK3FUxYp7g2T6EyO8j96HlZJlxmKPSh6KAcqwyDrfx7hKjXpAW/0FhFfTJsR0Yt4lAjLI2coMptIHQ==", + "dev": true }, "@babel/core": { "version": "7.18.10", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.18.10.tgz", "integrity": "sha512-JQM6k6ENcBFKVtWvLavlvi/mPcpYZ3+R+2EySDEMSMbp7Mn4FexlbbJVrx2R7Ijhr01T8gyqrOaABWIOgxeUyw==", + "dev": true, "requires": { "@ampproject/remapping": "^2.1.0", "@babel/code-frame": "^7.18.6", @@ -27960,6 +28055,7 @@ "version": "7.18.12", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.18.12.tgz", "integrity": "sha512-dfQ8ebCN98SvyL7IxNMCUtZQSq5R7kxgN+r8qYTGDmmSion1hX2C0zq2yo1bsCDhXixokv1SAWTZUMYbO/V5zg==", + "dev": true, "requires": { "@babel/types": "^7.18.10", "@jridgewell/gen-mapping": "^0.3.2", @@ -27970,6 +28066,7 @@ "version": "0.3.2", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", + "dev": true, "requires": { "@jridgewell/set-array": "^1.0.1", "@jridgewell/sourcemap-codec": "^1.4.10", @@ -28001,6 +28098,7 @@ "version": "7.18.9", "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.18.9.tgz", "integrity": "sha512-tzLCyVmqUiFlcFoAPLA/gL9TeYrF61VLNtb+hvkuVaB5SUjW7jcfrglBIX1vUIoT7CLP3bBlIMeyEsIl2eFQNg==", + "dev": true, "requires": { "@babel/compat-data": "^7.18.8", "@babel/helper-validator-option": "^7.18.6", @@ -28050,7 +28148,8 @@ "@babel/helper-environment-visitor": { "version": "7.18.9", "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz", - "integrity": "sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==" + "integrity": "sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==", + "dev": true }, "@babel/helper-explode-assignable-expression": { "version": "7.18.6", @@ -28065,6 +28164,7 @@ "version": "7.18.9", "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.18.9.tgz", "integrity": "sha512-fJgWlZt7nxGksJS9a0XdSaI4XvpExnNIgRP+rVefWh5U7BL8pPuir6SJUmFKRfjWQ51OtWSzwOxhaH/EBWWc0A==", + "dev": true, "requires": { "@babel/template": "^7.18.6", "@babel/types": "^7.18.9" @@ -28074,6 +28174,7 @@ "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz", "integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==", + "dev": true, "requires": { "@babel/types": "^7.18.6" } @@ -28091,6 +28192,7 @@ "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz", "integrity": "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==", + "dev": true, "requires": { "@babel/types": "^7.18.6" } @@ -28099,6 +28201,7 @@ "version": "7.18.9", "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.18.9.tgz", "integrity": "sha512-KYNqY0ICwfv19b31XzvmI/mfcylOzbLtowkw+mfvGPAQ3kfCnMLYbED3YecL5tPd8nAYFQFAd6JHp2LxZk/J1g==", + "dev": true, "requires": { "@babel/helper-environment-visitor": "^7.18.9", "@babel/helper-module-imports": "^7.18.6", @@ -28122,7 +28225,8 @@ "@babel/helper-plugin-utils": { "version": "7.18.9", "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.18.9.tgz", - "integrity": "sha512-aBXPT3bmtLryXaoJLyYPXPlSD4p1ld9aYeR+sJNOZjJJGiOpb+fKfh3NkcCu7J54nUJwCERPBExCCpyCOHnu/w==" + "integrity": "sha512-aBXPT3bmtLryXaoJLyYPXPlSD4p1ld9aYeR+sJNOZjJJGiOpb+fKfh3NkcCu7J54nUJwCERPBExCCpyCOHnu/w==", + "dev": true }, "@babel/helper-remap-async-to-generator": { "version": "7.18.9", @@ -28153,6 +28257,7 @@ "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.18.6.tgz", "integrity": "sha512-iNpIgTgyAvDQpDj76POqg+YEt8fPxx3yaNBg3S30dxNKm2SWfYhD0TGrK/Eu9wHpUW63VQU894TsTg+GLbUa1g==", + "dev": true, "requires": { "@babel/types": "^7.18.6" } @@ -28170,6 +28275,7 @@ "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz", "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==", + "dev": true, "requires": { "@babel/types": "^7.18.6" } @@ -28177,17 +28283,20 @@ "@babel/helper-string-parser": { "version": "7.18.10", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.18.10.tgz", - "integrity": "sha512-XtIfWmeNY3i4t7t4D2t02q50HvqHybPqW2ki1kosnvWCwuCMeo81Jf0gwr85jy/neUdg5XDdeFE/80DXiO+njw==" + "integrity": "sha512-XtIfWmeNY3i4t7t4D2t02q50HvqHybPqW2ki1kosnvWCwuCMeo81Jf0gwr85jy/neUdg5XDdeFE/80DXiO+njw==", + "dev": true }, "@babel/helper-validator-identifier": { "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.18.6.tgz", - "integrity": "sha512-MmetCkz9ej86nJQV+sFCxoGGrUbU3q02kgLciwkrt9QqEB7cP39oKEY0PakknEO0Gu20SskMRi+AYZ3b1TpN9g==" + "integrity": "sha512-MmetCkz9ej86nJQV+sFCxoGGrUbU3q02kgLciwkrt9QqEB7cP39oKEY0PakknEO0Gu20SskMRi+AYZ3b1TpN9g==", + "dev": true }, "@babel/helper-validator-option": { "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz", - "integrity": "sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw==" + "integrity": "sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw==", + "dev": true }, "@babel/helper-wrap-function": { "version": "7.18.11", @@ -28205,6 +28314,7 @@ "version": "7.18.9", "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.18.9.tgz", "integrity": "sha512-Jf5a+rbrLoR4eNdUmnFu8cN5eNJT6qdTdOg5IHIzq87WwyRw9PwguLFOWYgktN/60IP4fgDUawJvs7PjQIzELQ==", + "dev": true, "requires": { "@babel/template": "^7.18.6", "@babel/traverse": "^7.18.9", @@ -28215,6 +28325,7 @@ "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", + "dev": true, "requires": { "@babel/helper-validator-identifier": "^7.18.6", "chalk": "^2.0.0", @@ -28224,7 +28335,8 @@ "@babel/parser": { "version": "7.18.11", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.18.11.tgz", - "integrity": "sha512-9JKn5vN+hDt0Hdqn1PiJ2guflwP+B6Ga8qbDuoF0PzzVhrzsKIJo8yGqVk6CmMHiMei9w1C1Bp9IMJSIK+HPIQ==" + "integrity": "sha512-9JKn5vN+hDt0Hdqn1PiJ2guflwP+B6Ga8qbDuoF0PzzVhrzsKIJo8yGqVk6CmMHiMei9w1C1Bp9IMJSIK+HPIQ==", + "dev": true }, "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { "version": "7.18.6", @@ -28522,6 +28634,7 @@ "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.18.6.tgz", "integrity": "sha512-6mmljtAedFGTWu2p/8WIORGwy+61PLgOMPOdazc7YoJ9ZCWUyFy3A6CpPkRKLKD1ToAesxX8KGEViAiLo9N+7Q==", + "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.18.6" } @@ -29152,6 +29265,7 @@ "version": "7.18.10", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.18.10.tgz", "integrity": "sha512-TI+rCtooWHr3QJ27kJxfjutghu44DLnasDMwpDqCXVTal9RLp3RSYNh4NdBrRP2cQAoG9A8juOQl6P6oZG4JxA==", + "dev": true, "requires": { "@babel/code-frame": "^7.18.6", "@babel/parser": "^7.18.10", @@ -29162,6 +29276,7 @@ "version": "7.18.11", "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.18.11.tgz", "integrity": "sha512-TG9PiM2R/cWCAy6BPJKeHzNbu4lPzOSZpeMfeNErskGpTJx6trEvFaVCbDvpcxwy49BKWmEPwiW8mrysNiDvIQ==", + "dev": true, "requires": { "@babel/code-frame": "^7.18.6", "@babel/generator": "^7.18.10", @@ -29179,6 +29294,7 @@ "version": "7.18.10", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.18.10.tgz", "integrity": "sha512-MJvnbEiiNkpjo+LknnmRrqbY1GPUUggjv+wQVjetM/AONoupqRALB7I6jGqNUAZsKcRIEu2J6FRFvsczljjsaQ==", + "dev": true, "requires": { "@babel/helper-string-parser": "^7.18.10", "@babel/helper-validator-identifier": "^7.18.6", @@ -29390,6 +29506,7 @@ "version": "11.10.0", "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.10.0.tgz", "integrity": "sha512-xVnpDAAbtxL1dsuSelU5A7BnY/lftws0wUexNJZTPsvX/1tM4GZJbclgODhvW4E+NH7E5VFcH0bBn30NvniPJA==", + "dev": true, "requires": { "@babel/helper-module-imports": "^7.16.7", "@babel/plugin-syntax-jsx": "^7.17.12", @@ -29409,6 +29526,7 @@ "version": "11.10.1", "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.10.1.tgz", "integrity": "sha512-uZTj3Yz5D69GE25iFZcIQtibnVCFsc/6+XIozyL3ycgWvEdif2uEw9wlUt6umjLr4Keg9K6xRPHmD8LGi+6p1A==", + "dev": true, "requires": { "@emotion/memoize": "^0.8.0", "@emotion/sheet": "^1.2.0", @@ -29420,7 +29538,8 @@ "@emotion/hash": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.0.tgz", - "integrity": "sha512-14FtKiHhy2QoPIzdTcvh//8OyBlknNs2nXRwIhG904opCby3l+9Xaf/wuPvICBF0rc1ZCNBd3nKe9cd2mecVkQ==" + "integrity": "sha512-14FtKiHhy2QoPIzdTcvh//8OyBlknNs2nXRwIhG904opCby3l+9Xaf/wuPvICBF0rc1ZCNBd3nKe9cd2mecVkQ==", + "dev": true }, "@emotion/is-prop-valid": { "version": "1.2.0", @@ -29434,12 +29553,14 @@ "@emotion/memoize": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.0.tgz", - "integrity": "sha512-G/YwXTkv7Den9mXDO7AhLWkE3q+I92B+VqAE+dYG4NGPaHZGvt3G8Q0p9vmE+sq7rTGphUbAvmQ9YpbfMQGGlA==" + "integrity": "sha512-G/YwXTkv7Den9mXDO7AhLWkE3q+I92B+VqAE+dYG4NGPaHZGvt3G8Q0p9vmE+sq7rTGphUbAvmQ9YpbfMQGGlA==", + "dev": true }, "@emotion/react": { "version": "11.10.0", "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.10.0.tgz", "integrity": "sha512-K6z9zlHxxBXwN8TcpwBKcEsBsOw4JWCCmR+BeeOWgqp8GIU1yA2Odd41bwdAAr0ssbQrbJbVnndvv7oiv1bZeQ==", + "dev": true, "requires": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.10.0", @@ -29454,6 +29575,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.1.0.tgz", "integrity": "sha512-F1ZZZW51T/fx+wKbVlwsfchr5q97iW8brAnXmsskz4d0hVB4O3M/SiA3SaeH06x02lSNzkkQv+n3AX3kCXKSFA==", + "dev": true, "requires": { "@emotion/hash": "^0.9.0", "@emotion/memoize": "^0.8.0", @@ -29465,7 +29587,8 @@ "@emotion/sheet": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.2.0.tgz", - "integrity": "sha512-OiTkRgpxescko+M51tZsMq7Puu/KP55wMT8BgpcXVG2hqXc0Vo0mfymJ/Uj24Hp0i083ji/o0aLddh08UEjq8w==" + "integrity": "sha512-OiTkRgpxescko+M51tZsMq7Puu/KP55wMT8BgpcXVG2hqXc0Vo0mfymJ/Uj24Hp0i083ji/o0aLddh08UEjq8w==", + "dev": true }, "@emotion/styled": { "version": "11.10.0", @@ -29483,17 +29606,20 @@ "@emotion/unitless": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.0.tgz", - "integrity": "sha512-VINS5vEYAscRl2ZUDiT3uMPlrFQupiKgHz5AA4bCH1miKBg4qtwkim1qPmJj/4WG6TreYMY111rEFsjupcOKHw==" + "integrity": "sha512-VINS5vEYAscRl2ZUDiT3uMPlrFQupiKgHz5AA4bCH1miKBg4qtwkim1qPmJj/4WG6TreYMY111rEFsjupcOKHw==", + "dev": true }, "@emotion/utils": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.2.0.tgz", - "integrity": "sha512-sn3WH53Kzpw8oQ5mgMmIzzyAaH2ZqFEbozVVBSYp538E06OSE6ytOp7pRAjNQR+Q/orwqdQYJSe2m3hCOeznkw==" + "integrity": "sha512-sn3WH53Kzpw8oQ5mgMmIzzyAaH2ZqFEbozVVBSYp538E06OSE6ytOp7pRAjNQR+Q/orwqdQYJSe2m3hCOeznkw==", + "dev": true }, "@emotion/weak-memoize": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.0.tgz", - "integrity": "sha512-AHPmaAx+RYfZz0eYu6Gviiagpmiyw98ySSlQvCUhVGDRtDFe4DBS0x1bSjdF3gqUDYOczB+yYvBTtEylYSdRhg==" + "integrity": "sha512-AHPmaAx+RYfZz0eYu6Gviiagpmiyw98ySSlQvCUhVGDRtDFe4DBS0x1bSjdF3gqUDYOczB+yYvBTtEylYSdRhg==", + "dev": true }, "@esbuild/linux-loong64": { "version": "0.14.54", @@ -29780,6 +29906,7 @@ "version": "0.1.1", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz", "integrity": "sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==", + "dev": true, "requires": { "@jridgewell/set-array": "^1.0.0", "@jridgewell/sourcemap-codec": "^1.4.10" @@ -29788,12 +29915,14 @@ "@jridgewell/resolve-uri": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", - "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==" + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "dev": true }, "@jridgewell/set-array": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", - "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==" + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true }, "@jridgewell/source-map": { "version": "0.3.2", @@ -29821,12 +29950,14 @@ "@jridgewell/sourcemap-codec": { "version": "1.4.14", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", - "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==" + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", + "dev": true }, "@jridgewell/trace-mapping": { "version": "0.3.14", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.14.tgz", "integrity": "sha512-bJWEfQ9lPTvm3SneWwRFVLzrh6nhjwqw7TUFFBEMzwvg7t7PCDenf2lDwqo4NQXzdpgBXyFgDWnQA+2vkruksQ==", + "dev": true, "requires": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" @@ -30196,8 +30327,7 @@ "version": "1.6.22", "resolved": "https://registry.npmjs.org/@mdx-js/react/-/react-1.6.22.tgz", "integrity": "sha512-TDoPum4SHdfPiGSAaRBw7ECyI8VaHpK8GJugbJIJuqyh6kzw9ZLJZW3HGL3NNrJGxcAixUvqROm+YuQOo5eXtg==", - "dev": true, - "requires": {} + "dev": true } } }, @@ -31057,8 +31187,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/webpack-filter-warnings-plugin/-/webpack-filter-warnings-plugin-1.2.1.tgz", "integrity": "sha512-Ez6ytc9IseDMLPo0qCuNNYzgtUl8NovOqjIq4uAU8LTD4uoa1w1KpZyyzFtLTEMZpkkOkLfL9eN+KGYdk1Qtwg==", - "dev": true, - "requires": {} + "dev": true }, "webpack-sources": { "version": "1.4.3", @@ -31251,8 +31380,7 @@ "version": "5.1.0", "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", - "dev": true, - "requires": {} + "dev": true }, "jest-worker": { "version": "27.5.1", @@ -31297,8 +31425,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz", "integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==", - "dev": true, - "requires": {} + "dev": true }, "postcss-modules-local-by-default": { "version": "4.0.0", @@ -33865,8 +33992,7 @@ "version": "5.1.0", "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", - "dev": true, - "requires": {} + "dev": true }, "jest-worker": { "version": "27.5.1", @@ -33911,8 +34037,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz", "integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==", - "dev": true, - "requires": {} + "dev": true }, "postcss-modules-local-by-default": { "version": "4.0.0", @@ -34959,7 +35084,8 @@ "@types/parse-json": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==" + "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==", + "dev": true }, "@types/parse5": { "version": "5.0.3", @@ -35588,8 +35714,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-1.2.0.tgz", "integrity": "sha512-4FB8Tj6xyVkyqjj1OaTqCjXYULB9FMkqQ8yGrZjRDrYh0nOE+7Lhs45WioWQQMV+ceFlE368Ukhe6xdvJM9Egg==", - "dev": true, - "requires": {} + "dev": true }, "@webpack-cli/info": { "version": "1.5.0", @@ -35604,8 +35729,7 @@ "version": "1.7.0", "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-1.7.0.tgz", "integrity": "sha512-oxnCNGj88fL+xzV+dacXs44HcDwf1ovs3AuEzvP7mqXw7fQntqIhQ1BRmynh4qEKQSSSRSWVyXRjmTbZIX9V2Q==", - "dev": true, - "requires": {} + "dev": true }, "@xmldom/xmldom": { "version": "0.7.5", @@ -35645,8 +35769,7 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "requires": {} + "dev": true }, "acorn-walk": { "version": "7.2.0", @@ -35711,8 +35834,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-1.0.1.tgz", "integrity": "sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ==", - "dev": true, - "requires": {} + "dev": true }, "ajv-formats": { "version": "2.1.1", @@ -35747,8 +35869,7 @@ "version": "3.5.2", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "dev": true, - "requires": {} + "dev": true }, "ansi-align": { "version": "3.0.1", @@ -35798,6 +35919,7 @@ "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, "requires": { "color-convert": "^1.9.0" } @@ -36319,6 +36441,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", + "dev": true, "requires": { "@babel/runtime": "^7.12.5", "cosmiconfig": "^7.0.0", @@ -36808,6 +36931,7 @@ "version": "4.21.3", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.3.tgz", "integrity": "sha512-898rgRXLAyRkM1GryrrBHGkqA5hlpkV5MhtZwg9QXeiyLUYs2k00Un05aX5l2/yJIOObYKOpS2JNo8nJDE7fWQ==", + "dev": true, "requires": { "caniuse-lite": "^1.0.30001370", "electron-to-chromium": "^1.4.202", @@ -36998,7 +37122,8 @@ "callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==" + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true }, "camel-case": { "version": "4.1.2", @@ -37045,7 +37170,8 @@ "caniuse-lite": { "version": "1.0.30001375", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001375.tgz", - "integrity": "sha512-kWIMkNzLYxSvnjy0hL8w1NOaWNr2rn39RTAVyIwcw8juu60bZDWiF1/loOYANzjtJmy6qPgNmn38ro5Pygagdw==" + "integrity": "sha512-kWIMkNzLYxSvnjy0hL8w1NOaWNr2rn39RTAVyIwcw8juu60bZDWiF1/loOYANzjtJmy6qPgNmn38ro5Pygagdw==", + "dev": true }, "capture-exit": { "version": "2.0.0", @@ -37078,6 +37204,7 @@ "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", @@ -37087,7 +37214,8 @@ "escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==" + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true } } }, @@ -37395,6 +37523,7 @@ "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, "requires": { "color-name": "1.1.3" } @@ -37402,7 +37531,8 @@ "color-name": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true }, "color-support": { "version": "1.1.3", @@ -37601,6 +37731,7 @@ "version": "1.8.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.8.0.tgz", "integrity": "sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA==", + "dev": true, "requires": { "safe-buffer": "~5.1.1" } @@ -37694,6 +37825,7 @@ "version": "7.0.1", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.1.tgz", "integrity": "sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ==", + "dev": true, "requires": { "@types/parse-json": "^4.0.0", "import-fresh": "^3.2.1", @@ -38120,7 +38252,8 @@ "csstype": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.0.tgz", - "integrity": "sha512-uX1KG+x9h5hIJsaKR9xHUeUraxf8IODOwq9JLNPq6BwB04a/xgpq3rcx47l5BZu5zBPlgD342tdke3Hom/nJRA==" + "integrity": "sha512-uX1KG+x9h5hIJsaKR9xHUeUraxf8IODOwq9JLNPq6BwB04a/xgpq3rcx47l5BZu5zBPlgD342tdke3Hom/nJRA==", + "dev": true }, "currently-unhandled": { "version": "0.4.1", @@ -38296,6 +38429,7 @@ "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, "requires": { "ms": "2.1.2" } @@ -38803,7 +38937,8 @@ "electron-to-chromium": { "version": "1.4.215", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.215.tgz", - "integrity": "sha512-vqZxT8C5mlDZ//hQFhneHmOLnj1LhbzxV0+I1yqHV8SB1Oo4Y5Ne9+qQhwHl7O1s9s9cRuo2l5CoLEHdhMTwZg==" + "integrity": "sha512-vqZxT8C5mlDZ//hQFhneHmOLnj1LhbzxV0+I1yqHV8SB1Oo4Y5Ne9+qQhwHl7O1s9s9cRuo2l5CoLEHdhMTwZg==", + "dev": true }, "elliptic": { "version": "6.5.4", @@ -38843,8 +38978,7 @@ "emotion-reset": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/emotion-reset/-/emotion-reset-3.0.1.tgz", - "integrity": "sha512-v6scW83qSu+wtxg7lX1s0+/2U4EAAGFxDQMkvXE10jhKtyuXCzy3/su5/MU9ZjXeNv6ZjxZH51WktrKosKUy9g==", - "requires": {} + "integrity": "sha512-v6scW83qSu+wtxg7lX1s0+/2U4EAAGFxDQMkvXE10jhKtyuXCzy3/su5/MU9ZjXeNv6ZjxZH51WktrKosKUy9g==" }, "encodeurl": { "version": "1.0.2", @@ -38916,6 +39050,7 @@ "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, "requires": { "is-arrayish": "^0.2.1" } @@ -39206,7 +39341,8 @@ "escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==" + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true }, "escape-html": { "version": "1.0.3", @@ -39217,7 +39353,8 @@ "escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==" + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true }, "escodegen": { "version": "2.0.0", @@ -39391,8 +39528,7 @@ "version": "8.5.0", "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.5.0.tgz", "integrity": "sha512-obmWKLUNCnhtQRKc+tmnYuQl0pFU1ibYJQ5BGhTVB08bHe9wC8qUeG7c08dj9XX+AuPj1YSGSQIHl1pnDHZR0Q==", - "dev": true, - "requires": {} + "dev": true }, "eslint-import-resolver-node": { "version": "0.3.6", @@ -39658,8 +39794,7 @@ "version": "4.6.0", "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz", "integrity": "sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==", - "dev": true, - "requires": {} + "dev": true }, "eslint-plugin-storybook": { "version": "0.5.13", @@ -40435,7 +40570,8 @@ "find-root": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", - "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==" + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", + "dev": true }, "find-up": { "version": "5.0.0", @@ -40864,7 +41000,8 @@ "function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true }, "function.prototype.name": { "version": "1.1.5", @@ -40910,7 +41047,8 @@ "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==" + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true }, "get-caller-file": { "version": "2.0.5", @@ -41056,7 +41194,8 @@ "globals": { "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==" + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true }, "globalthis": { "version": "1.0.3", @@ -41142,6 +41281,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, "requires": { "function-bind": "^1.1.1" } @@ -41155,7 +41295,8 @@ "has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==" + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true }, "has-glob": { "version": "1.0.0", @@ -41408,6 +41549,7 @@ "version": "3.3.2", "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "dev": true, "requires": { "react-is": "^16.7.0" } @@ -41654,6 +41796,7 @@ "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, "requires": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" @@ -41662,7 +41805,8 @@ "resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==" + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true } } }, @@ -41915,7 +42059,8 @@ "is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true }, "is-bigint": { "version": "1.0.4", @@ -41970,6 +42115,7 @@ "version": "2.10.0", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.10.0.tgz", "integrity": "sha512-Erxj2n/LDAZ7H8WNJXd9tw38GYM3dv8rk8Zcs+jJuxYTW7sozH+SS8NtrSjVL1/vpLvWi1hxy96IzjJ3EHTJJg==", + "dev": true, "requires": { "has": "^1.0.3" } @@ -42684,7 +42830,8 @@ "jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==" + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true }, "json-parse-better-errors": { "version": "1.0.2", @@ -42695,7 +42842,8 @@ "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", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true }, "json-schema": { "version": "0.4.0", @@ -42724,7 +42872,8 @@ "json5": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", - "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==" + "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==", + "dev": true }, "jsonfile": { "version": "6.1.0", @@ -42837,7 +42986,8 @@ "lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true }, "listr2": { "version": "3.14.0", @@ -43681,7 +43831,8 @@ "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true }, "msw": { "version": "0.43.1", @@ -43995,7 +44146,8 @@ "node-releases": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.6.tgz", - "integrity": "sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==" + "integrity": "sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==", + "dev": true }, "normalize-package-data": { "version": "2.5.0", @@ -44612,6 +44764,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, "requires": { "callsites": "^3.0.0" } @@ -44647,6 +44800,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, "requires": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", @@ -44720,7 +44874,8 @@ "path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true }, "path-to-regexp": { "version": "0.1.7", @@ -44731,7 +44886,8 @@ "path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==" + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true }, "pbkdf2": { "version": "3.1.2", @@ -45294,8 +45450,7 @@ "version": "2.2.2", "resolved": "https://registry.npmjs.org/react-docgen-typescript/-/react-docgen-typescript-2.2.2.tgz", "integrity": "sha512-tvg2ZtOpOi6QDwsb3GZhOjDkkX0h8Z2gipvTg6OVMUyoYoURhEiRNePT8NZItTVCDh39JJHnLdfCOkzoLbFnTg==", - "dev": true, - "requires": {} + "dev": true }, "react-dom": { "version": "18.2.0", @@ -45309,7 +45464,8 @@ "react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true }, "react-merge-refs": { "version": "1.1.0", @@ -45807,6 +45963,7 @@ "version": "1.22.1", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", + "dev": true, "requires": { "is-core-module": "^2.9.0", "path-parse": "^1.0.7", @@ -45937,7 +46094,8 @@ "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true }, "safe-regex": { "version": "1.1.0", @@ -46232,7 +46390,8 @@ "semver": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true }, "send": { "version": "0.18.0", @@ -46737,7 +46896,8 @@ "source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==" + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "dev": true }, "source-map-js": { "version": "1.0.2", @@ -47347,12 +47507,14 @@ "stylis": { "version": "4.0.13", "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.0.13.tgz", - "integrity": "sha512-xGPXiFVl4YED9Jh7Euv2V220mriG9u4B2TA6Ybjc1catrstKD2PpIdU3U0RKpkVBC2EhmL/F0sPCr9vrFTNRag==" + "integrity": "sha512-xGPXiFVl4YED9Jh7Euv2V220mriG9u4B2TA6Ybjc1catrstKD2PpIdU3U0RKpkVBC2EhmL/F0sPCr9vrFTNRag==", + "dev": true }, "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" } @@ -47360,7 +47522,8 @@ "supports-preserve-symlinks-flag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==" + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true }, "symbol.prototype.description": { "version": "1.0.5", @@ -47694,7 +47857,8 @@ "to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==" + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true }, "to-object-path": { "version": "0.3.0", @@ -48186,6 +48350,7 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.5.tgz", "integrity": "sha512-dteFFpCyvuDdr9S/ff1ISkKt/9YZxKjI9WlRR99c180GaztJtRa/fn18FdxGVKVsnPY7/a/FDN68mcvUmP4U7Q==", + "dev": true, "requires": { "escalade": "^3.1.1", "picocolors": "^1.0.0" @@ -48194,7 +48359,8 @@ "picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true } } }, @@ -48757,8 +48923,7 @@ "version": "1.8.0", "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz", "integrity": "sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==", - "dev": true, - "requires": {} + "dev": true }, "has-flag": { "version": "4.0.0", @@ -49225,8 +49390,7 @@ "version": "8.8.1", "resolved": "https://registry.npmjs.org/ws/-/ws-8.8.1.tgz", "integrity": "sha512-bGy2JzvzkPowEJV++hF07hAD6niYSr0JzBNo/J29WsB57A2r7Wlc1UFcTR9IzrPvuNVO4B8LGqF8qcpsVOhJCA==", - "dev": true, - "requires": {} + "dev": true }, "x-default-browser": { "version": "0.4.0", @@ -49258,7 +49422,8 @@ "yaml": { "version": "1.10.2", "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==" + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true }, "yargs": { "version": "17.5.1", diff --git a/frontend/public/index.html b/frontend/public/index.html index 2e9ffa56a..84983cc27 100644 --- a/frontend/public/index.html +++ b/frontend/public/index.html @@ -1,13 +1,13 @@ <!DOCTYPE html> <html lang="ko"> -<head> - <meta charset="UTF-8"> - <meta http-equiv="X-UA-Compatible" content="IE=edge"> - <meta name="viewport" content="width=device-width, initial-scale=1.0"> - <link rel="icon" type="image/png" href="favicon.png"> - <title>MOAMOA - - -
- + + + + + 모아모아 + + +
+ + diff --git a/frontend/public/open-graph-image.png b/frontend/public/open-graph-image.png new file mode 100644 index 0000000000000000000000000000000000000000..af6d41dd695b1c4adf43f9283a262ab49f0ba7a0 GIT binary patch literal 31293 zcmeFZXE{FuJHSj54~>MG`f` z=v{)*6TN@?xbNrt`~H8g_lIjP=j^@q+H0@A*E+9twC*x7a5F#<#B~4OZCwbWE`T6P zZ@OdPiMDcpIryP>zh~kBL5yth9|aVja0)!6@X)<`3(D{KhYbFqcDSi|6M_ogG49#W zK#(8t{_UIkz7&i5^ic`xfdYF6CE}tvJi(#R>HAHLbZ->ciZ6H^e=E_^)IV-6{#t;` zFXEGjYp7&lVntHOKVSd8o+){9iC4~ZwtxhbB0bB(Yl+wOs~t4;3H;4;1-$PI zptJNnrluovy-(*iJ(bcf;Pnmq{_2xvJhUoI)6C{_^RreCTixOc+#VjLQUeqrnrjji z;72)_9&8YTE=y5x!oOjc>L|faN*A>;{4aYv#0Y*+x7e)V(K-l95%@8KCJO8eQcnGU zlm8#V{*O$esRcn_G|{D*2L}Yl?ys(nGabRimh>|4C~|-G=k69EF{4xXuu@ILRO)c0 zg%BvIXJysdzuYN58T4mz8@D$)>38=TFQi-+{~!N$)t~!3SmB{aa3;n{C2az12FIr=3 z4KQb+E*%)c(qA=jd&rRd43{y!53?qfisp zhZceD&Gz;bkk?rN-H9`m6vDjsh21=`Q$zTzU~W;q#X&E&odrrUgrPfan_~`xc9oo1 zznt@nnE26&R5sv*bgls%q*I7s)^~08&G|tITkU?O9WclD%p>+9%d6jx-e$fx@-%Ym<7Wg23 z0RZmmbK&q`4UZza!{>2V1sI`k;D}RN>GLlaAq1-@R?o0Jq)?6pgjS9{Y5r4-+63wv z0h{i6L~VqZyvYf93IO1q^z|Hx4*@@sGjT!v1wPR1NwBL~ceJ#tdhKo@jy`|(UY{7! z*>roG`C+sPq&#yH0DL*OJch0*pL>hni3woyl!Kv3!_ZO?9M$VFwXL^AW;t0c^R~b) z!#S2UAY}(uFpc)LbUlLWP3g^9L)M2QYovEyFu01#6j0(#z$m=@l$g$EJ7{nieD~`v zyM{qs*I{(04RJLxfqxi%5L}^lv-9)!`z(+KrWJqp=^=y_z!jnQQ*4W@jTFwaDwCiP z;50Da963zZx1&%-!>FPQEm)j<8fTQ=8)D0a*4NtsGP^9`1&_oR-ml`h1vLke+L^6l zN=g*aB8-QtGKr&S43Jb!2D^uH0t--qRW8dK7L?KSQX7owlsn==^89kqyF# z_vNLQhlAQ*4)>5FpCHtvZZHi*!~Sw^ld#z;ef(C4M!dpB&1JHtp2OM~a>h{T>rcdn z;vHgHpGfBt1QP+EW=B1>VegBcJ&!s{~zrZPf?&hCA@i zn>ri2jGc4sxx2&B1jl|JU znHuO11}2(`();BeSnji?40h6-Et?J*<3bIXKmIKoZug98_RT%ML9)-`nRPKYWh?Al z8C)_DUc0NfKxOp_i5}rtd?;;GKMbSC+`niQ0Qi>4|&1%6sl=j(YJx*!Qkn(G-U_rED&{AJ}GxutY@ZVhMTZ{qch=;xh6tPOn(? zqPfE6a}m@Md{D-Lf`iqS3Gb~(oOOw&E2{aM z;f&{`^rV0{@!n?96K(jI0launL$LQVI>Bw_{WG>Ze?%pRly%l!_@5RyBaFF&m#F0Kp$$3RyY{#(3H zAt7k-hbnmzxo&!vQyKjyp9z}cuTFbqt7m=OJo&0?rDiY4Oas2H5Wj zgc_@Nn!V>Ut&-p{(e8{|%Mjo9S51~LLe{b{rleF0Q`Oz4!)3pGrf1!TiU_2Y`}#epz`-7BWeH&0#;JR;+aFfK zuCsUwr8+u~wIH_>iti}F@_pL zG_W8}8P`6x+u3TViAgyrO>>nGbMTASTPfuGrFgFX}kp~PnD*d*v*KZDF!iZ0ZYQ!{fX{<~u zlVdr>ldKpPj};B=tn+kNwn#@Oz07<4K$?b#V6_YkX#ZQvPPA{T})mq(v8^AMAfi)Fg+5f%4i`MSPpijd>P?GlNIE2+t4_-ZazE4~Pn(^&x5$62vt=*e@P~utHMwer zA^Ga zg3Xms(RGhZ2Of4a(w(i1vP0b+!dO-IS=W#jwzIuoUbD3~Q6kQXhC-eM;2>s3jP4Ee ze4pd6jIR2-C}Y1`7~YYoJZRx^>5Wjzthf&29%8(pxq60O`8%jrI+vZXCkark&lusL$NP(y8Pxi?j9e z?C!+MihI1Am(yI_bx8`HGeD77n(AAs(ijRv#2E<;Nf~1dfR+q=LQ2eu2C#T;(-`X8 za+rX_e0a?i2qFII?;#@3n6JN3xW&Fau^26r_2I(@qy1vN9e?$*nzeYz&U=iV3IH|_ zP#qTYlF0{#rh%=5oKK==r5Ex7*IIKP?CjDKc`P?yXByf7F)!Ph{i{xdospbuTYIB) z+D;kgZtId0|H!@%+9*!Wtgo+U3mT0g?vk84Kj`~W*$`oYuoMzXkDfGh_ogG*d3l6^HlzQnP>^wT9xp)lv# zs1l~6d-@1P!V|s9<=`_}XKt)m4_4-R4E0gWY0?1Xv$BiP+%}8pR`*+DM71J4cjM1w zjO`om+q*;qg<%5x6uLL@k^%m`YQ{Z&#*v8gZ({1lb_o&1t|8I=ga98y`VxoErACei ztXG;{d$9u5BFJ9%Bc`bRp2q#3Pi&Cq z2l4#Mbm0a}*U(UcN>`dHsq;ypAd3?hhn5WP;KA7$8&#f(cE`Q6xjCPp8BGq?(+cj^ zNdJj2QOBXuyd2#JXV({s(M3Xek{zv% z84EyQJXSqkNZVL7F^(9=FctG~Lx}8~U;Elv(4(?kugikM`?pQ4KjiP5i`{XSEyey3#+}{vBH!p4UfM z^73LT^bgo2HiR*DPq(813c-YcZmPrGRL=?GY7y|jErIe*5xz|#an-utox8NZFW{D* z(>W%&k~=0m#x9%gDPHh=C&G~6Q@zg=hJNYwy*a{(p&&QWi{mYarRZlR@qG+qN9WSJ z(nnxp#SYS$$+3?sepVPLsm~+@SV{|EEWx_T8;d;oxmQKXf*RS#T%AhEV!I@(pSpBc zz8(ic*D!Cj`#Rr|x9yt8vEGYZhsfeOC(kW^+#;&$f_kULdnPaOj}}!gh(!^#=6`1H zy00f~inI^X1o&qf?s6trmS}TD)2|X1LunOF)3HRO01gU!ReAh`ggky+RGXaA|~-# zakW?4=j1hgX*BT@-$fls$$No9A7O>&8Y&sagSN$;QRc(5ii8|vB|DV8Bo!m-EeRM<3%}+wS&zLpN8Wwm=(CFE zzF3+XIOEoPF{R|jZbcQ5fZW{$c4~*|+?jXTbvx@m)nfY#Geg~VNfY>A2^HknAjil4 zQ#@P`RR&w8>Vm0EO7z8^XJ@$iTI4nlI3&gy*r}@9yfUoaTAjbnDBNV^0@~#Z`W8Y^O9}^Mh;9;oS2KM};*1`Q5{n^v2pCaz-q1Z+lgb(5$n$HR`i89OuLu zZLIWcyoD^2U)`V~k|OMQmOACUDxw$;o&^JUy2sQaS!k!-MmO9k!&4D?p&q-_!1Bx^ zhke(3>;Hsldc4a;$nA9Evi`I+GMKztPp2&oKs|1W9hF~k zh^kk4Gha-Ry@P8F%%Qs!-GI>xnEB$*!^d}Hvai_xt5Kws*IX2qNrFo>H1cHL>j+L! zwJFw+ku(0keo}Y2!gNR=VU`zndh4&)On#B_^4E^{m^)3z9kgh7x0l^`KXLD+~-fuw$(3VX%5cLAH|t`PKRkI`{n`Y?&AG6Di;Ol&u!U9EH45X{Qb z>r@a(EPUuFw}|y1Vr#mIQI6f+$Olr>}XqN#=h2-}+-?Q>ul?~S zmuAOu7#L@TfRUB^nrqq*{sIE{hry?74g-^x+;@MiA| zfwF|P)$`?W#GzJp?HY}h@@jL*ek_ei5sQKGsy11D{i?_G2Z~-sv$D)Tz7yGy%f^C9 z&gryh_oZRIZr|!*tD!q8C~qm7v2O<|^w6L=wGmhCw&Vb`sde%w3s8h|y3?5uu9E8{ zqJR+>RoqbCFM{D;93*6$*lNGD3N4{CmUM~pR zuWT&N%>)W~6uE9N5GcA-JrnTKj5Yo0+}^4KjMhxw%^6WuzD8iz*Nl@Rc#fej_4)Es(JhxxG6A1$!z_nxI zx#E+so*OLs9R{Re@nbVtG}b&ScU@b_PeF0x6Vpp1FUNz3_y>C`m@32aNozIcz|NMX`mq^n{N1&s$qlyEtuColYTaqWo@mx3*GX)3WMC!%$;Z zdtUA1E{9pjAO+Vga_8Hv{V+zZOuyCXEJI&lztj%-8jk*Ou$at%>7g7szZ zQO0VTxi&gnM4Cn`PWn8iC|=IhUuD{uBH`N^b05DQeX=2Sr`t%p>2M9N?QNQ?F=YpH874HGATRwHO3}>QY2Xc`!<{RLm)MQ!kwMzX*?lK^ z?xcC>Ejv0$xj6GHm)N3b+cC(faO)qxzLR@Kujg2vdnO0;Jh3N7518=JejHXtp({d7 zB3L_DC525dT>Fa>MjYyK79}G{L)RO$)i}|hDTBQ^iVJKn8De;4eXl4Iwzs9O7AcYR z+CBm|%8dJ;;eg@JI5{s{WBrXX>J^=`a-e zHkam!XUd2Nq}wb)cR>=b%UjHU)uDUGi$_lpxU-%S=L~E}0Vx3j#_n^&Qf0Ux$p%QS z`4=_hdSUR`JgcQA1;ivKN0 z@}%T7P1l=(p#D@j-zvt1mb#Mkix@DL5LZmJIvjh)l^J;@H`04~1QYH4W`<(CAKpb| zc1`$VNc1-uny-7gIo%F0P8oDqd#&UT(;ah~iW$4$^drz4SxicPf}(`+9)0SpWI353 zwQsX8zLsmo%SuqJFaSm1I(3_5^czi?PE@m|@x;*OoWskNu$wmT|C-WI2-?IYX{gt3 zx#_ZMBuigwUrhXDIa(q7p(xIpQUrA#ppRg@Yh(r1zLJ}@#wT0lU^tg3A>1xFqMcu$ zy7M}@T0dI|3*y5f!-fwZ_H%}sU9LcHAA&f}bz&X2f7^eenedVpHw19UIO=6)>e+8* zntkHSIZ`Fea4PD{`6Q z8@xWYy>s1x!x&l=8)NsK!;apfHoPmW#$kv=9!|U~a~+DY?@USrF1Bu&alzDb@$UW3 z(NC8i2g*iP`x&Tp&j>((UD*3!+YSvrP?VvOZVfqHacQN(^6tyO+gi`mL=!7!I_~LZ zX{jfQ<>E4zklQObf#a^4dVCgwO_Zl#Pvm{tj!-ExB|ZeCcSbq;XY1!9S7sZ{h@-(D zq>3HXoT4W_d^jHzI9?arxLP1*VHs$!T8$yE{UkVg{2l0m1u*^A^fbXfU(R{*@3Gaq-#eMMhm=jl`?J4O~Mj+CdO~ zTGBr1v7}zst6WTR8L}#^GjVOrDvQdYMpO7~R|!nd4xjcKO4F2CfWza-o(dDLBz@N_kOOE{9EMBu%L+53-c z@BPZar+PX!-)X1_Bsf|x*F8DhURhZ&ezm?M<)|BQSluzYu_$G?Rgqg`kB?lj$SpwS zUjXN#11DOkyXX=N1Pe^~0=L3qsEs89v_|nz#b86HDf_4f;#aZ7&Ss7VD z8>^p!Mq9;e6G~WGa}+UcPKb=;6qLyjXI`=`@S8Z8}FYoiA&yU*rx<;Fih*? zjYGkIW1=xPQ56jBF%x+Z#yyEW~nf2C8}l=pN@1;O&-%a+dFW$Mv&U%I^3H>UNQG? z2Tns&ho4{uc~|>@cJa(Twil(DDuDk{kHL*+3d?afS|J-f>(!dB)))PKacxk~O+;w{L0&N+#0{A!% zdTXy|?HqWRl+_)8+-1s%vpvzPWVC&)Q?cwiudI8Dd$cjDsj4r>+1~rAuS&>KKL+WU zk6-xLr)R0Duy>`rJjQ;xz=?48%qa0W)Z}wlf`x_izCt^T1)~)=h85Vv2*FA{Pye?nd|-%iHsnG0b-z zry351FI-YGY*$k@ZqACRj?!~=S;``^+eJU!F(UvE=|8{@4bi?Y`+ybX-3JT^7 zg-IF?i6vJO{hjP;Ht)ZDL42NdMq`!lFiYR3DwDT1(tU?apE3mRG-vssy^G)+V^(`K;I565={c zsxK=N3O9sC`ZcydPKB7HSXEqMyq^0teB8;>pTAvxYl_&yrdHdH`qA(??3%G$etZAn z98%iPo^n>2Z6k7_&zMQMwfc9snZET+^y@kyH%5`U%sRhShc7^7RgjCy2$_?(fl@EG z9dRpDv~&!m=vyhbgEr4qWI%vZDV!x43tC2I`hNxq!g%@3o+nJc$zHQFj`gW$qLvPS zN00p@NMn3x%&>XqQf8&R-KGSl;KkU14A?&G4nJ}^6S~0yn?!JdcLGf*9xycExZTpB zEg6uhzwX6K-20C7bKY69Qj8w)RSgZu0|_%BEJ#I(@2|cPAo{ARPt>Fm$Zxu*Gl|G1 z?JD(&{%_w7*n%|mP22CG^_Ejxx`$QXeYx+rLQ0nUt)rI3oQ3!E&#ek3I&b06Bj>vW zxd6w*I_go)suG_07Hi2WaB!q=j0R8rS&xWmyN?WNwX=U1?bs~cz`{eH4t$)#ujdC@ z$Ep_^q#pf16jboHYX;E1Bw7ph57r`)a_-~S8@nJ|sL?I3mSrNoA1k%q_15mC;oX3J zWjji2>UouZmExE$`!l$E&P3g=kiHkvEYw`?YYZn*R z765~7F8j%`au|z0;-Sa5wqsv*&$O$%W!nhb=$ly_U}Ej9tn#R`4h-X~w^yT)H}!24 zWe5kw1sl=N3mspSQb}18`Jsei6=wsho084LxilW?XX{MViK;lHo!sFa1HfwGg&H<9k@M4Th3nb zWsP-d8!H;oH;{!gAe#EH&*3L}L;@Xldc8vvr#Q7BQn1rkixzI!I68Fq~&&Rs}Mt>?WjSh5sBKe<)neJ!F8_-8GTF_pgC0LNdwU(tfgwyx&n-aX}a@9 zqkF9{UzWVPt8c?A``f}GP!lA0TUh41@Eay+P}e~tG*e)%yAezGP z70bNh@?II49NC$fCFA;|0RsZYh9_mm`MR5u@P}=O?=Kxrt~xdxxJDx_LXERdWZ@rc zsfCmn+~kYjjHj-C-F<;zvNu?tj|kfO5E%_dz!K}JA05iet8`x&%C{z@hW=L zS^K!{TkB^H+nRQw52BKiJ`lfhH(Xl`fnEZ8i^v5@$|BBN<0RXa9l_!Ny(7h^?chS| zf{)W=D9}QS-}|Qc=Tj_j$Kx!dxfa}Ct?wNgSjbNR&Q7e&iYjHwT$z#*?bv!bo9{I7Eb;X)V$qe2E%85OZYI_ehoPA8vm8eQQX&cJJ${^YmBMI2^l zP~@9wsWblKlN~0<$*IFyDVw3`1@E?6=fO75l|8Hb|3AoTiJN8NC{W&$A|Jul9>-$Fj@q(uk{rlqyW%f6Fn=B5ENqv0rHL|?sTRRJ!mllTPxosDDG#vZX z9UHnvi8E~)FM}LS9Mjqo-o8b6b)m{ zS7%eRF{CzOv(>YqSuH%FY4)@Sr*12r{_i9 zKl3~a-dBjmsvjo$}he?@A}N%$kacWv+U{1O!4oJX|?_OCdSb2?)({4ROyG+?B4%g z)@T+_&{J?OiccH0FU395vkq3+Sz%;iDma)?)E2N59i*c%7VtsXYD<^%qW-|~sTA{& z@;#pLoAVOBfrf(RMS=yN(-If!kz1;j3$D2P>zpK`cQ-_yDaB-~IX`r#gMI^NLaJSb znX%yV4K(FYcf8x)g+w6Rj-z{VIY~9UM%s_ga^md8PC@Ok78jIO>Pv=YOh<~<_%{^@ z#9gc;boR7s*Y25aH(TWmS^uHKr4>ckMa(EP5d2@hfZRoZc8s@e1Z;iz`7^!oVG}}5 zmM4gxW?sc_qG{J^9%kTm-2`*Uh25{{e&`7E6eINCqCF|ZqZWg(n*dzSaF@XQ^I0{2z* zrXjVF1a_niGv~5tPSJG%UgVs;_09Bt_HTtvd%5MRVO?nP>*o^*Uu4>omCiehzo~Od z@r>Xs&px;FguFW0r!SDC1R;(A`D7|{?t2eKRQ`~m*?7xjRVMhJ?e&k`@(0KVd7L)Z zgkv*Qb$?*Dqnt@wPWI{z5K+&>JO{Vf>@8m%yfZSm65^qh4=~9SD3y)NsGbs{30s_!v%Q_sSX{${rLSbh&FQY}-Ch2LX0s{iRz_JSbsOZah7% zRUpVB_enDpk_rL7*b6pmF99@Vs(k-#I%P>1iOW26RiFr@i*O8H@iRQ;Y8}A?fccdc z=?)H<9^zI7pj2E(tIz+u4$>cQn*n%kP%W#hl2hm8CQU;b4X+QE(Q?mK%rLpT34muM z{bF^LDfKYWo_vellg*Fx_;r|tz>08HE>p<_y%}9ZQ>JnX>&A%%ZJ?du=}vu1ski6* z+3BQ07{I^q8jUVN9eWrMBRZ=yn;?1iaS*cJEG#?En>Ou9(V&w#1&IIXFEOquD3^2 zcn{YXVPS8uf7}b@oenZpcVOjqMX2DdC^HtGPlfO)3}ZPW4WxMmSo0s5$`F(dYOGK% z{PH7Smcxk(9uR{{eT3^$KuKG6QIQ}E^sj|$To_c0k~5&zXVqWVK-=i?D_?EmMSVS5 zqsrS$3k8T(_j-}HKN(7S0598ff2_#lLqSl4Ck@7LLZU#$Z*U6CMMK}j6F>mURW zM+>`zmd-aZd~#U32qD;EIu~zO9Oyqh8c|nMk9W4#9xU-$i=hR1ft`RG&>~FYK0w^@ zUmKCfvtoUg+n!^1Gj^wF452O!Kv6jmkwF4nuyZKiB7}w4OM?j}0Z1=fL5Dq;9bP8{ zGcU9Vy97gEkK?}p#w#6=2V*s zxG=ogeD>eQS6_sw6HhDdwii;$9z#=7q&$Tg?9#O9Vq&=rg*AZs<|TYsWOCfc(u=62 z9#bJ28I;2qe5O z`%DIr6ndEMk-f&CHt2tB4KCUv3p`-%th4c%{!Due!1GANqplm* z{d8eiP{2tXGN6vZrcDRz{+_Z=E0r*sQohdw#!>@@)Q%qegj%xGv-T+rWfaio4$F|+C`_8n$qfvEi10Z8&d~^m!%3SX@qYYpOsVi4LqKukS){lf05Z6(_ z!(Lkk+@WS3+4dO=cPIa~2~qmWIL%tB;g45zMB?dt=e7*N1LXSUj zQkQC&0&xzAqo&8k$5HA;P{dN%Ok6$gPMhKflcn93f|FMWm;XZ;+g6yy1LFu+mT5|Y zOrG5G^KK%@2_&T?{!ffYriq|P;d*fvP(PgTxDsFZP+VIwm1T)U4+ATTF1Q3S-ib_; zm9adw>iPU!s|?L_Ua&Mx!v`^mI$qQzm>=}vJv>hn9AiIKA6>_wwLnzo6!>rDM<+N{ zffV+1>J!FAmG$bTmN*IrnzNjzP;fA#1~WtqB6~@ zzsRhwX@G>mdHT6#U|6dGst%y0r^^M#N^IIX(So56&w7;)%v`lNS529s3aEf@QjJXz zt4k9CPv;3LyZn$SoF=*`2FJ!9bfQR44vnKh}JLVS1KGzJ^F<0+Y zR1^hQb`;9OF#Y0yffuTq18VNpZaFBZrH4#m``Vgd#85CR20!&*K6EyrL>a=zc@<`s zcC?bD>rkc4&o~!`(2jx?>?kNz10r!WQ5_Cp$h(^giL<~;+;XVA4I>CUJQ+bIC{iy_ z3ezEd!5ynLW6$&eRM^Pq{=Cx<(i=?2-&`1k^E>4fiK2tF;soCZ{|$J0xG^&GW%+=l z_u3Yy^h)%`O(+~z@T~)Pj77kV>IQHJFtV6_rt$)XGMxKHR3OeP;5-8+1%}w#jcNq1 zEYTP$cfiwNU+t_2VO*cW@YAngQc_?D(AENLAAZMPf-Y0SreK9<+WOiX&0yuc(LWn9 z7yf&CrPu-FbL76m3MBZ?tM(93hO7?hP=ljlm`Yr}aTjHfo@f_s0HCe^9X;Ay8c@4B zrwStrvK-X7Po27-@EHI--@LN1xj$V*4XMKABUjQQS3pAL(vbw-g{_wmfNx*^3qFdY zcp!D}??pyx50vhOa>Nr?Reyk`7>nVI|lIY%-&YHC5<3Rrfly%G55#q`m< zky*Vy!Zixz5AZzv9V1ZLMdw+SF_V;J04iFKo>u$A^KMvTzn9AB=IO;Y@alOsa+f*~+*!b!2Bb1#u2j;hfO99tkmD+n~>bfm><_K&a z^T3`a7dE;EBkAWBVXY&Kla;aX|DJ|l!PZEDispq)9}t4@B=zf75vg> zFc*}vgWSWZufo$%;BWQiob4-zv1N)0G$s#HX%_h#2GKR1nv^JSI9DkO7g!QNQtxCB zC=JWLor9m7^G)NP-&ES}%jo;nL9HrXWZ0!Hh^SY$W|pZ zU|3c|5B8jEy?iY>*_#hOOyIMS0fU8J9{v+?Oysc|*VGjTRA~l$k6Pl~r(+nF5g6e# zvRhUgOQXFGGQyw&ag*Nk_jo;(Bf{EYP^MgliFxTh?Z&%*4jdvl+qE}GkapxCOMvbd z7{)b+jTf$*1KvTwcsFxBQXO*-~+^F(31L?k>#Hk)B>w-g*!AOy+N%PAhZzHcEz9CP{LP{5;G$1)=r60 zp=JK!WKp%3N1k&QQ-iveE(t)hQ1aos+9c5F@LkzQYZjQ7tUFbBcI%OZl&gh;?g(om z?$dlfmYnHx-e!Ty#Js&d+LMqcB|z9S6v4RL;}t6)n*?k}>|>CzzE-6AVS~#9yRj5> zeX+mtI!|@Y`al*jKi2ZwF&&EtyCJjsjC*qZ+Gueu(!j z3ZSqZGA{Z4hi;X3r+tvnRrnI=K0}HTR zQ`B3GuAF=S^+Mbhu`4z)qP%NS~Fjy~<$e_RPTeI)Yh5*OvpX z4gOM71L8-yNEq3uvXi$ec7wJaI^lPh4jt17a4lwC?gupGykm1XhWIEp8ALm5n*T}W z3HIVw`8(}OeQMuqfPbS6S1(+{M!NskBJdF*o;S#Dto>82eB&n$mT9{jAaiz(7k?on zXy?&oic#FZ;SynXE0`MsVg4;`S5k-h*K+i)3(QcWjTja)+At%?^D59>yx_v_?ryQ} zx1Luw=f}3uUIUS~pbTa6Em!x(OmYV$CN>Fqi4ki{7f@B~#e#aA62^kcXUPlYN%EDr z34Cs=osIb41A&US=f~kPBa<+$W}RP}!RzdKA^$SGC#)YWZr0WPoa3-d$Mt;*jU8hTeE9x2Vgc-GS z1(OP;Y3{s&YrM;Ef|Iql4cEQ?W<>toB%_`Av~-pNL4OdVtZ?2}um=Nj$y&&6vYOQ0 zAU_j~$+;tG-`{e~dmZ@a#zRN)1&+9?VAe$B31j~tyce8=ClF0D{; zk!S9NeoJQE^s65jU|dj-r6oYKoSFa9bNpYA+9VWPL>;K7SswjkJ9x0y?bs^GLsain z`gF|uF_-1FZ<<#71uG2DM<65bU#Bh=K3RepuTB`W1SLibkGUb_p*JA-f*u7ee?le# zQZ3%bavWSYN~`3FZV&ZjLQ_TvyeP#5t_kPh-0Uf#JHS4V zUzTPges^IIk>;WOx3*VBSmrmfllCpeuzD5?Z0{vq&0YEJT2qyeb4xO$hFTV4yEVMA2bi&~APKhv>?% zaI!%d%6G0$%6!(z(iK+1&M8mzjdBo_yj2Ilrr?oJPTG3C1naPx z(_^zk!TdQDxc7;_X%6`IS1n<5&H(Z4dCh{^Z_ccnNDX$`s;r-X(*h3P-So-60X>xc zj(+#f#u+t8dE^-^D4@Y?!$o81V6xLly_GMcr5hb+dt+O3~TkV0H=IWx3znhf@y> zlw^atWPyw!J}OnMQ}8g%*JUb)+rrHlaHj+{Bn7t%#?Qi4_(umX1n8*h*=?kh02*fv zD4}?mKAN0749LSzNLR7mCm>q=&|I*w{A{yWnu${R6X4er8r$;SWvk6T$3`~s%*O!2 zVncy9gn+Nd@m*X8zm~h<0*%Vo0R40h;e0jFp79dk#SP0&k3tZ5vo%#?09_um@fH{M0Uw-PEL)ne47|}4$ zI^&r3XyrvGlK%r8FEB~coX4y@TQKWnZLZhRs>MSVoZ{!)S^$>;^3&RsJ zWk~1741lGE`mfSK%^<)+J$Jry%@71b6RC;={|L_>lgQqmAKSWJx$t+D4bnMkwxe3- zTS=ld+&+$(ATm`(-du#bZh%#~?!dY{aKpogq#s%0=8lK+hn0!B!Vp3U-n`_4$n{uf zz8hp05j5olN#6pro5F{Rmh+c31O}}Z4EHVRzp_Qb1sRh#AiN+<(h79V&L&ygZAZ)B zF#t*Vwg;$#$HvU!RRb?fA?-RCV;DH`7Ve6;Xx&5VGuLPERr<&&8XpUxHsEcY zcnXRzyB%%>nhACd#;2H_CZ2#2f$ISABGyGWV;+yOJ^X2qX5ro(?gMN-1*!wi3ZBTw zd;i$BivPdp(j;^>;9j@7n*7nJPUZ+5wWf#b$t&vg>6gLqN6l+ol7r<5*WB>znk zSH|zwF`;rw=qw5ME^o=-oaR3%{2=>hCDnyB}4)z2Ti33 zMX-Ixwxmc?Ed3}6ppaq{? zt*Dd1@oyqG<)W>!7?vE4yKi+!^iV8W5sF}z!(-| z005|Ch(Vk(Z+$-L{(P@%+q8q@b*u%R+LH4o>CsHqqgGB#0VOmD_wgdsB~+Y8?EJDz z&bkhkx5qT5H^tOFax;zUxDgO!Xi>7D8Bsh&1tDaBSftoR89BL%(3t)V>SwAq^wTNv z{i6_V04cYCR0(8Z6Ugc0+AdUfZrFcgAkQwi?`uQZ`8&!aGlukY2|rYFf$n_7VL%`h zQ^QjLKS5uj3>;NUdS_dPiVB1e%lB?HKozT|4Y9g0-~i9QI|pdwY&fqJ@9=Be%ImhlC^o) zM~5zJQ!qt02?RyhMKP`R4cZ9c5;Oa_U-_wdwb*fLcD0M?UF_FyxZ?8V(9R%TCJn{iyCLKY4YGZ}yxvb1`~YRlFRiUq1V{9E|S=8vQ7!HK$?!ySvJWsmK= zBSOshPW+mz;}olQe`30k*-ZZ1+NtzU3j6)48*jI2&iJ~kc-IDvNDfRqQJ9nv4e0*p zIq*om8_1{mw`7Bor5a~fQ^HO_%d?=#KNf~NCix(uW7svG3aFV`Dj^rPNM|}Ek4STW zjw0L$P@hnyzVM`Kr9pMTS&~ohXVcWHffQpKJsXm-Vmbk)~cPU%Q2Zza~*5^giy&6W#c$UjFe?kDTHa z{M5XY(NcbEQ&hVzi+4k4Pkey`+z){&Wb0vO=0~++T<=6rBrTq@Z+2b{T;Bi7N;`M| zPeN4EOW$fZ^8e%Oc6ndKDE}E!C|rTP9DJeyBlo#2=y9^Y5v=iY=q9-_>rc#gQ>~o8 zcV0xg$r`Sl#-$7DIXvv^YkJLQ<(1exFN}2MJ#|`af5+Opw$^iTQQX4eZbXXMsP>@5 z`0Kt|F7-pPZ^`Mt%s-Mo70#sY{rTFt3ce}vWi99_NH4+{aqgkAJ?=PEqM5F%$l3;pL;3+K96eOhO`B49%jY7IG4P=Rz{pNTWY_-*Ev5MyqrGCol+5RU{jIEs9?KdqFj|ShcSvr`^KB(-NT$fRhdcPl}zcRepc-XVj z@}#=!O3kvPr2V75fga-KjnR1#KC|lB<4g;3d#6bPPV|{<*D_9Cj|WX0#MPeF z+|G!3=gn3#=Gx@FW|PI&4+N0u+i#l53yQ#X^2K9Q{= z%2fDwMToBjJK9ae5~s4Kb-3TzfjGB_3i0gF`3xb)=Hju&7ZL5!1HePH*x-ILkjv}?Ytb6&Bpi;HOH8Vdtqj|ui$X$=lhnuWlN466?Gw)RF=HFFYW*mEFfbhkR z7bi^?HqX0hA8s_(+nJlOW^!Kt^?F7yzxYbk+Vb-OkD25(=gDgo0WA?GRr7;)Yl*C) zKOlCvO{np&HUEip))PHWExqj~S58KAyo)+b2EIi znC|#0YU_6N?MTj>y-TrH3g*!Up=zY5?}rvTj=s^YW4mS?Sjw(}OP8Q~>5TS6OfaX7AQ!G+6!0|q%mStv(fm6 z9nEVQR{i`EWoq_b_{^WF3>9Rks_MzHzQwYAyNy4?G!ePm#D#0T??hij;A(y^>te7b ztLaiZ`)j0sc0%_YIMW8*EqGtNGZK2<=O1cx_TY^PAw4=tVmB{oa$g{~X)+*kK6kT6 zvfI%ivHK%HWQ+%i+#8;J@~*TYNE&lO_+$2oHsSePdfY8b*3+GSqQxN4ybT`At!mh)+NDL)~-V*K%IRd7Q_2T*eYs%CWouos@+9&h9#kMMfXbAO~*g zJ1gEbWXcPlNLxyUT7;+5e@)#T6D2VOwROwrs=Cl^oR@UE;^C2IFnR-J8CO_g?hg25 z!M&u=bs3rPns(dOKCET7#L1zwdzx>)FF-wiAN-fO&=cnVmQ-Y}4`<>4p!Pd3V`n+&3~?;~W-g0T+67 z+{Zf|7*&{OVHkP}Pc1#A!=&i%O)b^Uw^FHLbnSenm^!uaWuMkeD_y>Tb3sfSvb+t) zfG+raTI64JQJ$NuwC+ablAxGe#LK<)tK{Q9Vz33lyF8W>kzXQRjJsR*lol z>;kXLusH1k`1Ol_eKxK+bvF}Oo4Fc@ohszfJn9-Y1I-M5YjJg>5@Fu|!*~SV31N!2}s18+u6!Fpg;vZ)Rj++2?NSlJyegX!Jw@~K5bIXlA%Txd@f5Wlw_kqbrgQH@%>orH z_AJseU25VwdPgt|2C>|l2b9LQInfkPS{@nn2xTYL8St%&Hio>QrbNi z*dLyxv$Qn>=dWRtH@u~kqf!SAQe+}Rg4tv}A{1s_=eg0qjX9R7`K{1@Fc^GfqQ1L+ zdy}77Z}$R`{5mYM$c9!?5e_^6#xkmraU9i|hu!RM=~Og!C2?p)$X6hXXV>-3eY^FR zUglwpaa?Mr>8yLM`YYAj&>rBC!ZD%eBa z5m_JmShtq;``19-mPTNV*?(=d?XGqO4$wu1~Lmov3uH-*P-f$LSSz5;2Z$l3AkGpxvs zpNaw8MdS@0P?>#u`Yl5$8rv9XwN%yN`Y!JBC8$~F3e;xF;>Ur{%xhB`{oUJLBE76^ zy~XZQHANb+BnLp^IK+a-orXw$mZA<(kUJVuv=rvl@z6Q<^Al^H4Ve?FCA-US$R$W( z`Hw~gTA%F$RWc5%{+r4FRj&9P9U$YfS*8~Q&g{geXde1}Q)_1=hjr_%!hXk5JN249 z%AuSC_GOe3CFXGmQ^|{!D8;9{>t6s;jjI6hSQ_bfax~(J6 zwO#4@(akd2a@u9K)EDhvCJ4{TR;d5r^xTKihg{<_rnQWW**5J`(RSziplngQamDwT z+W0X)P3XspKU`na974zBaGczaSl5!me2rpMS+IOY1xU41hr*VcL~9J=qMEe7=%fbL z=$lFQx_g~{fgP{OhtAiHPlR+MxBoO0y%uLhrK7PyOCDO=V}J#?30lC$E zPYF>Zzs}x?J4&;ZaJynQrzDkMu{k=)x~`5T{NCu0$>TYRAez9LEIRsWb;&FA(&nE@ z+uMMu)LySCoQN*3w`d%D2lWeH_I3Z`UlQ!8rVhB8Hh&F^BlS3xoK{?8~bY7l_`Pe+_?{W4M_VR-SMl~4779x2&^`G{INxfA` zRJdYlrAtWu5-(H=uS3&5Iu`pAQR+2a#8rmwWuB(ZL>>=yZ#B;3QHBEJWGSNdkg zEc#NjpN{8E>I*YL`t-i%>KbOo2x_Aiy2U^Bx+_yu;CJD_EKm`H#f0v&&Y8S9zGECO zzmw+bq%_Wyr3;QVUfT4`-sh! zvWI5FMN}v4eNbo5#ET8NVg$AVhL^lub2pr==pk?wS;y0!V1ucEC=q+R=R9m=2O*3Gqwln|BcK10%}+NiPD;8e)HtFAVXyB7}?Oi23fIxg!2 zEf7&1giWf}2*x1sfzHc5u=`S2dpQ^B)#31U{vu-#&RmCZ$5$or*`;`BtU|ES{%a+G z5IIs|cG~u3aQWMx9!ktIhzi>-3@J^d7u6EZLeTu1N{%wapk^j?^oJz;lv>zQ1FOYY z)?{|1ib`|dta3Jy`)0yD$gqaV!w=F_G)s39U9Q;!t^My!R4e_7-o{GiPp)PYML4mdAm>rH3xPQ=*3g9NVhtCz5 z{n-ud#ud6_Z)*O>Te@Mz<^pUPZ8K{(hswg}y~d(lt+NgnZ!2h9%}otwU@_X|`gF=s z!*RpBC$49N$EH=%2*%i=;G&z3=HS2IpAS_2sa$*ii7o{mzfSJmP>}VJYqG*$Fsf=^ zPJpd!b^prx2>tQiSP$dSz7&0plA86Ax`vRD<)Z)&>VhXz(rNl)drcp~<%9WbB)b`V z^0*al+%6q`s5ksquSw3hZ!jqR-D9R|I3WBpk)OYr5-dA8PguJAOsx#+I8mC+5-&x*uPK0UsL-*GLVtKaw^{1HGZqp%d2#1}epI9E*2WP*ncZ$jehsx|1=2lo z=IVUn;H4v%Vm(tNH;)P@Z{=Leqv5>R7t@3ye)B}b=d({1n2a5ruxUe-eZ@!mm90NJ z+nrzFDir6D8pH3EBithknZNi|Yj-A6b03a+moOwMN`Y-9%HVUbGuK3!=eZ6=E$-#| z*i&aZZ~;1sPp)5;I4l4D6mr$V(+M*cDeT$$yCCkLB4(=s#KAj6`JBwYV%MU3ZIPpZ zi_8yh8@`Eq*l^xUYCsJ(7@}_z)(^4N-tpH)%kMQ(1$H0WFyQts?hxh*UBiqN;$Frq zNLv2o%2q&rP%Uc}j(SSA-w%0Rcx|Bu!Hz*HD@d%KuYv$NdZCoEV zKq5nIDmzBC>iV(*Ip3zDD*26+0e$Evf+ia{pN(_=8xxm%zE+vy{Mc%aD{l+vg%fS@ zg~y`#y~tL;!jiQ)Z?pu5ndZk!6V*Jh;n$WH5|LZ5?x$DG5H(_0667dhuO!30-04Mc z>n%*f08VH>%~=jE-iWAv=LsEMXk?rjx_Js|KJ^_WgjQioE@tJZVLKJuB(3KE-WEJa z2y&R3G*Jq=Wq7T)@Gx=5PVUyLg{Bqf>lS!~WYXdUd;fTa}r?rST5w@0#dZP~QB0cF3h?b35i z2+AXmLkOn}MsaAbjHZl!TG_yTN1n%+D(flm7*X2Qs~0Yr(37Ph2YN{TiPImFXqm4n zgqs=aIvG^O(p1@QdnlQeI+9uYvS8u>FmEhfPuFB0T>aT=29rXVj;Gi<8;_?j7%@FFAUG{_IPqsygTPl9 z!*1S+36(=vCO9kfMYD(BiFcRt(N zntqx+R+Ps%Ejf5Kr9uy(dAT}N)lv&bn?9)%`BCN3-_^t!nKgdNimf!^GMo(VikdlG zUno5ZdKY&iq*q~mNP%ZwknRU-Ies)I*?bV)x*zI2S)@BaVkWSpchuhrX zDlj-Hz`pC3N;kbdZdmqWVm6ZdxvJ*_Yn;sD@p$stp-Cp-VWcb65wa3o-b=1}WO2@r z{>s+*AP)a1wpW*`rpL5@_8+|mw0;O8ET>YCsj~xDwp8hY+gOhV2ngleb-nN!Ap}c5 zh2a!4*K{F(56c7U=SPiFkm$<*Wq4S2>MQIfy6G&zj~Kd{*3xkmq_x@SV}Nk}wV%wt zj?*+lQ{j)Oi|($2(MibfdQ5mYK)osm3R9NrUT&efqM{2WhBhN>(3Q#Huybl2=)_!! z>!~A`s}62D^qO5IlUSUiPU|>G^i4}aw#Xrn%@57grn;T`IhWU)L4=!2*AR*5P?xISjqHvYd{b3^s-QZTY3KR!pY!W1CAH8sjDwdiS!wED)E zIorr=)O)6E(6uYL=!i={?Wd|qS4;goxae@j>V)Z6t`_AqXR>IC#)+q+|9jENgaFqm zaa`GR-$HyC8G6k<7us5sPrv+=6geaYvwdD$;=T6c$uyDt#Ua7ByLrKDy&cs|HZ0l* z%MTT4?o{sJ*sn6n;?bco?wu9kJB@fv9n+^|W-I}y3erTXkYC#z9U7u~I0XIE^Xw&a zo2{a4Rgq08>WTELq!Msq;}~sb`}oVqrLB<<4pE6B`9GPm4RAW}P;(OG7%l ztMu3ZUyRHb;AQ%6=aFBRe!YM0nk5g|J59;7xMzIDM*0nv2mbo{8AjMR2F>8Zfen4( z0wT!hiOVY^tS6A8JSI1DGKw3LXdV#KnbN#*8wR% z^GO3bj0fZX{!H#UFR9^bx0_h~Ee(ZffC{5|G8}7W$lt9N)ePww=SRojnF8!!m|fgW zMU1w+i(MIfDUhknrDnL|bdS%xyHtKA>kU1OU<8H|SL=P#-ycz!q z_i)`V!Yrjs=o7VwLhz3^fG?0C?iWL&XSv1c3vhgwmhkNOLa6j8yQ@Sjag7?DWFk+y zHl0T)A|wt?&as6-9j*|RTO+D)y?=i~uj$jm#BpON#Y>OP_)W47=kC!~LJ9GHV+#C6 z(d31dP*uSADtG-Xp^@KyaRxS8ftnO>`Z~80iL{GeO&tnu2`6o0cy)v@t0`1jE5>W{ zGRV{4f;`@F#>y2JJ!m$Ep{eWBommBa)2|FvS12Z0QM}Eas#;U(Etj$F zFFrIh%ZYfjJkX~zYT)$V+HCJfPS(e+-uuMA%MSwfvC!*^_l>fJ;Mnr{h4oi8u@UV$ z-_T!e&71T&vs>mm$`FgG(Z!Q_p9{#?PY3BcTrq(Jln+zNyj84Wpa6EE z0UKJ24D~2{tZKqtL}T&spIX^4_xk#je}EKjtIcb z4P&UolcQnUL;L_$(y%+>jGf**dI_{)WLueQT!Z^&z5GzimBblMA<8&}$Fr)*r-%2J z`#QT}g*gpVxu-n+6o#V_ZJ#xM$~vpu{}H6J1F=gd$nVxbbkC(^WNqVnVxYUJykx!f zVvYaY_f$9)rYun;*wDY;{PYdKfbgnz>FL)M_CP2cN2PfVRylWJyy`B(<-8seCNyIF zL32vByb;LI7Jy}DmOaP*H?#fM7xY?`x)~Bg*fz3z_>iB5)60xLm)Q0jZXBHUrRkQEg>*&fQB4) z3=EOBV|Mi&LRSdNWFJ4JQ|CmBB)@!%)i#@a@*fxwfY5b@H=Y-U{z5M~}&PKi%p#OkL(np1zlCWP=+8)1Lt1C5XU z$qd9&H5`^?349+}VU%7os_H#WTpNF;g>As5rm2Dy@iElMIzlCA#s_hUQ6$1)<5QDZ^9Hx9ti4l_m*`jXs%9ez zCVvQ-e~qjoqG;$!^pOdhm-wAO+E0qg{DCmWkM=)h$_E9OqjIa3-@VfASxWG@Lr+55 zF)#6h{0eD-fz^r<`YNG>L+~df%l$I9^bRQ(jXEE&_uTJGy0Ocbye6M^F2I04(?qGeqny)L)RoUnNeAkKdngG`>-I= zuNAj*Y<>89wpiK0FaP_hBCyw?Cp^bvE3JlzRduv0w?$t4^i8r@P>v0P*>@#^x#M?u zvdVHshNlBKvry%6+F10+eeg<>e|zSbm)dJgxbw?E%Cq&&mC`Y>2Vmf+o1heLKu@ZG z!8OxPlPfI^aLKaT1JF^>2s2b6c}Ko2;1I7KH<@?PS`A*<@Wi1h=TS^Eww(|IXFQsIGrjqAn7D4J=c=@UgitTMZ z*j4Cu{l3GQe|*O*I{F0);H-ZKTHWwTYvxSF&r)*2jGx zN*J4y zg||)pjYtJY>^6Cp{L4OyOe9kp?=~2XsCDE3rQHK;Md7uW3o1@}@cto;z_EbArCYN6 z_`nX{jTHD)B_|AAGgsSUk#p2nkJ>RB2JXnXUApqxm2`Q4&u7eFs?=PI@f=Ya*%UYv!LqoA zkXc+|{d8L0h4s~#ykq2{#o|%M`lwlqCn(;3EBgqVl{ZQZynKX$(o z+j~2QNZwdyOcnvybLBiQ{YbqH7FGASa`63PuPxhc1x$sCs7GK(rCfdF)^tOpx>crs z&l{u;USTV6AZ?G_gO|ELc(7?*{VC2u4KuDCDL8%a)OIcJMAwRHRMRLthmwNK;lk1e zd3U&oc19c4C{!o>IJWyC5*r#AQnn!mAJRYa#8vV3!Q(S$g-bXF9~NGa65P%Slh!8c z^7F+ysI-eix8)yp((710Mz;3s2@UYIM}v9z8gO@obMH72w;CdX}Y7y7V>vf!xdx$qOB#_fidKncTY+3Z(E0?S^l zl{@}2y)Gio?c>J>UgjP6kd$fr;#j~?eXnD$CDjHX0zOyz_oxnVZiSF2XnK298e z{I1&0SoH-}Y+QEX3joI+*`@_6FzYFm%f*`2UO(3#4yj6CU-*$RjU7LJrla3BrfTV6 z#Kc9=#F_Mh|E^@}Xx3`iA|uz7VfE%z=8t(%fo~f|0mGYW`PmA?yO3-=>0G16;(I*i zjC6>kiT2`@m*e(12<0Nb%j0=YyZb7qBRvOxMl;~|T?sW$I<|t=v_q{n!L>h@)-+IH z^xr>LHlii~RePDbKmNz47G}3y`vHr7GTAVYRzfEtc!h_&h}ll}SN`sg-vi6nmYR;^ zb}!s63ZLP%gB1N5oeW@4hmZ|Ex;md5KYr|GaiHP$?Ytk4*GDp)f!Yh&8nX`l9?fGWW_B7haW(V@iJO|8nxdI{w8teJTP0d!mIDiq4tz&SS#K58XSPdje ziU)NgWnxAe@+|Q_36Vd)O_2?@nPKlKu?ej z(kH!XQCilgCRyOJj?HsX*$2-Cx}Dy?brrGqd3KMyc8?CER*H?jCSUx-7-NZDpg3!0 zI7P8x`(@yq+FYMK2Px#rE`9wzkYAYkGC^2c-Dt0z zzVLIO?`c*VuME#Wj~LUsldO=b?Oz(X6bO=k0k&vwowAB9Y-cm0Fp2QGJ0zIQrw~sqQWwhqJvSq9M_GOdB{65MEPYxf7P!NYWP2l4r&6 z@L`eq_Zw-Z1EbytKi`zRkyCmM(z(FM*;`;OtSGYEYGZ*cf z8ja~kdlySfms|DQ6iilxgmPI$8Y1UzMqq?}y;^$D{_PaVy1s$X$D---G(L4MaF$h)h)_Ym|_Pz-u5RQzb+eKWy)#EyJJ z6tTaXu`}Kyjm+|y%@6&rNk|l?*W?hFrHyUI3PmrzTCF<8L)LB&6c=gnLu=+gXH)nt z$MZUs?R>sR+sVniv2H`uBaX7vQv5wpvg;qau&)4UNRz@B+(|QDnqRJQm^JNH4QhK} z15c_@Ldazy(pa7{-@(NO2w?X1R zVzaTYULkGi@0rP15tQxK!azZP#41LEan&kJvr-90S~ zpDa{}*j`E`M+lcQcMt=Y9^zF+Yx~N7Ziu@%VI0nrHexD$nBsk)e>3EdKb)@$7&q{x z9wXsj98lP;>ji{b-Jm%V1}oX)Z8;SZd+jv#t>;>|Fmi+PKlP7c+H34<8T{C*{%G0J z68d>ikG%s*T&fBW$C?Q*dtRzifb#40%$VTwXq%UCX&b%g4ta`&od$=Q;UXF3w=oW_ zy5vHA!pSp`h3`B5cTLb6^^fB~@~PW-FNKkP_KlWig^Z_P?EJJ+?ZTID`!Ay88;zC_ zt+?9VZZ2}bo90CI96Qagi=p-SmtD5c@s!F%O%m+BNB;)7{&|zg4=UBMdt`nZh$KIb z?rm8(LwZDAo9=19-WEx=czU0&ir2h;$7}8vqG02g&C8U8Ll(x0ri9M8s2s)n%(A(f zfA4&A-e(>v<0iBD2=UP8m|JImqQCo~)eHXRt&-(tYe!|0>mCxPS69W?b98P9)S4EO z8uVL}1;3d{{R{WAXQheBtoVY2RmugGMax|J%s;EFJd0U*JuU69wUJ))vFgKi%RzW3 z5EV{&H#zJo4H`Gb>A6TT<9ssVPFwTMSoE8g-llbPDq?Yxm|YjbaIuq?HNmNJ!3Aw3 z#}$!7$VD5f*UaV#)zdq_rvH_bO}};3L4SBMcr(h(GM7rA$irrbGFeMNO z7d26d|J;z>E#I<3v+t#a$OFCa2W0zt?ixBKs6jS;3W=2CCxo-^cihcOI8C}yIm5{- z!hjp>{>+h_!c9UwPo^DQJU~f}z(c>gp@q`9Dn7Z5EmXRasUVrwpHBT#nheF zc`sOc{7xUTA&KZ38+9f}U|NvKAw0N<4}+qojcmRgN|=1W@Pl#2QQNdrWya~ZVD&dz z4_&u8`S}`me+~(0L`Y(=rb$`T=j%Z;qJ-35lwVArJWh0EW+Zbn>F(*xjZO37%i?#= z*@g!J7ysL%lW)AvKs8!jAKSf0Uk2wE=}#p)xY+7{Pj-To%yVX3ic+ z1UD!({`VGwx+i%e3cZ}a<&h@L*t0eQfJ*maScL}>o*n)7?!Y(++jeXVL`vh;sk<4$ zT`jAv+gyV|3&8|2)Ng@s_V5F^JMW@7iA>fT_(0=uO~qr0vtdwKDT9agjdHaNiwD;` zGtwVs{aGBWoaN3;hGzZojh`FYlv!1q(j4B*nF)D!HOxSq6bJ=!hFg6l4#;Jzma!Bd zmY(8DwEUmkn$~ZhT!x?Bt@+3oRtF3YV+?AvKACFr}Y86Va7l+wBI1zHrCvZ?3 zYNoM2o{G&=knSB%R=nG5y-kd2Z}=k9*2!1`g{fg*uCElvmg%nHUglsez+DN;a=&Lq z)!!OrXX3HhCo__HBEg57RdHyP($gCC#uh)s)?^FAr7XB`)pvr{->ur(a|N5G?B3-sRXidFRLVj-7xIH zlB@#(|HI>14&V|F%r${4*+`D|&gZvl+lD1{s%-@LzVT8G#SX|2q@*1}y#g>gi9m)>JH3wAgA&`$ zE7YJ9iIleUby*MRWWf{vgy?Nqp_Xtm9lv8HZjfFF*3lZY`FIj>9TD}Jnc=kH+vwt< zA#)EY3n@cr^IeIGJiVAn(Y1@q$>9)ouaqj5AWp=t|f|M(eyyZ5 zKZolDTICEc{t6}e2Ww$a!*POJApdvzCx+g0KH;KpL{Hpc<}VEisrKQwgDtyh*-#=V zYNXp3&aQ>=f0YDmKCkzME~fK;xCaIVdGP1|_PxPn+u9i7;Obd3?s?wF{-<~E22scE G>Hh;)4!ukO literal 0 HcmV?d00001 diff --git a/frontend/src/api/auth/index.ts b/frontend/src/api/auth/index.ts new file mode 100644 index 000000000..6fc86b2c2 --- /dev/null +++ b/frontend/src/api/auth/index.ts @@ -0,0 +1,43 @@ +import type { AxiosError, AxiosResponse } from 'axios'; +import { useMutation } from 'react-query'; + +import axiosInstance from '@api/axiosInstance'; + +// login +export type PostLoginRequestParams = { + code: string; +}; +export type PostLoginResponseData = { + accessToken: string; + expiredTime: number; +}; + +export const postLogin = async ({ code }: PostLoginRequestParams) => { + const response = await axiosInstance.post< + PostLoginResponseData, + AxiosResponse, + PostLoginRequestParams + >(`/api/auth/login?code=${code}`); + return response.data; +}; + +export const usePostLogin = () => useMutation(postLogin); + +// logout +export const deleteLogout = async () => { + const response = await axiosInstance.delete, null>(`/api/auth/logout`); + return response.data; +}; + +export const useDeleteLogout = () => useMutation(deleteLogout); + +// refresh +export type GetRefreshResponseData = { + accessToken: string; + expiredTime: number; +}; + +export const getRefresh = async () => { + const response = await axiosInstance.get(`/api/auth/refresh`); + return response.data; +}; diff --git a/frontend/src/api/axiosInstance.ts b/frontend/src/api/axiosInstance.ts index 00cf80571..f28312599 100644 --- a/frontend/src/api/axiosInstance.ts +++ b/frontend/src/api/axiosInstance.ts @@ -1,5 +1,5 @@ import axios from 'axios'; -import type { AxiosError, AxiosResponse } from 'axios'; +import type { AxiosError } from 'axios'; import AccessTokenController from '@auth/accessToken'; @@ -11,8 +11,8 @@ const axiosInstance = axios.create({ withCredentials: true, }); -const handleAxiosError = (error: AxiosError) => { - const { data } = error.response as AxiosResponse<{ message: string; code?: number }>; +const handleAxiosError = (error: AxiosError<{ message: string; code?: number }>) => { + const data = error.response?.data; if (data?.message) { console.error(data.message); // TODO: 커스텀 에러 코드를 만들어서 그에 맞는 message를 담은 error 객체를 return 하도록 해야 함 diff --git a/frontend/src/api/deleteRefreshToken.ts b/frontend/src/api/deleteRefreshToken.ts deleted file mode 100644 index 6afb15afe..000000000 --- a/frontend/src/api/deleteRefreshToken.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { AxiosResponse } from 'axios'; - -import { axiosInstance } from '@api'; - -const deleteRefreshToken = async () => { - const response = await axiosInstance.delete, null>(`/api/auth/logout`); - return response.data; -}; - -export default deleteRefreshToken; diff --git a/frontend/src/api/deleteReview.ts b/frontend/src/api/deleteReview.ts deleted file mode 100644 index 8d3b3544f..000000000 --- a/frontend/src/api/deleteReview.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { AxiosResponse } from 'axios'; - -import type { DeleteReviewRequestBody } from '@custom-types'; - -import { axiosInstance } from '@api'; - -const deleteReview = async ({ studyId, reviewId }: DeleteReviewRequestBody) => { - const response = await axiosInstance.delete, DeleteReviewRequestBody>( - `/api/studies/${studyId}/reviews/${reviewId}`, - ); - return response.data; -}; - -export default deleteReview; diff --git a/frontend/src/api/getAccessToken.ts b/frontend/src/api/getAccessToken.ts deleted file mode 100644 index b4a570aa1..000000000 --- a/frontend/src/api/getAccessToken.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { GetTokenResponseData } from '@custom-types'; - -import { axiosInstance } from '@api'; - -const getAccessToken = async () => { - const response = await axiosInstance.get(`/api/auth/refresh`); - return response.data; -}; - -export default getAccessToken; diff --git a/frontend/src/api/getMyStudyList.ts b/frontend/src/api/getMyStudyList.ts deleted file mode 100644 index 509b00e91..000000000 --- a/frontend/src/api/getMyStudyList.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { GetMyStudyResponseData } from '@custom-types'; - -import { axiosInstance } from '@api'; - -const getMyStudyList = async () => { - const response = await axiosInstance.get(`/api/my/studies`); - return response.data; -}; - -export default getMyStudyList; diff --git a/frontend/src/api/getStudyDetail.ts b/frontend/src/api/getStudyDetail.ts deleted file mode 100644 index d5e05ae2e..000000000 --- a/frontend/src/api/getStudyDetail.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { GetStudyDetailRequestParams, GetStudyDetailResponseData } from '@custom-types'; - -import { axiosInstance } from '@api'; - -const getStudyDetail = async ({ studyId }: GetStudyDetailRequestParams) => { - const response = await axiosInstance.get(`/api/studies/${studyId}`); - return response.data; -}; - -export default getStudyDetail; diff --git a/frontend/src/api/getStudyList.ts b/frontend/src/api/getStudyList.ts deleted file mode 100644 index a636ee05d..000000000 --- a/frontend/src/api/getStudyList.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { DEFAULT_STUDY_CARD_QUERY_PARAM } from '@constants'; - -import type { GetStudyListRequestParams, GetStudyListResponseData } from '@custom-types'; - -import { axiosInstance } from '@api'; - -const { PAGE, SIZE } = DEFAULT_STUDY_CARD_QUERY_PARAM; -const getStudyList = async ({ page = PAGE, size = SIZE, title, selectedFilters }: GetStudyListRequestParams) => { - const tagParams = selectedFilters.map(({ id, categoryName }) => `&${categoryName}=${id}`).join(''); - const titleParams = title && `&title=${title}`; - - const response = await axiosInstance.get( - `/api/studies/search?page=${page}&size=${size}${titleParams}${tagParams}`, - ); - return response.data; -}; - -export default getStudyList; diff --git a/frontend/src/api/getStudyReviews.ts b/frontend/src/api/getStudyReviews.ts deleted file mode 100644 index 646ceda5c..000000000 --- a/frontend/src/api/getStudyReviews.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { GetReviewRequestParams, GetReviewResponseData } from '@custom-types'; - -import { axiosInstance } from '@api'; - -const getStudyReviews = async ({ studyId, size }: GetReviewRequestParams) => { - const url = size ? `/api/studies/${studyId}/reviews?size=${size}` : `/api/studies/${studyId}/reviews`; - const response = await axiosInstance.get(url); - return response.data; -}; - -export default getStudyReviews; diff --git a/frontend/src/api/getTagList.ts b/frontend/src/api/getTagList.ts deleted file mode 100644 index 2b0e0c3d7..000000000 --- a/frontend/src/api/getTagList.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { GetTagListResponseData } from '@custom-types'; - -import { axiosInstance } from '@api'; - -const getTagList = async () => { - const response = await axiosInstance.get(`/api/tags`); - return response.data; -}; - -export default getTagList; diff --git a/frontend/src/api/getUserInformation.ts b/frontend/src/api/getUserInformation.ts deleted file mode 100644 index e6266ddae..000000000 --- a/frontend/src/api/getUserInformation.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { GetUserInformationResponseData } from '@custom-types'; - -import { axiosInstance } from '@api'; - -const getUserInformation = async () => { - const response = await axiosInstance.get(`/api/members/me`); - return response.data; -}; - -export default getUserInformation; diff --git a/frontend/src/api/getUserRole.ts b/frontend/src/api/getUserRole.ts deleted file mode 100644 index 9d93c9a0a..000000000 --- a/frontend/src/api/getUserRole.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { GetUserRoleRequestParams, GetUserRoleResponseData } from '@custom-types'; - -import { axiosInstance } from '@api'; - -const getUserRole = async ({ studyId }: GetUserRoleRequestParams) => { - const response = await axiosInstance.get(`/api/members/me/role?study-id=${studyId}`); - return response.data; -}; - -export default getUserRole; diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts deleted file mode 100644 index ffdbb8464..000000000 --- a/frontend/src/api/index.ts +++ /dev/null @@ -1,16 +0,0 @@ -export { default as axiosInstance } from '@api/axiosInstance'; -export { default as deleteRefreshToken } from '@api/deleteRefreshToken'; -export { default as deleteReview } from '@api/deleteReview'; -export { default as getAccessToken } from '@api/getAccessToken'; -export { default as getMyStudyList } from '@api/getMyStudyList'; -export { default as getStudyDetail } from '@api/getStudyDetail'; -export { default as getStudyList } from '@api/getStudyList'; -export { default as getStudyReviews } from '@api/getStudyReviews'; -export { default as getTagList } from '@api/getTagList'; -export { default as getUserInformation } from '@api/getUserInformation'; -export { default as getUserRole } from '@api/getUserRole'; -export { default as postJoiningStudy } from '@api/postJoiningStudy'; -export { default as postLogin } from '@api/postLogin'; -export { default as postNewStudy } from '@api/postNewStudy'; -export { default as postReview } from '@api/postReview'; -export { default as putReview } from '@api/putReview'; diff --git a/frontend/src/api/link-preview/index.ts b/frontend/src/api/link-preview/index.ts new file mode 100644 index 000000000..644b74423 --- /dev/null +++ b/frontend/src/api/link-preview/index.ts @@ -0,0 +1,68 @@ +import axios from 'axios'; +import type { AxiosError } from 'axios'; +import { useQuery } from 'react-query'; + +import AccessTokenController from '@auth/accessToken'; + +// get +export type GetLinkPreviewRequestParams = { + linkUrl: string; +}; +export type GetLinkPreviewResponseData = { + title: string; + description: string | null; + imageUrl: string | null; + domainName: string | null; +}; + +const axiosInstance = axios.create({ + // TODO: 링크 미리보기 서버 배포 url로 변경 필요 + baseURL: process.env.LINK_PREVIEW_API_URL, + headers: { + 'Content-Type': 'application/json', + }, +}); + +const handleAxiosError = (error: AxiosError<{ message: string }>) => { + const data = error.response?.data; + if (data?.message) { + console.error(data.message); + // TODO: 커스텀 에러 코드를 만들어서 그에 맞는 message를 담은 error 객체를 return 하도록 해야 함 + return Promise.reject(error); + } + + console.error('서버에 에러가 발생했습니다.', `코드: ${error.code}`); + error.message = '알 수 없는 오류가 발생했습니다. 잠시 후 다시 시도해주세요 :('; + return Promise.reject(error); +}; + +axiosInstance.interceptors.response.use(response => response, handleAxiosError); + +axiosInstance.interceptors.request.use( + config => { + const accessToken = AccessTokenController.accessToken; + + if (!accessToken) return config; + if (!config.headers) { + config.headers = { + Authorization: `Bearer ${accessToken}`, + }; + return config; + } + config.headers['Authorization'] = `Bearer ${accessToken}`; + + return config; + }, + error => { + return Promise.reject(error); + }, +); + +export const getLinkPreview = async ({ linkUrl }: GetLinkPreviewRequestParams) => { + const response = await axiosInstance.get(`/api/link-preview?linkUrl=${linkUrl}`); + return response.data; +}; + +export const useGetLinkPreview = ({ linkUrl }: GetLinkPreviewRequestParams) => { + return useQuery(['link-preview', linkUrl], () => getLinkPreview({ linkUrl })); +}; diff --git a/frontend/src/api/link/index.ts b/frontend/src/api/link/index.ts new file mode 100644 index 000000000..70fad0c3b --- /dev/null +++ b/frontend/src/api/link/index.ts @@ -0,0 +1,54 @@ +import type { AxiosError, AxiosResponse } from 'axios'; +import { useMutation } from 'react-query'; + +import type { Link, LinkId, StudyId } from '@custom-types'; + +import axiosInstance from '@api/axiosInstance'; + +// post +export type PostLinkRequestBody = Pick; +export type PostLinkRequestParams = { studyId: StudyId }; +export type PostLinkRequestVariables = PostLinkRequestBody & PostLinkRequestParams; + +export const postLink = async ({ studyId, linkUrl, description }: PostLinkRequestVariables) => { + const response = await axiosInstance.post, PostLinkRequestBody>( + `/api/studies/${studyId}/reference-room/links`, + { + linkUrl, + description, + }, + ); + return response.data; +}; + +export const usePostLink = () => useMutation(postLink); + +// put +export type PutLinkRequestBody = PostLinkRequestBody; +export type PutLinkRequestParams = { studyId: StudyId; linkId: LinkId }; +export type PutLinkRequestVariables = PutLinkRequestBody & PutLinkRequestParams; + +export const putLink = async ({ studyId, linkId, linkUrl, description }: PutLinkRequestVariables) => { + const response = await axiosInstance.put, PutLinkRequestBody>( + `/api/studies/${studyId}/reference-room/links/${linkId}`, + { + linkUrl, + description, + }, + ); + return response.data; +}; + +export const usePutLink = () => useMutation(putLink); + +// delete +export type DeleteLinkRequestParams = { studyId: StudyId; linkId: LinkId }; + +export const deleteLink = async ({ studyId, linkId }: DeleteLinkRequestParams) => { + const response = await axiosInstance.delete, null>( + `/api/studies/${studyId}/reference-room/links/${linkId}`, + ); + return response.data; +}; + +export const useDeleteLink = () => useMutation(deleteLink); diff --git a/frontend/src/api/links/index.ts b/frontend/src/api/links/index.ts new file mode 100644 index 000000000..801083e11 --- /dev/null +++ b/frontend/src/api/links/index.ts @@ -0,0 +1,55 @@ +import type { AxiosError } from 'axios'; +import { useInfiniteQuery } from 'react-query'; + +import { DEFAULT_LINK_QUERY_PARAM } from '@constants'; + +import type { Link, Page, Size, StudyId } from '@custom-types'; + +import axiosInstance from '@api/axiosInstance'; + +// get +export type GetLinksRequestParams = { + studyId: StudyId; + page?: Page; + size?: Size; +}; +export type GetLinksResponseData = { + links: Array; + hasNext: boolean; +}; +type PageParam = { page: Page }; +type NextPageParam = PageParam | undefined; +type GetLinksResponseDataWithPage = GetLinksResponseData & PageParam; +type UseGetInfiniteLinksParams = { studyId: StudyId }; + +const defaultParam: PageParam = { + page: DEFAULT_LINK_QUERY_PARAM.PAGE, +}; + +export const getLinks = async ({ studyId, page, size }: GetLinksRequestParams) => { + const response = await axiosInstance.get( + `/api/studies/${studyId}/reference-room/links?page=${page}&size=${size}`, + ); + return response.data; +}; + +const getLinksWithPage = + (studyId: StudyId) => + async ({ pageParam = defaultParam }): Promise => { + const data = await getLinks({ + ...pageParam, + studyId, + size: DEFAULT_LINK_QUERY_PARAM.SIZE, + }); + return { ...data, page: pageParam.page + 1 }; + }; + +export const QK_LINKS = 'infinite-study-links'; +export const useGetInfiniteLinks = ({ studyId }: UseGetInfiniteLinksParams) => { + return useInfiniteQuery([QK_LINKS, studyId], getLinksWithPage(studyId), { + getNextPageParam: (lastPage): NextPageParam => { + if (!lastPage.hasNext) return; + return { page: lastPage.page }; + }, + }); +}; diff --git a/frontend/src/api/member/index.ts b/frontend/src/api/member/index.ts new file mode 100644 index 000000000..1c6f10a8c --- /dev/null +++ b/frontend/src/api/member/index.ts @@ -0,0 +1,41 @@ +import type { AxiosError } from 'axios'; +import { QueryKey, UseQueryOptions, useQuery } from 'react-query'; + +import type { Member, StudyId, UserRole } from '@custom-types'; + +import axiosInstance from '@api/axiosInstance'; + +// study role +export type GetUserRoleRequestParams = { + studyId: StudyId; +}; +export type GetUserRoleResponseData = { + role: UserRole; +}; +type UseGetUserRole = GetUserRoleRequestParams & { + options?: Omit< + UseQueryOptions, + 'queryKey' | 'queryFn' + >; +}; + +export const getUserRole = async ({ studyId }: GetUserRoleRequestParams) => { + const response = await axiosInstance.get(`/api/members/me/role?study-id=${studyId}`); + return response.data; +}; + +export const useGetUserRole = ({ studyId, options }: UseGetUserRole) => + useQuery('my-role', () => getUserRole({ studyId }), options); + +// user info +export type GetUserInformationResponseData = Member; + +export const getUserInformation = async () => { + const response = await axiosInstance.get(`/api/members/me`); + return response.data; +}; + +export const useGetUserInformation = () => + useQuery('user-info', getUserInformation, { + enabled: false, + }); diff --git a/frontend/src/api/my-studies/index.ts b/frontend/src/api/my-studies/index.ts new file mode 100644 index 000000000..71f000eea --- /dev/null +++ b/frontend/src/api/my-studies/index.ts @@ -0,0 +1,20 @@ +import type { AxiosError } from 'axios'; +import { useQuery } from 'react-query'; + +import type { MyStudy } from '@custom-types'; + +import axiosInstance from '@api/axiosInstance'; + +// get +export type GetMyStudiesResponseData = { + studies: Array; +}; + +export const getMyStudies = async () => { + const response = await axiosInstance.get(`/api/my/studies`); + return response.data; +}; + +export const useGetMyStudies = () => { + return useQuery('my-studies', getMyStudies); +}; diff --git a/frontend/src/api/my-study/index.ts b/frontend/src/api/my-study/index.ts new file mode 100644 index 000000000..14a572af0 --- /dev/null +++ b/frontend/src/api/my-study/index.ts @@ -0,0 +1,20 @@ +import type { AxiosError, AxiosResponse } from 'axios'; +import { useMutation } from 'react-query'; + +import type { StudyId } from '@custom-types'; + +import axiosInstance from '@api/axiosInstance'; + +// post - join study +export type PostMyStudyRequestParams = { + studyId: StudyId; +}; + +export const postMyStudy = async ({ studyId }: PostMyStudyRequestParams) => { + const response = await axiosInstance.post, PostMyStudyRequestParams>( + `/api/studies/${studyId}/members`, + ); + return response.data; +}; + +export const usePostMyStudy = () => useMutation(postMyStudy); diff --git a/frontend/src/api/postJoiningStudy.ts b/frontend/src/api/postJoiningStudy.ts deleted file mode 100644 index 51f4443d7..000000000 --- a/frontend/src/api/postJoiningStudy.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { AxiosResponse } from 'axios'; - -import type { PostJoiningStudyRequestParams } from '@custom-types'; - -import { axiosInstance } from '@api'; - -const postJoiningStudy = async ({ studyId }: PostJoiningStudyRequestParams) => { - const response = await axiosInstance.post, PostJoiningStudyRequestParams>( - `/api/studies/${studyId}`, - ); - return response.data; -}; - -export default postJoiningStudy; diff --git a/frontend/src/api/postLogin.ts b/frontend/src/api/postLogin.ts deleted file mode 100644 index d1417c5e3..000000000 --- a/frontend/src/api/postLogin.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { AxiosResponse } from 'axios'; - -import type { PostLoginRequestParams, PostLoginResponseData } from '@custom-types'; - -import { axiosInstance } from '@api'; - -const postLogin = async ({ code }: PostLoginRequestParams) => { - const response = await axiosInstance.post< - PostLoginResponseData, - AxiosResponse, - PostLoginRequestParams - >(`/api/auth/login?code=${code}`); - return response.data; -}; - -export default postLogin; diff --git a/frontend/src/api/postNewStudy.ts b/frontend/src/api/postNewStudy.ts deleted file mode 100644 index dbb6aba1f..000000000 --- a/frontend/src/api/postNewStudy.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { AxiosResponse } from 'axios'; - -import type { PostNewStudyRequestBody } from '@custom-types'; - -import { axiosInstance } from '@api'; - -const postNewStudy = async (data: PostNewStudyRequestBody) => { - const response = await axiosInstance.post, PostNewStudyRequestBody>(`/api/studies`, data); - return response.data; -}; - -export default postNewStudy; diff --git a/frontend/src/api/postReview.ts b/frontend/src/api/postReview.ts deleted file mode 100644 index cb1e150f4..000000000 --- a/frontend/src/api/postReview.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { AxiosResponse } from 'axios'; - -import { PostReviewRequestBody, PostReviewRequestVariables } from '@custom-types'; - -import { axiosInstance } from '@api'; - -const postReview = async ({ studyId, content }: PostReviewRequestVariables) => { - const response = await axiosInstance.post, PostReviewRequestBody>( - `/api/studies/${studyId}/reviews`, - { - content, - }, - ); - return response.data; -}; - -export default postReview; diff --git a/frontend/src/api/putReview.ts b/frontend/src/api/putReview.ts deleted file mode 100644 index d3fadeab0..000000000 --- a/frontend/src/api/putReview.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { AxiosResponse } from 'axios'; - -import type { PutReviewRequestBody, PutReviewRequestVariables } from '@custom-types'; - -import { axiosInstance } from '@api'; - -const putReview = async ({ studyId, reviewId, content }: PutReviewRequestVariables) => { - const response = await axiosInstance.put, PutReviewRequestBody>( - `/api/studies/${studyId}/reviews/${reviewId}`, - { - content, - }, - ); - return response.data; -}; - -export default putReview; diff --git a/frontend/src/api/review/index.ts b/frontend/src/api/review/index.ts new file mode 100644 index 000000000..a43dd5ad4 --- /dev/null +++ b/frontend/src/api/review/index.ts @@ -0,0 +1,64 @@ +import type { AxiosError, AxiosResponse } from 'axios'; +import { useMutation } from 'react-query'; + +import type { ReviewId, StudyId } from '@custom-types'; + +import axiosInstance from '@api/axiosInstance'; + +// post +export type PostReviewRequestParams = { + studyId: StudyId; +}; +export type PostReviewRequestBody = { + content: string; +}; +export type PostReviewRequestVariables = PostReviewRequestParams & PostReviewRequestBody; + +export const postReview = async ({ studyId, content }: PostReviewRequestVariables) => { + const response = await axiosInstance.post, PostReviewRequestBody>( + `/api/studies/${studyId}/reviews`, + { + content, + }, + ); + return response.data; +}; + +export const usePostReview = () => useMutation(postReview); + +// patch +export type PutReviewRequestParams = { + studyId: number; + reviewId: number; +}; +export type PutReviewRequestBody = { + content: string; +}; +export type PutReviewRequestVariables = PutReviewRequestParams & PutReviewRequestBody; + +export const putReview = async ({ studyId, reviewId, content }: PutReviewRequestVariables) => { + const response = await axiosInstance.put, PutReviewRequestBody>( + `/api/studies/${studyId}/reviews/${reviewId}`, + { + content, + }, + ); + return response.data; +}; + +export const usePutReview = () => useMutation(putReview); + +// delete +export type DeleteReviewRequestBody = { + studyId: StudyId; + reviewId: ReviewId; +}; + +export const deleteReview = async ({ studyId, reviewId }: DeleteReviewRequestBody) => { + const response = await axiosInstance.delete, DeleteReviewRequestBody>( + `/api/studies/${studyId}/reviews/${reviewId}`, + ); + return response.data; +}; + +export const useDeleteReview = () => useMutation(deleteReview); diff --git a/frontend/src/api/reviews/index.ts b/frontend/src/api/reviews/index.ts new file mode 100644 index 000000000..a582ac59f --- /dev/null +++ b/frontend/src/api/reviews/index.ts @@ -0,0 +1,29 @@ +import type { AxiosError } from 'axios'; +import { useQuery } from 'react-query'; + +import type { StudyReview } from '@custom-types'; + +import axiosInstance from '@api/axiosInstance'; + +export const QK_STUDY_REVIEWS = 'study-reviews'; + +// get +export type GetReviewsRequestParams = { + studyId: number; + size?: number; +}; +export type GetReviewsResponseData = { + reviews: Array; + totalCount: number; +}; + +export const getStudyReviews = async ({ studyId, size }: GetReviewsRequestParams) => { + const url = size ? `/api/studies/${studyId}/reviews?size=${size}` : `/api/studies/${studyId}/reviews`; + const response = await axiosInstance.get(url); + return response.data; +}; + +export const useGetStudyReviews = (studyId: number, size?: number) => { + const queryKey = size ? [QK_STUDY_REVIEWS, studyId] : [QK_STUDY_REVIEWS, studyId, 'all']; + return useQuery(queryKey, () => getStudyReviews({ studyId, size })); +}; diff --git a/frontend/src/api/studies/index.ts b/frontend/src/api/studies/index.ts new file mode 100644 index 000000000..ff62d6a84 --- /dev/null +++ b/frontend/src/api/studies/index.ts @@ -0,0 +1,66 @@ +import type { AxiosError } from 'axios'; +import { useInfiniteQuery } from 'react-query'; + +import { DEFAULT_STUDY_CARD_QUERY_PARAM } from '@constants'; + +import type { Page, Size, Study, TagInfo } from '@custom-types'; + +import axiosInstance from '@api/axiosInstance'; + +// get +export type GetStudiesRequestParams = { + page?: Page; + size?: Size; + title: string; + selectedFilters: Array; +}; +export type GetStudiesResponseData = { + studies: Array; + hasNext: boolean; +}; +type PageParam = { page: number }; +type NextPageParam = PageParam | undefined; +type GetStudiesResponseDataWithPage = GetStudiesResponseData & { + page: number; +}; +type UseGetInfiniteStudiesParams = Pick; + +const { PAGE, SIZE } = DEFAULT_STUDY_CARD_QUERY_PARAM; +const defaultParam: PageParam = { + page: DEFAULT_STUDY_CARD_QUERY_PARAM.PAGE, +}; + +export const getStudies = async ({ page = PAGE, size = SIZE, title, selectedFilters }: GetStudiesRequestParams) => { + const tagParams = selectedFilters.map(({ id, categoryName }) => `&${categoryName}=${id}`).join(''); + const titleParams = title && `&title=${title}`; + + const response = await axiosInstance.get( + `/api/studies/search?page=${page}&size=${size}${titleParams}${tagParams}`, + ); + return response.data; +}; + +const getStudiesWithPage = + (title: string, selectedFilters: Array) => + async ({ pageParam = defaultParam }): Promise => { + const data = await getStudies({ + ...pageParam, + title, + selectedFilters, + size: DEFAULT_STUDY_CARD_QUERY_PARAM.SIZE, + }); + return { ...data, page: pageParam.page + 1 }; + }; + +export const useGetInfiniteStudies = ({ title, selectedFilters }: UseGetInfiniteStudiesParams) => { + return useInfiniteQuery( + ['infinite-scroll-searched-study-list', title, selectedFilters], + getStudiesWithPage(title, selectedFilters), + { + getNextPageParam: (lastPage): NextPageParam => { + if (!lastPage.hasNext) return; + return { page: lastPage.page }; + }, + }, + ); +}; diff --git a/frontend/src/api/study/index.ts b/frontend/src/api/study/index.ts new file mode 100644 index 000000000..e59158185 --- /dev/null +++ b/frontend/src/api/study/index.ts @@ -0,0 +1,46 @@ +import type { AxiosError, AxiosResponse } from 'axios'; +import { useMutation, useQuery } from 'react-query'; + +import type { MakeOptional, StudyDetail, TagId } from '@custom-types'; + +import axiosInstance from '@api/axiosInstance'; + +export const QK_STUDY_DETAIL = 'study-detail'; + +// post +export type PostStudyRequestBody = { + tagIds?: Array; + thumbnail: string; +} & MakeOptional< + Pick< + StudyDetail, + 'title' | 'excerpt' | 'description' | 'maxMemberCount' | 'enrollmentEndDate' | 'startDate' | 'endDate' | 'owner' + >, + 'maxMemberCount' | 'enrollmentEndDate' | 'endDate' | 'owner' +>; + +export const postStudy = async (newStudy: PostStudyRequestBody) => { + const response = await axiosInstance.post, PostStudyRequestBody>(`/api/studies`, newStudy); + return response.data; +}; + +export const usePostStudy = () => { + return useMutation(postStudy); +}; + +// get +export type GetStudyRequestParams = { + studyId: number; +}; +export type GetStudyResponseData = StudyDetail; + +export const getStudy = async ({ studyId }: GetStudyRequestParams) => { + const response = await axiosInstance.get(`/api/studies/${studyId}`); + return response.data; +}; + +export const useGetStudy = ({ studyId }: GetStudyRequestParams) => { + return useQuery([QK_STUDY_DETAIL, studyId], () => getStudy({ studyId })); +}; + +// put diff --git a/frontend/src/api/tags/index.ts b/frontend/src/api/tags/index.ts new file mode 100644 index 000000000..c97ec6805 --- /dev/null +++ b/frontend/src/api/tags/index.ts @@ -0,0 +1,20 @@ +import type { AxiosError } from 'axios'; +import { useQuery } from 'react-query'; + +import type { Tag } from '@custom-types'; + +import axiosInstance from '@api/axiosInstance'; + +// get +export type GetTagsResponseData = { + tags: Array; +}; + +export const getTags = async () => { + const response = await axiosInstance.get(`/api/tags`); + return response.data; +}; + +export const useGetTags = () => { + return useQuery('filters', getTags); +}; diff --git a/frontend/src/auth/accessToken.ts b/frontend/src/auth/accessToken.ts index f4b64207a..0e19e7fea 100644 --- a/frontend/src/auth/accessToken.ts +++ b/frontend/src/auth/accessToken.ts @@ -2,7 +2,7 @@ import { AxiosError } from 'axios'; import { API_ERROR } from '@constants'; -import { deleteRefreshToken, getAccessToken } from '@api'; +import { deleteLogout, getRefresh } from '@api/auth'; class AccessTokenController { private static ACCESS_TOKEN_KEY = 'accessToken'; @@ -34,7 +34,7 @@ class AccessTokenController { private static async fetchLogout() { try { - await deleteRefreshToken(); + await deleteLogout(); this.removeAccessToken(); } catch (error) { alert('로그아웃에 실패했습니다. :('); @@ -44,7 +44,7 @@ class AccessTokenController { static async fetchAccessTokenWithRefresh() { try { - const data = await getAccessToken(); + const data = await getRefresh(); this.setAccessToken(data.accessToken); this.setTokenExpiredMsTime(data.expiredTime); diff --git a/frontend/src/components/arrow-button/ArrowButton.tsx b/frontend/src/components/arrow-button/ArrowButton.tsx index 5509c09bb..0b896e99b 100644 --- a/frontend/src/components/arrow-button/ArrowButton.tsx +++ b/frontend/src/components/arrow-button/ArrowButton.tsx @@ -1,4 +1,5 @@ import * as S from '@components/arrow-button/ArrowButton.style'; +import { LeftDirectionSvg, RightDirectionSvg } from '@components/svg'; export type SlideButtonProps = { direction: 'right' | 'left'; @@ -6,40 +7,6 @@ export type SlideButtonProps = { onSlideButtonClick: React.MouseEventHandler; }; -const BsChevronLeft = () => ( - - - -); - -const BsChevronRight = () => ( - - - -); - const SlideButton: React.FC = ({ direction, ariaLabel, @@ -47,7 +14,7 @@ const SlideButton: React.FC = ({ }) => { return ( - {direction === 'right' ? : } + {direction === 'right' ? : } ); }; diff --git a/frontend/src/components/avatar/Avatar.style.tsx b/frontend/src/components/avatar/Avatar.style.tsx index 0587d4c3b..fd497ad1c 100644 --- a/frontend/src/components/avatar/Avatar.style.tsx +++ b/frontend/src/components/avatar/Avatar.style.tsx @@ -2,7 +2,6 @@ import { SerializedStyles, css } from '@emotion/react'; import styled from '@emotion/styled'; import type { AvatarProps } from '@components/avatar/Avatar'; -import Image from '@components/image/Image'; const dynamicSize = { xs: css` @@ -51,10 +50,3 @@ export const Avatar = styled.div` ${dynamicImageContainer} `; - -export const AvatarImage = styled(Image)` - width: 100%; - height: 100%; - object-fit: cover; - object-position: center; -`; diff --git a/frontend/src/components/avatar/Avatar.tsx b/frontend/src/components/avatar/Avatar.tsx index f31774eca..9ab078097 100644 --- a/frontend/src/components/avatar/Avatar.tsx +++ b/frontend/src/components/avatar/Avatar.tsx @@ -1,6 +1,7 @@ import type { MakeOptional } from '@custom-types'; import * as S from '@components/avatar/Avatar.style'; +import CenterImage from '@components/center-image/CenterImage'; export type AvatarProps = { profileImg: string; @@ -13,7 +14,7 @@ type OptionalAvatarProps = MakeOptional; const Avatar: React.FC = ({ size = 'sm', profileImg, profileAlt }) => { return ( - + ); }; diff --git a/frontend/src/components/card/Card.style.tsx b/frontend/src/components/card/Card.style.tsx index 8bf1be900..b3840af19 100644 --- a/frontend/src/components/card/Card.style.tsx +++ b/frontend/src/components/card/Card.style.tsx @@ -1,8 +1,6 @@ import { css } from '@emotion/react'; import styled from '@emotion/styled'; -import Image from '@components/image/Image'; - export const Card = styled.div` ${({ theme }) => css` display: flex; @@ -27,14 +25,6 @@ export const ImageContainer = styled.div` overflow: hidden; `; -export const CardImage = styled(Image)` - width: 100%; - height: 100%; - - object-fit: cover; - object-position: center; -`; - export const Content = styled.div` padding: 8px 8px 12px; diff --git a/frontend/src/components/card/Card.tsx b/frontend/src/components/card/Card.tsx index aa859a833..eca297bb7 100644 --- a/frontend/src/components/card/Card.tsx +++ b/frontend/src/components/card/Card.tsx @@ -1,6 +1,7 @@ import { ReactNode } from 'react'; import * as S from '@components/card/Card.style'; +import CenterImage from '@components/center-image/CenterImage'; export type CardProps = { thumbnailUrl: string; @@ -14,7 +15,7 @@ const Card: React.FC = ({ thumbnailUrl, thumbnailAlt, title, excerpt, return ( - + {title} diff --git a/frontend/src/components/center-image/CenterImage.style.tsx b/frontend/src/components/center-image/CenterImage.style.tsx new file mode 100644 index 000000000..e59dfe5c8 --- /dev/null +++ b/frontend/src/components/center-image/CenterImage.style.tsx @@ -0,0 +1,9 @@ +import styled from '@emotion/styled'; + +export const CenterImage = styled.img` + width: 100%; + height: 100%; + + object-fit: cover; + object-position: center; +`; diff --git a/frontend/src/components/center-image/CenterImage.tsx b/frontend/src/components/center-image/CenterImage.tsx new file mode 100644 index 000000000..df803b740 --- /dev/null +++ b/frontend/src/components/center-image/CenterImage.tsx @@ -0,0 +1,19 @@ +import notFoundImage from '@assets/images/no-image-found.png'; + +import * as S from '@components/center-image/CenterImage.style'; + +export type ImageProps = { + className?: string; + src?: string | null; + alt: string; +}; + +const CenterImage: React.FC = ({ className, src, alt }) => { + const handleImageError = ({ currentTarget }: React.SyntheticEvent) => { + currentTarget.src = notFoundImage; + }; + + return ; +}; + +export default CenterImage; diff --git a/frontend/src/components/checkbox/Checkbox.tsx b/frontend/src/components/checkbox/Checkbox.tsx index 0fd5a7141..79a5290b8 100644 --- a/frontend/src/components/checkbox/Checkbox.tsx +++ b/frontend/src/components/checkbox/Checkbox.tsx @@ -1,4 +1,4 @@ -import * as S from './Checkbox.style'; +import * as S from '@components/checkbox/Checkbox.style'; export type CheckboxProps = React.HTMLProps; diff --git a/frontend/src/components/drop-down-box/DropDownBox.stories.tsx b/frontend/src/components/drop-down-box/DropDownBox.stories.tsx new file mode 100644 index 000000000..16d613270 --- /dev/null +++ b/frontend/src/components/drop-down-box/DropDownBox.stories.tsx @@ -0,0 +1,37 @@ +import type { Story } from '@storybook/react'; +import { useState } from 'react'; + +import { css } from '@emotion/react'; + +import tw from '@utils/tw'; + +import DropDownBox from '@components/drop-down-box/DropDownBox'; +import type { DropDownBoxProps } from '@components/drop-down-box/DropDownBox'; + +export default { + title: 'Components/DropDownBox', + component: DropDownBox, +}; + +const Template: Story = props => { + const [isOpen, setIsOpen] = useState(false); + + const handleButtonClick = () => { + setIsOpen(prev => !prev); + }; + + return ( +
+ + {isOpen && setIsOpen(false)} />} +
+ ); +}; + +export const Default = Template.bind({}); +Default.args = { + children: 'hihi', + top: '30px', + left: '0', +}; +Default.parameters = { controls: { exclude: ['buttonRef', 'onClose'] } }; diff --git a/frontend/src/layout/header/components/drop-down-box/DropDownBox.style.tsx b/frontend/src/components/drop-down-box/DropDownBox.style.tsx similarity index 74% rename from frontend/src/layout/header/components/drop-down-box/DropDownBox.style.tsx rename to frontend/src/components/drop-down-box/DropDownBox.style.tsx index e43d4dcdd..f1e54504b 100644 --- a/frontend/src/layout/header/components/drop-down-box/DropDownBox.style.tsx +++ b/frontend/src/components/drop-down-box/DropDownBox.style.tsx @@ -1,31 +1,21 @@ import { css } from '@emotion/react'; import styled from '@emotion/styled'; -import type { DropDownBoxProps } from '@layout/header/components/drop-down-box/DropDownBox'; - -export const DropDownBoxContainer = styled.div` - position: fixed; - top: 0; - left: 0; - background: trnasparent; - height: 100%; - width: 100vw; -`; +import type { DropDownBoxProps } from '@components/drop-down-box/DropDownBox'; export const DropDownBox = styled.div>` ${({ theme, top, bottom, left, right }) => css` position: absolute; - white-space: nowrap; ${top && `top: ${top};`} ${bottom && `bottom: ${bottom};`} ${left && `left: ${left};`} ${right && `right: ${right};`} - padding: 16px; + z-index: 3; + white-space: nowrap; border: 1px solid ${theme.colors.secondary.dark}; border-radius: 5px; background-color: ${theme.colors.secondary.light}; - z-index: 3; transform-origin: top; animation: slide-down 0.1s ease; diff --git a/frontend/src/components/drop-down-box/DropDownBox.tsx b/frontend/src/components/drop-down-box/DropDownBox.tsx new file mode 100644 index 000000000..11a08fe7e --- /dev/null +++ b/frontend/src/components/drop-down-box/DropDownBox.tsx @@ -0,0 +1,33 @@ +import { useEffect } from 'react'; + +import type { Noop } from '@custom-types'; + +import * as S from '@components/drop-down-box/DropDownBox.style'; + +export type DropDownBoxProps = { + className?: string; + children: React.ReactNode; + onClose: Noop; + top?: string; + bottom?: string; + left?: string; + right?: string; +}; + +const DropDownBox: React.FC = ({ className, children, onClose: handleClose, ...positions }) => { + useEffect(() => { + // 이벤트 전파가 끝나기 전에 document에 click event listener가 붙기 때문에 + // click event listener를 add하는 일을 다음 frame으로 늦춘다 + // Test: https://codepen.io/airman5573/pen/qBopRpO + requestAnimationFrame(() => document.body.addEventListener('click', handleClose)); + return () => document.body.removeEventListener('click', handleClose); + }, []); + + return ( + + {children} + + ); +}; + +export default DropDownBox; diff --git a/frontend/src/components/image/Image.tsx b/frontend/src/components/image/Image.tsx deleted file mode 100644 index c2cf1f68b..000000000 --- a/frontend/src/components/image/Image.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import notFoundImage from '@assets/images/no-image-found.png'; - -export type ImageProps = { - className?: string; - src: string; - alt: string; -}; - -const Image: React.FC = ({ className, src, alt }) => { - const handleImageError = ({ currentTarget }: React.SyntheticEvent) => { - currentTarget.src = notFoundImage; - }; - - return {alt}; -}; - -export default Image; diff --git a/frontend/src/components/infinite-scroll/InfiniteScroll.tsx b/frontend/src/components/infinite-scroll/InfiniteScroll.tsx index a6a392d91..e0b56bac7 100644 --- a/frontend/src/components/infinite-scroll/InfiniteScroll.tsx +++ b/frontend/src/components/infinite-scroll/InfiniteScroll.tsx @@ -1,7 +1,9 @@ import { useEffect, useMemo, useRef } from 'react'; +import type { Noop } from '@custom-types'; + type InfiniteScrollProps = { - onContentLoad: () => void; + onContentLoad: Noop; observingCondition: boolean; children: React.ReactNode; }; diff --git a/frontend/src/pages/create-study-page/components/input/Input.style.tsx b/frontend/src/components/input/Input.style.tsx similarity index 81% rename from frontend/src/pages/create-study-page/components/input/Input.style.tsx rename to frontend/src/components/input/Input.style.tsx index 47e05ce91..e639971c0 100644 --- a/frontend/src/pages/create-study-page/components/input/Input.style.tsx +++ b/frontend/src/components/input/Input.style.tsx @@ -2,8 +2,7 @@ import { css } from '@emotion/react'; import styled from '@emotion/styled'; const invalidInputStyle = () => css` - border: none !important; - outline: 2px solid red !important; + border: 1px solid red !important; `; type InputProps = { @@ -12,8 +11,7 @@ type InputProps = { export const Input = styled.input` ${({ theme, isValid }) => css` - box-shadow: 0 0 0 transparent; - border-radius: 4px; + border-radius: 5px; border: 1px solid ${theme.colors.secondary.base}; background-color: ${theme.colors.white}; padding: 4px 8px; diff --git a/frontend/src/components/modal/Modal.stories.tsx b/frontend/src/components/modal/Modal.stories.tsx new file mode 100644 index 000000000..8483295db --- /dev/null +++ b/frontend/src/components/modal/Modal.stories.tsx @@ -0,0 +1,41 @@ +import type { Story } from '@storybook/react'; +import { useState } from 'react'; + +import { css } from '@emotion/react'; + +import Modal from '@components/modal/Modal'; +import type { ModalProps } from '@components/modal/Modal'; + +export default { + title: 'Components/Modal', + component: Modal, +}; + +const Template: Story = () => { + const [isOpen, setIsOpen] = useState(false); + + return ( + <> + + {isOpen && ( + setIsOpen(false)}> +
+ +
+
+ )} + + ); +}; + +export const Default = Template.bind({}); +Default.args = {}; diff --git a/frontend/src/components/modal/Modal.style.tsx b/frontend/src/components/modal/Modal.style.tsx new file mode 100644 index 000000000..703661826 --- /dev/null +++ b/frontend/src/components/modal/Modal.style.tsx @@ -0,0 +1,25 @@ +import { css } from '@emotion/react'; +import styled from '@emotion/styled'; + +export const ModalContainer = styled.div``; +export const ModalOutside = styled.div` + ${({ theme }) => css` + display: flex; + justify-content: center; + align-items: center; + + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 10; + + width: 100vw; + height: 100%; + + background-color: ${`${theme.colors.secondary.base}33`}; + `} +`; + +export const ModalContent = styled.div``; diff --git a/frontend/src/components/modal/Modal.tsx b/frontend/src/components/modal/Modal.tsx new file mode 100644 index 000000000..7e1dc18d1 --- /dev/null +++ b/frontend/src/components/modal/Modal.tsx @@ -0,0 +1,23 @@ +import { createPortal } from 'react-dom'; + +import * as S from '@components/modal/Modal.style'; + +export type ModalProps = { + children: React.ReactNode; + onModalOutsideClick: React.MouseEventHandler; +}; + +const Modal: React.FC = ({ children, onModalOutsideClick: handleModalOutsideClick }) => { + return ( + + e.stopPropagation()}>{children} + + ); +}; + +const modalElement = document.querySelector('#modal') as Element; +const ModalPortal = ({ children, onModalOutsideClick: handleModalOutsideClick }: ModalProps) => { + return createPortal({children}, modalElement); +}; + +export default ModalPortal; diff --git a/frontend/src/components/svg/index.tsx b/frontend/src/components/svg/index.tsx new file mode 100644 index 000000000..306f820de --- /dev/null +++ b/frontend/src/components/svg/index.tsx @@ -0,0 +1,199 @@ +export const LeftDirectionSvg = () => ( + + + +); + +export const RightDirectionSvg = () => ( + + + +); + +export const LoginSvg = () => ( + + + + +); + +export const LogoutSvg = () => ( + + + + +); + +export const BookmarkSvg = () => ( + + + +); + +export const SearchSvg = () => ( + + + + +); + +export const CrownSvg = () => ( + + + + + +); + +export const PencilSvg = () => ( + + + +); + +export const TrashCanSvg = () => ( + + + +); + +export const MeatballMenuSvg = () => ( + + + +); + +export const RightUpArrowSvg = () => ( + + + + +); + +export const PlusSvg = () => ( + + + +); diff --git a/frontend/src/constants.ts b/frontend/src/constants.ts index e7b47faf3..0819f4d62 100644 --- a/frontend/src/constants.ts +++ b/frontend/src/constants.ts @@ -19,9 +19,10 @@ export const DEFAULT_STUDY_CARD_QUERY_PARAM = { PAGE: 0, SIZE: 12, }; - -export const QK_FETCH_STUDY_DETAIL = 'fetch_study_detail'; -export const QK_FETCH_STUDY_REVIEWS = 'fetch_study_reviews'; +export const DEFAULT_LINK_QUERY_PARAM = { + PAGE: 0, + SIZE: 9, +}; export const DEFAULT_VISIBLE_STUDY_MEMBER_CARD_COUNT = 6; export const DEFAULT_LOAD_STUDY_REVIEW_COUNT = 6; @@ -35,15 +36,6 @@ export const BREAK_POINTS = { xxl: 1400, }; -export const VALIDATIONS = { - EXCERPT: { - LENGTH: { - MIN: 1, - MAX: 50, - }, - }, -}; - export const EXCERPT_LENGTH = { MIN: { VALUE: 1, @@ -118,3 +110,43 @@ export const REVIEW_LENGTH = { }, }, }; + +export const LINK_URL_LENGTH = { + MIN: { + VALUE: 1, + get MESSAGE() { + return `${this.VALUE}글자 이상이어야 합니다`; + }, + }, + MAX: { + VALUE: 1000, + get MESSAGE() { + return `${this.VALUE}글자까지 입력할 수 있습니다`; + }, + }, + FORMAT: { + TEST(text: string) { + return /[(http(s)?):\/\/(www\.)?a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)/.test( + text, + ); + }, + get MESSAGE() { + return 'URL을 입력해주세요.'; + }, + }, +}; + +export const LINK_DESCRIPTION_LENGTH = { + MIN: { + VALUE: 1, + get MESSAGE() { + return `${this.VALUE}글자 이상이어야 합니다`; + }, + }, + MAX: { + VALUE: 40, + get MESSAGE() { + return `${this.VALUE}글자까지 입력할 수 있습니다`; + }, + }, +}; diff --git a/frontend/src/custom-types/common.d.ts b/frontend/src/custom-types/common.d.ts index de48842fa..37bcd96c4 100644 --- a/frontend/src/custom-types/common.d.ts +++ b/frontend/src/custom-types/common.d.ts @@ -7,6 +7,7 @@ declare namespace NodeJS { export type ProcessEnv = { API_URL: string; CLIENT_ID: string; + LINK_PREVIEW_API_URL: string; NODE_ENV: 'development' | 'production'; }; } diff --git a/frontend/src/custom-types/index.ts b/frontend/src/custom-types/index.ts index 06f0c851f..b87501a51 100644 --- a/frontend/src/custom-types/index.ts +++ b/frontend/src/custom-types/index.ts @@ -8,6 +8,8 @@ export type Required = T & { export type MakeRequired = Pick> & Required; +export type Noop = () => void; + export type oneToNine = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9; export type d = oneToNine | 0; export type DD = `0${oneToNine}` | `1${d}` | `2${d}` | `3${0 | 1}`; @@ -22,6 +24,9 @@ export type TagId = number; export type ReviewId = number; export type MemberId = number; export type CategoryId = number; +export type LinkId = number; +export type Page = number; +export type Size = number; export type Study = { id: StudyId; @@ -73,11 +78,6 @@ export type StudyReview = { content: string; }; -export type TagInfo = { - id: TagId; - categoryName: string; -}; - export type Tag = { id: TagId; name: string; @@ -87,6 +87,7 @@ export type Tag = { name: string; }; }; +export type TagInfo = Pick & { categoryName: Tag['category']['name'] }; export type StudyStatus = 'PREPARE' | 'IN_PROGRESS' | 'DONE'; @@ -99,92 +100,11 @@ export type MyStudy = Pick< export type UserRole = 'OWNER' | 'MEMBER' | 'NON_MEMBER'; -export type GetStudyDetailRequestParams = { - studyId: number; -}; -export type GetStudyDetailResponseData = StudyDetail; - -export type GetStudyListRequestParams = { - page?: number; - size?: number; - title: string; - selectedFilters: Array; -}; -export type GetStudyListResponseData = { - studies: Array; - hasNext: boolean; -}; - -export type GetTagListResponseData = { - tags: Array; -}; - -export type PostLoginRequestParams = { - code: string; -}; -export type PostLoginResponseData = { - accessToken: string; - expiredTime: number; -}; -export type GetTokenResponseData = { - accessToken: string; - expiredTime: number; -}; - -export type GetReviewResponseData = { - reviews: Array; - totalCount: number; -}; -export type GetReviewRequestParams = { - studyId: number; - size?: number; -}; -export type PostReviewRequestParams = { - studyId: StudyId; -}; -export type PostReviewRequestBody = { - content: string; -}; -export type PostReviewRequestVariables = PostReviewRequestParams & PostReviewRequestBody; - -export type PatchReviewRequestParams = { - studyId: number; - reviewId: number; -}; -export type PutReviewRequestBody = { - content: string; -}; -export type PutReviewRequestVariables = PatchReviewRequestParams & PutReviewRequestBody; - -export type DeleteReviewRequestBody = { - studyId: StudyId; - reviewId: ReviewId; -}; - -export type GetMyStudyResponseData = { - studies: Array; -}; - -export type PostJoiningStudyRequestParams = { - studyId: StudyId; -}; - -export type PostNewStudyRequestBody = { - tagIds?: Array; - thumbnail: string; -} & MakeOptional< - Pick< - StudyDetail, - 'title' | 'excerpt' | 'description' | 'maxMemberCount' | 'enrollmentEndDate' | 'startDate' | 'endDate' | 'owner' - >, - 'maxMemberCount' | 'enrollmentEndDate' | 'endDate' | 'owner' ->; - -export type GetUserRoleRequestParams = { - studyId: StudyId; -}; -export type GetUserRoleResponseData = { - role: UserRole; +export type Link = { + id: number; + author: Member; + linkUrl: string; + description: string; + createdDate: DateYMD; + lastModifiedDate: DateYMD; }; - -export type GetUserInformationResponseData = Member; diff --git a/frontend/src/hooks/useAuth.ts b/frontend/src/hooks/useAuth.ts index 3b08c2c25..14825268e 100644 --- a/frontend/src/hooks/useAuth.ts +++ b/frontend/src/hooks/useAuth.ts @@ -1,8 +1,6 @@ -import { AxiosError } from 'axios'; import { useContext } from 'react'; -import { useMutation } from 'react-query'; -import { deleteRefreshToken } from '@api'; +import { useDeleteLogout } from '@api/auth'; import AccessTokenController from '@auth/accessToken'; @@ -14,7 +12,7 @@ export const useAuth = () => { const { isLoggedIn, setIsLoggedIn } = useContext(LoginContext); const { fetchUserInfo } = useUserInfo(); - const { mutate } = useMutation(deleteRefreshToken); + const { mutate } = useDeleteLogout(); const login = (accesssToken: string) => { AccessTokenController.setAccessToken(accesssToken); diff --git a/frontend/src/hooks/useForm.tsx b/frontend/src/hooks/useForm.tsx index 91b84b6cf..9624e85a4 100644 --- a/frontend/src/hooks/useForm.tsx +++ b/frontend/src/hooks/useForm.tsx @@ -9,7 +9,7 @@ type FieldErrors = Record; type FieldValidationResult = { hasError: boolean; errorMessage?: string }; type ValidateHandler = (val: any) => FieldValidationResult; type ChangeHandler = React.ChangeEventHandler; -type FieldElement = HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement; +export type FieldElement = HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement; type ValidationMode = 'change' | 'submit'; type Field = { diff --git a/frontend/src/hooks/useUserInfo.ts b/frontend/src/hooks/useUserInfo.ts index b0443c97e..462bbf444 100644 --- a/frontend/src/hooks/useUserInfo.ts +++ b/frontend/src/hooks/useUserInfo.ts @@ -1,23 +1,12 @@ -import type { AxiosError } from 'axios'; import { useContext, useEffect } from 'react'; -import { useQuery } from 'react-query'; -import type { GetUserInformationResponseData } from '@custom-types'; - -import { getUserInformation } from '@api'; +import { useGetUserInformation } from '@api/member'; import { UserInfoContext } from '@context/userInfo/UserInfoProvider'; export const useUserInfo = () => { const { userInfo, setUserInfo } = useContext(UserInfoContext); - const { - data, - refetch: fetchUserInfo, - isError, - isSuccess, - } = useQuery('user-info', getUserInformation, { - enabled: false, - }); + const { data, refetch: fetchUserInfo, isError, isSuccess } = useGetUserInformation(); useEffect(() => { if (!data || isError || !isSuccess) return; diff --git a/frontend/src/layout/header/Header.style.tsx b/frontend/src/layout/header/Header.style.tsx index 359bdbfe1..3e8015f86 100644 --- a/frontend/src/layout/header/Header.style.tsx +++ b/frontend/src/layout/header/Header.style.tsx @@ -20,7 +20,7 @@ export const SearchBarContainer = styled.div` } `; -export const Row = styled.header` +export const Header = styled.header` ${({ theme }) => css` display: flex; justify-content: space-between; @@ -49,6 +49,10 @@ export const Nav = styled.nav` column-gap: 16px; `; +export const AvatarContainer = styled.div` + position: relative; +`; + export const AvatarButton = styled.button` border: none; background-color: transparent; diff --git a/frontend/src/layout/header/Header.tsx b/frontend/src/layout/header/Header.tsx index 08495b7eb..6268327b6 100644 --- a/frontend/src/layout/header/Header.tsx +++ b/frontend/src/layout/header/Header.tsx @@ -3,71 +3,30 @@ import { Link, useNavigate } from 'react-router-dom'; import { PATH } from '@constants'; +import tw from '@utils/tw'; + import { useAuth } from '@hooks/useAuth'; import { useUserInfo } from '@hooks/useUserInfo'; import { SearchContext } from '@context/search/SearchProvider'; import * as S from '@layout/header/Header.style'; -import DropDownBox from '@layout/header/components/drop-down-box/DropDownBox'; import Logo from '@layout/header/components/logo/Logo'; import NavButton from '@layout/header/components/nav-button/NavButton'; import SearchBar from '@layout/header/components/search-bar/SearchBar'; import Avatar from '@components/avatar/Avatar'; +import DropDownBox from '@components/drop-down-box/DropDownBox'; +import { BookmarkSvg, LoginSvg, LogoutSvg } from '@components/svg'; export type HeaderProps = { className?: string; }; -const MdOutlineLogin = () => ( - - - - -); - -const MdOutlineLogout = () => ( - - - - -); - -const BiBookmark = () => ( - - - -); - const Header: React.FC = ({ className }) => { const { setKeyword } = useContext(SearchContext); - const [openDropDownBox, setOpenDropDownBox] = useState(false); + const [isOpenDropDownBox, setIsOpenDropDownBox] = useState(false); const navigate = useNavigate(); const { logout, isLoggedIn } = useAuth(); @@ -88,11 +47,13 @@ const Header: React.FC = ({ className }) => { logout(); }; - const handleAvatarButtonClick = () => setOpenDropDownBox(prev => !prev); - const handleOutsideDropBoxClick = () => setOpenDropDownBox(false); + const handleAvatarButtonClick = () => { + setIsOpenDropDownBox(prev => !prev); + }; + const handleDropDownBoxClose = () => setIsOpenDropDownBox(false); return ( - +
@@ -103,31 +64,33 @@ const Header: React.FC = ({ className }) => { - + 내 스터디 - - - - {openDropDownBox && ( - - - - 로그아웃 - - - )} + + + + + {isOpenDropDownBox && ( + + + + 로그아웃 + + + )} + ) : ( - + 로그인 )} - + ); }; diff --git a/frontend/src/layout/header/components/drop-down-box/DropDownBox.stories.tsx b/frontend/src/layout/header/components/drop-down-box/DropDownBox.stories.tsx deleted file mode 100644 index 2853da6f7..000000000 --- a/frontend/src/layout/header/components/drop-down-box/DropDownBox.stories.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import type { Story } from '@storybook/react'; -import { useState } from 'react'; - -import DropDownBox from '@layout/header/components/drop-down-box/DropDownBox'; -import type { DropDownBoxProps } from '@layout/header/components/drop-down-box/DropDownBox'; - -export default { - title: 'Components/DropDownBox', - component: DropDownBox, - argTypes: { - children: { controls: 'text' }, - top: { controls: 'text' }, - right: { controls: 'text' }, - left: { controls: 'text' }, - bottom: { controls: 'text' }, - }, -}; - -const Template: Story = props => { - const [isOpen, setIsOpen] = useState(false); - return ( -
- - {isOpen && setIsOpen(false)} />} -
- ); -}; - -export const Default = Template.bind({}); -Default.args = { - children: 'hihi', - top: '40px', - left: '15px', -}; diff --git a/frontend/src/layout/header/components/drop-down-box/DropDownBox.tsx b/frontend/src/layout/header/components/drop-down-box/DropDownBox.tsx deleted file mode 100644 index 9bbf2e273..000000000 --- a/frontend/src/layout/header/components/drop-down-box/DropDownBox.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import * as S from '@layout/header/components/drop-down-box/DropDownBox.style'; - -export type DropDownBoxProps = { - children: React.ReactNode; - onOutOfBoxClick: React.MouseEventHandler; - top?: string; - bottom?: string; - left?: string; - right?: string; -}; - -const DropDownBox: React.FC = ({ children, onOutOfBoxClick: handleOutOfBoxClick, ...positions }) => { - return ( - - {children} - - ); -}; - -export default DropDownBox; diff --git a/frontend/src/layout/header/components/logo/Logo.tsx b/frontend/src/layout/header/components/logo/Logo.tsx index f8131b21d..4f858f70c 100644 --- a/frontend/src/layout/header/components/logo/Logo.tsx +++ b/frontend/src/layout/header/components/logo/Logo.tsx @@ -2,13 +2,13 @@ import logoImage from '@assets/images/logo.png'; import * as S from '@layout/header/components/logo/Logo.style'; -import Image from '@components/image/Image'; +import CenterImage from '@components/center-image/CenterImage'; const Logo: React.FC = () => { return ( - 모아모아 로고 이미지 + MOAMOA diff --git a/frontend/src/layout/header/components/search-bar/SearchBar.tsx b/frontend/src/layout/header/components/search-bar/SearchBar.tsx index 090b694a9..c90e09ec1 100644 --- a/frontend/src/layout/header/components/search-bar/SearchBar.tsx +++ b/frontend/src/layout/header/components/search-bar/SearchBar.tsx @@ -1,34 +1,19 @@ import * as S from '@layout/header/components/search-bar/SearchBar.style'; +import { SearchSvg } from '@components/svg'; + export type SearchBarProps = { onSubmit: (e: React.FormEvent, inputName: string) => void; inputName?: string; }; -const FiSearch = () => ( - - - - -); - const SearchBar: React.FC = ({ onSubmit, inputName = 'keyword' }) => { return ( onSubmit(e, inputName)}> - + diff --git a/frontend/src/mocks/detailStudyHandlers.ts b/frontend/src/mocks/detailStudyHandlers.ts index 884b683de..10540e806 100644 --- a/frontend/src/mocks/detailStudyHandlers.ts +++ b/frontend/src/mocks/detailStudyHandlers.ts @@ -7,11 +7,13 @@ const detailStudyHandlers = [ rest.get('/api/studies/:studyId', (req, res, ctx) => { const studyId = req.params.studyId; + if (!studyId) return res(ctx.status(400), ctx.json({ message: '스터디 아이디가 없음' })); + const study = studiesJSON.studies.find(study => String(study.id) === studyId); return res(ctx.status(200), ctx.json(study)); }), - rest.post('/api/studies/:studyId', (req, res, ctx) => { + rest.post('/api/studies/:studyId/memebers', (req, res, ctx) => { // const studyId = req.params.studyId; return res(ctx.status(200)); diff --git a/frontend/src/mocks/handlers.ts b/frontend/src/mocks/handlers.ts index 4bcdafc33..bfae4d808 100644 --- a/frontend/src/mocks/handlers.ts +++ b/frontend/src/mocks/handlers.ts @@ -1,6 +1,7 @@ import { rest } from 'msw'; import detailStudyHandlers from '@mocks/detailStudyHandlers'; +import { linkHandlers } from '@mocks/linkHandlers'; import { memberHandlers } from '@mocks/memberHandlers'; import { myHandlers } from '@mocks/myHandlers'; import { reviewHandlers } from '@mocks/reviewHandler'; @@ -63,4 +64,5 @@ export const handlers = [ ...reviewHandlers, ...tokenHandlers, ...memberHandlers, + ...linkHandlers, ]; diff --git a/frontend/src/mocks/linkHandlers.ts b/frontend/src/mocks/linkHandlers.ts new file mode 100644 index 000000000..09fb4bb3f --- /dev/null +++ b/frontend/src/mocks/linkHandlers.ts @@ -0,0 +1,86 @@ +import { rest } from 'msw'; + +import linkJson from '@mocks/links.json'; +import { user } from '@mocks/memberHandlers'; + +import { PostLinkRequestBody, PutLinkRequestBody } from '@api/link'; + +export const linkHandlers = [ + rest.get('/api/studies/:studyId/reference-room/links', (req, res, ctx) => { + const page = req.url.searchParams.get('page'); + const size = req.url.searchParams.get('size'); + if ((size === null && page !== null) || (size !== null && page === null)) { + return res(ctx.status(400), ctx.json({ message: 'size혹은 page가 없습니다' })); + } + + const sizeNum = Number(size); + const pageNum = Number(page); + const startIndex = pageNum * sizeNum; + const endIndexExclusive = startIndex + sizeNum; + + const { links } = linkJson; + return res( + ctx.status(200), + ctx.json({ + links: links.slice(startIndex, endIndexExclusive), + hasNext: endIndexExclusive < linkJson.links.length, + }), + ); + }), + rest.post('/api/studies/:studyId/reference-room/links', (req, res, ctx) => { + const { linkUrl, description } = req.body; + if (!linkUrl || !description) + return res(ctx.status(400), ctx.json({ message: 'linkeUrl 또는 description이 없음' })); + + const { links } = linkJson; + linkJson.links = [ + { + id: Math.random() * 100000 + 1000, + author: user, + linkUrl, + description, + createdDate: '2022-09-13', + lastModifiedDate: '2022-09-13', + }, + ...links, + ]; + + return res(ctx.status(201)); + }), + rest.put('/api/studies/:studyId/reference-room/links/:linkId', (req, res, ctx) => { + const linkId = Number(req.params.linkId); + if (!linkId) return res(ctx.status(400), ctx.json({ message: '링크 아이디가 없음' })); + + const { linkUrl, description } = req.body; + if (!linkUrl || !description) + return res(ctx.status(400), ctx.json({ message: 'linkeUrl 또는 description이 없음' })); + + const { links } = linkJson; + const isExist = links.some(link => link.id === linkId); + if (!isExist) return res(ctx.status(404), ctx.json({ message: '해당하는 링크 없음' })); + + linkJson.links = links.map(link => { + if (link.id === linkId) + return { + ...link, + linkUrl, + description, + }; + return link; + }); + return res(ctx.status(204)); + }), + rest.delete('/api/studies/:studyId/reference-room/links/:linkId', (req, res, ctx) => { + const linkId = Number(req.params.linkId); + const { links } = linkJson; + + if (!linkId) return res(ctx.status(400), ctx.json({ message: '링크 아이디가 없음' })); + + const isExist = links.some(link => link.id === linkId); + if (!isExist) return res(ctx.status(404), ctx.json({ message: '해당하는 링크 없음' })); + + const filteredLinks = links.filter(link => link.id !== linkId); + linkJson.links = filteredLinks; + return res(ctx.status(204)); + }), +]; diff --git a/frontend/src/mocks/links.json b/frontend/src/mocks/links.json new file mode 100644 index 000000000..657f50efe --- /dev/null +++ b/frontend/src/mocks/links.json @@ -0,0 +1,43 @@ +{ + "links": [ + { + "id": 1, + "author": { + "username": "nan-noo", + "imageUrl": "https://picsum.photos/id/50/200/300", + "profileUrl": "github.com/greenlawn", + "id": 2 + }, + "linkUrl": "https://naver.com", + "description": "네이버 홈", + "createdDate": "2022-08-13", + "lastModifiedDate": "2022-08-13" + }, + { + "id": 2, + "author": { + "username": "nan-noo", + "imageUrl": "https://picsum.photos/id/50/200/300", + "profileUrl": "github.com/greenlawn", + "id": 2 + }, + "linkUrl": "https://www.npmjs.com/package/he", + "description": "이것도 테스트해보자!", + "createdDate": "2022-08-13", + "lastModifiedDate": "2022-08-13" + }, + { + "id": 3, + "author": { + "username": "nan-noo", + "imageUrl": "https://picsum.photos/id/50/200/300", + "profileUrl": "github.com/greenlawn", + "id": 2 + }, + "linkUrl": "https://dev.moamoa.space/", + "description": "이게 모아모아다", + "createdDate": "2022-08-13", + "lastModifiedDate": "2022-08-13" + } + ] +} diff --git a/frontend/src/mocks/memberHandlers.ts b/frontend/src/mocks/memberHandlers.ts index f9a4191a7..7e478e9f7 100644 --- a/frontend/src/mocks/memberHandlers.ts +++ b/frontend/src/mocks/memberHandlers.ts @@ -1,6 +1,6 @@ import { rest } from 'msw'; -const user = { +export const user = { id: 20, username: 'tco0427', imageUrl: @@ -15,12 +15,13 @@ export const memberHandlers = [ rest.get('/api/members/me/role', (req, res, ctx) => { // const studyId = req.url.searchParams.get('study-id'); - const roles = ['OWNER', 'MEMBER', 'NON_MEMBER']; - const selectedRole = roles[Math.floor(Math.random() * roles.length)]; + // const roles = ['OWNER', 'MEMBER', 'NON_MEMBER']; + // const selectedRole = roles[Math.floor(Math.random() * roles.length)]; return res( ctx.status(200), ctx.json({ - role: selectedRole, + role: 'OWNER', + // role: selectedRole, }), ); }), diff --git a/frontend/src/mocks/reviewHandler.ts b/frontend/src/mocks/reviewHandler.ts index ed3f7fde1..bf95f75ee 100644 --- a/frontend/src/mocks/reviewHandler.ts +++ b/frontend/src/mocks/reviewHandler.ts @@ -1,5 +1,6 @@ import { rest } from 'msw'; +import { user } from '@mocks/memberHandlers'; import reviewJSON from '@mocks/reviews.json'; import { isObject } from '@utils'; @@ -35,13 +36,7 @@ export const reviewHandlers = [ const review = { id: 1, - member: { - id: 20, - username: 'tco0427', - imageUrl: - 'https://images.unsplash.com/flagged/photo-1570612861542-284f4c12e75f?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2070&q=80', - profileUrl: 'github.com', - }, + member: user, content, createdDate: '2022-07-12', lastModifiedDate: '', diff --git a/frontend/src/pages/create-study-page/components/category/Category.tsx b/frontend/src/pages/create-study-page/components/category/Category.tsx index 2800cd531..884279ab8 100644 --- a/frontend/src/pages/create-study-page/components/category/Category.tsx +++ b/frontend/src/pages/create-study-page/components/category/Category.tsx @@ -2,13 +2,14 @@ import tw from '@utils/tw'; import type { Tag } from '@custom-types'; +import { useGetTags } from '@api/tags'; + import { useFormContext } from '@hooks/useForm'; import Checkbox from '@components/checkbox/Checkbox'; import * as S from '@create-study-page/components/category/Category.style'; import MetaBox from '@create-study-page/components/meta-box/MetaBox'; -import useGetTagList from '@create-study-page/hooks/useGetTagList'; export type CategoryProps = { className?: string; @@ -29,7 +30,7 @@ const getClassifiedTags = (tags: Array) => { const Category = ({ className }: CategoryProps) => { const { register } = useFormContext(); - const { data, isLoading, isError, isSuccess } = useGetTagList(); + const { data, isLoading, isError, isSuccess } = useGetTags(); const renderContent = () => { if (isLoading) return
loading...
; diff --git a/frontend/src/pages/create-study-page/components/enrollment-end-date/EnrollmentEndDate.style.tsx b/frontend/src/pages/create-study-page/components/enrollment-end-date/EnrollmentEndDate.style.tsx index d790dca41..aadbe1546 100644 --- a/frontend/src/pages/create-study-page/components/enrollment-end-date/EnrollmentEndDate.style.tsx +++ b/frontend/src/pages/create-study-page/components/enrollment-end-date/EnrollmentEndDate.style.tsx @@ -1,6 +1,6 @@ import styled from '@emotion/styled'; -export { Input } from '@create-study-page/components/input/Input.style'; +export { Input } from '@components/input/Input.style'; export const EnrollmentEndDate = styled.div``; diff --git a/frontend/src/pages/create-study-page/components/excerpt/Excerpt.tsx b/frontend/src/pages/create-study-page/components/excerpt/Excerpt.tsx index 12e39d4da..7f7644dac 100644 --- a/frontend/src/pages/create-study-page/components/excerpt/Excerpt.tsx +++ b/frontend/src/pages/create-study-page/components/excerpt/Excerpt.tsx @@ -1,6 +1,7 @@ import { EXCERPT_LENGTH } from '@constants'; import { makeValidationResult, useFormContext } from '@hooks/useForm'; +import type { FieldElement } from '@hooks/useForm'; import LetterCounter from '@components/letter-counter/LetterCounter'; import useLetterCount from '@components/letter-counter/useLetterCount'; @@ -20,6 +21,8 @@ const Excerpt = ({ className }: ExcerptProps) => { const { count, setCount, maxCount } = useLetterCount(EXCERPT_LENGTH.MAX.VALUE); + const handleExcerptChange = ({ target: { value } }: React.ChangeEvent) => setCount(value.length); + return ( @@ -45,7 +48,7 @@ const Excerpt = ({ className }: ExcerptProps) => { return makeValidationResult(false); }, validationMode: 'change', - onChange: e => setCount(e.target.value.length), + onChange: handleExcerptChange, minLength: EXCERPT_LENGTH.MIN.VALUE, maxLength: EXCERPT_LENGTH.MAX.VALUE, required: true, diff --git a/frontend/src/pages/create-study-page/components/max-member-count/MaxMemberCount.style.tsx b/frontend/src/pages/create-study-page/components/max-member-count/MaxMemberCount.style.tsx index 0a6446fc7..7e31ba0f0 100644 --- a/frontend/src/pages/create-study-page/components/max-member-count/MaxMemberCount.style.tsx +++ b/frontend/src/pages/create-study-page/components/max-member-count/MaxMemberCount.style.tsx @@ -2,7 +2,7 @@ import styled from '@emotion/styled'; import OriginalCheckbox from '@components/checkbox/Checkbox'; -export { Input } from '@create-study-page/components/input/Input.style'; +export { Input } from '@components/input/Input.style'; export const MaxMemberCount = styled.div``; diff --git a/frontend/src/pages/create-study-page/components/period/Period.style.tsx b/frontend/src/pages/create-study-page/components/period/Period.style.tsx index c741db64f..dfddd9d67 100644 --- a/frontend/src/pages/create-study-page/components/period/Period.style.tsx +++ b/frontend/src/pages/create-study-page/components/period/Period.style.tsx @@ -1,6 +1,6 @@ import styled from '@emotion/styled'; -export { Input } from '@create-study-page/components/input/Input.style'; +export { Input } from '@components/input/Input.style'; export const Period = styled.div``; diff --git a/frontend/src/pages/create-study-page/components/select/Select.style.tsx b/frontend/src/pages/create-study-page/components/select/Select.style.tsx index 40b990e71..fb084f667 100644 --- a/frontend/src/pages/create-study-page/components/select/Select.style.tsx +++ b/frontend/src/pages/create-study-page/components/select/Select.style.tsx @@ -1,7 +1,7 @@ import { css } from '@emotion/react'; import styled from '@emotion/styled'; -import { Input } from '@create-study-page/components/input/Input.style'; +import { Input } from '@components/input/Input.style'; export const Select = styled(Input.withComponent('select'))` ${({ theme }) => css` diff --git a/frontend/src/pages/create-study-page/components/subject/Subject.tsx b/frontend/src/pages/create-study-page/components/subject/Subject.tsx index 2fc79173c..3184cece4 100644 --- a/frontend/src/pages/create-study-page/components/subject/Subject.tsx +++ b/frontend/src/pages/create-study-page/components/subject/Subject.tsx @@ -1,10 +1,11 @@ import tw from '@utils/tw'; +import { useGetTags } from '@api/tags'; + import { useFormContext } from '@hooks/useForm'; import MetaBox from '@create-study-page/components/meta-box/MetaBox'; import * as S from '@create-study-page/components/subject/Subject.style'; -import useGetTagList from '@create-study-page/hooks/useGetTagList'; type SubjectProps = { className?: string; @@ -12,7 +13,7 @@ type SubjectProps = { const Subject = ({ className }: SubjectProps) => { const { register } = useFormContext(); - const { data, isLoading, isError } = useGetTagList(); + const { data, isLoading, isError } = useGetTags(); const render = () => { if (isLoading) return
loading...
; diff --git a/frontend/src/pages/create-study-page/components/textarea/Textarea.style.tsx b/frontend/src/pages/create-study-page/components/textarea/Textarea.style.tsx index 229903f0d..1a29fc361 100644 --- a/frontend/src/pages/create-study-page/components/textarea/Textarea.style.tsx +++ b/frontend/src/pages/create-study-page/components/textarea/Textarea.style.tsx @@ -1,6 +1,6 @@ import styled from '@emotion/styled'; -import { Input } from '@create-study-page/components/input/Input.style'; +import { Input } from '@components/input/Input.style'; export const Textarea = styled(Input.withComponent('textarea'))` overflow: auto; diff --git a/frontend/src/pages/create-study-page/components/title/Title.style.tsx b/frontend/src/pages/create-study-page/components/title/Title.style.tsx index 7389f760d..d01ab3a69 100644 --- a/frontend/src/pages/create-study-page/components/title/Title.style.tsx +++ b/frontend/src/pages/create-study-page/components/title/Title.style.tsx @@ -1,7 +1,7 @@ import { css } from '@emotion/react'; import styled from '@emotion/styled'; -import { Input as OriginalInput } from '@create-study-page/components/input/Input.style'; +import { Input as OriginalInput } from '@components/input/Input.style'; export const Container = styled.div` position: relative; diff --git a/frontend/src/pages/create-study-page/components/title/Title.tsx b/frontend/src/pages/create-study-page/components/title/Title.tsx index d339289d2..f0b4d4a0d 100644 --- a/frontend/src/pages/create-study-page/components/title/Title.tsx +++ b/frontend/src/pages/create-study-page/components/title/Title.tsx @@ -3,6 +3,7 @@ import { useState } from 'react'; import { TITLE_LENGTH } from '@constants'; import { makeValidationResult, useFormContext } from '@hooks/useForm'; +import type { FieldElement } from '@hooks/useForm'; import LetterCounter from '@components/letter-counter/LetterCounter'; @@ -14,6 +15,8 @@ const Title: React.FC = () => { const [count, setCount] = useState(0); const isValid = !!errors['title']?.hasError; + const handleTitleChange = ({ target: { value } }: React.ChangeEvent) => setCount(value.length); + return ( @@ -37,7 +40,7 @@ const Title: React.FC = () => { return makeValidationResult(false); }, validationMode: 'change', - onChange: e => setCount(e.target.value.length), + onChange: handleTitleChange, minLength: TITLE_LENGTH.MIN.VALUE, maxLength: TITLE_LENGTH.MAX.VALUE, required: true, diff --git a/frontend/src/pages/create-study-page/hooks/useCreateStudyPage.ts b/frontend/src/pages/create-study-page/hooks/useCreateStudyPage.ts index c15e18868..e802afedd 100644 --- a/frontend/src/pages/create-study-page/hooks/useCreateStudyPage.ts +++ b/frontend/src/pages/create-study-page/hooks/useCreateStudyPage.ts @@ -4,13 +4,12 @@ import { PATH } from '@constants'; import { getRandomInt } from '@utils'; -import type { PostNewStudyRequestBody } from '@custom-types'; +import { usePostStudy } from '@api/study'; +import type { PostStudyRequestBody } from '@api/study'; import { useForm } from '@hooks/useForm'; import type { UseFormSubmitResult } from '@hooks/useForm'; -import usePostNewStudy from '@create-study-page/hooks/usePostNewStudy'; - const useCreateStudyPage = () => { const navigate = useNavigate(); @@ -27,7 +26,7 @@ const useCreateStudyPage = () => { const formMethods = useForm(); - const { mutateAsync } = usePostNewStudy(); + const { mutateAsync } = usePostStudy(); const onSubmit = async (_: React.FormEvent, submitResult: UseFormSubmitResult) => { if (!submitResult.values) return; @@ -45,7 +44,7 @@ const useCreateStudyPage = () => { const thumbnail = `https://picsum.photos/id/${getRandomInt(1, 100)}/200/300`; - const postData: PostNewStudyRequestBody = { + const postData: PostStudyRequestBody = { title: values['title'], excerpt: values['excerpt'], thumbnail, diff --git a/frontend/src/pages/create-study-page/hooks/useGetTagList.ts b/frontend/src/pages/create-study-page/hooks/useGetTagList.ts deleted file mode 100644 index 5448dfa04..000000000 --- a/frontend/src/pages/create-study-page/hooks/useGetTagList.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { AxiosError } from 'axios'; -import { useQuery } from 'react-query'; - -import type { GetTagListResponseData } from '@custom-types'; - -import { getTagList } from '@api'; - -const useGetTagList = () => { - return useQuery('filters', getTagList); -}; - -export default useGetTagList; diff --git a/frontend/src/pages/create-study-page/hooks/usePostNewStudy.ts b/frontend/src/pages/create-study-page/hooks/usePostNewStudy.ts deleted file mode 100644 index 2978117ca..000000000 --- a/frontend/src/pages/create-study-page/hooks/usePostNewStudy.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { AxiosError } from 'axios'; -import { useMutation } from 'react-query'; - -import type { PostNewStudyRequestBody } from '@custom-types'; - -import { postNewStudy } from '@api'; - -const usePostNewStudy = () => { - return useMutation(postNewStudy); -}; - -export default usePostNewStudy; diff --git a/frontend/src/pages/detail-page/components/study-member-section/StudyMemberSection.stories.tsx b/frontend/src/pages/detail-page/components/study-member-section/StudyMemberSection.stories.tsx index efb1171d6..b9e97d4df 100644 --- a/frontend/src/pages/detail-page/components/study-member-section/StudyMemberSection.stories.tsx +++ b/frontend/src/pages/detail-page/components/study-member-section/StudyMemberSection.stories.tsx @@ -8,6 +8,14 @@ export default { component: StudyMemberSection, }; +const owner = { + id: 1, + username: 'nan-noo', + profileUrl: '/', + imageUrl: + 'https://images.unsplash.com/photo-1554151228-14d9def656e4?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxzZWFyY2h8N3x8cGVyc29ufGVufDB8fDB8fA%3D%3D&auto=format&fit=crop&w=800&q=60', +}; + const members = [ { id: 9640683, @@ -102,4 +110,4 @@ const Template: Story = props => ( ); export const Default = Template.bind({}); -Default.args = { members }; +Default.args = { owner, members }; diff --git a/frontend/src/pages/detail-page/components/study-member-section/StudyMemberSection.tsx b/frontend/src/pages/detail-page/components/study-member-section/StudyMemberSection.tsx index 76ad6c50c..9066d8969 100644 --- a/frontend/src/pages/detail-page/components/study-member-section/StudyMemberSection.tsx +++ b/frontend/src/pages/detail-page/components/study-member-section/StudyMemberSection.tsx @@ -6,6 +6,8 @@ import { changeDateSeperator } from '@utils'; import type { Member, Owner } from '@custom-types'; +import { CrownSvg } from '@components/svg'; + import MoreButton from '@detail-page/components/more-button/MoreButton'; import StudyMemberCard from '@detail-page/components/study-member-card/StudyMemberCard'; import * as S from '@detail-page/components/study-member-section/StudyMemberSection.style'; @@ -15,24 +17,6 @@ export type StudyMemberSectionProps = { members: Array; }; -const TbCrown = () => ( - - - - - -); - const StudyMemberSection: React.FC = ({ owner, members }) => { const [showAll, setShowAll] = useState(false); @@ -52,7 +36,7 @@ const StudyMemberSection: React.FC = ({ owner, members <> - + = ({ owner, members <> - + { const { studyId } = useParams() as { studyId: string }; const { isLoggedIn } = useAuth(); - const detailQueryResult = useGetDetail(Number(studyId)); + const detailQueryResult = useGetStudy({ studyId: Number(studyId) }); - const { mutate } = useMutation(postJoiningStudy); - const userRoleQueryResult = useQuery( - 'my-role', - () => getUserRole({ studyId: Number(studyId) }), - { + const { mutate } = usePostMyStudy(); + const userRoleQueryResult = useGetUserRole({ + studyId: Number(studyId), + options: { enabled: isLoggedIn, }, - ); + }); const handleRegisterButtonClick = () => { if (!isLoggedIn) { diff --git a/frontend/src/pages/detail-page/hooks/useGetDetail.ts b/frontend/src/pages/detail-page/hooks/useGetDetail.ts deleted file mode 100644 index 60769b99d..000000000 --- a/frontend/src/pages/detail-page/hooks/useGetDetail.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { AxiosError } from 'axios'; -import { useQuery } from 'react-query'; - -import { QK_FETCH_STUDY_DETAIL } from '@constants'; - -import type { GetStudyDetailResponseData } from '@custom-types'; - -import { getStudyDetail } from '@api'; - -const useGetDetail = (studyId: number) => { - return useQuery([QK_FETCH_STUDY_DETAIL, studyId], () => - getStudyDetail({ studyId }), - ); -}; - -export default useGetDetail; diff --git a/frontend/src/pages/detail-page/hooks/useGetStudyReviews.ts b/frontend/src/pages/detail-page/hooks/useGetStudyReviews.ts deleted file mode 100644 index cf5007926..000000000 --- a/frontend/src/pages/detail-page/hooks/useGetStudyReviews.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { AxiosError } from 'axios'; -import { useQuery } from 'react-query'; - -import { QK_FETCH_STUDY_REVIEWS } from '@constants'; - -import type { GetReviewResponseData } from '@custom-types'; - -import { getStudyReviews } from '@api'; - -const useGetStudyReviews = (studyId: number, size?: number) => { - const queryKey = size ? [QK_FETCH_STUDY_REVIEWS, studyId] : [QK_FETCH_STUDY_REVIEWS, studyId, 'all']; - return useQuery(queryKey, () => getStudyReviews({ studyId, size })); -}; - -export default useGetStudyReviews; diff --git a/frontend/src/pages/login-redirect-page/hooks/useLoginRedirectPage.ts b/frontend/src/pages/login-redirect-page/hooks/useLoginRedirectPage.ts index 2ed86a4d2..8c437711f 100644 --- a/frontend/src/pages/login-redirect-page/hooks/useLoginRedirectPage.ts +++ b/frontend/src/pages/login-redirect-page/hooks/useLoginRedirectPage.ts @@ -1,13 +1,9 @@ -import type { AxiosError } from 'axios'; import { useEffect } from 'react'; -import { useMutation } from 'react-query'; import { useNavigate, useSearchParams } from 'react-router-dom'; import { PATH } from '@constants'; -import { PostLoginRequestParams, PostLoginResponseData } from '@custom-types'; - -import { postLogin } from '@api'; +import { usePostLogin } from '@api/auth'; import AccessTokenController from '@auth/accessToken'; @@ -20,7 +16,7 @@ const useLoginRedirectPage = () => { const { login } = useAuth(); - const { mutate } = useMutation(postLogin); + const { mutate } = usePostLogin(); useEffect(() => { if (!codeParam) { diff --git a/frontend/src/pages/main-page/MainPage.tsx b/frontend/src/pages/main-page/MainPage.tsx index 0962ed025..0f6684918 100644 --- a/frontend/src/pages/main-page/MainPage.tsx +++ b/frontend/src/pages/main-page/MainPage.tsx @@ -14,10 +14,10 @@ import StudyCard from '@main-page/components/study-card/StudyCard'; import useMainPage from '@main-page/hooks/useMainPage'; const MainPage: React.FC = () => { - const { studyListQueryResult, selectedFilters, handleFilterButtonClick, handleCreateNewStudyButtonClick } = + const { studiesQueryResult, selectedFilters, handleFilterButtonClick, handleCreateNewStudyButtonClick } = useMainPage(); - const { isFetching, isError, isSuccess, data, fetchNextPage } = studyListQueryResult; + const { isFetching, isError, isSuccess, data, fetchNextPage } = studiesQueryResult; const renderStudyList = () => { if (isError || !isSuccess) { diff --git a/frontend/src/pages/main-page/components/create-new-study-button/CreateNewStudyButton.style.tsx b/frontend/src/pages/main-page/components/create-new-study-button/CreateNewStudyButton.style.tsx index e2bdc48e9..0e846ee06 100644 --- a/frontend/src/pages/main-page/components/create-new-study-button/CreateNewStudyButton.style.tsx +++ b/frontend/src/pages/main-page/components/create-new-study-button/CreateNewStudyButton.style.tsx @@ -26,5 +26,9 @@ export const CreateNewStudyButton = styled.button` &:active { background-color: ${theme.colors.primary.dark}; } + + & > svg { + stroke: ${theme.colors.white}; + } `} `; diff --git a/frontend/src/pages/main-page/components/create-new-study-button/CreateNewStudyButton.tsx b/frontend/src/pages/main-page/components/create-new-study-button/CreateNewStudyButton.tsx index 949405991..5b9368f3a 100644 --- a/frontend/src/pages/main-page/components/create-new-study-button/CreateNewStudyButton.tsx +++ b/frontend/src/pages/main-page/components/create-new-study-button/CreateNewStudyButton.tsx @@ -1,4 +1,4 @@ -import { theme } from '@styles/theme'; +import { PencilSvg } from '@components/svg'; import * as S from '@main-page/components/create-new-study-button/CreateNewStudyButton.style'; @@ -9,21 +9,7 @@ type CreateNewStudyButtonProps = { const CreateNewStudyButton = ({ onClick: handleClick }: CreateNewStudyButtonProps) => { return ( - - - - + ); }; diff --git a/frontend/src/pages/main-page/components/filter-section/FilterSection.style.tsx b/frontend/src/pages/main-page/components/filter-section/FilterSection.style.tsx index 09eb76f1d..8e05074e7 100644 --- a/frontend/src/pages/main-page/components/filter-section/FilterSection.style.tsx +++ b/frontend/src/pages/main-page/components/filter-section/FilterSection.style.tsx @@ -7,13 +7,14 @@ export const FilterSectionContainer = styled.div` ${({ theme }) => css` position: sticky; top: 85px; + z-index: 1; + max-width: 1280px; margin: 0 auto 16px; padding: 16px 20px 0; background-color: ${theme.colors.secondary.light}; border-bottom: 1px solid ${theme.colors.secondary.dark}; - z-index: 1; `} ${mqDown('md')} { @@ -35,11 +36,12 @@ export const RightButtonContainer = styled.div` position: absolute; top: 0; right: 20px; + z-index: 2; + height: 100%; padding: auto 0; background-color: ${theme.colors.secondary.light}cc; - z-index: 2; ${mqDown('md')} { display: none; diff --git a/frontend/src/pages/main-page/components/filter-section/FilterSection.tsx b/frontend/src/pages/main-page/components/filter-section/FilterSection.tsx index 78ddebe08..43387a5ae 100644 --- a/frontend/src/pages/main-page/components/filter-section/FilterSection.tsx +++ b/frontend/src/pages/main-page/components/filter-section/FilterSection.tsx @@ -1,10 +1,8 @@ -import type { AxiosError } from 'axios'; import { useRef } from 'react'; -import { useQuery } from 'react-query'; -import type { GetTagListResponseData, Tag, TagInfo } from '@custom-types'; +import type { Tag, TagInfo } from '@custom-types'; -import { getTagList } from '@api'; +import { useGetTags } from '@api/tags'; import ArrowButton from '@components/arrow-button/ArrowButton'; @@ -27,7 +25,7 @@ const FilterSection: React.FC = ({ }) => { const sliderRef = useRef(null); - const { data, isLoading, isError } = useQuery('filters', getTagList); + const { data, isLoading, isError } = useGetTags(); const generationTags = filterByCategory(data?.tags, 1); const areaTags = filterByCategory(data?.tags, 2); diff --git a/frontend/src/pages/main-page/components/filter-section/filter-button-list/FilterButtonList.style.tsx b/frontend/src/pages/main-page/components/filter-section/filter-button-list/FilterButtonList.style.tsx index 0926d8e47..0980b8828 100644 --- a/frontend/src/pages/main-page/components/filter-section/filter-button-list/FilterButtonList.style.tsx +++ b/frontend/src/pages/main-page/components/filter-section/filter-button-list/FilterButtonList.style.tsx @@ -2,7 +2,7 @@ import styled from '@emotion/styled'; import { mqDown } from '@utils'; -export const FilterButtons = styled.ul` +export const FilterButtonList = styled.ul` display: flex; justify-content: center; align-items: center; diff --git a/frontend/src/pages/main-page/components/filter-section/filter-button-list/FilterButtonList.tsx b/frontend/src/pages/main-page/components/filter-section/filter-button-list/FilterButtonList.tsx index a07fbb6f1..7f6bc553a 100644 --- a/frontend/src/pages/main-page/components/filter-section/filter-button-list/FilterButtonList.tsx +++ b/frontend/src/pages/main-page/components/filter-section/filter-button-list/FilterButtonList.tsx @@ -18,7 +18,7 @@ const FilterButtonList: React.FC = ({ onFilterButtonClick: handleFilterButtonClick, }) => { return ( - + {filters.map(({ id, name, description, category }) => (
  • = ({ />
  • ))} -
    + ); }; diff --git a/frontend/src/pages/main-page/hooks/useGetInfiniteStudyList.ts b/frontend/src/pages/main-page/hooks/useGetInfiniteStudyList.ts deleted file mode 100644 index 71836df58..000000000 --- a/frontend/src/pages/main-page/hooks/useGetInfiniteStudyList.ts +++ /dev/null @@ -1,49 +0,0 @@ -import type { AxiosError } from 'axios'; -import { useInfiniteQuery } from 'react-query'; - -import { DEFAULT_STUDY_CARD_QUERY_PARAM } from '@constants'; - -import type { GetStudyListRequestParams, GetStudyListResponseData, TagInfo } from '@custom-types'; - -import { getStudyList } from '@api'; - -type PageParam = { page: number }; - -type GetStudyListResponseDataWithPage = GetStudyListResponseData & { - page: number; -}; - -type UseGetInfiniteStudyListParams = Pick; - -type NextPageParam = PageParam | undefined; - -const defaultParam: PageParam = { - page: DEFAULT_STUDY_CARD_QUERY_PARAM.PAGE, -}; - -const getStudyListWithPage = - (title: string, selectedFilters: Array) => - async ({ pageParam = defaultParam }): Promise => { - const data = await getStudyList({ - ...pageParam, - title, - selectedFilters, - size: DEFAULT_STUDY_CARD_QUERY_PARAM.SIZE, - }); - return { ...data, page: pageParam.page + 1 }; - }; - -const useGetInfiniteStudyList = ({ title, selectedFilters }: UseGetInfiniteStudyListParams) => { - return useInfiniteQuery( - ['infinite-scroll-searched-study-list', title, selectedFilters], - getStudyListWithPage(title, selectedFilters), - { - getNextPageParam: (lastPage): NextPageParam => { - if (!lastPage.hasNext) return; - return { page: lastPage.page }; - }, - }, - ); -}; - -export default useGetInfiniteStudyList; diff --git a/frontend/src/pages/main-page/hooks/useMainPage.ts b/frontend/src/pages/main-page/hooks/useMainPage.ts index 9c86df301..2442feab3 100644 --- a/frontend/src/pages/main-page/hooks/useMainPage.ts +++ b/frontend/src/pages/main-page/hooks/useMainPage.ts @@ -5,12 +5,12 @@ import { PATH } from '@constants'; import type { TagInfo } from '@custom-types'; +import { useGetInfiniteStudies } from '@api/studies'; + import { useAuth } from '@hooks/useAuth'; import { SearchContext } from '@context/search/SearchProvider'; -import useGetInfiniteStudyList from '@main-page/hooks/useGetInfiniteStudyList'; - const useMainPage = () => { const navigate = useNavigate(); @@ -19,7 +19,7 @@ const useMainPage = () => { const { keyword } = useContext(SearchContext); const [selectedFilters, setSelectedFilters] = useState>([]); - const studyListQueryResult = useGetInfiniteStudyList({ + const studiesQueryResult = useGetInfiniteStudies({ title: keyword, selectedFilters, }); @@ -43,7 +43,7 @@ const useMainPage = () => { }; return { - studyListQueryResult, + studiesQueryResult, selectedFilters, handleFilterButtonClick, handleCreateNewStudyButtonClick, diff --git a/frontend/src/pages/my-study-page/components/my-study-card/MyStudyCard.tsx b/frontend/src/pages/my-study-page/components/my-study-card/MyStudyCard.tsx index 229414a7a..541e8e0a8 100644 --- a/frontend/src/pages/my-study-page/components/my-study-card/MyStudyCard.tsx +++ b/frontend/src/pages/my-study-page/components/my-study-card/MyStudyCard.tsx @@ -1,5 +1,7 @@ import type { MakeOptional, Tag } from '@custom-types'; +import { CrownSvg, TrashCanSvg } from '@components/svg'; + import * as S from '@my-study-page/components/my-study-card/MyStudyCard.style'; export type MyStudyCardProps = { @@ -13,43 +15,6 @@ export type MyStudyCardProps = { type OptionalMyStudyCardProps = MakeOptional; -const TbCrown = () => ( - - - - - -); - -const HiOutlineTrash = () => ( - - - -); - const MyStudyCard: React.FC = ({ title, ownerName, @@ -64,7 +29,7 @@ const MyStudyCard: React.FC = ({ {title} - + {ownerName} @@ -78,7 +43,7 @@ const MyStudyCard: React.FC = ({ {startDate} ~ {endDate || ''} - +
    diff --git a/frontend/src/pages/my-study-page/hooks/useGetMyStudy.ts b/frontend/src/pages/my-study-page/hooks/useGetMyStudy.ts deleted file mode 100644 index 98a08122d..000000000 --- a/frontend/src/pages/my-study-page/hooks/useGetMyStudy.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { AxiosError } from 'axios'; -import { useQuery } from 'react-query'; - -import type { GetMyStudyResponseData } from '@custom-types'; - -import { getMyStudyList } from '@api'; - -const useGetMyStudy = () => { - return useQuery('my-studies', getMyStudyList); -}; - -export default useGetMyStudy; diff --git a/frontend/src/pages/my-study-page/hooks/useMyStudyPage.ts b/frontend/src/pages/my-study-page/hooks/useMyStudyPage.ts index 2cfe77052..a333b4344 100644 --- a/frontend/src/pages/my-study-page/hooks/useMyStudyPage.ts +++ b/frontend/src/pages/my-study-page/hooks/useMyStudyPage.ts @@ -2,14 +2,14 @@ import { useMemo } from 'react'; import type { MyStudy, StudyStatus } from '@custom-types'; -import useGetMyStudy from '@my-study-page/hooks/useGetMyStudy'; +import { useGetMyStudies } from '@api/my-studies'; const filterStudiesByStatus = (studies: Array, status: StudyStatus) => { return studies.filter(({ studyStatus }) => studyStatus === status); }; export const useMyStudyPage = () => { - const myStudyQueryResult = useGetMyStudy(); + const myStudyQueryResult = useGetMyStudies(); const filteredStudies: Record> = useMemo(() => { const studies = myStudyQueryResult.data?.studies ?? []; diff --git a/frontend/src/pages/study-room-page/StudyRoomPage.tsx b/frontend/src/pages/study-room-page/StudyRoomPage.tsx index 0c7d088fd..5555a6699 100644 --- a/frontend/src/pages/study-room-page/StudyRoomPage.tsx +++ b/frontend/src/pages/study-room-page/StudyRoomPage.tsx @@ -28,7 +28,7 @@ const StudyRoomPage: React.FC = () => { ; + +export const Default = Template.bind({}); +Default.args = {}; diff --git a/frontend/src/pages/study-room-page/components/link-room-tab-panel/LinkRoomTabPanel.style.tsx b/frontend/src/pages/study-room-page/components/link-room-tab-panel/LinkRoomTabPanel.style.tsx new file mode 100644 index 000000000..5e6850539 --- /dev/null +++ b/frontend/src/pages/study-room-page/components/link-room-tab-panel/LinkRoomTabPanel.style.tsx @@ -0,0 +1,63 @@ +import { css } from '@emotion/react'; +import styled from '@emotion/styled'; + +import { mqDown } from '@utils'; + +export const LinkAddButtonContainer = styled.div` + margin-bottom: 16px; + padding: 4px 0; +`; + +export const PlusSvgContainer = styled.span` + ${({ theme }) => css` + width: 20px; + height: 20px; + margin-right: 4px; + + background-color: ${theme.colors.primary.base}; + border-radius: 50%; + + & > svg { + fill: ${theme.colors.white}; + } + `} +`; + +export const LinkAddButton = styled.button` + ${({ theme }) => css` + display: flex; + align-items: center; + + margin-left: auto; + + color: ${theme.colors.primary.base}; + background-color: transparent; + border: none; + + & > span { + font-size: 20px; + color: ${theme.colors.primary.base}; + } + + &:hover, + &:active { + & > span { + color: ${theme.colors.primary.light}; + } + } + `} +`; + +export const LinkList = styled.ul` + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 24px; + + ${mqDown('lg')} { + grid-template-columns: repeat(2, 1fr); + } + + ${mqDown('sm')} { + grid-template-columns: repeat(1, 1fr); + } +`; diff --git a/frontend/src/pages/study-room-page/components/link-room-tab-panel/LinkRoomTabPanel.tsx b/frontend/src/pages/study-room-page/components/link-room-tab-panel/LinkRoomTabPanel.tsx new file mode 100644 index 000000000..041adc479 --- /dev/null +++ b/frontend/src/pages/study-room-page/components/link-room-tab-panel/LinkRoomTabPanel.tsx @@ -0,0 +1,76 @@ +import type { Link } from '@custom-types'; + +import InfiniteScroll from '@components/infinite-scroll/InfiniteScroll'; +import ModalPortal from '@components/modal/Modal'; +import { PlusSvg } from '@components/svg'; +import Wrapper from '@components/wrapper/Wrapper'; + +import * as S from '@study-room-page/components/link-room-tab-panel/LinkRoomTabPanel.style'; +import LinkForm from '@study-room-page/components/link-room-tab-panel/components/link-form/LinkForm'; +import LinkItem from '@study-room-page/components/link-room-tab-panel/components/link-item/LinkItem'; +import { useLinkRoomTabPanel } from '@study-room-page/components/link-room-tab-panel/hooks/useLinkRoomTabPanel'; + +const LinkRoomTabPanel: React.FC = () => { + const { + studyId, + userInfo, + infiniteLinksQueryResult, + isModalOpen, + handleLinkAddButtonClick, + handleModalClose, + handlePostLinkError, + handlePostLinkSuccess, + } = useLinkRoomTabPanel(); + + const renderLinkList = () => { + const { data, isError, isSuccess, fetchNextPage } = infiniteLinksQueryResult; + if (isError || !isSuccess) { + return
    에러가 발생했습니다
    ; + } + + const links = data.pages.reduce>((acc, cur) => [...acc, ...cur.links], []); + + if (links.length === 0) { + return
    등록된 링크가 없습니다
    ; + } + + return ( + + + {links.map(link => ( +
  • + +
  • + ))} +
    +
    + ); + }; + + return ( + + + + + + + 링크 추가하기 + + + {renderLinkList()} + {isModalOpen && ( + + + + )} + + ); +}; + +export default LinkRoomTabPanel; diff --git a/frontend/src/pages/study-room-page/components/link-room-tab-panel/components/link-edit-form/LinkEditForm.style.tsx b/frontend/src/pages/study-room-page/components/link-room-tab-panel/components/link-edit-form/LinkEditForm.style.tsx new file mode 100644 index 000000000..85ea7ce01 --- /dev/null +++ b/frontend/src/pages/study-room-page/components/link-room-tab-panel/components/link-edit-form/LinkEditForm.style.tsx @@ -0,0 +1,11 @@ +export { + LinkFormContainer, + AuthorInfoContainer, + AuthorName, + Form, + FormLabel, + FormInput, + TextAreaContainer, + FormTextArea, + LetterCounterContainer, +} from '@study-room-page/components/link-room-tab-panel/components/link-form/LinkForm.style'; diff --git a/frontend/src/pages/study-room-page/components/link-room-tab-panel/components/link-edit-form/LinkEditForm.tsx b/frontend/src/pages/study-room-page/components/link-room-tab-panel/components/link-edit-form/LinkEditForm.tsx new file mode 100644 index 000000000..7af7827d3 --- /dev/null +++ b/frontend/src/pages/study-room-page/components/link-room-tab-panel/components/link-edit-form/LinkEditForm.tsx @@ -0,0 +1,136 @@ +import { useParams } from 'react-router-dom'; + +import { LINK_DESCRIPTION_LENGTH, LINK_URL_LENGTH } from '@constants'; + +import tw from '@utils/tw'; + +import type { Link, LinkId, Member, Noop } from '@custom-types'; + +import { usePutLink } from '@api/link'; + +import { makeValidationResult, useForm } from '@hooks/useForm'; +import type { FieldElement, UseFormSubmitResult } from '@hooks/useForm'; + +import Avatar from '@components/avatar/Avatar'; +import Button from '@components/button/Button'; +import LetterCounter from '@components/letter-counter/LetterCounter'; +import useLetterCount from '@components/letter-counter/useLetterCount'; + +import * as S from '@study-room-page/components/link-room-tab-panel/components/link-edit-form/LinkEditForm.style'; + +export type LinkFormProps = { + linkId: LinkId; + author: Member; + originalContent: Pick; + onPutSuccess: Noop; + onPutError: (error: Error) => void; +}; + +const LINK_URL = 'link-url'; +const LINK_DESCRIPTION = 'link-description'; + +const LinkEditForm: React.FC = ({ author, linkId, originalContent, onPutSuccess, onPutError }) => { + const { studyId } = useParams<{ studyId: string }>(); + const { mutateAsync } = usePutLink(); + const { count, maxCount, setCount } = useLetterCount( + LINK_DESCRIPTION_LENGTH.MAX.VALUE, + originalContent.description.length, + ); + const { + register, + handleSubmit, + formState: { errors }, + } = useForm(); + + const isLinkValid = !!errors[LINK_URL]?.hasError; + const isDescValid = !!errors[LINK_DESCRIPTION]?.hasError; + + const onSubmit = async (_: React.FormEvent, submitResult: UseFormSubmitResult) => { + if (!submitResult.values) { + return; + } + + const putData = { + studyId: Number(studyId), + linkId, + linkUrl: submitResult.values[LINK_URL], + description: submitResult.values[LINK_DESCRIPTION], + }; + + return mutateAsync(putData, { + onSuccess: () => { + onPutSuccess(); + }, + onError: error => { + onPutError(error); + }, + }); + }; + + const handleLinkDescriptionChange = ({ target: { value } }: React.ChangeEvent) => + setCount(value.length); + + return ( + + + + person + + + 링크* + { + if (val.length < LINK_URL_LENGTH.MIN.VALUE) { + return makeValidationResult(true, LINK_URL_LENGTH.MIN.MESSAGE); + } + if (val.length > LINK_URL_LENGTH.MAX.VALUE) + return makeValidationResult(true, LINK_URL_LENGTH.MAX.MESSAGE); + if (!LINK_URL_LENGTH.FORMAT.TEST(val)) return makeValidationResult(true, LINK_URL_LENGTH.FORMAT.MESSAGE); + return makeValidationResult(false); + }, + validationMode: 'change', + maxLength: LINK_URL_LENGTH.MAX.VALUE, + minLength: LINK_URL_LENGTH.MIN.VALUE, + required: true, + })} + /> + 설명* + + { + if (val.length < LINK_DESCRIPTION_LENGTH.MIN.VALUE) { + return makeValidationResult(true, LINK_DESCRIPTION_LENGTH.MIN.MESSAGE); + } + if (val.length > LINK_DESCRIPTION_LENGTH.MAX.VALUE) + return makeValidationResult(true, LINK_DESCRIPTION_LENGTH.MAX.MESSAGE); + return makeValidationResult(false); + }, + validationMode: 'change', + onChange: handleLinkDescriptionChange, + maxLength: LINK_DESCRIPTION_LENGTH.MAX.VALUE, + minLength: LINK_DESCRIPTION_LENGTH.MIN.VALUE, + required: true, + })} + /> + + + + + + + + ); +}; + +export default LinkEditForm; diff --git a/frontend/src/pages/study-room-page/components/link-room-tab-panel/components/link-form/LinkForm.stories.tsx b/frontend/src/pages/study-room-page/components/link-room-tab-panel/components/link-form/LinkForm.stories.tsx new file mode 100644 index 000000000..75328f6f1 --- /dev/null +++ b/frontend/src/pages/study-room-page/components/link-room-tab-panel/components/link-form/LinkForm.stories.tsx @@ -0,0 +1,14 @@ +import type { Story } from '@storybook/react'; + +import LinkForm from '@study-room-page/components/link-room-tab-panel/components/link-form/LinkForm'; +import type { LinkFormProps } from '@study-room-page/components/link-room-tab-panel/components/link-form/LinkForm'; + +export default { + title: 'Components/LinkForm', + component: LinkForm, +}; + +const Template: Story = props => ; + +export const Default = Template.bind({}); +Default.args = {}; diff --git a/frontend/src/pages/study-room-page/components/link-room-tab-panel/components/link-form/LinkForm.style.tsx b/frontend/src/pages/study-room-page/components/link-room-tab-panel/components/link-form/LinkForm.style.tsx new file mode 100644 index 000000000..be3697690 --- /dev/null +++ b/frontend/src/pages/study-room-page/components/link-room-tab-panel/components/link-form/LinkForm.style.tsx @@ -0,0 +1,74 @@ +import { css } from '@emotion/react'; +import styled from '@emotion/styled'; + +import { Input } from '@components/input/Input.style'; + +export const LinkFormContainer = styled.div` + ${({ theme }) => css` + width: 480px; + height: 300px; + padding: 16px; + + background-color: ${theme.colors.white}; + border-radius: 15px; + `} +`; + +export const AuthorInfoContainer = styled.div` + display: flex; + align-items: center; + + margin-bottom: 16px; +`; + +export const AuthorName = styled.p` + margin-left: 8px; + + font-weight: 700; +`; + +export const Form = styled.form` + display: flex; + flex-direction: column; + row-gap: 8px; +`; + +export const FormLabel = styled.label` + font-weight: 600; +`; + +export const FormInput = styled(Input)` + ${({ theme }) => css` + margin-bottom: 8px; + padding: 8px; + + border: 1px solid ${theme.colors.secondary.base}; + border-radius: 10px; + background-color: ${theme.colors.secondary.light}; + `} +`; + +export const TextAreaContainer = styled.div` + position: relative; +`; + +export const FormTextArea = styled(Input.withComponent('textarea'))` + ${({ theme }) => css` + width: 100%; + margin-bottom: 8px; + padding: 8px; + + border: 1px solid ${theme.colors.secondary.base}; + border-radius: 10px; + background-color: ${theme.colors.secondary.light}; + `} +`; + +export const LetterCounterContainer = styled.div` + position: absolute; + bottom: 18px; + right: 8px; + + display: flex; + justify-content: flex-end; +`; diff --git a/frontend/src/pages/study-room-page/components/link-room-tab-panel/components/link-form/LinkForm.tsx b/frontend/src/pages/study-room-page/components/link-room-tab-panel/components/link-form/LinkForm.tsx new file mode 100644 index 000000000..fc7e0eb53 --- /dev/null +++ b/frontend/src/pages/study-room-page/components/link-room-tab-panel/components/link-form/LinkForm.tsx @@ -0,0 +1,128 @@ +import { useParams } from 'react-router-dom'; + +import { LINK_DESCRIPTION_LENGTH, LINK_URL_LENGTH } from '@constants'; + +import tw from '@utils/tw'; + +import type { Member, Noop } from '@custom-types'; + +import { usePostLink } from '@api/link'; + +import { UseFormSubmitResult, makeValidationResult, useForm } from '@hooks/useForm'; +import type { FieldElement } from '@hooks/useForm'; + +import Avatar from '@components/avatar/Avatar'; +import Button from '@components/button/Button'; +import LetterCounter from '@components/letter-counter/LetterCounter'; +import useLetterCount from '@components/letter-counter/useLetterCount'; + +import * as S from '@study-room-page/components/link-room-tab-panel/components/link-form/LinkForm.style'; + +export type LinkFormProps = { + author: Member; + onPostSuccess: Noop; + onPostError: (error: Error) => void; +}; + +const LINK_URL = 'link-url'; +const LINK_DESCRIPTION = 'link-description'; + +const LinkForm: React.FC = ({ author, onPostSuccess, onPostError }) => { + const { studyId } = useParams<{ studyId: string }>(); + const { mutateAsync } = usePostLink(); + const { count, maxCount, setCount } = useLetterCount(LINK_DESCRIPTION_LENGTH.MAX.VALUE); + const { + register, + handleSubmit, + formState: { errors }, + } = useForm(); + + const isLinkValid = !!errors[LINK_URL]?.hasError; + const isDescValid = !!errors[LINK_DESCRIPTION]?.hasError; + + const onSubmit = async (_: React.FormEvent, submitResult: UseFormSubmitResult) => { + if (!submitResult.values) { + return; + } + + const postData = { + studyId: Number(studyId), + linkUrl: submitResult.values[LINK_URL], + description: submitResult.values[LINK_DESCRIPTION], + }; + + return mutateAsync(postData, { + onSuccess: () => { + onPostSuccess(); + }, + onError: error => { + onPostError(error); + }, + }); + }; + + const handleLinkDescriptionChange = ({ target: { value } }: React.ChangeEvent) => + setCount(value.length); + + return ( + + + + person + + + 링크* + { + if (val.length < LINK_URL_LENGTH.MIN.VALUE) { + return makeValidationResult(true, LINK_URL_LENGTH.MIN.MESSAGE); + } + if (val.length > LINK_URL_LENGTH.MAX.VALUE) + return makeValidationResult(true, LINK_URL_LENGTH.MAX.MESSAGE); + if (!LINK_URL_LENGTH.FORMAT.TEST(val)) return makeValidationResult(true, LINK_URL_LENGTH.FORMAT.MESSAGE); + return makeValidationResult(false); + }, + validationMode: 'change', + maxLength: LINK_URL_LENGTH.MAX.VALUE, + minLength: LINK_URL_LENGTH.MIN.VALUE, + required: true, + })} + /> + 설명* + + { + if (val.length < LINK_DESCRIPTION_LENGTH.MIN.VALUE) { + return makeValidationResult(true, LINK_DESCRIPTION_LENGTH.MIN.MESSAGE); + } + if (val.length > LINK_DESCRIPTION_LENGTH.MAX.VALUE) + return makeValidationResult(true, LINK_DESCRIPTION_LENGTH.MAX.MESSAGE); + return makeValidationResult(false); + }, + validationMode: 'change', + onChange: handleLinkDescriptionChange, + maxLength: LINK_DESCRIPTION_LENGTH.MAX.VALUE, + minLength: LINK_DESCRIPTION_LENGTH.MIN.VALUE, + required: true, + })} + /> + + + + + + + + ); +}; + +export default LinkForm; diff --git a/frontend/src/pages/study-room-page/components/link-room-tab-panel/components/link-item/LinkItem.stories.tsx b/frontend/src/pages/study-room-page/components/link-room-tab-panel/components/link-item/LinkItem.stories.tsx new file mode 100644 index 000000000..ad20d3011 --- /dev/null +++ b/frontend/src/pages/study-room-page/components/link-room-tab-panel/components/link-item/LinkItem.stories.tsx @@ -0,0 +1,24 @@ +import type { Story } from '@storybook/react'; + +import LinkItem from '@study-room-page/components/link-room-tab-panel/components/link-item/LinkItem'; +import type { LinkItemProps } from '@study-room-page/components/link-room-tab-panel/components/link-item/LinkItem'; + +export default { + title: 'Components/LinkItem', + component: LinkItem, +}; + +const Template: Story = props => ; + +export const Default = Template.bind({}); +Default.args = { + linkUrl: 'https://www.naver.com/', + author: { + id: 1, + username: 'your-name', + profileUrl: '/', + imageUrl: + 'https://images.unsplash.com/photo-1554151228-14d9def656e4?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxzZWFyY2h8N3x8cGVyc29ufGVufDB8fDB8fA%3D%3D&auto=format&fit=crop&w=800&q=60', + }, + description: '안녕하세요 글자는 약 40자 정도로 하면 좋을 것 같네요. 가나다라마바사아', +}; diff --git a/frontend/src/pages/study-room-page/components/link-room-tab-panel/components/link-item/LinkItem.style.tsx b/frontend/src/pages/study-room-page/components/link-room-tab-panel/components/link-item/LinkItem.style.tsx new file mode 100644 index 000000000..0114ff8e5 --- /dev/null +++ b/frontend/src/pages/study-room-page/components/link-room-tab-panel/components/link-item/LinkItem.style.tsx @@ -0,0 +1,64 @@ +import { css } from '@emotion/react'; +import styled from '@emotion/styled'; + +export const LinkItemContainer = styled.div` + position: relative; +`; + +export const PreviewMeatballMenuContainer = styled.div` + ${({ theme }) => css` + display: flex; + justify-content: center; + align-items: center; + + position: absolute; + top: 8px; + right: 8px; + z-index: 3; + + width: 30px; + height: 30px; + + background-color: ${theme.colors.white}; + border-radius: 50%; + transition: background-color 0.3s ease; + + &:hover, + &:active { + background-color: ${theme.colors.secondary.base}; + } + `} +`; + +export const MeatballMenuButton = styled.button` + background-color: transparent; + border: none; +`; + +export const DropBoxButtonList = styled.ul` + ${({ theme }) => css` + display: flex; + flex-direction: column; + row-gap: 8px; + padding: 8px; + + background-color: ${theme.colors.white}; + + & > li:first-of-type { + padding-bottom: 8px; + border-bottom: 1px solid ${theme.colors.secondary.base}; + } + `} +`; + +export const DropBoxButton = styled.button` + padding: 10px; + + background-color: transparent; + border: none; + white-space: nowrap; + + &:hover { + font-weight: 600; + } +`; diff --git a/frontend/src/pages/study-room-page/components/link-room-tab-panel/components/link-item/LinkItem.tsx b/frontend/src/pages/study-room-page/components/link-room-tab-panel/components/link-item/LinkItem.tsx new file mode 100644 index 000000000..4b2a999e0 --- /dev/null +++ b/frontend/src/pages/study-room-page/components/link-room-tab-panel/components/link-item/LinkItem.tsx @@ -0,0 +1,104 @@ +import tw from '@utils/tw'; + +import type { Link, StudyId } from '@custom-types'; + +import type { GetLinkPreviewResponseData } from '@api/link-preview'; + +import DropDownBox from '@components/drop-down-box/DropDownBox'; +import ModalPortal from '@components/modal/Modal'; +import { MeatballMenuSvg } from '@components/svg'; + +import LinkEditForm from '@study-room-page/components/link-room-tab-panel/components/link-edit-form/LinkEditForm'; +import * as S from '@study-room-page/components/link-room-tab-panel/components/link-item/LinkItem.style'; +import { useLinkItem } from '@study-room-page/components/link-room-tab-panel/components/link-item/hooks/useLinkItem'; +import LinkPreview from '@study-room-page/components/link-room-tab-panel/components/link-preview/LinkPreview'; +import UserDescription from '@study-room-page/components/link-room-tab-panel/components/user-description/UserDescription'; + +export type LinkItemProps = Pick & { + studyId: StudyId; +}; + +const LinkItem: React.FC = ({ studyId, id: linkId, linkUrl, author, description }) => { + const { + userInfo, + linkPreviewQueryResult, + isOpenDropBox, + isModalOpen, + handleMeatballMenuClick, + handleDropDownBoxClose, + handleModalClose, + handleEditLinkButtonClick, + handleDeleteLinkButtonClick, + handlePutLinkSuccess, + handlePutLinkError, + } = useLinkItem({ studyId, linkId, linkUrl }); + + const isMyLink = author.id === userInfo.id; + const originalContent = { + linkUrl, + description, + }; + + const renderLinkPreview = () => { + const { data, isError, isSuccess, isFetching } = linkPreviewQueryResult; + + const errorPreviewResult: GetLinkPreviewResponseData = { + title: '%Error%', + description: '링크 불러오기에 실패했습니다 :(', + imageUrl: null, + domainName: null, + }; + if (isFetching) return
    로딩중...
    ; + + if (isError || !isSuccess) return ; + + return ; + }; + + return ( + <> + + {isMyLink && ( + + + + + {isOpenDropBox && ( + + +
  • + + 수정 + +
  • +
  • + + 삭제 + +
  • +
    +
    + )} +
    + )} +
    + {renderLinkPreview()} + + + + {isModalOpen && ( + + + + )} + + ); +}; + +export default LinkItem; diff --git a/frontend/src/pages/study-room-page/components/link-room-tab-panel/components/link-item/hooks/useLinkItem.ts b/frontend/src/pages/study-room-page/components/link-room-tab-panel/components/link-item/hooks/useLinkItem.ts new file mode 100644 index 000000000..2e587e2d2 --- /dev/null +++ b/frontend/src/pages/study-room-page/components/link-room-tab-panel/components/link-item/hooks/useLinkItem.ts @@ -0,0 +1,78 @@ +import { useState } from 'react'; +import { useQueryClient } from 'react-query'; + +import type { LinkId, StudyId } from '@custom-types'; + +import { useDeleteLink } from '@api/link'; +import { useGetLinkPreview } from '@api/link-preview'; +import { QK_LINKS } from '@api/links'; + +import { useUserInfo } from '@hooks/useUserInfo'; + +export type UseLinkItemParams = { + studyId: StudyId; + linkId: LinkId; + linkUrl: string; +}; + +export const useLinkItem = ({ studyId, linkId, linkUrl }: UseLinkItemParams) => { + const { mutate } = useDeleteLink(); + const linkPreviewQueryResult = useGetLinkPreview({ linkUrl }); + + const [isOpenDropBox, setIsOpenDropBox] = useState(false); + const [isModalOpen, setIsModalOpen] = useState(false); + + const { userInfo } = useUserInfo(); + + const queryClient = useQueryClient(); + const refetch = () => { + queryClient.refetchQueries([QK_LINKS, studyId]); + }; + + const handleMeatballMenuClick = () => { + setIsOpenDropBox(prev => !prev); + }; + const handleDropDownBoxClose = () => setIsOpenDropBox(false); + + const handleModalClose = () => setIsModalOpen(false); + const handleEditLinkButtonClick = () => { + setIsModalOpen(true); + }; + const handleDeleteLinkButtonClick = () => { + mutate( + { studyId: Number(studyId), linkId }, + { + onError: () => { + alert('링크를 삭제하지 못했습니다 :('); + }, + onSuccess: () => { + alert('링크를 삭제했습니다'); + refetch(); + }, + }, + ); + }; + + const handlePutLinkSuccess = () => { + alert('링크를 수정했습니다 :D'); + refetch(); + handleModalClose(); + }; + const handlePutLinkError = () => { + alert('링크를 수정하지 못했습니다 :('); + }; + + return { + userInfo, + linkPreviewQueryResult, + isOpenDropBox, + isModalOpen, + handleMeatballMenuClick, + handleDropDownBoxClose, + handleModalClose, + handleEditLinkButtonClick, + handleDeleteLinkButtonClick, + handlePutLinkSuccess, + handlePutLinkError, + }; +}; diff --git a/frontend/src/pages/study-room-page/components/link-room-tab-panel/components/link-preview/LinkPreview.stories.tsx b/frontend/src/pages/study-room-page/components/link-room-tab-panel/components/link-preview/LinkPreview.stories.tsx new file mode 100644 index 000000000..eda8b9400 --- /dev/null +++ b/frontend/src/pages/study-room-page/components/link-room-tab-panel/components/link-preview/LinkPreview.stories.tsx @@ -0,0 +1,31 @@ +import type { Story } from '@storybook/react'; + +import LinkPreview from '@study-room-page/components/link-room-tab-panel/components/link-preview/LinkPreview'; +import type { LinkPreviewProps } from '@study-room-page/components/link-room-tab-panel/components/link-preview/LinkPreview'; + +export default { + title: 'Components/LinkPreview', + component: LinkPreview, +}; + +const Template: Story = props => ( +
    + +
    +); + +export const Default = Template.bind({}); +Default.args = { + previewResult: { + title: '합성 컴포넌트 어쩌구 저쩌구 쏼라쏼라', + description: '카카오 엔터테인먼트 FE 기술 블로그', + imageUrl: + 'https://images.unsplash.com/photo-1572059002053-8cc5ad2f4a38?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxzZWFyY2h8MTV8fGdvb2dsZXxlbnwwfHwwfHw%3D&auto=format&fit=crop&w=800&q=60', + domainName: 'naver.com', + }, +}; diff --git a/frontend/src/pages/study-room-page/components/link-room-tab-panel/components/link-preview/LinkPreview.style.tsx b/frontend/src/pages/study-room-page/components/link-room-tab-panel/components/link-preview/LinkPreview.style.tsx new file mode 100644 index 000000000..daced4cac --- /dev/null +++ b/frontend/src/pages/study-room-page/components/link-room-tab-panel/components/link-preview/LinkPreview.style.tsx @@ -0,0 +1,78 @@ +import { css } from '@emotion/react'; +import styled from '@emotion/styled'; + +const onelineEllipsis = css` + overflow: clip; + text-overflow: ellipsis; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 1; + word-break: break-all; +`; + +export const PreviewContainer = styled.div` + position: relative; + + &:hover { + & > div { + visibility: visible; + opacity: 1; + } + } +`; + +export const PreviewImageContainer = styled.div` + height: 140px; + margin-bottom: 8px; + overflow: hidden; + + border-radius: 15px; +`; + +export const PreviewDomain = styled.div` + ${({ theme }) => css` + position: absolute; + bottom: 62px; + right: 8px; + width: 110px; + height: 30px; + padding: 4px; + + background-color: ${theme.colors.white}; + border-radius: 15px; + text-align: center; + visibility: hidden; + opacity: 0; + transition: visibility 0.2s ease, opacity 0.2s ease; + + & > span { + font-weight: 600; + font-size: 14px; + } + `} + + ${onelineEllipsis} +`; + +export const PreviewContentContainer = styled.div` + padding: 0 8px; +`; + +export const PreviewTitle = styled.p` + margin-bottom: 8px; + + font-weight: 700; + + ${onelineEllipsis} +`; + +export const PreviewDescription = styled.p` + ${({ theme }) => css` + margin-bottom: 8px; + + font-size: 14px; + color: ${theme.colors.secondary.dark}; + `} + + ${onelineEllipsis} +`; diff --git a/frontend/src/pages/study-room-page/components/link-room-tab-panel/components/link-preview/LinkPreview.tsx b/frontend/src/pages/study-room-page/components/link-room-tab-panel/components/link-preview/LinkPreview.tsx new file mode 100644 index 000000000..7f31c00dd --- /dev/null +++ b/frontend/src/pages/study-room-page/components/link-room-tab-panel/components/link-preview/LinkPreview.tsx @@ -0,0 +1,33 @@ +import { GetLinkPreviewResponseData } from '@api/link-preview'; + +import CenterImage from '@components/center-image/CenterImage'; +import { RightUpArrowSvg } from '@components/svg'; + +import * as S from '@study-room-page/components/link-room-tab-panel/components/link-preview/LinkPreview.style'; + +export type LinkPreviewProps = { + previewResult: GetLinkPreviewResponseData; + linkUrl: string; +}; + +const LinkPreview: React.FC = ({ previewResult, linkUrl }) => { + const domain = new URL(linkUrl); + + return ( + + + + + + + {domain.hostname.replace('www.', '')} + + + {previewResult.title} + {previewResult.description ?? previewResult.domainName ?? linkUrl} + + + ); +}; + +export default LinkPreview; diff --git a/frontend/src/pages/study-room-page/components/link-room-tab-panel/components/user-description/UserDescription.stories.tsx b/frontend/src/pages/study-room-page/components/link-room-tab-panel/components/user-description/UserDescription.stories.tsx new file mode 100644 index 000000000..5d1f76966 --- /dev/null +++ b/frontend/src/pages/study-room-page/components/link-room-tab-panel/components/user-description/UserDescription.stories.tsx @@ -0,0 +1,31 @@ +import type { Story } from '@storybook/react'; + +import UserDescription from '@study-room-page/components/link-room-tab-panel/components/user-description/UserDescription'; +import type { UserDescriptionProps } from '@study-room-page/components/link-room-tab-panel/components/user-description/UserDescription'; + +export default { + title: 'Components/UserDescription', + component: UserDescription, +}; + +const Template: Story = props => ( +
    + +
    +); + +export const Default = Template.bind({}); +Default.args = { + author: { + id: 1, + username: 'your-name', + profileUrl: '/', + imageUrl: + 'https://images.unsplash.com/photo-1554151228-14d9def656e4?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxzZWFyY2h8N3x8cGVyc29ufGVufDB8fDB8fA%3D%3D&auto=format&fit=crop&w=800&q=60', + }, + description: '안녕하세요 글자는 약 40자 정도로 하면 좋을 것 같네요. 가나다라마바사아', +}; diff --git a/frontend/src/pages/study-room-page/components/link-room-tab-panel/components/user-description/UserDescription.style.tsx b/frontend/src/pages/study-room-page/components/link-room-tab-panel/components/user-description/UserDescription.style.tsx new file mode 100644 index 000000000..0f4e1ff33 --- /dev/null +++ b/frontend/src/pages/study-room-page/components/link-room-tab-panel/components/user-description/UserDescription.style.tsx @@ -0,0 +1,23 @@ +import { css } from '@emotion/react'; +import styled from '@emotion/styled'; + +const twoLineEllipsis = css` + overflow: clip; + text-overflow: ellipsis; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + word-break: break-all; +`; + +export const Container = styled.div` + display: flex; +`; + +export const Description = styled.p` + margin-left: 8px; + + font-size: 12px; + + ${twoLineEllipsis} +`; diff --git a/frontend/src/pages/study-room-page/components/link-room-tab-panel/components/user-description/UserDescription.tsx b/frontend/src/pages/study-room-page/components/link-room-tab-panel/components/user-description/UserDescription.tsx new file mode 100644 index 000000000..f57e1f41b --- /dev/null +++ b/frontend/src/pages/study-room-page/components/link-room-tab-panel/components/user-description/UserDescription.tsx @@ -0,0 +1,22 @@ +import type { Member } from '@custom-types'; + +import Avatar from '@components/avatar/Avatar'; + +import * as S from '@study-room-page/components/link-room-tab-panel/components/user-description/UserDescription.style'; + +export type UserDescriptionProps = { + className?: string; + author: Member; + description: string; +}; + +const UserDescription: React.FC = ({ author, description, className }) => { + return ( + + + {description} + + ); +}; + +export default UserDescription; diff --git a/frontend/src/pages/study-room-page/components/link-room-tab-panel/hooks/useLinkRoomTabPanel.ts b/frontend/src/pages/study-room-page/components/link-room-tab-panel/hooks/useLinkRoomTabPanel.ts new file mode 100644 index 000000000..68ce51954 --- /dev/null +++ b/frontend/src/pages/study-room-page/components/link-room-tab-panel/hooks/useLinkRoomTabPanel.ts @@ -0,0 +1,43 @@ +import { useEffect, useState } from 'react'; +import { useParams } from 'react-router-dom'; + +import { useGetInfiniteLinks } from '@api/links'; + +import { useUserInfo } from '@hooks/useUserInfo'; + +export const useLinkRoomTabPanel = () => { + const { studyId } = useParams<{ studyId: string }>(); + const { fetchUserInfo, userInfo } = useUserInfo(); + const infiniteLinksQueryResult = useGetInfiniteLinks({ studyId: Number(studyId) }); + + const [isModalOpen, setIsModalOpen] = useState(false); + + useEffect(() => { + fetchUserInfo(); + }, []); + + const handleLinkAddButtonClick = () => setIsModalOpen(prev => !prev); + const handleModalClose = () => setIsModalOpen(false); + + const handlePostLinkSuccess = () => { + alert('링크를 등록했습니다 :D'); + infiniteLinksQueryResult.refetch(); + handleModalClose(); + }; + + const handlePostLinkError = () => { + alert('링크를 등록하지 못했습니다 :('); + handleModalClose(); + }; + + return { + studyId: Number(studyId), + userInfo, + infiniteLinksQueryResult, + isModalOpen, + handleLinkAddButtonClick, + handleModalClose, + handlePostLinkSuccess, + handlePostLinkError, + }; +}; diff --git a/frontend/src/pages/study-room-page/components/review-tab-panel/ReviewTabPanel.tsx b/frontend/src/pages/study-room-page/components/review-tab-panel/ReviewTabPanel.tsx index 60752fed1..5f3c2a57b 100644 --- a/frontend/src/pages/study-room-page/components/review-tab-panel/ReviewTabPanel.tsx +++ b/frontend/src/pages/study-room-page/components/review-tab-panel/ReviewTabPanel.tsx @@ -1,12 +1,12 @@ import { useEffect } from 'react'; +import { useGetStudyReviews } from '@api/reviews'; + import { useUserInfo } from '@hooks/useUserInfo'; import Divider from '@components/divider/Divider'; import Wrapper from '@components/wrapper/Wrapper'; -import useGetStudyReviews from '@detail-page/hooks/useGetStudyReviews'; - import * as S from '@study-room-page/components/review-tab-panel/ReviewTabPanel.style'; import ReviewForm from '@study-room-page/components/review-tab-panel/components/reivew-form/ReviewForm'; import ReviewComment from '@study-room-page/components/review-tab-panel/components/review-comment/ReviewComment'; diff --git a/frontend/src/pages/study-room-page/components/review-tab-panel/components/reivew-form/ReviewForm.stories.tsx b/frontend/src/pages/study-room-page/components/review-tab-panel/components/reivew-form/ReviewForm.stories.tsx index 356b92ccf..61cfea344 100644 --- a/frontend/src/pages/study-room-page/components/review-tab-panel/components/reivew-form/ReviewForm.stories.tsx +++ b/frontend/src/pages/study-room-page/components/review-tab-panel/components/reivew-form/ReviewForm.stories.tsx @@ -12,3 +12,13 @@ export default { const Template: Story = props => ; export const Default = Template.bind({}); +Default.args = { + studyId: 1, + author: { + id: 1, + username: 'your-name', + profileUrl: '/', + imageUrl: + 'https://images.unsplash.com/photo-1554151228-14d9def656e4?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxzZWFyY2h8N3x8cGVyc29ufGVufDB8fDB8fA%3D%3D&auto=format&fit=crop&w=800&q=60', + }, +}; diff --git a/frontend/src/pages/study-room-page/components/review-tab-panel/components/reivew-form/ReviewForm.tsx b/frontend/src/pages/study-room-page/components/review-tab-panel/components/reivew-form/ReviewForm.tsx index 1c24a661d..6b954ba3c 100644 --- a/frontend/src/pages/study-room-page/components/review-tab-panel/components/reivew-form/ReviewForm.tsx +++ b/frontend/src/pages/study-room-page/components/review-tab-panel/components/reivew-form/ReviewForm.tsx @@ -1,14 +1,11 @@ -import type { AxiosError } from 'axios'; -import { useMutation } from 'react-query'; - import { REVIEW_LENGTH } from '@constants'; -import type { Member, PostReviewRequestVariables, StudyId } from '@custom-types'; +import type { Member, Noop, StudyId } from '@custom-types'; -import { postReview } from '@api'; +import { usePostReview } from '@api/review'; import { makeValidationResult, useForm } from '@hooks/useForm'; -import type { UseFormSubmitResult } from '@hooks/useForm'; +import type { FieldElement, UseFormSubmitResult } from '@hooks/useForm'; import Avatar from '@components/avatar/Avatar'; import { Button } from '@components/button/Button.style'; @@ -20,14 +17,14 @@ import * as S from '@study-room-page/components/review-tab-panel/components/reiv export type ReviewFormProps = { studyId: StudyId; author: Member; - onPostSuccess: () => void; + onPostSuccess: Noop; onPostError: (e: Error) => void; }; const ReviewForm: React.FC = ({ studyId, author, onPostSuccess, onPostError }) => { const { count, setCount, maxCount } = useLetterCount(REVIEW_LENGTH.MAX.VALUE); const { register, handleSubmit, reset } = useForm(); - const { mutateAsync } = useMutation(postReview); + const { mutateAsync } = usePostReview(); const onSubmit = async (_: React.FormEvent, submitResult: UseFormSubmitResult) => { if (!submitResult.values) { @@ -50,6 +47,8 @@ const ReviewForm: React.FC = ({ studyId, author, onPostSuccess, ); }; + const handleReviewChange = ({ target: { value } }: React.ChangeEvent) => setCount(value.length); + return ( @@ -69,7 +68,7 @@ const ReviewForm: React.FC = ({ studyId, author, onPostSuccess, return makeValidationResult(false); }, validationMode: 'change', - onChange: e => setCount(e.target.value.length), + onChange: handleReviewChange, minLength: REVIEW_LENGTH.MIN.VALUE, maxLength: REVIEW_LENGTH.MAX.VALUE, })} diff --git a/frontend/src/pages/study-room-page/components/review-tab-panel/components/review-comment/ReviewComment.style.tsx b/frontend/src/pages/study-room-page/components/review-tab-panel/components/review-comment/ReviewComment.style.tsx index cf52e5921..e9dc46ec6 100644 --- a/frontend/src/pages/study-room-page/components/review-tab-panel/components/review-comment/ReviewComment.style.tsx +++ b/frontend/src/pages/study-room-page/components/review-tab-panel/components/review-comment/ReviewComment.style.tsx @@ -22,49 +22,34 @@ export const Date = styled.span` `} `; -type DropDownProps = { - isOpen?: boolean; -}; - export const Content = styled.div` line-height: 20px; `; -export const DropDownMenu = styled.ul` - ${({ theme, isOpen }) => css` - position: absolute; - top: calc(100% + 3px); - right: 6px; - z-index: 3; +export const KebabMenuContainer = styled.div` + position: relative; +`; - display: ${isOpen ? 'flex' : 'none'}; +export const DropBoxButtonList = styled.ul` + ${({ theme }) => css` + display: flex; flex-direction: column; row-gap: 8px; - - font-size: 24px; - padding: 10px; - - background-color: ${theme.colors.white}; - border: 1px solid ${theme.colors.secondary.dark}; - border-radius: 5px; + padding: 8px; & > li:first-of-type { padding-bottom: 8px; border-bottom: 1px solid ${theme.colors.secondary.base}; } - - button { - padding: 10px; - - background-color: transparent; - border: none; - white-space: nowrap; - } `} `; -export const DropDown = styled.div` - position: relative; +export const DropBoxButton = styled.button` + padding: 10px; + + background-color: transparent; + border: none; + white-space: nowrap; `; export const ReviewCommentHead = styled.div` diff --git a/frontend/src/pages/study-room-page/components/review-tab-panel/components/review-comment/ReviewComment.tsx b/frontend/src/pages/study-room-page/components/review-tab-panel/components/review-comment/ReviewComment.tsx index cb74ca92f..f2b41f0fa 100644 --- a/frontend/src/pages/study-room-page/components/review-tab-panel/components/review-comment/ReviewComment.tsx +++ b/frontend/src/pages/study-room-page/components/review-tab-panel/components/review-comment/ReviewComment.tsx @@ -3,6 +3,7 @@ import { changeDateSeperator } from '@utils'; import type { DateYMD, Member, ReviewId, StudyId } from '@custom-types'; import Avatar from '@components/avatar/Avatar'; +import DropDownBox from '@components/drop-down-box/DropDownBox'; import KebabMenu from '@components/kebab-menu/KebabMenu'; import * as S from '@study-room-page/components/review-tab-panel/components/review-comment/ReviewComment.style'; @@ -22,7 +23,8 @@ const ReviewComment: React.FC = ({ id, studyId, author, date const { isOpen, isEditing, - handleDropDownClick, + handleKebabMenuClick, + handleDropDownBoxClose, handleEditReviewBtnClick, handleDeleteReviewBtnClick, handleCancelEditBtnClick, @@ -59,17 +61,25 @@ const ReviewComment: React.FC = ({ id, studyId, author, date {isMyComment && ( - - - -
  • - -
  • -
  • - -
  • -
    -
    + + + {isOpen && ( + + +
  • + + 수정 + +
  • +
  • + + 삭제 + +
  • +
    +
    + )} +
    )} diff --git a/frontend/src/pages/study-room-page/components/review-tab-panel/components/review-comment/useReviewComment.ts b/frontend/src/pages/study-room-page/components/review-tab-panel/components/review-comment/useReviewComment.ts index 0179a4290..12483e304 100644 --- a/frontend/src/pages/study-room-page/components/review-tab-panel/components/review-comment/useReviewComment.ts +++ b/frontend/src/pages/study-room-page/components/review-tab-panel/components/review-comment/useReviewComment.ts @@ -1,25 +1,27 @@ -import type { AxiosError } from 'axios'; -import { useCallback, useEffect, useState } from 'react'; -import { useMutation, useQueryClient } from 'react-query'; +import { useState } from 'react'; +import { useQueryClient } from 'react-query'; -import { QK_FETCH_STUDY_REVIEWS } from '@constants'; +import type { ReviewId, StudyId } from '@custom-types'; -import type { DeleteReviewRequestBody, ReviewId, StudyId } from '@custom-types'; - -import { deleteReview } from '@api'; +import { useDeleteReview } from '@api/review'; +import { QK_STUDY_REVIEWS } from '@api/reviews'; const useReviewComment = (id: ReviewId, studyId: StudyId) => { const [isOpen, setIsOpen] = useState(false); const [isEditing, setIsEditing] = useState(false); - const { mutateAsync } = useMutation(deleteReview); + const { mutateAsync } = useDeleteReview(); const queryClient = useQueryClient(); const refetch = () => { - queryClient.refetchQueries([QK_FETCH_STUDY_REVIEWS, studyId]); + queryClient.refetchQueries([QK_STUDY_REVIEWS, studyId]); }; - const handleDropDownClick = useCallback(() => { + const handleKebabMenuClick = () => { setIsOpen(prev => !prev); - }, []); + }; + + const handleDropDownBoxClose = () => { + setIsOpen(false); + }; const handleDeleteReviewBtnClick = () => { mutateAsync({ reviewId: id, studyId }) @@ -49,23 +51,12 @@ const useReviewComment = (id: ReviewId, studyId: StudyId) => { alert('수정에 에러가 발생했습니다!'); }; - useEffect(() => { - document.removeEventListener('click', handleDropDownClick); - if (isOpen) { - // 이벤트 전파가 끝나기 전에 document에 click event listener가 붙기 때문에 - // click event listener를 add하는 일을 다음 frame으로 늦춘다 - // Test: https://codepen.io/airman5573/pen/qBopRpO - requestAnimationFrame(() => { - document.addEventListener('click', handleDropDownClick); - }); - } - }, [isOpen, handleDropDownClick]); - return { isOpen, isEditing, setIsEditing, - handleDropDownClick, + handleKebabMenuClick, + handleDropDownBoxClose, handleDeleteReviewBtnClick, handleEditReviewBtnClick, handleCancelEditBtnClick, diff --git a/frontend/src/pages/study-room-page/components/review-tab-panel/components/review-edit-form/ReviewEditForm.tsx b/frontend/src/pages/study-room-page/components/review-tab-panel/components/review-edit-form/ReviewEditForm.tsx index 3d27821f7..1a6c6774e 100644 --- a/frontend/src/pages/study-room-page/components/review-tab-panel/components/review-edit-form/ReviewEditForm.tsx +++ b/frontend/src/pages/study-room-page/components/review-tab-panel/components/review-edit-form/ReviewEditForm.tsx @@ -1,16 +1,13 @@ -import type { AxiosError } from 'axios'; -import { useMutation } from 'react-query'; - import { REVIEW_LENGTH } from '@constants'; import { changeDateSeperator } from '@utils'; -import type { DateYMD, Member, PutReviewRequestVariables, ReviewId, StudyId } from '@custom-types'; +import type { DateYMD, Member, Noop, ReviewId, StudyId } from '@custom-types'; -import { putReview } from '@api'; +import { usePutReview } from '@api/review'; import { makeValidationResult, useForm } from '@hooks/useForm'; -import type { UseFormSubmitResult } from '@hooks/useForm'; +import type { FieldElement, UseFormSubmitResult } from '@hooks/useForm'; import Avatar from '@components/avatar/Avatar'; import Button from '@components/button/Button'; @@ -25,9 +22,9 @@ export type ReviewEditFormProps = { originalContent: string; date: DateYMD; author: Member; - onEditSuccess: () => void; + onEditSuccess: Noop; onEditError: (e: Error) => void; - onCancelEditBtnClick: () => void; + onCancelEditBtnClick: Noop; }; const ReviewEditForm: React.FC = ({ @@ -42,7 +39,7 @@ const ReviewEditForm: React.FC = ({ }) => { const { count, setCount, maxCount } = useLetterCount(REVIEW_LENGTH.MAX.VALUE, originalContent.length); const { register, handleSubmit } = useForm(); - const { mutateAsync } = useMutation(putReview); + const { mutateAsync } = usePutReview(); const onSubmit = async (_: React.FormEvent, submitResult: UseFormSubmitResult) => { if (!submitResult.values) { @@ -64,6 +61,8 @@ const ReviewEditForm: React.FC = ({ ); }; + const handleReviewChange = ({ target: { value } }: React.ChangeEvent) => setCount(value.length); + return ( @@ -89,7 +88,7 @@ const ReviewEditForm: React.FC = ({ return makeValidationResult(false); }, validationMode: 'change', - onChange: e => setCount(e.target.value.length), + onChange: handleReviewChange, minLength: REVIEW_LENGTH.MIN.VALUE, maxLength: REVIEW_LENGTH.MAX.VALUE, })} diff --git a/frontend/src/pages/study-room-page/components/side-menu/SideMenu.stories.tsx b/frontend/src/pages/study-room-page/components/side-menu/SideMenu.stories.tsx index 79b524729..3ed727e2c 100644 --- a/frontend/src/pages/study-room-page/components/side-menu/SideMenu.stories.tsx +++ b/frontend/src/pages/study-room-page/components/side-menu/SideMenu.stories.tsx @@ -13,7 +13,7 @@ export default { const Template: Story = () => { const tabs: Tabs = [ { id: 'notice', name: '공지사항', content: '공지사항입니다.' }, - { id: 'material', name: '자료실', content: '자료실입니다.' }, + { id: 'link-room', name: '자료실', content: '자료실입니다.' }, { id: 'review', name: '후기', content: '후기입니다.' }, ]; diff --git a/frontend/src/pages/study-room-page/components/side-menu/SideMenu.style.tsx b/frontend/src/pages/study-room-page/components/side-menu/SideMenu.style.tsx index 6c070aa3f..d906ebe54 100644 --- a/frontend/src/pages/study-room-page/components/side-menu/SideMenu.style.tsx +++ b/frontend/src/pages/study-room-page/components/side-menu/SideMenu.style.tsx @@ -1,6 +1,8 @@ import { css } from '@emotion/react'; import styled from '@emotion/styled'; +import { mqDown, mqUp } from '@utils'; + export const Sidebar = styled.nav` ${({ theme }) => css` width: 100%; @@ -9,6 +11,33 @@ export const Sidebar = styled.nav` background-color: ${theme.colors.secondary.light}; text-align: center; + + ${mqDown('lg')} { + display: none; + } + `} +`; + +export const Bottombar = styled.nav` + ${({ theme }) => css` + display: flex; + align-items: space-between; + column-gap: 16px; + + position: fixed; + left: 0; + bottom: 0; z-index: 1; + + width: 100%; + padding: 16px; + + background-color: ${theme.colors.white}; + text-align: center; + border-top: 1px solid ${theme.colors.secondary.base}; + + ${mqUp('lg')} { + display: none; + } `} `; diff --git a/frontend/src/pages/study-room-page/components/side-menu/SideMenu.tsx b/frontend/src/pages/study-room-page/components/side-menu/SideMenu.tsx index 091bb72b6..e5e8e0ff5 100644 --- a/frontend/src/pages/study-room-page/components/side-menu/SideMenu.tsx +++ b/frontend/src/pages/study-room-page/components/side-menu/SideMenu.tsx @@ -22,13 +22,22 @@ const SideMenu: React.FC = ({ onTabButtonClick: handleTabButtonClick, }) => { return ( - - {tabs.map(({ id, name }) => ( - - {name} - - ))} - + <> + + {tabs.map(({ id, name }) => ( + + {name} + + ))} + + + {tabs.map(({ id, name }) => ( + + {name} + + ))} + + ); }; diff --git a/frontend/src/pages/study-room-page/components/tab-button/TabButton.stories.tsx b/frontend/src/pages/study-room-page/components/tab-button/TabButton.stories.tsx index d7e71ecc6..ec223b0b2 100644 --- a/frontend/src/pages/study-room-page/components/tab-button/TabButton.stories.tsx +++ b/frontend/src/pages/study-room-page/components/tab-button/TabButton.stories.tsx @@ -33,17 +33,3 @@ Default.args = { isSelected: false, }; Default.parameters = { controls: { exclude: ['isSelected', 'onClick'] } }; - -export const Clicked = Template.bind({}); -Clicked.args = { - children: '공지사항', - isSelected: true, -}; -Clicked.parameters = { controls: { exclude: ['isSelected', 'onClick'] } }; - -export const Unclicked = Template.bind({}); -Unclicked.args = { - children: '공지사항', - isSelected: false, -}; -Unclicked.parameters = { controls: { exclude: ['isSelected', 'onClick'] } }; diff --git a/frontend/src/pages/study-room-page/components/tab-button/TabButton.style.tsx b/frontend/src/pages/study-room-page/components/tab-button/TabButton.style.tsx index 8b3dc7065..b616c3627 100644 --- a/frontend/src/pages/study-room-page/components/tab-button/TabButton.style.tsx +++ b/frontend/src/pages/study-room-page/components/tab-button/TabButton.style.tsx @@ -1,7 +1,7 @@ import { Theme, css } from '@emotion/react'; import styled from '@emotion/styled'; -import { TabButtonProps } from './TabButton'; +import { TabButtonProps } from '@study-room-page/components/tab-button/TabButton'; const applySelectedStyle = (theme: Theme) => css` color: ${theme.colors.primary.base}; diff --git a/frontend/src/pages/study-room-page/hooks/useStudyRoomPage.tsx b/frontend/src/pages/study-room-page/hooks/useStudyRoomPage.tsx index 60f3ff660..b41a8b08e 100644 --- a/frontend/src/pages/study-room-page/hooks/useStudyRoomPage.tsx +++ b/frontend/src/pages/study-room-page/hooks/useStudyRoomPage.tsx @@ -1,15 +1,12 @@ -import type { AxiosError } from 'axios'; import { useState } from 'react'; -import { useQuery } from 'react-query'; import { useParams } from 'react-router-dom'; -import type { GetUserRoleResponseData } from '@custom-types'; - -import { getUserRole } from '@api'; +import { useGetUserRole } from '@api/member'; +import LinkRoomTabPanel from '@study-room-page/components/link-room-tab-panel/LinkRoomTabPanel'; import ReviewTabPanel from '@study-room-page/components/review-tab-panel/ReviewTabPanel'; -export type TabId = 'notice' | 'material' | 'review'; +export type TabId = 'notice' | 'link-room' | 'review'; export type Tab = { id: TabId; name: string; content: React.ReactNode }; @@ -18,13 +15,11 @@ export type Tabs = Array; const useStudyRoomPage = () => { const { studyId } = useParams() as { studyId: string }; - const userRoleQueryResult = useQuery('my-role', () => - getUserRole({ studyId: Number(studyId) }), - ); + const userRoleQueryResult = useGetUserRole({ studyId: Number(studyId) }); const tabs: Tabs = [ { id: 'notice', name: '공지사항', content: '공지사항입니다.' }, - { id: 'material', name: '자료실', content: '자료실입니다.' }, + { id: 'link-room', name: '링크 모음', content: }, { id: 'review', name: '후기', content: }, ]; diff --git a/frontend/src/styles/Globalstyles.tsx b/frontend/src/styles/Globalstyles.tsx index 51722e7f5..7c40e7033 100644 --- a/frontend/src/styles/Globalstyles.tsx +++ b/frontend/src/styles/Globalstyles.tsx @@ -188,7 +188,8 @@ const GlobalStyles = () => { resize: none; } - input { + input, + textarea { background-color: ${theme.colors.white}; &::placeholder { color: ${theme.colors.secondary.dark}; diff --git a/frontend/src/utils/tw.ts b/frontend/src/utils/tw.ts index 2bcc1a374..0a86d8b06 100644 --- a/frontend/src/utils/tw.ts +++ b/frontend/src/utils/tw.ts @@ -33,6 +33,7 @@ const flex = { 'flex-auto': 'flex: 1 1 auto', 'flex-initial': 'flex: 0 1 auto', 'flex-none': 'flex: none', + 'flex-col': 'flex-direction: column', }; const justifyContent = { diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index e2546b315..b16509733 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -44,5 +44,5 @@ }, "typeRoots": ["src/custom-types"] }, - "include": ["src", "node_modules/cypress", "cypress/**/*.cy.ts", "cypress/**/*.cy.tsx"] + "include": ["src"] } diff --git a/frontend/webpack/webpack.common.js b/frontend/webpack/webpack.common.js index 89d379890..cc5d78a15 100644 --- a/frontend/webpack/webpack.common.js +++ b/frontend/webpack/webpack.common.js @@ -35,6 +35,19 @@ module.exports = { new HtmlWebpackPlugin({ template: join(__dirname, '../public/index.html'), favicon: join(__dirname, '../public/favicon.png'), + meta: { + description: { name: 'description', contnet: '스터디를 쉽고, 편리하게! 스터디 아카이브 모아모아입니다' }, + 'og:title': { property: 'og:title', content: '모아모아' }, + 'og:type': { property: 'og:type', content: 'website' }, + 'og:description': { + name: 'og:description', + contnet: '스터디를 쉽고, 편리하게! 스터디 아카이브 모아모아입니다', + }, + 'og:url': { property: 'og:url', content: 'https://moamoa.space' }, + 'og:image': { property: 'og:image', content: '%PUBLIC_URL%/open-graph-image.png' }, + 'og:image:width': { property: 'og:url', content: 1200 }, + 'og:image:height': { property: 'og:url', content: 630 }, + }, }), new CleanWebpackPlugin(), ], diff --git a/frontend/webpack/webpack.dev.js b/frontend/webpack/webpack.dev.js index c6bf3d46a..fda7c7a4d 100644 --- a/frontend/webpack/webpack.dev.js +++ b/frontend/webpack/webpack.dev.js @@ -14,6 +14,7 @@ module.exports = merge(common, { new webpack.DefinePlugin({ 'process.env.API_URL': JSON.stringify(process.env.API_URL), 'process.env.CLIENT_ID': JSON.stringify(process.env.CLIENT_ID), + 'process.env.LINK_PREVIEW_API_URL': JSON.stringify(process.env.LINK_PREVIEW_API_URL), }), ], }); diff --git a/frontend/webpack/webpack.local.js b/frontend/webpack/webpack.local.js index 4412f35f5..f500df405 100644 --- a/frontend/webpack/webpack.local.js +++ b/frontend/webpack/webpack.local.js @@ -26,6 +26,7 @@ module.exports = merge(common, { new webpack.DefinePlugin({ 'process.env.API_URL': JSON.stringify(process.env.API_URL), 'process.env.CLIENT_ID': JSON.stringify(process.env.CLIENT_ID), + 'process.env.LINK_PREVIEW_API_URL': JSON.stringify(process.env.LINK_PREVIEW_API_URL), }), ], }); diff --git a/frontend/webpack/webpack.prod.js b/frontend/webpack/webpack.prod.js index 2def125e4..a7a13a82c 100644 --- a/frontend/webpack/webpack.prod.js +++ b/frontend/webpack/webpack.prod.js @@ -13,6 +13,7 @@ module.exports = merge(common, { new webpack.DefinePlugin({ 'process.env.API_URL': JSON.stringify(process.env.API_URL), 'process.env.CLIENT_ID': JSON.stringify(process.env.CLIENT_ID), + 'process.env.LINK_PREVIEW_API_URL': JSON.stringify(process.env.LINK_PREVIEW_API_URL), }), ], }); From c1889d403190dea3906c1d2fc92935d7685034c6 Mon Sep 17 00:00:00 2001 From: Donggyu Date: Wed, 17 Aug 2022 20:48:50 +0900 Subject: [PATCH 29/51] =?UTF-8?q?[BE]=20issue255:=20=EC=8A=A4=ED=84=B0?= =?UTF-8?q?=EB=94=94=20=EC=83=81=EC=84=B8=20=EC=A0=95=EB=B3=B4=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20(#278)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 임시 커밋 * chore: 충돌 해결 * feat: 스터디 업데이트 구현 * refactor: 피드백 반영 * docs: asciidocs 추가 * refactor: 피드백 반영 --- backend/src/docs/asciidoc/index.adoc | 3 + .../auth/config/AuthRequestMatchConfig.java | 3 +- .../study/controller/StudyController.java | 17 +++- .../moamoa/study/domain/AttachedTag.java | 2 + .../moamoa/study/domain/Study.java | 17 ++++ .../moamoa/study/service/StudyService.java | 15 +++- ...ingStudyRequest.java => StudyRequest.java} | 2 +- ...tBuilder.java => StudyRequestBuilder.java} | 24 +++--- .../acceptance/steps/AfterLoginSteps.java | 14 ++-- .../acceptance/steps/CreatingStudyStep.java | 6 +- .../SetRequiredDataToCreatingStudySteps.java | 6 +- .../study/UpdatingStudyAcceptanceTest.java | 58 +++++++++++++ .../CommunityArticleControllerTest.java | 4 +- ...eletingCommunityArticleControllerTest.java | 9 +- ...GettingCommunityArticleControllerTest.java | 9 +- ...mmunityArticleSummariesControllerTest.java | 8 +- ...pdatingCommunityArticleControllerTest.java | 8 +- .../moamoa/fixtures/StudyFixtures.java | 30 +++---- .../ReferenceRoomControllerTest.java | 4 +- .../SearchingReferenceRoomControllerTest.java | 4 +- .../referenceroom/query/LinkDaoTest.java | 4 +- .../controller/ReviewControllerTest.java | 4 +- .../SearchingReviewControllerTest.java | 6 +- .../moamoa/review/query/ReviewDaoTest.java | 6 +- .../SearchingStudyControllerTest.java | 14 ++-- .../study/controller/StudyControllerTest.java | 84 +++++++++++++++---- .../moamoa/study/domain/StudyTest.java | 26 ++++-- .../schedule/AutoUpdateStatusTaskTest.java | 14 ++-- 28 files changed, 275 insertions(+), 126 deletions(-) rename backend/src/main/java/com/woowacourse/moamoa/study/service/request/{CreatingStudyRequest.java => StudyRequest.java} (98%) rename backend/src/main/java/com/woowacourse/moamoa/study/service/request/{CreatingStudyRequestBuilder.java => StudyRequestBuilder.java} (54%) create mode 100644 backend/src/test/java/com/woowacourse/acceptance/test/study/UpdatingStudyAcceptanceTest.java diff --git a/backend/src/docs/asciidoc/index.adoc b/backend/src/docs/asciidoc/index.adoc index 8c0633fc0..d615ecaa7 100644 --- a/backend/src/docs/asciidoc/index.adoc +++ b/backend/src/docs/asciidoc/index.adoc @@ -51,6 +51,9 @@ operation::studies/details[snippets='http-request,http-response'] === 참여한 스터디 목록 조회 operation::studies/myStudy[snippets='http-request,http-response'] +=== 스터디 상세 정보 수정 +operation::studies/update[snippets='http-request,http-response'] + [[Comunity]] == 커뮤니티 diff --git a/backend/src/main/java/com/woowacourse/moamoa/auth/config/AuthRequestMatchConfig.java b/backend/src/main/java/com/woowacourse/moamoa/auth/config/AuthRequestMatchConfig.java index 5d037fc64..eef1c8e22 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/auth/config/AuthRequestMatchConfig.java +++ b/backend/src/main/java/com/woowacourse/moamoa/auth/config/AuthRequestMatchConfig.java @@ -31,7 +31,8 @@ public AuthenticationRequestMatcher authenticationRequestMatcher() { ) .addUpAuthenticationPath(HttpMethod.PUT, "/api/studies/\\d+/reviews/\\d+", - "/api/studies/\\d+/reference-room/links/\\d+" + "/api/studies/\\d+/reference-room/links/\\d+", + "/api/study/\\d+" ) .addUpAuthenticationPath(HttpMethod.DELETE, "/api/studies/\\d+/reviews/\\d+", diff --git a/backend/src/main/java/com/woowacourse/moamoa/study/controller/StudyController.java b/backend/src/main/java/com/woowacourse/moamoa/study/controller/StudyController.java index af85f87a3..57a4e3a64 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/study/controller/StudyController.java +++ b/backend/src/main/java/com/woowacourse/moamoa/study/controller/StudyController.java @@ -1,15 +1,17 @@ package com.woowacourse.moamoa.study.controller; +import com.woowacourse.moamoa.auth.config.AuthenticatedMember; import com.woowacourse.moamoa.auth.config.AuthenticationPrincipal; import com.woowacourse.moamoa.study.domain.Study; import com.woowacourse.moamoa.study.service.StudyService; -import com.woowacourse.moamoa.study.service.request.CreatingStudyRequest; +import com.woowacourse.moamoa.study.service.request.StudyRequest; import java.net.URI; import javax.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -24,9 +26,9 @@ public class StudyController { @PostMapping public ResponseEntity createStudy( @AuthenticationPrincipal final Long githubId, - @Valid @RequestBody(required = false) final CreatingStudyRequest creatingStudyRequest + @Valid @RequestBody(required = false) final StudyRequest studyRequest ) { - final Study study = studyService.createStudy(githubId, creatingStudyRequest); + final Study study = studyService.createStudy(githubId, studyRequest); return ResponseEntity.created(URI.create("/api/studies/" + study.getId())).build(); } @@ -37,4 +39,13 @@ public ResponseEntity participateStudy(@AuthenticationPrincipal final Long studyService.participateStudy(githubId, studyId); return ResponseEntity.ok().build(); } + + @PutMapping("/{study-id}") + public ResponseEntity updateStudy(@AuthenticatedMember final Long memberId, + @PathVariable("study-id") final Long studyId, + @Valid @RequestBody(required = false) final StudyRequest request + ) { + studyService.updateStudy(memberId, studyId, request); + return ResponseEntity.ok().build(); + } } diff --git a/backend/src/main/java/com/woowacourse/moamoa/study/domain/AttachedTag.java b/backend/src/main/java/com/woowacourse/moamoa/study/domain/AttachedTag.java index 9c27e56e9..24affbbfc 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/study/domain/AttachedTag.java +++ b/backend/src/main/java/com/woowacourse/moamoa/study/domain/AttachedTag.java @@ -5,6 +5,7 @@ import javax.persistence.Column; import javax.persistence.Embeddable; import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; @@ -12,6 +13,7 @@ @NoArgsConstructor(access = PROTECTED) @AllArgsConstructor @Getter +@EqualsAndHashCode public class AttachedTag { @Column(name = "tag_id", nullable = false) diff --git a/backend/src/main/java/com/woowacourse/moamoa/study/domain/Study.java b/backend/src/main/java/com/woowacourse/moamoa/study/domain/Study.java index b9a12accb..307bfb1cc 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/study/domain/Study.java +++ b/backend/src/main/java/com/woowacourse/moamoa/study/domain/Study.java @@ -3,6 +3,7 @@ import static javax.persistence.GenerationType.IDENTITY; import static lombok.AccessLevel.PROTECTED; +import com.woowacourse.moamoa.common.exception.UnauthorizedException; import com.woowacourse.moamoa.study.domain.exception.InvalidPeriodException; import com.woowacourse.moamoa.study.service.exception.FailureParticipationException; import java.time.LocalDate; @@ -129,4 +130,20 @@ public MemberRole getRole(final Long memberId) { } return MemberRole.NON_MEMBER; } + + public void update(Long memberId, Content content, RecruitPlanner recruitPlanner, AttachedTags attachedTags, + StudyPlanner studyPlanner + ) { + checkOwner(memberId); + this.content = content; + this.recruitPlanner = recruitPlanner; + this.attachedTags = attachedTags; + this.studyPlanner = studyPlanner; + } + + private void checkOwner(Long memberId) { + if (!participants.isOwner(memberId)) { + throw new UnauthorizedException("스터디 방장만이 스터디를 수정할 수 있습니다."); + } + } } diff --git a/backend/src/main/java/com/woowacourse/moamoa/study/service/StudyService.java b/backend/src/main/java/com/woowacourse/moamoa/study/service/StudyService.java index 23fe93b6a..290d09bee 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/study/service/StudyService.java +++ b/backend/src/main/java/com/woowacourse/moamoa/study/service/StudyService.java @@ -12,7 +12,7 @@ import com.woowacourse.moamoa.study.domain.StudyPlanner; import com.woowacourse.moamoa.study.domain.repository.StudyRepository; import com.woowacourse.moamoa.study.service.exception.StudyNotFoundException; -import com.woowacourse.moamoa.study.service.request.CreatingStudyRequest; +import com.woowacourse.moamoa.study.service.request.StudyRequest; import java.time.LocalDate; import java.time.LocalDateTime; import java.util.List; @@ -36,7 +36,7 @@ public StudyService(final StudyRepository studyRepository, this.dateTimeSystem = dateTimeSystem; } - public Study createStudy(final Long githubId, final CreatingStudyRequest request) { + public Study createStudy(final Long githubId, final StudyRequest request) { final LocalDateTime createdAt = dateTimeSystem.now(); final Member owner = findMemberBy(githubId); @@ -47,7 +47,8 @@ public Study createStudy(final Long githubId, final CreatingStudyRequest request final AttachedTags attachedTags = request.mapToAttachedTags(); final Content content = request.mapToContent(); - return studyRepository.save(new Study(content, participants, recruitPlanner, studyPlanner, attachedTags, createdAt)); + return studyRepository.save( + new Study(content, participants, recruitPlanner, studyPlanner, attachedTags, createdAt)); } public void participateStudy(final Long githubId, final Long studyId) { @@ -74,4 +75,12 @@ public void autoUpdateStatus() { study.changeStatus(now); } } + + public void updateStudy(Long memberId, Long studyId, StudyRequest request) { + Study study = studyRepository.findById(studyId) + .orElseThrow(StudyNotFoundException::new); + + study.update(memberId, request.mapToContent(), request.mapToRecruitPlan(), request.mapToAttachedTags(), + request.mapToStudyPlanner(LocalDate.now())); + } } diff --git a/backend/src/main/java/com/woowacourse/moamoa/study/service/request/CreatingStudyRequest.java b/backend/src/main/java/com/woowacourse/moamoa/study/service/request/StudyRequest.java similarity index 98% rename from backend/src/main/java/com/woowacourse/moamoa/study/service/request/CreatingStudyRequest.java rename to backend/src/main/java/com/woowacourse/moamoa/study/service/request/StudyRequest.java index b1266a0c2..4dc47d579 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/study/service/request/CreatingStudyRequest.java +++ b/backend/src/main/java/com/woowacourse/moamoa/study/service/request/StudyRequest.java @@ -27,7 +27,7 @@ @NoArgsConstructor @Getter @Builder -public class CreatingStudyRequest { +public class StudyRequest { @NotBlank private String title; diff --git a/backend/src/main/java/com/woowacourse/moamoa/study/service/request/CreatingStudyRequestBuilder.java b/backend/src/main/java/com/woowacourse/moamoa/study/service/request/StudyRequestBuilder.java similarity index 54% rename from backend/src/main/java/com/woowacourse/moamoa/study/service/request/CreatingStudyRequestBuilder.java rename to backend/src/main/java/com/woowacourse/moamoa/study/service/request/StudyRequestBuilder.java index 2de4346b8..a4d4b0ac4 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/study/service/request/CreatingStudyRequestBuilder.java +++ b/backend/src/main/java/com/woowacourse/moamoa/study/service/request/StudyRequestBuilder.java @@ -3,7 +3,7 @@ import java.time.LocalDate; import java.util.List; -public class CreatingStudyRequestBuilder { +public class StudyRequestBuilder { private String title; @@ -23,53 +23,53 @@ public class CreatingStudyRequestBuilder { private List tagIds; - public CreatingStudyRequestBuilder title(final String title) { + public StudyRequestBuilder title(final String title) { this.title = title; return this; } - public CreatingStudyRequestBuilder excerpt(final String excerpt) { + public StudyRequestBuilder excerpt(final String excerpt) { this.excerpt = excerpt; return this; } - public CreatingStudyRequestBuilder thumbnail(final String thumbnail) { + public StudyRequestBuilder thumbnail(final String thumbnail) { this.thumbnail = thumbnail; return this; } - public CreatingStudyRequestBuilder description(final String description) { + public StudyRequestBuilder description(final String description) { this.description = description; return this; } - public CreatingStudyRequestBuilder maxMemberCount(final Integer maxMemberCount) { + public StudyRequestBuilder maxMemberCount(final Integer maxMemberCount) { this.maxMemberCount = maxMemberCount; return this; } - public CreatingStudyRequestBuilder startDate(final LocalDate startDate) { + public StudyRequestBuilder startDate(final LocalDate startDate) { this.startDate = startDate; return this; } - public CreatingStudyRequestBuilder enrollmentEndDate(final LocalDate enrollmentEndDate) { + public StudyRequestBuilder enrollmentEndDate(final LocalDate enrollmentEndDate) { this.enrollmentEndDate = enrollmentEndDate; return this; } - public CreatingStudyRequestBuilder endDate(final LocalDate endDate) { + public StudyRequestBuilder endDate(final LocalDate endDate) { this.endDate = endDate; return this; } - public CreatingStudyRequestBuilder tagIds(final List tagIds) { + public StudyRequestBuilder tagIds(final List tagIds) { this.tagIds = tagIds; return this; } - public CreatingStudyRequest build() { - return new CreatingStudyRequest(title, excerpt, thumbnail, description, maxMemberCount, startDate, + public StudyRequest build() { + return new StudyRequest(title, excerpt, thumbnail, description, maxMemberCount, startDate, enrollmentEndDate, endDate, tagIds); } } diff --git a/backend/src/test/java/com/woowacourse/acceptance/steps/AfterLoginSteps.java b/backend/src/test/java/com/woowacourse/acceptance/steps/AfterLoginSteps.java index 2180a3b2b..ac07acb65 100644 --- a/backend/src/test/java/com/woowacourse/acceptance/steps/AfterLoginSteps.java +++ b/backend/src/test/java/com/woowacourse/acceptance/steps/AfterLoginSteps.java @@ -25,7 +25,7 @@ import static com.woowacourse.acceptance.fixture.StudyFixtures.자바스크립트_스터디_요약; import static com.woowacourse.acceptance.fixture.StudyFixtures.자바스크립트_스터디_제목; -import com.woowacourse.moamoa.study.service.request.CreatingStudyRequestBuilder; +import com.woowacourse.moamoa.study.service.request.StudyRequestBuilder; public class AfterLoginSteps extends Steps { @@ -36,21 +36,21 @@ public class AfterLoginSteps extends Steps { } public SetRequiredDataToCreatingStudySteps 자바_스터디를() { - CreatingStudyRequestBuilder builder = new CreatingStudyRequestBuilder() + StudyRequestBuilder builder = new StudyRequestBuilder() .title(자바_스터디_제목).excerpt(자바_스터디_요약).description(자바_스터디_설명).thumbnail(자바_스터디_썸네일); return new SetRequiredDataToCreatingStudySteps(token, builder); } public SetRequiredDataToCreatingStudySteps 리액트_스터디를() { - CreatingStudyRequestBuilder builder = new CreatingStudyRequestBuilder() + StudyRequestBuilder builder = new StudyRequestBuilder() .title(리액트_스터디_제목).excerpt(리액트_스터디_요약).description(리액트_스터디_설명).thumbnail(리액트_스터디_썸네일); return new SetRequiredDataToCreatingStudySteps(token, builder); } public SetRequiredDataToCreatingStudySteps 자바스크립트_스터디를() { - CreatingStudyRequestBuilder builder = new CreatingStudyRequestBuilder() + StudyRequestBuilder builder = new StudyRequestBuilder() .title(자바스크립트_스터디_제목).excerpt(자바스크립트_스터디_요약) .description(자바스크립트_스터디_설명).thumbnail(자바스크립트_스터디_썸네일); @@ -58,7 +58,7 @@ public class AfterLoginSteps extends Steps { } public SetRequiredDataToCreatingStudySteps HTTP_스터디를() { - CreatingStudyRequestBuilder builder = new CreatingStudyRequestBuilder() + StudyRequestBuilder builder = new StudyRequestBuilder() .title(HTTP_스터디_제목).excerpt(HTTP_스터디_요약) .description(HTTP_스터디_설명).thumbnail(HTTP_스터디_썸네일); @@ -66,7 +66,7 @@ public class AfterLoginSteps extends Steps { } public SetRequiredDataToCreatingStudySteps 알고리즘_스터디를() { - CreatingStudyRequestBuilder builder = new CreatingStudyRequestBuilder() + StudyRequestBuilder builder = new StudyRequestBuilder() .title(알고리즘_스터디_제목).excerpt(알고리즘_스터디_요약) .description(알고리즘_스터디_설명).thumbnail(알고리즘_스터디_썸네일); @@ -74,7 +74,7 @@ public class AfterLoginSteps extends Steps { } public SetRequiredDataToCreatingStudySteps 리눅스_스터디를() { - CreatingStudyRequestBuilder builder = new CreatingStudyRequestBuilder() + StudyRequestBuilder builder = new StudyRequestBuilder() .title(리눅스_스터디_제목).excerpt(리눅스_스터디_요약) .description(리눅스_스터디_설명).thumbnail(리눅스_스터디_썸네일); diff --git a/backend/src/test/java/com/woowacourse/acceptance/steps/CreatingStudyStep.java b/backend/src/test/java/com/woowacourse/acceptance/steps/CreatingStudyStep.java index 315c0c149..7cf4aa93d 100644 --- a/backend/src/test/java/com/woowacourse/acceptance/steps/CreatingStudyStep.java +++ b/backend/src/test/java/com/woowacourse/acceptance/steps/CreatingStudyStep.java @@ -1,6 +1,6 @@ package com.woowacourse.acceptance.steps; -import com.woowacourse.moamoa.study.service.request.CreatingStudyRequestBuilder; +import com.woowacourse.moamoa.study.service.request.StudyRequestBuilder; import io.restassured.RestAssured; import java.time.LocalDate; import java.util.List; @@ -12,9 +12,9 @@ public class CreatingStudyStep extends Steps { private final String token; - private final CreatingStudyRequestBuilder builder; + private final StudyRequestBuilder builder; - CreatingStudyStep(final String token, final CreatingStudyRequestBuilder builder) { + CreatingStudyStep(final String token, final StudyRequestBuilder builder) { this.token = token; this.builder = builder; } diff --git a/backend/src/test/java/com/woowacourse/acceptance/steps/SetRequiredDataToCreatingStudySteps.java b/backend/src/test/java/com/woowacourse/acceptance/steps/SetRequiredDataToCreatingStudySteps.java index 7f9d35370..05d88f12d 100644 --- a/backend/src/test/java/com/woowacourse/acceptance/steps/SetRequiredDataToCreatingStudySteps.java +++ b/backend/src/test/java/com/woowacourse/acceptance/steps/SetRequiredDataToCreatingStudySteps.java @@ -1,14 +1,14 @@ package com.woowacourse.acceptance.steps; -import com.woowacourse.moamoa.study.service.request.CreatingStudyRequestBuilder; +import com.woowacourse.moamoa.study.service.request.StudyRequestBuilder; import java.time.LocalDate; public class SetRequiredDataToCreatingStudySteps extends Steps { private final String token; - private final CreatingStudyRequestBuilder builder; + private final StudyRequestBuilder builder; - SetRequiredDataToCreatingStudySteps(final String token, final CreatingStudyRequestBuilder builder) { + SetRequiredDataToCreatingStudySteps(final String token, final StudyRequestBuilder builder) { this.token = token; this.builder = builder; } diff --git a/backend/src/test/java/com/woowacourse/acceptance/test/study/UpdatingStudyAcceptanceTest.java b/backend/src/test/java/com/woowacourse/acceptance/test/study/UpdatingStudyAcceptanceTest.java new file mode 100644 index 000000000..850378fc7 --- /dev/null +++ b/backend/src/test/java/com/woowacourse/acceptance/test/study/UpdatingStudyAcceptanceTest.java @@ -0,0 +1,58 @@ +package com.woowacourse.acceptance.test.study; + +import static com.woowacourse.acceptance.fixture.TagFixtures.BE_태그_ID; +import static com.woowacourse.acceptance.fixture.TagFixtures.우테코4기_태그_ID; +import static com.woowacourse.acceptance.fixture.TagFixtures.자바_태그_ID; +import static com.woowacourse.acceptance.steps.LoginSteps.짱구가; +import static org.springframework.http.HttpHeaders.ACCEPT; +import static org.springframework.http.HttpHeaders.AUTHORIZATION; +import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; +import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; +import static org.springframework.restdocs.restassured3.RestAssuredRestDocumentation.document; + +import com.woowacourse.acceptance.AcceptanceTest; +import com.woowacourse.moamoa.study.service.request.StudyRequest; +import com.woowacourse.moamoa.study.service.request.StudyRequestBuilder; +import io.restassured.RestAssured; +import java.time.LocalDate; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; + +public class UpdatingStudyAcceptanceTest extends AcceptanceTest { + + @DisplayName("스터디 내용을 수정할 수 있다.") + @Test + public void updateStudy() { + final LocalDate 지금 = LocalDate.now(); + final long studyId = 짱구가().로그인하고().자바_스터디를() + .시작일자는(지금).태그는(자바_태그_ID, 우테코4기_태그_ID, BE_태그_ID) + .생성한다(); + final String accessToken = 짱구가().로그인한다(); + + final StudyRequest request = new StudyRequestBuilder().title("변경된 제목") + .description("변경된 설명") + .excerpt("변경된 한 줄 설명") + .thumbnail("변경된 썸네일") + .startDate(LocalDate.now()) + .endDate(LocalDate.now().plusMonths(1)) + .enrollmentEndDate(LocalDate.now().plusDays(5)) + .tagIds(List.of(자바_태그_ID, 우테코4기_태그_ID)) + .build(); + + RestAssured.given(spec).log().all() + .filter(document("studies/update", + requestHeaders(headerWithName("Authorization").description("Bearer Token")))) + .header(AUTHORIZATION, accessToken) + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) + .header(ACCEPT, MediaType.APPLICATION_JSON_VALUE) + .pathParam("study-id", studyId) + .body(request) + .when().log().all() + .put("/api/studies/{study-id}") + .then().statusCode(HttpStatus.OK.value()); + } +} diff --git a/backend/src/test/java/com/woowacourse/moamoa/community/controller/CommunityArticleControllerTest.java b/backend/src/test/java/com/woowacourse/moamoa/community/controller/CommunityArticleControllerTest.java index 05a187dd4..0692ed6c4 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/community/controller/CommunityArticleControllerTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/community/controller/CommunityArticleControllerTest.java @@ -17,7 +17,7 @@ import com.woowacourse.moamoa.study.domain.repository.StudyRepository; import com.woowacourse.moamoa.study.service.StudyService; import com.woowacourse.moamoa.study.service.exception.StudyNotFoundException; -import com.woowacourse.moamoa.study.service.request.CreatingStudyRequestBuilder; +import com.woowacourse.moamoa.study.service.request.StudyRequestBuilder; import java.time.LocalDate; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -29,7 +29,7 @@ @RepositoryTest public class CommunityArticleControllerTest { - CreatingStudyRequestBuilder javaStudyRequest = new CreatingStudyRequestBuilder() + StudyRequestBuilder javaStudyRequest = new StudyRequestBuilder() .title("java 스터디").excerpt("자바 설명").thumbnail("java image").description("자바 소개"); @Autowired diff --git a/backend/src/test/java/com/woowacourse/moamoa/community/controller/DeletingCommunityArticleControllerTest.java b/backend/src/test/java/com/woowacourse/moamoa/community/controller/DeletingCommunityArticleControllerTest.java index 3778aaa87..8d3ea958a 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/community/controller/DeletingCommunityArticleControllerTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/community/controller/DeletingCommunityArticleControllerTest.java @@ -10,19 +10,14 @@ import com.woowacourse.moamoa.community.query.CommunityArticleDao; import com.woowacourse.moamoa.community.service.CommunityArticleService; import com.woowacourse.moamoa.community.service.exception.ArticleNotFoundException; -import com.woowacourse.moamoa.community.service.exception.NotArticleAuthorException; -import com.woowacourse.moamoa.community.service.exception.NotRelatedArticleException; import com.woowacourse.moamoa.community.service.exception.UneditableArticleException; import com.woowacourse.moamoa.community.service.request.ArticleRequest; import com.woowacourse.moamoa.member.domain.Member; import com.woowacourse.moamoa.member.domain.repository.MemberRepository; -import com.woowacourse.moamoa.member.service.exception.MemberNotFoundException; -import com.woowacourse.moamoa.member.service.exception.NotParticipatedMemberException; import com.woowacourse.moamoa.study.domain.Study; import com.woowacourse.moamoa.study.domain.repository.StudyRepository; import com.woowacourse.moamoa.study.service.StudyService; -import com.woowacourse.moamoa.study.service.exception.StudyNotFoundException; -import com.woowacourse.moamoa.study.service.request.CreatingStudyRequestBuilder; +import com.woowacourse.moamoa.study.service.request.StudyRequestBuilder; import java.time.LocalDate; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -32,7 +27,7 @@ @RepositoryTest public class DeletingCommunityArticleControllerTest { - CreatingStudyRequestBuilder javaStudyRequest = new CreatingStudyRequestBuilder() + StudyRequestBuilder javaStudyRequest = new StudyRequestBuilder() .title("java 스터디").excerpt("자바 설명").thumbnail("java image").description("자바 소개"); @Autowired diff --git a/backend/src/test/java/com/woowacourse/moamoa/community/controller/GettingCommunityArticleControllerTest.java b/backend/src/test/java/com/woowacourse/moamoa/community/controller/GettingCommunityArticleControllerTest.java index 24e9bfffd..1f8bac2eb 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/community/controller/GettingCommunityArticleControllerTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/community/controller/GettingCommunityArticleControllerTest.java @@ -10,22 +10,17 @@ import com.woowacourse.moamoa.community.query.CommunityArticleDao; import com.woowacourse.moamoa.community.service.CommunityArticleService; import com.woowacourse.moamoa.community.service.exception.ArticleNotFoundException; -import com.woowacourse.moamoa.community.service.exception.NotRelatedArticleException; import com.woowacourse.moamoa.community.service.exception.UnviewableArticleException; import com.woowacourse.moamoa.community.service.request.ArticleRequest; import com.woowacourse.moamoa.community.service.response.ArticleResponse; import com.woowacourse.moamoa.community.service.response.AuthorResponse; import com.woowacourse.moamoa.member.domain.Member; import com.woowacourse.moamoa.member.domain.repository.MemberRepository; -import com.woowacourse.moamoa.member.service.exception.MemberNotFoundException; -import com.woowacourse.moamoa.member.service.exception.NotParticipatedMemberException; import com.woowacourse.moamoa.study.domain.Study; import com.woowacourse.moamoa.study.domain.repository.StudyRepository; import com.woowacourse.moamoa.study.service.StudyService; -import com.woowacourse.moamoa.study.service.exception.StudyNotFoundException; -import com.woowacourse.moamoa.study.service.request.CreatingStudyRequestBuilder; +import com.woowacourse.moamoa.study.service.request.StudyRequestBuilder; import java.time.LocalDate; -import javax.persistence.EntityManager; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -36,7 +31,7 @@ @RepositoryTest public class GettingCommunityArticleControllerTest { - CreatingStudyRequestBuilder javaStudyRequest = new CreatingStudyRequestBuilder() + StudyRequestBuilder javaStudyRequest = new StudyRequestBuilder() .title("java 스터디").excerpt("자바 설명").thumbnail("java image").description("자바 소개"); @Autowired diff --git a/backend/src/test/java/com/woowacourse/moamoa/community/controller/GettingCommunityArticleSummariesControllerTest.java b/backend/src/test/java/com/woowacourse/moamoa/community/controller/GettingCommunityArticleSummariesControllerTest.java index 54ba3455a..2fcd84051 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/community/controller/GettingCommunityArticleSummariesControllerTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/community/controller/GettingCommunityArticleSummariesControllerTest.java @@ -9,7 +9,6 @@ import com.woowacourse.moamoa.community.domain.repository.CommunityArticleRepository; import com.woowacourse.moamoa.community.query.CommunityArticleDao; import com.woowacourse.moamoa.community.service.CommunityArticleService; -import com.woowacourse.moamoa.community.service.exception.ArticleNotFoundException; import com.woowacourse.moamoa.community.service.exception.UnviewableArticleException; import com.woowacourse.moamoa.community.service.request.ArticleRequest; import com.woowacourse.moamoa.community.service.response.ArticleSummariesResponse; @@ -17,14 +16,11 @@ import com.woowacourse.moamoa.community.service.response.AuthorResponse; import com.woowacourse.moamoa.member.domain.Member; import com.woowacourse.moamoa.member.domain.repository.MemberRepository; -import com.woowacourse.moamoa.member.service.exception.MemberNotFoundException; -import com.woowacourse.moamoa.member.service.exception.NotParticipatedMemberException; import com.woowacourse.moamoa.study.domain.Study; import com.woowacourse.moamoa.study.domain.repository.StudyRepository; import com.woowacourse.moamoa.study.service.StudyService; import com.woowacourse.moamoa.study.service.exception.StudyNotFoundException; -import com.woowacourse.moamoa.study.service.request.CreatingStudyRequestBuilder; -import java.lang.reflect.Field; +import com.woowacourse.moamoa.study.service.request.StudyRequestBuilder; import java.time.LocalDate; import java.util.List; import org.junit.jupiter.api.BeforeEach; @@ -38,7 +34,7 @@ @RepositoryTest public class GettingCommunityArticleSummariesControllerTest { - CreatingStudyRequestBuilder javaStudyRequest = new CreatingStudyRequestBuilder() + StudyRequestBuilder javaStudyRequest = new StudyRequestBuilder() .title("java 스터디").excerpt("자바 설명").thumbnail("java image").description("자바 소개"); private CommunityArticleService communityArticleService; diff --git a/backend/src/test/java/com/woowacourse/moamoa/community/controller/UpdatingCommunityArticleControllerTest.java b/backend/src/test/java/com/woowacourse/moamoa/community/controller/UpdatingCommunityArticleControllerTest.java index ea0d3cacd..874a961ee 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/community/controller/UpdatingCommunityArticleControllerTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/community/controller/UpdatingCommunityArticleControllerTest.java @@ -10,18 +10,14 @@ import com.woowacourse.moamoa.community.query.CommunityArticleDao; import com.woowacourse.moamoa.community.service.CommunityArticleService; import com.woowacourse.moamoa.community.service.exception.ArticleNotFoundException; -import com.woowacourse.moamoa.community.service.exception.NotArticleAuthorException; -import com.woowacourse.moamoa.community.service.exception.NotRelatedArticleException; import com.woowacourse.moamoa.community.service.exception.UneditableArticleException; import com.woowacourse.moamoa.community.service.request.ArticleRequest; import com.woowacourse.moamoa.member.domain.Member; import com.woowacourse.moamoa.member.domain.repository.MemberRepository; -import com.woowacourse.moamoa.member.service.exception.NotParticipatedMemberException; import com.woowacourse.moamoa.study.domain.Study; import com.woowacourse.moamoa.study.domain.repository.StudyRepository; import com.woowacourse.moamoa.study.service.StudyService; -import com.woowacourse.moamoa.study.service.exception.StudyNotFoundException; -import com.woowacourse.moamoa.study.service.request.CreatingStudyRequestBuilder; +import com.woowacourse.moamoa.study.service.request.StudyRequestBuilder; import java.time.LocalDate; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -33,7 +29,7 @@ @RepositoryTest public class UpdatingCommunityArticleControllerTest { - CreatingStudyRequestBuilder javaStudyBuilder = new CreatingStudyRequestBuilder() + StudyRequestBuilder javaStudyBuilder = new StudyRequestBuilder() .title("java 스터디").excerpt("자바 설명").thumbnail("java image").description("자바 소개"); @Autowired diff --git a/backend/src/test/java/com/woowacourse/moamoa/fixtures/StudyFixtures.java b/backend/src/test/java/com/woowacourse/moamoa/fixtures/StudyFixtures.java index 5c4190028..3dd865003 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/fixtures/StudyFixtures.java +++ b/backend/src/test/java/com/woowacourse/moamoa/fixtures/StudyFixtures.java @@ -38,7 +38,7 @@ import com.woowacourse.moamoa.study.domain.RecruitPlanner; import com.woowacourse.moamoa.study.domain.Study; import com.woowacourse.moamoa.study.domain.StudyPlanner; -import com.woowacourse.moamoa.study.service.request.CreatingStudyRequest; +import com.woowacourse.moamoa.study.service.request.StudyRequest; import java.time.LocalDate; import java.time.LocalDateTime; import java.util.List; @@ -149,51 +149,51 @@ public class StudyFixtures { OS_스터디_계획, OS_스터디_태그, LocalDateTime.now()); } - public static CreatingStudyRequest 자바_스터디_신청서(LocalDate now) { - return CreatingStudyRequest.builder() + public static StudyRequest 자바_스터디_신청서(LocalDate now) { + return StudyRequest.builder() .title("java 스터디").excerpt("자바 설명").thumbnail("java image").description("자바 소개") .startDate(now) .build(); } - public static CreatingStudyRequest 자바_스터디_신청서(List tagIds, int maxMemberCount, LocalDate now) { - return CreatingStudyRequest.builder() + public static StudyRequest 자바_스터디_신청서(List tagIds, int maxMemberCount, LocalDate now) { + return StudyRequest.builder() .title("Java 스터디").excerpt("자바 설명").thumbnail("java thumbnail").description("그린론의 우당탕탕 자바 스터디입니다.") .startDate(now).tagIds(tagIds).maxMemberCount(maxMemberCount) .build(); } - public static CreatingStudyRequest 리액트_스터디_신청서(LocalDate now) { - return CreatingStudyRequest.builder() + public static StudyRequest 리액트_스터디_신청서(LocalDate now) { + return StudyRequest.builder() .title("react 스터디").excerpt("리액트 설명").thumbnail("react image").description("리액트 소개") .startDate(now) .build(); } - public static CreatingStudyRequest 리액트_스터디_신청서(List tagIds, int maxMemberCount, LocalDate now) { - return CreatingStudyRequest.builder() + public static StudyRequest 리액트_스터디_신청서(List tagIds, int maxMemberCount, LocalDate now) { + return StudyRequest.builder() .title("React 스터디").excerpt("리액트 설명").thumbnail("react thumbnail").description("디우의 뤼액트 스터디입니다.") .startDate(LocalDate.now()).endDate(now).enrollmentEndDate(LocalDate.now()) .tagIds(tagIds).maxMemberCount(maxMemberCount) .build(); } - public static CreatingStudyRequest 자바스크립트_스터디_신청서(List tagIds, LocalDate now) { - return CreatingStudyRequest.builder() + public static StudyRequest 자바스크립트_스터디_신청서(List tagIds, LocalDate now) { + return StudyRequest.builder() .title("javaScript 스터디").excerpt("자바스크립트 설명").thumbnail("javascript thumbnail").description("자바스크립트 설명") .startDate(now).tagIds(tagIds) .build(); } - public static CreatingStudyRequest HTTP_스터디_신청서(List tagIds, LocalDate now) { - return CreatingStudyRequest.builder() + public static StudyRequest HTTP_스터디_신청서(List tagIds, LocalDate now) { + return StudyRequest.builder() .title("HTTP 스터디").excerpt("HTTP 설명").thumbnail("http thumbnail").description("HTTP 설명") .startDate(now).tagIds(tagIds) .build(); } - public static CreatingStudyRequest 알고리즘_스터디_신청서(List tagIds, LocalDate now) { - return CreatingStudyRequest.builder() + public static StudyRequest 알고리즘_스터디_신청서(List tagIds, LocalDate now) { + return StudyRequest.builder() .title("알고리즘 스터디").excerpt("알고리즘 설명").thumbnail("algorithm thumbnail").description("알고리즘 설명") .startDate(now).tagIds(tagIds) .build(); diff --git a/backend/src/test/java/com/woowacourse/moamoa/referenceroom/controller/ReferenceRoomControllerTest.java b/backend/src/test/java/com/woowacourse/moamoa/referenceroom/controller/ReferenceRoomControllerTest.java index 59651a7d9..dcd398c27 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/referenceroom/controller/ReferenceRoomControllerTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/referenceroom/controller/ReferenceRoomControllerTest.java @@ -21,7 +21,7 @@ import com.woowacourse.moamoa.referenceroom.service.request.EditingLinkRequest; import com.woowacourse.moamoa.study.domain.repository.StudyRepository; import com.woowacourse.moamoa.study.service.StudyService; -import com.woowacourse.moamoa.study.service.request.CreatingStudyRequest; +import com.woowacourse.moamoa.study.service.request.StudyRequest; import java.time.LocalDate; import javax.persistence.EntityManager; import org.junit.jupiter.api.BeforeEach; @@ -64,7 +64,7 @@ void setUp() { // 스터디 생성 final StudyService studyService = new StudyService(studyRepository, memberRepository, new DateTimeSystem()); final LocalDate startDate = LocalDate.now(); - final CreatingStudyRequest javaStudyRequest = 자바_스터디_신청서(startDate); + final StudyRequest javaStudyRequest = 자바_스터디_신청서(startDate); javaStudyId = studyService.createStudy(짱구_깃허브_아이디, javaStudyRequest).getId(); studyService.participateStudy(베루스_깃허브_아이디, javaStudyId); diff --git a/backend/src/test/java/com/woowacourse/moamoa/referenceroom/controller/SearchingReferenceRoomControllerTest.java b/backend/src/test/java/com/woowacourse/moamoa/referenceroom/controller/SearchingReferenceRoomControllerTest.java index 6a4ef8766..8cbe6f695 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/referenceroom/controller/SearchingReferenceRoomControllerTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/referenceroom/controller/SearchingReferenceRoomControllerTest.java @@ -36,7 +36,7 @@ import com.woowacourse.moamoa.study.domain.Study; import com.woowacourse.moamoa.study.domain.repository.StudyRepository; import com.woowacourse.moamoa.study.service.StudyService; -import com.woowacourse.moamoa.study.service.request.CreatingStudyRequest; +import com.woowacourse.moamoa.study.service.request.StudyRequest; import java.time.LocalDate; import java.util.List; import javax.persistence.EntityManager; @@ -88,7 +88,7 @@ void setUp() { StudyService studyService = new StudyService(studyRepository, memberRepository, new DateTimeSystem()); final LocalDate startDate = LocalDate.now(); - CreatingStudyRequest javaStudyRequest = 자바_스터디_신청서(startDate); + StudyRequest javaStudyRequest = 자바_스터디_신청서(startDate); javaStudy = studyService.createStudy(짱구_깃허브_아이디, javaStudyRequest); diff --git a/backend/src/test/java/com/woowacourse/moamoa/referenceroom/query/LinkDaoTest.java b/backend/src/test/java/com/woowacourse/moamoa/referenceroom/query/LinkDaoTest.java index 655b683a3..e491a8e0f 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/referenceroom/query/LinkDaoTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/referenceroom/query/LinkDaoTest.java @@ -17,7 +17,7 @@ import com.woowacourse.moamoa.study.domain.Study; import com.woowacourse.moamoa.study.domain.repository.StudyRepository; import com.woowacourse.moamoa.study.service.StudyService; -import com.woowacourse.moamoa.study.service.request.CreatingStudyRequest; +import com.woowacourse.moamoa.study.service.request.StudyRequest; import java.time.LocalDate; import java.util.List; import javax.persistence.EntityManager; @@ -67,7 +67,7 @@ void setUp() { StudyService createStudyService = new StudyService(studyRepository, memberRepository, new DateTimeSystem()); final LocalDate startDate = LocalDate.now(); - CreatingStudyRequest javaStudyRequest = 자바_스터디_신청서(startDate); + StudyRequest javaStudyRequest = 자바_스터디_신청서(startDate); javaStudy = createStudyService.createStudy(JJANGGU.getGithubId(), javaStudyRequest); createStudyService.participateStudy(GREENLAWN.getGithubId(), javaStudy.getId()); diff --git a/backend/src/test/java/com/woowacourse/moamoa/review/controller/ReviewControllerTest.java b/backend/src/test/java/com/woowacourse/moamoa/review/controller/ReviewControllerTest.java index ff15c2af3..340a35678 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/review/controller/ReviewControllerTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/review/controller/ReviewControllerTest.java @@ -21,7 +21,7 @@ import com.woowacourse.moamoa.study.domain.Study; import com.woowacourse.moamoa.study.domain.repository.StudyRepository; import com.woowacourse.moamoa.study.service.StudyService; -import com.woowacourse.moamoa.study.service.request.CreatingStudyRequest; +import com.woowacourse.moamoa.study.service.request.StudyRequest; import java.time.LocalDate; import javax.persistence.EntityManager; import org.junit.jupiter.api.BeforeEach; @@ -60,7 +60,7 @@ void setUp() { StudyService studyService = new StudyService(studyRepository, memberRepository, new DateTimeSystem()); final LocalDate startDate = LocalDate.now(); - CreatingStudyRequest javaStudyRequest = CreatingStudyRequest.builder() + StudyRequest javaStudyRequest = StudyRequest.builder() .title("java 스터디").excerpt("자바 설명").thumbnail("java image").description("자바 소개") .startDate(startDate) .build(); diff --git a/backend/src/test/java/com/woowacourse/moamoa/review/controller/SearchingReviewControllerTest.java b/backend/src/test/java/com/woowacourse/moamoa/review/controller/SearchingReviewControllerTest.java index 5b0c92ecc..3237cddbb 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/review/controller/SearchingReviewControllerTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/review/controller/SearchingReviewControllerTest.java @@ -28,7 +28,7 @@ import com.woowacourse.moamoa.study.domain.Study; import com.woowacourse.moamoa.study.domain.repository.StudyRepository; import com.woowacourse.moamoa.study.service.StudyService; -import com.woowacourse.moamoa.study.service.request.CreatingStudyRequest; +import com.woowacourse.moamoa.study.service.request.StudyRequest; import java.time.LocalDate; import java.util.List; import javax.persistence.EntityManager; @@ -77,8 +77,8 @@ void setUp() { StudyService studyService = new StudyService(studyRepository, memberRepository, new DateTimeSystem()); final LocalDate startDate = LocalDate.now(); - CreatingStudyRequest javaStudyRequest = 자바_스터디_신청서(startDate); - CreatingStudyRequest reactStudyRequest = 리액트_스터디_신청서(startDate); + StudyRequest javaStudyRequest = 자바_스터디_신청서(startDate); + StudyRequest reactStudyRequest = 리액트_스터디_신청서(startDate); javaStudy = studyService.createStudy(1L, javaStudyRequest); final Study reactStudy = studyService.createStudy(1L, reactStudyRequest); diff --git a/backend/src/test/java/com/woowacourse/moamoa/review/query/ReviewDaoTest.java b/backend/src/test/java/com/woowacourse/moamoa/review/query/ReviewDaoTest.java index 72f3dea96..9d67e4e93 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/review/query/ReviewDaoTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/review/query/ReviewDaoTest.java @@ -34,7 +34,7 @@ import com.woowacourse.moamoa.study.domain.Study; import com.woowacourse.moamoa.study.domain.repository.StudyRepository; import com.woowacourse.moamoa.study.service.StudyService; -import com.woowacourse.moamoa.study.service.request.CreatingStudyRequest; +import com.woowacourse.moamoa.study.service.request.StudyRequest; import java.time.LocalDate; import java.util.List; import javax.persistence.EntityManager; @@ -86,8 +86,8 @@ void setUp() { StudyService createStudyService = new StudyService(studyRepository, memberRepository, new DateTimeSystem()); final LocalDate startDate = LocalDate.now(); - CreatingStudyRequest javaStudyRequest = 자바_스터디_신청서(startDate); - CreatingStudyRequest reactStudyRequest = 리액트_스터디_신청서(startDate); + StudyRequest javaStudyRequest = 자바_스터디_신청서(startDate); + StudyRequest reactStudyRequest = 리액트_스터디_신청서(startDate); javaStudy = createStudyService.createStudy(1L, javaStudyRequest); reactStudy = createStudyService.createStudy(1L, reactStudyRequest); diff --git a/backend/src/test/java/com/woowacourse/moamoa/study/controller/SearchingStudyControllerTest.java b/backend/src/test/java/com/woowacourse/moamoa/study/controller/SearchingStudyControllerTest.java index ad6f22e0a..8012154fe 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/study/controller/SearchingStudyControllerTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/study/controller/SearchingStudyControllerTest.java @@ -21,7 +21,7 @@ import com.woowacourse.moamoa.study.query.data.StudyDetailsData; import com.woowacourse.moamoa.study.service.SearchingStudyService; import com.woowacourse.moamoa.study.service.StudyService; -import com.woowacourse.moamoa.study.service.request.CreatingStudyRequest; +import com.woowacourse.moamoa.study.service.request.StudyRequest; import com.woowacourse.moamoa.study.service.response.StudiesResponse; import com.woowacourse.moamoa.study.service.response.StudyDetailResponse; import com.woowacourse.moamoa.tag.query.TagDao; @@ -84,22 +84,22 @@ void initDataBase() { StudyService studyService = new StudyService(studyRepository, memberRepository, new DateTimeSystem()); - CreatingStudyRequest javaStudyRequest = 자바_스터디_신청서(List.of(1L, 2L, 3L), 10, LocalDate.now()); + StudyRequest javaStudyRequest = 자바_스터디_신청서(List.of(1L, 2L, 3L), 10, LocalDate.now()); javaStudyId = studyService.createStudy(jjanggu.getGithubId(), javaStudyRequest).getId(); - CreatingStudyRequest reactStudyRequest = 리액트_스터디_신청서(List.of(2L, 4L, 5L), 5, LocalDate.now()); + StudyRequest reactStudyRequest = 리액트_스터디_신청서(List.of(2L, 4L, 5L), 5, LocalDate.now()); reactStudyId = studyService.createStudy(dwoo.getGithubId(), reactStudyRequest).getId(); - CreatingStudyRequest javaScriptStudyRequest = 자바스크립트_스터디_신청서(List.of(2L, 4L), LocalDate.now()); + StudyRequest javaScriptStudyRequest = 자바스크립트_스터디_신청서(List.of(2L, 4L), LocalDate.now()); javaScriptId = studyService.createStudy(jjanggu.getGithubId(), javaScriptStudyRequest).getId(); - CreatingStudyRequest httpStudyRequest = HTTP_스터디_신청서(List.of(2L, 3L), LocalDate.now()); + StudyRequest httpStudyRequest = HTTP_스터디_신청서(List.of(2L, 3L), LocalDate.now()); httpStudyId = studyService.createStudy(jjanggu.getGithubId(), httpStudyRequest).getId(); - CreatingStudyRequest algorithmStudyRequest = 알고리즘_스터디_신청서(List.of(), LocalDate.now()); + StudyRequest algorithmStudyRequest = 알고리즘_스터디_신청서(List.of(), LocalDate.now()); algorithmStudyId = studyService.createStudy(jjanggu.getGithubId(), algorithmStudyRequest).getId(); - CreatingStudyRequest linuxStudyRequest = CreatingStudyRequest.builder() + StudyRequest linuxStudyRequest = StudyRequest.builder() .title("Linux 스터디").excerpt("리눅스 설명").thumbnail("linux thumbnail").description("Linux를 공부하자의 베루스입니다.") .startDate(LocalDate.now()).endDate(LocalDate.now()).enrollmentEndDate(LocalDate.now()) .tagIds(List.of()) diff --git a/backend/src/test/java/com/woowacourse/moamoa/study/controller/StudyControllerTest.java b/backend/src/test/java/com/woowacourse/moamoa/study/controller/StudyControllerTest.java index 053af15c9..3622bf45c 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/study/controller/StudyControllerTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/study/controller/StudyControllerTest.java @@ -12,6 +12,7 @@ import com.woowacourse.moamoa.common.utils.DateTimeSystem; import com.woowacourse.moamoa.member.domain.Member; import com.woowacourse.moamoa.member.domain.repository.MemberRepository; +import com.woowacourse.moamoa.study.domain.AttachedTag; import com.woowacourse.moamoa.study.domain.Content; import com.woowacourse.moamoa.study.domain.Participants; import com.woowacourse.moamoa.study.domain.Study; @@ -19,7 +20,7 @@ import com.woowacourse.moamoa.study.domain.exception.InvalidPeriodException; import com.woowacourse.moamoa.study.domain.repository.StudyRepository; import com.woowacourse.moamoa.study.service.StudyService; -import com.woowacourse.moamoa.study.service.request.CreatingStudyRequest; +import com.woowacourse.moamoa.study.service.request.StudyRequest; import java.time.LocalDate; import java.util.List; import java.util.Optional; @@ -30,7 +31,7 @@ import org.springframework.http.ResponseEntity; @RepositoryTest -public class StudyControllerTest { +class StudyControllerTest { @Autowired private StudyRepository studyRepository; @@ -50,7 +51,7 @@ void openStudy() { // given StudyController sut = new StudyController(new StudyService(studyRepository, memberRepository, new DateTimeSystem())); - final CreatingStudyRequest creatingStudyRequest = CreatingStudyRequest.builder() + final StudyRequest studyRequest = StudyRequest.builder() .title("Java") .excerpt("java excerpt") .thumbnail("java image") @@ -63,7 +64,7 @@ void openStudy() { .build(); // when - final ResponseEntity response = sut.createStudy(1L, creatingStudyRequest); + final ResponseEntity response = sut.createStudy(1L, studyRequest); // then final String id = response.getHeaders().getLocation().getPath().replace("/api/studies/", ""); @@ -78,9 +79,9 @@ void openStudy() { assertThat(study.get().getCreatedAt()).isNotNull(); assertThat(study.get().getStudyPlanner()).isEqualTo( new StudyPlanner( - creatingStudyRequest.getStartDate(), LocalDate.parse(creatingStudyRequest.getEndDate()), PREPARE)); + studyRequest.getStartDate(), LocalDate.parse(studyRequest.getEndDate()), PREPARE)); assertThat(study.get().getAttachedTags().getValue()) - .extracting("tagId").containsAnyElementsOf(creatingStudyRequest.getTagIds()); + .extracting("tagId").containsAnyElementsOf(studyRequest.getTagIds()); } @DisplayName("유효하지 않은 스터디 기간으로 생성 시 예외 발생") @@ -88,7 +89,7 @@ void openStudy() { void createStudyByInvalidPeriod() { StudyController sut = new StudyController(new StudyService(studyRepository, memberRepository, new DateTimeSystem())); - final CreatingStudyRequest creatingStudyRequest = CreatingStudyRequest.builder() + final StudyRequest studyRequest = StudyRequest.builder() .title("Java") .excerpt("java excerpt") .thumbnail("java image") @@ -101,7 +102,7 @@ void createStudyByInvalidPeriod() { .build(); // when - assertThatThrownBy(() -> sut.createStudy(1L, creatingStudyRequest)) + assertThatThrownBy(() -> sut.createStudy(1L, studyRequest)) .isInstanceOf(InvalidPeriodException.class); } @@ -111,7 +112,7 @@ void checkStudyStatusIfCreateDateSameStartDate() { StudyController sut = new StudyController(new StudyService(studyRepository, memberRepository, new DateTimeSystem())); - final CreatingStudyRequest createStudyRequest = CreatingStudyRequest.builder() + final StudyRequest createStudyRequest = StudyRequest.builder() .title("Java") .excerpt("java excerpt") .thumbnail("java image") @@ -140,7 +141,7 @@ void checkStudyStatusIfCreateDateSameStartDate() { void createStudyByNotFoundUser() { StudyController sut = new StudyController(new StudyService(studyRepository, memberRepository, new DateTimeSystem())); - final CreatingStudyRequest creatingStudyRequest = CreatingStudyRequest.builder() + final StudyRequest studyRequest = StudyRequest.builder() .title("Java") .excerpt("java excerpt") .thumbnail("java image") @@ -153,17 +154,17 @@ void createStudyByNotFoundUser() { .build(); // when - assertThatThrownBy(() -> sut.createStudy(100L, creatingStudyRequest)) // 존재하지 않는 사용자로 추가 시 예외 발생 + assertThatThrownBy(() -> sut.createStudy(100L, studyRequest)) // 존재하지 않는 사용자로 추가 시 예외 발생 .isInstanceOf(UnauthorizedException.class); } @DisplayName("회원은 스터디에 참여할 수 있다.") @Test - public void participateStudy() { + void participateStudy() { // given StudyController studyController = new StudyController(new StudyService(studyRepository, memberRepository, new DateTimeSystem())); - final CreatingStudyRequest creatingStudyRequest = CreatingStudyRequest.builder() + final StudyRequest studyRequest = StudyRequest.builder() .title("Java") .excerpt("java excerpt") .thumbnail("java image") @@ -175,7 +176,7 @@ public void participateStudy() { .tagIds(List.of(1L, 2L)) .build(); - final ResponseEntity createdResponse = studyController.createStudy(1L, creatingStudyRequest); + final ResponseEntity createdResponse = studyController.createStudy(1L, studyRequest); // when final String location = createdResponse.getHeaders().getLocation().getPath(); @@ -191,11 +192,11 @@ public void participateStudy() { @DisplayName("최대인원이 한 명인 경우 바로 모집 종료가 되어야 한다.") @Test - public void createdStudyWithMaxSizeOne() { + void createdStudyWithMaxSizeOne() { // given StudyController studyController = new StudyController(new StudyService(studyRepository, memberRepository, new DateTimeSystem())); - final CreatingStudyRequest creatingStudyRequest = CreatingStudyRequest.builder() + final StudyRequest studyRequest = StudyRequest.builder() .title("Java") .excerpt("java excerpt") .thumbnail("java image") @@ -207,7 +208,7 @@ public void createdStudyWithMaxSizeOne() { .tagIds(List.of(1L, 2L)) .build(); - final ResponseEntity createdResponse = studyController.createStudy(1L, creatingStudyRequest); + final ResponseEntity createdResponse = studyController.createStudy(1L, studyRequest); // when final String location = createdResponse.getHeaders().getLocation().getPath(); @@ -218,6 +219,55 @@ public void createdStudyWithMaxSizeOne() { assertThat(study.getRecruitPlanner().getRecruitStatus()).isEqualTo(RECRUITMENT_END); } + @DisplayName("스터디 상세 정보를 업데이트할 수 있다.") + @Test + void updateStudyDetails() { + // given + StudyController studyController = new StudyController(new StudyService(studyRepository, memberRepository, + new DateTimeSystem())); + + final StudyRequest studyRequest = StudyRequest.builder() + .title("Java") + .excerpt("java excerpt") + .thumbnail("java image") + .description("자바 스터디 상세설명 입니다.") + .startDate(LocalDate.now().plusDays(1)) + .endDate(LocalDate.now().plusDays(4)) + .enrollmentEndDate(LocalDate.now().plusDays(2)) + .maxMemberCount(1) + .tagIds(List.of(1L, 2L)) + .build(); + + final ResponseEntity createdResponse = studyController.createStudy(1L, studyRequest); + final String location = createdResponse.getHeaders().getLocation().getPath(); + final long studyId = getStudyIdBy(location); + Study study = studyRepository.findById(studyId).orElseThrow(); + + final StudyRequest updatingStudyRequest = StudyRequest.builder() + .title("변경된 title") + .excerpt("변경된 excerpt") + .thumbnail("변경된 image") + .description("변경된 상세설명") + .startDate(LocalDate.now().plusDays(1)) + .endDate(LocalDate.now().plusDays(4)) + .enrollmentEndDate(LocalDate.now().plusDays(2)) + .maxMemberCount(10) + .tagIds(List.of(1L)) + .build(); + + // when + studyController.updateStudy(study.getParticipants().getOwnerId(), studyId, updatingStudyRequest); + + // then + study = studyRepository.findById(studyId).orElseThrow(); + assertThat(study.getContent().getTitle()).isEqualTo("변경된 title"); + assertThat(study.getContent().getExcerpt()).isEqualTo("변경된 excerpt"); + assertThat(study.getContent().getThumbnail()).isEqualTo("변경된 image"); + assertThat(study.getContent().getDescription()).isEqualTo("변경된 상세설명"); + assertThat(study.getRecruitPlanner().getMax()).isEqualTo(10); + assertThat(study.getAttachedTags().getAttachedTags().get(0)).isEqualTo(new AttachedTag(1L)); + } + private long getStudyIdBy(final String location) { final String[] splitLocation = location.split("/"); return Long.parseLong(splitLocation[3]); diff --git a/backend/src/test/java/com/woowacourse/moamoa/study/domain/StudyTest.java b/backend/src/test/java/com/woowacourse/moamoa/study/domain/StudyTest.java index a18b1d389..a13326907 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/study/domain/StudyTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/study/domain/StudyTest.java @@ -5,21 +5,18 @@ import static com.woowacourse.moamoa.study.domain.StudyStatus.DONE; import static com.woowacourse.moamoa.study.domain.StudyStatus.IN_PROGRESS; import static com.woowacourse.moamoa.study.domain.StudyStatus.PREPARE; - +import static java.time.LocalDateTime.now; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatCode; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static java.time.LocalDateTime.now; - +import com.woowacourse.moamoa.common.exception.UnauthorizedException; import com.woowacourse.moamoa.study.domain.exception.InvalidPeriodException; import com.woowacourse.moamoa.study.service.exception.FailureParticipationException; - import java.time.LocalDate; import java.time.LocalDateTime; import java.util.Set; import java.util.stream.Stream; - import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -344,4 +341,23 @@ void checkIsParticipant(Long ownerId, Long participantId, Long targetMemberId, b // act assertThat(sut.isParticipant(targetMemberId)).isEqualTo(expected); } + + @DisplayName("참여자는 스터디를 업데이트할 수 없다.") + @Test + public void updateStudyWithParticipant() { + // given + final Content content = new Content("title", "excerpt", "thumbnail", "description"); + final Participants participants = Participants.createBy(1L); + final RecruitPlanner recruitPlanner = new RecruitPlanner(2, RECRUITMENT_START, LocalDate.now().minusDays(1)); + final StudyPlanner studyPlanner = new StudyPlanner(LocalDate.now(), LocalDate.now().plusDays(5), PREPARE); + final Study sut = new Study(content, participants, recruitPlanner, studyPlanner, AttachedTags.empty(), now().minusDays(2)); + + final Content updatingContent = new Content("새로운 title", "새로운 excerpt", "새로운 thumbnail", "새로운 description"); + final RecruitPlanner updatingRecruitPlanner = new RecruitPlanner(10, RECRUITMENT_START, LocalDate.now().minusDays(1)); + final StudyPlanner updatingStudyPlanner = new StudyPlanner(LocalDate.now(), LocalDate.now().plusDays(5), PREPARE); + + // when & then + assertThatThrownBy(() -> sut.update(2L, updatingContent, updatingRecruitPlanner, null, updatingStudyPlanner)) + .isInstanceOf(UnauthorizedException.class); + } } diff --git a/backend/src/test/java/com/woowacourse/moamoa/study/schedule/AutoUpdateStatusTaskTest.java b/backend/src/test/java/com/woowacourse/moamoa/study/schedule/AutoUpdateStatusTaskTest.java index 1d2d6251a..1c5bae37c 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/study/schedule/AutoUpdateStatusTaskTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/study/schedule/AutoUpdateStatusTaskTest.java @@ -13,7 +13,7 @@ import com.woowacourse.moamoa.study.domain.Study; import com.woowacourse.moamoa.study.domain.repository.StudyRepository; import com.woowacourse.moamoa.study.service.StudyService; -import com.woowacourse.moamoa.study.service.request.CreatingStudyRequest; +import com.woowacourse.moamoa.study.service.request.StudyRequest; import java.time.LocalDateTime; import java.time.ZoneId; import java.time.ZoneOffset; @@ -63,7 +63,7 @@ void initDataBase() { StudyService studyService = new StudyService(studyRepository, memberRepository, dateTimeSystem); - CreatingStudyRequest javaRequest = CreatingStudyRequest.builder() + StudyRequest javaRequest = StudyRequest.builder() .title("Java 스터디").excerpt("자바 설명").thumbnail("java thumbnail") .description("그린론의 우당탕탕 자바 스터디입니다.") .startDate(dateTimeSystem.now().toLocalDate().plusDays(3)) @@ -71,7 +71,7 @@ void initDataBase() { .build(); javaStudyId = studyService.createStudy(1L, javaRequest).getId(); - CreatingStudyRequest reactRequest = CreatingStudyRequest.builder() + StudyRequest reactRequest = StudyRequest.builder() .title("React 스터디").excerpt("리액트 설명").thumbnail("react thumbnail") .description("디우의 뤼액트 스터디입니다.") .startDate(dateTimeSystem.now().toLocalDate().plusDays(2)) @@ -79,7 +79,7 @@ void initDataBase() { .build(); reactStudyId = studyService.createStudy(1L, reactRequest).getId(); - CreatingStudyRequest javascriptRequest = CreatingStudyRequest.builder() + StudyRequest javascriptRequest = StudyRequest.builder() .title("javaScript 스터디").excerpt("자바스크립트 설명").thumbnail("javascript thumbnail") .description("그린론의 자바스크립트 접해보기") .startDate(dateTimeSystem.now().toLocalDate().plusDays(8)) @@ -87,14 +87,14 @@ void initDataBase() { .build(); javascriptStudyId = studyService.createStudy(1L, javascriptRequest).getId(); - CreatingStudyRequest httpRequest = CreatingStudyRequest.builder() + StudyRequest httpRequest = StudyRequest.builder() .title("Http 스터디").excerpt("Http 설명").thumbnail("http thumbnail") .description("그린론의 HTTP 접해보기") .startDate(dateTimeSystem.now().toLocalDate().plusDays(8)) .build(); httpStudyId = studyService.createStudy(1L, httpRequest).getId(); - CreatingStudyRequest linuxRequest = CreatingStudyRequest.builder() + StudyRequest linuxRequest = StudyRequest.builder() .title("Linux 스터디").excerpt("리눅스 설명").thumbnail("linux thumbnail") .description("Linux를 공부하자의 베루스입니다.") .startDate(dateTimeSystem.now().toLocalDate()) @@ -102,7 +102,7 @@ void initDataBase() { .build(); linuxStudyId = studyService.createStudy(1L, linuxRequest).getId(); - CreatingStudyRequest algorithmRequest = CreatingStudyRequest.builder() + StudyRequest algorithmRequest = StudyRequest.builder() .title("알고리즘 스터디").excerpt("알고리즘 설명").thumbnail("algorithm thumbnail") .description("알고리즘을 TDD로 풀자의 베루스입니다.") .startDate(dateTimeSystem.now().toLocalDate().plusDays(2)) From ec06c6540b2a5a6f77fec2b4f7a5d67cd6e99d69 Mon Sep 17 00:00:00 2001 From: TaeYoon Date: Wed, 17 Aug 2022 20:52:28 +0900 Subject: [PATCH 30/51] =?UTF-8?q?[FE]=20issue275:=20=EC=8A=A4=ED=84=B0?= =?UTF-8?q?=EB=94=94=EC=9B=90=20=EC=8A=A4=ED=84=B0=EB=94=94=20=ED=83=88?= =?UTF-8?q?=ED=87=B4=20(#287)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: MyStudyCard 수정 탈퇴 버튼을 클릭하면 스터디 룸으로 이동하는 버그가 있어 컴포넌트를 분리하고 스타일을 수정 e.stopPropagation을 해도 기본 a, button의 동작은 막지 않음 * feat: 스터디원 탈퇴 구현 내 스터디 목록 페이지에서 쓰레기통 버튼 클릭시 탈퇴 --- frontend/src/api/my-studies/index.ts | 4 +- frontend/src/api/my-study/index.ts | 14 +++++++ frontend/src/mocks/detailStudyHandlers.ts | 5 --- frontend/src/mocks/myHandlers.ts | 16 ++++++++ .../MyStudyCardListSection.style.tsx | 30 ++++++++++++++- .../MyStudyCardListSection.tsx | 37 ++++++++++++++++++- .../my-study-card/MyStudyCard.style.tsx | 17 --------- .../components/my-study-card/MyStudyCard.tsx | 5 +-- 8 files changed, 97 insertions(+), 31 deletions(-) diff --git a/frontend/src/api/my-studies/index.ts b/frontend/src/api/my-studies/index.ts index 71f000eea..deea4b686 100644 --- a/frontend/src/api/my-studies/index.ts +++ b/frontend/src/api/my-studies/index.ts @@ -5,6 +5,8 @@ import type { MyStudy } from '@custom-types'; import axiosInstance from '@api/axiosInstance'; +export const QK_MY_STUDIES = 'my-studies'; + // get export type GetMyStudiesResponseData = { studies: Array; @@ -16,5 +18,5 @@ export const getMyStudies = async () => { }; export const useGetMyStudies = () => { - return useQuery('my-studies', getMyStudies); + return useQuery(QK_MY_STUDIES, getMyStudies); }; diff --git a/frontend/src/api/my-study/index.ts b/frontend/src/api/my-study/index.ts index 14a572af0..bf32d520b 100644 --- a/frontend/src/api/my-study/index.ts +++ b/frontend/src/api/my-study/index.ts @@ -18,3 +18,17 @@ export const postMyStudy = async ({ studyId }: PostMyStudyRequestParams) => { }; export const usePostMyStudy = () => useMutation(postMyStudy); + +// delete - quit study +export type DeleteMyStudyRequestParams = { + studyId: StudyId; +}; + +export const deleteMyStudy = async ({ studyId }: DeleteMyStudyRequestParams) => { + const response = await axiosInstance.delete, DeleteMyStudyRequestParams>( + `/api/studies/${studyId}/members`, + ); + return response.data; +}; + +export const useDeleteMyStudy = () => useMutation(deleteMyStudy); diff --git a/frontend/src/mocks/detailStudyHandlers.ts b/frontend/src/mocks/detailStudyHandlers.ts index 10540e806..fd16894fe 100644 --- a/frontend/src/mocks/detailStudyHandlers.ts +++ b/frontend/src/mocks/detailStudyHandlers.ts @@ -13,11 +13,6 @@ const detailStudyHandlers = [ return res(ctx.status(200), ctx.json(study)); }), - rest.post('/api/studies/:studyId/memebers', (req, res, ctx) => { - // const studyId = req.params.studyId; - - return res(ctx.status(200)); - }), rest.get('/api/studies/:studyId/reviews', (req, res, ctx) => { const size = req.url.searchParams.get('size'); if (size) { diff --git a/frontend/src/mocks/myHandlers.ts b/frontend/src/mocks/myHandlers.ts index 88067aa01..db0f1c042 100644 --- a/frontend/src/mocks/myHandlers.ts +++ b/frontend/src/mocks/myHandlers.ts @@ -6,4 +6,20 @@ export const myHandlers = [ rest.get('/api/my/studies', (req, res, ctx) => { return res(ctx.status(200), ctx.json(myStudiesJson)); }), + rest.post('/api/studies/:studyId/members', (req, res, ctx) => { + // join study + const studyId = req.params.studyId; + if (!studyId) return res(ctx.status(400), ctx.json({ message: '스터디 아이디 없음' })); + + return res(ctx.status(200)); + }), + rest.delete('/api/studies/:studyId/members', (req, res, ctx) => { + // quit study + const studyId = req.params.studyId; + if (!studyId) return res(ctx.status(400), ctx.json({ message: '스터디 아이디 없음' })); + + const { studies } = myStudiesJson; + myStudiesJson.studies = studies.filter(study => study.id !== Number(studyId)); + return res(ctx.status(200)); + }), ]; diff --git a/frontend/src/pages/my-study-page/components/my-study-card-list-section/MyStudyCardListSection.style.tsx b/frontend/src/pages/my-study-page/components/my-study-card-list-section/MyStudyCardListSection.style.tsx index 2a167f6c8..f9caae6ea 100644 --- a/frontend/src/pages/my-study-page/components/my-study-card-list-section/MyStudyCardListSection.style.tsx +++ b/frontend/src/pages/my-study-page/components/my-study-card-list-section/MyStudyCardListSection.style.tsx @@ -1,3 +1,4 @@ +import { css } from '@emotion/react'; import styled from '@emotion/styled'; import { mqDown } from '@utils'; @@ -9,6 +10,10 @@ export const SectionTitle = styled.h3` font-weight: 700; `; +export const MyStudyCardListSection = styled.section` + padding: 8px; +`; + export const MyStudyList = styled.ul` display: grid; grid-template-columns: repeat(3, minmax(auto, 1fr)); @@ -27,6 +32,27 @@ export const MyStudyList = styled.ul` } `; -export const MyStudyCardListSection = styled.section` - padding: 8px; +export const MyStudyCardItem = styled.li` + position: relative; +`; + +export const TrashButton = styled.button` + ${({ theme }) => css` + position: absolute; + bottom: 12px; + right: 12px; + + background-color: transparent; + border: none; + outline: none; + + & > svg { + stroke: ${theme.colors.primary.base}; + + &:hover, + &:active { + stroke: ${theme.colors.primary.light}; + } + } + `}; `; diff --git a/frontend/src/pages/my-study-page/components/my-study-card-list-section/MyStudyCardListSection.tsx b/frontend/src/pages/my-study-page/components/my-study-card-list-section/MyStudyCardListSection.tsx index 1d74c4d85..1437d8d39 100644 --- a/frontend/src/pages/my-study-page/components/my-study-card-list-section/MyStudyCardListSection.tsx +++ b/frontend/src/pages/my-study-page/components/my-study-card-list-section/MyStudyCardListSection.tsx @@ -1,9 +1,16 @@ +import { AxiosError } from 'axios'; +import { useQueryClient } from 'react-query'; import { Link } from 'react-router-dom'; import { PATH } from '@constants'; import type { MakeOptional, MyStudy } from '@custom-types'; +import { QK_MY_STUDIES } from '@api/my-studies'; +import { useDeleteMyStudy } from '@api/my-study'; + +import { TrashCanSvg } from '@components/svg'; + import * as S from '@my-study-page/components/my-study-card-list-section/MyStudyCardListSection.style'; import MyStudyCard from '@my-study-page/components/my-study-card/MyStudyCard'; @@ -22,6 +29,29 @@ const MyStudyCardListSection: React.FC = ({ studies, disabled = false, }) => { + const queryClient = useQueryClient(); + const { mutate } = useDeleteMyStudy(); + + const handleTrashButtonClick = + ({ title, id }: Pick) => + () => { + if (!confirm(`정말 '${title}'을(를) 탈퇴하실 건가요? :(`)) return; + + mutate( + { studyId: id }, + { + onError: error => { + if (error instanceof AxiosError) console.error(error.message); + alert('문제가 발생하여 스터디를 탈퇴하지 못했습니다'); + }, + onSuccess: () => { + queryClient.refetchQueries(QK_MY_STUDIES); + alert('스터디를 탈퇴했습니다.'); + }, + }, + ); + }; + return ( {sectionTitle} @@ -30,7 +60,7 @@ const MyStudyCardListSection: React.FC = ({
  • 해당하는 스터디가 없습니다
  • ) : ( studies.map(study => ( -
  • + = ({ disabled={disabled} /> -
  • + + + + )) )} diff --git a/frontend/src/pages/my-study-page/components/my-study-card/MyStudyCard.style.tsx b/frontend/src/pages/my-study-page/components/my-study-card/MyStudyCard.style.tsx index 8324c5bbf..50e8219c6 100644 --- a/frontend/src/pages/my-study-page/components/my-study-card/MyStudyCard.style.tsx +++ b/frontend/src/pages/my-study-page/components/my-study-card/MyStudyCard.style.tsx @@ -81,20 +81,3 @@ export const MyStudyCard = styled.div>` ${disabled && disabledStyle(theme)} `} `; - -export const TrashButton = styled.button` - ${({ theme }) => css` - background-color: transparent; - border: none; - outline: none; - - & > svg { - stroke: ${theme.colors.primary.base}; - - &:hover, - &:active { - stroke: ${theme.colors.primary.dark}; - } - } - `}; -`; diff --git a/frontend/src/pages/my-study-page/components/my-study-card/MyStudyCard.tsx b/frontend/src/pages/my-study-page/components/my-study-card/MyStudyCard.tsx index 541e8e0a8..0680af879 100644 --- a/frontend/src/pages/my-study-page/components/my-study-card/MyStudyCard.tsx +++ b/frontend/src/pages/my-study-page/components/my-study-card/MyStudyCard.tsx @@ -1,6 +1,6 @@ import type { MakeOptional, Tag } from '@custom-types'; -import { CrownSvg, TrashCanSvg } from '@components/svg'; +import { CrownSvg } from '@components/svg'; import * as S from '@my-study-page/components/my-study-card/MyStudyCard.style'; @@ -42,9 +42,6 @@ const MyStudyCard: React.FC = ({ {startDate} ~ {endDate || ''} - - - From 84384e86b55d59bcbf6b4290a4aec4edbc7f17be Mon Sep 17 00:00:00 2001 From: SeungCheol Shin <47477359+sc0116@users.noreply.github.com> Date: Wed, 17 Aug 2022 21:05:52 +0900 Subject: [PATCH 31/51] =?UTF-8?q?[BE]=20issue254:=20=EC=8A=A4=ED=84=B0?= =?UTF-8?q?=EB=94=94=20=ED=83=88=ED=87=B4=20(#281)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * test: 스터디 탈퇴 인수테스트 작성 * feat: 스터디에 참여하지 않은 회원이 탈퇴하려는 경우 예외 처리 구현 * feat: 스터디장이 스터디를 탈퇴하려는 경우 예외 처리 구현 * test: 스터디 탈퇴 인수 테스트 수정 * feat: 스터디 탈퇴 구현 * feat: 스터디 탈퇴 리팩토링 * refactor: 스터디 참여 로직 위치 변경 * refactor: 피드백 반영 * refactor: 피드백 반영 * refactor: 코드 구린내 청소 * refactor: 피드백 반영 * refactor: 피드백 반영 --- backend/src/docs/asciidoc/index.adoc | 3 + .../service/CommunityArticleService.java | 3 - .../service/ReferenceRoomService.java | 2 +- .../study/controller/StudyController.java | 15 +--- .../StudyParticipantController.java | 35 +++++++++ .../moamoa/study/domain/Participants.java | 17 ++-- .../moamoa/study/domain/Study.java | 52 +++++++++---- .../service/StudyParticipantService.java | 38 +++++++++ .../moamoa/study/service/StudyService.java | 6 -- .../exception/OwnerCanNotLeaveException.java | 10 +++ .../service/response/StudiesResponse.java | 1 - .../service/{ => response}/StudyResponse.java | 2 +- .../acceptance/steps/StudyRelatedSteps.java | 5 +- .../GettingStudiesSummaryAcceptanceTest.java | 2 +- .../ParticipationStudyAcceptanceTest.java | 36 --------- .../study/StudyParticipantAcceptanceTest.java | 78 +++++++++++++++++++ .../ReferenceRoomControllerTest.java | 6 +- .../SearchingReferenceRoomControllerTest.java | 8 +- .../referenceroom/query/LinkDaoTest.java | 9 ++- .../controller/ReviewControllerTest.java | 4 +- .../SearchingReviewControllerTest.java | 10 ++- .../SearchingStudyControllerTest.java | 17 ++-- .../study/controller/StudyControllerTest.java | 33 -------- .../StudyParticipantControllerTest.java | 76 ++++++++++++++++++ .../moamoa/study/domain/StudyTest.java | 75 ++++++++++++++++-- 25 files changed, 399 insertions(+), 144 deletions(-) create mode 100644 backend/src/main/java/com/woowacourse/moamoa/study/controller/StudyParticipantController.java create mode 100644 backend/src/main/java/com/woowacourse/moamoa/study/service/StudyParticipantService.java create mode 100644 backend/src/main/java/com/woowacourse/moamoa/study/service/exception/OwnerCanNotLeaveException.java rename backend/src/main/java/com/woowacourse/moamoa/study/service/{ => response}/StudyResponse.java (93%) delete mode 100644 backend/src/test/java/com/woowacourse/acceptance/test/study/ParticipationStudyAcceptanceTest.java create mode 100644 backend/src/test/java/com/woowacourse/acceptance/test/study/StudyParticipantAcceptanceTest.java create mode 100644 backend/src/test/java/com/woowacourse/moamoa/study/controller/StudyParticipantControllerTest.java diff --git a/backend/src/docs/asciidoc/index.adoc b/backend/src/docs/asciidoc/index.adoc index d615ecaa7..5c2baaafc 100644 --- a/backend/src/docs/asciidoc/index.adoc +++ b/backend/src/docs/asciidoc/index.adoc @@ -54,6 +54,9 @@ operation::studies/myStudy[snippets='http-request,http-response'] === 스터디 상세 정보 수정 operation::studies/update[snippets='http-request,http-response'] +=== 스터디 탈퇴 +operation::studies/leave[snippets='http-request,request-headers,http-response'] + [[Comunity]] == 커뮤니티 diff --git a/backend/src/main/java/com/woowacourse/moamoa/community/service/CommunityArticleService.java b/backend/src/main/java/com/woowacourse/moamoa/community/service/CommunityArticleService.java index b4b272591..2157c4054 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/community/service/CommunityArticleService.java +++ b/backend/src/main/java/com/woowacourse/moamoa/community/service/CommunityArticleService.java @@ -5,8 +5,6 @@ import com.woowacourse.moamoa.community.query.CommunityArticleDao; import com.woowacourse.moamoa.community.query.data.CommunityArticleData; import com.woowacourse.moamoa.community.service.exception.ArticleNotFoundException; -import com.woowacourse.moamoa.community.service.exception.NotArticleAuthorException; -import com.woowacourse.moamoa.community.service.exception.NotRelatedArticleException; import com.woowacourse.moamoa.community.service.exception.UneditableArticleException; import com.woowacourse.moamoa.community.service.exception.UnviewableArticleException; import com.woowacourse.moamoa.community.service.request.ArticleRequest; @@ -16,7 +14,6 @@ import com.woowacourse.moamoa.member.domain.Member; import com.woowacourse.moamoa.member.domain.repository.MemberRepository; import com.woowacourse.moamoa.member.service.exception.MemberNotFoundException; -import com.woowacourse.moamoa.member.service.exception.NotParticipatedMemberException; import com.woowacourse.moamoa.study.domain.Study; import com.woowacourse.moamoa.study.domain.repository.StudyRepository; import com.woowacourse.moamoa.study.service.exception.StudyNotFoundException; diff --git a/backend/src/main/java/com/woowacourse/moamoa/referenceroom/service/ReferenceRoomService.java b/backend/src/main/java/com/woowacourse/moamoa/referenceroom/service/ReferenceRoomService.java index f890aa702..74b2765d1 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/referenceroom/service/ReferenceRoomService.java +++ b/backend/src/main/java/com/woowacourse/moamoa/referenceroom/service/ReferenceRoomService.java @@ -34,7 +34,7 @@ public Link createLink(final Long githubId, final Long studyId, final CreatingLi final Study study = studyRepository.findById(studyId) .orElseThrow(StudyNotFoundException::new); - if (!study.isReviewWritable(member.getId())) { + if (!study.isParticipant(member.getId())) { throw new NotCreatingLinkException(); } diff --git a/backend/src/main/java/com/woowacourse/moamoa/study/controller/StudyController.java b/backend/src/main/java/com/woowacourse/moamoa/study/controller/StudyController.java index 57a4e3a64..b67a29673 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/study/controller/StudyController.java +++ b/backend/src/main/java/com/woowacourse/moamoa/study/controller/StudyController.java @@ -32,18 +32,11 @@ public ResponseEntity createStudy( return ResponseEntity.created(URI.create("/api/studies/" + study.getId())).build(); } - @PostMapping("/{study-id}") - public ResponseEntity participateStudy(@AuthenticationPrincipal final Long githubId, - @PathVariable("study-id") final Long studyId - ) { - studyService.participateStudy(githubId, studyId); - return ResponseEntity.ok().build(); - } - @PutMapping("/{study-id}") - public ResponseEntity updateStudy(@AuthenticatedMember final Long memberId, - @PathVariable("study-id") final Long studyId, - @Valid @RequestBody(required = false) final StudyRequest request + public ResponseEntity updateStudy( + @AuthenticatedMember final Long memberId, + @PathVariable("study-id") final Long studyId, + @Valid @RequestBody(required = false) final StudyRequest request ) { studyService.updateStudy(memberId, studyId, request); return ResponseEntity.ok().build(); diff --git a/backend/src/main/java/com/woowacourse/moamoa/study/controller/StudyParticipantController.java b/backend/src/main/java/com/woowacourse/moamoa/study/controller/StudyParticipantController.java new file mode 100644 index 000000000..83140f511 --- /dev/null +++ b/backend/src/main/java/com/woowacourse/moamoa/study/controller/StudyParticipantController.java @@ -0,0 +1,35 @@ +package com.woowacourse.moamoa.study.controller; + +import com.woowacourse.moamoa.auth.config.AuthenticatedMember; +import com.woowacourse.moamoa.study.service.StudyParticipantService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/studies/{study-id}/members") +@RequiredArgsConstructor +public class StudyParticipantController { + + private final StudyParticipantService studyParticipantService; + + @PostMapping + public ResponseEntity participateStudy( + @AuthenticatedMember final Long memberId, @PathVariable("study-id") final Long studyId + ) { + studyParticipantService.participateStudy(memberId, studyId); + return ResponseEntity.noContent().build(); + } + + @DeleteMapping + public ResponseEntity leaveStudy( + @AuthenticatedMember final Long memberId, @PathVariable("study-id") final Long studyId + ) { + studyParticipantService.leaveStudy(memberId, studyId); + return ResponseEntity.noContent().build(); + } +} diff --git a/backend/src/main/java/com/woowacourse/moamoa/study/domain/Participants.java b/backend/src/main/java/com/woowacourse/moamoa/study/domain/Participants.java index 3603be43d..dca1b1d6d 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/study/domain/Participants.java +++ b/backend/src/main/java/com/woowacourse/moamoa/study/domain/Participants.java @@ -40,16 +40,25 @@ public Participants(Long ownerId, final Set participants) { .collect(Collectors.toSet()); } + public static Participants createBy(Long ownerId) { + return new Participants(ownerId, new HashSet<>()); + } + void participate(Long memberId) { if (isParticipation(memberId)) { throw new FailureParticipationException(); } participants.add(new Participant(memberId)); - size = size + 1; + size++; + } + + public void leave(final Participant participant) { + participants.remove(participant); + size--; } - public boolean isParticipation(final Long memberId) { + boolean isParticipation(final Long memberId) { return participants.contains(new Participant(memberId)) || isOwner(memberId); } @@ -84,8 +93,4 @@ public boolean equals(final Object o) { public int hashCode() { return Objects.hash(size, getParticipants()); } - - public static Participants createBy(Long ownerId) { - return new Participants(ownerId, new HashSet<>()); - } } diff --git a/backend/src/main/java/com/woowacourse/moamoa/study/domain/Study.java b/backend/src/main/java/com/woowacourse/moamoa/study/domain/Study.java index 307bfb1cc..e43e00851 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/study/domain/Study.java +++ b/backend/src/main/java/com/woowacourse/moamoa/study/domain/Study.java @@ -3,9 +3,11 @@ import static javax.persistence.GenerationType.IDENTITY; import static lombok.AccessLevel.PROTECTED; +import com.woowacourse.moamoa.referenceroom.service.exception.NotParticipatedMemberException; import com.woowacourse.moamoa.common.exception.UnauthorizedException; import com.woowacourse.moamoa.study.domain.exception.InvalidPeriodException; import com.woowacourse.moamoa.study.service.exception.FailureParticipationException; +import com.woowacourse.moamoa.study.service.exception.OwnerCanNotLeaveException; import java.time.LocalDate; import java.time.LocalDateTime; import javax.persistence.Column; @@ -43,15 +45,17 @@ public class Study { @Column(updatable = false) private LocalDateTime createdAt; - public Study(final Content content, final Participants participants, final RecruitPlanner recruitPlanner, - final StudyPlanner studyPlanner, final AttachedTags attachedTags, LocalDateTime createdAt + public Study( + final Content content, final Participants participants, final RecruitPlanner recruitPlanner, + final StudyPlanner studyPlanner, final AttachedTags attachedTags, LocalDateTime createdAt ) { this(null, content, participants, recruitPlanner, studyPlanner, attachedTags, createdAt); } - public Study(final Long id, final Content content, final Participants participants, - final RecruitPlanner recruitPlanner, final StudyPlanner studyPlanner, final AttachedTags attachedTags, - final LocalDateTime createdAt + public Study( + final Long id, final Content content, final Participants participants, + final RecruitPlanner recruitPlanner, final StudyPlanner studyPlanner, final AttachedTags attachedTags, + final LocalDateTime createdAt ) { if (isRecruitingAfterEndStudy(recruitPlanner, studyPlanner) || isRecruitedOrStartStudyBeforeCreatedAt(recruitPlanner, studyPlanner, createdAt)) { @@ -71,18 +75,6 @@ public Study(final Long id, final Content content, final Participants participan this.attachedTags = attachedTags; } - private boolean isRecruitingAfterEndStudy(final RecruitPlanner recruitPlanner, final StudyPlanner studyPlanner) { - return recruitPlanner.hasEnrollmentEndDate() && studyPlanner - .isEndBeforeThan(recruitPlanner.getEnrollmentEndDate()); - } - - private boolean isRecruitedOrStartStudyBeforeCreatedAt(final RecruitPlanner recruitPlanner, - final StudyPlanner studyPlanner, - final LocalDateTime createdAt) { - return studyPlanner.isStartBeforeThan(createdAt.toLocalDate()) || - recruitPlanner.isRecruitedBeforeThan(createdAt.toLocalDate()); - } - public boolean isReviewWritable(final Long memberId) { return participants.isParticipation(memberId) && !studyPlanner.isPreparing(); } @@ -109,6 +101,32 @@ public void changeStatus(final LocalDate now) { studyPlanner.updateStatus(now); } + public void leave(final Participant participant) { + verifyCanLeave(participant); + participants.leave(participant); + } + + private boolean isRecruitingAfterEndStudy(final RecruitPlanner recruitPlanner, final StudyPlanner studyPlanner) { + return recruitPlanner.hasEnrollmentEndDate() && studyPlanner + .isEndBeforeThan(recruitPlanner.getEnrollmentEndDate()); + } + + private boolean isRecruitedOrStartStudyBeforeCreatedAt( + final RecruitPlanner recruitPlanner, final StudyPlanner studyPlanner, final LocalDateTime createdAt + ) { + return studyPlanner.isStartBeforeThan(createdAt.toLocalDate()) || + recruitPlanner.isRecruitedBeforeThan(createdAt.toLocalDate()); + } + + private void verifyCanLeave(final Participant participant) { + if (participants.isOwner(participant.getMemberId())) { + throw new OwnerCanNotLeaveException(); + } + if (!participants.isParticipation(participant.getMemberId())) { + throw new NotParticipatedMemberException(); + } + } + public boolean isProgressStatus() { return studyPlanner.isProgress(); } diff --git a/backend/src/main/java/com/woowacourse/moamoa/study/service/StudyParticipantService.java b/backend/src/main/java/com/woowacourse/moamoa/study/service/StudyParticipantService.java new file mode 100644 index 000000000..e491163f7 --- /dev/null +++ b/backend/src/main/java/com/woowacourse/moamoa/study/service/StudyParticipantService.java @@ -0,0 +1,38 @@ +package com.woowacourse.moamoa.study.service; + +import com.woowacourse.moamoa.member.domain.repository.MemberRepository; +import com.woowacourse.moamoa.member.service.exception.MemberNotFoundException; +import com.woowacourse.moamoa.study.domain.Participant; +import com.woowacourse.moamoa.study.domain.Study; +import com.woowacourse.moamoa.study.domain.repository.StudyRepository; +import com.woowacourse.moamoa.study.service.exception.StudyNotFoundException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional +@RequiredArgsConstructor +public class StudyParticipantService { + + private final MemberRepository memberRepository; + private final StudyRepository studyRepository; + + public void participateStudy(final Long memberId, final Long studyId) { + memberRepository.findById(memberId) + .orElseThrow(MemberNotFoundException::new); + final Study study = studyRepository.findById(studyId) + .orElseThrow(StudyNotFoundException::new); + + study.participate(memberId); + } + + public void leaveStudy(final Long memberId, final Long studyId) { + memberRepository.findById(memberId) + .orElseThrow(MemberNotFoundException::new); + final Study study = studyRepository.findById(studyId) + .orElseThrow(StudyNotFoundException::new); + + study.leave(new Participant(memberId)); + } +} diff --git a/backend/src/main/java/com/woowacourse/moamoa/study/service/StudyService.java b/backend/src/main/java/com/woowacourse/moamoa/study/service/StudyService.java index 290d09bee..9684746ab 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/study/service/StudyService.java +++ b/backend/src/main/java/com/woowacourse/moamoa/study/service/StudyService.java @@ -51,12 +51,6 @@ public Study createStudy(final Long githubId, final StudyRequest request) { new Study(content, participants, recruitPlanner, studyPlanner, attachedTags, createdAt)); } - public void participateStudy(final Long githubId, final Long studyId) { - final Member member = findMemberBy(githubId); - final Study study = findStudyBy(studyId); - study.participate(member.getId()); - } - private Study findStudyBy(final Long studyId) { return studyRepository.findById(studyId) .orElseThrow(StudyNotFoundException::new); diff --git a/backend/src/main/java/com/woowacourse/moamoa/study/service/exception/OwnerCanNotLeaveException.java b/backend/src/main/java/com/woowacourse/moamoa/study/service/exception/OwnerCanNotLeaveException.java new file mode 100644 index 000000000..4f3bb8684 --- /dev/null +++ b/backend/src/main/java/com/woowacourse/moamoa/study/service/exception/OwnerCanNotLeaveException.java @@ -0,0 +1,10 @@ +package com.woowacourse.moamoa.study.service.exception; + +import com.woowacourse.moamoa.common.exception.BadRequestException; + +public class OwnerCanNotLeaveException extends BadRequestException { + + public OwnerCanNotLeaveException() { + super("스터디장은 스터디를 탈퇴할 수 없습니다."); + } +} diff --git a/backend/src/main/java/com/woowacourse/moamoa/study/service/response/StudiesResponse.java b/backend/src/main/java/com/woowacourse/moamoa/study/service/response/StudiesResponse.java index d1faf0cda..51205469c 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/study/service/response/StudiesResponse.java +++ b/backend/src/main/java/com/woowacourse/moamoa/study/service/response/StudiesResponse.java @@ -1,7 +1,6 @@ package com.woowacourse.moamoa.study.service.response; import com.woowacourse.moamoa.study.query.data.StudySummaryData; -import com.woowacourse.moamoa.study.service.StudyResponse; import com.woowacourse.moamoa.tag.query.response.TagSummaryData; import java.util.List; import java.util.Map; diff --git a/backend/src/main/java/com/woowacourse/moamoa/study/service/StudyResponse.java b/backend/src/main/java/com/woowacourse/moamoa/study/service/response/StudyResponse.java similarity index 93% rename from backend/src/main/java/com/woowacourse/moamoa/study/service/StudyResponse.java rename to backend/src/main/java/com/woowacourse/moamoa/study/service/response/StudyResponse.java index baabc6c4d..1834d60df 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/study/service/StudyResponse.java +++ b/backend/src/main/java/com/woowacourse/moamoa/study/service/response/StudyResponse.java @@ -1,4 +1,4 @@ -package com.woowacourse.moamoa.study.service; +package com.woowacourse.moamoa.study.service.response; import com.woowacourse.moamoa.study.query.data.StudySummaryData; import com.woowacourse.moamoa.tag.query.response.TagSummaryData; diff --git a/backend/src/test/java/com/woowacourse/acceptance/steps/StudyRelatedSteps.java b/backend/src/test/java/com/woowacourse/acceptance/steps/StudyRelatedSteps.java index d8aced7dc..68213adc3 100644 --- a/backend/src/test/java/com/woowacourse/acceptance/steps/StudyRelatedSteps.java +++ b/backend/src/test/java/com/woowacourse/acceptance/steps/StudyRelatedSteps.java @@ -27,10 +27,11 @@ public class StudyRelatedSteps extends Steps { RestAssured.given().log().all() .header(CONTENT_TYPE, APPLICATION_JSON_VALUE) .header(AUTHORIZATION, token) + .pathParam("study-id", studyId) .when().log().all() - .post("/api/studies/" + studyId) + .post("/api/studies/{study-id}/members") .then().log().all() - .statusCode(HttpStatus.OK.value()); + .statusCode(HttpStatus.NO_CONTENT.value()); } public Long 리뷰를_작성한다(String content) { diff --git a/backend/src/test/java/com/woowacourse/acceptance/test/study/GettingStudiesSummaryAcceptanceTest.java b/backend/src/test/java/com/woowacourse/acceptance/test/study/GettingStudiesSummaryAcceptanceTest.java index 842e52473..557319dea 100644 --- a/backend/src/test/java/com/woowacourse/acceptance/test/study/GettingStudiesSummaryAcceptanceTest.java +++ b/backend/src/test/java/com/woowacourse/acceptance/test/study/GettingStudiesSummaryAcceptanceTest.java @@ -10,7 +10,7 @@ import static org.springframework.restdocs.restassured3.RestAssuredRestDocumentation.document; import com.woowacourse.acceptance.AcceptanceTest; -import com.woowacourse.moamoa.study.service.StudyResponse; +import com.woowacourse.moamoa.study.service.response.StudyResponse; import com.woowacourse.moamoa.study.service.response.StudiesResponse; import com.woowacourse.moamoa.tag.query.response.TagSummaryData; import io.restassured.RestAssured; diff --git a/backend/src/test/java/com/woowacourse/acceptance/test/study/ParticipationStudyAcceptanceTest.java b/backend/src/test/java/com/woowacourse/acceptance/test/study/ParticipationStudyAcceptanceTest.java deleted file mode 100644 index f9009eacc..000000000 --- a/backend/src/test/java/com/woowacourse/acceptance/test/study/ParticipationStudyAcceptanceTest.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.woowacourse.acceptance.test.study; - -import static com.woowacourse.acceptance.steps.LoginSteps.그린론이; -import static com.woowacourse.acceptance.steps.LoginSteps.디우가; -import static org.springframework.http.HttpHeaders.AUTHORIZATION; -import static org.springframework.http.HttpHeaders.CONTENT_TYPE; -import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; -import static org.springframework.restdocs.restassured3.RestAssuredRestDocumentation.document; - -import com.woowacourse.acceptance.AcceptanceTest; -import io.restassured.RestAssured; -import java.time.LocalDate; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.springframework.http.HttpStatus; - -public class ParticipationStudyAcceptanceTest extends AcceptanceTest { - - @DisplayName("아직 스터디에 가입되지 않은 회원은 스터디에 참여가 가능하다.") - @Test - public void participateStudy() { - LocalDate 지금 = LocalDate.now(); - long 자바_스터디_ID = 그린론이().로그인하고().자바_스터디를().시작일자는(지금).모집인원은(10).생성한다(); - String token = 디우가().로그인한다(); - - RestAssured.given(spec).log().all() - .filter(document("studies/participant")) - .header(CONTENT_TYPE, APPLICATION_JSON_VALUE) - .header(AUTHORIZATION, token) - .pathParam("study-id", 자바_스터디_ID) - .when().log().all() - .post("/api/studies/{study-id}") - .then().log().all() - .statusCode(HttpStatus.OK.value()); - } -} diff --git a/backend/src/test/java/com/woowacourse/acceptance/test/study/StudyParticipantAcceptanceTest.java b/backend/src/test/java/com/woowacourse/acceptance/test/study/StudyParticipantAcceptanceTest.java new file mode 100644 index 000000000..8d8773aa5 --- /dev/null +++ b/backend/src/test/java/com/woowacourse/acceptance/test/study/StudyParticipantAcceptanceTest.java @@ -0,0 +1,78 @@ +package com.woowacourse.acceptance.test.study; + +import static com.woowacourse.acceptance.steps.LoginSteps.그린론이; +import static com.woowacourse.acceptance.steps.LoginSteps.디우가; +import static com.woowacourse.acceptance.steps.LoginSteps.짱구가; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.springframework.http.HttpHeaders.AUTHORIZATION; +import static org.springframework.http.HttpHeaders.CONTENT_TYPE; +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; +import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; +import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; +import static org.springframework.restdocs.restassured3.RestAssuredRestDocumentation.document; + +import com.woowacourse.acceptance.AcceptanceTest; +import com.woowacourse.moamoa.study.service.response.StudyDetailResponse; +import io.restassured.RestAssured; +import java.time.LocalDate; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; + +class StudyParticipantAcceptanceTest extends AcceptanceTest { + + @DisplayName("아직 스터디에 가입되지 않은 회원은 스터디에 참여가 가능하다.") + @Test + void participateStudy() { + LocalDate 지금 = LocalDate.now(); + long 자바_스터디_ID = 그린론이().로그인하고().자바_스터디를().시작일자는(지금).모집인원은(10).생성한다(); + String token = 디우가().로그인한다(); + + RestAssured.given(spec).log().all() + .filter(document("studies/participant")) + .header(CONTENT_TYPE, APPLICATION_JSON_VALUE) + .header(AUTHORIZATION, token) + .pathParam("study-id", 자바_스터디_ID) + .when().log().all() + .post("/api/studies/{study-id}/members") + .then().log().all() + .statusCode(HttpStatus.NO_CONTENT.value()); + } + + @DisplayName("스터디를 탈퇴한다.") + @Test + void leaveStudy() { + final LocalDate 지금 = LocalDate.now(); + final Long studyId = 짱구가().로그인하고().자바_스터디를().시작일자는(지금).생성한다(); + 디우가().로그인하고().스터디에(studyId).참여한다(); + + final String token = 디우가().로그인한다(); + + RestAssured.given(spec).log().all() + .filter(document("studies/leave", + requestHeaders( + headerWithName("Authorization").description("JWT Token") + ))) + .header(HttpHeaders.AUTHORIZATION, token) + .pathParam("study-id", studyId) + .when().log().all() + .delete("/api/studies/{study-id}/members") + .then().log().all() + .statusCode(HttpStatus.NO_CONTENT.value()); + + final StudyDetailResponse studyDetailResponse = RestAssured.given().log().all() + .pathParam("study-id", studyId) + .when().log().all() + .get("/api/studies/{study-id}") + .then().log().all() + .statusCode(HttpStatus.OK.value()) + .extract().as(StudyDetailResponse.class); + + assertAll( + () -> assertThat(studyDetailResponse.getCurrentMemberCount()).isEqualTo(1), + () -> assertThat(studyDetailResponse.getMembers()).isEmpty() + ); + } +} diff --git a/backend/src/test/java/com/woowacourse/moamoa/referenceroom/controller/ReferenceRoomControllerTest.java b/backend/src/test/java/com/woowacourse/moamoa/referenceroom/controller/ReferenceRoomControllerTest.java index dcd398c27..e5bf4d526 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/referenceroom/controller/ReferenceRoomControllerTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/referenceroom/controller/ReferenceRoomControllerTest.java @@ -3,7 +3,6 @@ import static com.woowacourse.moamoa.fixtures.MemberFixtures.디우; import static com.woowacourse.moamoa.fixtures.MemberFixtures.디우_깃허브_아이디; import static com.woowacourse.moamoa.fixtures.MemberFixtures.베루스; -import static com.woowacourse.moamoa.fixtures.MemberFixtures.베루스_깃허브_아이디; import static com.woowacourse.moamoa.fixtures.MemberFixtures.짱구; import static com.woowacourse.moamoa.fixtures.MemberFixtures.짱구_깃허브_아이디; import static com.woowacourse.moamoa.fixtures.StudyFixtures.자바_스터디_신청서; @@ -20,6 +19,7 @@ import com.woowacourse.moamoa.referenceroom.service.request.CreatingLinkRequest; import com.woowacourse.moamoa.referenceroom.service.request.EditingLinkRequest; import com.woowacourse.moamoa.study.domain.repository.StudyRepository; +import com.woowacourse.moamoa.study.service.StudyParticipantService; import com.woowacourse.moamoa.study.service.StudyService; import com.woowacourse.moamoa.study.service.request.StudyRequest; import java.time.LocalDate; @@ -67,7 +67,9 @@ void setUp() { final StudyRequest javaStudyRequest = 자바_스터디_신청서(startDate); javaStudyId = studyService.createStudy(짱구_깃허브_아이디, javaStudyRequest).getId(); - studyService.participateStudy(베루스_깃허브_아이디, javaStudyId); + + StudyParticipantService participantService = new StudyParticipantService(memberRepository, studyRepository); + participantService.participateStudy(verusId, javaStudyId); // 링크 공유 생성 final ReferenceRoomService referenceRoomService = diff --git a/backend/src/test/java/com/woowacourse/moamoa/referenceroom/controller/SearchingReferenceRoomControllerTest.java b/backend/src/test/java/com/woowacourse/moamoa/referenceroom/controller/SearchingReferenceRoomControllerTest.java index 8cbe6f695..fce895e5e 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/referenceroom/controller/SearchingReferenceRoomControllerTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/referenceroom/controller/SearchingReferenceRoomControllerTest.java @@ -35,6 +35,7 @@ import com.woowacourse.moamoa.referenceroom.service.response.LinksResponse; import com.woowacourse.moamoa.study.domain.Study; import com.woowacourse.moamoa.study.domain.repository.StudyRepository; +import com.woowacourse.moamoa.study.service.StudyParticipantService; import com.woowacourse.moamoa.study.service.StudyService; import com.woowacourse.moamoa.study.service.request.StudyRequest; import java.time.LocalDate; @@ -92,9 +93,10 @@ void setUp() { javaStudy = studyService.createStudy(짱구_깃허브_아이디, javaStudyRequest); - studyService.participateStudy(그린론_깃허브_아이디, javaStudy.getId()); - studyService.participateStudy(디우_깃허브_아이디, javaStudy.getId()); - studyService.participateStudy(베루스_깃허브_아이디, javaStudy.getId()); + StudyParticipantService participantService = new StudyParticipantService(memberRepository, studyRepository); + participantService.participateStudy(greenlawn.getId(), javaStudy.getId()); + participantService.participateStudy(dwoo.getId(), javaStudy.getId()); + participantService.participateStudy(verus.getId(), javaStudy.getId()); // 링크 공유 추가 final ReferenceRoomService linkService = new ReferenceRoomService(memberRepository, studyRepository, diff --git a/backend/src/test/java/com/woowacourse/moamoa/referenceroom/query/LinkDaoTest.java b/backend/src/test/java/com/woowacourse/moamoa/referenceroom/query/LinkDaoTest.java index e491a8e0f..f148c4421 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/referenceroom/query/LinkDaoTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/referenceroom/query/LinkDaoTest.java @@ -16,6 +16,7 @@ import com.woowacourse.moamoa.referenceroom.service.request.CreatingLinkRequest; import com.woowacourse.moamoa.study.domain.Study; import com.woowacourse.moamoa.study.domain.repository.StudyRepository; +import com.woowacourse.moamoa.study.service.StudyParticipantService; import com.woowacourse.moamoa.study.service.StudyService; import com.woowacourse.moamoa.study.service.request.StudyRequest; import java.time.LocalDate; @@ -70,9 +71,11 @@ void setUp() { StudyRequest javaStudyRequest = 자바_스터디_신청서(startDate); javaStudy = createStudyService.createStudy(JJANGGU.getGithubId(), javaStudyRequest); - createStudyService.participateStudy(GREENLAWN.getGithubId(), javaStudy.getId()); - createStudyService.participateStudy(DWOO.getGithubId(), javaStudy.getId()); - createStudyService.participateStudy(VERUS.getGithubId(), javaStudy.getId()); + + StudyParticipantService participantService = new StudyParticipantService(memberRepository, studyRepository); + participantService.participateStudy(greenlawn.getId(), javaStudy.getId()); + participantService.participateStudy(dwoo.getId(), javaStudy.getId()); + participantService.participateStudy(verus.getId(), javaStudy.getId()); // 링크 공유 추가 final ReferenceRoomService linkService = new ReferenceRoomService(memberRepository, studyRepository, linkRepository); diff --git a/backend/src/test/java/com/woowacourse/moamoa/review/controller/ReviewControllerTest.java b/backend/src/test/java/com/woowacourse/moamoa/review/controller/ReviewControllerTest.java index 340a35678..f41cd5ac2 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/review/controller/ReviewControllerTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/review/controller/ReviewControllerTest.java @@ -20,6 +20,7 @@ import com.woowacourse.moamoa.review.service.response.WriterResponse; import com.woowacourse.moamoa.study.domain.Study; import com.woowacourse.moamoa.study.domain.repository.StudyRepository; +import com.woowacourse.moamoa.study.service.StudyParticipantService; import com.woowacourse.moamoa.study.service.StudyService; import com.woowacourse.moamoa.study.service.request.StudyRequest; import java.time.LocalDate; @@ -67,7 +68,8 @@ void setUp() { Study javaStudy = studyService.createStudy(1L, javaStudyRequest); - studyService.participateStudy(greelawn.getGithubId(), javaStudy.getId()); + StudyParticipantService participantService = new StudyParticipantService(memberRepository, studyRepository); + participantService.participateStudy(greelawn.getId(), javaStudy.getId()); // 리뷰 추가 ReviewService reviewService = new ReviewService(reviewRepository, memberRepository, studyRepository); diff --git a/backend/src/test/java/com/woowacourse/moamoa/review/controller/SearchingReviewControllerTest.java b/backend/src/test/java/com/woowacourse/moamoa/review/controller/SearchingReviewControllerTest.java index 3237cddbb..41f521d01 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/review/controller/SearchingReviewControllerTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/review/controller/SearchingReviewControllerTest.java @@ -27,6 +27,7 @@ import com.woowacourse.moamoa.review.service.response.WriterResponse; import com.woowacourse.moamoa.study.domain.Study; import com.woowacourse.moamoa.study.domain.repository.StudyRepository; +import com.woowacourse.moamoa.study.service.StudyParticipantService; import com.woowacourse.moamoa.study.service.StudyService; import com.woowacourse.moamoa.study.service.request.StudyRequest; import java.time.LocalDate; @@ -82,10 +83,11 @@ void setUp() { javaStudy = studyService.createStudy(1L, javaStudyRequest); final Study reactStudy = studyService.createStudy(1L, reactStudyRequest); - - studyService.participateStudy(greenlawn.getGithubId(), javaStudy.getId()); - studyService.participateStudy(dwoo.getGithubId(), javaStudy.getId()); - studyService.participateStudy(verus.getGithubId(), javaStudy.getId()); + + StudyParticipantService participantService = new StudyParticipantService(memberRepository, studyRepository); + participantService.participateStudy(greenlawn.getId(), javaStudy.getId()); + participantService.participateStudy(dwoo.getId(), javaStudy.getId()); + participantService.participateStudy(verus.getId(), javaStudy.getId()); // 리뷰 추가 ReviewService reviewService = new ReviewService(reviewRepository, memberRepository, studyRepository); diff --git a/backend/src/test/java/com/woowacourse/moamoa/study/controller/SearchingStudyControllerTest.java b/backend/src/test/java/com/woowacourse/moamoa/study/controller/SearchingStudyControllerTest.java index 8012154fe..6b2caa5b3 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/study/controller/SearchingStudyControllerTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/study/controller/SearchingStudyControllerTest.java @@ -20,6 +20,7 @@ import com.woowacourse.moamoa.study.query.StudySummaryDao; import com.woowacourse.moamoa.study.query.data.StudyDetailsData; import com.woowacourse.moamoa.study.service.SearchingStudyService; +import com.woowacourse.moamoa.study.service.StudyParticipantService; import com.woowacourse.moamoa.study.service.StudyService; import com.woowacourse.moamoa.study.service.request.StudyRequest; import com.woowacourse.moamoa.study.service.response.StudiesResponse; @@ -106,15 +107,17 @@ void initDataBase() { .build(); linuxStudyId = studyService.createStudy(verus.getGithubId(), linuxStudyRequest).getId(); - studyService.participateStudy(dwoo.getGithubId(), javaStudyId); - studyService.participateStudy(verus.getGithubId(), javaStudyId); + StudyParticipantService participantService = new StudyParticipantService(memberRepository, studyRepository); + + participantService.participateStudy(dwoo.getId(), javaStudyId); + participantService.participateStudy(verus.getId(), javaStudyId); - studyService.participateStudy(jjanggu.getGithubId(), reactStudyId); - studyService.participateStudy(greenlawn.getGithubId(), reactStudyId); - studyService.participateStudy(verus.getGithubId(), reactStudyId); + participantService.participateStudy(jjanggu.getId(), reactStudyId); + participantService.participateStudy(greenlawn.getId(), reactStudyId); + participantService.participateStudy(verus.getId(), reactStudyId); - studyService.participateStudy(dwoo.getGithubId(), javaScriptId); - studyService.participateStudy(verus.getGithubId(), javaScriptId); + participantService.participateStudy(dwoo.getId(), javaScriptId); + participantService.participateStudy(verus.getId(), javaScriptId); entityManager.flush(); entityManager.clear(); diff --git a/backend/src/test/java/com/woowacourse/moamoa/study/controller/StudyControllerTest.java b/backend/src/test/java/com/woowacourse/moamoa/study/controller/StudyControllerTest.java index 3622bf45c..8c1c90d17 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/study/controller/StudyControllerTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/study/controller/StudyControllerTest.java @@ -5,7 +5,6 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.springframework.http.HttpStatus.CREATED; -import static org.springframework.http.HttpStatus.OK; import com.woowacourse.moamoa.common.RepositoryTest; import com.woowacourse.moamoa.common.exception.UnauthorizedException; @@ -158,38 +157,6 @@ void createStudyByNotFoundUser() { .isInstanceOf(UnauthorizedException.class); } - @DisplayName("회원은 스터디에 참여할 수 있다.") - @Test - void participateStudy() { - // given - StudyController studyController = new StudyController(new StudyService(studyRepository, memberRepository, - new DateTimeSystem())); - final StudyRequest studyRequest = StudyRequest.builder() - .title("Java") - .excerpt("java excerpt") - .thumbnail("java image") - .description("자바 스터디 상세설명 입니다.") - .startDate(LocalDate.now().plusDays(1)) - .endDate(LocalDate.now().plusDays(4)) - .enrollmentEndDate(LocalDate.now().plusDays(2)) - .maxMemberCount(10) - .tagIds(List.of(1L, 2L)) - .build(); - - final ResponseEntity createdResponse = studyController.createStudy(1L, studyRequest); - - // when - final String location = createdResponse.getHeaders().getLocation().getPath(); - final long studyId = getStudyIdBy(location); - - final Member participant = memberRepository.findByGithubId(2L).get(); - final ResponseEntity response = studyController.participateStudy(participant.getGithubId(), - studyId); - - // then - assertThat(response.getStatusCode()).isEqualTo(OK); - } - @DisplayName("최대인원이 한 명인 경우 바로 모집 종료가 되어야 한다.") @Test void createdStudyWithMaxSizeOne() { diff --git a/backend/src/test/java/com/woowacourse/moamoa/study/controller/StudyParticipantControllerTest.java b/backend/src/test/java/com/woowacourse/moamoa/study/controller/StudyParticipantControllerTest.java new file mode 100644 index 000000000..1f5a441df --- /dev/null +++ b/backend/src/test/java/com/woowacourse/moamoa/study/controller/StudyParticipantControllerTest.java @@ -0,0 +1,76 @@ +package com.woowacourse.moamoa.study.controller; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.http.HttpStatus.NO_CONTENT; + +import com.woowacourse.moamoa.common.RepositoryTest; +import com.woowacourse.moamoa.common.utils.DateTimeSystem; +import com.woowacourse.moamoa.member.domain.Member; +import com.woowacourse.moamoa.member.domain.repository.MemberRepository; +import com.woowacourse.moamoa.study.domain.repository.StudyRepository; +import com.woowacourse.moamoa.study.service.StudyParticipantService; +import com.woowacourse.moamoa.study.service.StudyService; +import com.woowacourse.moamoa.study.service.request.StudyRequest; +import java.time.LocalDate; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; + +@RepositoryTest +class StudyParticipantControllerTest { + + @Autowired + private StudyRepository studyRepository; + + @Autowired + private MemberRepository memberRepository; + + private Member jjanggu; + private Member dwoo; + + @BeforeEach + void initDataBase() { + jjanggu = memberRepository.save(new Member(1L, "jjanggu", "https://image", "github.com")); + dwoo = memberRepository.save(new Member(2L, "dwoo", "https://image", "github.com")); + } + + @DisplayName("회원은 스터디에 참여할 수 있다.") + @Test + void participateStudy() { + // given + StudyController studyController = new StudyController(new StudyService(studyRepository, memberRepository, + new DateTimeSystem())); + final StudyRequest studyRequest = StudyRequest.builder() + .title("Java") + .excerpt("java excerpt") + .thumbnail("java image") + .description("자바 스터디 상세설명 입니다.") + .startDate(LocalDate.now().plusDays(1)) + .endDate(LocalDate.now().plusDays(4)) + .enrollmentEndDate(LocalDate.now().plusDays(2)) + .maxMemberCount(10) + .tagIds(List.of(1L, 2L)) + .build(); + + final ResponseEntity createdResponse = studyController.createStudy(jjanggu.getGithubId(), studyRequest); + + // when + final String location = createdResponse.getHeaders().getLocation().getPath(); + final long studyId = getStudyIdBy(location); + + final StudyParticipantController sut = new StudyParticipantController( + new StudyParticipantService(memberRepository, studyRepository)); + final ResponseEntity response = sut.participateStudy(dwoo.getId(), studyId); + + // then + assertThat(response.getStatusCode()).isEqualTo(NO_CONTENT); + } + + private long getStudyIdBy(final String location) { + final String[] splitLocation = location.split("/"); + return Long.parseLong(splitLocation[3]); + } +} diff --git a/backend/src/test/java/com/woowacourse/moamoa/study/domain/StudyTest.java b/backend/src/test/java/com/woowacourse/moamoa/study/domain/StudyTest.java index a13326907..ba891b05c 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/study/domain/StudyTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/study/domain/StudyTest.java @@ -9,10 +9,14 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatCode; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import com.woowacourse.moamoa.common.exception.UnauthorizedException; +import com.woowacourse.moamoa.referenceroom.service.exception.NotParticipatedMemberException; import com.woowacourse.moamoa.study.domain.exception.InvalidPeriodException; import com.woowacourse.moamoa.study.service.exception.FailureParticipationException; +import com.woowacourse.moamoa.study.service.exception.OwnerCanNotLeaveException; +import com.woowacourse.moamoa.common.exception.UnauthorizedException; import java.time.LocalDate; import java.time.LocalDateTime; import java.util.Set; @@ -281,8 +285,10 @@ public void autoCloseStudyStatus() { final Content content = new Content("title", "excerpt", "thumbnail", "description"); final Participants participants = Participants.createBy(1L); final RecruitPlanner recruitPlanner = new RecruitPlanner(2, RECRUITMENT_END, LocalDate.now().minusDays(3)); - final StudyPlanner studyPlanner = new StudyPlanner(LocalDate.now().minusDays(2), LocalDate.now().plusDays(1), IN_PROGRESS); - final Study sut = new Study(content, participants, recruitPlanner, studyPlanner, AttachedTags.empty(), now().minusDays(4)); + final StudyPlanner studyPlanner = new StudyPlanner(LocalDate.now().minusDays(2), LocalDate.now().plusDays(1), + IN_PROGRESS); + final Study sut = new Study(content, participants, recruitPlanner, studyPlanner, AttachedTags.empty(), + now().minusDays(4)); // when sut.changeStatus(LocalDate.now().plusDays(2)); @@ -299,7 +305,8 @@ public void updateInProgressStatus() { final Participants participants = Participants.createBy(1L); final RecruitPlanner recruitPlanner = new RecruitPlanner(2, RECRUITMENT_START, LocalDate.now().minusDays(1)); final StudyPlanner studyPlanner = new StudyPlanner(LocalDate.now(), LocalDate.now().plusDays(5), PREPARE); - final Study sut = new Study(content, participants, recruitPlanner, studyPlanner, AttachedTags.empty(), now().minusDays(2)); + final Study sut = new Study(content, participants, recruitPlanner, studyPlanner, AttachedTags.empty(), + now().minusDays(2)); // when sut.changeStatus(LocalDate.now()); @@ -316,7 +323,8 @@ public void autoCloseEnrollment() { final Participants participants = Participants.createBy(1L); final RecruitPlanner recruitPlanner = new RecruitPlanner(2, RECRUITMENT_START, LocalDate.now().minusDays(1)); final StudyPlanner studyPlanner = new StudyPlanner(LocalDate.now(), LocalDate.now().plusDays(5), PREPARE); - final Study sut = new Study(content, participants, recruitPlanner, studyPlanner, AttachedTags.empty(), now().minusDays(2)); + final Study sut = new Study(content, participants, recruitPlanner, studyPlanner, AttachedTags.empty(), + now().minusDays(2)); // when sut.changeStatus(LocalDate.now()); @@ -334,7 +342,8 @@ void checkIsParticipant(Long ownerId, Long participantId, Long targetMemberId, b final Participants participants = Participants.createBy(ownerId); final RecruitPlanner recruitPlanner = new RecruitPlanner(2, RECRUITMENT_START, LocalDate.now().minusDays(1)); final StudyPlanner studyPlanner = new StudyPlanner(LocalDate.now(), LocalDate.now().plusDays(5), PREPARE); - final Study sut = new Study(content, participants, recruitPlanner, studyPlanner, AttachedTags.empty(), now().minusDays(2)); + final Study sut = new Study(content, participants, recruitPlanner, studyPlanner, AttachedTags.empty(), + now().minusDays(2)); sut.participate(participantId); @@ -342,6 +351,60 @@ void checkIsParticipant(Long ownerId, Long participantId, Long targetMemberId, b assertThat(sut.isParticipant(targetMemberId)).isEqualTo(expected); } + @DisplayName("스터디장은 탈퇴할 수 없다.") + @Test + void notLeaveOwner() { + final Participant owner = new Participant(1L); + + final Content content = new Content("title", "excerpt", "thumbnail", "description"); + final Participants participants = Participants.createBy(owner.getMemberId()); + final RecruitPlanner recruitPlanner = new RecruitPlanner(2, RECRUITMENT_START, LocalDate.now().minusDays(1)); + final StudyPlanner studyPlanner = new StudyPlanner(LocalDate.now(), LocalDate.now().plusDays(5), PREPARE); + final Study sut = new Study(content, participants, recruitPlanner, studyPlanner, AttachedTags.empty(), + now().minusDays(2)); + + assertThatThrownBy(() -> sut.leave(owner)) + .isInstanceOf(OwnerCanNotLeaveException.class); + } + + @DisplayName("스터디에 참여하지 않은 회원은 탈퇴할 수 없다.") + @Test + void notLeaveNonParticipatedMember() { + final Participant owner = new Participant(1L); + final Participant participant = new Participant(2L); + + final Content content = new Content("title", "excerpt", "thumbnail", "description"); + final Participants participants = Participants.createBy(owner.getMemberId()); + final RecruitPlanner recruitPlanner = new RecruitPlanner(2, RECRUITMENT_START, LocalDate.now().minusDays(1)); + final StudyPlanner studyPlanner = new StudyPlanner(LocalDate.now(), LocalDate.now().plusDays(5), PREPARE); + final Study sut = new Study(content, participants, recruitPlanner, studyPlanner, AttachedTags.empty(), + now().minusDays(2)); + + assertThatThrownBy(() -> sut.leave(participant)) + .isInstanceOf(NotParticipatedMemberException.class); + } + + @DisplayName("스터디원은 탈퇴할 수 있다.") + @Test + void LeaveParticipatedMember() { + final Participant owner = new Participant(1L); + final Participant participant = new Participant(2L); + + final Content content = new Content("title", "excerpt", "thumbnail", "description"); + final Participants participants = Participants.createBy(owner.getMemberId()); + final RecruitPlanner recruitPlanner = new RecruitPlanner(2, RECRUITMENT_START, LocalDate.now().minusDays(1)); + final StudyPlanner studyPlanner = new StudyPlanner(LocalDate.now(), LocalDate.now().plusDays(5), PREPARE); + final Study sut = new Study(content, participants, recruitPlanner, studyPlanner, AttachedTags.empty(), + now().minusDays(2)); + + sut.participate(participant.getMemberId()); + + assertAll( + () -> assertDoesNotThrow(() -> sut.leave(participant)), + () -> assertThat(sut.getParticipants()).isEqualTo(new Participants(owner.getMemberId(), Set.of())) + ); + } + @DisplayName("참여자는 스터디를 업데이트할 수 없다.") @Test public void updateStudyWithParticipant() { From 5ad92f3c2306c23ad0a6e12041dd329920a7b1ff Mon Sep 17 00:00:00 2001 From: jaeseo yoo Date: Wed, 17 Aug 2022 21:11:15 +0900 Subject: [PATCH 32/51] =?UTF-8?q?[BE]=20issue248:=20=EC=8A=A4=ED=84=B0?= =?UTF-8?q?=EB=94=94=20=EA=B3=B5=EC=A7=80=EC=82=AC=ED=95=AD=20CRUD=20=20(#?= =?UTF-8?q?260)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: ArticleController 이름 변경 및 PathVariable 추가 * feat: 스터디 공지사항 CRUD 구현 * refactor: 레포지토리 정리 및 ArticleType 추가 * refactor: PermittedParticipants 추가 (#264) * refactor: PermittedParticipants 추가 * refactor: StudyRoom, PermittedParticipants 추출 * chore: 충돌 해결 * refactor: 리뷰 반영 * fix: 네이밍 수정 Co-authored-by: 정진혁 Co-authored-by: Donggyu --- .../auth/config/AuthRequestMatchConfig.java | 15 +- .../entity/ReadOnlyCollectionPersister.java | 23 ++ .../community/domain/CommunityArticle.java | 104 ------ .../CommunityArticleRepository.java | 7 - .../community/query/CommunityArticleDao.java | 80 ----- .../service/CommunityArticleService.java | 108 ------ .../controller/ArticleController.java} | 43 ++- .../converter/ArticleTypeConverter.java | 14 + .../moamoa/studyroom/domain/Accessor.java | 20 ++ .../moamoa/studyroom/domain/Article.java | 56 +++ .../moamoa/studyroom/domain/ArticleType.java | 10 + .../studyroom/domain/CommunityArticle.java | 72 ++++ .../studyroom/domain/NoticeArticle.java | 72 ++++ .../domain/PermittedParticipants.java | 44 +++ .../moamoa/studyroom/domain/StudyRoom.java | 68 ++++ .../repository/article/ArticleRepository.java | 18 + .../article/ArticleRepositoryFactory.java | 24 ++ .../article/CommunityArticleRepository.java | 13 + .../article/NoticeArticleRepository.java | 13 + .../studyroom/JpaStudyRoomRepository.java | 7 + .../studyroom/StudyRoomRepository.java | 9 + .../moamoa/studyroom/query/ArticleDao.java | 86 +++++ .../query/data/ArticleData.java} | 10 +- .../studyroom/service/ArticleService.java | 118 +++++++ .../exception/ArticleNotFoundException.java | 2 +- .../exception/NotArticleAuthorException.java | 2 +- .../exception/NotRelatedArticleException.java | 2 +- .../exception/UneditableArticleException.java | 2 +- .../exception/UnviewableArticleException.java | 2 +- .../service/request/ArticleRequest.java | 2 +- .../service/response/ArticleResponse.java | 6 +- .../response/ArticleSummariesResponse.java | 2 +- .../response/ArticleSummaryResponse.java | 6 +- .../service/response/AuthorResponse.java | 2 +- .../acceptance/AcceptanceTest.java | 3 +- .../acceptance/steps/StudyRelatedSteps.java | 22 +- .../CommunityAcceptanceTest.java | 12 +- .../test/studyroom/NoticeAcceptanceTest.java | 327 ++++++++++++++++++ .../CommunityArticleControllerTest.java | 103 ------ .../domain/CommunityArticleTest.java | 102 ------ .../controller/ArticleControllerTest.java | 133 +++++++ .../DeletingArticleControllerTest.java} | 53 +-- .../GettingArticleControllerTest.java} | 62 ++-- ...mmunityArticleSummariesControllerTest.java | 65 ++-- .../UpdatingArticleControllerTest.java} | 78 +++-- .../moamoa/studyroom/domain/ArticleTest.java | 90 +++++ .../CreatingArticleControllerWebMvcTest.java} | 12 +- .../DeletingArticleControllerWebMvcTest.java} | 4 +- .../GettingArticleControllerWebMvcTest.java} | 4 +- .../UpdatingArticleControllerWebMvcTest.java} | 6 +- backend/src/test/resources/schema.sql | 18 +- 51 files changed, 1471 insertions(+), 685 deletions(-) create mode 100644 backend/src/main/java/com/woowacourse/moamoa/common/entity/ReadOnlyCollectionPersister.java delete mode 100644 backend/src/main/java/com/woowacourse/moamoa/community/domain/CommunityArticle.java delete mode 100644 backend/src/main/java/com/woowacourse/moamoa/community/domain/repository/CommunityArticleRepository.java delete mode 100644 backend/src/main/java/com/woowacourse/moamoa/community/query/CommunityArticleDao.java delete mode 100644 backend/src/main/java/com/woowacourse/moamoa/community/service/CommunityArticleService.java rename backend/src/main/java/com/woowacourse/moamoa/{community/controller/CommunityArticleController.java => studyroom/controller/ArticleController.java} (62%) create mode 100644 backend/src/main/java/com/woowacourse/moamoa/studyroom/controller/converter/ArticleTypeConverter.java create mode 100644 backend/src/main/java/com/woowacourse/moamoa/studyroom/domain/Accessor.java create mode 100644 backend/src/main/java/com/woowacourse/moamoa/studyroom/domain/Article.java create mode 100644 backend/src/main/java/com/woowacourse/moamoa/studyroom/domain/ArticleType.java create mode 100644 backend/src/main/java/com/woowacourse/moamoa/studyroom/domain/CommunityArticle.java create mode 100644 backend/src/main/java/com/woowacourse/moamoa/studyroom/domain/NoticeArticle.java create mode 100644 backend/src/main/java/com/woowacourse/moamoa/studyroom/domain/PermittedParticipants.java create mode 100644 backend/src/main/java/com/woowacourse/moamoa/studyroom/domain/StudyRoom.java create mode 100644 backend/src/main/java/com/woowacourse/moamoa/studyroom/domain/repository/article/ArticleRepository.java create mode 100644 backend/src/main/java/com/woowacourse/moamoa/studyroom/domain/repository/article/ArticleRepositoryFactory.java create mode 100644 backend/src/main/java/com/woowacourse/moamoa/studyroom/domain/repository/article/CommunityArticleRepository.java create mode 100644 backend/src/main/java/com/woowacourse/moamoa/studyroom/domain/repository/article/NoticeArticleRepository.java create mode 100644 backend/src/main/java/com/woowacourse/moamoa/studyroom/domain/repository/studyroom/JpaStudyRoomRepository.java create mode 100644 backend/src/main/java/com/woowacourse/moamoa/studyroom/domain/repository/studyroom/StudyRoomRepository.java create mode 100644 backend/src/main/java/com/woowacourse/moamoa/studyroom/query/ArticleDao.java rename backend/src/main/java/com/woowacourse/moamoa/{community/query/data/CommunityArticleData.java => studyroom/query/data/ArticleData.java} (62%) create mode 100644 backend/src/main/java/com/woowacourse/moamoa/studyroom/service/ArticleService.java rename backend/src/main/java/com/woowacourse/moamoa/{community => studyroom}/service/exception/ArticleNotFoundException.java (82%) rename backend/src/main/java/com/woowacourse/moamoa/{community => studyroom}/service/exception/NotArticleAuthorException.java (84%) rename backend/src/main/java/com/woowacourse/moamoa/{community => studyroom}/service/exception/NotRelatedArticleException.java (84%) rename backend/src/main/java/com/woowacourse/moamoa/{community => studyroom}/service/exception/UneditableArticleException.java (77%) rename backend/src/main/java/com/woowacourse/moamoa/{community => studyroom}/service/exception/UnviewableArticleException.java (85%) rename backend/src/main/java/com/woowacourse/moamoa/{community => studyroom}/service/request/ArticleRequest.java (90%) rename backend/src/main/java/com/woowacourse/moamoa/{community => studyroom}/service/response/ArticleResponse.java (78%) rename backend/src/main/java/com/woowacourse/moamoa/{community => studyroom}/service/response/ArticleSummariesResponse.java (92%) rename backend/src/main/java/com/woowacourse/moamoa/{community => studyroom}/service/response/ArticleSummaryResponse.java (84%) rename backend/src/main/java/com/woowacourse/moamoa/{community => studyroom}/service/response/AuthorResponse.java (91%) rename backend/src/test/java/com/woowacourse/acceptance/test/{community => studyroom}/CommunityAcceptanceTest.java (98%) create mode 100644 backend/src/test/java/com/woowacourse/acceptance/test/studyroom/NoticeAcceptanceTest.java delete mode 100644 backend/src/test/java/com/woowacourse/moamoa/community/controller/CommunityArticleControllerTest.java delete mode 100644 backend/src/test/java/com/woowacourse/moamoa/community/domain/CommunityArticleTest.java create mode 100644 backend/src/test/java/com/woowacourse/moamoa/studyroom/controller/ArticleControllerTest.java rename backend/src/test/java/com/woowacourse/moamoa/{community/controller/DeletingCommunityArticleControllerTest.java => studyroom/controller/DeletingArticleControllerTest.java} (65%) rename backend/src/test/java/com/woowacourse/moamoa/{community/controller/GettingCommunityArticleControllerTest.java => studyroom/controller/GettingArticleControllerTest.java} (63%) rename backend/src/test/java/com/woowacourse/moamoa/{community => studyroom}/controller/GettingCommunityArticleSummariesControllerTest.java (66%) rename backend/src/test/java/com/woowacourse/moamoa/{community/controller/UpdatingCommunityArticleControllerTest.java => studyroom/controller/UpdatingArticleControllerTest.java} (51%) create mode 100644 backend/src/test/java/com/woowacourse/moamoa/studyroom/domain/ArticleTest.java rename backend/src/test/java/com/woowacourse/moamoa/{community/webmvc/CreatingCommunityArticleControllerWebMvcTest.java => studyroom/webmvc/CreatingArticleControllerWebMvcTest.java} (93%) rename backend/src/test/java/com/woowacourse/moamoa/{community/webmvc/DeletingCommunityArticleControllerWebMvcTest.java => studyroom/webmvc/DeletingArticleControllerWebMvcTest.java} (93%) rename backend/src/test/java/com/woowacourse/moamoa/{community/webmvc/GettingCommunityArticleControllerWebMvcTest.java => studyroom/webmvc/GettingArticleControllerWebMvcTest.java} (97%) rename backend/src/test/java/com/woowacourse/moamoa/{community/webmvc/UpdatingCommunityArticleControllerWebMvcTest.java => studyroom/webmvc/UpdatingArticleControllerWebMvcTest.java} (96%) diff --git a/backend/src/main/java/com/woowacourse/moamoa/auth/config/AuthRequestMatchConfig.java b/backend/src/main/java/com/woowacourse/moamoa/auth/config/AuthRequestMatchConfig.java index eef1c8e22..dcad55ba3 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/auth/config/AuthRequestMatchConfig.java +++ b/backend/src/main/java/com/woowacourse/moamoa/auth/config/AuthRequestMatchConfig.java @@ -1,5 +1,7 @@ package com.woowacourse.moamoa.auth.config; +import static org.springframework.http.HttpMethod.*; + import com.woowacourse.moamoa.auth.controller.matcher.AuthenticationRequestMatcher; import com.woowacourse.moamoa.auth.controller.matcher.AuthenticationRequestMatcherBuilder; import lombok.AllArgsConstructor; @@ -14,14 +16,13 @@ public class AuthRequestMatchConfig { @Bean public AuthenticationRequestMatcher authenticationRequestMatcher() { return new AuthenticationRequestMatcherBuilder() - .addUpAuthenticationPath(HttpMethod.POST, + .addUpAuthenticationPath(POST, "/api/studies", "/api/studies/\\d+/reviews", "/api/studies/\\d+/reviews/\\d+", - "/api/studies/\\w+/community/articles", - "/api/studies" - ) - .addUpAuthenticationPath(HttpMethod.GET, + "/api/studies/\\w+/\\w+/articles", + "/api/studies") + .addUpAuthenticationPath(GET, "/api/my/studies", "/api/members/me", "/api/members/me/role", @@ -36,9 +37,7 @@ public AuthenticationRequestMatcher authenticationRequestMatcher() { ) .addUpAuthenticationPath(HttpMethod.DELETE, "/api/studies/\\d+/reviews/\\d+", - "/api/studies/\\w+/community/articles/\\w+", - "/api/studies/\\d+/reference-room/links/\\d+" - ) + "/api/studies/\\w+/\\w+/articles/\\w+") .build(); } } diff --git a/backend/src/main/java/com/woowacourse/moamoa/common/entity/ReadOnlyCollectionPersister.java b/backend/src/main/java/com/woowacourse/moamoa/common/entity/ReadOnlyCollectionPersister.java new file mode 100644 index 000000000..25547d936 --- /dev/null +++ b/backend/src/main/java/com/woowacourse/moamoa/common/entity/ReadOnlyCollectionPersister.java @@ -0,0 +1,23 @@ +package com.woowacourse.moamoa.common.entity; + +import org.hibernate.MappingException; +import org.hibernate.cache.CacheException; +import org.hibernate.cache.spi.access.CollectionDataAccess; +import org.hibernate.mapping.Collection; +import org.hibernate.persister.collection.BasicCollectionPersister; +import org.hibernate.persister.spi.PersisterCreationContext; + +public class ReadOnlyCollectionPersister extends BasicCollectionPersister { + + public ReadOnlyCollectionPersister(final Collection collectionBinding, + final CollectionDataAccess cacheAccessStrategy, + final PersisterCreationContext creationContext) + throws MappingException, CacheException { + super(asInverse(collectionBinding), cacheAccessStrategy, creationContext); + } + + private static Collection asInverse(Collection collection) { + collection.setInverse(true); + return collection; + } +} diff --git a/backend/src/main/java/com/woowacourse/moamoa/community/domain/CommunityArticle.java b/backend/src/main/java/com/woowacourse/moamoa/community/domain/CommunityArticle.java deleted file mode 100644 index 4adb36eb4..000000000 --- a/backend/src/main/java/com/woowacourse/moamoa/community/domain/CommunityArticle.java +++ /dev/null @@ -1,104 +0,0 @@ -package com.woowacourse.moamoa.community.domain; - -import static javax.persistence.GenerationType.IDENTITY; - -import com.woowacourse.moamoa.common.entity.BaseEntity; -import com.woowacourse.moamoa.community.service.request.ArticleRequest; -import com.woowacourse.moamoa.member.domain.Member; -import com.woowacourse.moamoa.member.service.exception.NotParticipatedMemberException; -import com.woowacourse.moamoa.study.domain.Study; -import java.util.Objects; -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.FetchType; -import javax.persistence.GeneratedValue; -import javax.persistence.Id; -import javax.persistence.JoinColumn; -import javax.persistence.ManyToOne; -import javax.persistence.Table; -import lombok.AccessLevel; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Entity -@NoArgsConstructor(access = AccessLevel.PROTECTED) -@Getter -@Table(name = "article") -public class CommunityArticle extends BaseEntity { - - @Id - @GeneratedValue(strategy = IDENTITY) - private Long id; - - private String title; - - private String content; - - @Column(name = "author_id") - private Long authorId; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "study_id") - private Study study; - - private CommunityArticle(final String title, final String content, final Long authorId, final Study study) { - this(null, title, content, authorId, study); - } - - public CommunityArticle(final Long id, final String title, final String content, final Long authorId, - final Study study) { - this.id = id; - this.title = title; - this.content = content; - this.authorId = authorId; - this.study = study; - } - - public static CommunityArticle write(final Member member, final Study study, final ArticleRequest request) { - if (!study.isParticipant(member.getId())) { - throw new NotParticipatedMemberException(); - } - - return new CommunityArticle(request.getTitle(), request.getContent(), member.getId(), study); - } - - public void update(final String title, final String content) { - this.title = title; - this.content = content; - } - - public boolean isViewableBy(final Long studyId, final Long memberId) { - return isBelongTo(studyId) && study.isParticipant(memberId); - } - - public boolean isEditableBy(final Long studyId, final Long memberId) { - return isViewableBy(studyId, memberId) && isAuthor(memberId); - } - - private boolean isBelongTo(final Long studyId) { - return this.study.getId().equals(studyId); - } - - private boolean isAuthor(final Long memberId) { - return this.authorId.equals(memberId); - } - - @Override - public boolean equals(final Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - final CommunityArticle that = (CommunityArticle) o; - return Objects.equals(getId(), that.getId()) && Objects.equals(getTitle(), that.getTitle()) - && Objects.equals(getContent(), that.getContent()) && Objects.equals(getAuthorId(), - that.getAuthorId()) && Objects.equals(getStudy().getId(), that.getStudy().getId()); - } - - @Override - public int hashCode() { - return Objects.hash(getId(), getTitle(), getContent(), getAuthorId(), getStudy().getId()); - } -} diff --git a/backend/src/main/java/com/woowacourse/moamoa/community/domain/repository/CommunityArticleRepository.java b/backend/src/main/java/com/woowacourse/moamoa/community/domain/repository/CommunityArticleRepository.java deleted file mode 100644 index d7b1d0dd5..000000000 --- a/backend/src/main/java/com/woowacourse/moamoa/community/domain/repository/CommunityArticleRepository.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.woowacourse.moamoa.community.domain.repository; - -import com.woowacourse.moamoa.community.domain.CommunityArticle; -import org.springframework.data.jpa.repository.JpaRepository; - -public interface CommunityArticleRepository extends JpaRepository { -} diff --git a/backend/src/main/java/com/woowacourse/moamoa/community/query/CommunityArticleDao.java b/backend/src/main/java/com/woowacourse/moamoa/community/query/CommunityArticleDao.java deleted file mode 100644 index c8b5c4e49..000000000 --- a/backend/src/main/java/com/woowacourse/moamoa/community/query/CommunityArticleDao.java +++ /dev/null @@ -1,80 +0,0 @@ -package com.woowacourse.moamoa.community.query; - -import com.woowacourse.moamoa.community.query.data.CommunityArticleData; -import com.woowacourse.moamoa.member.query.data.MemberData; -import java.time.LocalDate; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageImpl; -import org.springframework.data.domain.Pageable; -import org.springframework.jdbc.core.RowMapper; -import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; -import org.springframework.stereotype.Repository; - -@Repository -public class CommunityArticleDao { - - public static final RowMapper ROW_MAPPER = (rs, rn) -> { - final long id = rs.getLong("article.id"); - final String title = rs.getString("article.title"); - final String content = rs.getString("article.content"); - final LocalDate createdDate = rs.getObject("article.created_date", LocalDate.class); - final LocalDate lastModifiedDate = rs.getObject("article.last_modified_date", LocalDate.class); - - final long githubId = rs.getLong("member.github_id"); - final String username = rs.getString("member.username"); - final String imageUrl = rs.getString("member.image_url"); - final String profileUrl = rs.getString("member.profile_url"); - MemberData memberData = new MemberData(githubId, username, imageUrl, profileUrl); - - return new CommunityArticleData(id, memberData, title, content, createdDate, lastModifiedDate); - }; - private final NamedParameterJdbcTemplate namedParameterJdbcTemplate; - - - public CommunityArticleDao(final NamedParameterJdbcTemplate namedParameterJdbcTemplate) { - this.namedParameterJdbcTemplate = namedParameterJdbcTemplate; - } - - public Optional getById(final Long articleId) { - final String sql = "SELECT article.id, article.title, article.content, " - + "article.created_date, article.last_modified_date, " - + "member.github_id, member.username, member.image_url, member.profile_url " - + "FROM article JOIN member ON article.author_id = member.id " - + "WHERE article.id = :articleId"; - final Map params = Map.of("articleId", articleId); - return namedParameterJdbcTemplate.query(sql, params, ROW_MAPPER).stream().findAny(); - } - - public Page getAllByStudyId(final Long studyId, final Pageable pageable) { - final List content = getContent(studyId, pageable); - final int totalCount = getTotalCount(studyId); - return new PageImpl<>(content, pageable, totalCount); - } - - private List getContent(final Long studyId, final Pageable pageable) { - final String sql = "SELECT article.id, article.title, article.content, " - + "article.created_date, article.last_modified_date, " - + "member.github_id, member.username, member.image_url, member.profile_url " - + "FROM article JOIN member ON article.author_id = member.id " - + "WHERE article.study_id = :studyId " - + "ORDER BY created_date DESC, id DESC " - + "LIMIT :size OFFSET :offset"; - - final Map params = Map.of( - "studyId", studyId, - "size", pageable.getPageSize(), - "offset", pageable.getOffset() - ); - - return namedParameterJdbcTemplate.query(sql, params, ROW_MAPPER); - } - - private int getTotalCount(final Long studyId) { - final String sql = "SELECT count(article.id) FROM article WHERE article.study_id = :studyId"; - final Map param = Map.of("studyId", studyId); - return namedParameterJdbcTemplate.queryForObject(sql, param, (rs, rn) -> rs.getInt(1)); - } -} diff --git a/backend/src/main/java/com/woowacourse/moamoa/community/service/CommunityArticleService.java b/backend/src/main/java/com/woowacourse/moamoa/community/service/CommunityArticleService.java deleted file mode 100644 index 2157c4054..000000000 --- a/backend/src/main/java/com/woowacourse/moamoa/community/service/CommunityArticleService.java +++ /dev/null @@ -1,108 +0,0 @@ -package com.woowacourse.moamoa.community.service; - -import com.woowacourse.moamoa.community.domain.CommunityArticle; -import com.woowacourse.moamoa.community.domain.repository.CommunityArticleRepository; -import com.woowacourse.moamoa.community.query.CommunityArticleDao; -import com.woowacourse.moamoa.community.query.data.CommunityArticleData; -import com.woowacourse.moamoa.community.service.exception.ArticleNotFoundException; -import com.woowacourse.moamoa.community.service.exception.UneditableArticleException; -import com.woowacourse.moamoa.community.service.exception.UnviewableArticleException; -import com.woowacourse.moamoa.community.service.request.ArticleRequest; -import com.woowacourse.moamoa.community.service.response.ArticleResponse; -import com.woowacourse.moamoa.community.service.response.ArticleSummariesResponse; -import com.woowacourse.moamoa.community.service.response.ArticleSummaryResponse; -import com.woowacourse.moamoa.member.domain.Member; -import com.woowacourse.moamoa.member.domain.repository.MemberRepository; -import com.woowacourse.moamoa.member.service.exception.MemberNotFoundException; -import com.woowacourse.moamoa.study.domain.Study; -import com.woowacourse.moamoa.study.domain.repository.StudyRepository; -import com.woowacourse.moamoa.study.service.exception.StudyNotFoundException; -import java.util.List; -import java.util.stream.Collectors; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -@Service -@Transactional(readOnly = true) -public class CommunityArticleService { - - private final MemberRepository memberRepository; - private final StudyRepository studyRepository; - private final CommunityArticleRepository communityArticleRepository; - private final CommunityArticleDao communityArticleDao; - - public CommunityArticleService(final MemberRepository memberRepository, - final StudyRepository studyRepository, - final CommunityArticleRepository communityArticleRepository, - final CommunityArticleDao communityArticleDao) { - this.memberRepository = memberRepository; - this.studyRepository = studyRepository; - this.communityArticleRepository = communityArticleRepository; - this.communityArticleDao = communityArticleDao; - } - - @Transactional - public CommunityArticle createArticle(final Long memberId, final Long studyId, - final ArticleRequest request) { - final Member member = memberRepository.findById(memberId).orElseThrow(MemberNotFoundException::new); - final Study study = studyRepository.findById(studyId).orElseThrow(StudyNotFoundException::new); - - return communityArticleRepository.save(CommunityArticle.write(member, study, request)); - } - - public ArticleResponse getArticle(final Long memberId, final Long studyId, final Long articleId) { - final CommunityArticle article = communityArticleRepository.findById(articleId) - .orElseThrow(() -> new ArticleNotFoundException(articleId)); - - if (!article.isViewableBy(studyId, memberId)) { - throw new UnviewableArticleException(studyId, memberId); - } - - final CommunityArticleData data = communityArticleDao.getById(articleId) - .orElseThrow(() -> new ArticleNotFoundException(articleId)); - return new ArticleResponse(data); - } - - @Transactional - public void deleteArticle(final Long memberId, final Long studyId, final Long articleId) { - final CommunityArticle article = communityArticleRepository.findById(articleId) - .orElseThrow(() -> new ArticleNotFoundException(articleId)); - - if (!article.isEditableBy(studyId, memberId)) { - throw new UneditableArticleException(); - } - - communityArticleRepository.deleteById(articleId); - } - - public ArticleSummariesResponse getArticles(final Long memberId, final Long studyId, final Pageable pageable) { - final Study study = studyRepository.findById(studyId).orElseThrow(StudyNotFoundException::new); - - if (!study.isParticipant(memberId)) { - throw new UnviewableArticleException(studyId, memberId); - } - - final Page page = communityArticleDao.getAllByStudyId(studyId, pageable); - - final List articles = page.getContent().stream() - .map(ArticleSummaryResponse::new) - .collect(Collectors.toList()); - - return new ArticleSummariesResponse(articles, page.getNumber(), page.getTotalPages() - 1, page.getTotalElements()); - } - - @Transactional - public void updateArticle(final Long memberId, final Long studyId, final Long articleId, - final ArticleRequest request) { - final CommunityArticle article = communityArticleRepository.findById(articleId) - .orElseThrow(() -> new ArticleNotFoundException(articleId)); - - if (!article.isEditableBy(studyId, memberId)) { - throw new UneditableArticleException(); - } - - article.update(request.getTitle(), request.getContent()); - } -} diff --git a/backend/src/main/java/com/woowacourse/moamoa/community/controller/CommunityArticleController.java b/backend/src/main/java/com/woowacourse/moamoa/studyroom/controller/ArticleController.java similarity index 62% rename from backend/src/main/java/com/woowacourse/moamoa/community/controller/CommunityArticleController.java rename to backend/src/main/java/com/woowacourse/moamoa/studyroom/controller/ArticleController.java index ca9fe0ef9..435b4113b 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/community/controller/CommunityArticleController.java +++ b/backend/src/main/java/com/woowacourse/moamoa/studyroom/controller/ArticleController.java @@ -1,12 +1,12 @@ -package com.woowacourse.moamoa.community.controller; +package com.woowacourse.moamoa.studyroom.controller; import com.woowacourse.moamoa.auth.config.AuthenticatedMember; -import com.woowacourse.moamoa.auth.config.AuthenticationPrincipal; -import com.woowacourse.moamoa.community.domain.CommunityArticle; -import com.woowacourse.moamoa.community.service.CommunityArticleService; -import com.woowacourse.moamoa.community.service.request.ArticleRequest; -import com.woowacourse.moamoa.community.service.response.ArticleResponse; -import com.woowacourse.moamoa.community.service.response.ArticleSummariesResponse; +import com.woowacourse.moamoa.studyroom.domain.Article; +import com.woowacourse.moamoa.studyroom.domain.ArticleType; +import com.woowacourse.moamoa.studyroom.service.ArticleService; +import com.woowacourse.moamoa.studyroom.service.request.ArticleRequest; +import com.woowacourse.moamoa.studyroom.service.response.ArticleResponse; +import com.woowacourse.moamoa.studyroom.service.response.ArticleSummariesResponse; import java.net.URI; import javax.validation.Valid; import org.springframework.data.domain.Pageable; @@ -23,49 +23,53 @@ import org.springframework.web.bind.annotation.RestController; @RestController -@RequestMapping("api/studies/{study-id}/community/articles") -public class CommunityArticleController { +@RequestMapping("api/studies/{study-id}/{article-type}/articles") +public class ArticleController { - private final CommunityArticleService communityArticleService; + private final ArticleService articleService; - public CommunityArticleController(final CommunityArticleService communityArticleService) { - this.communityArticleService = communityArticleService; + public ArticleController(final ArticleService articleService) { + this.articleService = articleService; } @PostMapping public ResponseEntity createArticle(@AuthenticatedMember final Long id, @PathVariable("study-id") final Long studyId, + @PathVariable("article-type") final ArticleType type, @Valid @RequestBody final ArticleRequest request ) { - final CommunityArticle article = communityArticleService.createArticle(id, studyId, request); - final URI location = URI.create("/api/studies/" + studyId + "/community/articles/" + article.getId()); + final Article article = articleService.createArticle(id, studyId, request, type); + final URI location = URI.create("/api/studies/" + studyId + "/" + type.lowerName() + "/articles/" + article.getId()); return ResponseEntity.created(location).header("Access-Control-Allow-Headers", HttpHeaders.LOCATION).build(); } @GetMapping("/{article-id}") public ResponseEntity getArticle(@AuthenticatedMember final Long id, @PathVariable("study-id") final Long studyId, + @PathVariable("article-type") final ArticleType articleType, @PathVariable("article-id") final Long articleId ) { - ArticleResponse response = communityArticleService.getArticle(id, studyId, articleId); + ArticleResponse response = articleService.getArticle(id, studyId, articleId, articleType); return ResponseEntity.ok().body(response); } @DeleteMapping("{article-id}") public ResponseEntity deleteArticle(@AuthenticatedMember final Long id, @PathVariable("study-id") final Long studyId, - @PathVariable("article-id") final Long articleId + @PathVariable("article-id") final Long articleId, + @PathVariable("article-type") final ArticleType type ) { - communityArticleService.deleteArticle(id, studyId, articleId); + articleService.deleteArticle(id, studyId, articleId, type); return ResponseEntity.noContent().build(); } @GetMapping public ResponseEntity getArticles(@AuthenticatedMember final Long id, @PathVariable("study-id") final Long studyId, + @PathVariable("article-type") final ArticleType type, @PageableDefault final Pageable pageable ) { - ArticleSummariesResponse response = communityArticleService.getArticles(id, studyId, pageable); + ArticleSummariesResponse response = articleService.getArticles(id, studyId, pageable, type); return ResponseEntity.ok().body(response); } @@ -73,9 +77,10 @@ public ResponseEntity getArticles(@AuthenticatedMember public ResponseEntity updateArticle(@AuthenticatedMember final Long id, @PathVariable("study-id") final Long studyId, @PathVariable("article-id") final Long articleId, + @PathVariable("article-type") final ArticleType type, @Valid @RequestBody final ArticleRequest request ) { - communityArticleService.updateArticle(id, studyId, articleId, request); + articleService.updateArticle(id, studyId, articleId, request, type); return ResponseEntity.noContent().build(); } } diff --git a/backend/src/main/java/com/woowacourse/moamoa/studyroom/controller/converter/ArticleTypeConverter.java b/backend/src/main/java/com/woowacourse/moamoa/studyroom/controller/converter/ArticleTypeConverter.java new file mode 100644 index 000000000..bdb8eabb8 --- /dev/null +++ b/backend/src/main/java/com/woowacourse/moamoa/studyroom/controller/converter/ArticleTypeConverter.java @@ -0,0 +1,14 @@ +package com.woowacourse.moamoa.studyroom.controller.converter; + +import com.woowacourse.moamoa.studyroom.domain.ArticleType; +import org.springframework.core.convert.converter.Converter; +import org.springframework.stereotype.Component; + +@Component +public class ArticleTypeConverter implements Converter { + + @Override + public ArticleType convert(final String source) { + return ArticleType.valueOf(source.toUpperCase()); + } +} diff --git a/backend/src/main/java/com/woowacourse/moamoa/studyroom/domain/Accessor.java b/backend/src/main/java/com/woowacourse/moamoa/studyroom/domain/Accessor.java new file mode 100644 index 000000000..9510ed9bc --- /dev/null +++ b/backend/src/main/java/com/woowacourse/moamoa/studyroom/domain/Accessor.java @@ -0,0 +1,20 @@ +package com.woowacourse.moamoa.studyroom.domain; + +public class Accessor { + + private final Long memberId; + private final Long studyId; + + public Accessor(final Long memberId, final Long studyId) { + this.memberId = memberId; + this.studyId = studyId; + } + + Long getStudyId() { + return studyId; + } + + Long getMemberId() { + return memberId; + } +} diff --git a/backend/src/main/java/com/woowacourse/moamoa/studyroom/domain/Article.java b/backend/src/main/java/com/woowacourse/moamoa/studyroom/domain/Article.java new file mode 100644 index 000000000..7a2030836 --- /dev/null +++ b/backend/src/main/java/com/woowacourse/moamoa/studyroom/domain/Article.java @@ -0,0 +1,56 @@ +package com.woowacourse.moamoa.studyroom.domain; + +import static javax.persistence.GenerationType.IDENTITY; + +import com.woowacourse.moamoa.common.entity.BaseEntity; +import com.woowacourse.moamoa.member.service.exception.NotParticipatedMemberException; +import javax.persistence.Column; +import javax.persistence.FetchType; +import javax.persistence.GeneratedValue; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.MappedSuperclass; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@MappedSuperclass +@NoArgsConstructor +@Getter +public abstract class Article extends BaseEntity { + + @Id + @GeneratedValue(strategy = IDENTITY) + private Long id; + + @Column(name = "author_id") + private Long authorId; + + @ManyToOne(fetch = FetchType.EAGER) + @JoinColumn(name = "study_id") + private StudyRoom studyRoom; + + public Article(final Long id, final Long authorId, StudyRoom studyRoom) { + this.id = id; + this.authorId = authorId; + this.studyRoom = studyRoom; + } + + protected final boolean isPermittedAccessor(final Accessor accessor) { + return studyRoom.isPermittedAccessor(accessor); + } + + protected final boolean isAuthor(final Accessor accessor) { + return this.authorId.equals(accessor.getMemberId()); + } + + protected final boolean isOwner(final Accessor accessor) { + return studyRoom.isOwner(accessor); + } + + public abstract void update(Accessor accessor, String title, String content); + + public abstract boolean isEditableBy(final Accessor accessor); + + public abstract boolean isViewableBy(final Accessor accessor); +} diff --git a/backend/src/main/java/com/woowacourse/moamoa/studyroom/domain/ArticleType.java b/backend/src/main/java/com/woowacourse/moamoa/studyroom/domain/ArticleType.java new file mode 100644 index 000000000..8b299d329 --- /dev/null +++ b/backend/src/main/java/com/woowacourse/moamoa/studyroom/domain/ArticleType.java @@ -0,0 +1,10 @@ +package com.woowacourse.moamoa.studyroom.domain; + +public enum ArticleType { + + COMMUNITY, NOTICE; + + public String lowerName() { + return name().toLowerCase(); + } +} diff --git a/backend/src/main/java/com/woowacourse/moamoa/studyroom/domain/CommunityArticle.java b/backend/src/main/java/com/woowacourse/moamoa/studyroom/domain/CommunityArticle.java new file mode 100644 index 000000000..c44a6f1fa --- /dev/null +++ b/backend/src/main/java/com/woowacourse/moamoa/studyroom/domain/CommunityArticle.java @@ -0,0 +1,72 @@ +package com.woowacourse.moamoa.studyroom.domain; + +import java.util.Objects; +import javax.persistence.Entity; +import javax.persistence.Table; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +@Table(name = "community") +public class CommunityArticle extends Article { + + private String title; + + private String content; + + CommunityArticle(final String title, final String content, final Long authorId, + final StudyRoom studyRoom) { + super(null, authorId, studyRoom); + this.title = title; + this.content = content; + } + + public CommunityArticle(final Long id, final String title, final String content, final Long authorId, + final StudyRoom studyRoom) { + super(id, authorId, studyRoom); + this.title = title; + this.content = content; + } + + @Override + public boolean isViewableBy(final Accessor accessor) { + return isPermittedAccessor(accessor); + } + + @Override + public boolean isEditableBy(final Accessor accessor) { + return isPermittedAccessor(accessor) && isAuthor(accessor); + } + + @Override + public void update(final Accessor accessor, final String title, final String content) { + if (!isEditableBy(accessor)) { + throw new IllegalArgumentException(); + } + + this.title = title; + this.content = content; + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final CommunityArticle that = (CommunityArticle) o; + return Objects.equals(getId(), that.getId()) && Objects.equals(getTitle(), that.getTitle()) + && Objects.equals(getContent(), that.getContent()) && Objects.equals(getAuthorId(), + that.getAuthorId()) && Objects.equals(getStudyRoom(), that.getStudyRoom()); + } + + @Override + public int hashCode() { + return Objects.hash(getId(), getTitle(), getContent(), getAuthorId(), getStudyRoom()); + } +} diff --git a/backend/src/main/java/com/woowacourse/moamoa/studyroom/domain/NoticeArticle.java b/backend/src/main/java/com/woowacourse/moamoa/studyroom/domain/NoticeArticle.java new file mode 100644 index 000000000..2f70da4aa --- /dev/null +++ b/backend/src/main/java/com/woowacourse/moamoa/studyroom/domain/NoticeArticle.java @@ -0,0 +1,72 @@ +package com.woowacourse.moamoa.studyroom.domain; + +import java.util.Objects; +import javax.persistence.Entity; +import javax.persistence.Table; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +@Table(name = "notice") +public class NoticeArticle extends Article { + + private String title; + + private String content; + + NoticeArticle(final String title, final String content, final Long authorId, + final StudyRoom studyRoom) { + super(null, authorId, studyRoom); + this.title = title; + this.content = content; + } + + public NoticeArticle(final Long id, final String title, final String content, final Long authorId, + final StudyRoom studyRoom) { + super(id, authorId, studyRoom); + this.title = title; + this.content = content; + } + + @Override + public final boolean isViewableBy(final Accessor accessor) { + return isPermittedAccessor(accessor); + } + + @Override + public final boolean isEditableBy(final Accessor accessor) { + return isOwner(accessor); + } + + @Override + public final void update(final Accessor accessor, final String title, final String content) { + if (!isEditableBy(accessor)) { + throw new IllegalArgumentException(); + } + + this.title = title; + this.content = content; + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final NoticeArticle that = (NoticeArticle) o; + return Objects.equals(getId(), that.getId()) && Objects.equals(getTitle(), that.getTitle()) + && Objects.equals(getContent(), that.getContent()) && Objects.equals(getAuthorId(), + that.getAuthorId()) && Objects.equals(getStudyRoom(), that.getStudyRoom()); + } + + @Override + public int hashCode() { + return Objects.hash(getId(), getTitle(), getContent(), getAuthorId(), getStudyRoom()); + } +} diff --git a/backend/src/main/java/com/woowacourse/moamoa/studyroom/domain/PermittedParticipants.java b/backend/src/main/java/com/woowacourse/moamoa/studyroom/domain/PermittedParticipants.java new file mode 100644 index 000000000..1916a1127 --- /dev/null +++ b/backend/src/main/java/com/woowacourse/moamoa/studyroom/domain/PermittedParticipants.java @@ -0,0 +1,44 @@ +package com.woowacourse.moamoa.studyroom.domain; + +import com.woowacourse.moamoa.common.entity.ReadOnlyCollectionPersister; +import com.woowacourse.moamoa.studyroom.domain.Accessor; +import java.util.Set; +import javax.persistence.CollectionTable; +import javax.persistence.Column; +import javax.persistence.ElementCollection; +import javax.persistence.Embeddable; +import javax.persistence.FetchType; +import javax.persistence.JoinColumn; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.Persister; + +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Embeddable +public class PermittedParticipants { + + @Column(name = "owner_id", nullable = false, updatable = false, insertable = false) + private Long ownerId; + + @ElementCollection(fetch = FetchType.EAGER) + @CollectionTable( + name = "study_member", + joinColumns = @JoinColumn(name = "study_id", updatable = false, insertable = false) + ) + @Column(name = "member_id", updatable = false, insertable = false) + @Persister(impl = ReadOnlyCollectionPersister.class) + private Set participants; + + public PermittedParticipants(final Long ownerId, final Set participants) { + this.ownerId = ownerId; + this.participants = participants; + } + + boolean isOwner(final Accessor accessor) { + return ownerId.equals(accessor.getMemberId()); + } + + boolean isPermittedAccessor(final Accessor accessor) { + return isOwner(accessor) || participants.contains(accessor.getMemberId()); + } +} diff --git a/backend/src/main/java/com/woowacourse/moamoa/studyroom/domain/StudyRoom.java b/backend/src/main/java/com/woowacourse/moamoa/studyroom/domain/StudyRoom.java new file mode 100644 index 000000000..588428dd7 --- /dev/null +++ b/backend/src/main/java/com/woowacourse/moamoa/studyroom/domain/StudyRoom.java @@ -0,0 +1,68 @@ +package com.woowacourse.moamoa.studyroom.domain; + +import com.woowacourse.moamoa.member.service.exception.NotParticipatedMemberException; +import java.util.Objects; +import java.util.Set; +import javax.persistence.Column; +import javax.persistence.Embedded; +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.Table; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@Entity +@Table(name = "study") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class StudyRoom { + + @Id + @Column(name = "id", updatable = false, insertable = false) + private Long studyId; + + @Embedded + private PermittedParticipants permittedParticipants; + + public StudyRoom(Long studyId, Long ownerId, Set participants) { + this.studyId = studyId; + this.permittedParticipants = new PermittedParticipants(ownerId, participants); + } + + boolean isOwner(final Accessor accessor) { + return studyId.equals(accessor.getStudyId()) && permittedParticipants.isOwner(accessor); + } + + public boolean isPermittedAccessor(final Accessor accessor) { + return studyId.equals(accessor.getStudyId()) && permittedParticipants.isPermittedAccessor(accessor); + } + + public Article write(final Accessor accessor, final String title, final String content, final ArticleType type) { + if (type == ArticleType.COMMUNITY && isPermittedAccessor(accessor)) { + return new CommunityArticle(title, content, accessor.getMemberId(), this); + } + + if (type == ArticleType.NOTICE && isOwner(accessor)) { + return new NoticeArticle(title, content, accessor.getMemberId(), this); + } + + throw new NotParticipatedMemberException(); + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final StudyRoom that = (StudyRoom) o; + return Objects.equals(studyId, that.studyId); + } + + @Override + public int hashCode() { + return Objects.hash(studyId); + } + +} diff --git a/backend/src/main/java/com/woowacourse/moamoa/studyroom/domain/repository/article/ArticleRepository.java b/backend/src/main/java/com/woowacourse/moamoa/studyroom/domain/repository/article/ArticleRepository.java new file mode 100644 index 000000000..3ad14b61d --- /dev/null +++ b/backend/src/main/java/com/woowacourse/moamoa/studyroom/domain/repository/article/ArticleRepository.java @@ -0,0 +1,18 @@ +package com.woowacourse.moamoa.studyroom.domain.repository.article; + +import com.woowacourse.moamoa.studyroom.domain.Article; +import com.woowacourse.moamoa.studyroom.domain.ArticleType; +import java.util.Optional; + +public interface ArticleRepository { + + T save(T article); + + Optional findById(Long id); + + void deleteById(Long id); + + boolean existsById(Long id); + + boolean isSupportType(ArticleType articleType); +} diff --git a/backend/src/main/java/com/woowacourse/moamoa/studyroom/domain/repository/article/ArticleRepositoryFactory.java b/backend/src/main/java/com/woowacourse/moamoa/studyroom/domain/repository/article/ArticleRepositoryFactory.java new file mode 100644 index 000000000..a8f159507 --- /dev/null +++ b/backend/src/main/java/com/woowacourse/moamoa/studyroom/domain/repository/article/ArticleRepositoryFactory.java @@ -0,0 +1,24 @@ +package com.woowacourse.moamoa.studyroom.domain.repository.article; + +import com.woowacourse.moamoa.studyroom.domain.Article; +import com.woowacourse.moamoa.studyroom.domain.ArticleType; +import java.util.List; +import org.springframework.stereotype.Repository; + +@Repository +public class ArticleRepositoryFactory { + + private final List> repositories; + + public ArticleRepositoryFactory(List> repositories) { + this.repositories = repositories; + } + + @SuppressWarnings("unchecked") + public ArticleRepository
    getRepository(final ArticleType articleType) { + return (ArticleRepository
    ) repositories.stream() + .filter(repository -> repository.isSupportType(articleType)) + .findFirst() + .orElseThrow(); + } +} diff --git a/backend/src/main/java/com/woowacourse/moamoa/studyroom/domain/repository/article/CommunityArticleRepository.java b/backend/src/main/java/com/woowacourse/moamoa/studyroom/domain/repository/article/CommunityArticleRepository.java new file mode 100644 index 000000000..a5e789b74 --- /dev/null +++ b/backend/src/main/java/com/woowacourse/moamoa/studyroom/domain/repository/article/CommunityArticleRepository.java @@ -0,0 +1,13 @@ +package com.woowacourse.moamoa.studyroom.domain.repository.article; + +import com.woowacourse.moamoa.studyroom.domain.ArticleType; +import com.woowacourse.moamoa.studyroom.domain.CommunityArticle; +import org.springframework.data.jpa.repository.JpaRepository; + +interface CommunityArticleRepository extends JpaRepository, ArticleRepository { + + @Override + default boolean isSupportType(ArticleType articleType) { + return articleType.equals(ArticleType.COMMUNITY); + } +} diff --git a/backend/src/main/java/com/woowacourse/moamoa/studyroom/domain/repository/article/NoticeArticleRepository.java b/backend/src/main/java/com/woowacourse/moamoa/studyroom/domain/repository/article/NoticeArticleRepository.java new file mode 100644 index 000000000..a369ce1eb --- /dev/null +++ b/backend/src/main/java/com/woowacourse/moamoa/studyroom/domain/repository/article/NoticeArticleRepository.java @@ -0,0 +1,13 @@ +package com.woowacourse.moamoa.studyroom.domain.repository.article; + +import com.woowacourse.moamoa.studyroom.domain.ArticleType; +import com.woowacourse.moamoa.studyroom.domain.NoticeArticle; +import org.springframework.data.jpa.repository.JpaRepository; + +interface NoticeArticleRepository extends JpaRepository, ArticleRepository { + + @Override + default boolean isSupportType(ArticleType articleType) { + return articleType.equals(ArticleType.NOTICE); + } +} diff --git a/backend/src/main/java/com/woowacourse/moamoa/studyroom/domain/repository/studyroom/JpaStudyRoomRepository.java b/backend/src/main/java/com/woowacourse/moamoa/studyroom/domain/repository/studyroom/JpaStudyRoomRepository.java new file mode 100644 index 000000000..055093d81 --- /dev/null +++ b/backend/src/main/java/com/woowacourse/moamoa/studyroom/domain/repository/studyroom/JpaStudyRoomRepository.java @@ -0,0 +1,7 @@ +package com.woowacourse.moamoa.studyroom.domain.repository.studyroom; + +import com.woowacourse.moamoa.studyroom.domain.StudyRoom; +import org.springframework.data.jpa.repository.JpaRepository; + +interface JpaStudyRoomRepository extends JpaRepository, StudyRoomRepository { +} diff --git a/backend/src/main/java/com/woowacourse/moamoa/studyroom/domain/repository/studyroom/StudyRoomRepository.java b/backend/src/main/java/com/woowacourse/moamoa/studyroom/domain/repository/studyroom/StudyRoomRepository.java new file mode 100644 index 000000000..b9901962b --- /dev/null +++ b/backend/src/main/java/com/woowacourse/moamoa/studyroom/domain/repository/studyroom/StudyRoomRepository.java @@ -0,0 +1,9 @@ +package com.woowacourse.moamoa.studyroom.domain.repository.studyroom; + +import com.woowacourse.moamoa.studyroom.domain.StudyRoom; +import java.util.Optional; + +public interface StudyRoomRepository { + + Optional findByStudyId(Long studyId); +} diff --git a/backend/src/main/java/com/woowacourse/moamoa/studyroom/query/ArticleDao.java b/backend/src/main/java/com/woowacourse/moamoa/studyroom/query/ArticleDao.java new file mode 100644 index 000000000..665cbf4aa --- /dev/null +++ b/backend/src/main/java/com/woowacourse/moamoa/studyroom/query/ArticleDao.java @@ -0,0 +1,86 @@ +package com.woowacourse.moamoa.studyroom.query; + +import com.woowacourse.moamoa.studyroom.domain.ArticleType; +import com.woowacourse.moamoa.studyroom.query.data.ArticleData; +import com.woowacourse.moamoa.member.query.data.MemberData; +import java.time.LocalDate; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.stereotype.Repository; + +@Repository +public class ArticleDao { + + public static final RowMapper ROW_MAPPER = (rs, rn) -> { + final long id = rs.getLong("article_id"); + final String title = rs.getString("article_title"); + final String content = rs.getString("article_content"); + final LocalDate createdDate = rs.getObject("article_created_date", LocalDate.class); + final LocalDate lastModifiedDate = rs.getObject("article_last_modified_date", LocalDate.class); + + final long githubId = rs.getLong("member.github_id"); + final String username = rs.getString("member.username"); + final String imageUrl = rs.getString("member.image_url"); + final String profileUrl = rs.getString("member.profile_url"); + MemberData memberData = new MemberData(githubId, username, imageUrl, profileUrl); + + return new ArticleData(id, memberData, title, content, createdDate, lastModifiedDate); + }; + private final NamedParameterJdbcTemplate namedParameterJdbcTemplate; + + + public ArticleDao(final NamedParameterJdbcTemplate namedParameterJdbcTemplate) { + this.namedParameterJdbcTemplate = namedParameterJdbcTemplate; + } + + public Optional getById(final Long articleId, ArticleType type) { + final String sql = "SELECT {}.id as article_id, {}.title as article_title, {}.content as article_content, " + + "{}.created_date as article_created_date, {}.last_modified_date as article_last_modified_date, " + + "member.github_id, member.username, member.image_url, member.profile_url " + + "FROM {} JOIN member ON {}.author_id = member.id " + + "WHERE {}.id = :{}Id"; + + final Map params = Map.of(nameOf(type) + "Id", articleId); + return namedParameterJdbcTemplate.query(sql.replaceAll("\\{\\}", nameOf(type)), params, ROW_MAPPER).stream().findAny(); + } + + public Page getAllByStudyId(final Long studyId, final Pageable pageable, ArticleType type) { + final List content = getContent(studyId, pageable, type); + final int totalCount = getTotalCount(studyId, type); + return new PageImpl<>(content, pageable, totalCount); + } + + private List getContent(final Long studyId, final Pageable pageable, ArticleType type) { + final String sql = "SELECT {}.id as article_id, {}.title as article_title, {}.content as article_content, " + + "{}.created_date as article_created_date, {}.last_modified_date as article_last_modified_date, " + + "member.github_id, member.username, member.image_url, member.profile_url " + + "FROM {} JOIN member ON {}.author_id = member.id " + + "WHERE {}.study_id = :studyId " + + "ORDER BY created_date DESC, {}.id DESC " + + "LIMIT :size OFFSET :offset"; + + final Map params = Map.of( + "studyId", studyId, + "size", pageable.getPageSize(), + "offset", pageable.getOffset() + ); + + return namedParameterJdbcTemplate.query(sql.replaceAll("\\{\\}", nameOf(type)), params, ROW_MAPPER); + } + + private Integer getTotalCount(final Long studyId, ArticleType type) { + final String sql = "SELECT count({}.id) FROM {} WHERE {}.study_id = :studyId"; + final Map param = Map.of("studyId", studyId); + return namedParameterJdbcTemplate.queryForObject(sql.replaceAll("\\{\\}", nameOf(type)), param, (rs, rn) -> rs.getInt(1)); + } + + private String nameOf(final ArticleType type) { + return type.name().toLowerCase(); + } +} diff --git a/backend/src/main/java/com/woowacourse/moamoa/community/query/data/CommunityArticleData.java b/backend/src/main/java/com/woowacourse/moamoa/studyroom/query/data/ArticleData.java similarity index 62% rename from backend/src/main/java/com/woowacourse/moamoa/community/query/data/CommunityArticleData.java rename to backend/src/main/java/com/woowacourse/moamoa/studyroom/query/data/ArticleData.java index 8814fb211..a8e928bcb 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/community/query/data/CommunityArticleData.java +++ b/backend/src/main/java/com/woowacourse/moamoa/studyroom/query/data/ArticleData.java @@ -1,11 +1,11 @@ -package com.woowacourse.moamoa.community.query.data; +package com.woowacourse.moamoa.studyroom.query.data; import com.woowacourse.moamoa.member.query.data.MemberData; import java.time.LocalDate; import lombok.Getter; @Getter -public class CommunityArticleData { +public class ArticleData { private final Long id; private final MemberData memberData; @@ -14,9 +14,9 @@ public class CommunityArticleData { private final LocalDate createdDate; private final LocalDate lastModifiedDate; - public CommunityArticleData(final Long id, final MemberData memberData, final String title, final String content, - final LocalDate createdDate, - final LocalDate lastModifiedDate) { + public ArticleData(final Long id, final MemberData memberData, final String title, final String content, + final LocalDate createdDate, + final LocalDate lastModifiedDate) { this.id = id; this.memberData = memberData; this.title = title; diff --git a/backend/src/main/java/com/woowacourse/moamoa/studyroom/service/ArticleService.java b/backend/src/main/java/com/woowacourse/moamoa/studyroom/service/ArticleService.java new file mode 100644 index 000000000..66b86dbfa --- /dev/null +++ b/backend/src/main/java/com/woowacourse/moamoa/studyroom/service/ArticleService.java @@ -0,0 +1,118 @@ +package com.woowacourse.moamoa.studyroom.service; + +import com.woowacourse.moamoa.studyroom.domain.Accessor; +import com.woowacourse.moamoa.studyroom.domain.Article; +import com.woowacourse.moamoa.studyroom.domain.ArticleType; +import com.woowacourse.moamoa.studyroom.domain.StudyRoom; +import com.woowacourse.moamoa.studyroom.domain.repository.studyroom.StudyRoomRepository; +import com.woowacourse.moamoa.studyroom.domain.repository.article.ArticleRepository; +import com.woowacourse.moamoa.studyroom.domain.repository.article.ArticleRepositoryFactory; +import com.woowacourse.moamoa.studyroom.query.ArticleDao; +import com.woowacourse.moamoa.studyroom.query.data.ArticleData; +import com.woowacourse.moamoa.studyroom.service.exception.ArticleNotFoundException; +import com.woowacourse.moamoa.studyroom.service.exception.UneditableArticleException; +import com.woowacourse.moamoa.studyroom.service.exception.UnviewableArticleException; +import com.woowacourse.moamoa.studyroom.service.request.ArticleRequest; +import com.woowacourse.moamoa.studyroom.service.response.ArticleResponse; +import com.woowacourse.moamoa.studyroom.service.response.ArticleSummariesResponse; +import com.woowacourse.moamoa.studyroom.service.response.ArticleSummaryResponse; +import com.woowacourse.moamoa.study.service.exception.StudyNotFoundException; +import java.util.List; +import java.util.stream.Collectors; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional(readOnly = true) +public class ArticleService { + + private final StudyRoomRepository studyRoomRepository; + private final ArticleRepositoryFactory articleRepositoryFactory; + private final ArticleDao articleDao; + + @Autowired + public ArticleService(final StudyRoomRepository studyRoomRepository, + final ArticleRepositoryFactory articleRepositoryFactory, + final ArticleDao articleDao) { + this.studyRoomRepository = studyRoomRepository; + this.articleRepositoryFactory = articleRepositoryFactory; + this.articleDao = articleDao; + } + + @Transactional + public Article createArticle(final Long memberId, final Long studyId, + final ArticleRequest request, final ArticleType articleType) { + final StudyRoom studyRoom = studyRoomRepository.findByStudyId(studyId) + .orElseThrow(StudyNotFoundException::new); + final Accessor accessor = new Accessor(memberId, studyId); + final Article article = studyRoom.write(accessor, request.getTitle(), request.getContent(), articleType); + final ArticleRepository
    repository = articleRepositoryFactory.getRepository(articleType); + return repository.save(article); + } + + public ArticleResponse getArticle(final Long memberId, final Long studyId, final Long articleId, + final ArticleType type) { + final Article article = articleRepositoryFactory.getRepository(type) + .findById(articleId) + .orElseThrow(() -> new ArticleNotFoundException(articleId)); + + if (!article.isViewableBy(new Accessor(memberId, studyId))) { + throw new UnviewableArticleException(studyId, memberId); + } + + final ArticleData data = articleDao.getById(articleId, type) + .orElseThrow(() -> new ArticleNotFoundException(articleId)); + return new ArticleResponse(data); + } + + @Transactional + public void deleteArticle(final Long memberId, final Long studyId, final Long articleId, final ArticleType type) { + final Article article = articleRepositoryFactory.getRepository(type) + .findById(articleId) + .orElseThrow(() -> new ArticleNotFoundException(articleId)); + + if (!article.isEditableBy(new Accessor(memberId, studyId))) { + throw new UneditableArticleException(); + } + + articleRepositoryFactory.getRepository(type).deleteById(articleId); + } + + public ArticleSummariesResponse getArticles(final Long memberId, final Long studyId, final Pageable pageable, + final ArticleType type) { + final StudyRoom studyRoom = studyRoomRepository.findByStudyId(studyId) + .orElseThrow(StudyNotFoundException::new); + + if (!studyRoom.isPermittedAccessor(new Accessor(memberId, studyId))) { + throw new UnviewableArticleException(studyId, memberId); + } + + final Page page = articleDao.getAllByStudyId(studyId, pageable, type); + + final List articles = page.getContent().stream() + .map(ArticleSummaryResponse::new) + .collect(Collectors.toList()); + + return new ArticleSummariesResponse(articles, page.getNumber(), page.getTotalPages() - 1, + page.getTotalElements()); + } + + @Transactional + public void updateArticle(final Long memberId, final Long studyId, final Long articleId, + final ArticleRequest request, final ArticleType type) { + final Article article = articleRepositoryFactory.getRepository(type) + .findById(articleId) + .orElseThrow(() -> new ArticleNotFoundException(articleId)); + + final Accessor accessor = new Accessor(memberId, studyId); + + if (!article.isEditableBy(accessor)) { + throw new UneditableArticleException(); + } + + article.update(accessor, request.getTitle(), request.getContent()); + } +} diff --git a/backend/src/main/java/com/woowacourse/moamoa/community/service/exception/ArticleNotFoundException.java b/backend/src/main/java/com/woowacourse/moamoa/studyroom/service/exception/ArticleNotFoundException.java similarity index 82% rename from backend/src/main/java/com/woowacourse/moamoa/community/service/exception/ArticleNotFoundException.java rename to backend/src/main/java/com/woowacourse/moamoa/studyroom/service/exception/ArticleNotFoundException.java index 5977414b7..1b54a0e02 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/community/service/exception/ArticleNotFoundException.java +++ b/backend/src/main/java/com/woowacourse/moamoa/studyroom/service/exception/ArticleNotFoundException.java @@ -1,4 +1,4 @@ -package com.woowacourse.moamoa.community.service.exception; +package com.woowacourse.moamoa.studyroom.service.exception; import com.woowacourse.moamoa.common.exception.NotFoundException; diff --git a/backend/src/main/java/com/woowacourse/moamoa/community/service/exception/NotArticleAuthorException.java b/backend/src/main/java/com/woowacourse/moamoa/studyroom/service/exception/NotArticleAuthorException.java similarity index 84% rename from backend/src/main/java/com/woowacourse/moamoa/community/service/exception/NotArticleAuthorException.java rename to backend/src/main/java/com/woowacourse/moamoa/studyroom/service/exception/NotArticleAuthorException.java index c66e9115b..4cfd4553b 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/community/service/exception/NotArticleAuthorException.java +++ b/backend/src/main/java/com/woowacourse/moamoa/studyroom/service/exception/NotArticleAuthorException.java @@ -1,4 +1,4 @@ -package com.woowacourse.moamoa.community.service.exception; +package com.woowacourse.moamoa.studyroom.service.exception; import com.woowacourse.moamoa.common.exception.BadRequestException; diff --git a/backend/src/main/java/com/woowacourse/moamoa/community/service/exception/NotRelatedArticleException.java b/backend/src/main/java/com/woowacourse/moamoa/studyroom/service/exception/NotRelatedArticleException.java similarity index 84% rename from backend/src/main/java/com/woowacourse/moamoa/community/service/exception/NotRelatedArticleException.java rename to backend/src/main/java/com/woowacourse/moamoa/studyroom/service/exception/NotRelatedArticleException.java index f66eee223..3d9726e7b 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/community/service/exception/NotRelatedArticleException.java +++ b/backend/src/main/java/com/woowacourse/moamoa/studyroom/service/exception/NotRelatedArticleException.java @@ -1,4 +1,4 @@ -package com.woowacourse.moamoa.community.service.exception; +package com.woowacourse.moamoa.studyroom.service.exception; import com.woowacourse.moamoa.common.exception.BadRequestException; diff --git a/backend/src/main/java/com/woowacourse/moamoa/community/service/exception/UneditableArticleException.java b/backend/src/main/java/com/woowacourse/moamoa/studyroom/service/exception/UneditableArticleException.java similarity index 77% rename from backend/src/main/java/com/woowacourse/moamoa/community/service/exception/UneditableArticleException.java rename to backend/src/main/java/com/woowacourse/moamoa/studyroom/service/exception/UneditableArticleException.java index 31d2cc696..552290963 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/community/service/exception/UneditableArticleException.java +++ b/backend/src/main/java/com/woowacourse/moamoa/studyroom/service/exception/UneditableArticleException.java @@ -1,4 +1,4 @@ -package com.woowacourse.moamoa.community.service.exception; +package com.woowacourse.moamoa.studyroom.service.exception; import com.woowacourse.moamoa.common.exception.BadRequestException; diff --git a/backend/src/main/java/com/woowacourse/moamoa/community/service/exception/UnviewableArticleException.java b/backend/src/main/java/com/woowacourse/moamoa/studyroom/service/exception/UnviewableArticleException.java similarity index 85% rename from backend/src/main/java/com/woowacourse/moamoa/community/service/exception/UnviewableArticleException.java rename to backend/src/main/java/com/woowacourse/moamoa/studyroom/service/exception/UnviewableArticleException.java index 875e9155f..8c39a9ff6 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/community/service/exception/UnviewableArticleException.java +++ b/backend/src/main/java/com/woowacourse/moamoa/studyroom/service/exception/UnviewableArticleException.java @@ -1,4 +1,4 @@ -package com.woowacourse.moamoa.community.service.exception; +package com.woowacourse.moamoa.studyroom.service.exception; import com.woowacourse.moamoa.common.exception.BadRequestException; diff --git a/backend/src/main/java/com/woowacourse/moamoa/community/service/request/ArticleRequest.java b/backend/src/main/java/com/woowacourse/moamoa/studyroom/service/request/ArticleRequest.java similarity index 90% rename from backend/src/main/java/com/woowacourse/moamoa/community/service/request/ArticleRequest.java rename to backend/src/main/java/com/woowacourse/moamoa/studyroom/service/request/ArticleRequest.java index 97f7424f8..4c9e14518 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/community/service/request/ArticleRequest.java +++ b/backend/src/main/java/com/woowacourse/moamoa/studyroom/service/request/ArticleRequest.java @@ -1,4 +1,4 @@ -package com.woowacourse.moamoa.community.service.request; +package com.woowacourse.moamoa.studyroom.service.request; import javax.validation.constraints.NotBlank; import lombok.Getter; diff --git a/backend/src/main/java/com/woowacourse/moamoa/community/service/response/ArticleResponse.java b/backend/src/main/java/com/woowacourse/moamoa/studyroom/service/response/ArticleResponse.java similarity index 78% rename from backend/src/main/java/com/woowacourse/moamoa/community/service/response/ArticleResponse.java rename to backend/src/main/java/com/woowacourse/moamoa/studyroom/service/response/ArticleResponse.java index 409c7f593..e8f776ea8 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/community/service/response/ArticleResponse.java +++ b/backend/src/main/java/com/woowacourse/moamoa/studyroom/service/response/ArticleResponse.java @@ -1,6 +1,6 @@ -package com.woowacourse.moamoa.community.service.response; +package com.woowacourse.moamoa.studyroom.service.response; -import com.woowacourse.moamoa.community.query.data.CommunityArticleData; +import com.woowacourse.moamoa.studyroom.query.data.ArticleData; import java.time.LocalDate; import lombok.AllArgsConstructor; import lombok.Builder; @@ -24,7 +24,7 @@ public class ArticleResponse { private LocalDate createdDate; private LocalDate lastModifiedDate; - public ArticleResponse(CommunityArticleData data) { + public ArticleResponse(ArticleData data) { this(data.getId(), new AuthorResponse(data.getMemberData()), data.getTitle(), data.getContent(), data.getCreatedDate(), data.getLastModifiedDate()); } diff --git a/backend/src/main/java/com/woowacourse/moamoa/community/service/response/ArticleSummariesResponse.java b/backend/src/main/java/com/woowacourse/moamoa/studyroom/service/response/ArticleSummariesResponse.java similarity index 92% rename from backend/src/main/java/com/woowacourse/moamoa/community/service/response/ArticleSummariesResponse.java rename to backend/src/main/java/com/woowacourse/moamoa/studyroom/service/response/ArticleSummariesResponse.java index df05e4b65..f33df6f9d 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/community/service/response/ArticleSummariesResponse.java +++ b/backend/src/main/java/com/woowacourse/moamoa/studyroom/service/response/ArticleSummariesResponse.java @@ -1,4 +1,4 @@ -package com.woowacourse.moamoa.community.service.response; +package com.woowacourse.moamoa.studyroom.service.response; import java.util.List; import lombok.EqualsAndHashCode; diff --git a/backend/src/main/java/com/woowacourse/moamoa/community/service/response/ArticleSummaryResponse.java b/backend/src/main/java/com/woowacourse/moamoa/studyroom/service/response/ArticleSummaryResponse.java similarity index 84% rename from backend/src/main/java/com/woowacourse/moamoa/community/service/response/ArticleSummaryResponse.java rename to backend/src/main/java/com/woowacourse/moamoa/studyroom/service/response/ArticleSummaryResponse.java index 1f5bc7543..5e37beef5 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/community/service/response/ArticleSummaryResponse.java +++ b/backend/src/main/java/com/woowacourse/moamoa/studyroom/service/response/ArticleSummaryResponse.java @@ -1,6 +1,6 @@ -package com.woowacourse.moamoa.community.service.response; +package com.woowacourse.moamoa.studyroom.service.response; -import com.woowacourse.moamoa.community.query.data.CommunityArticleData; +import com.woowacourse.moamoa.studyroom.query.data.ArticleData; import java.time.LocalDate; import lombok.EqualsAndHashCode; import lombok.Getter; @@ -19,7 +19,7 @@ public class ArticleSummaryResponse { private LocalDate createdDate; private LocalDate lastModifiedDate; - public ArticleSummaryResponse(CommunityArticleData data) { + public ArticleSummaryResponse(ArticleData data) { this.id = data.getId(); this.author = new AuthorResponse(data.getMemberData()); this.title = data.getTitle(); diff --git a/backend/src/main/java/com/woowacourse/moamoa/community/service/response/AuthorResponse.java b/backend/src/main/java/com/woowacourse/moamoa/studyroom/service/response/AuthorResponse.java similarity index 91% rename from backend/src/main/java/com/woowacourse/moamoa/community/service/response/AuthorResponse.java rename to backend/src/main/java/com/woowacourse/moamoa/studyroom/service/response/AuthorResponse.java index e83a5149a..7da9d9429 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/community/service/response/AuthorResponse.java +++ b/backend/src/main/java/com/woowacourse/moamoa/studyroom/service/response/AuthorResponse.java @@ -1,4 +1,4 @@ -package com.woowacourse.moamoa.community.service.response; +package com.woowacourse.moamoa.studyroom.service.response; import com.woowacourse.moamoa.member.query.data.MemberData; import lombok.AllArgsConstructor; diff --git a/backend/src/test/java/com/woowacourse/acceptance/AcceptanceTest.java b/backend/src/test/java/com/woowacourse/acceptance/AcceptanceTest.java index 5bce1bd77..e9800e538 100644 --- a/backend/src/test/java/com/woowacourse/acceptance/AcceptanceTest.java +++ b/backend/src/test/java/com/woowacourse/acceptance/AcceptanceTest.java @@ -96,7 +96,8 @@ void mockingGithubServer() { @AfterEach void tearDown() { jdbcTemplate.update("SET REFERENTIAL_INTEGRITY FALSE"); - jdbcTemplate.update("TRUNCATE TABLE article"); + jdbcTemplate.update("TRUNCATE TABLE notice"); + jdbcTemplate.update("TRUNCATE TABLE community"); jdbcTemplate.update("TRUNCATE TABLE member"); jdbcTemplate.update("TRUNCATE TABLE study_tag"); jdbcTemplate.update("TRUNCATE TABLE study_member"); diff --git a/backend/src/test/java/com/woowacourse/acceptance/steps/StudyRelatedSteps.java b/backend/src/test/java/com/woowacourse/acceptance/steps/StudyRelatedSteps.java index 68213adc3..280f86f42 100644 --- a/backend/src/test/java/com/woowacourse/acceptance/steps/StudyRelatedSteps.java +++ b/backend/src/test/java/com/woowacourse/acceptance/steps/StudyRelatedSteps.java @@ -4,9 +4,9 @@ import static org.springframework.http.HttpHeaders.CONTENT_TYPE; import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; -import com.woowacourse.moamoa.community.service.request.ArticleRequest; import com.woowacourse.moamoa.referenceroom.service.request.CreatingLinkRequest; import com.woowacourse.moamoa.review.service.request.WriteReviewRequest; +import com.woowacourse.moamoa.studyroom.service.request.ArticleRequest; import io.restassured.RestAssured; import org.junit.jupiter.api.Assertions; import org.springframework.http.HttpHeaders; @@ -51,7 +51,6 @@ public class StudyRelatedSteps extends Steps { return null; } } - public Long 링크를_공유한다(final CreatingLinkRequest request) { try { final String location = RestAssured.given().log().all() @@ -70,6 +69,25 @@ public class StudyRelatedSteps extends Steps { } } + public Long 공지사항을_작성한다(final String title, final String content) { + try { + final String location = RestAssured.given().log().all() + .header(org.apache.http.HttpHeaders.AUTHORIZATION, token) + .header(org.apache.http.HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) + .body(objectMapper.writeValueAsString(new ArticleRequest(title, content))) + .pathParam("study-id", studyId) + .when().log().all() + .post("/api/studies/{study-id}/notice/articles") + .then().log().all() + .statusCode(HttpStatus.CREATED.value()) + .extract().header(HttpHeaders.LOCATION); + return Long.parseLong(location.replaceAll("/api/studies/" + studyId + "/notice/articles/", "")); + } catch (Exception e) { + Assertions.fail("공지사항 작성 실패"); + return null; + } + } + public Long 게시글을_작성한다(final String title, final String content) { try { final String location = RestAssured.given().log().all() diff --git a/backend/src/test/java/com/woowacourse/acceptance/test/community/CommunityAcceptanceTest.java b/backend/src/test/java/com/woowacourse/acceptance/test/studyroom/CommunityAcceptanceTest.java similarity index 98% rename from backend/src/test/java/com/woowacourse/acceptance/test/community/CommunityAcceptanceTest.java rename to backend/src/test/java/com/woowacourse/acceptance/test/studyroom/CommunityAcceptanceTest.java index c1104bd40..978cc6baf 100644 --- a/backend/src/test/java/com/woowacourse/acceptance/test/community/CommunityAcceptanceTest.java +++ b/backend/src/test/java/com/woowacourse/acceptance/test/studyroom/CommunityAcceptanceTest.java @@ -1,4 +1,4 @@ -package com.woowacourse.acceptance.test.community; +package com.woowacourse.acceptance.test.studyroom; import static com.woowacourse.acceptance.fixture.MemberFixtures.그린론_깃허브_ID; import static com.woowacourse.acceptance.fixture.MemberFixtures.그린론_이름; @@ -24,11 +24,11 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.woowacourse.acceptance.AcceptanceTest; -import com.woowacourse.moamoa.community.service.request.ArticleRequest; -import com.woowacourse.moamoa.community.service.response.ArticleResponse; -import com.woowacourse.moamoa.community.service.response.ArticleSummariesResponse; -import com.woowacourse.moamoa.community.service.response.ArticleSummaryResponse; -import com.woowacourse.moamoa.community.service.response.AuthorResponse; +import com.woowacourse.moamoa.studyroom.service.request.ArticleRequest; +import com.woowacourse.moamoa.studyroom.service.response.ArticleResponse; +import com.woowacourse.moamoa.studyroom.service.response.ArticleSummariesResponse; +import com.woowacourse.moamoa.studyroom.service.response.ArticleSummaryResponse; +import com.woowacourse.moamoa.studyroom.service.response.AuthorResponse; import io.restassured.RestAssured; import java.time.LocalDate; import java.util.List; diff --git a/backend/src/test/java/com/woowacourse/acceptance/test/studyroom/NoticeAcceptanceTest.java b/backend/src/test/java/com/woowacourse/acceptance/test/studyroom/NoticeAcceptanceTest.java new file mode 100644 index 000000000..2e5ae63e1 --- /dev/null +++ b/backend/src/test/java/com/woowacourse/acceptance/test/studyroom/NoticeAcceptanceTest.java @@ -0,0 +1,327 @@ +package com.woowacourse.acceptance.test.studyroom; + +import static com.woowacourse.acceptance.fixture.MemberFixtures.그린론_깃허브_ID; +import static com.woowacourse.acceptance.fixture.MemberFixtures.그린론_이름; +import static com.woowacourse.acceptance.fixture.MemberFixtures.그린론_이미지_URL; +import static com.woowacourse.acceptance.fixture.MemberFixtures.그린론_프로필_URL; +import static com.woowacourse.acceptance.steps.LoginSteps.그린론이; +import static com.woowacourse.acceptance.steps.LoginSteps.베루스가; +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; +import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; +import static org.springframework.restdocs.headers.HeaderDocumentation.responseHeaders; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; +import static org.springframework.restdocs.request.RequestDocumentation.requestParameters; +import static org.springframework.restdocs.restassured3.RestAssuredRestDocumentation.document; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.woowacourse.acceptance.AcceptanceTest; +import com.woowacourse.moamoa.studyroom.service.request.ArticleRequest; +import com.woowacourse.moamoa.studyroom.service.response.ArticleResponse; +import com.woowacourse.moamoa.studyroom.service.response.ArticleSummariesResponse; +import com.woowacourse.moamoa.studyroom.service.response.ArticleSummaryResponse; +import com.woowacourse.moamoa.studyroom.service.response.AuthorResponse; +import io.restassured.RestAssured; +import java.time.LocalDate; +import java.util.List; +import org.apache.http.HttpHeaders; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.restdocs.payload.JsonFieldType; + +public class NoticeAcceptanceTest extends AcceptanceTest { + + @DisplayName("스터디에 공지사항을 작성한다.") + @Test + void writeNotice() throws Exception { + // arrange + long 스터디_ID = 그린론이().로그인하고().자바_스터디를().시작일자는(LocalDate.now()).생성한다(); + String 토큰 = 그린론이().로그인한다(); + ArticleRequest request = new ArticleRequest("공지사항 제목", "공지사항 내용"); + + // act + final String location = RestAssured.given(spec).log().all() + .header(HttpHeaders.AUTHORIZATION, 토큰) + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) + .body(objectMapper.writeValueAsString(request)) + .pathParam("study-id", 스터디_ID) + .when().log().all() + .filter(document("write/notice", + requestHeaders( + headerWithName(HttpHeaders.AUTHORIZATION).description("JWT 토큰") + ), + pathParameters( + parameterWithName("study-id").description("스터디 식별 ID") + ), + requestFields( + fieldWithPath("title").type(JsonFieldType.STRING).description("공지사항 제목"), + fieldWithPath("content").type(JsonFieldType.STRING).description("공지사항 내용") + ), + responseHeaders( + headerWithName(HttpHeaders.LOCATION).description("생성된 공지사항 url"), + headerWithName("Access-Control-Allow-Headers").description("접근 가능한 헤더") + )) + ) + .post("/api/studies/{study-id}/notice/articles") + .then().log().all() + .statusCode(HttpStatus.CREATED.value()) + .extract() + .header(HttpHeaders.LOCATION); + + // assert + Long articleId = Long.valueOf(location.split("/")[6]); + + final ArticleResponse actualResponse = RestAssured + .given(spec).log().all() + .header(HttpHeaders.AUTHORIZATION, 토큰) + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) + .pathParam("study-id", 스터디_ID) + .pathParam("article-id", articleId) + .filter(document("get/notice", + requestHeaders( + headerWithName(HttpHeaders.AUTHORIZATION).description("JWT 토큰") + ), + pathParameters( + parameterWithName("study-id").description("스터디 식별 ID"), + parameterWithName("article-id").description("공지사항 식별 ID") + ), + responseFields( + fieldWithPath("id").type(JsonFieldType.NUMBER).description("공지사항 식별 ID"), + fieldWithPath("author").type(JsonFieldType.OBJECT).description("작성자"), + fieldWithPath("author.id").type(JsonFieldType.NUMBER).description("작성자 github ID"), + fieldWithPath("author.username").type(JsonFieldType.STRING) + .description("작성자 github 사용자 이름"), + fieldWithPath("author.imageUrl").type(JsonFieldType.STRING) + .description("작성자 github 이미지 URL"), + fieldWithPath("author.profileUrl").type(JsonFieldType.STRING) + .description("작성자 github 프로필 URL"), + fieldWithPath("title").type(JsonFieldType.STRING).description("공지사항 제목"), + fieldWithPath("content").type(JsonFieldType.STRING).description("공지사항 내용"), + fieldWithPath("createdDate").type(JsonFieldType.STRING).description("공지사항 작성일"), + fieldWithPath("lastModifiedDate").type(JsonFieldType.STRING).description("공지사항 수정일") + ) + )) + .when().log().all() + .get("/api/studies/{study-id}/notice/articles/{article-id}") + .then().log().all() + .statusCode(HttpStatus.OK.value()) + .extract().as(ArticleResponse.class); + + final ArticleResponse expectedResponse = ArticleResponse.builder() + .id(articleId) + .author(new AuthorResponse(그린론_깃허브_ID, 그린론_이름, 그린론_이미지_URL, 그린론_프로필_URL)) + .title("공지사항 제목") + .content("공지사항 내용") + .createdDate(LocalDate.now()) + .lastModifiedDate(LocalDate.now()) + .build(); + + assertThat(actualResponse).isEqualTo(expectedResponse); + } + + @DisplayName("스터디 공지사항 게시글을 삭제한다.") + @Test + void deleteCommunityArticle() { + // arrange + long 스터디_ID = 그린론이().로그인하고().자바_스터디를().시작일자는(LocalDate.now()).생성한다(); + long 공지사항_ID = 그린론이().로그인하고().스터디에(스터디_ID).공지사항을_작성한다("게시글 제목", "게시글 내용"); + String 토큰 = 그린론이().로그인한다(); + + // act + RestAssured.given(spec).log().all() + .header(HttpHeaders.AUTHORIZATION, 토큰) + .pathParam("study-id", 스터디_ID) + .pathParam("article-id", 공지사항_ID) + .filter(document("delete/notice", + requestHeaders( + headerWithName(HttpHeaders.AUTHORIZATION).description("JWT 토큰") + ), + pathParameters( + parameterWithName("study-id").description("스터디 식별 번호"), + parameterWithName("article-id").description("게시글 식별 번호") + ) + )) + .when().log().all() + .delete("/api/studies/{study-id}/notice/articles/{article-id}") + .then().log().all() + .statusCode(HttpStatus.NO_CONTENT.value()); + + // assert + RestAssured + .given(spec).log().all() + .header(HttpHeaders.AUTHORIZATION, 토큰) + .pathParam("study-id", 스터디_ID) + .pathParam("article-id", 공지사항_ID) + .when().log().all() + .get("/api/studies/{study-id}/notice/articles/{article-id}") + .then().log().all() + .statusCode(HttpStatus.NOT_FOUND.value()); + } + + @DisplayName("스터디 공지사항 전체 게시글을 조회한다.") + @Test + void getStudyCommunityArticles() { + // arrange + long 자바_스터디_ID = 그린론이().로그인하고().자바_스터디를().시작일자는(LocalDate.now()).생성한다(); + 그린론이().로그인하고().스터디에(자바_스터디_ID).공지사항을_작성한다("자바 게시글 제목1", "자바 게시글 내용1"); + long 자바_공지글2_ID = 그린론이().로그인하고().스터디에(자바_스터디_ID).공지사항을_작성한다("자바 게시글 제목2", "자바 게시글 내용2"); + long 자바_공지글3_ID = 그린론이().로그인하고().스터디에(자바_스터디_ID).공지사항을_작성한다("자바 게시글 제목3", "자바 게시글 내용3"); + long 자바_공지글4_ID = 그린론이().로그인하고().스터디에(자바_스터디_ID).공지사항을_작성한다("자바 게시글 제목4", "자바 게시글 내용4"); + + long 리액트_스터디_ID = 베루스가().로그인하고().리액트_스터디를().시작일자는(LocalDate.now()).생성한다(); + 베루스가().로그인하고().스터디에(리액트_스터디_ID).공지사항을_작성한다("리액트 게시글 제목", "리액트 게시글 내용"); + + String 토큰 = 그린론이().로그인한다(); + + // act + final ArticleSummariesResponse response = RestAssured.given(spec).log().all() + .header(HttpHeaders.AUTHORIZATION, 토큰) + .pathParam("study-id", 자바_스터디_ID) + .queryParam("page", 0) + .queryParam("size", 3) + .filter(document("get/notices", + requestHeaders( + headerWithName(HttpHeaders.AUTHORIZATION).description("Jwt 토큰") + ), + pathParameters( + parameterWithName("study-id").description("스터디 ID") + ), + requestParameters( + parameterWithName("page").description("페이지"), + parameterWithName("size").description("사이즈") + ), + responseFields( + fieldWithPath("articles").type(JsonFieldType.ARRAY).description("게시물 목록"), + fieldWithPath("articles[].id").type(JsonFieldType.NUMBER).description("게시글 식별 ID"), + fieldWithPath("articles[].author").type(JsonFieldType.OBJECT).description("작성자"), + fieldWithPath("articles[].author.id").type(JsonFieldType.NUMBER) + .description("작성자 github ID"), + fieldWithPath("articles[].author.username").type(JsonFieldType.STRING) + .description("작성자 github 사용자 이름"), + fieldWithPath("articles[].author.imageUrl").type(JsonFieldType.STRING) + .description("작성자 github 이미지 URL"), + fieldWithPath("articles[].author.profileUrl").type(JsonFieldType.STRING) + .description("작성자 github 프로필 URL"), + fieldWithPath("articles[].title").type(JsonFieldType.STRING).description("게시글 제목"), + fieldWithPath("articles[].createdDate").type(JsonFieldType.STRING) + .description("게시글 작성일"), + fieldWithPath("articles[].lastModifiedDate").type(JsonFieldType.STRING) + .description("게시글 수정일"), + fieldWithPath("currentPage").type(JsonFieldType.NUMBER).description("현재 페이지 번호"), + fieldWithPath("lastPage").type(JsonFieldType.NUMBER).description("마지막 페이지 번호"), + fieldWithPath("totalCount").type(JsonFieldType.NUMBER).description("게시글 전체 갯수") + ) + )) + .when().log().all() + .get("/api/studies/{study-id}/notice/articles") + .then().log().all() + .statusCode(HttpStatus.OK.value()) + .extract().as(ArticleSummariesResponse.class); + + // assert + AuthorResponse 그린론 = new AuthorResponse(그린론_깃허브_ID, 그린론_이름, 그린론_이미지_URL, 그린론_프로필_URL); + + List articles = List.of( + new ArticleSummaryResponse(자바_공지글4_ID, 그린론, "자바 게시글 제목4", LocalDate.now(), LocalDate.now()), + new ArticleSummaryResponse(자바_공지글3_ID, 그린론, "자바 게시글 제목3", LocalDate.now(), LocalDate.now()), + new ArticleSummaryResponse(자바_공지글2_ID, 그린론, "자바 게시글 제목2", LocalDate.now(), LocalDate.now()) + ); + + assertThat(response).isEqualTo(new ArticleSummariesResponse(articles, 0, 1, 4)); + } + + @DisplayName("스터디 커뮤니티 전체 공지사항을 기본 페이징 정보로 조회한다.") + @Test + void getStudyCommunityArticlesByDefaultPageable() { + // arrange + long 스터디_ID = 그린론이().로그인하고().자바_스터디를().시작일자는(LocalDate.now()).생성한다(); + long 공지글1_ID = 그린론이().로그인하고().스터디에(스터디_ID).공지사항을_작성한다("자바 게시글 제목1", "자바 게시글 내용1"); + long 공지글2_ID = 그린론이().로그인하고().스터디에(스터디_ID).공지사항을_작성한다("자바 게시글 제목2", "자바 게시글 내용2"); + long 공지글3_ID = 그린론이().로그인하고().스터디에(스터디_ID).공지사항을_작성한다("자바 게시글 제목3", "자바 게시글 내용3"); + long 공지글4_ID = 그린론이().로그인하고().스터디에(스터디_ID).공지사항을_작성한다("자바 게시글 제목4", "자바 게시글 내용4"); + + String 토큰 = 그린론이().로그인한다(); + + // act + final ArticleSummariesResponse response = RestAssured.given(spec).log().all() + .header(HttpHeaders.AUTHORIZATION, 토큰) + .pathParam("study-id", 스터디_ID) + .when().log().all() + .get("/api/studies/{study-id}/notice/articles") + .then().log().all() + .statusCode(HttpStatus.OK.value()) + .extract().as(ArticleSummariesResponse.class); + + // assert + AuthorResponse 그린론 = new AuthorResponse(그린론_깃허브_ID, 그린론_이름, 그린론_이미지_URL, 그린론_프로필_URL); + + List articles = List.of( + new ArticleSummaryResponse(공지글4_ID, 그린론, "자바 게시글 제목4", LocalDate.now(), LocalDate.now()), + new ArticleSummaryResponse(공지글3_ID, 그린론, "자바 게시글 제목3", LocalDate.now(), LocalDate.now()), + new ArticleSummaryResponse(공지글2_ID, 그린론, "자바 게시글 제목2", LocalDate.now(), LocalDate.now()), + new ArticleSummaryResponse(공지글1_ID, 그린론, "자바 게시글 제목1", LocalDate.now(), LocalDate.now()) + ); + + assertThat(response).isEqualTo(new ArticleSummariesResponse(articles, 0, 0, 4)); + } + + @DisplayName("커뮤니티 글을 수정한다.") + @Test + void updateArticleToCommunity() throws JsonProcessingException { + // arrange + long 스터디_ID = 그린론이().로그인하고().자바_스터디를().시작일자는(LocalDate.now()).생성한다(); + long 공지글_ID = 그린론이().로그인하고().스터디에(스터디_ID).공지사항을_작성한다("게시글 제목", "게시글 내용"); + String 토큰 = 그린론이().로그인한다(); + + final ArticleRequest request = new ArticleRequest("게시글 제목 수정", "게시글 내용 수정"); + + // act + RestAssured.given(spec).log().all() + .header(HttpHeaders.AUTHORIZATION, 토큰) + .pathParam("study-id", 스터디_ID) + .pathParam("article-id", 공지글_ID) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .body(objectMapper.writeValueAsString(request)) + .filter(document("update/notice", + requestHeaders( + headerWithName(HttpHeaders.AUTHORIZATION).description("JWT 토큰") + ), + pathParameters( + parameterWithName("study-id").description("스터디 식별 번호"), + parameterWithName("article-id").description("게시글 식별 번호") + ), + requestFields( + fieldWithPath("title").type(JsonFieldType.STRING).description("게시글 수정 제목"), + fieldWithPath("content").type(JsonFieldType.STRING).description("게시글 내용 수정") + ) + )) + .when().log().all() + .put("/api/studies/{study-id}/notice/articles/{article-id}") + .then().log().all() + .statusCode(HttpStatus.NO_CONTENT.value()); + + // assert + final ArticleResponse response = RestAssured + .given().log().all() + .header(HttpHeaders.AUTHORIZATION, 토큰) + .pathParam("study-id", 스터디_ID) + .pathParam("article-id", 공지글_ID) + .when().log().all() + .get("/api/studies/{study-id}/notice/articles/{article-id}") + .then().log().all() + .statusCode(HttpStatus.OK.value()) + .extract() + .as(ArticleResponse.class); + + final AuthorResponse authorResponse = new AuthorResponse(그린론_깃허브_ID, 그린론_이름, 그린론_이미지_URL, 그린론_프로필_URL); + + assertThat(response).isEqualTo(new ArticleResponse(스터디_ID, authorResponse, "게시글 제목 수정", + "게시글 내용 수정", LocalDate.now(), LocalDate.now())); + } +} diff --git a/backend/src/test/java/com/woowacourse/moamoa/community/controller/CommunityArticleControllerTest.java b/backend/src/test/java/com/woowacourse/moamoa/community/controller/CommunityArticleControllerTest.java deleted file mode 100644 index 0692ed6c4..000000000 --- a/backend/src/test/java/com/woowacourse/moamoa/community/controller/CommunityArticleControllerTest.java +++ /dev/null @@ -1,103 +0,0 @@ -package com.woowacourse.moamoa.community.controller; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -import com.woowacourse.moamoa.common.RepositoryTest; -import com.woowacourse.moamoa.common.utils.DateTimeSystem; -import com.woowacourse.moamoa.community.domain.CommunityArticle; -import com.woowacourse.moamoa.community.domain.repository.CommunityArticleRepository; -import com.woowacourse.moamoa.community.query.CommunityArticleDao; -import com.woowacourse.moamoa.community.service.CommunityArticleService; -import com.woowacourse.moamoa.community.service.request.ArticleRequest; -import com.woowacourse.moamoa.member.domain.Member; -import com.woowacourse.moamoa.member.domain.repository.MemberRepository; -import com.woowacourse.moamoa.member.service.exception.MemberNotFoundException; -import com.woowacourse.moamoa.study.domain.Study; -import com.woowacourse.moamoa.study.domain.repository.StudyRepository; -import com.woowacourse.moamoa.study.service.StudyService; -import com.woowacourse.moamoa.study.service.exception.StudyNotFoundException; -import com.woowacourse.moamoa.study.service.request.StudyRequestBuilder; -import java.time.LocalDate; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; - -@RepositoryTest -public class CommunityArticleControllerTest { - - StudyRequestBuilder javaStudyRequest = new StudyRequestBuilder() - .title("java 스터디").excerpt("자바 설명").thumbnail("java image").description("자바 소개"); - - @Autowired - private StudyRepository studyRepository; - - @Autowired - private MemberRepository memberRepository; - - @Autowired - private CommunityArticleRepository communityArticleRepository; - - @Autowired - private CommunityArticleDao communityArticleDao; - - private StudyService studyService; - private CommunityArticleController sut; - - @BeforeEach - void setUp() { - studyService = new StudyService(studyRepository, memberRepository, new DateTimeSystem()); - sut = new CommunityArticleController(new CommunityArticleService(memberRepository, studyRepository, - communityArticleRepository, communityArticleDao)); - } - - @DisplayName("커뮤니티 게시글을 작성한다.") - @Test - void createCommunityArticle() { - // arrange - Member member = memberRepository.save(new Member(1L, "username", "imageUrl", "profileUrl")); - Study study = studyService - .createStudy(member.getGithubId(), javaStudyRequest.startDate(LocalDate.now()).build()); - - ArticleRequest request = new ArticleRequest("게시글 제목", "게시글 내용"); - - // act - ResponseEntity response = sut.createArticle(member.getId(), study.getId(), request); - - // assert - String location = response.getHeaders().getLocation().getPath(); - Long articleId = Long.valueOf(location.replaceAll("/api/studies/\\d+/community/articles/", "")); - - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED); - assertThat(location).matches("/api/studies/\\d+/community/articles/\\d+"); - assertThat(communityArticleRepository.findById(articleId).get()) - .isEqualTo(new CommunityArticle(articleId, "게시글 제목", "게시글 내용", member.getId(), study)); - } - - @DisplayName("사용자가 없는 경우 게시글 작성 시 예외가 발생한다.") - @Test - void throwExceptionWhenCreateByNotFoundMember() { - // arrange - Member member = memberRepository.save(new Member(1L, "username", "imageUrl", "profileUrl")); - Study study = studyService - .createStudy(member.getGithubId(), javaStudyRequest.startDate(LocalDate.now()).build()); - - // act & assert - assertThatThrownBy(() -> sut.createArticle(member.getId() + 1, study.getId(), new ArticleRequest("제목", "내용"))) - .isInstanceOf(MemberNotFoundException.class); - } - - @DisplayName("스터디가 없는 경우 게시글 작성 시 예외가 발생한다.") - @Test - void throwExceptionWhenWriteToNotFoundStudy() { - // arrange - Member member = memberRepository.save(new Member(1L, "username", "imageUrl", "profileUrl")); - - // act & assert - assertThatThrownBy(() -> sut.createArticle(member.getId(), 1L, new ArticleRequest("제목", "내용"))) - .isInstanceOf(StudyNotFoundException.class); - } -} diff --git a/backend/src/test/java/com/woowacourse/moamoa/community/domain/CommunityArticleTest.java b/backend/src/test/java/com/woowacourse/moamoa/community/domain/CommunityArticleTest.java deleted file mode 100644 index cab877996..000000000 --- a/backend/src/test/java/com/woowacourse/moamoa/community/domain/CommunityArticleTest.java +++ /dev/null @@ -1,102 +0,0 @@ -package com.woowacourse.moamoa.community.domain; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -import com.woowacourse.moamoa.community.service.request.ArticleRequest; -import com.woowacourse.moamoa.member.domain.Member; -import com.woowacourse.moamoa.member.service.exception.NotParticipatedMemberException; -import com.woowacourse.moamoa.study.domain.AttachedTags; -import com.woowacourse.moamoa.study.domain.Content; -import com.woowacourse.moamoa.study.domain.Participants; -import com.woowacourse.moamoa.study.domain.RecruitPlanner; -import com.woowacourse.moamoa.study.domain.RecruitStatus; -import com.woowacourse.moamoa.study.domain.Study; -import com.woowacourse.moamoa.study.domain.StudyPlanner; -import com.woowacourse.moamoa.study.domain.StudyStatus; - -import java.time.LocalDate; -import java.time.LocalDateTime; -import java.util.List; - -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.CsvSource; - -class CommunityArticleTest { - - @DisplayName("스터디에 참여한 참가자만 게시글을 작성할 수 있다.") - @Test - void writeCommunityArticleByParticipant() { - final Member owner = createMember(1L); - final Member another = createMember(2L); - final Study study = createStudy(1L, owner); - - assertThatThrownBy(() -> CommunityArticle.write(another, study, new ArticleRequest("제목", "내용"))) - .isInstanceOf(NotParticipatedMemberException.class); - } - - @DisplayName("스터디 참여자는 게시글을 조회할 수 있다.") - @ParameterizedTest - @CsvSource({"1,true", "2,true", "3,false"}) - void getArticle(Long viewerId, boolean expected) { - final Member owner = createMember(1L); - final Member participant = createMember(2L); - final Study study = createStudy(1L, owner); - study.participate(participant.getId()); - - final CommunityArticle communityArticle = CommunityArticle.write(owner, study, new ArticleRequest("제목", "내용")); - - assertThat(communityArticle.isViewableBy(study.getId(), viewerId)).isEqualTo(expected); - } - - @DisplayName("스터디에 속해 있는 게시글이 맞을 경우 조회할 수 있다.") - @ParameterizedTest - @CsvSource({"1,1,true", "1,2,false"}) - void deleteArticle(Long studyId, Long wantToViewStudyId, boolean expected) { - final Member member = createMember(1L); - final Study study = createStudy(studyId, member); - final CommunityArticle communityArticle = CommunityArticle.write(member, study, new ArticleRequest("제목", "내용")); - - assertThat(communityArticle.isViewableBy(wantToViewStudyId, member.getId())).isEqualTo(expected); - } - - @DisplayName("스터디에 참여했고, 작성자인 경우 스터디 게시글을 수정,삭제할 수 있다.") - @ParameterizedTest - @CsvSource({"1,true", "2,false", "3,false"}) - void updateArticle(Long editorId, boolean expected) { - final Member owner = createMember(1L); - final Member participant = createMember(2L); - final Study study = createStudy(1L, owner); - study.participate(participant.getId()); - final CommunityArticle communityArticle = CommunityArticle.write(owner, study, new ArticleRequest("제목", "내용")); - - assertThat(communityArticle.isEditableBy(study.getId(), editorId)).isEqualTo(expected); - } - - @DisplayName("스터디에 속해 있지 않은 게시글인 경우 수정,삭제할 수 없다.") - @Test - void editArticleByInvalidStudyId() { - final Member member = createMember(1L); - final Study study = createStudy(1L, member); - final CommunityArticle communityArticle = CommunityArticle.write(member, study, new ArticleRequest("제목", "내용")); - - assertThat(communityArticle.isViewableBy(2L, member.getId())).isFalse(); - } - - private Study createStudy(final long id, final Member owner) { - return new Study(id, - new Content("제목", "한 줄 소개", "http://image", "설명"), - Participants.createBy(owner.getId()), - new RecruitPlanner(10, RecruitStatus.RECRUITMENT_START, null), - new StudyPlanner(LocalDate.now(), null, StudyStatus.IN_PROGRESS), - new AttachedTags(List.of()), - LocalDateTime.now() - ); - } - - private Member createMember(final long id) { - return new Member(id, id, "username" + id, "image", "profile"); - } -} diff --git a/backend/src/test/java/com/woowacourse/moamoa/studyroom/controller/ArticleControllerTest.java b/backend/src/test/java/com/woowacourse/moamoa/studyroom/controller/ArticleControllerTest.java new file mode 100644 index 000000000..a1777fbac --- /dev/null +++ b/backend/src/test/java/com/woowacourse/moamoa/studyroom/controller/ArticleControllerTest.java @@ -0,0 +1,133 @@ +package com.woowacourse.moamoa.studyroom.controller; + +import static com.woowacourse.moamoa.studyroom.domain.ArticleType.COMMUNITY; +import static com.woowacourse.moamoa.studyroom.domain.ArticleType.NOTICE; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.woowacourse.moamoa.common.RepositoryTest; +import com.woowacourse.moamoa.common.utils.DateTimeSystem; +import com.woowacourse.moamoa.member.domain.Member; +import com.woowacourse.moamoa.member.domain.repository.MemberRepository; +import com.woowacourse.moamoa.study.domain.Study; +import com.woowacourse.moamoa.study.domain.repository.StudyRepository; +import com.woowacourse.moamoa.study.service.StudyService; +import com.woowacourse.moamoa.study.service.exception.StudyNotFoundException; +import com.woowacourse.moamoa.study.service.request.StudyRequestBuilder; +import com.woowacourse.moamoa.studyroom.domain.Article; +import com.woowacourse.moamoa.studyroom.domain.CommunityArticle; +import com.woowacourse.moamoa.studyroom.domain.NoticeArticle; +import com.woowacourse.moamoa.studyroom.domain.StudyRoom; +import com.woowacourse.moamoa.studyroom.domain.repository.article.ArticleRepositoryFactory; +import com.woowacourse.moamoa.studyroom.domain.repository.studyroom.StudyRoomRepository; +import com.woowacourse.moamoa.studyroom.query.ArticleDao; +import com.woowacourse.moamoa.studyroom.service.ArticleService; +import com.woowacourse.moamoa.studyroom.service.request.ArticleRequest; +import java.time.LocalDate; +import java.util.Set; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +@RepositoryTest +public class ArticleControllerTest { + + StudyRequestBuilder javaStudyRequest = new StudyRequestBuilder() + .title("java 스터디").excerpt("자바 설명").thumbnail("java image").description("자바 소개"); + + @Autowired + private StudyRepository studyRepository; + + @Autowired + private MemberRepository memberRepository; + + @Autowired + private ArticleRepositoryFactory articleRepositoryFactory; + + @Autowired + private StudyRoomRepository studyRoomRepository; + + @Autowired + private ArticleDao articleDao; + + private StudyService studyService; + private ArticleController sut; + + @BeforeEach + void setUp() { + studyService = new StudyService(studyRepository, memberRepository, new DateTimeSystem()); + sut = new ArticleController( + new ArticleService(studyRoomRepository, articleRepositoryFactory, articleDao)); + } + + @DisplayName("커뮤니티 게시글을 작성한다.") + @Test + void createCommunityArticle() { + // arrange + Member owner = memberRepository.save(new Member(1L, "username", "imageUrl", "profileUrl")); + Study study = studyService + .createStudy(owner.getGithubId(), javaStudyRequest.startDate(LocalDate.now()).build()); + + ArticleRequest request = new ArticleRequest("게시글 제목", "게시글 내용"); + + // act + ResponseEntity response = sut.createArticle(owner.getId(), study.getId(), COMMUNITY, request); + + // assert + String location = response.getHeaders().getLocation().getPath(); + Long articleId = Long.valueOf(location.replaceAll("/api/studies/\\d+/community/articles/", "")); + + Article actualArticle = articleRepositoryFactory.getRepository(COMMUNITY).findById(articleId) + .orElseThrow(); + StudyRoom expectStudyRoom = new StudyRoom(study.getId(), owner.getId(), Set.of()); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED); + assertThat(location).matches("/api/studies/\\d+/community/articles/\\d+"); + assertThat(actualArticle).isEqualTo( + new CommunityArticle(articleId, "게시글 제목", "게시글 내용", owner.getId(), expectStudyRoom) + ); + } + + @DisplayName("커뮤니티 공지사항을 작성한다.") + @Test + void createNoticeArticle() { + // arrange + Member owner = memberRepository.save(new Member(1L, "username", "imageUrl", "profileUrl")); + Study study = studyService + .createStudy(owner.getGithubId(), javaStudyRequest.startDate(LocalDate.now()).build()); + + ArticleRequest request = new ArticleRequest("게시글 제목", "게시글 내용"); + + // act + ResponseEntity response = sut.createArticle(owner.getId(), study.getId(), NOTICE, request); + + // assert + String location = response.getHeaders().getLocation().getPath(); + Long articleId = Long.valueOf(location.replaceAll("/api/studies/\\d+/notice/articles/", "")); + Article actualArticle = articleRepositoryFactory.getRepository(NOTICE).findById(articleId).orElseThrow(); + + StudyRoom expectStudyRoom = new StudyRoom(study.getId(), owner.getId(), Set.of()); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED); + assertThat(location).matches("/api/studies/\\d+/notice/articles/\\d+"); + assertThat(actualArticle).isEqualTo( + new NoticeArticle(articleId, "게시글 제목", "게시글 내용", owner.getId(), expectStudyRoom) + ); + } + + @DisplayName("스터디가 없는 경우 게시글 작성 시 예외가 발생한다.") + @Test + void throwExceptionWhenWriteToNotFoundStudy() { + // arrange + Member member = memberRepository.save(new Member(1L, "username", "imageUrl", "profileUrl")); + + // act & assert + assertThatThrownBy(() -> sut.createArticle(member.getId(), 1L, COMMUNITY, + new ArticleRequest("제목", "내용") + )) + .isInstanceOf(StudyNotFoundException.class); + } +} diff --git a/backend/src/test/java/com/woowacourse/moamoa/community/controller/DeletingCommunityArticleControllerTest.java b/backend/src/test/java/com/woowacourse/moamoa/studyroom/controller/DeletingArticleControllerTest.java similarity index 65% rename from backend/src/test/java/com/woowacourse/moamoa/community/controller/DeletingCommunityArticleControllerTest.java rename to backend/src/test/java/com/woowacourse/moamoa/studyroom/controller/DeletingArticleControllerTest.java index 8d3ea958a..aa64819a5 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/community/controller/DeletingCommunityArticleControllerTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/studyroom/controller/DeletingArticleControllerTest.java @@ -1,23 +1,26 @@ -package com.woowacourse.moamoa.community.controller; +package com.woowacourse.moamoa.studyroom.controller; +import static com.woowacourse.moamoa.studyroom.domain.ArticleType.COMMUNITY; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import com.woowacourse.moamoa.common.RepositoryTest; import com.woowacourse.moamoa.common.utils.DateTimeSystem; -import com.woowacourse.moamoa.community.domain.CommunityArticle; -import com.woowacourse.moamoa.community.domain.repository.CommunityArticleRepository; -import com.woowacourse.moamoa.community.query.CommunityArticleDao; -import com.woowacourse.moamoa.community.service.CommunityArticleService; -import com.woowacourse.moamoa.community.service.exception.ArticleNotFoundException; -import com.woowacourse.moamoa.community.service.exception.UneditableArticleException; -import com.woowacourse.moamoa.community.service.request.ArticleRequest; import com.woowacourse.moamoa.member.domain.Member; import com.woowacourse.moamoa.member.domain.repository.MemberRepository; import com.woowacourse.moamoa.study.domain.Study; import com.woowacourse.moamoa.study.domain.repository.StudyRepository; import com.woowacourse.moamoa.study.service.StudyService; import com.woowacourse.moamoa.study.service.request.StudyRequestBuilder; +import com.woowacourse.moamoa.studyroom.domain.Article; +import com.woowacourse.moamoa.studyroom.domain.repository.studyroom.StudyRoomRepository; +import com.woowacourse.moamoa.studyroom.domain.repository.article.ArticleRepository; +import com.woowacourse.moamoa.studyroom.domain.repository.article.ArticleRepositoryFactory; +import com.woowacourse.moamoa.studyroom.query.ArticleDao; +import com.woowacourse.moamoa.studyroom.service.ArticleService; +import com.woowacourse.moamoa.studyroom.service.exception.ArticleNotFoundException; +import com.woowacourse.moamoa.studyroom.service.exception.UneditableArticleException; +import com.woowacourse.moamoa.studyroom.service.request.ArticleRequest; import java.time.LocalDate; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -25,7 +28,7 @@ import org.springframework.beans.factory.annotation.Autowired; @RepositoryTest -public class DeletingCommunityArticleControllerTest { +public class DeletingArticleControllerTest { StudyRequestBuilder javaStudyRequest = new StudyRequestBuilder() .title("java 스터디").excerpt("자바 설명").thumbnail("java image").description("자바 소개"); @@ -33,25 +36,28 @@ public class DeletingCommunityArticleControllerTest { @Autowired private StudyRepository studyRepository; + @Autowired + private StudyRoomRepository studyRoomRepository; + @Autowired private MemberRepository memberRepository; @Autowired - private CommunityArticleRepository communityArticleRepository; + private ArticleRepositoryFactory articleRepositoryFactory; @Autowired - private CommunityArticleDao communityArticleDao; + private ArticleDao articleDao; private StudyService studyService; - private CommunityArticleController sut; - private CommunityArticleService communityArticleService; + private ArticleController sut; + private ArticleService articleService; @BeforeEach void setUp() { studyService = new StudyService(studyRepository, memberRepository, new DateTimeSystem()); - communityArticleService = new CommunityArticleService(memberRepository, studyRepository, - communityArticleRepository, communityArticleDao); - sut = new CommunityArticleController(communityArticleService); + articleService = new ArticleService(studyRoomRepository, + articleRepositoryFactory, articleDao); + sut = new ArticleController(articleService); } @DisplayName("스터디 커뮤니티 게시글을 삭제한다.") @@ -64,14 +70,14 @@ void deleteCommunityArticle() { .createStudy(member.getGithubId(), javaStudyRequest.startDate(LocalDate.now()).build()); ArticleRequest request = new ArticleRequest("게시글 제목", "게시글 내용"); - CommunityArticle article = communityArticleService.createArticle(member.getId(), study.getId(), - request); + Article article = articleService.createArticle(member.getId(), study.getId(), request, COMMUNITY); //act - sut.deleteArticle(member.getId(), study.getId(), article.getId()); + sut.deleteArticle(member.getId(), study.getId(), article.getId(), COMMUNITY); //assert - assertThat(communityArticleRepository.existsById(article.getId())).isFalse(); + ArticleRepository
    articleRepository = articleRepositoryFactory.getRepository(COMMUNITY); + assertThat(articleRepository.existsById(article.getId())).isFalse(); } @DisplayName("게시글이 없는 경우 조회 시 예외가 발생한다.") @@ -83,7 +89,7 @@ void throwExceptionWhenGettingToNotFoundArticle() { .createStudy(member.getGithubId(), javaStudyRequest.startDate(LocalDate.now()).build()); // act & assert - assertThatThrownBy(() -> sut.deleteArticle(member.getId(), study.getId(), 1L)) + assertThatThrownBy(() -> sut.deleteArticle(member.getId(), study.getId(), 1L, COMMUNITY)) .isInstanceOf(ArticleNotFoundException.class); } @@ -98,11 +104,10 @@ void throwExceptionWhenDeletingByNotParticipant() { .createStudy(member.getGithubId(), javaStudyRequest.startDate(LocalDate.now()).build()); ArticleRequest request = new ArticleRequest("게시글 제목", "게시글 내용"); - final CommunityArticle article = communityArticleService.createArticle(member.getId(), study.getId(), - request); + Article article = articleService.createArticle(member.getId(), study.getId(), request, COMMUNITY); // act & assert - assertThatThrownBy(() -> sut.deleteArticle(other.getId(), study.getId(), article.getId())) + assertThatThrownBy(() -> sut.deleteArticle(other.getId(), study.getId(), article.getId(), COMMUNITY)) .isInstanceOf(UneditableArticleException.class); } } diff --git a/backend/src/test/java/com/woowacourse/moamoa/community/controller/GettingCommunityArticleControllerTest.java b/backend/src/test/java/com/woowacourse/moamoa/studyroom/controller/GettingArticleControllerTest.java similarity index 63% rename from backend/src/test/java/com/woowacourse/moamoa/community/controller/GettingCommunityArticleControllerTest.java rename to backend/src/test/java/com/woowacourse/moamoa/studyroom/controller/GettingArticleControllerTest.java index 1f8bac2eb..2ef1df752 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/community/controller/GettingCommunityArticleControllerTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/studyroom/controller/GettingArticleControllerTest.java @@ -1,25 +1,27 @@ -package com.woowacourse.moamoa.community.controller; +package com.woowacourse.moamoa.studyroom.controller; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import com.woowacourse.moamoa.common.RepositoryTest; import com.woowacourse.moamoa.common.utils.DateTimeSystem; -import com.woowacourse.moamoa.community.domain.CommunityArticle; -import com.woowacourse.moamoa.community.domain.repository.CommunityArticleRepository; -import com.woowacourse.moamoa.community.query.CommunityArticleDao; -import com.woowacourse.moamoa.community.service.CommunityArticleService; -import com.woowacourse.moamoa.community.service.exception.ArticleNotFoundException; -import com.woowacourse.moamoa.community.service.exception.UnviewableArticleException; -import com.woowacourse.moamoa.community.service.request.ArticleRequest; -import com.woowacourse.moamoa.community.service.response.ArticleResponse; -import com.woowacourse.moamoa.community.service.response.AuthorResponse; import com.woowacourse.moamoa.member.domain.Member; import com.woowacourse.moamoa.member.domain.repository.MemberRepository; import com.woowacourse.moamoa.study.domain.Study; import com.woowacourse.moamoa.study.domain.repository.StudyRepository; import com.woowacourse.moamoa.study.service.StudyService; import com.woowacourse.moamoa.study.service.request.StudyRequestBuilder; +import com.woowacourse.moamoa.studyroom.domain.Article; +import com.woowacourse.moamoa.studyroom.domain.ArticleType; +import com.woowacourse.moamoa.studyroom.domain.repository.studyroom.StudyRoomRepository; +import com.woowacourse.moamoa.studyroom.domain.repository.article.ArticleRepositoryFactory; +import com.woowacourse.moamoa.studyroom.query.ArticleDao; +import com.woowacourse.moamoa.studyroom.service.ArticleService; +import com.woowacourse.moamoa.studyroom.service.exception.ArticleNotFoundException; +import com.woowacourse.moamoa.studyroom.service.exception.UnviewableArticleException; +import com.woowacourse.moamoa.studyroom.service.request.ArticleRequest; +import com.woowacourse.moamoa.studyroom.service.response.ArticleResponse; +import com.woowacourse.moamoa.studyroom.service.response.AuthorResponse; import java.time.LocalDate; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -29,7 +31,7 @@ import org.springframework.http.ResponseEntity; @RepositoryTest -public class GettingCommunityArticleControllerTest { +public class GettingArticleControllerTest { StudyRequestBuilder javaStudyRequest = new StudyRequestBuilder() .title("java 스터디").excerpt("자바 설명").thumbnail("java image").description("자바 소개"); @@ -37,25 +39,27 @@ public class GettingCommunityArticleControllerTest { @Autowired private StudyRepository studyRepository; + @Autowired + private StudyRoomRepository studyRoomRepository; + @Autowired private MemberRepository memberRepository; @Autowired - private CommunityArticleRepository communityArticleRepository; + private ArticleRepositoryFactory articleRepositoryFactory; @Autowired - private CommunityArticleDao communityArticleDao; + private ArticleDao articleDao; private StudyService studyService; - private CommunityArticleController sut; - private CommunityArticleService communityArticleService; + private ArticleController sut; + private ArticleService articleService; @BeforeEach void setUp() { studyService = new StudyService(studyRepository, memberRepository, new DateTimeSystem()); - communityArticleService = new CommunityArticleService(memberRepository, studyRepository, - communityArticleRepository, communityArticleDao); - sut = new CommunityArticleController(communityArticleService); + articleService = new ArticleService(studyRoomRepository, articleRepositoryFactory, articleDao); + sut = new ArticleController(articleService); } @DisplayName("스터디 게시글을 단건 조회한다.") @@ -67,19 +71,20 @@ void getStudyCommunityArticle() { .createStudy(member.getGithubId(), javaStudyRequest.startDate(LocalDate.now()).build()); ArticleRequest request = new ArticleRequest("게시글 제목", "게시글 내용"); - final CommunityArticle article = communityArticleService.createArticle(member.getId(), study.getId(), - request); + Article article = articleService.createArticle(member.getId(), study.getId(), request, ArticleType.COMMUNITY); //act final ResponseEntity response = sut.getArticle(member.getId(), study.getId(), - article.getId()); + ArticleType.COMMUNITY, article.getId()); //assert + final AuthorResponse expectedAuthorResponse = new AuthorResponse( + member.getGithubId(), member.getUsername(), member.getImageUrl(), member.getProfileUrl() + ); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(response.getBody()).isEqualTo(new ArticleResponse(article.getId(), - new AuthorResponse(member.getGithubId(), member.getUsername(), member.getImageUrl(), - member.getProfileUrl()), - request.getTitle(), request.getContent(), LocalDate.now(), LocalDate.now())); + assertThat(response.getBody()).isEqualTo( + new ArticleResponse(article.getId(), expectedAuthorResponse, request.getTitle(), request.getContent(), + LocalDate.now(), LocalDate.now())); } @DisplayName("게시글이 없는 경우 조회 시 예외가 발생한다.") @@ -91,7 +96,7 @@ void throwExceptionWhenGettingToNotFoundArticle() { .createStudy(member.getGithubId(), javaStudyRequest.startDate(LocalDate.now()).build()); // act & assert - assertThatThrownBy(() -> sut.getArticle(member.getId(), study.getId(), 1L)) + assertThatThrownBy(() -> sut.getArticle(member.getId(), study.getId(), ArticleType.COMMUNITY, 1L)) .isInstanceOf(ArticleNotFoundException.class); } @@ -106,11 +111,10 @@ void throwExceptionWhenGettingByNotParticipant() { .createStudy(member.getGithubId(), javaStudyRequest.startDate(LocalDate.now()).build()); ArticleRequest request = new ArticleRequest("게시글 제목", "게시글 내용"); - final CommunityArticle article = communityArticleService.createArticle(member.getId(), study.getId(), - request); + Article article = articleService.createArticle(member.getId(), study.getId(), request, ArticleType.COMMUNITY); // act & assert - assertThatThrownBy(() -> sut.getArticle(other.getId(), study.getId(), article.getId())) + assertThatThrownBy(() -> sut.getArticle(other.getId(), study.getId(), ArticleType.COMMUNITY, article.getId())) .isInstanceOf(UnviewableArticleException.class); } } diff --git a/backend/src/test/java/com/woowacourse/moamoa/community/controller/GettingCommunityArticleSummariesControllerTest.java b/backend/src/test/java/com/woowacourse/moamoa/studyroom/controller/GettingCommunityArticleSummariesControllerTest.java similarity index 66% rename from backend/src/test/java/com/woowacourse/moamoa/community/controller/GettingCommunityArticleSummariesControllerTest.java rename to backend/src/test/java/com/woowacourse/moamoa/studyroom/controller/GettingCommunityArticleSummariesControllerTest.java index 2fcd84051..71310a133 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/community/controller/GettingCommunityArticleSummariesControllerTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/studyroom/controller/GettingCommunityArticleSummariesControllerTest.java @@ -1,19 +1,21 @@ -package com.woowacourse.moamoa.community.controller; +package com.woowacourse.moamoa.studyroom.controller; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import com.woowacourse.moamoa.common.RepositoryTest; import com.woowacourse.moamoa.common.utils.DateTimeSystem; -import com.woowacourse.moamoa.community.domain.CommunityArticle; -import com.woowacourse.moamoa.community.domain.repository.CommunityArticleRepository; -import com.woowacourse.moamoa.community.query.CommunityArticleDao; -import com.woowacourse.moamoa.community.service.CommunityArticleService; -import com.woowacourse.moamoa.community.service.exception.UnviewableArticleException; -import com.woowacourse.moamoa.community.service.request.ArticleRequest; -import com.woowacourse.moamoa.community.service.response.ArticleSummariesResponse; -import com.woowacourse.moamoa.community.service.response.ArticleSummaryResponse; -import com.woowacourse.moamoa.community.service.response.AuthorResponse; +import com.woowacourse.moamoa.studyroom.domain.Article; +import com.woowacourse.moamoa.studyroom.domain.ArticleType; +import com.woowacourse.moamoa.studyroom.domain.repository.studyroom.StudyRoomRepository; +import com.woowacourse.moamoa.studyroom.domain.repository.article.ArticleRepositoryFactory; +import com.woowacourse.moamoa.studyroom.query.ArticleDao; +import com.woowacourse.moamoa.studyroom.service.ArticleService; +import com.woowacourse.moamoa.studyroom.service.exception.UnviewableArticleException; +import com.woowacourse.moamoa.studyroom.service.request.ArticleRequest; +import com.woowacourse.moamoa.studyroom.service.response.ArticleSummariesResponse; +import com.woowacourse.moamoa.studyroom.service.response.ArticleSummaryResponse; +import com.woowacourse.moamoa.studyroom.service.response.AuthorResponse; import com.woowacourse.moamoa.member.domain.Member; import com.woowacourse.moamoa.member.domain.repository.MemberRepository; import com.woowacourse.moamoa.study.domain.Study; @@ -37,30 +39,33 @@ public class GettingCommunityArticleSummariesControllerTest { StudyRequestBuilder javaStudyRequest = new StudyRequestBuilder() .title("java 스터디").excerpt("자바 설명").thumbnail("java image").description("자바 소개"); - private CommunityArticleService communityArticleService; + private ArticleService articleService; private StudyService studyService; @Autowired private MemberRepository memberRepository; + @Autowired + private StudyRoomRepository studyRoomRepository; + @Autowired private StudyRepository studyRepository; @Autowired - private CommunityArticleRepository communityArticleRepository; + private ArticleRepositoryFactory articleRepositoryFactory; @Autowired - private CommunityArticleDao communityArticleDao; + private ArticleDao articleDao; - private CommunityArticleController sut; + private ArticleController sut; @BeforeEach void setUp() { - communityArticleService = new CommunityArticleService(memberRepository, studyRepository, - communityArticleRepository, communityArticleDao); + articleService = new ArticleService(studyRoomRepository, + articleRepositoryFactory, articleDao); studyService = new StudyService(studyRepository, memberRepository, new DateTimeSystem()); - sut = new CommunityArticleController(communityArticleService); + sut = new ArticleController(articleService); } @DisplayName("스터디 커뮤니티 글 목록을 조회한다.") @@ -71,17 +76,20 @@ void getCommunityArticles() { Study study = studyService.createStudy(그린론.getGithubId(), javaStudyRequest.startDate(LocalDate.now()).build()); - communityArticleService.createArticle(그린론.getId(), study.getId(), new ArticleRequest("제목1", "내용1")); - communityArticleService.createArticle(그린론.getId(), study.getId(), new ArticleRequest("제목2", "내용2")); - CommunityArticle article3 = communityArticleService - .createArticle(그린론.getId(), study.getId(), new ArticleRequest("제목3", "내용3")); - CommunityArticle article4 = communityArticleService - .createArticle(그린론.getId(), study.getId(), new ArticleRequest("제목4", "내용4")); - CommunityArticle article5 = communityArticleService - .createArticle(그린론.getId(), study.getId(), new ArticleRequest("제목5", "내용5")); + articleService + .createArticle(그린론.getId(), study.getId(), new ArticleRequest("제목1", "내용1"), ArticleType.COMMUNITY); + articleService + .createArticle(그린론.getId(), study.getId(), new ArticleRequest("제목2", "내용2"), ArticleType.COMMUNITY); + Article article3 = articleService + .createArticle(그린론.getId(), study.getId(), new ArticleRequest("제목3", "내용3"), ArticleType.COMMUNITY); + Article article4 = articleService + .createArticle(그린론.getId(), study.getId(), new ArticleRequest("제목4", "내용4"), ArticleType.COMMUNITY); + Article article5 = articleService + .createArticle(그린론.getId(), study.getId(), new ArticleRequest("제목5", "내용5"), ArticleType.COMMUNITY); // act - ResponseEntity response = sut.getArticles(그린론.getId(), study.getId(), PageRequest.of(0, 3)); + ResponseEntity response = sut.getArticles(그린론.getId(), study.getId(), + ArticleType.COMMUNITY, PageRequest.of(0, 3)); // assert AuthorResponse author = new AuthorResponse(1L, "그린론", "http://image", "http://profile"); @@ -105,7 +113,7 @@ void throwExceptionWhenWriteToNotFoundStudy() { Member member = memberRepository.save(new Member(1L, "username", "imageUrl", "profileUrl")); // act & assert - assertThatThrownBy(() -> sut.getArticles(member.getId(), 1L, PageRequest.of(0, 3))) + assertThatThrownBy(() -> sut.getArticles(member.getId(), 1L, ArticleType.COMMUNITY, PageRequest.of(0, 3))) .isInstanceOf(StudyNotFoundException.class); } @@ -120,7 +128,8 @@ void throwExceptionWhenGettingByNotParticipant() { .createStudy(member.getGithubId(), javaStudyRequest.startDate(LocalDate.now()).build()); // act & assert - assertThatThrownBy(() -> sut.getArticles(other.getId(), study.getId(), PageRequest.of(0, 3))) + assertThatThrownBy( + () -> sut.getArticles(other.getId(), study.getId(), ArticleType.COMMUNITY, PageRequest.of(0, 3))) .isInstanceOf(UnviewableArticleException.class); } } diff --git a/backend/src/test/java/com/woowacourse/moamoa/community/controller/UpdatingCommunityArticleControllerTest.java b/backend/src/test/java/com/woowacourse/moamoa/studyroom/controller/UpdatingArticleControllerTest.java similarity index 51% rename from backend/src/test/java/com/woowacourse/moamoa/community/controller/UpdatingCommunityArticleControllerTest.java rename to backend/src/test/java/com/woowacourse/moamoa/studyroom/controller/UpdatingArticleControllerTest.java index 874a961ee..09e270eb8 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/community/controller/UpdatingCommunityArticleControllerTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/studyroom/controller/UpdatingArticleControllerTest.java @@ -1,24 +1,30 @@ -package com.woowacourse.moamoa.community.controller; +package com.woowacourse.moamoa.studyroom.controller; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import com.woowacourse.moamoa.common.RepositoryTest; import com.woowacourse.moamoa.common.utils.DateTimeSystem; -import com.woowacourse.moamoa.community.domain.CommunityArticle; -import com.woowacourse.moamoa.community.domain.repository.CommunityArticleRepository; -import com.woowacourse.moamoa.community.query.CommunityArticleDao; -import com.woowacourse.moamoa.community.service.CommunityArticleService; -import com.woowacourse.moamoa.community.service.exception.ArticleNotFoundException; -import com.woowacourse.moamoa.community.service.exception.UneditableArticleException; -import com.woowacourse.moamoa.community.service.request.ArticleRequest; import com.woowacourse.moamoa.member.domain.Member; import com.woowacourse.moamoa.member.domain.repository.MemberRepository; import com.woowacourse.moamoa.study.domain.Study; import com.woowacourse.moamoa.study.domain.repository.StudyRepository; import com.woowacourse.moamoa.study.service.StudyService; import com.woowacourse.moamoa.study.service.request.StudyRequestBuilder; +import com.woowacourse.moamoa.studyroom.domain.Article; +import com.woowacourse.moamoa.studyroom.domain.ArticleType; +import com.woowacourse.moamoa.studyroom.domain.CommunityArticle; +import com.woowacourse.moamoa.studyroom.domain.StudyRoom; +import com.woowacourse.moamoa.studyroom.domain.repository.studyroom.StudyRoomRepository; +import com.woowacourse.moamoa.studyroom.domain.repository.article.ArticleRepository; +import com.woowacourse.moamoa.studyroom.domain.repository.article.ArticleRepositoryFactory; +import com.woowacourse.moamoa.studyroom.query.ArticleDao; +import com.woowacourse.moamoa.studyroom.service.ArticleService; +import com.woowacourse.moamoa.studyroom.service.exception.ArticleNotFoundException; +import com.woowacourse.moamoa.studyroom.service.exception.UneditableArticleException; +import com.woowacourse.moamoa.studyroom.service.request.ArticleRequest; import java.time.LocalDate; +import java.util.Set; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -27,7 +33,7 @@ import org.springframework.http.ResponseEntity; @RepositoryTest -public class UpdatingCommunityArticleControllerTest { +public class UpdatingArticleControllerTest { StudyRequestBuilder javaStudyBuilder = new StudyRequestBuilder() .title("java 스터디").excerpt("자바 설명").thumbnail("java image").description("자바 소개"); @@ -35,44 +41,52 @@ public class UpdatingCommunityArticleControllerTest { @Autowired private StudyRepository studyRepository; + @Autowired + private StudyRoomRepository studyRoomRepository; + @Autowired private MemberRepository memberRepository; @Autowired - private CommunityArticleRepository communityArticleRepository; + private ArticleRepositoryFactory articleRepositoryFactory; @Autowired - private CommunityArticleDao communityArticleDao; + private ArticleDao articleDao; private StudyService studyService; - private CommunityArticleController sut; - private CommunityArticleService communityArticleService; + private ArticleController sut; + private ArticleService articleService; @BeforeEach void setUp() { studyService = new StudyService(studyRepository, memberRepository, new DateTimeSystem()); - communityArticleService = new CommunityArticleService(memberRepository, studyRepository, - communityArticleRepository, communityArticleDao); - sut = new CommunityArticleController(communityArticleService); + articleService = new ArticleService(studyRoomRepository, + articleRepositoryFactory, articleDao); + sut = new ArticleController(articleService); } @DisplayName("게시글을 수정한다.") @Test void updateArticle() { // arrange - Member member = memberRepository.save(new Member(1L, "username", "image", "profile")); + Member owner = memberRepository.save(new Member(1L, "username", "image", "profile")); Study study = studyService - .createStudy(member.getGithubId(), javaStudyBuilder.startDate(LocalDate.now()).build()); - CommunityArticle article = communityArticleService - .createArticle(member.getId(), study.getId(), new ArticleRequest("제목", "내용")); + .createStudy(owner.getGithubId(), javaStudyBuilder.startDate(LocalDate.now()).build()); + Article article = articleService + .createArticle(owner.getId(), study.getId(), new ArticleRequest("제목", "내용"), ArticleType.COMMUNITY); // act - final ResponseEntity response = sut.updateArticle(member.getId(), study.getId(), article.getId(), - new ArticleRequest("제목 수정", "내용 수정")); + final ResponseEntity response = sut + .updateArticle(owner.getId(), study.getId(), article.getId(), ArticleType.COMMUNITY, + new ArticleRequest("제목 수정", "내용 수정")); // assert - CommunityArticle actualArticle = communityArticleRepository.findById(article.getId()).orElseThrow(); - CommunityArticle expectArticle = new CommunityArticle(article.getId(), "제목 수정", "내용 수정", member.getId(), study); + ArticleRepository
    articleRepository = articleRepositoryFactory.getRepository(ArticleType.COMMUNITY); + Article actualArticle = articleRepository.findById(article.getId()).orElseThrow(); + + StudyRoom expectStudyRoom = new StudyRoom(study.getId(), owner.getId(), Set.of()); + CommunityArticle expectArticle = new CommunityArticle(article.getId(), "제목 수정", "내용 수정", owner.getId(), + expectStudyRoom); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NO_CONTENT); assertThat(actualArticle).isEqualTo(expectArticle); @@ -87,8 +101,10 @@ void throwExceptionWhenUpdateToNotFoundArticle() { .createStudy(member.getGithubId(), javaStudyBuilder.startDate(LocalDate.now()).build()); // act & assert - assertThatThrownBy( - () -> sut.updateArticle(member.getId(), study.getId(), 1L, new ArticleRequest("제목 수정", "내용 수정"))) + assertThatThrownBy(() -> + sut.updateArticle(member.getId(), study.getId(), 1L, ArticleType.COMMUNITY, + new ArticleRequest("제목 수정", "내용 수정")) + ) .isInstanceOf(ArticleNotFoundException.class); } @@ -103,12 +119,14 @@ void throwExceptionWhenUpdateByNotParticipant() { .createStudy(member.getGithubId(), javaStudyBuilder.startDate(LocalDate.now()).build()); ArticleRequest request = new ArticleRequest("게시글 제목", "게시글 내용"); - final CommunityArticle article = communityArticleService.createArticle(member.getId(), study.getId(), - request); + final Article article = articleService + .createArticle(member.getId(), study.getId(), request, ArticleType.COMMUNITY); // act & assert - assertThatThrownBy(() -> sut - .updateArticle(other.getId(), study.getId(), article.getId(), new ArticleRequest("제목 수정", "내용 수정"))) + assertThatThrownBy(() -> + sut.updateArticle(other.getId(), study.getId(), article.getId(), ArticleType.COMMUNITY, + new ArticleRequest("제목 수정", "내용 수정")) + ) .isInstanceOf(UneditableArticleException.class); } } diff --git a/backend/src/test/java/com/woowacourse/moamoa/studyroom/domain/ArticleTest.java b/backend/src/test/java/com/woowacourse/moamoa/studyroom/domain/ArticleTest.java new file mode 100644 index 000000000..54500f51d --- /dev/null +++ b/backend/src/test/java/com/woowacourse/moamoa/studyroom/domain/ArticleTest.java @@ -0,0 +1,90 @@ +package com.woowacourse.moamoa.studyroom.domain; + +import static com.woowacourse.moamoa.studyroom.domain.ArticleType.COMMUNITY; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.woowacourse.moamoa.member.domain.Member; +import com.woowacourse.moamoa.member.service.exception.NotParticipatedMemberException; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +class ArticleTest { + + @DisplayName("스터디에 참여한 참가자만 게시글을 작성할 수 있다.") + @Test + void writeCommunityArticleByParticipant() { + long studyId = 1L; + Member owner = createMember(1L); + Member another = createMember(2L); + StudyRoom studyRoom = createPermittedAccessors(studyId, owner); + Accessor accessor = new Accessor(another.getId(), 1L); + + assertThatThrownBy(() -> studyRoom.write(accessor, "제목", "내용", COMMUNITY)) + .isInstanceOf(NotParticipatedMemberException.class); + } + + @DisplayName("스터디 참여자는 게시글을 조회할 수 있다.") + @ParameterizedTest + @CsvSource({"1,true", "2,true", "3,false"}) + void getArticle(Long viewerId, boolean expected) { + long studyId = 1L; + Member owner = createMember(1L); + Member participant = createMember(2L); + StudyRoom studyRoom = createPermittedAccessors(studyId, owner, participant); + Accessor accessor = new Accessor(owner.getId(), studyId); + Article article = studyRoom.write(accessor, "제목", "내용", COMMUNITY); + + assertThat(article.isViewableBy(new Accessor(viewerId, studyId))).isEqualTo(expected); + } + + @DisplayName("스터디에 속해 있는 게시글이 맞을 경우 조회할 수 있다.") + @ParameterizedTest + @CsvSource({"1,1,true", "1,2,false"}) + void deleteArticle(Long studyId, Long wantToViewStudyId, boolean expected) { + Member member = createMember(1L); + StudyRoom studyRoom = createPermittedAccessors(studyId, member); + Accessor accessor = new Accessor(member.getId(), studyId); + Article article = studyRoom.write(accessor, "제목", "내용", COMMUNITY); + + assertThat(article.isViewableBy(new Accessor(member.getId(), wantToViewStudyId))).isEqualTo(expected); + } + + @DisplayName("스터디에 참여했고, 작성자인 경우 스터디 게시글을 수정,삭제할 수 있다.") + @ParameterizedTest + @CsvSource({"1,true", "2,false", "3,false"}) + void updateArticle(Long editorId, boolean expected) { + Member owner = createMember(1L); + Member participant = createMember(2L); + StudyRoom studyRoom = createPermittedAccessors(1L, owner, participant); + Accessor accessor = new Accessor(owner.getId(), 1L); + Article article = studyRoom.write(accessor, "제목", "내용", COMMUNITY); + + assertThat(article.isEditableBy(new Accessor(editorId, 1L))).isEqualTo(expected); + } + + @DisplayName("스터디에 속해 있지 않은 게시글인 경우 수정,삭제할 수 없다.") + @Test + void editArticleByInvalidStudyId() { + Member member = createMember(1L); + StudyRoom studyRoom = createPermittedAccessors(1L, member); + Accessor accessor = new Accessor(member.getId(), 1L); + Article article = studyRoom.write(accessor, "제목", "내용", COMMUNITY); + + assertThat(article.isViewableBy(new Accessor(member.getId(), 2L))).isFalse(); + } + + private StudyRoom createPermittedAccessors(long studyId, Member owner, Member... participant) { + final Set participants = Stream.of(participant).map(Member::getId).collect(Collectors.toSet()); + return new StudyRoom(studyId, owner.getId(), participants); + } + + private Member createMember(final long id) { + return new Member(id, id, "username" + id, "image", "profile"); + } +} diff --git a/backend/src/test/java/com/woowacourse/moamoa/community/webmvc/CreatingCommunityArticleControllerWebMvcTest.java b/backend/src/test/java/com/woowacourse/moamoa/studyroom/webmvc/CreatingArticleControllerWebMvcTest.java similarity index 93% rename from backend/src/test/java/com/woowacourse/moamoa/community/webmvc/CreatingCommunityArticleControllerWebMvcTest.java rename to backend/src/test/java/com/woowacourse/moamoa/studyroom/webmvc/CreatingArticleControllerWebMvcTest.java index 9b9f80539..97f0e587a 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/community/webmvc/CreatingCommunityArticleControllerWebMvcTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/studyroom/webmvc/CreatingArticleControllerWebMvcTest.java @@ -1,4 +1,4 @@ -package com.woowacourse.moamoa.community.webmvc; +package com.woowacourse.moamoa.studyroom.webmvc; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; @@ -7,8 +7,8 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import com.woowacourse.moamoa.WebMVCTest; -import com.woowacourse.moamoa.community.service.CommunityArticleService; -import com.woowacourse.moamoa.community.service.request.ArticleRequest; +import com.woowacourse.moamoa.studyroom.service.ArticleService; +import com.woowacourse.moamoa.studyroom.service.request.ArticleRequest; import com.woowacourse.moamoa.member.service.exception.NotParticipatedMemberException; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -19,10 +19,10 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; -public class CreatingCommunityArticleControllerWebMvcTest extends WebMVCTest { +public class CreatingArticleControllerWebMvcTest extends WebMVCTest { @MockBean - private CommunityArticleService communityArticleService; + private ArticleService articleService; @DisplayName("잘못된 토큰으로 커뮤니티 글을 생성할 경우 401을 반환한다.") @ParameterizedTest @@ -146,7 +146,7 @@ void badRequestByInvalidLengthContent() throws Exception { @DisplayName("스터디에 참여한 참가자가 아닌 경우, NotParticipatedMemberException이 발생하고 401을 반환한다.") @Test void unauthorizedByNotParticipant() throws Exception { - when(communityArticleService.createArticle(any(), any(), any())).thenThrow(NotParticipatedMemberException.class); + when(articleService.createArticle(any(), any(), any(), any())).thenThrow(NotParticipatedMemberException.class); final String token = "Bearer" + tokenProvider.createToken(1L).getAccessToken(); diff --git a/backend/src/test/java/com/woowacourse/moamoa/community/webmvc/DeletingCommunityArticleControllerWebMvcTest.java b/backend/src/test/java/com/woowacourse/moamoa/studyroom/webmvc/DeletingArticleControllerWebMvcTest.java similarity index 93% rename from backend/src/test/java/com/woowacourse/moamoa/community/webmvc/DeletingCommunityArticleControllerWebMvcTest.java rename to backend/src/test/java/com/woowacourse/moamoa/studyroom/webmvc/DeletingArticleControllerWebMvcTest.java index 6104ad81b..4c87c82ac 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/community/webmvc/DeletingCommunityArticleControllerWebMvcTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/studyroom/webmvc/DeletingArticleControllerWebMvcTest.java @@ -1,4 +1,4 @@ -package com.woowacourse.moamoa.community.webmvc; +package com.woowacourse.moamoa.studyroom.webmvc; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; @@ -12,7 +12,7 @@ import org.junit.jupiter.params.provider.ValueSource; import org.springframework.http.HttpHeaders; -public class DeletingCommunityArticleControllerWebMvcTest extends WebMVCTest { +public class DeletingArticleControllerWebMvcTest extends WebMVCTest { @DisplayName("잘못된 토큰으로 커뮤니티 글을 생성할 경우 401을 반환한다.") @ParameterizedTest diff --git a/backend/src/test/java/com/woowacourse/moamoa/community/webmvc/GettingCommunityArticleControllerWebMvcTest.java b/backend/src/test/java/com/woowacourse/moamoa/studyroom/webmvc/GettingArticleControllerWebMvcTest.java similarity index 97% rename from backend/src/test/java/com/woowacourse/moamoa/community/webmvc/GettingCommunityArticleControllerWebMvcTest.java rename to backend/src/test/java/com/woowacourse/moamoa/studyroom/webmvc/GettingArticleControllerWebMvcTest.java index 710b1294d..2ac43481f 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/community/webmvc/GettingCommunityArticleControllerWebMvcTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/studyroom/webmvc/GettingArticleControllerWebMvcTest.java @@ -1,4 +1,4 @@ -package com.woowacourse.moamoa.community.webmvc; +package com.woowacourse.moamoa.studyroom.webmvc; import static org.assertj.core.api.Assertions.assertThat; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; @@ -13,7 +13,7 @@ import org.junit.jupiter.params.provider.ValueSource; import org.springframework.http.HttpHeaders; -public class GettingCommunityArticleControllerWebMvcTest extends WebMVCTest { +public class GettingArticleControllerWebMvcTest extends WebMVCTest { @DisplayName("잘못된 토큰으로 커뮤니티 글을 조회할 경우 401을 반환한다.") @ParameterizedTest diff --git a/backend/src/test/java/com/woowacourse/moamoa/community/webmvc/UpdatingCommunityArticleControllerWebMvcTest.java b/backend/src/test/java/com/woowacourse/moamoa/studyroom/webmvc/UpdatingArticleControllerWebMvcTest.java similarity index 96% rename from backend/src/test/java/com/woowacourse/moamoa/community/webmvc/UpdatingCommunityArticleControllerWebMvcTest.java rename to backend/src/test/java/com/woowacourse/moamoa/studyroom/webmvc/UpdatingArticleControllerWebMvcTest.java index 336b9d17a..4b131bfd0 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/community/webmvc/UpdatingCommunityArticleControllerWebMvcTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/studyroom/webmvc/UpdatingArticleControllerWebMvcTest.java @@ -1,11 +1,11 @@ -package com.woowacourse.moamoa.community.webmvc; +package com.woowacourse.moamoa.studyroom.webmvc; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import com.woowacourse.moamoa.WebMVCTest; -import com.woowacourse.moamoa.community.service.request.ArticleRequest; +import com.woowacourse.moamoa.studyroom.service.request.ArticleRequest; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -15,7 +15,7 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; -public class UpdatingCommunityArticleControllerWebMvcTest extends WebMVCTest { +public class UpdatingArticleControllerWebMvcTest extends WebMVCTest { @DisplayName("잘못된 토큰으로 커뮤니티 글을 수정할 경우 401을 반환한다.") @ParameterizedTest diff --git a/backend/src/test/resources/schema.sql b/backend/src/test/resources/schema.sql index c0f61e824..5d1780f24 100644 --- a/backend/src/test/resources/schema.sql +++ b/backend/src/test/resources/schema.sql @@ -1,4 +1,5 @@ -DROP TABLE IF EXISTS article; +DROP TABLE IF EXISTS community; +DROP TABLE IF EXISTS notice; DROP TABLE IF EXISTS study_tag; DROP TABLE IF EXISTS study_member; DROP TABLE IF EXISTS tag; @@ -97,7 +98,20 @@ CREATE TABLE study_member FOREIGN KEY (member_id) REFERENCES member (id) ); -CREATE TABLE article +CREATE TABLE community +( + id BIGINT PRIMARY KEY AUTO_INCREMENT, + title VARCHAR(255) NOT NULL, + content MEDIUMTEXT NOT NULL, + author_id BIGINT, + study_id BIGINT, + created_date DATETIME not null, + last_modified_date DATETIME not null, + FOREIGN KEY (author_id) REFERENCES member (id), + FOREIGN KEY (study_id) REFERENCES study (id) +); + +CREATE TABLE notice ( id BIGINT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(255) NOT NULL, From 5ea1be4956bf0ecafabdcb36ce37aedc9ee58326 Mon Sep 17 00:00:00 2001 From: Donggyu Date: Wed, 17 Aug 2022 21:43:29 +0900 Subject: [PATCH 33/51] =?UTF-8?q?[BE]=20issue284:=20Code=20smell=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0=20(#286)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: 충돌 해결 * refactor: Code smell 제거 * refactor: 중복 문자열 rollback * fix: 깨지는 테스트 수정 --- .../auth/config/AuthRequestMatchConfig.java | 1 - .../auth/config/AuthenticationExtractor.java | 3 ++ .../AuthenticatedMemberResolver.java | 1 - .../AuthenticationRequestMatcherBuilder.java | 1 - .../PageableVerificationArgumentResolver.java | 5 --- .../controller/SearchingTagController.java | 1 - .../converter/CategoryIdConverter.java | 1 - .../tag/query/response/CategoryData.java | 1 - .../tag/service/SearchingTagService.java | 1 - .../test/auth/AuthAcceptanceTest.java | 4 +-- .../test/cors/CorsAcceptanceTest.java | 2 +- .../test/member/MemberAcceptanceTest.java | 2 +- .../ReferenceRoomAcceptanceTest.java | 2 +- .../test/review/ReviewsAcceptanceTest.java | 6 ++-- .../AutoCloseEnrollmentAcceptanceTest.java | 3 +- .../study/CreatingStudyAcceptanceTest.java | 4 +-- .../study/GettingMyStudiesAcceptanceTest.java | 7 +--- .../GettingStudiesSummaryAcceptanceTest.java | 14 ++++---- .../GettingStudyDetailsAcceptanceTest.java | 6 ++-- .../ParticipationStudyAcceptanceTest.java | 36 +++++++++++++++++++ .../study/SearchingStudiesAcceptanceTest.java | 18 +++++----- .../studyroom/CommunityAcceptanceTest.java | 2 +- .../test/tag/TagAcceptanceTest.java | 2 +- .../auth/controller/AuthControllerTest.java | 2 +- .../infrastructure/TokenProviderTest.java | 5 +-- .../moamoa/auth/service/AuthServiceTest.java | 12 +++---- .../moamoa/auth/service/OAuthClientTest.java | 2 +- .../controller/MemberControllerTest.java | 2 +- .../repository/MemberRepositoryTest.java | 2 +- .../member/webmvc/MemberWebMvcTest.java | 8 ++--- .../ReferenceRoomControllerTest.java | 2 +- .../SearchingReferenceRoomControllerTest.java | 7 ++-- .../moamoa/referenceroom/domain/LinkTest.java | 8 +++-- .../referenceroom/query/LinkDaoTest.java | 2 +- .../webmvc/ReferenceRoomWebMvcTest.java | 2 +- .../controller/ReviewControllerTest.java | 2 +- .../SearchingReviewControllerTest.java | 4 +-- .../moamoa/review/domain/ReviewTest.java | 8 +++-- .../webmvc/UnauthorizedReviewWebMvcTest.java | 2 +- .../controller/MyStudyControllerTest.java | 4 +-- .../SearchingStudyControllerTest.java | 10 +++--- .../moamoa/study/domain/StudyPlannerTest.java | 2 +- .../moamoa/study/domain/StudyTest.java | 26 ++++++++------ .../moamoa/study/query/MyStudyDaoTest.java | 4 +-- .../study/query/StudySummaryDaoTest.java | 8 ++--- .../study/service/MyStudyServiceTest.java | 4 +-- .../BadRequestMyMemberRoleWebMvcTest.java | 2 +- .../controller/ArticleControllerTest.java | 9 +++-- .../DeletingArticleControllerTest.java | 13 +++++-- .../GettingArticleControllerTest.java | 20 +++++++---- ...mmunityArticleSummariesControllerTest.java | 27 ++++++++------ .../UpdatingArticleControllerTest.java | 36 +++++++++++-------- .../CreatingArticleControllerWebMvcTest.java | 2 +- .../DeletingArticleControllerWebMvcTest.java | 3 +- .../GettingArticleControllerWebMvcTest.java | 3 +- .../UpdatingArticleControllerWebMvcTest.java | 2 +- 56 files changed, 212 insertions(+), 156 deletions(-) create mode 100644 backend/src/test/java/com/woowacourse/acceptance/test/study/ParticipationStudyAcceptanceTest.java diff --git a/backend/src/main/java/com/woowacourse/moamoa/auth/config/AuthRequestMatchConfig.java b/backend/src/main/java/com/woowacourse/moamoa/auth/config/AuthRequestMatchConfig.java index dcad55ba3..523620388 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/auth/config/AuthRequestMatchConfig.java +++ b/backend/src/main/java/com/woowacourse/moamoa/auth/config/AuthRequestMatchConfig.java @@ -17,7 +17,6 @@ public class AuthRequestMatchConfig { public AuthenticationRequestMatcher authenticationRequestMatcher() { return new AuthenticationRequestMatcherBuilder() .addUpAuthenticationPath(POST, - "/api/studies", "/api/studies/\\d+/reviews", "/api/studies/\\d+/reviews/\\d+", "/api/studies/\\w+/\\w+/articles", diff --git a/backend/src/main/java/com/woowacourse/moamoa/auth/config/AuthenticationExtractor.java b/backend/src/main/java/com/woowacourse/moamoa/auth/config/AuthenticationExtractor.java index ebfda47b1..cbd4c9770 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/auth/config/AuthenticationExtractor.java +++ b/backend/src/main/java/com/woowacourse/moamoa/auth/config/AuthenticationExtractor.java @@ -1,10 +1,13 @@ package com.woowacourse.moamoa.auth.config; +import static lombok.AccessLevel.PRIVATE; import static org.springframework.http.HttpHeaders.AUTHORIZATION; import java.util.Enumeration; import javax.servlet.http.HttpServletRequest; +import lombok.NoArgsConstructor; +@NoArgsConstructor(access = PRIVATE) public class AuthenticationExtractor { private static final String BEARER_TYPE = "Bearer"; diff --git a/backend/src/main/java/com/woowacourse/moamoa/auth/controller/AuthenticatedMemberResolver.java b/backend/src/main/java/com/woowacourse/moamoa/auth/controller/AuthenticatedMemberResolver.java index caf2be9eb..805ae1322 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/auth/controller/AuthenticatedMemberResolver.java +++ b/backend/src/main/java/com/woowacourse/moamoa/auth/controller/AuthenticatedMemberResolver.java @@ -2,7 +2,6 @@ import com.woowacourse.moamoa.auth.config.AuthenticatedMember; import com.woowacourse.moamoa.auth.config.AuthenticationExtractor; -import com.woowacourse.moamoa.auth.config.AuthenticationPrincipal; import com.woowacourse.moamoa.auth.infrastructure.TokenProvider; import com.woowacourse.moamoa.common.exception.UnauthorizedException; import com.woowacourse.moamoa.member.domain.Member; diff --git a/backend/src/main/java/com/woowacourse/moamoa/auth/controller/matcher/AuthenticationRequestMatcherBuilder.java b/backend/src/main/java/com/woowacourse/moamoa/auth/controller/matcher/AuthenticationRequestMatcherBuilder.java index d3ff525ac..8b55f616d 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/auth/controller/matcher/AuthenticationRequestMatcherBuilder.java +++ b/backend/src/main/java/com/woowacourse/moamoa/auth/controller/matcher/AuthenticationRequestMatcherBuilder.java @@ -5,7 +5,6 @@ import java.util.Map; import java.util.stream.Collectors; import org.springframework.http.HttpMethod; -import org.springframework.stereotype.Component; public class AuthenticationRequestMatcherBuilder { diff --git a/backend/src/main/java/com/woowacourse/moamoa/common/config/PageableVerificationArgumentResolver.java b/backend/src/main/java/com/woowacourse/moamoa/common/config/PageableVerificationArgumentResolver.java index 7ca9cdd4b..affa6cb5c 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/common/config/PageableVerificationArgumentResolver.java +++ b/backend/src/main/java/com/woowacourse/moamoa/common/config/PageableVerificationArgumentResolver.java @@ -13,11 +13,6 @@ public class PageableVerificationArgumentResolver extends PageableHandlerMethodA private static final int MINIMUM_PAGE = 0; private static final int MINIMUM_SIZE = 1; - @Override - public boolean supportsParameter(final MethodParameter parameter) { - return super.supportsParameter(parameter); - } - @Override public Pageable resolveArgument(final MethodParameter methodParameter, final ModelAndViewContainer mavContainer, diff --git a/backend/src/main/java/com/woowacourse/moamoa/tag/controller/SearchingTagController.java b/backend/src/main/java/com/woowacourse/moamoa/tag/controller/SearchingTagController.java index 07012265c..b4b332b25 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/tag/controller/SearchingTagController.java +++ b/backend/src/main/java/com/woowacourse/moamoa/tag/controller/SearchingTagController.java @@ -3,7 +3,6 @@ import com.woowacourse.moamoa.tag.query.request.CategoryIdRequest; import com.woowacourse.moamoa.tag.service.SearchingTagService; import com.woowacourse.moamoa.tag.service.response.TagsResponse; -import java.util.Optional; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; diff --git a/backend/src/main/java/com/woowacourse/moamoa/tag/controller/converter/CategoryIdConverter.java b/backend/src/main/java/com/woowacourse/moamoa/tag/controller/converter/CategoryIdConverter.java index 7cdae65b2..b31be8b94 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/tag/controller/converter/CategoryIdConverter.java +++ b/backend/src/main/java/com/woowacourse/moamoa/tag/controller/converter/CategoryIdConverter.java @@ -1,7 +1,6 @@ package com.woowacourse.moamoa.tag.controller.converter; import com.woowacourse.moamoa.tag.query.request.CategoryIdRequest; -import java.util.Optional; import org.springframework.core.convert.converter.Converter; import org.springframework.stereotype.Component; diff --git a/backend/src/main/java/com/woowacourse/moamoa/tag/query/response/CategoryData.java b/backend/src/main/java/com/woowacourse/moamoa/tag/query/response/CategoryData.java index 376738023..b4d16698e 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/tag/query/response/CategoryData.java +++ b/backend/src/main/java/com/woowacourse/moamoa/tag/query/response/CategoryData.java @@ -1,6 +1,5 @@ package com.woowacourse.moamoa.tag.query.response; -import com.woowacourse.moamoa.tag.domain.Category; import lombok.AllArgsConstructor; import lombok.Getter; diff --git a/backend/src/main/java/com/woowacourse/moamoa/tag/service/SearchingTagService.java b/backend/src/main/java/com/woowacourse/moamoa/tag/service/SearchingTagService.java index 8da3ff39d..d5d7f8330 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/tag/service/SearchingTagService.java +++ b/backend/src/main/java/com/woowacourse/moamoa/tag/service/SearchingTagService.java @@ -5,7 +5,6 @@ import com.woowacourse.moamoa.tag.query.response.TagData; import com.woowacourse.moamoa.tag.service.response.TagsResponse; import java.util.List; -import java.util.Optional; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; diff --git a/backend/src/test/java/com/woowacourse/acceptance/test/auth/AuthAcceptanceTest.java b/backend/src/test/java/com/woowacourse/acceptance/test/auth/AuthAcceptanceTest.java index b41780ed6..25909c47a 100644 --- a/backend/src/test/java/com/woowacourse/acceptance/test/auth/AuthAcceptanceTest.java +++ b/backend/src/test/java/com/woowacourse/acceptance/test/auth/AuthAcceptanceTest.java @@ -28,7 +28,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; -public class AuthAcceptanceTest extends AcceptanceTest { +class AuthAcceptanceTest extends AcceptanceTest { @Autowired private TokenRepository tokenRepository; @@ -81,7 +81,7 @@ void refreshToken() { @DisplayName("로그아웃시에 쿠키를 제거해준다.") @Test - public void logout() { + void logout() { final String token = getBearerTokenBySignInOrUp(new GithubProfileResponse(4L, "verus", "https://image", "github.com")); final Token foundToken = tokenRepository.findByGithubId(4L).get(); diff --git a/backend/src/test/java/com/woowacourse/acceptance/test/cors/CorsAcceptanceTest.java b/backend/src/test/java/com/woowacourse/acceptance/test/cors/CorsAcceptanceTest.java index 76cac2cf9..bb37074c3 100644 --- a/backend/src/test/java/com/woowacourse/acceptance/test/cors/CorsAcceptanceTest.java +++ b/backend/src/test/java/com/woowacourse/acceptance/test/cors/CorsAcceptanceTest.java @@ -5,7 +5,7 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -public class CorsAcceptanceTest extends AcceptanceTest { +class CorsAcceptanceTest extends AcceptanceTest { @DisplayName("cors 적용 여부 확인") @Test diff --git a/backend/src/test/java/com/woowacourse/acceptance/test/member/MemberAcceptanceTest.java b/backend/src/test/java/com/woowacourse/acceptance/test/member/MemberAcceptanceTest.java index 74b2aeca9..a2384d642 100644 --- a/backend/src/test/java/com/woowacourse/acceptance/test/member/MemberAcceptanceTest.java +++ b/backend/src/test/java/com/woowacourse/acceptance/test/member/MemberAcceptanceTest.java @@ -17,7 +17,7 @@ import org.junit.jupiter.api.Test; import org.springframework.http.HttpStatus; -public class MemberAcceptanceTest extends AcceptanceTest { +class MemberAcceptanceTest extends AcceptanceTest { @Test void getCurrentMember() { diff --git a/backend/src/test/java/com/woowacourse/acceptance/test/referenceroom/ReferenceRoomAcceptanceTest.java b/backend/src/test/java/com/woowacourse/acceptance/test/referenceroom/ReferenceRoomAcceptanceTest.java index b578bf2a2..b81795a77 100644 --- a/backend/src/test/java/com/woowacourse/acceptance/test/referenceroom/ReferenceRoomAcceptanceTest.java +++ b/backend/src/test/java/com/woowacourse/acceptance/test/referenceroom/ReferenceRoomAcceptanceTest.java @@ -45,7 +45,7 @@ import org.springframework.restdocs.payload.JsonFieldType; @DisplayName("링크 모음 인수 테스트") -public class ReferenceRoomAcceptanceTest extends AcceptanceTest { +class ReferenceRoomAcceptanceTest extends AcceptanceTest { @DisplayName("참여한 스터디의 링크 공유실에 정상적으로 글을 작성한다.") @Test diff --git a/backend/src/test/java/com/woowacourse/acceptance/test/review/ReviewsAcceptanceTest.java b/backend/src/test/java/com/woowacourse/acceptance/test/review/ReviewsAcceptanceTest.java index dbf4cec3d..1b36569db 100644 --- a/backend/src/test/java/com/woowacourse/acceptance/test/review/ReviewsAcceptanceTest.java +++ b/backend/src/test/java/com/woowacourse/acceptance/test/review/ReviewsAcceptanceTest.java @@ -39,7 +39,7 @@ import org.springframework.http.MediaType; @DisplayName("리뷰 인수 테스트") -public class ReviewsAcceptanceTest extends AcceptanceTest { +class ReviewsAcceptanceTest extends AcceptanceTest { @DisplayName("리뷰를 작성한다.") @Test @@ -117,7 +117,7 @@ void getAllReviews() { @DisplayName("원하는 갯수만큼 스터디에 달린 리뷰 목록을 조회할 수 있다.") @Test - public void getReviewsBySize() { + void getReviewsBySize() { // arrange final LocalDate 지금 = LocalDate.now(); @@ -194,7 +194,7 @@ void deleteReview() { .extract().as(ReviewsResponse.class); assertThat(response.getReviews()).isEmpty(); - assertThat(response.getTotalCount()).isEqualTo(0); + assertThat(response.getTotalCount()).isZero(); } @DisplayName("자신이 참여한 스터디에 작성한 리뷰를 수정할 수 있다.") diff --git a/backend/src/test/java/com/woowacourse/acceptance/test/study/AutoCloseEnrollmentAcceptanceTest.java b/backend/src/test/java/com/woowacourse/acceptance/test/study/AutoCloseEnrollmentAcceptanceTest.java index c24322fd0..19668d511 100644 --- a/backend/src/test/java/com/woowacourse/acceptance/test/study/AutoCloseEnrollmentAcceptanceTest.java +++ b/backend/src/test/java/com/woowacourse/acceptance/test/study/AutoCloseEnrollmentAcceptanceTest.java @@ -1,7 +1,6 @@ package com.woowacourse.acceptance.test.study; import static org.assertj.core.api.Assertions.assertThat; -import static org.hamcrest.Matchers.contains; import com.woowacourse.acceptance.AcceptanceTest; import java.util.Set; @@ -14,7 +13,7 @@ import org.springframework.scheduling.config.Task; import org.springframework.scheduling.config.TriggerTask; -public class AutoCloseEnrollmentAcceptanceTest extends AcceptanceTest { +class AutoCloseEnrollmentAcceptanceTest extends AcceptanceTest { @Autowired private ScheduledAnnotationBeanPostProcessor processor; diff --git a/backend/src/test/java/com/woowacourse/acceptance/test/study/CreatingStudyAcceptanceTest.java b/backend/src/test/java/com/woowacourse/acceptance/test/study/CreatingStudyAcceptanceTest.java index db2959a0d..6bcd27700 100644 --- a/backend/src/test/java/com/woowacourse/acceptance/test/study/CreatingStudyAcceptanceTest.java +++ b/backend/src/test/java/com/woowacourse/acceptance/test/study/CreatingStudyAcceptanceTest.java @@ -8,8 +8,6 @@ import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; -import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; -import static org.springframework.restdocs.request.RequestDocumentation.requestParameters; import static org.springframework.restdocs.restassured3.RestAssuredRestDocumentation.document; import com.woowacourse.acceptance.AcceptanceTest; @@ -29,7 +27,7 @@ import org.junit.jupiter.params.provider.ValueSource; import org.springframework.http.HttpStatus; -public class CreatingStudyAcceptanceTest extends AcceptanceTest { +class CreatingStudyAcceptanceTest extends AcceptanceTest { @DisplayName("유효하지 않은 토큰으로 스터디 개설 시 401을 반환한다.") @ParameterizedTest diff --git a/backend/src/test/java/com/woowacourse/acceptance/test/study/GettingMyStudiesAcceptanceTest.java b/backend/src/test/java/com/woowacourse/acceptance/test/study/GettingMyStudiesAcceptanceTest.java index 0c6dbfb26..e9bac0a31 100644 --- a/backend/src/test/java/com/woowacourse/acceptance/test/study/GettingMyStudiesAcceptanceTest.java +++ b/backend/src/test/java/com/woowacourse/acceptance/test/study/GettingMyStudiesAcceptanceTest.java @@ -11,12 +11,8 @@ import static com.woowacourse.acceptance.fixture.StudyFixtures.리액트_스터디_제목; import static com.woowacourse.acceptance.fixture.StudyFixtures.자바_스터디_제목; import static com.woowacourse.acceptance.fixture.TagFixtures.BE_태그_ID; -import static com.woowacourse.acceptance.fixture.TagFixtures.BE_태그_설명; import static com.woowacourse.acceptance.fixture.TagFixtures.BE_태그명; -import static com.woowacourse.acceptance.fixture.TagFixtures.리액트_태그_ID; -import static com.woowacourse.acceptance.fixture.TagFixtures.리액트_태그_설명; import static com.woowacourse.acceptance.fixture.TagFixtures.자바_태그_ID; -import static com.woowacourse.acceptance.fixture.TagFixtures.자바_태그_설명; import static com.woowacourse.acceptance.fixture.TagFixtures.자바_태그명; import static com.woowacourse.acceptance.steps.LoginSteps.그린론이; import static com.woowacourse.acceptance.steps.LoginSteps.디우가; @@ -44,13 +40,12 @@ import io.restassured.RestAssured; import java.time.LocalDate; import java.util.List; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.http.HttpStatus; import org.springframework.restdocs.payload.JsonFieldType; -public class GettingMyStudiesAcceptanceTest extends AcceptanceTest { +class GettingMyStudiesAcceptanceTest extends AcceptanceTest { @DisplayName("내가 참여한 스터디를 조회한다.") @Test diff --git a/backend/src/test/java/com/woowacourse/acceptance/test/study/GettingStudiesSummaryAcceptanceTest.java b/backend/src/test/java/com/woowacourse/acceptance/test/study/GettingStudiesSummaryAcceptanceTest.java index 557319dea..3b41d2386 100644 --- a/backend/src/test/java/com/woowacourse/acceptance/test/study/GettingStudiesSummaryAcceptanceTest.java +++ b/backend/src/test/java/com/woowacourse/acceptance/test/study/GettingStudiesSummaryAcceptanceTest.java @@ -24,7 +24,7 @@ import org.springframework.http.HttpStatus; @DisplayName("스터디 목록 조회 인수 테스트") -public class GettingStudiesSummaryAcceptanceTest extends AcceptanceTest { +class GettingStudiesSummaryAcceptanceTest extends AcceptanceTest { private Long javaStudyId; private Long reactStudyId; @@ -76,7 +76,7 @@ void initDataBase() { @DisplayName("첫번째 페이지의 스터디 목록을 조회 한다.") @Test - public void getFirstPageOfStudies() { + void getFirstPageOfStudies() { final StudiesResponse studiesResponse = RestAssured.given(spec).log().all() .filter(document("studies/summary")) .queryParam("page", 0) @@ -104,7 +104,7 @@ public void getFirstPageOfStudies() { @DisplayName("마지막 페이지의 스터디 목록을 조회 한다.") @Test - public void getLastPageOfStudies() { + void getLastPageOfStudies() { final StudiesResponse studiesResponse = RestAssured.given(spec).log().all() .filter(document("studies/summary")) .queryParam("page", 1) @@ -133,7 +133,7 @@ public void getLastPageOfStudies() { @DisplayName("잘못된 페이징 정보로 목록을 조회시 400에러를 응답한다.") @ParameterizedTest @CsvSource({"-1,3", "1,0", "one,1", "1,one"}) - public void response400WhenRequestByInvalidPagingInfo(String page, String size) { + void response400WhenRequestByInvalidPagingInfo(String page, String size) { RestAssured.given(spec).log().all() .filter(document("studies/summary")) .queryParam("page", page) @@ -147,7 +147,7 @@ public void response400WhenRequestByInvalidPagingInfo(String page, String size) @DisplayName("페이지 정보 없이 목록 조회시 400에러를 응답한다.") @Test - public void getStudiesByDefaultPage() { + void getStudiesByDefaultPage() { RestAssured.given().log().all() .when().log().all() .get("/api/studies?size=5") @@ -158,7 +158,7 @@ public void getStudiesByDefaultPage() { @DisplayName("사이즈 정보 없이 목록 조회시 400에러를 응답한다.") @Test - public void getStudiesByDefaultSize() { + void getStudiesByDefaultSize() { RestAssured.given().log().all() .when().log().all() .get("/api/studies?page=0") @@ -169,7 +169,7 @@ public void getStudiesByDefaultSize() { @DisplayName("페이징 정보가 없는 경우에는 기본값을 사용해 스터디 목록을 조회한다.") @Test - public void getStudiesByDefaultPagingInfo() { + void getStudiesByDefaultPagingInfo() { final StudiesResponse studiesResponse = RestAssured.given().log().all() .when().log().all() .get("/api/studies") diff --git a/backend/src/test/java/com/woowacourse/acceptance/test/study/GettingStudyDetailsAcceptanceTest.java b/backend/src/test/java/com/woowacourse/acceptance/test/study/GettingStudyDetailsAcceptanceTest.java index de18993ee..66d642e72 100644 --- a/backend/src/test/java/com/woowacourse/acceptance/test/study/GettingStudyDetailsAcceptanceTest.java +++ b/backend/src/test/java/com/woowacourse/acceptance/test/study/GettingStudyDetailsAcceptanceTest.java @@ -43,11 +43,11 @@ import org.junit.jupiter.api.Test; import org.springframework.http.HttpStatus; -public class GettingStudyDetailsAcceptanceTest extends AcceptanceTest { +class GettingStudyDetailsAcceptanceTest extends AcceptanceTest { @DisplayName("스터디 요약 정보 외에 상세 정보를 포함하여 조회할 수 있다.") @Test - public void getStudyDetails() { + void getStudyDetails() { LocalDate 지금 = LocalDate.now(); long 리액트_스터디 = 디우가().로그인하고().리액트_스터디를() .시작일자는(지금).모집종료일자는(지금.plusDays(4)).종료일자는(지금.plusDays(10)) @@ -91,7 +91,7 @@ public void getStudyDetails() { @DisplayName("선택 데이터가 없는 스터디 세부사항을 조회한다.") @Test - public void getNotHasOptionalDataStudyDetails() { + void getNotHasOptionalDataStudyDetails() { LocalDate 지금 = LocalDate.now(); final long 알고리즘_스터디 = 베루스가().로그인하고().알고리즘_스터디를().시작일자는(지금).생성한다(); diff --git a/backend/src/test/java/com/woowacourse/acceptance/test/study/ParticipationStudyAcceptanceTest.java b/backend/src/test/java/com/woowacourse/acceptance/test/study/ParticipationStudyAcceptanceTest.java new file mode 100644 index 000000000..8334002d9 --- /dev/null +++ b/backend/src/test/java/com/woowacourse/acceptance/test/study/ParticipationStudyAcceptanceTest.java @@ -0,0 +1,36 @@ +package com.woowacourse.acceptance.test.study; + +import static com.woowacourse.acceptance.steps.LoginSteps.그린론이; +import static com.woowacourse.acceptance.steps.LoginSteps.디우가; +import static org.springframework.http.HttpHeaders.AUTHORIZATION; +import static org.springframework.http.HttpHeaders.CONTENT_TYPE; +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; +import static org.springframework.restdocs.restassured3.RestAssuredRestDocumentation.document; + +import com.woowacourse.acceptance.AcceptanceTest; +import io.restassured.RestAssured; +import java.time.LocalDate; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpStatus; + +class ParticipationStudyAcceptanceTest extends AcceptanceTest { + + @DisplayName("아직 스터디에 가입되지 않은 회원은 스터디에 참여가 가능하다.") + @Test + void participateStudy() { + LocalDate 지금 = LocalDate.now(); + long 자바_스터디_ID = 그린론이().로그인하고().자바_스터디를().시작일자는(지금).모집인원은(10).생성한다(); + String token = 디우가().로그인한다(); + + RestAssured.given(spec).log().all() + .filter(document("studies/participant")) + .header(CONTENT_TYPE, APPLICATION_JSON_VALUE) + .header(AUTHORIZATION, token) + .pathParam("study-id", 자바_스터디_ID) + .when().log().all() + .post("/api/studies/{study-id}/members") + .then().log().all() + .statusCode(HttpStatus.NO_CONTENT.value()); + } +} diff --git a/backend/src/test/java/com/woowacourse/acceptance/test/study/SearchingStudiesAcceptanceTest.java b/backend/src/test/java/com/woowacourse/acceptance/test/study/SearchingStudiesAcceptanceTest.java index ec9819552..c74daaf60 100644 --- a/backend/src/test/java/com/woowacourse/acceptance/test/study/SearchingStudiesAcceptanceTest.java +++ b/backend/src/test/java/com/woowacourse/acceptance/test/study/SearchingStudiesAcceptanceTest.java @@ -26,7 +26,7 @@ import org.springframework.http.HttpStatus; @DisplayName("키워드 검색 인수 테스트") -public class SearchingStudiesAcceptanceTest extends AcceptanceTest { +class SearchingStudiesAcceptanceTest extends AcceptanceTest { @BeforeEach void setUp() { @@ -60,7 +60,7 @@ void setUp() { @DisplayName("잘못된 페이징 정보로 목록을 검색시 400에러를 응답한다.") @ParameterizedTest @CsvSource({"-1,3", "1,0", "one,1", "1,one"}) - public void response400WhenRequestByInvalidPagingInfo(String page, String size) { + void response400WhenRequestByInvalidPagingInfo(String page, String size) { RestAssured.given().log().all() .queryParam("title", "java") .queryParam("page", page) @@ -74,7 +74,7 @@ public void response400WhenRequestByInvalidPagingInfo(String page, String size) @DisplayName("페이지 정보 없이 목록 검색시 400에러를 응답한다.") @Test - public void getStudiesByDefaultPage() { + void getStudiesByDefaultPage() { RestAssured.given().log().all() .queryParam("title", "java") .queryParam("size", 5) @@ -87,7 +87,7 @@ public void getStudiesByDefaultPage() { @DisplayName("사이즈 정보 없이 목록 조회시 400에러를 응답한다.") @Test - public void getStudiesByDefaultSize() { + void getStudiesByDefaultSize() { RestAssured.given().log().all() .queryParam("title", "java") .queryParam("page", 0) @@ -100,7 +100,7 @@ public void getStudiesByDefaultSize() { @DisplayName("페이징 정보 및 키워드가 없는 경우에는 기본페이징 정보를 사용해 전체 스터디 목록에서 조회한다.") @Test - public void getStudiesByDefaultPagingInfo() { + void getStudiesByDefaultPagingInfo() { RestAssured.given(spec).log().all() .filter(document("studies/search")) .when().log().all() @@ -162,7 +162,7 @@ void getStudiesByHasSpaceKeyword() { @DisplayName("필터로 필터링하여 스터디 목록을 조회한다.") @Test - public void getStudiesByFilter() { + void getStudiesByFilter() { RestAssured.given().log().all() .queryParam("title", "") .queryParam("area", 3) @@ -183,7 +183,7 @@ public void getStudiesByFilter() { @DisplayName("필터로 필터링한 내용과 제목 검색을 함께 조합해 스터디 목록을 조회한다.") @Test - public void getStudiesByFilterAndTitle() { + void getStudiesByFilterAndTitle() { RestAssured.given(spec).log().all() .filter(document("studies/searchWithTags")) .queryParam("title", "ja") @@ -205,7 +205,7 @@ public void getStudiesByFilterAndTitle() { @DisplayName("같은 카테고리의 필터로 필터링하여 스터디 목록을 조회한다.") @Test - public void getStudiesBySameCategoryFilter() { + void getStudiesBySameCategoryFilter() { RestAssured.given().log().all() .queryParam("title", "") .queryParam("area", 3) @@ -228,7 +228,7 @@ public void getStudiesBySameCategoryFilter() { @DisplayName("서로 다른 카테고리의 필터로 필터링하여 스터디 목록을 조회한다.") @Test - public void getStudiesByAnotherCategoryFilter() { + void getStudiesByAnotherCategoryFilter() { RestAssured.given().log().all() .queryParam("title", "") .queryParam("area", 3) diff --git a/backend/src/test/java/com/woowacourse/acceptance/test/studyroom/CommunityAcceptanceTest.java b/backend/src/test/java/com/woowacourse/acceptance/test/studyroom/CommunityAcceptanceTest.java index 978cc6baf..be0cded07 100644 --- a/backend/src/test/java/com/woowacourse/acceptance/test/studyroom/CommunityAcceptanceTest.java +++ b/backend/src/test/java/com/woowacourse/acceptance/test/studyroom/CommunityAcceptanceTest.java @@ -39,7 +39,7 @@ import org.springframework.http.MediaType; import org.springframework.restdocs.payload.JsonFieldType; -public class CommunityAcceptanceTest extends AcceptanceTest { +class CommunityAcceptanceTest extends AcceptanceTest { @DisplayName("커뮤니티에 글을 작성한다.") @Test diff --git a/backend/src/test/java/com/woowacourse/acceptance/test/tag/TagAcceptanceTest.java b/backend/src/test/java/com/woowacourse/acceptance/test/tag/TagAcceptanceTest.java index deb6b889c..f3aff2bfd 100644 --- a/backend/src/test/java/com/woowacourse/acceptance/test/tag/TagAcceptanceTest.java +++ b/backend/src/test/java/com/woowacourse/acceptance/test/tag/TagAcceptanceTest.java @@ -12,7 +12,7 @@ import org.junit.jupiter.api.Test; import org.springframework.http.HttpStatus; -public class TagAcceptanceTest extends AcceptanceTest { +class TagAcceptanceTest extends AcceptanceTest { @DisplayName("전체 태그 목록을 조회한다.") @Test diff --git a/backend/src/test/java/com/woowacourse/moamoa/auth/controller/AuthControllerTest.java b/backend/src/test/java/com/woowacourse/moamoa/auth/controller/AuthControllerTest.java index f4b14e128..1e74600df 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/auth/controller/AuthControllerTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/auth/controller/AuthControllerTest.java @@ -13,7 +13,7 @@ import org.junit.jupiter.api.Test; import org.springframework.boot.test.mock.mockito.MockBean; -public class AuthControllerTest extends WebMVCTest { +class AuthControllerTest extends WebMVCTest { @MockBean AuthService authService; diff --git a/backend/src/test/java/com/woowacourse/moamoa/auth/infrastructure/TokenProviderTest.java b/backend/src/test/java/com/woowacourse/moamoa/auth/infrastructure/TokenProviderTest.java index 4fed7e132..f2305ce31 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/auth/infrastructure/TokenProviderTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/auth/infrastructure/TokenProviderTest.java @@ -4,15 +4,12 @@ import com.woowacourse.moamoa.WebMVCTest; import com.woowacourse.moamoa.auth.config.AuthenticationExtractor; - import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.security.Keys; - import java.nio.charset.StandardCharsets; import java.util.Date; import javax.crypto.SecretKey; - import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.mock.web.MockHttpServletRequest; @@ -77,6 +74,6 @@ void validateJwtTokenFormat() { final String[] parts = token.split("\\."); - assertThat(parts.length).isEqualTo(3); + assertThat(parts).hasSize(3); } } diff --git a/backend/src/test/java/com/woowacourse/moamoa/auth/service/AuthServiceTest.java b/backend/src/test/java/com/woowacourse/moamoa/auth/service/AuthServiceTest.java index 28106b3b5..13914777f 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/auth/service/AuthServiceTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/auth/service/AuthServiceTest.java @@ -61,7 +61,7 @@ void setUp() { @DisplayName("RefreshToken 을 저장한다.") @Test - public void saveRefreshToken() { + void saveRefreshToken() { authService.createToken("authorization-code"); final Token token = tokenRepository.findByGithubId(1L).get(); @@ -70,7 +70,7 @@ public void saveRefreshToken() { @DisplayName("RefreshToken 을 이용하여 AccessToken 을 업데이트한다.") @Test - public void updateRefreshToken() { + void updateRefreshToken() { authService.createToken("authorization-code"); final Token token = tokenRepository.findByGithubId(1L).get(); final String refreshToken = token.getRefreshToken(); @@ -82,14 +82,14 @@ public void updateRefreshToken() { @DisplayName("DB에 저장되어 있지 않은 refresh token으로 access token을 발급받을 수 없다.") @Test - public void validateRefreshToken() { + void validateRefreshToken() { assertThatThrownBy(() -> authService.refreshToken(1L, "InvalidRefreshToken")) .isInstanceOf(UnauthorizedException.class); } @DisplayName("refresh token을 통해 access token을 발급받을 수 있다.") @Test - public void recreationAccessToken() { + void recreationAccessToken() { authService.createToken("authorization-code"); final Token token = tokenRepository.findByGithubId(1L).get(); @@ -98,7 +98,7 @@ public void recreationAccessToken() { @DisplayName("로그아웃을 하면 Token 을 제거한다.") @Test - public void logout() { + void logout() { authService.createToken("authorization-code"); final Token token = tokenRepository.findByGithubId(1L).get(); @@ -107,6 +107,6 @@ public void logout() { final Optional foundToken = tokenRepository.findByGithubId(token.getGithubId()); assertThat(token).isNotNull(); - assertThat(foundToken.isEmpty()).isTrue(); + assertThat(foundToken).isEmpty(); } } diff --git a/backend/src/test/java/com/woowacourse/moamoa/auth/service/OAuthClientTest.java b/backend/src/test/java/com/woowacourse/moamoa/auth/service/OAuthClientTest.java index a5280ca3a..5d2f0e763 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/auth/service/OAuthClientTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/auth/service/OAuthClientTest.java @@ -61,7 +61,7 @@ void getAccessToken() throws JsonProcessingException { mockServer.verify(); - assertThat(accessTokenResponse.get("access_token")).isEqualTo(accessToken); + assertThat(accessTokenResponse).containsEntry("access_token", accessToken); } @DisplayName("token을 받아서 사용자 프로필을 조회한다.") diff --git a/backend/src/test/java/com/woowacourse/moamoa/member/controller/MemberControllerTest.java b/backend/src/test/java/com/woowacourse/moamoa/member/controller/MemberControllerTest.java index c344eabdc..85f864e42 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/member/controller/MemberControllerTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/member/controller/MemberControllerTest.java @@ -17,7 +17,7 @@ import org.springframework.http.ResponseEntity; @RepositoryTest -public class MemberControllerTest { +class MemberControllerTest { @Autowired MemberDao memberDao; diff --git a/backend/src/test/java/com/woowacourse/moamoa/member/domain/repository/MemberRepositoryTest.java b/backend/src/test/java/com/woowacourse/moamoa/member/domain/repository/MemberRepositoryTest.java index 34659c744..76c9ffe58 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/member/domain/repository/MemberRepositoryTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/member/domain/repository/MemberRepositoryTest.java @@ -76,7 +76,7 @@ void findByNotExistMember() { @DisplayName("member id들로 각각 일치하는 Member들을 찾은 후 반환한다.") @Test - public void findAllById() { + void findAllById() { final Long memberId1 = memberRepository.findByGithubId(1L).get().getId(); final Long memberId2 = memberRepository.findByGithubId(2L).get().getId(); diff --git a/backend/src/test/java/com/woowacourse/moamoa/member/webmvc/MemberWebMvcTest.java b/backend/src/test/java/com/woowacourse/moamoa/member/webmvc/MemberWebMvcTest.java index 7debac1c1..9a041f7e6 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/member/webmvc/MemberWebMvcTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/member/webmvc/MemberWebMvcTest.java @@ -6,19 +6,17 @@ import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import com.woowacourse.moamoa.WebMVCTest; import com.woowacourse.moamoa.member.service.MemberService; +import com.woowacourse.moamoa.member.service.exception.MemberNotFoundException; import org.apache.http.HttpHeaders; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; - -import com.woowacourse.moamoa.WebMVCTest; -import com.woowacourse.moamoa.member.service.exception.MemberNotFoundException; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.MockBean; -public class MemberWebMvcTest extends WebMVCTest { +class MemberWebMvcTest extends WebMVCTest { @MockBean MemberService memberService; diff --git a/backend/src/test/java/com/woowacourse/moamoa/referenceroom/controller/ReferenceRoomControllerTest.java b/backend/src/test/java/com/woowacourse/moamoa/referenceroom/controller/ReferenceRoomControllerTest.java index e5bf4d526..d9f760e07 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/referenceroom/controller/ReferenceRoomControllerTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/referenceroom/controller/ReferenceRoomControllerTest.java @@ -30,7 +30,7 @@ import org.springframework.beans.factory.annotation.Autowired; @RepositoryTest -public class ReferenceRoomControllerTest { +class ReferenceRoomControllerTest { @Autowired private MemberRepository memberRepository; diff --git a/backend/src/test/java/com/woowacourse/moamoa/referenceroom/controller/SearchingReferenceRoomControllerTest.java b/backend/src/test/java/com/woowacourse/moamoa/referenceroom/controller/SearchingReferenceRoomControllerTest.java index fce895e5e..7911f7b87 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/referenceroom/controller/SearchingReferenceRoomControllerTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/referenceroom/controller/SearchingReferenceRoomControllerTest.java @@ -50,7 +50,7 @@ import org.springframework.http.ResponseEntity; @RepositoryTest -public class SearchingReferenceRoomControllerTest { +class SearchingReferenceRoomControllerTest { @Autowired private LinkDao linkDao; @@ -148,7 +148,10 @@ void getLinks() { @DisplayName("스터디에 참여하지 않은 회원은 링크 공유글을 조회할 수 없다.") @Test void getLinksByNotParticipatedMember() { - assertThatThrownBy(() -> sut.getLinks(병민_깃허브_아이디, javaStudy.getId(), PageRequest.of(0, 5))) + final Long javaStudyId = javaStudy.getId(); + final PageRequest pageRequest = PageRequest.of(0, 5); + + assertThatThrownBy(() -> sut.getLinks(병민_깃허브_아이디, javaStudyId, pageRequest)) .isInstanceOf(NotParticipatedMemberException.class); } } diff --git a/backend/src/test/java/com/woowacourse/moamoa/referenceroom/domain/LinkTest.java b/backend/src/test/java/com/woowacourse/moamoa/referenceroom/domain/LinkTest.java index 8130e0438..103643af2 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/referenceroom/domain/LinkTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/referenceroom/domain/LinkTest.java @@ -10,7 +10,7 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -public class LinkTest { +class LinkTest { @DisplayName("링크 공유를 수정한다.") @Test @@ -68,8 +68,9 @@ void delete() { void deleteByNotAuthor() { final Link link = new Link(new AssociatedStudy(1L), new Author(1L), "link", "설명"); final Author nonAuthor = new Author(2L); + final AssociatedStudy associatedStudy = new AssociatedStudy(1L); - assertThatThrownBy(() -> link.delete(new AssociatedStudy(1L), nonAuthor)) + assertThatThrownBy(() -> link.delete(associatedStudy, nonAuthor)) .isInstanceOf(NotLinkAuthorException.class); } @@ -78,8 +79,9 @@ void deleteByNotAuthor() { void deleteByNotBelongToStudy() { final Link link = new Link(new AssociatedStudy(1L), new Author(1L), "link", "설명"); final AssociatedStudy unrelatedStudy = new AssociatedStudy(2L); + final Author author = new Author(1L); - assertThatThrownBy(() -> link.delete(unrelatedStudy, new Author(1L))) + assertThatThrownBy(() -> link.delete(unrelatedStudy, author)) .isInstanceOf(NotRelatedLinkException.class); } } diff --git a/backend/src/test/java/com/woowacourse/moamoa/referenceroom/query/LinkDaoTest.java b/backend/src/test/java/com/woowacourse/moamoa/referenceroom/query/LinkDaoTest.java index f148c4421..37d3c8468 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/referenceroom/query/LinkDaoTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/referenceroom/query/LinkDaoTest.java @@ -30,7 +30,7 @@ import org.springframework.data.domain.Slice; @RepositoryTest -public class LinkDaoTest { +class LinkDaoTest { private static final MemberData JJANGGU = new MemberData(1L, "jjanggu", "https://image", "github.com"); private static final MemberData GREENLAWN = new MemberData(2L, "greenlawn", "https://image", "github.com"); diff --git a/backend/src/test/java/com/woowacourse/moamoa/referenceroom/webmvc/ReferenceRoomWebMvcTest.java b/backend/src/test/java/com/woowacourse/moamoa/referenceroom/webmvc/ReferenceRoomWebMvcTest.java index 8386a42e4..3e7e9a45f 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/referenceroom/webmvc/ReferenceRoomWebMvcTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/referenceroom/webmvc/ReferenceRoomWebMvcTest.java @@ -12,7 +12,7 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; -public class ReferenceRoomWebMvcTest extends WebMVCTest { +class ReferenceRoomWebMvcTest extends WebMVCTest { @DisplayName("필수 데이터인 링크 URL이 null인 경우 400을 반환한다.") @Test diff --git a/backend/src/test/java/com/woowacourse/moamoa/review/controller/ReviewControllerTest.java b/backend/src/test/java/com/woowacourse/moamoa/review/controller/ReviewControllerTest.java index f41cd5ac2..066ec6fc6 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/review/controller/ReviewControllerTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/review/controller/ReviewControllerTest.java @@ -31,7 +31,7 @@ import org.springframework.beans.factory.annotation.Autowired; @RepositoryTest -public class ReviewControllerTest { +class ReviewControllerTest { @Autowired private MemberRepository memberRepository; diff --git a/backend/src/test/java/com/woowacourse/moamoa/review/controller/SearchingReviewControllerTest.java b/backend/src/test/java/com/woowacourse/moamoa/review/controller/SearchingReviewControllerTest.java index 41f521d01..89aa4b7a9 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/review/controller/SearchingReviewControllerTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/review/controller/SearchingReviewControllerTest.java @@ -117,7 +117,7 @@ void setUp() { @DisplayName("스터디의 전체 후기를 조회할 수 있다.") @Test - public void getAllReviews() { + void getAllReviews() { final ResponseEntity reviewsResponse = sut.getReviews(javaStudy.getId(), SizeRequest.empty()); assertThat(reviewsResponse.getStatusCode()).isEqualTo(HttpStatus.OK); @@ -128,7 +128,7 @@ public void getAllReviews() { @DisplayName("원하는 갯수 만큼 스터디의 후기를 조회할 수 있다.") @Test - public void getReviewsByStudy() { + void getReviewsByStudy() { final ResponseEntity reviewsResponse = sut.getReviews(javaStudy.getId(), new SizeRequest(2)); assertThat(reviewsResponse.getStatusCode()).isEqualTo(HttpStatus.OK); diff --git a/backend/src/test/java/com/woowacourse/moamoa/review/domain/ReviewTest.java b/backend/src/test/java/com/woowacourse/moamoa/review/domain/ReviewTest.java index 1ca77ead7..57989eae4 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/review/domain/ReviewTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/review/domain/ReviewTest.java @@ -7,7 +7,7 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -public class ReviewTest { +class ReviewTest { private final Long writtenMemberId = 1L; private final Long unwrittenMemberId = 2L; @@ -28,8 +28,9 @@ void updateReviewContent() { void updateReviewException() { final Review review = new Review(new AssociatedStudy(1L), new Reviewer(writtenMemberId), "content"); final String updatedContent = "update content"; + final Reviewer reviewer = new Reviewer(unwrittenMemberId); - assertThatThrownBy(() -> review.updateContent(new Reviewer(unwrittenMemberId), updatedContent)) + assertThatThrownBy(() -> review.updateContent(reviewer, updatedContent)) .isInstanceOf(UnwrittenReviewException.class); } @@ -47,8 +48,9 @@ void deleteReview() { @Test void deleteReviewException() { final Review review = new Review(new AssociatedStudy(1L), new Reviewer(writtenMemberId), "content"); + final Reviewer reviewer = new Reviewer(unwrittenMemberId); - assertThatThrownBy(() -> review.delete(new Reviewer(unwrittenMemberId))) + assertThatThrownBy(() -> review.delete(reviewer)) .isInstanceOf(UnwrittenReviewException.class); } } diff --git a/backend/src/test/java/com/woowacourse/moamoa/review/webmvc/UnauthorizedReviewWebMvcTest.java b/backend/src/test/java/com/woowacourse/moamoa/review/webmvc/UnauthorizedReviewWebMvcTest.java index 93cf269df..8dd3f3e3f 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/review/webmvc/UnauthorizedReviewWebMvcTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/review/webmvc/UnauthorizedReviewWebMvcTest.java @@ -12,7 +12,7 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; -public class UnauthorizedReviewWebMvcTest extends WebMVCTest { +class UnauthorizedReviewWebMvcTest extends WebMVCTest { @DisplayName("유효하지 않은 토큰으로 리뷰 작성하려는 경우 401 에러을 반환한다.") @Test diff --git a/backend/src/test/java/com/woowacourse/moamoa/study/controller/MyStudyControllerTest.java b/backend/src/test/java/com/woowacourse/moamoa/study/controller/MyStudyControllerTest.java index 6e39e9fc2..4859f7223 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/study/controller/MyStudyControllerTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/study/controller/MyStudyControllerTest.java @@ -193,7 +193,7 @@ void getMyStudies() { ); - assertThat(tags.get(3).size()).isZero(); - assertThat(tags.get(4).size()).isZero(); + assertThat(tags.get(3)).isEmpty(); + assertThat(tags.get(4)).isEmpty(); } } diff --git a/backend/src/test/java/com/woowacourse/moamoa/study/controller/SearchingStudyControllerTest.java b/backend/src/test/java/com/woowacourse/moamoa/study/controller/SearchingStudyControllerTest.java index 6b2caa5b3..a9ca47568 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/study/controller/SearchingStudyControllerTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/study/controller/SearchingStudyControllerTest.java @@ -40,7 +40,7 @@ import org.springframework.http.ResponseEntity; @RepositoryTest -public class SearchingStudyControllerTest { +class SearchingStudyControllerTest { private SearchingStudyController sut; @@ -131,7 +131,7 @@ void setUp() { @DisplayName("페이징 정보로 스터디 목록 조회") @Test - public void getStudies() { + void getStudies() { ResponseEntity response = sut.getStudies(PageRequest.of(0, 3)); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); @@ -212,7 +212,7 @@ void searchByDifferentKindFilters() { assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); assertThat(response.getBody()).isNotNull(); assertThat(response.getBody().isHasNext()).isFalse(); - assertThat(response.getBody().getStudies()).hasSize(0); + assertThat(response.getBody().getStudies()).isEmpty(); } @DisplayName("같은 종류의 필터들은 OR 조건으로 스터디 목록을 조회") @@ -238,7 +238,7 @@ void searchBySameAndDifferentKindFilters() { @DisplayName("스터디 상세 정보를 조회할 수 있다.") @Test - public void getStudyDetails() { + void getStudyDetails() { StudyDetailsData expect = StudyDetailsData.builder() // Study Content .id(javaStudyId).title("Java 스터디").excerpt("자바 설명").thumbnail("java thumbnail") @@ -273,7 +273,7 @@ public void getStudyDetails() { @DisplayName("선택적으로 입력 가능한 정보를 포함한 스터디 상세 정보를 조회할 수 있다.") @Test - public void getStudyDetailsWithOptional() { + void getStudyDetailsWithOptional() { final StudyDetailsData expect = StudyDetailsData.builder() // Study Content .id(reactStudyId).title("React 스터디").excerpt("리액트 설명").thumbnail("react thumbnail") diff --git a/backend/src/test/java/com/woowacourse/moamoa/study/domain/StudyPlannerTest.java b/backend/src/test/java/com/woowacourse/moamoa/study/domain/StudyPlannerTest.java index 8eb2ab793..49ee26e12 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/study/domain/StudyPlannerTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/study/domain/StudyPlannerTest.java @@ -7,7 +7,7 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -public class StudyPlannerTest { +class StudyPlannerTest { @DisplayName("시작일자는 종료일자보다 클 수 없다.") @Test diff --git a/backend/src/test/java/com/woowacourse/moamoa/study/domain/StudyTest.java b/backend/src/test/java/com/woowacourse/moamoa/study/domain/StudyTest.java index ba891b05c..19e8937ed 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/study/domain/StudyTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/study/domain/StudyTest.java @@ -28,7 +28,7 @@ import org.junit.jupiter.params.provider.CsvSource; import org.junit.jupiter.params.provider.MethodSource; -public class StudyTest { +class StudyTest { @DisplayName("생성일자는 스터디 시작일자보다 클 수 없다.") @Test @@ -45,8 +45,10 @@ void createdAtMustBeforeStartDate() { final RecruitPlanner recruitPlanner = new RecruitPlanner(10, RECRUITMENT_START, enrollmentEndDate); final StudyPlanner studyPlanner = new StudyPlanner(startDate, endDate, IN_PROGRESS); + final AttachedTags emptyAttachedTags = AttachedTags.empty(); + assertThatThrownBy(() -> - new Study(content, participants, recruitPlanner, studyPlanner, AttachedTags.empty(), createdAt) + new Study(content, participants, recruitPlanner, studyPlanner, emptyAttachedTags, createdAt) ).isInstanceOf(InvalidPeriodException.class); } @@ -65,8 +67,10 @@ void createdAtMustBeforeEnrollmentEndDate() { final RecruitPlanner recruitPlanner = new RecruitPlanner(10, RECRUITMENT_START, enrollmentEndDate); final StudyPlanner studyPlanner = new StudyPlanner(startDate, endDate, PREPARE); + final AttachedTags emptyAttachedTags = AttachedTags.empty(); + assertThatThrownBy(() -> - new Study(content, participants, recruitPlanner, studyPlanner, AttachedTags.empty(), createdAt) + new Study(content, participants, recruitPlanner, studyPlanner, emptyAttachedTags, createdAt) ).isInstanceOf(InvalidPeriodException.class); } @@ -105,14 +109,16 @@ void enrollmentEndDateIsBeforeEndDate() { final RecruitPlanner recruitPlanner = new RecruitPlanner(10, RECRUITMENT_START, enrollmentEndDate); final StudyPlanner studyPlanner = new StudyPlanner(startDate, endDate, IN_PROGRESS); + final AttachedTags emptyAttachedTags = AttachedTags.empty(); + assertThatCode(() -> - new Study(content, participants, recruitPlanner, studyPlanner, AttachedTags.empty(), createdAt) + new Study(content, participants, recruitPlanner, studyPlanner, emptyAttachedTags, createdAt) ).isInstanceOf(InvalidPeriodException.class); } @DisplayName("새로운 사용자는 스터디에 가입할 수 있다.") @Test - public void participate() { + void participate() { final Content content = new Content("title", "excerpt", "thumbnail", "description"); final Participants participants = Participants.createBy(1L); final RecruitPlanner recruitPlanner = new RecruitPlanner(10, RECRUITMENT_START, LocalDate.now()); @@ -127,7 +133,7 @@ public void participate() { @DisplayName("기존 참여자는 스터디에 가입할 수 없다.") @Test - public void participateTwice() { + void participateTwice() { final Content content = new Content("title", "excerpt", "thumbnail", "description"); final Participants participants = Participants.createBy(1L); final RecruitPlanner recruitPlanner = new RecruitPlanner(10, RECRUITMENT_START, LocalDate.now()); @@ -171,7 +177,7 @@ void participateFullOfMemberStudy() { @DisplayName("마지막 인원이 참여시 스터디 모집은 종료된다.") @Test - public void closeStudyByLastParticipant() { + void closeStudyByLastParticipant() { final Content content = new Content("title", "excerpt", "thumbnail", "description"); final Participants participants = Participants.createBy(1L); final RecruitPlanner recruitPlanner = new RecruitPlanner(2, RECRUITMENT_START, LocalDate.now()); @@ -280,7 +286,7 @@ void getMyRoleInStudy(Long memberId, MemberRole role) { @DisplayName("스터디 종료기간이 넘으면 자동으로 종료 상태가 된다.") @Test - public void autoCloseStudyStatus() { + void autoCloseStudyStatus() { // given final Content content = new Content("title", "excerpt", "thumbnail", "description"); final Participants participants = Participants.createBy(1L); @@ -299,7 +305,7 @@ public void autoCloseStudyStatus() { @DisplayName("스터디 시작기간(StartDate)이 되면 자동으로 진행중 상태가 된다.") @Test - public void updateInProgressStatus() { + void updateInProgressStatus() { // given final Content content = new Content("title", "excerpt", "thumbnail", "description"); final Participants participants = Participants.createBy(1L); @@ -317,7 +323,7 @@ public void updateInProgressStatus() { @DisplayName("모집 기간이 지난 스터디는 자동으로 모집이 종료된다.") @Test - public void autoCloseEnrollment() { + void autoCloseEnrollment() { // given final Content content = new Content("title", "excerpt", "thumbnail", "description"); final Participants participants = Participants.createBy(1L); diff --git a/backend/src/test/java/com/woowacourse/moamoa/study/query/MyStudyDaoTest.java b/backend/src/test/java/com/woowacourse/moamoa/study/query/MyStudyDaoTest.java index 8887bae55..e31b52f6e 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/study/query/MyStudyDaoTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/study/query/MyStudyDaoTest.java @@ -126,7 +126,7 @@ void getMyStudies() { void findStudyOwnersByEmptyStudyId() { final Map owners = myStudyDao.findOwners(List.of()); - assertThat(owners.size()).isZero(); + assertThat(owners).isEmpty(); } @DisplayName("스터디 ID가 비어있을 경우, 스터디 태그 빈 맵을 반환한다.") @@ -134,6 +134,6 @@ void findStudyOwnersByEmptyStudyId() { void findStudyTagsByEmptyStudyId() { final Map> tags = myStudyDao.findTags(List.of()); - assertThat(tags.size()).isZero(); + assertThat(tags).isEmpty(); } } diff --git a/backend/src/test/java/com/woowacourse/moamoa/study/query/StudySummaryDaoTest.java b/backend/src/test/java/com/woowacourse/moamoa/study/query/StudySummaryDaoTest.java index 47cdfcb84..2d00fd9cd 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/study/query/StudySummaryDaoTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/study/query/StudySummaryDaoTest.java @@ -39,7 +39,7 @@ import org.springframework.data.domain.Slice; @RepositoryTest -public class StudySummaryDaoTest { +class StudySummaryDaoTest { @Autowired private MemberRepository memberRepository; @@ -132,7 +132,7 @@ void initDataBase() { @DisplayName("페이징 정보를 사용해 스터디 목록 조회") @ParameterizedTest @MethodSource("providePageableAndExpect") - public void findAllByPageable(Pageable pageable, List expectedTuples, boolean expectedHasNext) { + void findAllByPageable(Pageable pageable, List expectedTuples, boolean expectedHasNext) { final Slice response = studySummaryDao.searchBy("", SearchingTags.emptyTags(), pageable); assertThat(response.hasNext()).isEqualTo(expectedHasNext); @@ -162,7 +162,7 @@ private static Stream providePageableAndExpect() { @DisplayName("키워드와 함께 페이징 정보를 사용해 스터디 목록 조회") @Test - public void findByTitleContaining() { + void findByTitleContaining() { final Slice response = studySummaryDao .searchBy("java", SearchingTags.emptyTags(), PageRequest.of(0, 3)); @@ -179,7 +179,7 @@ public void findByTitleContaining() { @DisplayName("빈 키워드와 함께 페이징 정보를 사용해 스터디 목록 조회") @Test - public void findByBlankTitle() { + void findByBlankTitle() { final Slice response = studySummaryDao.searchBy("", SearchingTags.emptyTags(), PageRequest.of(0, 5)); diff --git a/backend/src/test/java/com/woowacourse/moamoa/study/service/MyStudyServiceTest.java b/backend/src/test/java/com/woowacourse/moamoa/study/service/MyStudyServiceTest.java index 8eb3a890b..744b572f4 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/study/service/MyStudyServiceTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/study/service/MyStudyServiceTest.java @@ -182,8 +182,8 @@ void findMyStudiesWithoutTags() { tuple(디우.getGithubId(), 디우.getUsername(), 디우.getImageUrl(), 디우.getProfileUrl()) ); - assertThat(tags.get(4).size()).isZero(); - assertThat(tags.get(5).size()).isZero(); + assertThat(tags.get(4)).isEmpty(); + assertThat(tags.get(5)).isEmpty(); } @DisplayName("존재하지 않은 내가 참여한 스터디 조회 시 예외 발생") diff --git a/backend/src/test/java/com/woowacourse/moamoa/study/webmvc/BadRequestMyMemberRoleWebMvcTest.java b/backend/src/test/java/com/woowacourse/moamoa/study/webmvc/BadRequestMyMemberRoleWebMvcTest.java index c3e25b2b2..e659117bc 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/study/webmvc/BadRequestMyMemberRoleWebMvcTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/study/webmvc/BadRequestMyMemberRoleWebMvcTest.java @@ -11,7 +11,7 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; -public class BadRequestMyMemberRoleWebMvcTest extends WebMVCTest { +class BadRequestMyMemberRoleWebMvcTest extends WebMVCTest { @DisplayName("study Id가 없을 경우 400 에러가 발생한다. ") @Test diff --git a/backend/src/test/java/com/woowacourse/moamoa/studyroom/controller/ArticleControllerTest.java b/backend/src/test/java/com/woowacourse/moamoa/studyroom/controller/ArticleControllerTest.java index a1777fbac..5ca721134 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/studyroom/controller/ArticleControllerTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/studyroom/controller/ArticleControllerTest.java @@ -33,7 +33,7 @@ import org.springframework.http.ResponseEntity; @RepositoryTest -public class ArticleControllerTest { +class ArticleControllerTest { StudyRequestBuilder javaStudyRequest = new StudyRequestBuilder() .title("java 스터디").excerpt("자바 설명").thumbnail("java image").description("자바 소개"); @@ -124,9 +124,12 @@ void throwExceptionWhenWriteToNotFoundStudy() { // arrange Member member = memberRepository.save(new Member(1L, "username", "imageUrl", "profileUrl")); + final Long memberId = member.getId(); + final ArticleRequest articleRequest = new ArticleRequest("제목", "내용"); + // act & assert - assertThatThrownBy(() -> sut.createArticle(member.getId(), 1L, COMMUNITY, - new ArticleRequest("제목", "내용") + assertThatThrownBy(() -> sut.createArticle(memberId, 1L, COMMUNITY, + articleRequest )) .isInstanceOf(StudyNotFoundException.class); } diff --git a/backend/src/test/java/com/woowacourse/moamoa/studyroom/controller/DeletingArticleControllerTest.java b/backend/src/test/java/com/woowacourse/moamoa/studyroom/controller/DeletingArticleControllerTest.java index aa64819a5..b64397467 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/studyroom/controller/DeletingArticleControllerTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/studyroom/controller/DeletingArticleControllerTest.java @@ -28,7 +28,7 @@ import org.springframework.beans.factory.annotation.Autowired; @RepositoryTest -public class DeletingArticleControllerTest { +class DeletingArticleControllerTest { StudyRequestBuilder javaStudyRequest = new StudyRequestBuilder() .title("java 스터디").excerpt("자바 설명").thumbnail("java image").description("자바 소개"); @@ -88,8 +88,11 @@ void throwExceptionWhenGettingToNotFoundArticle() { Study study = studyService .createStudy(member.getGithubId(), javaStudyRequest.startDate(LocalDate.now()).build()); + final Long memberId = member.getId(); + final Long studyId = study.getId(); + // act & assert - assertThatThrownBy(() -> sut.deleteArticle(member.getId(), study.getId(), 1L, COMMUNITY)) + assertThatThrownBy(() -> sut.deleteArticle(memberId, studyId, 1L, COMMUNITY)) .isInstanceOf(ArticleNotFoundException.class); } @@ -106,8 +109,12 @@ void throwExceptionWhenDeletingByNotParticipant() { ArticleRequest request = new ArticleRequest("게시글 제목", "게시글 내용"); Article article = articleService.createArticle(member.getId(), study.getId(), request, COMMUNITY); + final Long otherId = other.getId(); + final Long studyId = study.getId(); + final Long articleId = article.getId(); + // act & assert - assertThatThrownBy(() -> sut.deleteArticle(other.getId(), study.getId(), article.getId(), COMMUNITY)) + assertThatThrownBy(() -> sut.deleteArticle(otherId, studyId, articleId, COMMUNITY)) .isInstanceOf(UneditableArticleException.class); } } diff --git a/backend/src/test/java/com/woowacourse/moamoa/studyroom/controller/GettingArticleControllerTest.java b/backend/src/test/java/com/woowacourse/moamoa/studyroom/controller/GettingArticleControllerTest.java index 2ef1df752..e40c53ebd 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/studyroom/controller/GettingArticleControllerTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/studyroom/controller/GettingArticleControllerTest.java @@ -1,5 +1,6 @@ package com.woowacourse.moamoa.studyroom.controller; +import static com.woowacourse.moamoa.studyroom.domain.ArticleType.*; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -31,7 +32,7 @@ import org.springframework.http.ResponseEntity; @RepositoryTest -public class GettingArticleControllerTest { +class GettingArticleControllerTest { StudyRequestBuilder javaStudyRequest = new StudyRequestBuilder() .title("java 스터디").excerpt("자바 설명").thumbnail("java image").description("자바 소개"); @@ -71,11 +72,11 @@ void getStudyCommunityArticle() { .createStudy(member.getGithubId(), javaStudyRequest.startDate(LocalDate.now()).build()); ArticleRequest request = new ArticleRequest("게시글 제목", "게시글 내용"); - Article article = articleService.createArticle(member.getId(), study.getId(), request, ArticleType.COMMUNITY); + Article article = articleService.createArticle(member.getId(), study.getId(), request, COMMUNITY); //act final ResponseEntity response = sut.getArticle(member.getId(), study.getId(), - ArticleType.COMMUNITY, article.getId()); + COMMUNITY, article.getId()); //assert final AuthorResponse expectedAuthorResponse = new AuthorResponse( @@ -95,8 +96,11 @@ void throwExceptionWhenGettingToNotFoundArticle() { Study study = studyService .createStudy(member.getGithubId(), javaStudyRequest.startDate(LocalDate.now()).build()); + final Long memberId = member.getId(); + final Long studyId = study.getId(); + // act & assert - assertThatThrownBy(() -> sut.getArticle(member.getId(), study.getId(), ArticleType.COMMUNITY, 1L)) + assertThatThrownBy(() -> sut.getArticle(memberId, studyId, COMMUNITY, 1L)) .isInstanceOf(ArticleNotFoundException.class); } @@ -111,10 +115,14 @@ void throwExceptionWhenGettingByNotParticipant() { .createStudy(member.getGithubId(), javaStudyRequest.startDate(LocalDate.now()).build()); ArticleRequest request = new ArticleRequest("게시글 제목", "게시글 내용"); - Article article = articleService.createArticle(member.getId(), study.getId(), request, ArticleType.COMMUNITY); + Article article = articleService.createArticle(member.getId(), study.getId(), request, COMMUNITY); + + final Long otherId = other.getId(); + final Long studyId = study.getId(); + final Long articleId = article.getId(); // act & assert - assertThatThrownBy(() -> sut.getArticle(other.getId(), study.getId(), ArticleType.COMMUNITY, article.getId())) + assertThatThrownBy(() -> sut.getArticle(otherId, studyId, COMMUNITY, articleId)) .isInstanceOf(UnviewableArticleException.class); } } diff --git a/backend/src/test/java/com/woowacourse/moamoa/studyroom/controller/GettingCommunityArticleSummariesControllerTest.java b/backend/src/test/java/com/woowacourse/moamoa/studyroom/controller/GettingCommunityArticleSummariesControllerTest.java index 71310a133..22bdb98a5 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/studyroom/controller/GettingCommunityArticleSummariesControllerTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/studyroom/controller/GettingCommunityArticleSummariesControllerTest.java @@ -1,5 +1,6 @@ package com.woowacourse.moamoa.studyroom.controller; +import static com.woowacourse.moamoa.studyroom.domain.ArticleType.*; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -34,7 +35,7 @@ import org.springframework.http.ResponseEntity; @RepositoryTest -public class GettingCommunityArticleSummariesControllerTest { +class GettingCommunityArticleSummariesControllerTest { StudyRequestBuilder javaStudyRequest = new StudyRequestBuilder() .title("java 스터디").excerpt("자바 설명").thumbnail("java image").description("자바 소개"); @@ -77,19 +78,19 @@ void getCommunityArticles() { Study study = studyService.createStudy(그린론.getGithubId(), javaStudyRequest.startDate(LocalDate.now()).build()); articleService - .createArticle(그린론.getId(), study.getId(), new ArticleRequest("제목1", "내용1"), ArticleType.COMMUNITY); + .createArticle(그린론.getId(), study.getId(), new ArticleRequest("제목1", "내용1"), COMMUNITY); articleService - .createArticle(그린론.getId(), study.getId(), new ArticleRequest("제목2", "내용2"), ArticleType.COMMUNITY); + .createArticle(그린론.getId(), study.getId(), new ArticleRequest("제목2", "내용2"), COMMUNITY); Article article3 = articleService - .createArticle(그린론.getId(), study.getId(), new ArticleRequest("제목3", "내용3"), ArticleType.COMMUNITY); + .createArticle(그린론.getId(), study.getId(), new ArticleRequest("제목3", "내용3"), COMMUNITY); Article article4 = articleService - .createArticle(그린론.getId(), study.getId(), new ArticleRequest("제목4", "내용4"), ArticleType.COMMUNITY); + .createArticle(그린론.getId(), study.getId(), new ArticleRequest("제목4", "내용4"), COMMUNITY); Article article5 = articleService - .createArticle(그린론.getId(), study.getId(), new ArticleRequest("제목5", "내용5"), ArticleType.COMMUNITY); + .createArticle(그린론.getId(), study.getId(), new ArticleRequest("제목5", "내용5"), COMMUNITY); // act ResponseEntity response = sut.getArticles(그린론.getId(), study.getId(), - ArticleType.COMMUNITY, PageRequest.of(0, 3)); + COMMUNITY, PageRequest.of(0, 3)); // assert AuthorResponse author = new AuthorResponse(1L, "그린론", "http://image", "http://profile"); @@ -112,8 +113,11 @@ void throwExceptionWhenWriteToNotFoundStudy() { // arrange Member member = memberRepository.save(new Member(1L, "username", "imageUrl", "profileUrl")); + final Long memberId = member.getId(); + final PageRequest pageRequest = PageRequest.of(0, 3); + // act & assert - assertThatThrownBy(() -> sut.getArticles(member.getId(), 1L, ArticleType.COMMUNITY, PageRequest.of(0, 3))) + assertThatThrownBy(() -> sut.getArticles(memberId, 1L, COMMUNITY, pageRequest)) .isInstanceOf(StudyNotFoundException.class); } @@ -127,9 +131,12 @@ void throwExceptionWhenGettingByNotParticipant() { Study study = studyService .createStudy(member.getGithubId(), javaStudyRequest.startDate(LocalDate.now()).build()); + final Long otherId = other.getId(); + final Long studyId = study.getId(); + final PageRequest pageRequest = PageRequest.of(0, 3); + // act & assert - assertThatThrownBy( - () -> sut.getArticles(other.getId(), study.getId(), ArticleType.COMMUNITY, PageRequest.of(0, 3))) + assertThatThrownBy(() -> sut.getArticles(otherId, studyId, COMMUNITY, pageRequest)) .isInstanceOf(UnviewableArticleException.class); } } diff --git a/backend/src/test/java/com/woowacourse/moamoa/studyroom/controller/UpdatingArticleControllerTest.java b/backend/src/test/java/com/woowacourse/moamoa/studyroom/controller/UpdatingArticleControllerTest.java index 09e270eb8..8014e1e1d 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/studyroom/controller/UpdatingArticleControllerTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/studyroom/controller/UpdatingArticleControllerTest.java @@ -1,5 +1,6 @@ package com.woowacourse.moamoa.studyroom.controller; +import static com.woowacourse.moamoa.studyroom.domain.ArticleType.COMMUNITY; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -12,12 +13,11 @@ import com.woowacourse.moamoa.study.service.StudyService; import com.woowacourse.moamoa.study.service.request.StudyRequestBuilder; import com.woowacourse.moamoa.studyroom.domain.Article; -import com.woowacourse.moamoa.studyroom.domain.ArticleType; import com.woowacourse.moamoa.studyroom.domain.CommunityArticle; import com.woowacourse.moamoa.studyroom.domain.StudyRoom; -import com.woowacourse.moamoa.studyroom.domain.repository.studyroom.StudyRoomRepository; import com.woowacourse.moamoa.studyroom.domain.repository.article.ArticleRepository; import com.woowacourse.moamoa.studyroom.domain.repository.article.ArticleRepositoryFactory; +import com.woowacourse.moamoa.studyroom.domain.repository.studyroom.StudyRoomRepository; import com.woowacourse.moamoa.studyroom.query.ArticleDao; import com.woowacourse.moamoa.studyroom.service.ArticleService; import com.woowacourse.moamoa.studyroom.service.exception.ArticleNotFoundException; @@ -33,7 +33,7 @@ import org.springframework.http.ResponseEntity; @RepositoryTest -public class UpdatingArticleControllerTest { +class UpdatingArticleControllerTest { StudyRequestBuilder javaStudyBuilder = new StudyRequestBuilder() .title("java 스터디").excerpt("자바 설명").thumbnail("java image").description("자바 소개"); @@ -73,15 +73,15 @@ void updateArticle() { Study study = studyService .createStudy(owner.getGithubId(), javaStudyBuilder.startDate(LocalDate.now()).build()); Article article = articleService - .createArticle(owner.getId(), study.getId(), new ArticleRequest("제목", "내용"), ArticleType.COMMUNITY); + .createArticle(owner.getId(), study.getId(), new ArticleRequest("제목", "내용"), COMMUNITY); // act final ResponseEntity response = sut - .updateArticle(owner.getId(), study.getId(), article.getId(), ArticleType.COMMUNITY, + .updateArticle(owner.getId(), study.getId(), article.getId(), COMMUNITY, new ArticleRequest("제목 수정", "내용 수정")); // assert - ArticleRepository
    articleRepository = articleRepositoryFactory.getRepository(ArticleType.COMMUNITY); + ArticleRepository
    articleRepository = articleRepositoryFactory.getRepository(COMMUNITY); Article actualArticle = articleRepository.findById(article.getId()).orElseThrow(); StudyRoom expectStudyRoom = new StudyRoom(study.getId(), owner.getId(), Set.of()); @@ -100,12 +100,14 @@ void throwExceptionWhenUpdateToNotFoundArticle() { Study study = studyService .createStudy(member.getGithubId(), javaStudyBuilder.startDate(LocalDate.now()).build()); + final Long memberId = member.getId(); + final Long studyId = study.getId(); + final ArticleRequest articleRequest = new ArticleRequest("제목 수정", "내용 수정"); + // act & assert assertThatThrownBy(() -> - sut.updateArticle(member.getId(), study.getId(), 1L, ArticleType.COMMUNITY, - new ArticleRequest("제목 수정", "내용 수정")) - ) - .isInstanceOf(ArticleNotFoundException.class); + sut.updateArticle(memberId, studyId, 1L, COMMUNITY, articleRequest) + ).isInstanceOf(ArticleNotFoundException.class); } @DisplayName("게시글을 수정할 수 없는 경우 예외가 발생한다.") @@ -120,13 +122,17 @@ void throwExceptionWhenUpdateByNotParticipant() { ArticleRequest request = new ArticleRequest("게시글 제목", "게시글 내용"); final Article article = articleService - .createArticle(member.getId(), study.getId(), request, ArticleType.COMMUNITY); + .createArticle(member.getId(), study.getId(), request, COMMUNITY); + + final Long otherId = other.getId(); + final Long studyId = study.getId(); + final Long articleId = article.getId(); + final ArticleRequest articleRequest = new ArticleRequest("제목 수정", "내용 수정"); // act & assert assertThatThrownBy(() -> - sut.updateArticle(other.getId(), study.getId(), article.getId(), ArticleType.COMMUNITY, - new ArticleRequest("제목 수정", "내용 수정")) - ) - .isInstanceOf(UneditableArticleException.class); + sut.updateArticle(otherId, studyId, articleId, COMMUNITY, + articleRequest + )).isInstanceOf(UneditableArticleException.class); } } diff --git a/backend/src/test/java/com/woowacourse/moamoa/studyroom/webmvc/CreatingArticleControllerWebMvcTest.java b/backend/src/test/java/com/woowacourse/moamoa/studyroom/webmvc/CreatingArticleControllerWebMvcTest.java index 97f0e587a..054c056d1 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/studyroom/webmvc/CreatingArticleControllerWebMvcTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/studyroom/webmvc/CreatingArticleControllerWebMvcTest.java @@ -19,7 +19,7 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; -public class CreatingArticleControllerWebMvcTest extends WebMVCTest { +class CreatingArticleControllerWebMvcTest extends WebMVCTest { @MockBean private ArticleService articleService; diff --git a/backend/src/test/java/com/woowacourse/moamoa/studyroom/webmvc/DeletingArticleControllerWebMvcTest.java b/backend/src/test/java/com/woowacourse/moamoa/studyroom/webmvc/DeletingArticleControllerWebMvcTest.java index 4c87c82ac..c90d64373 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/studyroom/webmvc/DeletingArticleControllerWebMvcTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/studyroom/webmvc/DeletingArticleControllerWebMvcTest.java @@ -1,7 +1,6 @@ package com.woowacourse.moamoa.studyroom.webmvc; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -12,7 +11,7 @@ import org.junit.jupiter.params.provider.ValueSource; import org.springframework.http.HttpHeaders; -public class DeletingArticleControllerWebMvcTest extends WebMVCTest { +class DeletingArticleControllerWebMvcTest extends WebMVCTest { @DisplayName("잘못된 토큰으로 커뮤니티 글을 생성할 경우 401을 반환한다.") @ParameterizedTest diff --git a/backend/src/test/java/com/woowacourse/moamoa/studyroom/webmvc/GettingArticleControllerWebMvcTest.java b/backend/src/test/java/com/woowacourse/moamoa/studyroom/webmvc/GettingArticleControllerWebMvcTest.java index 2ac43481f..e913ecf1c 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/studyroom/webmvc/GettingArticleControllerWebMvcTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/studyroom/webmvc/GettingArticleControllerWebMvcTest.java @@ -1,6 +1,5 @@ package com.woowacourse.moamoa.studyroom.webmvc; -import static org.assertj.core.api.Assertions.assertThat; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -13,7 +12,7 @@ import org.junit.jupiter.params.provider.ValueSource; import org.springframework.http.HttpHeaders; -public class GettingArticleControllerWebMvcTest extends WebMVCTest { +class GettingArticleControllerWebMvcTest extends WebMVCTest { @DisplayName("잘못된 토큰으로 커뮤니티 글을 조회할 경우 401을 반환한다.") @ParameterizedTest diff --git a/backend/src/test/java/com/woowacourse/moamoa/studyroom/webmvc/UpdatingArticleControllerWebMvcTest.java b/backend/src/test/java/com/woowacourse/moamoa/studyroom/webmvc/UpdatingArticleControllerWebMvcTest.java index 4b131bfd0..d4f50b63e 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/studyroom/webmvc/UpdatingArticleControllerWebMvcTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/studyroom/webmvc/UpdatingArticleControllerWebMvcTest.java @@ -15,7 +15,7 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; -public class UpdatingArticleControllerWebMvcTest extends WebMVCTest { +class UpdatingArticleControllerWebMvcTest extends WebMVCTest { @DisplayName("잘못된 토큰으로 커뮤니티 글을 수정할 경우 401을 반환한다.") @ParameterizedTest From 5499c19eee5aff8b4bdca056535b5fb25ba0ec71 Mon Sep 17 00:00:00 2001 From: Donggyu Date: Wed, 17 Aug 2022 22:05:01 +0900 Subject: [PATCH 34/51] =?UTF-8?q?fix:=20=EB=B2=84=EA=B7=B8=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/com/woowacourse/moamoa/tag/query/TagDao.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/backend/src/main/java/com/woowacourse/moamoa/tag/query/TagDao.java b/backend/src/main/java/com/woowacourse/moamoa/tag/query/TagDao.java index af20e1f77..27a22bc4d 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/tag/query/TagDao.java +++ b/backend/src/main/java/com/woowacourse/moamoa/tag/query/TagDao.java @@ -64,6 +64,10 @@ public List findTagsByStudyId(Long studyId) { } public Map> findTagsByStudyIds(final List studyIds) { + if (studyIds.isEmpty()) { + return new HashMap<>(); + } + final String sql = "SELECT tag.id tag_id, tag.name tag_name, study.id study_id " + "FROM tag " + "JOIN study_tag ON tag.id = study_tag.tag_id " From 05a4dfbfbc2bf39d4d820c97522a155825c7fe33 Mon Sep 17 00:00:00 2001 From: Donggyu Date: Thu, 18 Aug 2022 12:05:08 +0900 Subject: [PATCH 35/51] =?UTF-8?q?fix:=20`Participant`=20eq&hc=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../moamoa/study/domain/Participant.java | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/backend/src/main/java/com/woowacourse/moamoa/study/domain/Participant.java b/backend/src/main/java/com/woowacourse/moamoa/study/domain/Participant.java index 721d32915..76800a9fa 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/study/domain/Participant.java +++ b/backend/src/main/java/com/woowacourse/moamoa/study/domain/Participant.java @@ -3,10 +3,10 @@ import static lombok.AccessLevel.PROTECTED; import java.time.LocalDate; +import java.util.Objects; import javax.persistence.Column; import javax.persistence.Embeddable; import lombok.AllArgsConstructor; -import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.ToString; @@ -16,7 +16,6 @@ @NoArgsConstructor(access = PROTECTED) @AllArgsConstructor @ToString -@EqualsAndHashCode public class Participant { @Column(name = "member_id", nullable = false) @@ -29,4 +28,21 @@ public Participant(final Long memberId) { this.memberId = memberId; this.participationDate = LocalDate.now(); } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final Participant that = (Participant) o; + return Objects.equals(memberId, that.memberId); + } + + @Override + public int hashCode() { + return Objects.hash(memberId); + } } From 893c4070d3c5e44455904ae0de0797d86d4e81a2 Mon Sep 17 00:00:00 2001 From: SeungCheol Date: Thu, 18 Aug 2022 13:13:58 +0900 Subject: [PATCH 36/51] =?UTF-8?q?refactor:=20Link=20limit,=20offset=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../woowacourse/moamoa/referenceroom/query/LinkDao.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/backend/src/main/java/com/woowacourse/moamoa/referenceroom/query/LinkDao.java b/backend/src/main/java/com/woowacourse/moamoa/referenceroom/query/LinkDao.java index f4a1c09a0..574e0d206 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/referenceroom/query/LinkDao.java +++ b/backend/src/main/java/com/woowacourse/moamoa/referenceroom/query/LinkDao.java @@ -26,8 +26,11 @@ public Slice findAllByStudyId(final Long studyId, final Pageable pagea + "JOIN member ON link.member_id = member.id " + "WHERE link.deleted = false " + "AND link.study_id = :studyId " - + "ORDER BY link.created_date DESC, link.id DESC"; - final MapSqlParameterSource params = new MapSqlParameterSource("studyId", studyId); + + "ORDER BY link.created_date DESC, link.id DESC " + + "LIMIT :limit OFFSET :offset"; + final MapSqlParameterSource params = new MapSqlParameterSource("studyId", studyId) + .addValue("limit", pageable.getPageSize()) + .addValue("offset", pageable.getOffset()); final List linkData = namedParameterJdbcTemplate.query(sql, params, rowMapper()); return new SliceImpl<>(getCurrentPageLinks(linkData, pageable), pageable, hasNext(linkData, pageable)); From 55ea4bbaa8fec40ef44435e870883528c872f6bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=A7=84=ED=98=81?= Date: Thu, 18 Aug 2022 13:27:21 +0900 Subject: [PATCH 37/51] =?UTF-8?q?refactor:=20=EB=94=94=EB=B2=84=EA=B9=85?= =?UTF-8?q?=EC=9A=A9=20stack=20trace=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../moamoa/common/advice/CommonControllerAdvice.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/backend/src/main/java/com/woowacourse/moamoa/common/advice/CommonControllerAdvice.java b/backend/src/main/java/com/woowacourse/moamoa/common/advice/CommonControllerAdvice.java index 98ede3ea6..3811764ad 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/common/advice/CommonControllerAdvice.java +++ b/backend/src/main/java/com/woowacourse/moamoa/common/advice/CommonControllerAdvice.java @@ -13,6 +13,8 @@ import com.woowacourse.moamoa.study.domain.exception.InvalidPeriodException; import com.woowacourse.moamoa.study.service.exception.FailureParticipationException; import io.jsonwebtoken.JwtException; +import java.io.PrintStream; +import java.io.PrintWriter; import lombok.extern.slf4j.Slf4j; import org.springframework.http.ResponseEntity; import org.springframework.http.converter.HttpMessageNotReadableException; @@ -46,6 +48,7 @@ public ResponseEntity handleBadRequest(final Exception e) { @ExceptionHandler({UnauthorizedException.class, JwtException.class}) public ResponseEntity handleUnauthorized(final Exception e) { log.debug("UnauthorizedException : {}", e.getMessage()); + log.debug("Exception Stack Trace : \n{}", e.getStackTrace()); return ResponseEntity.status(UNAUTHORIZED).build(); } From 9eb2bee60e7ba5419e126050c5c11a4288151fd3 Mon Sep 17 00:00:00 2001 From: Donggyu Date: Thu, 18 Aug 2022 14:21:49 +0900 Subject: [PATCH 38/51] =?UTF-8?q?refactor:=20=EB=A7=81=ED=81=AC=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?(#288)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: SeungCheol --- .../com/woowacourse/moamoa/referenceroom/query/LinkDao.java | 2 +- .../test/referenceroom/ReferenceRoomAcceptanceTest.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/main/java/com/woowacourse/moamoa/referenceroom/query/LinkDao.java b/backend/src/main/java/com/woowacourse/moamoa/referenceroom/query/LinkDao.java index 574e0d206..b9e056b0e 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/referenceroom/query/LinkDao.java +++ b/backend/src/main/java/com/woowacourse/moamoa/referenceroom/query/LinkDao.java @@ -29,7 +29,7 @@ public Slice findAllByStudyId(final Long studyId, final Pageable pagea + "ORDER BY link.created_date DESC, link.id DESC " + "LIMIT :limit OFFSET :offset"; final MapSqlParameterSource params = new MapSqlParameterSource("studyId", studyId) - .addValue("limit", pageable.getPageSize()) + .addValue("limit", pageable.getPageSize() + 1) .addValue("offset", pageable.getOffset()); final List linkData = namedParameterJdbcTemplate.query(sql, params, rowMapper()); diff --git a/backend/src/test/java/com/woowacourse/acceptance/test/referenceroom/ReferenceRoomAcceptanceTest.java b/backend/src/test/java/com/woowacourse/acceptance/test/referenceroom/ReferenceRoomAcceptanceTest.java index b81795a77..a7a3b63c1 100644 --- a/backend/src/test/java/com/woowacourse/acceptance/test/referenceroom/ReferenceRoomAcceptanceTest.java +++ b/backend/src/test/java/com/woowacourse/acceptance/test/referenceroom/ReferenceRoomAcceptanceTest.java @@ -118,7 +118,7 @@ void getAllLink() { ))) .header(HttpHeaders.AUTHORIZATION, token) .pathParam("study-id", 자바_스터디_ID) - .param("page", 1) + .param("page", 0) .param("size", 5) .when().log().all() .get("/api/studies/{study-id}/reference-room/links") From df3b40961c5e625849c159ad3d0923507de8cb34 Mon Sep 17 00:00:00 2001 From: Donggyu Date: Thu, 18 Aug 2022 14:26:30 +0900 Subject: [PATCH 39/51] =?UTF-8?q?fix:=20refreshToken=20=EB=A1=9C=EC=A7=81?= =?UTF-8?q?=20=EC=88=98=EC=A0=95=20(#289)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../moamoa/auth/config/AuthConfig.java | 3 ++ .../auth/config/AuthRequestMatchConfig.java | 1 + .../auth/config/AuthenticatedRefresh.java | 11 ++++++ .../auth/controller/AuthController.java | 3 +- .../AuthenticatedRefreshArgumentResolver.java | 39 +++++++++++++++++++ .../controller/AuthenticationInterceptor.java | 2 +- .../auth/infrastructure/JwtTokenProvider.java | 15 +++++++ .../auth/infrastructure/TokenProvider.java | 2 + 8 files changed, 74 insertions(+), 2 deletions(-) create mode 100644 backend/src/main/java/com/woowacourse/moamoa/auth/config/AuthenticatedRefresh.java create mode 100644 backend/src/main/java/com/woowacourse/moamoa/auth/controller/AuthenticatedRefreshArgumentResolver.java diff --git a/backend/src/main/java/com/woowacourse/moamoa/auth/config/AuthConfig.java b/backend/src/main/java/com/woowacourse/moamoa/auth/config/AuthConfig.java index bf5946c87..618364021 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/auth/config/AuthConfig.java +++ b/backend/src/main/java/com/woowacourse/moamoa/auth/config/AuthConfig.java @@ -1,6 +1,7 @@ package com.woowacourse.moamoa.auth.config; import com.woowacourse.moamoa.auth.controller.AuthenticatedMemberResolver; +import com.woowacourse.moamoa.auth.controller.AuthenticatedRefreshArgumentResolver; import com.woowacourse.moamoa.auth.controller.AuthenticationArgumentResolver; import com.woowacourse.moamoa.auth.controller.AuthenticationInterceptor; @@ -18,6 +19,7 @@ @RequiredArgsConstructor public class AuthConfig implements WebMvcConfigurer { + private final AuthenticatedRefreshArgumentResolver authenticatedRefreshArgumentResolver; private final AuthenticationInterceptor authenticationInterceptor; private final AuthenticationArgumentResolver authenticationArgumentResolver; private final AuthenticatedMemberResolver authenticatedMemberResolver; @@ -26,6 +28,7 @@ public class AuthConfig implements WebMvcConfigurer { public void addArgumentResolvers(final List resolvers) { resolvers.add(authenticationArgumentResolver); resolvers.add(authenticatedMemberResolver); + resolvers.add(authenticatedRefreshArgumentResolver); } @Override diff --git a/backend/src/main/java/com/woowacourse/moamoa/auth/config/AuthRequestMatchConfig.java b/backend/src/main/java/com/woowacourse/moamoa/auth/config/AuthRequestMatchConfig.java index 523620388..7cd7c9f54 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/auth/config/AuthRequestMatchConfig.java +++ b/backend/src/main/java/com/woowacourse/moamoa/auth/config/AuthRequestMatchConfig.java @@ -23,6 +23,7 @@ public AuthenticationRequestMatcher authenticationRequestMatcher() { "/api/studies") .addUpAuthenticationPath(GET, "/api/my/studies", + "/api/auth/refresh", "/api/members/me", "/api/members/me/role", "/api/studies/\\w+/community/articles/\\w+", diff --git a/backend/src/main/java/com/woowacourse/moamoa/auth/config/AuthenticatedRefresh.java b/backend/src/main/java/com/woowacourse/moamoa/auth/config/AuthenticatedRefresh.java new file mode 100644 index 000000000..496e55306 --- /dev/null +++ b/backend/src/main/java/com/woowacourse/moamoa/auth/config/AuthenticatedRefresh.java @@ -0,0 +1,11 @@ +package com.woowacourse.moamoa.auth.config; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +public @interface AuthenticatedRefresh { +} diff --git a/backend/src/main/java/com/woowacourse/moamoa/auth/controller/AuthController.java b/backend/src/main/java/com/woowacourse/moamoa/auth/controller/AuthController.java index 17938a0c0..70ffe2204 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/auth/controller/AuthController.java +++ b/backend/src/main/java/com/woowacourse/moamoa/auth/controller/AuthController.java @@ -1,5 +1,6 @@ package com.woowacourse.moamoa.auth.controller; +import com.woowacourse.moamoa.auth.config.AuthenticatedRefresh; import com.woowacourse.moamoa.auth.config.AuthenticationPrincipal; import com.woowacourse.moamoa.auth.service.AuthService; import com.woowacourse.moamoa.auth.service.response.AccessTokenResponse; @@ -34,7 +35,7 @@ public ResponseEntity login(@RequestParam final String code } @GetMapping("/api/auth/refresh") - public ResponseEntity refreshToken(@AuthenticationPrincipal Long githubId, @CookieValue String refreshToken) { + public ResponseEntity refreshToken(@AuthenticatedRefresh Long githubId, @CookieValue String refreshToken) { return ResponseEntity.ok().body(authService.refreshToken(githubId, refreshToken)); } diff --git a/backend/src/main/java/com/woowacourse/moamoa/auth/controller/AuthenticatedRefreshArgumentResolver.java b/backend/src/main/java/com/woowacourse/moamoa/auth/controller/AuthenticatedRefreshArgumentResolver.java new file mode 100644 index 000000000..c7df9c6c8 --- /dev/null +++ b/backend/src/main/java/com/woowacourse/moamoa/auth/controller/AuthenticatedRefreshArgumentResolver.java @@ -0,0 +1,39 @@ +package com.woowacourse.moamoa.auth.controller; + +import com.woowacourse.moamoa.auth.config.AuthenticatedRefresh; +import com.woowacourse.moamoa.auth.config.AuthenticationExtractor; +import com.woowacourse.moamoa.auth.infrastructure.TokenProvider; +import com.woowacourse.moamoa.common.exception.UnauthorizedException; +import javax.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import org.springframework.core.MethodParameter; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; + +@Component +@RequiredArgsConstructor +public class AuthenticatedRefreshArgumentResolver implements HandlerMethodArgumentResolver { + + private final TokenProvider tokenProvider; + + @Override + public boolean supportsParameter(final MethodParameter parameter) { + return parameter.hasParameterAnnotation(AuthenticatedRefresh.class); + } + + @Override + public Object resolveArgument(final MethodParameter parameter, final ModelAndViewContainer mavContainer, + final NativeWebRequest webRequest, final WebDataBinderFactory binderFactory) { + final HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class); + final String token = AuthenticationExtractor.extract(request); + + if (token == null) { + throw new UnauthorizedException("인증 타입이 올바르지 않습니다."); + } + + return Long.valueOf(tokenProvider.getPayloadWithExpiredToken(token)); + } +} diff --git a/backend/src/main/java/com/woowacourse/moamoa/auth/controller/AuthenticationInterceptor.java b/backend/src/main/java/com/woowacourse/moamoa/auth/controller/AuthenticationInterceptor.java index 92b4d728b..1f4a0847c 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/auth/controller/AuthenticationInterceptor.java +++ b/backend/src/main/java/com/woowacourse/moamoa/auth/controller/AuthenticationInterceptor.java @@ -31,7 +31,7 @@ public boolean preHandle(final HttpServletRequest request, final HttpServletResp final String token = AuthenticationExtractor.extract(request); validateToken(token, request.getRequestURI()); - request.setAttribute("payload", tokenProvider.getPayload(token)); + request.setAttribute("payload", token); } return true; diff --git a/backend/src/main/java/com/woowacourse/moamoa/auth/infrastructure/JwtTokenProvider.java b/backend/src/main/java/com/woowacourse/moamoa/auth/infrastructure/JwtTokenProvider.java index 64772bdf3..b6b74fbf0 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/auth/infrastructure/JwtTokenProvider.java +++ b/backend/src/main/java/com/woowacourse/moamoa/auth/infrastructure/JwtTokenProvider.java @@ -3,6 +3,7 @@ import com.woowacourse.moamoa.auth.exception.RefreshTokenExpirationException; import com.woowacourse.moamoa.auth.service.response.TokensResponse; import io.jsonwebtoken.Claims; +import io.jsonwebtoken.ExpiredJwtException; import io.jsonwebtoken.Jws; import io.jsonwebtoken.JwtException; import io.jsonwebtoken.Jwts; @@ -60,6 +61,20 @@ public String getPayload(final String token) { .getSubject(); } + @Override + public String getPayloadWithExpiredToken(final String token) { + try { + return Jwts.parserBuilder() + .setSigningKey(key) + .build() + .parseClaimsJws(token) + .getBody() + .getSubject(); + } catch (ExpiredJwtException e) { + return e.getClaims().getSubject(); + } + } + @Override public boolean validateToken(final String token) { try { diff --git a/backend/src/main/java/com/woowacourse/moamoa/auth/infrastructure/TokenProvider.java b/backend/src/main/java/com/woowacourse/moamoa/auth/infrastructure/TokenProvider.java index daed6cd84..540ccde7c 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/auth/infrastructure/TokenProvider.java +++ b/backend/src/main/java/com/woowacourse/moamoa/auth/infrastructure/TokenProvider.java @@ -8,6 +8,8 @@ public interface TokenProvider { String getPayload(final String token); + String getPayloadWithExpiredToken(final String token); + boolean validateToken(final String token); String recreationAccessToken(final Long githubId, final String refreshToken); From 0091429deac9c557f33f12c044db61025f5b7100 Mon Sep 17 00:00:00 2001 From: jaeseo yoo Date: Thu, 18 Aug 2022 15:56:00 +0900 Subject: [PATCH 40/51] =?UTF-8?q?[BE]=20issue290:=20SonarQube=20PostgreSQL?= =?UTF-8?q?=20=EC=97=B0=EB=8F=99=20(#291)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: postgresql DB 연동 확인 * feat: postgresql DB 세팅 * feat: postgresql DB 세팅 * feat: postgresql DB 세팅 --- backend/build.gradle | 2 +- backend/src/test/java/com/woowacourse/moamoa/WebMVCTest.java | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/build.gradle b/backend/build.gradle index f6b75b240..1187d30b9 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -99,7 +99,7 @@ bootJar { sonarqube { properties { - property "sonar.projectKey", "woowacourse-teams_2022-moamoa_AYKqauCybW85TfLm8Krq" + property "sonar.projectKey", "woowacourse-teams_2022-moamoa_AYKvd_z4VbW_bWBvgn13" property "sonar.sources", "src" property "sonar.language", "java" property "sonar.sourceEncoding", "UTF-8" diff --git a/backend/src/test/java/com/woowacourse/moamoa/WebMVCTest.java b/backend/src/test/java/com/woowacourse/moamoa/WebMVCTest.java index 4b20a2754..0aab0a5f3 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/WebMVCTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/WebMVCTest.java @@ -13,6 +13,7 @@ import com.woowacourse.moamoa.member.domain.repository.MemberRepository; import com.woowacourse.moamoa.referenceroom.service.ReferenceRoomService; import com.woowacourse.moamoa.referenceroom.service.SearchingReferenceRoomService; + import java.util.Optional; import javax.servlet.http.HttpServletRequest; import org.junit.jupiter.api.BeforeEach; From 3b14718371e1d9caaee94d4e40fa70a9e60ff689 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=A7=84=ED=98=81?= Date: Thu, 18 Aug 2022 17:26:17 +0900 Subject: [PATCH 41/51] =?UTF-8?q?refactor:=20CORS=20=ED=97=88=EC=9A=A9=20U?= =?UTF-8?q?RL=20=EC=A3=BC=EC=9E=85=20=EC=84=A4=EC=A0=95=20(#293)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: CORS 허용 URL 주입 설정 * chore: ci/cd 변경 --- .github/workflows/backend.yml | 2 +- .github/workflows/deploy-backend-dev.yml | 2 +- .../moamoa/common/config/WebConfig.java | 14 ++++++++------ 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index 63725ef8d..7e03a214c 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -43,7 +43,7 @@ jobs: run: ./gradlew build --exclude-task test --exclude-task asciidoctor - name: Test - run: ./gradlew test -Doauth2.github.client-id=${{ env.client-id }} -Doauth2.github.client-secret=${{ env.client-secret }} -Dsecurity.jwt.token.secret-key=${{ env.jwt-secret-key }} -Dsecurity.jwt.token.expire-length=${{ env.jwt-expire-length }} + run: ./gradlew test -Dmoamoa.allow-origins='*' -Doauth2.github.client-id=${{ env.client-id }} -Doauth2.github.client-secret=${{ env.client-secret }} -Dsecurity.jwt.token.secret-key=${{ env.jwt-secret-key }} -Dsecurity.jwt.token.expire-length=${{ env.jwt-expire-length }} - name: SonarQube env: diff --git a/.github/workflows/deploy-backend-dev.yml b/.github/workflows/deploy-backend-dev.yml index e1d58d0de..94436f1b1 100644 --- a/.github/workflows/deploy-backend-dev.yml +++ b/.github/workflows/deploy-backend-dev.yml @@ -39,7 +39,7 @@ jobs: client-secret: ${{ secrets.CLIENT_SECRET }} jwt-secret-key: ${{ secrets.JWT_SECRET_KEY }} jwt-expire-length: ${{ secrets.JWT_EXPIRE_LENGTH }} - run: ./gradlew test -Doauth2.github.client-id=${{ env.client-id }} -Doauth2.github.client-secret=${{ env.client-secret }} -Dsecurity.jwt.token.secret-key=${{ env.jwt-secret-key }} -Dsecurity.jwt.token.expire-length=${{ env.jwt-expire-length }} + run: ./gradlew test -Dmoamoa.allow-origins='*' -Doauth2.github.client-id=${{ env.client-id }} -Doauth2.github.client-secret=${{ env.client-secret }} -Dsecurity.jwt.token.secret-key=${{ env.jwt-secret-key }} -Dsecurity.jwt.token.expire-length=${{ env.jwt-expire-length }} - name: SonarQube env: diff --git a/backend/src/main/java/com/woowacourse/moamoa/common/config/WebConfig.java b/backend/src/main/java/com/woowacourse/moamoa/common/config/WebConfig.java index 1e815d2c7..803925780 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/common/config/WebConfig.java +++ b/backend/src/main/java/com/woowacourse/moamoa/common/config/WebConfig.java @@ -4,6 +4,7 @@ import java.util.List; import org.slf4j.LoggerFactory; import org.slf4j.Logger; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpHeaders; @@ -14,6 +15,12 @@ @Configuration public class WebConfig implements WebMvcConfigurer { + private final String[] allowedOrigins; + + public WebConfig(@Value("${moamoa.allow-origins}") final String[] allowedOrigins) { + this.allowedOrigins = allowedOrigins; + } + private static final String[] ALLOW_METHODS = {"GET", "HEAD", "POST", "PUT", "DELETE", "TRACE", "OPTIONS", "PATCH"}; @Override @@ -24,15 +31,10 @@ public void addArgumentResolvers(final List resol @Override public void addCorsMappings(final CorsRegistry registry) { registry.addMapping("/api/**") - .allowedOrigins("https://dev.moamoa.space", "https://moamoa.space") + .allowedOriginPatterns(allowedOrigins) .allowedMethods(ALLOW_METHODS) .exposedHeaders(HttpHeaders.LOCATION) .exposedHeaders("Set-Cookie") .allowCredentials(true); } - - @Bean - public Logger logger() { - return LoggerFactory.getLogger(MoamoaApplication.class); - } } From 1dda5f09442d5cf6f7a0fb26a7a0155be35c2c2c Mon Sep 17 00:00:00 2001 From: Donggyu Date: Thu, 18 Aug 2022 18:37:26 +0900 Subject: [PATCH 42/51] =?UTF-8?q?fix:=20=EC=8A=A4=ED=84=B0=EB=94=94?= =?UTF-8?q?=EC=9E=A5=20=EB=98=90=ED=95=9C=20=EC=B0=B8=EC=97=AC=ED=95=9C=20?= =?UTF-8?q?=EC=8A=A4=ED=84=B0=EB=94=94=20=EA=B0=9C=EC=88=98=20=ED=91=9C?= =?UTF-8?q?=EC=8B=9C=20(#294)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../moamoa/member/query/data/OwnerData.java | 29 +++++++++++++++++++ .../moamoa/study/query/StudyDetailsDao.java | 15 ++++++++-- .../study/query/data/StudyDetailsData.java | 4 +-- .../query/data/StudyDetailsDataBuilder.java | 6 ++-- .../service/response/StudyDetailResponse.java | 4 +-- .../SearchingStudyControllerTest.java | 8 ++--- .../study/query/StudyDetailsDaoTest.java | 6 ++-- 7 files changed, 55 insertions(+), 17 deletions(-) create mode 100644 backend/src/main/java/com/woowacourse/moamoa/member/query/data/OwnerData.java diff --git a/backend/src/main/java/com/woowacourse/moamoa/member/query/data/OwnerData.java b/backend/src/main/java/com/woowacourse/moamoa/member/query/data/OwnerData.java new file mode 100644 index 000000000..55b45cd2a --- /dev/null +++ b/backend/src/main/java/com/woowacourse/moamoa/member/query/data/OwnerData.java @@ -0,0 +1,29 @@ +package com.woowacourse.moamoa.member.query.data; + +import static lombok.AccessLevel.PRIVATE; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@Getter +@NoArgsConstructor(access = PRIVATE) +@AllArgsConstructor +@EqualsAndHashCode +@ToString +public class OwnerData { + + @JsonProperty("id") + private Long githubId; + + private String username; + + private String imageUrl; + + private String profileUrl; + + private int numberOfStudy; +} diff --git a/backend/src/main/java/com/woowacourse/moamoa/study/query/StudyDetailsDao.java b/backend/src/main/java/com/woowacourse/moamoa/study/query/StudyDetailsDao.java index ba84e1c35..0bdbc7a98 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/study/query/StudyDetailsDao.java +++ b/backend/src/main/java/com/woowacourse/moamoa/study/query/StudyDetailsDao.java @@ -1,6 +1,6 @@ package com.woowacourse.moamoa.study.query; -import com.woowacourse.moamoa.member.query.data.MemberData; +import com.woowacourse.moamoa.member.query.data.OwnerData; import com.woowacourse.moamoa.study.query.data.StudyDetailsData; import com.woowacourse.moamoa.study.query.data.StudyDetailsDataBuilder; import com.woowacourse.moamoa.study.service.exception.StudyNotFoundException; @@ -27,7 +27,8 @@ public Optional findBy(Long studyId) { "SELECT study.id, title, excerpt, thumbnail, recruitment_status, description, current_member_count, " + "max_member_count, created_at, enrollment_end_date, start_date, end_date, owner_id, " + "member.github_id as owner_github_id, member.username as owner_username, " - + "member.image_url as owner_image_url, member.profile_url as owner_profile_url " + + "member.image_url as owner_image_url, member.profile_url as owner_profile_url, " + + countOfStudy() + "FROM study JOIN member ON study.owner_id = member.id " + "WHERE study.id = ?"; final StudyDetailsData data = jdbcTemplate.query(sql, new StudyDetailsDataExtractor(), studyId); @@ -37,6 +38,12 @@ public Optional findBy(Long studyId) { } } + private String countOfStudy() { + return "((SELECT count(case when (study_member.member_id = member.id) then 1 end) FROM study JOIN study_member ON study.id = study_member.study_id) " + + "+ " + + "(SELECT count(case when (study.owner_id = member.id) then 1 end) FROM study)) as number_of_study "; + } + private static class StudyDetailsDataExtractor implements ResultSetExtractor { private final StudyDetailsDataBuilder builder; @@ -89,9 +96,11 @@ private void appendParticipants(final ResultSet rs) throws SQLException { String imageUrl = rs.getString("owner_image_url"); String profileUrl = rs.getString("owner_profile_url"); + int numberOfStudy = rs.getInt("number_of_study"); + builder.currentMemberCount(currentMaxCount) .maxMemberCount(maxMemberCount) - .owner(new MemberData(githubId, username, imageUrl, profileUrl)); + .owner(new OwnerData(githubId, username, imageUrl, profileUrl, numberOfStudy)); } } } diff --git a/backend/src/main/java/com/woowacourse/moamoa/study/query/data/StudyDetailsData.java b/backend/src/main/java/com/woowacourse/moamoa/study/query/data/StudyDetailsData.java index 2330a01eb..2eab02a16 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/study/query/data/StudyDetailsData.java +++ b/backend/src/main/java/com/woowacourse/moamoa/study/query/data/StudyDetailsData.java @@ -1,6 +1,6 @@ package com.woowacourse.moamoa.study.query.data; -import com.woowacourse.moamoa.member.query.data.MemberData; +import com.woowacourse.moamoa.member.query.data.OwnerData; import java.time.LocalDate; import lombok.AccessLevel; import lombok.AllArgsConstructor; @@ -17,7 +17,7 @@ public class StudyDetailsData { private final String recruitmentStatus; private final String description; private final LocalDate createdDate; - private final MemberData owner; + private final OwnerData owner; private final Integer currentMemberCount; private final Integer maxMemberCount; private final LocalDate enrollmentEndDate; diff --git a/backend/src/main/java/com/woowacourse/moamoa/study/query/data/StudyDetailsDataBuilder.java b/backend/src/main/java/com/woowacourse/moamoa/study/query/data/StudyDetailsDataBuilder.java index e693baf41..8f4bfa43a 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/study/query/data/StudyDetailsDataBuilder.java +++ b/backend/src/main/java/com/woowacourse/moamoa/study/query/data/StudyDetailsDataBuilder.java @@ -2,7 +2,7 @@ import static java.util.Objects.requireNonNull; -import com.woowacourse.moamoa.member.query.data.MemberData; +import com.woowacourse.moamoa.member.query.data.OwnerData; import java.time.LocalDate; public class StudyDetailsDataBuilder { @@ -13,7 +13,7 @@ public class StudyDetailsDataBuilder { private String thumbnail; private String recruitmentStatus; private String description; - private MemberData owner; + private OwnerData owner; private Integer currentMemberCount; private Integer maxMemberCount; private LocalDate createdDate; @@ -53,7 +53,7 @@ public StudyDetailsDataBuilder description(String description) { return this; } - public StudyDetailsDataBuilder owner(MemberData owner) { + public StudyDetailsDataBuilder owner(OwnerData owner) { this.owner = owner; return this; } diff --git a/backend/src/main/java/com/woowacourse/moamoa/study/service/response/StudyDetailResponse.java b/backend/src/main/java/com/woowacourse/moamoa/study/service/response/StudyDetailResponse.java index 255bc81b4..ddc4b09f4 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/study/service/response/StudyDetailResponse.java +++ b/backend/src/main/java/com/woowacourse/moamoa/study/service/response/StudyDetailResponse.java @@ -1,6 +1,6 @@ package com.woowacourse.moamoa.study.service.response; -import com.woowacourse.moamoa.member.query.data.MemberData; +import com.woowacourse.moamoa.member.query.data.OwnerData; import com.woowacourse.moamoa.member.query.data.ParticipatingMemberData; import com.woowacourse.moamoa.study.query.data.StudyDetailsData; import com.woowacourse.moamoa.tag.query.response.TagData; @@ -29,7 +29,7 @@ public class StudyDetailResponse { private LocalDate enrollmentEndDate; private LocalDate startDate; private LocalDate endDate; - private MemberData owner; + private OwnerData owner; private List members; private List tags; diff --git a/backend/src/test/java/com/woowacourse/moamoa/study/controller/SearchingStudyControllerTest.java b/backend/src/test/java/com/woowacourse/moamoa/study/controller/SearchingStudyControllerTest.java index a9ca47568..e8f6de7a6 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/study/controller/SearchingStudyControllerTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/study/controller/SearchingStudyControllerTest.java @@ -14,7 +14,7 @@ import com.woowacourse.moamoa.member.domain.Member; import com.woowacourse.moamoa.member.domain.repository.MemberRepository; import com.woowacourse.moamoa.member.query.MemberDao; -import com.woowacourse.moamoa.member.query.data.MemberData; +import com.woowacourse.moamoa.member.query.data.OwnerData; import com.woowacourse.moamoa.study.domain.repository.StudyRepository; import com.woowacourse.moamoa.study.query.StudyDetailsDao; import com.woowacourse.moamoa.study.query.StudySummaryDao; @@ -245,7 +245,7 @@ void getStudyDetails() { .status("RECRUITMENT_START").description("그린론의 우당탕탕 자바 스터디입니다.").createdDate(LocalDate.now()) // Study Participant .currentMemberCount(3).maxMemberCount(10) - .owner(new MemberData(jjanggu.getGithubId(), "jjanggu", "https://image", "github.com")) + .owner(new OwnerData(jjanggu.getGithubId(), "jjanggu", "https://image", "github.com", 5)) // Study Period .startDate(LocalDate.now()) .build(); @@ -280,7 +280,7 @@ void getStudyDetailsWithOptional() { .status("RECRUITMENT_START").description("디우의 뤼액트 스터디입니다.").createdDate(LocalDate.now()) // Study Participant .currentMemberCount(4).maxMemberCount(5) - .owner(new MemberData(dwoo.getGithubId(), "dwoo", "https://image", "github.com")) + .owner(new OwnerData(dwoo.getGithubId(), "dwoo", "https://image", "github.com", 3)) // Study Period .enrollmentEndDate(LocalDate.now()) .startDate(LocalDate.now()) @@ -318,7 +318,7 @@ void getNotHasParticipantsAndAttachedTagsStudyDetails() { .status("RECRUITMENT_START").description("Linux를 공부하자의 베루스입니다.").createdDate(LocalDate.now()) // Study Participant .currentMemberCount(1) - .owner(new MemberData(verus.getGithubId(), "verus", "https://image", "github.com")) + .owner(new OwnerData(verus.getGithubId(), "verus", "https://image", "github.com", 4)) // Study Period .startDate(LocalDate.now()) .enrollmentEndDate(LocalDate.now()) diff --git a/backend/src/test/java/com/woowacourse/moamoa/study/query/StudyDetailsDaoTest.java b/backend/src/test/java/com/woowacourse/moamoa/study/query/StudyDetailsDaoTest.java index 267a6b47a..f1f6fb0c7 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/study/query/StudyDetailsDaoTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/study/query/StudyDetailsDaoTest.java @@ -15,7 +15,7 @@ import com.woowacourse.moamoa.common.RepositoryTest; import com.woowacourse.moamoa.member.domain.Member; import com.woowacourse.moamoa.member.domain.repository.MemberRepository; -import com.woowacourse.moamoa.member.query.data.MemberData; +import com.woowacourse.moamoa.member.query.data.OwnerData; import com.woowacourse.moamoa.study.domain.Study; import com.woowacourse.moamoa.study.domain.repository.StudyRepository; import com.woowacourse.moamoa.study.query.data.StudyDetailsData; @@ -85,7 +85,7 @@ void getNotHasEnrollmentEndDateAndEndDateStudyDetails() { .description(알고리즘_스터디.getContent().getDescription()).createdDate(actual.getCreatedDate()) // Study Participants .currentMemberCount(알고리즘_스터디.getParticipants().getSize()) - .owner(new MemberData(베루스.getGithubId(), 베루스.getUsername(), 베루스.getImageUrl(), 베루스.getProfileUrl())) + .owner(new OwnerData(베루스.getGithubId(), 베루스.getUsername(), 베루스.getImageUrl(), 베루스.getProfileUrl(), 5)) // Study Period .startDate(알고리즘_스터디.getStudyPlanner().getStartDate()) .build(); @@ -107,7 +107,7 @@ void getNotHasMaxMemberCountStudyDetails() { .status(리눅스_스터디.getRecruitPlanner().getRecruitStatus().toString()).description(리눅스_스터디.getContent().getDescription()).createdDate(actual.getCreatedDate()) // Study Participant .currentMemberCount(리눅스_스터디.getParticipants().getSize()) - .owner(new MemberData(베루스.getGithubId(), 베루스.getUsername(), 베루스.getImageUrl(), 베루스.getProfileUrl())) + .owner(new OwnerData(베루스.getGithubId(), 베루스.getUsername(), 베루스.getImageUrl(), 베루스.getProfileUrl(), 5)) // Study Period .startDate(리눅스_스터디.getStudyPlanner().getStartDate()) .enrollmentEndDate(리눅스_스터디.getRecruitPlanner().getEnrollmentEndDate()) From 1cdfce7b36c29a51b94cfaa4750dbacd03e4bf81 Mon Sep 17 00:00:00 2001 From: Donggyu Date: Thu, 18 Aug 2022 19:01:46 +0900 Subject: [PATCH 43/51] =?UTF-8?q?[BE]=20=EC=8A=A4=ED=84=B0=EB=94=94?= =?UTF-8?q?=EC=9E=A5=EB=8F=84=20=EC=8A=A4=ED=84=B0=EB=94=94=20=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20=EB=82=A0=EC=A7=9C=20=ED=8F=AC=ED=95=A8=20(#295)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: 스터디장 또한 참여한 스터디 개수 표시 * feat: 스터디장도 가입날짜 포함 --- .../com/woowacourse/moamoa/member/query/data/OwnerData.java | 3 +++ .../com/woowacourse/moamoa/study/query/StudyDetailsDao.java | 5 +++-- .../test/study/GettingStudyDetailsAcceptanceTest.java | 2 ++ .../study/controller/SearchingStudyControllerTest.java | 6 +++--- .../woowacourse/moamoa/study/query/StudyDetailsDaoTest.java | 5 +++-- 5 files changed, 14 insertions(+), 7 deletions(-) diff --git a/backend/src/main/java/com/woowacourse/moamoa/member/query/data/OwnerData.java b/backend/src/main/java/com/woowacourse/moamoa/member/query/data/OwnerData.java index 55b45cd2a..b00e2855f 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/member/query/data/OwnerData.java +++ b/backend/src/main/java/com/woowacourse/moamoa/member/query/data/OwnerData.java @@ -3,6 +3,7 @@ import static lombok.AccessLevel.PRIVATE; import com.fasterxml.jackson.annotation.JsonProperty; +import java.time.LocalDate; import lombok.AllArgsConstructor; import lombok.EqualsAndHashCode; import lombok.Getter; @@ -25,5 +26,7 @@ public class OwnerData { private String profileUrl; + private LocalDate participationDate; + private int numberOfStudy; } diff --git a/backend/src/main/java/com/woowacourse/moamoa/study/query/StudyDetailsDao.java b/backend/src/main/java/com/woowacourse/moamoa/study/query/StudyDetailsDao.java index 0bdbc7a98..9d2e19d1f 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/study/query/StudyDetailsDao.java +++ b/backend/src/main/java/com/woowacourse/moamoa/study/query/StudyDetailsDao.java @@ -27,7 +27,7 @@ public Optional findBy(Long studyId) { "SELECT study.id, title, excerpt, thumbnail, recruitment_status, description, current_member_count, " + "max_member_count, created_at, enrollment_end_date, start_date, end_date, owner_id, " + "member.github_id as owner_github_id, member.username as owner_username, " - + "member.image_url as owner_image_url, member.profile_url as owner_profile_url, " + + "member.image_url as owner_image_url, member.profile_url as owner_profile_url, created_at as participation_date, " + countOfStudy() + "FROM study JOIN member ON study.owner_id = member.id " + "WHERE study.id = ?"; @@ -97,10 +97,11 @@ private void appendParticipants(final ResultSet rs) throws SQLException { String profileUrl = rs.getString("owner_profile_url"); int numberOfStudy = rs.getInt("number_of_study"); + LocalDate participationDate = rs.getObject("participation_date", LocalDate.class); builder.currentMemberCount(currentMaxCount) .maxMemberCount(maxMemberCount) - .owner(new OwnerData(githubId, username, imageUrl, profileUrl, numberOfStudy)); + .owner(new OwnerData(githubId, username, imageUrl, profileUrl, participationDate, numberOfStudy)); } } } diff --git a/backend/src/test/java/com/woowacourse/acceptance/test/study/GettingStudyDetailsAcceptanceTest.java b/backend/src/test/java/com/woowacourse/acceptance/test/study/GettingStudyDetailsAcceptanceTest.java index 66d642e72..310a9f15a 100644 --- a/backend/src/test/java/com/woowacourse/acceptance/test/study/GettingStudyDetailsAcceptanceTest.java +++ b/backend/src/test/java/com/woowacourse/acceptance/test/study/GettingStudyDetailsAcceptanceTest.java @@ -79,6 +79,8 @@ void getStudyDetails() { .body("owner.username", is(디우_이름)) .body("owner.imageUrl", is(디우_이미지_URL)) .body("owner.profileUrl", is(디우_프로필_URL)) + .body("owner.participantDate", not(empty())) + .body("owner.numberOfStudy", is(1)) .body("members.id", not(empty())) .body("members.username", contains(짱구_이름, 그린론_이름, 베루스_이름)) .body("members.imageUrl", contains(짱구_이미지_URL, 그린론_이미지_URL, 베루스_이미지_URL)) diff --git a/backend/src/test/java/com/woowacourse/moamoa/study/controller/SearchingStudyControllerTest.java b/backend/src/test/java/com/woowacourse/moamoa/study/controller/SearchingStudyControllerTest.java index e8f6de7a6..777c278c3 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/study/controller/SearchingStudyControllerTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/study/controller/SearchingStudyControllerTest.java @@ -245,7 +245,7 @@ void getStudyDetails() { .status("RECRUITMENT_START").description("그린론의 우당탕탕 자바 스터디입니다.").createdDate(LocalDate.now()) // Study Participant .currentMemberCount(3).maxMemberCount(10) - .owner(new OwnerData(jjanggu.getGithubId(), "jjanggu", "https://image", "github.com", 5)) + .owner(new OwnerData(jjanggu.getGithubId(), "jjanggu", "https://image", "github.com", LocalDate.now(), 5)) // Study Period .startDate(LocalDate.now()) .build(); @@ -280,7 +280,7 @@ void getStudyDetailsWithOptional() { .status("RECRUITMENT_START").description("디우의 뤼액트 스터디입니다.").createdDate(LocalDate.now()) // Study Participant .currentMemberCount(4).maxMemberCount(5) - .owner(new OwnerData(dwoo.getGithubId(), "dwoo", "https://image", "github.com", 3)) + .owner(new OwnerData(dwoo.getGithubId(), "dwoo", "https://image", "github.com", LocalDate.now(),3)) // Study Period .enrollmentEndDate(LocalDate.now()) .startDate(LocalDate.now()) @@ -318,7 +318,7 @@ void getNotHasParticipantsAndAttachedTagsStudyDetails() { .status("RECRUITMENT_START").description("Linux를 공부하자의 베루스입니다.").createdDate(LocalDate.now()) // Study Participant .currentMemberCount(1) - .owner(new OwnerData(verus.getGithubId(), "verus", "https://image", "github.com", 4)) + .owner(new OwnerData(verus.getGithubId(), "verus", "https://image", "github.com", LocalDate.now(), 4)) // Study Period .startDate(LocalDate.now()) .enrollmentEndDate(LocalDate.now()) diff --git a/backend/src/test/java/com/woowacourse/moamoa/study/query/StudyDetailsDaoTest.java b/backend/src/test/java/com/woowacourse/moamoa/study/query/StudyDetailsDaoTest.java index f1f6fb0c7..ae3386156 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/study/query/StudyDetailsDaoTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/study/query/StudyDetailsDaoTest.java @@ -19,6 +19,7 @@ import com.woowacourse.moamoa.study.domain.Study; import com.woowacourse.moamoa.study.domain.repository.StudyRepository; import com.woowacourse.moamoa.study.query.data.StudyDetailsData; +import java.time.LocalDate; import java.util.Set; import javax.persistence.EntityManager; import org.junit.jupiter.api.BeforeEach; @@ -85,7 +86,7 @@ void getNotHasEnrollmentEndDateAndEndDateStudyDetails() { .description(알고리즘_스터디.getContent().getDescription()).createdDate(actual.getCreatedDate()) // Study Participants .currentMemberCount(알고리즘_스터디.getParticipants().getSize()) - .owner(new OwnerData(베루스.getGithubId(), 베루스.getUsername(), 베루스.getImageUrl(), 베루스.getProfileUrl(), 5)) + .owner(new OwnerData(베루스.getGithubId(), 베루스.getUsername(), 베루스.getImageUrl(), 베루스.getProfileUrl(), LocalDate.now(), 5)) // Study Period .startDate(알고리즘_스터디.getStudyPlanner().getStartDate()) .build(); @@ -107,7 +108,7 @@ void getNotHasMaxMemberCountStudyDetails() { .status(리눅스_스터디.getRecruitPlanner().getRecruitStatus().toString()).description(리눅스_스터디.getContent().getDescription()).createdDate(actual.getCreatedDate()) // Study Participant .currentMemberCount(리눅스_스터디.getParticipants().getSize()) - .owner(new OwnerData(베루스.getGithubId(), 베루스.getUsername(), 베루스.getImageUrl(), 베루스.getProfileUrl(), 5)) + .owner(new OwnerData(베루스.getGithubId(), 베루스.getUsername(), 베루스.getImageUrl(), 베루스.getProfileUrl(), LocalDate.now(), 5)) // Study Period .startDate(리눅스_스터디.getStudyPlanner().getStartDate()) .enrollmentEndDate(리눅스_스터디.getRecruitPlanner().getEnrollmentEndDate()) From d43114a1bd5dd9439a4b3ea5f0e83c8aa6cf1213 Mon Sep 17 00:00:00 2001 From: jaeseo yoo Date: Thu, 18 Aug 2022 19:52:35 +0900 Subject: [PATCH 44/51] =?UTF-8?q?refactor:=20REFRESH=5FTOKEN=5FEXPIRATION?= =?UTF-8?q?=20=EC=83=81=EC=88=98=20=EB=B2=84=EA=B7=B8=20=EC=88=98=EC=A0=95?= =?UTF-8?q?=20(#296)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../moamoa/auth/infrastructure/JwtTokenProvider.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/main/java/com/woowacourse/moamoa/auth/infrastructure/JwtTokenProvider.java b/backend/src/main/java/com/woowacourse/moamoa/auth/infrastructure/JwtTokenProvider.java index b6b74fbf0..468d0a062 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/auth/infrastructure/JwtTokenProvider.java +++ b/backend/src/main/java/com/woowacourse/moamoa/auth/infrastructure/JwtTokenProvider.java @@ -18,7 +18,7 @@ @Component public class JwtTokenProvider implements TokenProvider { - private static final long REFRESH_TOKEN_EXPIRATION = 7 * 24 * 60 * 60 * 1000; // 7일 + private static final long REFRESH_TOKEN_EXPIRATION = 7 * 24 * 60 * 60 * 1000L; // 7일 private final SecretKey key; private final long validityInMilliseconds; From 096e14e7c118aad7642705cad4e7705d337ea7f0 Mon Sep 17 00:00:00 2001 From: TaeYoon Date: Thu, 18 Aug 2022 20:25:13 +0900 Subject: [PATCH 45/51] =?UTF-8?q?[FE]=20=EC=8A=A4=ED=84=B0=EB=94=94=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=EA=B8=B0=EB=8A=A5=20(#297)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: 링크 폼에서 유저 네임도 보이도록 수정 * feat: 스터디 수정 모킹 서버 구현 * feat: 스터디 상세페이지에 수정 버튼 추가 스터디 OWNER일 때만 수정 버튼이 보이도록 구현 * chore: 스터디 수정 경로 추가 * feat: 스터디 수정 기능 구현 * feat: 타입 수정 * feat: 스터디 상세 페이지 스터디 시작 날짜, 가입 스터디 개수 추가 - owner 응답에 아직 participationDate, numberOfStudy가 제공되지 않음 * fix: 폼 작성시 선택한 태그가 submit 되지 않는 오류 수정 * feat: 스터디장 스터디 개수 및 스터디 가입날짜 추가 * feat: 스터디 수정 페이지 수정 * fix: 스터디최대인원 없을 시 처리 무한대 표시 * feat: 링크 미리보기 에러 처리 추가 --- frontend/.prettierrc.json | 1 + frontend/.storybook/main.js | 1 + frontend/env/.env.prod | 2 +- frontend/src/App.tsx | 6 + frontend/src/api/link-preview/index.ts | 1 - frontend/src/api/member/index.ts | 2 +- frontend/src/api/study/index.ts | 12 +- frontend/src/components/checkbox/Checkbox.tsx | 22 +- frontend/src/constants.ts | 1 + frontend/src/custom-types/index.ts | 59 +- frontend/src/mocks/detailStudyHandlers.ts | 104 +- frontend/src/mocks/studies.json | 3897 ++++------------- frontend/src/mocks/tags.json | 9 + .../create-study-page/CreateStudyPage.tsx | 2 +- .../components/category/Category.tsx | 26 +- .../description-tab/DescriptionTab.tsx | 9 +- .../enrollment-end-date/EnrollmentEndDate.tsx | 10 +- .../components/excerpt/Excerpt.tsx | 10 +- .../max-member-count/MaxMemberCount.tsx | 16 +- .../period/{Peroid.tsx => Period.tsx} | 22 +- .../components/publish/Publish.tsx | 13 +- .../components/subject/Subject.tsx | 19 +- .../components/title/Title.tsx | 15 +- frontend/src/pages/detail-page/DetailPage.tsx | 8 +- .../components/head/Head.stories.tsx | 70 +- .../components/head/Head.style.tsx | 28 + .../detail-page/components/head/Head.tsx | 40 +- .../study-member-card/StudyMemberCard.tsx | 1 - .../StudyMemberSection.stories.tsx | 32 +- .../StudyMemberSection.tsx | 46 +- .../StudyWideFloatBox.tsx | 2 +- .../edit-study-page/EditStudyPage.style.tsx | 8 + .../pages/edit-study-page/EditStudyPage.tsx | 79 + .../edit-study-page/hooks/useEditStudyPage.ts | 86 + .../filter-section/FilterSection.tsx | 4 +- .../FilterButtonList.stories.tsx | 22 +- .../filter-button-list/FilterButtonList.tsx | 6 +- .../src/pages/main-page/hooks/useMainPage.ts | 4 +- .../MyStudyCardListSection.stories.tsx | 38 - .../link-edit-form/LinkEditForm.tsx | 2 +- .../components/link-form/LinkForm.tsx | 2 +- .../components/link-preview/LinkPreview.tsx | 52 +- frontend/tsconfig.json | 1 + frontend/webpack/webpack.common.js | 1 + 44 files changed, 1415 insertions(+), 3376 deletions(-) rename frontend/src/pages/create-study-page/components/period/{Peroid.tsx => Period.tsx} (67%) create mode 100644 frontend/src/pages/edit-study-page/EditStudyPage.style.tsx create mode 100644 frontend/src/pages/edit-study-page/EditStudyPage.tsx create mode 100644 frontend/src/pages/edit-study-page/hooks/useEditStudyPage.ts diff --git a/frontend/.prettierrc.json b/frontend/.prettierrc.json index 382545cc9..bf6c46919 100644 --- a/frontend/.prettierrc.json +++ b/frontend/.prettierrc.json @@ -32,6 +32,7 @@ "^@main-page/(.*)$", "^@detail-page/(.*)$", "^@create-study-page/(.*)$", + "^@edit-study-page/(.*)$", "^@my-study-page/(.*)$", "^@study-room-page/(.*)$", "^@error-page/(.*)$", diff --git a/frontend/.storybook/main.js b/frontend/.storybook/main.js index 4db72d588..154442687 100644 --- a/frontend/.storybook/main.js +++ b/frontend/.storybook/main.js @@ -26,6 +26,7 @@ module.exports = { '@detail-page': resolve(__dirname, '../src/pages/detail-page'), '@main-page': resolve(__dirname, '../src/pages/main-page'), '@create-study-page': resolve(__dirname, '../src/pages/create-study-page'), + '@edit-study-page': resolve(__dirname, '../src/pages/edit-study-page'), '@my-study-page': resolve(__dirname, '../src/pages/my-study-page'), '@study-room-page': resolve(__dirname, '../src/pages/study-room-page'), '@login-redirect-page': resolve(__dirname, '../src/pages/login-redirect-page'), diff --git a/frontend/env/.env.prod b/frontend/env/.env.prod index b93b57072..fb4b0d0f8 100644 --- a/frontend/env/.env.prod +++ b/frontend/env/.env.prod @@ -1,3 +1,3 @@ API_URL="https://api.moamoa.space" -CLIENT_ID="cb83d95cd5644436b090" +CLIENT_ID="cf7c0528216f765c83c0" LINK_PREVIEW_API_URL="https://api.og.moamoa.space" diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 12fd499c1..945790a07 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -17,6 +17,8 @@ import { useAuth } from '@hooks/useAuth'; import { Footer, Header, Main } from '@layout'; +import EditStudyPage from '@edit-study-page/EditStudyPage'; + const App = () => { const { isLoggedIn } = useAuth(); @@ -43,6 +45,10 @@ const App = () => { path={PATH.STUDY_ROOM()} element={isLoggedIn ? : } /> + : } + /> } /> diff --git a/frontend/src/api/link-preview/index.ts b/frontend/src/api/link-preview/index.ts index 644b74423..4ed0f1186 100644 --- a/frontend/src/api/link-preview/index.ts +++ b/frontend/src/api/link-preview/index.ts @@ -16,7 +16,6 @@ export type GetLinkPreviewResponseData = { }; const axiosInstance = axios.create({ - // TODO: 링크 미리보기 서버 배포 url로 변경 필요 baseURL: process.env.LINK_PREVIEW_API_URL, headers: { 'Content-Type': 'application/json', diff --git a/frontend/src/api/member/index.ts b/frontend/src/api/member/index.ts index 1c6f10a8c..5d72704a0 100644 --- a/frontend/src/api/member/index.ts +++ b/frontend/src/api/member/index.ts @@ -25,7 +25,7 @@ export const getUserRole = async ({ studyId }: GetUserRoleRequestParams) => { }; export const useGetUserRole = ({ studyId, options }: UseGetUserRole) => - useQuery('my-role', () => getUserRole({ studyId }), options); + useQuery(['my-role', studyId], () => getUserRole({ studyId }), options); // user info export type GetUserInformationResponseData = Member; diff --git a/frontend/src/api/study/index.ts b/frontend/src/api/study/index.ts index e59158185..be2f85be7 100644 --- a/frontend/src/api/study/index.ts +++ b/frontend/src/api/study/index.ts @@ -1,7 +1,7 @@ import type { AxiosError, AxiosResponse } from 'axios'; import { useMutation, useQuery } from 'react-query'; -import type { MakeOptional, StudyDetail, TagId } from '@custom-types'; +import type { MakeOptional, StudyDetail, StudyId, TagId } from '@custom-types'; import axiosInstance from '@api/axiosInstance'; @@ -44,3 +44,13 @@ export const useGetStudy = ({ studyId }: GetStudyRequestParams) => { }; // put +export type PutStudyRequestParams = { studyId: StudyId }; +export type PutStudyRequestBody = PostStudyRequestBody; +export type PutStudyRequestVariables = PutStudyRequestParams & { editedStudy: PutStudyRequestBody }; + +export const putStudy = async ({ studyId, editedStudy }: PutStudyRequestVariables) => { + const response = await axiosInstance.put>(`/api/studies/${studyId}`, editedStudy); + return response.data; +}; + +export const usePutStudy = () => useMutation(putStudy); diff --git a/frontend/src/components/checkbox/Checkbox.tsx b/frontend/src/components/checkbox/Checkbox.tsx index 79a5290b8..df00403c4 100644 --- a/frontend/src/components/checkbox/Checkbox.tsx +++ b/frontend/src/components/checkbox/Checkbox.tsx @@ -1,9 +1,23 @@ +import { forwardRef } from 'react'; + import * as S from '@components/checkbox/Checkbox.style'; -export type CheckboxProps = React.HTMLProps; +export type CheckboxProps = React.HTMLProps & { dataTagId: number }; + +const Checkbox = forwardRef(({ className, id, checked, onChange, dataTagId }, ref) => { + return ( + + ); +}); -const Checkbox: React.FC = ({ className, id, checked, onChange }) => { - return ; -}; +Checkbox.displayName = 'Checkbox'; export default Checkbox; diff --git a/frontend/src/constants.ts b/frontend/src/constants.ts index 0819f4d62..8abb641f3 100644 --- a/frontend/src/constants.ts +++ b/frontend/src/constants.ts @@ -2,6 +2,7 @@ export const PATH = { MAIN: '/', STUDY_DETAIL: (studyId: ':studyId' | number = ':studyId') => `/study/${studyId}`, CREATE_STUDY: '/study/create', + EDIT_STUDY: (studyId: ':studyId' | number = ':studyId') => `/study/edit/${studyId}`, MY_STUDY: '/my/study', STUDY_ROOM: (studyId: ':studyId' | number = ':studyId') => `/study/room/${studyId}`, LOGIN: '/login', diff --git a/frontend/src/custom-types/index.ts b/frontend/src/custom-types/index.ts index b87501a51..8c4f8ea83 100644 --- a/frontend/src/custom-types/index.ts +++ b/frontend/src/custom-types/index.ts @@ -28,14 +28,6 @@ export type LinkId = number; export type Page = number; export type Size = number; -export type Study = { - id: StudyId; - title: string; - excerpt: string; - thumbnail: string; - recruitmentStatus: RecruitmentStatus; -}; - export type Owner = { id: MemberId; username: string; @@ -50,7 +42,18 @@ export type Member = { profileUrl: string; }; -export type StudyTag = { id: TagId; name: string }; +export type CategoryName = 'generation' | 'area' | 'subject'; + +export type Tag = { + id: TagId; + name: string; + description: string; + category: { + id: CategoryId; + name: CategoryName; + }; +}; +export type TagInfo = Pick & { categoryName: CategoryName }; export type StudyDetail = { id: StudyId; @@ -61,41 +64,31 @@ export type StudyDetail = { description: string; currentMemberCount: number; maxMemberCount?: number; - createdDate: string; - enrollmentEndDate?: string; - startDate: string; - endDate?: string; - owner: Owner; - members: Array; - tags: Array; -} & Study; + createdDate: DateYMD; + enrollmentEndDate?: DateYMD; + startDate: DateYMD; + endDate?: DateYMD; + owner: Owner & { participationDate: DateYMD; numberOfStudy: number }; + members: Array; + tags: Array; +}; + +export type Study = Pick; export type StudyReview = { id: ReviewId; member: Member; createdDate: DateYMD; - lastModifiedDate: string; + lastModifiedDate: DateYMD; content: string; }; -export type Tag = { - id: TagId; - name: string; - description: string; - category: { - id: CategoryId; - name: string; - }; -}; -export type TagInfo = Pick & { categoryName: Tag['category']['name'] }; - export type StudyStatus = 'PREPARE' | 'IN_PROGRESS' | 'DONE'; -export type MyStudy = Pick< - StudyDetail, - 'id' | 'title' | 'currentMemberCount' | 'maxMemberCount' | 'startDate' | 'endDate' | 'owner' | 'tags' -> & { +export type MyStudy = Pick & { studyStatus: StudyStatus; + tags: Array>; + owner: Owner; }; export type UserRole = 'OWNER' | 'MEMBER' | 'NON_MEMBER'; diff --git a/frontend/src/mocks/detailStudyHandlers.ts b/frontend/src/mocks/detailStudyHandlers.ts index fd16894fe..0f7d40c10 100644 --- a/frontend/src/mocks/detailStudyHandlers.ts +++ b/frontend/src/mocks/detailStudyHandlers.ts @@ -1,8 +1,10 @@ import { rest } from 'msw'; -import reviewJSON from '@mocks/reviews.json'; +import { user } from '@mocks/memberHandlers'; import studiesJSON from '@mocks/studies.json'; +import type { PostStudyRequestBody, PutStudyRequestBody } from '@api/study'; + const detailStudyHandlers = [ rest.get('/api/studies/:studyId', (req, res, ctx) => { const studyId = req.params.studyId; @@ -13,25 +15,87 @@ const detailStudyHandlers = [ return res(ctx.status(200), ctx.json(study)); }), - rest.get('/api/studies/:studyId/reviews', (req, res, ctx) => { - const size = req.url.searchParams.get('size'); - if (size) { - const sizeNum = Number(size); - return res( - ctx.status(200), - ctx.json({ - reviews: reviewJSON.reviews.slice(0, sizeNum), - totalCount: reviewJSON.reviews.length, - }), - ); - } - return res( - ctx.status(200), - ctx.json({ - reviews: reviewJSON.reviews, - totalCount: reviewJSON.reviews.length, - }), - ); + rest.post('/api/studies', (req, res, ctx) => { + const studyId = req.params.studyId; + const { thumbnail, title, description, excerpt, enrollmentEndDate, endDate, startDate, maxMemberCount } = req.body; + + if (!studyId) return res(ctx.status(400), ctx.json({ message: '스터디 아이디가 없음' })); + + const { studies } = studiesJSON; + + const isExist = studies.some(study => study.id === Number(studyId)); + if (isExist) return res(ctx.status(400), ctx.json({ message: '이미 존재하는 스터디' })); + + studiesJSON.studies = [ + { + id: Number(studyId), + thumbnail, + title, + description, + excerpt, + endDate: endDate ?? '', + enrollmentEndDate: enrollmentEndDate ?? '', + startDate, + maxMemberCount: maxMemberCount ?? 100, + recruitmentStatus: 'OPEN', + createdDate: '2022-08-18', + currentMemberCount: 1, + owner: user, + members: [user], + tags: [ + { + id: 2, + name: '4기', + description: '우테코4기', + category: { + id: 1, + name: 'generation', + }, + }, + { + id: 4, + name: 'FE', + description: '프론트엔드', + category: { + id: 2, + name: 'area', + }, + }, + { + id: 5, + name: 'React', + description: '리액트', + category: { + id: 3, + name: 'subject', + }, + }, + ], + }, + ...studies, + ]; + + return res(ctx.status(200)); + }), + rest.put('/api/studies/:studyId', (req, res, ctx) => { + const studyId = req.params.studyId; + const editedStudy = req.body; + + if (!studyId) return res(ctx.status(400), ctx.json({ message: '스터디 아이디가 없음' })); + + const { studies } = studiesJSON; + + const isExist = studies.some(study => study.id === Number(studyId)); + if (!isExist) return res(ctx.status(404), ctx.json({ message: '해당하는 스터디 없음' })); + + studiesJSON.studies = studies.map(study => { + if (study.id === Number(studyId)) { + return { ...study, ...editedStudy }; + } + return study; + }); + + return res(ctx.status(200)); }), ]; diff --git a/frontend/src/mocks/studies.json b/frontend/src/mocks/studies.json index 241c85dce..47509a8b0 100644 --- a/frontend/src/mocks/studies.json +++ b/frontend/src/mocks/studies.json @@ -9,7 +9,6 @@ "description": "# Prettier plugin sort imports\n\nA prettier plugin to sort import declarations by provided Regular Expression order.\n\n**Note: If you are migrating from v2.x.x to v3.x.x, [Please Read Migration Guidelines](./docs/MIGRATION.md)**\n\n### Input\n\n```javascript\nimport React, {\n FC,\n useEffect,\n useRef,\n ChangeEvent,\n KeyboardEvent,\n} from \"react\";\nimport { logger } from \"@core/logger\";\nimport { reduce, debounce } from \"lodash\";\nimport { Message } from \"../Message\";\nimport { createServer } from \"@server/node\";\nimport { Alert } from \"@ui/Alert\";\nimport { repeat, filter, add } from \"../utils\";\nimport { initializeApp } from \"@core/app\";\nimport { Popup } from \"@ui/Popup\";\nimport { createConnection } from \"@server/database\";\n```\n\n### Output\n\n```javascript\nimport { debounce, reduce } from \"lodash\";\nimport React, {\n ChangeEvent,\n FC,\n KeyboardEvent,\n useEffect,\n useRef,\n} from \"react\";\n\nimport { createConnection } from \"@server/database\";\nimport { createServer } from \"@server/node\";\n\nimport { initializeApp } from \"@core/app\";\nimport { logger } from \"@core/logger\";\n\nimport { Alert } from \"@ui/Alert\";\nimport { Popup } from \"@ui/Popup\";\n\nimport { Message } from \"../Message\";\nimport { add, filter, repeat } from \"../utils\";\n```\n", "currentMemberCount": 40, "maxMemberCount": 40, - "createdAt": "createdAt", "enrollmentEndDate": "2022-07-18", "startDate": "2022-07-02", "endDate": "2022-08-22", @@ -17,248 +16,72 @@ "id": 1, "username": "jaejae-yoo", "imageUrl": "https://picsum.photos/id/46/200/300", - "profileUrl": "https://github.com/user/jaejae-yoo" + "profileUrl": "https://github.com/user/jaejae,-yoo", + "participationDate": "2022-08-10" }, "members": [ { - "id": "1", + "id": 1, "username": "RPx_uZ", "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" }, { - "id": "2", + "id": 2, "username": "-ndL7X", "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" }, { - "id": "3", + "id": 3, "username": "6ybs9u", "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" }, { - "id": "4", + "id": 4, "username": "_9e_v0", "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" }, { - "id": "5", + "id": 5, "username": "IcTu0U", "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" }, { - "id": "6", + "id": 6, "username": "tffF2o", "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" }, { - "id": "7", + "id": 7, "username": "3z21C7", "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" }, { - "id": "8", + "id": 8, "username": "cQOvXv", "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" }, { - "id": "9", + "id": 9, "username": "hfN4EK", "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "10", - "username": "iPPH8t", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "11", - "username": "qGf-fE", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "12", - "username": "aPDVzD", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "13", - "username": "X-MIYs", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "14", - "username": "SkoVQi", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "15", - "username": "lfsh9s", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "16", - "username": "FlM-Yh", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "17", - "username": "7wp7tZ", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "18", - "username": "YC1kF-", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "19", - "username": "idGAKZ", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "20", - "username": "_yEEX9", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "21", - "username": "bNdffN", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "22", - "username": "WePoW_", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "23", - "username": "3YjXpp", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "24", - "username": "3vGvH0", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "25", - "username": "j5Es-z", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "26", - "username": "o_0Mxw", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "27", - "username": "3rNvu2", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "28", - "username": "MeN3vM", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "29", - "username": "Lls3_J", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "30", - "username": "RSRler", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "31", - "username": "uMkhe6", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "32", - "username": "5TMHkD", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "33", - "username": "oS5hek", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "34", - "username": "cFqPtI", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "35", - "username": "zXWhJM", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "36", - "username": "KKewrB", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "37", - "username": "3qNOzw", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "38", - "username": "A8Ot7B", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "39", - "username": "aYsKUn", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "40", - "username": "pTNHJL", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" } ], "tags": [ @@ -292,7 +115,6 @@ "description": "# Prettier plugin sort imports\n\nA prettier plugin to sort import declarations by provided Regular Expression order.\n\n**Note: If you are migrating from v2.x.x to v3.x.x, [Please Read Migration Guidelines](./docs/MIGRATION.md)**\n\n### Input\n\n```javascript\nimport React, {\n FC,\n useEffect,\n useRef,\n ChangeEvent,\n KeyboardEvent,\n} from \"react\";\nimport { logger } from \"@core/logger\";\nimport { reduce, debounce } from \"lodash\";\nimport { Message } from \"../Message\";\nimport { createServer } from \"@server/node\";\nimport { Alert } from \"@ui/Alert\";\nimport { repeat, filter, add } from \"../utils\";\nimport { initializeApp } from \"@core/app\";\nimport { Popup } from \"@ui/Popup\";\nimport { createConnection } from \"@server/database\";\n```\n\n### Output\n\n```javascript\nimport { debounce, reduce } from \"lodash\";\nimport React, {\n ChangeEvent,\n FC,\n KeyboardEvent,\n useEffect,\n useRef,\n} from \"react\";\n\nimport { createConnection } from \"@server/database\";\nimport { createServer } from \"@server/node\";\n\nimport { initializeApp } from \"@core/app\";\nimport { logger } from \"@core/logger\";\n\nimport { Alert } from \"@ui/Alert\";\nimport { Popup } from \"@ui/Popup\";\n\nimport { Message } from \"../Message\";\nimport { add, filter, repeat } from \"../utils\";\n```\n", "currentMemberCount": 25, "maxMemberCount": 38, - "createdAt": "createdAt", "enrollmentEndDate": "2022-07-03", "startDate": "2022-07-29", "endDate": "2022-08-22", @@ -300,158 +122,23 @@ "id": 1, "username": "jaejae-yoo", "imageUrl": "https://picsum.photos/id/46/200/300", - "profileUrl": "https://github.com/user/jaejae-yoo" + "profileUrl": "https://github.com/user/jaejae,-yoo", + "participationDate": "2022-08-10" }, "members": [ { - "id": "1", + "id": 1, "username": "SsDqk7", "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" }, { - "id": "2", + "id": 2, "username": "InG1JW", "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "3", - "username": "Ak8c2B", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "4", - "username": "abQO6y", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "5", - "username": "ynPjxP", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "6", - "username": "LywSVR", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "7", - "username": "Qlxx86", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "8", - "username": "GGFRQ-", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "9", - "username": "go8IuP", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "10", - "username": "4HLLya", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "11", - "username": "m_yibZ", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "12", - "username": "3nLQHZ", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "13", - "username": "Q_3SaD", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "14", - "username": "HWeL5s", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "15", - "username": "dbDnP7", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "16", - "username": "oFC-G7", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "17", - "username": "2v4GYR", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "18", - "username": "yMeFJo", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "19", - "username": "Fx3sQO", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "20", - "username": "pJctuB", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "21", - "username": "ezz_1Y", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "22", - "username": "j3hhAj", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "23", - "username": "uSjS5I", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "24", - "username": "4wcgwC", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "25", - "username": "ueTjYb", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" } ], "tags": [ @@ -496,36 +183,6 @@ "name": "TDD", "description": "테스트", "category": { "id": 3, "name": "subject" } - }, - { - "id": 8, - "name": "Alg", - "description": "알고리즘", - "category": { "id": 3, "name": "subject" } - }, - { - "id": 9, - "name": "Book", - "description": "독서", - "category": { "id": 3, "name": "subject" } - }, - { - "id": 10, - "name": "Health", - "description": "운동", - "category": { "id": 3, "name": "subject" } - }, - { - "id": 11, - "name": "Network", - "description": "네트워크", - "category": { "id": 3, "name": "subject" } - }, - { - "id": 12, - "name": "CS", - "description": "컴퓨터 과학", - "category": { "id": 3, "name": "subject" } } ], "createdDate": "2022-07-21" @@ -539,7 +196,6 @@ "description": "# Prettier plugin sort imports\n\nA prettier plugin to sort import declarations by provided Regular Expression order.\n\n**Note: If you are migrating from v2.x.x to v3.x.x, [Please Read Migration Guidelines](./docs/MIGRATION.md)**\n\n### Input\n\n```javascript\nimport React, {\n FC,\n useEffect,\n useRef,\n ChangeEvent,\n KeyboardEvent,\n} from \"react\";\nimport { logger } from \"@core/logger\";\nimport { reduce, debounce } from \"lodash\";\nimport { Message } from \"../Message\";\nimport { createServer } from \"@server/node\";\nimport { Alert } from \"@ui/Alert\";\nimport { repeat, filter, add } from \"../utils\";\nimport { initializeApp } from \"@core/app\";\nimport { Popup } from \"@ui/Popup\";\nimport { createConnection } from \"@server/database\";\n```\n\n### Output\n\n```javascript\nimport { debounce, reduce } from \"lodash\";\nimport React, {\n ChangeEvent,\n FC,\n KeyboardEvent,\n useEffect,\n useRef,\n} from \"react\";\n\nimport { createConnection } from \"@server/database\";\nimport { createServer } from \"@server/node\";\n\nimport { initializeApp } from \"@core/app\";\nimport { logger } from \"@core/logger\";\n\nimport { Alert } from \"@ui/Alert\";\nimport { Popup } from \"@ui/Popup\";\n\nimport { Message } from \"../Message\";\nimport { add, filter, repeat } from \"../utils\";\n```\n", "currentMemberCount": 23, "maxMemberCount": 40, - "createdAt": "createdAt", "enrollmentEndDate": "2022-07-29", "startDate": "2022-07-28", "endDate": "2022-08-23", @@ -547,166 +203,71 @@ "id": 1, "username": "jaejae-yoo", "imageUrl": "https://picsum.photos/id/46/200/300", - "profileUrl": "https://github.com/user/jaejae-yoo" + "profileUrl": "https://github.com/user/jaejae,-yoo", + "participationDate": "2022-08-10" }, "members": [ { - "id": "1", + "id": 1, "username": "wVYysj", "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" }, { - "id": "2", + "id": 2, "username": "XWUfOY", "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" }, { - "id": "3", + "id": 3, "username": "P8cD8H", "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" }, { - "id": "4", + "id": 4, "username": "twK-Uk", "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" }, { - "id": "5", + "id": 5, "username": "sc5kvP", "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" }, { - "id": "6", + "id": 6, "username": "SVNstq", "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "7", - "username": "WnleQm", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "8", - "username": "Xkrqbn", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "9", - "username": "sGw3AT", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "10", - "username": "C8k_Td", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" + } + ], + "tags": [ { - "id": "11", - "username": "eYQIaM", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" + "id": 1, + "name": "JS", + "description": "자바스크립트", + "category": { "id": 3, "name": "subject" } }, { - "id": "12", - "username": "D-sASK", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" + "id": 2, + "name": "Java", + "description": "자바", + "category": { "id": 3, "name": "subject" } }, { - "id": "13", - "username": "OtoDw7", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "14", - "username": "3RR9dl", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "15", - "username": "bfSk9D", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "16", - "username": "B1Mrjq", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "17", - "username": "Gy009P", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "18", - "username": "OAWfcf", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "19", - "username": "0IY2Wg", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "20", - "username": "jLrumO", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "21", - "username": "qdwQL3", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "22", - "username": "qojicF", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "23", - "username": "V-2iS8", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - } - ], - "tags": [ - { - "id": 1, - "name": "JS", - "description": "자바스크립트", - "category": { "id": 3, "name": "subject" } - }, - { - "id": 2, - "name": "Java", - "description": "자바", - "category": { "id": 3, "name": "subject" } - }, - { - "id": 3, - "name": "React", - "description": "리액트", - "category": { "id": 3, "name": "subject" } + "id": 3, + "name": "React", + "description": "리액트", + "category": { "id": 3, "name": "subject" } }, { "id": 4, @@ -719,2199 +280,65 @@ }, { "id": 15586900, - "title": "2022-gugu-spring-study", - "excerpt": "레벨1 구구조의 제로가 이끄는 스프링 스터디", - "thumbnail": "https://picsum.photos/id/84/200/300", - "recruitmentStatus": "RECRUITMENT_END", - "description": "# Prettier plugin sort imports\n\nA prettier plugin to sort import declarations by provided Regular Expression order.\n\n**Note: If you are migrating from v2.x.x to v3.x.x, [Please Read Migration Guidelines](./docs/MIGRATION.md)**\n\n### Input\n\n```javascript\nimport React, {\n FC,\n useEffect,\n useRef,\n ChangeEvent,\n KeyboardEvent,\n} from \"react\";\nimport { logger } from \"@core/logger\";\nimport { reduce, debounce } from \"lodash\";\nimport { Message } from \"../Message\";\nimport { createServer } from \"@server/node\";\nimport { Alert } from \"@ui/Alert\";\nimport { repeat, filter, add } from \"../utils\";\nimport { initializeApp } from \"@core/app\";\nimport { Popup } from \"@ui/Popup\";\nimport { createConnection } from \"@server/database\";\n```\n\n### Output\n\n```javascript\nimport { debounce, reduce } from \"lodash\";\nimport React, {\n ChangeEvent,\n FC,\n KeyboardEvent,\n useEffect,\n useRef,\n} from \"react\";\n\nimport { createConnection } from \"@server/database\";\nimport { createServer } from \"@server/node\";\n\nimport { initializeApp } from \"@core/app\";\nimport { logger } from \"@core/logger\";\n\nimport { Alert } from \"@ui/Alert\";\nimport { Popup } from \"@ui/Popup\";\n\nimport { Message } from \"../Message\";\nimport { add, filter, repeat } from \"../utils\";\n```\n", - "currentMemberCount": 6, - "maxMemberCount": 28, - "createdAt": "createdAt", - "enrollmentEndDate": "2022-07-19", - "startDate": "2022-07-14", - "endDate": "2022-08-15", - "owner": { - "id": 1, - "username": "jaejae-yoo", - "imageUrl": "https://picsum.photos/id/46/200/300", - "profileUrl": "https://github.com/user/jaejae-yoo" - }, - "members": [ - { - "id": "1", - "username": "fUvdCo", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "2", - "username": "cq3gYu", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "3", - "username": "cgbtEl", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "4", - "username": "QohGEX", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "5", - "username": "QYgErw", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "6", - "username": "hxR4He", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - } - ], - "tags": [ - { - "id": 1, - "name": "JS", - "description": "자바스크립트", - "category": { "id": 3, "name": "subject" } - }, - { - "id": 2, - "name": "Java", - "description": "자바", - "category": { "id": 3, "name": "subject" } - }, - { - "id": 3, - "name": "React", - "description": "리액트", - "category": { "id": 3, "name": "subject" } - } - ], - "createdDate": "2022-07-21" - }, - { - "id": 31010593, - "title": "2022-ConquerCS", - "excerpt": "CS 정복하기🇰🇷", - "thumbnail": "https://picsum.photos/id/17/200/300", - "recruitmentStatus": "RECRUITMENT_END", - "description": "# Prettier plugin sort imports\n\nA prettier plugin to sort import declarations by provided Regular Expression order.\n\n**Note: If you are migrating from v2.x.x to v3.x.x, [Please Read Migration Guidelines](./docs/MIGRATION.md)**\n\n### Input\n\n```javascript\nimport React, {\n FC,\n useEffect,\n useRef,\n ChangeEvent,\n KeyboardEvent,\n} from \"react\";\nimport { logger } from \"@core/logger\";\nimport { reduce, debounce } from \"lodash\";\nimport { Message } from \"../Message\";\nimport { createServer } from \"@server/node\";\nimport { Alert } from \"@ui/Alert\";\nimport { repeat, filter, add } from \"../utils\";\nimport { initializeApp } from \"@core/app\";\nimport { Popup } from \"@ui/Popup\";\nimport { createConnection } from \"@server/database\";\n```\n\n### Output\n\n```javascript\nimport { debounce, reduce } from \"lodash\";\nimport React, {\n ChangeEvent,\n FC,\n KeyboardEvent,\n useEffect,\n useRef,\n} from \"react\";\n\nimport { createConnection } from \"@server/database\";\nimport { createServer } from \"@server/node\";\n\nimport { initializeApp } from \"@core/app\";\nimport { logger } from \"@core/logger\";\n\nimport { Alert } from \"@ui/Alert\";\nimport { Popup } from \"@ui/Popup\";\n\nimport { Message } from \"../Message\";\nimport { add, filter, repeat } from \"../utils\";\n```\n", - "currentMemberCount": 34, - "maxMemberCount": 39, - "createdAt": "createdAt", - "enrollmentEndDate": "2022-07-11", - "startDate": "2022-07-20", - "endDate": "2022-08-30", - "owner": { - "id": 1, - "username": "jaejae-yoo", - "imageUrl": "https://picsum.photos/id/46/200/300", - "profileUrl": "https://github.com/user/jaejae-yoo" - }, - "members": [ - { - "id": "1", - "username": "yL0ovS", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "2", - "username": "lVfUgH", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "3", - "username": "xkEYIs", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "4", - "username": "oJ9BGE", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "5", - "username": "Rebgpq", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "6", - "username": "cydH_r", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "7", - "username": "4F0ATs", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "8", - "username": "dJA484", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "9", - "username": "Dfs_Qh", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "10", - "username": "FlneSA", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "11", - "username": "UiS-7b", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "12", - "username": "zlBq4B", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "13", - "username": "NpO35j", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "14", - "username": "maoiuw", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "15", - "username": "UhJF8o", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "16", - "username": "axRnEJ", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "17", - "username": "xScFMA", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "18", - "username": "OVn_ne", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "19", - "username": "6tTuBY", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "20", - "username": "kUwXSJ", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "21", - "username": "w5jlUD", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "22", - "username": "PrezMh", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "23", - "username": "yply-z", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "24", - "username": "q3uhAe", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "25", - "username": "3X1KkO", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "26", - "username": "-J2wiF", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "27", - "username": "UAVjfR", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "28", - "username": "gmd5et", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "29", - "username": "jflYFg", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "30", - "username": "EiWv_v", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "31", - "username": "q3hIG5", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "32", - "username": "gIUzVJ", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "33", - "username": "7TmGEj", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "34", - "username": "T8qDnA", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - } - ], - "tags": [ - { - "id": 1, - "name": "JS", - "description": "자바스크립트", - "category": { "id": 3, "name": "subject" } - }, - { - "id": 2, - "name": "Java", - "description": "자바", - "category": { "id": 3, "name": "subject" } - }, - { - "id": 3, - "name": "React", - "description": "리액트", - "category": { "id": 3, "name": "subject" } - }, - { - "id": 4, - "name": "Spring", - "description": "스프링", - "category": { "id": 3, "name": "subject" } - }, - { - "id": 5, - "name": "TS", - "description": "타입스크립트", - "category": { "id": 3, "name": "subject" } - }, - { - "id": 6, - "name": "JPA", - "description": "jpa", - "category": { "id": 3, "name": "subject" } - }, - { - "id": 7, - "name": "TDD", - "description": "테스트", - "category": { "id": 3, "name": "subject" } - }, - { - "id": 8, - "name": "Alg", - "description": "알고리즘", - "category": { "id": 3, "name": "subject" } - }, - { - "id": 9, - "name": "Book", - "description": "독서", - "category": { "id": 3, "name": "subject" } - }, - { - "id": 10, - "name": "Health", - "description": "운동", - "category": { "id": 3, "name": "subject" } - } - ], - "createdDate": "2022-07-21" - }, - { - "id": 28409308, - "title": "2022-looking-for-the-sound-of-an-object", - "excerpt": "객체의 소리를 찾아서 ", - "thumbnail": "https://picsum.photos/id/70/200/300", - "recruitmentStatus": "RECRUITMENT_END", - "description": "# Prettier plugin sort imports\n\nA prettier plugin to sort import declarations by provided Regular Expression order.\n\n**Note: If you are migrating from v2.x.x to v3.x.x, [Please Read Migration Guidelines](./docs/MIGRATION.md)**\n\n### Input\n\n```javascript\nimport React, {\n FC,\n useEffect,\n useRef,\n ChangeEvent,\n KeyboardEvent,\n} from \"react\";\nimport { logger } from \"@core/logger\";\nimport { reduce, debounce } from \"lodash\";\nimport { Message } from \"../Message\";\nimport { createServer } from \"@server/node\";\nimport { Alert } from \"@ui/Alert\";\nimport { repeat, filter, add } from \"../utils\";\nimport { initializeApp } from \"@core/app\";\nimport { Popup } from \"@ui/Popup\";\nimport { createConnection } from \"@server/database\";\n```\n\n### Output\n\n```javascript\nimport { debounce, reduce } from \"lodash\";\nimport React, {\n ChangeEvent,\n FC,\n KeyboardEvent,\n useEffect,\n useRef,\n} from \"react\";\n\nimport { createConnection } from \"@server/database\";\nimport { createServer } from \"@server/node\";\n\nimport { initializeApp } from \"@core/app\";\nimport { logger } from \"@core/logger\";\n\nimport { Alert } from \"@ui/Alert\";\nimport { Popup } from \"@ui/Popup\";\n\nimport { Message } from \"../Message\";\nimport { add, filter, repeat } from \"../utils\";\n```\n", - "currentMemberCount": 9, - "maxMemberCount": 20, - "createdAt": "createdAt", - "enrollmentEndDate": "2022-07-27", - "startDate": "2022-07-02", - "endDate": "2022-08-20", - "owner": { - "id": 1, - "username": "jaejae-yoo", - "imageUrl": "https://picsum.photos/id/46/200/300", - "profileUrl": "https://github.com/user/jaejae-yoo" - }, - "members": [ - { - "id": "1", - "username": "x95RXT", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "2", - "username": "YCA10T", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "3", - "username": "_ws6Lx", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "4", - "username": "cfqVdQ", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "5", - "username": "BVKAeK", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "6", - "username": "qBuxOB", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "7", - "username": "w7-Alk", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "8", - "username": "M2sXAn", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "9", - "username": "W9YJOX", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - } - ], - "tags": [ - { - "id": 1, - "name": "JS", - "description": "자바스크립트", - "category": { "id": 3, "name": "subject" } - }, - { - "id": 2, - "name": "Java", - "description": "자바", - "category": { "id": 3, "name": "subject" } - }, - { - "id": 3, - "name": "React", - "description": "리액트", - "category": { "id": 3, "name": "subject" } - }, - { - "id": 4, - "name": "Spring", - "description": "스프링", - "category": { "id": 3, "name": "subject" } - }, - { - "id": 5, - "name": "TS", - "description": "타입스크립트", - "category": { "id": 3, "name": "subject" } - }, - { - "id": 6, - "name": "JPA", - "description": "jpa", - "category": { "id": 3, "name": "subject" } - }, - { - "id": 7, - "name": "TDD", - "description": "테스트", - "category": { "id": 3, "name": "subject" } - }, - { - "id": 8, - "name": "Alg", - "description": "알고리즘", - "category": { "id": 3, "name": "subject" } - }, - { - "id": 9, - "name": "Book", - "description": "독서", - "category": { "id": 3, "name": "subject" } - }, - { - "id": 10, - "name": "Health", - "description": "운동", - "category": { "id": 3, "name": "subject" } - }, - { - "id": 11, - "name": "Network", - "description": "네트워크", - "category": { "id": 3, "name": "subject" } - }, - { - "id": 12, - "name": "CS", - "description": "컴퓨터 과학", - "category": { "id": 3, "name": "subject" } - } - ], - "createdDate": "2022-07-21" - }, - { - "id": 31449331, - "title": "2022-kotudy", - "excerpt": "우아한테크코스 4기 😋 코틀린 스터디", - "thumbnail": "https://picsum.photos/id/88/200/300", - "recruitmentStatus": "RECRUITMENT_START", - "description": "# Prettier plugin sort imports\n\nA prettier plugin to sort import declarations by provided Regular Expression order.\n\n**Note: If you are migrating from v2.x.x to v3.x.x, [Please Read Migration Guidelines](./docs/MIGRATION.md)**\n\n### Input\n\n```javascript\nimport React, {\n FC,\n useEffect,\n useRef,\n ChangeEvent,\n KeyboardEvent,\n} from \"react\";\nimport { logger } from \"@core/logger\";\nimport { reduce, debounce } from \"lodash\";\nimport { Message } from \"../Message\";\nimport { createServer } from \"@server/node\";\nimport { Alert } from \"@ui/Alert\";\nimport { repeat, filter, add } from \"../utils\";\nimport { initializeApp } from \"@core/app\";\nimport { Popup } from \"@ui/Popup\";\nimport { createConnection } from \"@server/database\";\n```\n\n### Output\n\n```javascript\nimport { debounce, reduce } from \"lodash\";\nimport React, {\n ChangeEvent,\n FC,\n KeyboardEvent,\n useEffect,\n useRef,\n} from \"react\";\n\nimport { createConnection } from \"@server/database\";\nimport { createServer } from \"@server/node\";\n\nimport { initializeApp } from \"@core/app\";\nimport { logger } from \"@core/logger\";\n\nimport { Alert } from \"@ui/Alert\";\nimport { Popup } from \"@ui/Popup\";\n\nimport { Message } from \"../Message\";\nimport { add, filter, repeat } from \"../utils\";\n```\n", - "currentMemberCount": 22, - "maxMemberCount": 28, - "createdAt": "createdAt", - "enrollmentEndDate": "2022-07-08", - "startDate": "2022-07-03", - "endDate": "2022-08-17", - "owner": { - "id": 1, - "username": "jaejae-yoo", - "imageUrl": "https://picsum.photos/id/46/200/300", - "profileUrl": "https://github.com/user/jaejae-yoo" - }, - "members": [ - { - "id": "1", - "username": "ls3jCf", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "2", - "username": "wZAre5", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "3", - "username": "WEnmAX", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "4", - "username": "7cOCQu", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "5", - "username": "BcKhmv", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "6", - "username": "Voo449", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "7", - "username": "2RoP9-", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "8", - "username": "UbqZjR", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "9", - "username": "hp0jNp", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "10", - "username": "22CmFK", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "11", - "username": "hMXSv8", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "12", - "username": "QvSvWX", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "13", - "username": "EsWFF2", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "14", - "username": "2bySu5", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "15", - "username": "yCdgB2", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "16", - "username": "qtVI_Z", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "17", - "username": "HfWCVi", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "18", - "username": "80tGvC", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "19", - "username": "2TR80R", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "20", - "username": "v7VtDk", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "21", - "username": "Tv3m-j", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "22", - "username": "l6iC23", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - } - ], - "tags": [ - { - "id": 1, - "name": "JS", - "description": "자바스크립트", - "category": { "id": 3, "name": "subject" } - } - ], - "createdDate": "2022-07-21" - }, - { - "id": 76240041, - "title": "2022-no-posting-you-die", - "excerpt": "'강제 블로그 포스팅 연합' 스터디입니다", - "thumbnail": "https://picsum.photos/id/27/200/300", - "recruitmentStatus": "RECRUITMENT_START", - "description": "# Prettier plugin sort imports\n\nA prettier plugin to sort import declarations by provided Regular Expression order.\n\n**Note: If you are migrating from v2.x.x to v3.x.x, [Please Read Migration Guidelines](./docs/MIGRATION.md)**\n\n### Input\n\n```javascript\nimport React, {\n FC,\n useEffect,\n useRef,\n ChangeEvent,\n KeyboardEvent,\n} from \"react\";\nimport { logger } from \"@core/logger\";\nimport { reduce, debounce } from \"lodash\";\nimport { Message } from \"../Message\";\nimport { createServer } from \"@server/node\";\nimport { Alert } from \"@ui/Alert\";\nimport { repeat, filter, add } from \"../utils\";\nimport { initializeApp } from \"@core/app\";\nimport { Popup } from \"@ui/Popup\";\nimport { createConnection } from \"@server/database\";\n```\n\n### Output\n\n```javascript\nimport { debounce, reduce } from \"lodash\";\nimport React, {\n ChangeEvent,\n FC,\n KeyboardEvent,\n useEffect,\n useRef,\n} from \"react\";\n\nimport { createConnection } from \"@server/database\";\nimport { createServer } from \"@server/node\";\n\nimport { initializeApp } from \"@core/app\";\nimport { logger } from \"@core/logger\";\n\nimport { Alert } from \"@ui/Alert\";\nimport { Popup } from \"@ui/Popup\";\n\nimport { Message } from \"../Message\";\nimport { add, filter, repeat } from \"../utils\";\n```\n", - "currentMemberCount": 29, - "maxMemberCount": 39, - "createdAt": "createdAt", - "enrollmentEndDate": "2022-07-16", - "startDate": "2022-07-18", - "endDate": "2022-08-29", - "owner": { - "id": 1, - "username": "jaejae-yoo", - "imageUrl": "https://picsum.photos/id/46/200/300", - "profileUrl": "https://github.com/user/jaejae-yoo" - }, - "members": [ - { - "id": "1", - "username": "o2KP49", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "2", - "username": "KsbIKf", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "3", - "username": "i8Gxro", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "4", - "username": "ub_d3J", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "5", - "username": "k2HsHB", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "6", - "username": "A67zDp", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "7", - "username": "SU33By", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "8", - "username": "Q4kL9E", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "9", - "username": "fsu41-", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "10", - "username": "NotLZH", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "11", - "username": "2U8goX", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "12", - "username": "UIeMmS", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "13", - "username": "NPVgvy", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "14", - "username": "VNCjEh", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "15", - "username": "ap2JFi", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "16", - "username": "t_30Aq", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "17", - "username": "fHe7pr", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "18", - "username": "OUiw7g", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "19", - "username": "dJRytJ", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "20", - "username": "LQbQZi", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "21", - "username": "WNMJr2", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "22", - "username": "ss2A4w", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "23", - "username": "gnQmnS", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "24", - "username": "IHU6Jg", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "25", - "username": "osRHjF", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "26", - "username": "BWerqd", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "27", - "username": "Cy8DjP", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "28", - "username": "boAipa", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "29", - "username": "9CyEVU", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - } - ], - "tags": [ - { - "id": 1, - "name": "JS", - "description": "자바스크립트", - "category": { "id": 3, "name": "subject" } - }, - { - "id": 2, - "name": "Java", - "description": "자바", - "category": { "id": 3, "name": "subject" } - }, - { - "id": 3, - "name": "React", - "description": "리액트", - "category": { "id": 3, "name": "subject" } - }, - { - "id": 4, - "name": "Spring", - "description": "스프링", - "category": { "id": 3, "name": "subject" } - }, - { - "id": 5, - "name": "TS", - "description": "타입스크립트", - "category": { "id": 3, "name": "subject" } - }, - { - "id": 6, - "name": "JPA", - "description": "jpa", - "category": { "id": 3, "name": "subject" } - }, - { - "id": 7, - "name": "TDD", - "description": "테스트", - "category": { "id": 3, "name": "subject" } - }, - { - "id": 8, - "name": "Alg", - "description": "알고리즘", - "category": { "id": 3, "name": "subject" } - }, - { - "id": 9, - "name": "Book", - "description": "독서", - "category": { "id": 3, "name": "subject" } - }, - { - "id": 10, - "name": "Health", - "description": "운동", - "category": { "id": 3, "name": "subject" } - }, - { - "id": 11, - "name": "Network", - "description": "네트워크", - "category": { "id": 3, "name": "subject" } - } - ], - "createdDate": "2022-07-21" - }, - { - "id": 55695590, - "title": "2022-Real-MySQL", - "excerpt": "⚡️토르⚡️의 짜릿한 Real MySQL 뽀개기 🔨", - "thumbnail": "https://picsum.photos/id/25/200/300", - "recruitmentStatus": "RECRUITMENT_END", - "description": "# Prettier plugin sort imports\n\nA prettier plugin to sort import declarations by provided Regular Expression order.\n\n**Note: If you are migrating from v2.x.x to v3.x.x, [Please Read Migration Guidelines](./docs/MIGRATION.md)**\n\n### Input\n\n```javascript\nimport React, {\n FC,\n useEffect,\n useRef,\n ChangeEvent,\n KeyboardEvent,\n} from \"react\";\nimport { logger } from \"@core/logger\";\nimport { reduce, debounce } from \"lodash\";\nimport { Message } from \"../Message\";\nimport { createServer } from \"@server/node\";\nimport { Alert } from \"@ui/Alert\";\nimport { repeat, filter, add } from \"../utils\";\nimport { initializeApp } from \"@core/app\";\nimport { Popup } from \"@ui/Popup\";\nimport { createConnection } from \"@server/database\";\n```\n\n### Output\n\n```javascript\nimport { debounce, reduce } from \"lodash\";\nimport React, {\n ChangeEvent,\n FC,\n KeyboardEvent,\n useEffect,\n useRef,\n} from \"react\";\n\nimport { createConnection } from \"@server/database\";\nimport { createServer } from \"@server/node\";\n\nimport { initializeApp } from \"@core/app\";\nimport { logger } from \"@core/logger\";\n\nimport { Alert } from \"@ui/Alert\";\nimport { Popup } from \"@ui/Popup\";\n\nimport { Message } from \"../Message\";\nimport { add, filter, repeat } from \"../utils\";\n```\n", - "currentMemberCount": 20, - "maxMemberCount": 39, - "createdAt": "createdAt", - "enrollmentEndDate": "2022-07-20", - "startDate": "2022-07-15", - "endDate": "2022-08-05", - "owner": { - "id": 1, - "username": "jaejae-yoo", - "imageUrl": "https://picsum.photos/id/46/200/300", - "profileUrl": "https://github.com/user/jaejae-yoo" - }, - "members": [ - { - "id": "1", - "username": "XEaojp", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "2", - "username": "kssaAC", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "3", - "username": "a1Odtq", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "4", - "username": "ydqFR1", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "5", - "username": "nikhhf", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "6", - "username": "S4K87p", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "7", - "username": "PqJicY", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "8", - "username": "ASPPzr", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "9", - "username": "H27a0Q", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "10", - "username": "m_ukzu", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "11", - "username": "W2HZDJ", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "12", - "username": "2VF7p6", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "13", - "username": "8nvdxo", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "14", - "username": "QYk4LP", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "15", - "username": "NPu4f4", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "16", - "username": "A0SG6x", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "17", - "username": "tdswHF", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "18", - "username": "mk0duP", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "19", - "username": "eAnCYr", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "20", - "username": "e-iqV9", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - } - ], - "tags": [ - { - "id": 1, - "name": "JS", - "description": "자바스크립트", - "category": { "id": 3, "name": "subject" } - }, - { - "id": 2, - "name": "Java", - "description": "자바", - "category": { "id": 3, "name": "subject" } - }, - { - "id": 3, - "name": "React", - "description": "리액트", - "category": { "id": 3, "name": "subject" } - }, - { - "id": 4, - "name": "Spring", - "description": "스프링", - "category": { "id": 3, "name": "subject" } - }, - { - "id": 5, - "name": "TS", - "description": "타입스크립트", - "category": { "id": 3, "name": "subject" } - }, - { - "id": 6, - "name": "JPA", - "description": "jpa", - "category": { "id": 3, "name": "subject" } - } - ], - "createdDate": "2022-07-21" - }, - { - "id": 67476689, - "title": "2022-weekly-log", - "excerpt": "👻 chalee's weekly-log challenge 👻", - "thumbnail": "https://picsum.photos/id/18/200/300", - "recruitmentStatus": "RECRUITMENT_START", - "description": "# Prettier plugin sort imports\n\nA prettier plugin to sort import declarations by provided Regular Expression order.\n\n**Note: If you are migrating from v2.x.x to v3.x.x, [Please Read Migration Guidelines](./docs/MIGRATION.md)**\n\n### Input\n\n```javascript\nimport React, {\n FC,\n useEffect,\n useRef,\n ChangeEvent,\n KeyboardEvent,\n} from \"react\";\nimport { logger } from \"@core/logger\";\nimport { reduce, debounce } from \"lodash\";\nimport { Message } from \"../Message\";\nimport { createServer } from \"@server/node\";\nimport { Alert } from \"@ui/Alert\";\nimport { repeat, filter, add } from \"../utils\";\nimport { initializeApp } from \"@core/app\";\nimport { Popup } from \"@ui/Popup\";\nimport { createConnection } from \"@server/database\";\n```\n\n### Output\n\n```javascript\nimport { debounce, reduce } from \"lodash\";\nimport React, {\n ChangeEvent,\n FC,\n KeyboardEvent,\n useEffect,\n useRef,\n} from \"react\";\n\nimport { createConnection } from \"@server/database\";\nimport { createServer } from \"@server/node\";\n\nimport { initializeApp } from \"@core/app\";\nimport { logger } from \"@core/logger\";\n\nimport { Alert } from \"@ui/Alert\";\nimport { Popup } from \"@ui/Popup\";\n\nimport { Message } from \"../Message\";\nimport { add, filter, repeat } from \"../utils\";\n```\n", - "currentMemberCount": 36, - "maxMemberCount": 39, - "createdAt": "createdAt", - "enrollmentEndDate": "2022-07-05", - "startDate": "2022-07-05", - "endDate": "2022-08-27", - "owner": { - "id": 1, - "username": "jaejae-yoo", - "imageUrl": "https://picsum.photos/id/46/200/300", - "profileUrl": "https://github.com/user/jaejae-yoo" - }, - "members": [ - { - "id": "1", - "username": "rTlNbm", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "2", - "username": "_CrSvy", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "3", - "username": "cY0sYE", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "4", - "username": "gxwKEt", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "5", - "username": "YjCEJi", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "6", - "username": "-owSb9", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "7", - "username": "f3ij_E", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "8", - "username": "Qsu53d", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "9", - "username": "n_89Jq", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "10", - "username": "gXfwrT", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "11", - "username": "rtoDGs", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "12", - "username": "7zgeB7", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "13", - "username": "JQdUrT", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "14", - "username": "B_rxeP", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "15", - "username": "SPsaMn", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "16", - "username": "W-oCQy", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "17", - "username": "rX18mw", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "18", - "username": "gUNt8g", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "19", - "username": "9PZYFW", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "20", - "username": "LSZXRj", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "21", - "username": "yfH1Xd", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "22", - "username": "6-pA5n", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "23", - "username": "sFYe3Y", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "24", - "username": "TwcNRW", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "25", - "username": "tkF8VL", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "26", - "username": "2QxJAi", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "27", - "username": "FF6azE", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "28", - "username": "X5a4I8", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "29", - "username": "gLk7pT", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "30", - "username": "a7a9-C", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "31", - "username": "kkmhN_", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "32", - "username": "fzg2K-", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "33", - "username": "M2Kdg6", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "34", - "username": "XMRDAe", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "35", - "username": "1aOPyE", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "36", - "username": "2bNwjV", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - } - ], - "tags": [ - { - "id": 1, - "name": "JS", - "description": "자바스크립트", - "category": { "id": 3, "name": "subject" } - }, - { - "id": 2, - "name": "Java", - "description": "자바", - "category": { "id": 3, "name": "subject" } - }, - { - "id": 3, - "name": "React", - "description": "리액트", - "category": { "id": 3, "name": "subject" } - }, - { - "id": 4, - "name": "Spring", - "description": "스프링", - "category": { "id": 3, "name": "subject" } - }, - { - "id": 5, - "name": "TS", - "description": "타입스크립트", - "category": { "id": 3, "name": "subject" } - }, - { - "id": 6, - "name": "JPA", - "description": "jpa", - "category": { "id": 3, "name": "subject" } - }, - { - "id": 7, - "name": "TDD", - "description": "테스트", - "category": { "id": 3, "name": "subject" } - }, - { - "id": 8, - "name": "Alg", - "description": "알고리즘", - "category": { "id": 3, "name": "subject" } - }, - { - "id": 9, - "name": "Book", - "description": "독서", - "category": { "id": 3, "name": "subject" } - }, - { - "id": 10, - "name": "Health", - "description": "운동", - "category": { "id": 3, "name": "subject" } - } - ], - "createdDate": "2022-07-21" - }, - { - "id": 59551568, - "title": "2022-spring-study", - "excerpt": "찐들의 스프링 스터디", - "thumbnail": "https://picsum.photos/id/14/200/300", - "recruitmentStatus": "RECRUITMENT_END", - "description": "# Prettier plugin sort imports\n\nA prettier plugin to sort import declarations by provided Regular Expression order.\n\n**Note: If you are migrating from v2.x.x to v3.x.x, [Please Read Migration Guidelines](./docs/MIGRATION.md)**\n\n### Input\n\n```javascript\nimport React, {\n FC,\n useEffect,\n useRef,\n ChangeEvent,\n KeyboardEvent,\n} from \"react\";\nimport { logger } from \"@core/logger\";\nimport { reduce, debounce } from \"lodash\";\nimport { Message } from \"../Message\";\nimport { createServer } from \"@server/node\";\nimport { Alert } from \"@ui/Alert\";\nimport { repeat, filter, add } from \"../utils\";\nimport { initializeApp } from \"@core/app\";\nimport { Popup } from \"@ui/Popup\";\nimport { createConnection } from \"@server/database\";\n```\n\n### Output\n\n```javascript\nimport { debounce, reduce } from \"lodash\";\nimport React, {\n ChangeEvent,\n FC,\n KeyboardEvent,\n useEffect,\n useRef,\n} from \"react\";\n\nimport { createConnection } from \"@server/database\";\nimport { createServer } from \"@server/node\";\n\nimport { initializeApp } from \"@core/app\";\nimport { logger } from \"@core/logger\";\n\nimport { Alert } from \"@ui/Alert\";\nimport { Popup } from \"@ui/Popup\";\n\nimport { Message } from \"../Message\";\nimport { add, filter, repeat } from \"../utils\";\n```\n", - "currentMemberCount": 23, - "maxMemberCount": 26, - "createdAt": "createdAt", - "enrollmentEndDate": "2022-07-15", - "startDate": "2022-07-16", - "endDate": "2022-08-25", - "owner": { - "id": 1, - "username": "jaejae-yoo", - "imageUrl": "https://picsum.photos/id/46/200/300", - "profileUrl": "https://github.com/user/jaejae-yoo" - }, - "members": [ - { - "id": "1", - "username": "jrJ9TP", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "2", - "username": "ozrxx0", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "3", - "username": "Y3T-Yb", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "4", - "username": "xm6QK8", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "5", - "username": "yv2itP", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "6", - "username": "U_XHPb", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "7", - "username": "OzU0EC", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "8", - "username": "m-qeTy", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "9", - "username": "NL1fBp", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "10", - "username": "CK6rEd", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "11", - "username": "v-A9pJ", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "12", - "username": "sfqFLW", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "13", - "username": "p6EZvm", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "14", - "username": "w_Rko_", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "15", - "username": "DvjmFU", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "16", - "username": "AdvYip", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "17", - "username": "NzHbhr", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "18", - "username": "XguWkR", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "19", - "username": "xhO0QT", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "20", - "username": "GcA7A3", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "21", - "username": "kmaqE6", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "22", - "username": "4EKgY5", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "23", - "username": "kYKB_O", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - } - ], - "tags": [ - { - "id": 1, - "name": "JS", - "description": "자바스크립트", - "category": { "id": 3, "name": "subject" } - }, - { - "id": 2, - "name": "Java", - "description": "자바", - "category": { "id": 3, "name": "subject" } - }, - { - "id": 3, - "name": "React", - "description": "리액트", - "category": { "id": 3, "name": "subject" } - }, - { - "id": 4, - "name": "Spring", - "description": "스프링", - "category": { "id": 3, "name": "subject" } - }, - { - "id": 5, - "name": "TS", - "description": "타입스크립트", - "category": { "id": 3, "name": "subject" } - }, - { - "id": 6, - "name": "JPA", - "description": "jpa", - "category": { "id": 3, "name": "subject" } - }, - { - "id": 7, - "name": "TDD", - "description": "테스트", - "category": { "id": 3, "name": "subject" } - } - ], - "createdDate": "2022-07-21" - }, - { - "id": 93014654, - "title": "http-network-basic-level2-study", - "excerpt": "4기 Level2 http network basic 스터디", - "thumbnail": "https://picsum.photos/id/93/200/300", - "recruitmentStatus": "RECRUITMENT_END", - "description": "# Prettier plugin sort imports\n\nA prettier plugin to sort import declarations by provided Regular Expression order.\n\n**Note: If you are migrating from v2.x.x to v3.x.x, [Please Read Migration Guidelines](./docs/MIGRATION.md)**\n\n### Input\n\n```javascript\nimport React, {\n FC,\n useEffect,\n useRef,\n ChangeEvent,\n KeyboardEvent,\n} from \"react\";\nimport { logger } from \"@core/logger\";\nimport { reduce, debounce } from \"lodash\";\nimport { Message } from \"../Message\";\nimport { createServer } from \"@server/node\";\nimport { Alert } from \"@ui/Alert\";\nimport { repeat, filter, add } from \"../utils\";\nimport { initializeApp } from \"@core/app\";\nimport { Popup } from \"@ui/Popup\";\nimport { createConnection } from \"@server/database\";\n```\n\n### Output\n\n```javascript\nimport { debounce, reduce } from \"lodash\";\nimport React, {\n ChangeEvent,\n FC,\n KeyboardEvent,\n useEffect,\n useRef,\n} from \"react\";\n\nimport { createConnection } from \"@server/database\";\nimport { createServer } from \"@server/node\";\n\nimport { initializeApp } from \"@core/app\";\nimport { logger } from \"@core/logger\";\n\nimport { Alert } from \"@ui/Alert\";\nimport { Popup } from \"@ui/Popup\";\n\nimport { Message } from \"../Message\";\nimport { add, filter, repeat } from \"../utils\";\n```\n", - "currentMemberCount": 23, - "maxMemberCount": 31, - "createdAt": "createdAt", - "enrollmentEndDate": "2022-07-22", - "startDate": "2022-07-25", - "endDate": "2022-08-14", - "owner": { - "id": 1, - "username": "jaejae-yoo", - "imageUrl": "https://picsum.photos/id/46/200/300", - "profileUrl": "https://github.com/user/jaejae-yoo" - }, - "members": [ - { - "id": "1", - "username": "28S97i", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "2", - "username": "DkL8I2", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "3", - "username": "uS7V92", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "4", - "username": "OS5-AR", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "5", - "username": "n6NOCW", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "6", - "username": "JDZGLD", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "7", - "username": "6i-_7k", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "8", - "username": "V86ZXq", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "9", - "username": "b95S-H", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "10", - "username": "I4AIyI", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "11", - "username": "B4AFRX", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "12", - "username": "AR77k0", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "13", - "username": "cWEyWp", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "14", - "username": "QAYoMG", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "15", - "username": "mdeTej", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "16", - "username": "_YLidW", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "17", - "username": "m_QC2-", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "18", - "username": "48WUcp", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "19", - "username": "miQpO3", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "20", - "username": "2pLRxe", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "21", - "username": "MeZhvb", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "22", - "username": "BCGONi", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "23", - "username": "nwoJr0", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - } - ], - "tags": [ - { - "id": 1, - "name": "JS", - "description": "자바스크립트", - "category": { "id": 3, "name": "subject" } - }, - { - "id": 2, - "name": "Java", - "description": "자바", - "category": { "id": 3, "name": "subject" } - }, - { - "id": 3, - "name": "React", - "description": "리액트", - "category": { "id": 3, "name": "subject" } - }, - { - "id": 4, - "name": "Spring", - "description": "스프링", - "category": { "id": 3, "name": "subject" } - }, - { - "id": 5, - "name": "TS", - "description": "타입스크립트", - "category": { "id": 3, "name": "subject" } - }, - { - "id": 6, - "name": "JPA", - "description": "jpa", - "category": { "id": 3, "name": "subject" } - }, - { - "id": 7, - "name": "TDD", - "description": "테스트", - "category": { "id": 3, "name": "subject" } - }, - { - "id": 8, - "name": "Alg", - "description": "알고리즘", - "category": { "id": 3, "name": "subject" } - } - ], - "createdDate": "2022-07-21" - }, - { - "id": 35887618, - "title": "2022-code-review-study-2", - "excerpt": "우아한테크코스 프론트엔드 미션 코드리뷰 피드백 정리", - "thumbnail": "https://picsum.photos/id/33/200/300", - "recruitmentStatus": "RECRUITMENT_END", - "description": "# Prettier plugin sort imports\n\nA prettier plugin to sort import declarations by provided Regular Expression order.\n\n**Note: If you are migrating from v2.x.x to v3.x.x, [Please Read Migration Guidelines](./docs/MIGRATION.md)**\n\n### Input\n\n```javascript\nimport React, {\n FC,\n useEffect,\n useRef,\n ChangeEvent,\n KeyboardEvent,\n} from \"react\";\nimport { logger } from \"@core/logger\";\nimport { reduce, debounce } from \"lodash\";\nimport { Message } from \"../Message\";\nimport { createServer } from \"@server/node\";\nimport { Alert } from \"@ui/Alert\";\nimport { repeat, filter, add } from \"../utils\";\nimport { initializeApp } from \"@core/app\";\nimport { Popup } from \"@ui/Popup\";\nimport { createConnection } from \"@server/database\";\n```\n\n### Output\n\n```javascript\nimport { debounce, reduce } from \"lodash\";\nimport React, {\n ChangeEvent,\n FC,\n KeyboardEvent,\n useEffect,\n useRef,\n} from \"react\";\n\nimport { createConnection } from \"@server/database\";\nimport { createServer } from \"@server/node\";\n\nimport { initializeApp } from \"@core/app\";\nimport { logger } from \"@core/logger\";\n\nimport { Alert } from \"@ui/Alert\";\nimport { Popup } from \"@ui/Popup\";\n\nimport { Message } from \"../Message\";\nimport { add, filter, repeat } from \"../utils\";\n```\n", - "currentMemberCount": 33, - "maxMemberCount": 38, - "createdAt": "createdAt", - "enrollmentEndDate": "2022-07-29", - "startDate": "2022-07-22", - "endDate": "2022-08-24", - "owner": { - "id": 1, - "username": "jaejae-yoo", - "imageUrl": "https://picsum.photos/id/46/200/300", - "profileUrl": "https://github.com/user/jaejae-yoo" - }, - "members": [ - { - "id": "1", - "username": "UbUEne", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "2", - "username": "zmR3sR", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "3", - "username": "Q3hpul", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "4", - "username": "o1OtIH", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "5", - "username": "X6k33n", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "6", - "username": "KJQ0Bt", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "7", - "username": "oqO120", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "8", - "username": "OIk-15", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "9", - "username": "m-Gil6", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "10", - "username": "z62G_x", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "11", - "username": "9nueTS", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "12", - "username": "GIYhNE", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "13", - "username": "BcIrkn", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "14", - "username": "-K2L5q", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "15", - "username": "-xbIPF", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "16", - "username": "ve66UH", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "17", - "username": "k8pQ4J", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "18", - "username": "QTh5Sz", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "19", - "username": "KU3I1c", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "20", - "username": "j0qeLu", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "21", - "username": "u-fG4b", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "22", - "username": "q3IYah", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "23", - "username": "vrVkQi", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "24", - "username": "zPsi0O", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "25", - "username": "MA0uxC", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "26", - "username": "os5reh", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "27", - "username": "XJrGUS", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "28", - "username": "t0HtoK", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "29", - "username": "aWIvL6", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "30", - "username": "8QR4yH", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "31", - "username": "AXBRHm", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "32", - "username": "LCJuct", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "33", - "username": "Ovwl8O", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - } - ], - "tags": [ - { - "id": 1, - "name": "JS", - "description": "자바스크립트", - "category": { "id": 3, "name": "subject" } - }, - { - "id": 2, - "name": "Java", - "description": "자바", - "category": { "id": 3, "name": "subject" } - }, - { - "id": 3, - "name": "React", - "description": "리액트", - "category": { "id": 3, "name": "subject" } - }, - { - "id": 4, - "name": "Spring", - "description": "스프링", - "category": { "id": 3, "name": "subject" } - }, - { - "id": 5, - "name": "TS", - "description": "타입스크립트", - "category": { "id": 3, "name": "subject" } - }, - { - "id": 6, - "name": "JPA", - "description": "jpa", - "category": { "id": 3, "name": "subject" } - }, - { - "id": 7, - "name": "TDD", - "description": "테스트", - "category": { "id": 3, "name": "subject" } - }, - { - "id": 8, - "name": "Alg", - "description": "알고리즘", - "category": { "id": 3, "name": "subject" } - }, - { - "id": 9, - "name": "Book", - "description": "독서", - "category": { "id": 3, "name": "subject" } - }, - { - "id": 10, - "name": "Health", - "description": "운동", - "category": { "id": 3, "name": "subject" } - }, - { - "id": 11, - "name": "Network", - "description": "네트워크", - "category": { "id": 3, "name": "subject" } - }, - { - "id": 12, - "name": "CS", - "description": "컴퓨터 과학", - "category": { "id": 3, "name": "subject" } - } - ], - "createdDate": "2022-07-21" - }, - { - "id": 11036970, - "title": "2022-http-web-basic-for-all-developer", - "excerpt": "모든 개발자를 위한 HTTP 웹 기본 지식 익히기", - "thumbnail": "https://picsum.photos/id/78/200/300", - "recruitmentStatus": "RECRUITMENT_START", + "title": "2022-gugu-spring-study", + "excerpt": "레벨1 구구조의 제로가 이끄는 스프링 스터디", + "thumbnail": "https://picsum.photos/id/84/200/300", + "recruitmentStatus": "RECRUITMENT_END", "description": "# Prettier plugin sort imports\n\nA prettier plugin to sort import declarations by provided Regular Expression order.\n\n**Note: If you are migrating from v2.x.x to v3.x.x, [Please Read Migration Guidelines](./docs/MIGRATION.md)**\n\n### Input\n\n```javascript\nimport React, {\n FC,\n useEffect,\n useRef,\n ChangeEvent,\n KeyboardEvent,\n} from \"react\";\nimport { logger } from \"@core/logger\";\nimport { reduce, debounce } from \"lodash\";\nimport { Message } from \"../Message\";\nimport { createServer } from \"@server/node\";\nimport { Alert } from \"@ui/Alert\";\nimport { repeat, filter, add } from \"../utils\";\nimport { initializeApp } from \"@core/app\";\nimport { Popup } from \"@ui/Popup\";\nimport { createConnection } from \"@server/database\";\n```\n\n### Output\n\n```javascript\nimport { debounce, reduce } from \"lodash\";\nimport React, {\n ChangeEvent,\n FC,\n KeyboardEvent,\n useEffect,\n useRef,\n} from \"react\";\n\nimport { createConnection } from \"@server/database\";\nimport { createServer } from \"@server/node\";\n\nimport { initializeApp } from \"@core/app\";\nimport { logger } from \"@core/logger\";\n\nimport { Alert } from \"@ui/Alert\";\nimport { Popup } from \"@ui/Popup\";\n\nimport { Message } from \"../Message\";\nimport { add, filter, repeat } from \"../utils\";\n```\n", "currentMemberCount": 6, - "maxMemberCount": 9, - "createdAt": "createdAt", - "enrollmentEndDate": "2022-07-08", - "startDate": "2022-07-05", - "endDate": "2022-08-27", + "maxMemberCount": 28, + "enrollmentEndDate": "2022-07-19", + "startDate": "2022-07-14", + "endDate": "2022-08-15", "owner": { "id": 1, "username": "jaejae-yoo", "imageUrl": "https://picsum.photos/id/46/200/300", - "profileUrl": "https://github.com/user/jaejae-yoo" + "profileUrl": "https://github.com/user/jaejae,-yoo", + "participationDate": "2022-08-10" }, "members": [ { - "id": "1", - "username": "79ZbJM", + "id": 1, + "username": "fUvdCo", "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" }, { - "id": "2", - "username": "1bAMe-", + "id": 2, + "username": "cq3gYu", "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" }, { - "id": "3", - "username": "1U3Gb7", + "id": 3, + "username": "cgbtEl", "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" }, { - "id": "4", - "username": "kIaKjm", + "id": 4, + "username": "QohGEX", "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" }, { - "id": "5", - "username": "W4o2-l", + "id": 5, + "username": "QYgErw", "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" }, { - "id": "6", - "username": "nIWJ-x", + "id": 6, + "username": "hxR4He", "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" } ], "tags": [ @@ -2932,185 +359,267 @@ "name": "React", "description": "리액트", "category": { "id": 3, "name": "subject" } + } + ], + "createdDate": "2022-07-21" + }, + { + "id": 31010593, + "title": "2022-ConquerCS", + "excerpt": "CS 정복하기🇰🇷", + "thumbnail": "https://picsum.photos/id/17/200/300", + "recruitmentStatus": "RECRUITMENT_END", + "description": "# Prettier plugin sort imports\n\nA prettier plugin to sort import declarations by provided Regular Expression order.\n\n**Note: If you are migrating from v2.x.x to v3.x.x, [Please Read Migration Guidelines](./docs/MIGRATION.md)**\n\n### Input\n\n```javascript\nimport React, {\n FC,\n useEffect,\n useRef,\n ChangeEvent,\n KeyboardEvent,\n} from \"react\";\nimport { logger } from \"@core/logger\";\nimport { reduce, debounce } from \"lodash\";\nimport { Message } from \"../Message\";\nimport { createServer } from \"@server/node\";\nimport { Alert } from \"@ui/Alert\";\nimport { repeat, filter, add } from \"../utils\";\nimport { initializeApp } from \"@core/app\";\nimport { Popup } from \"@ui/Popup\";\nimport { createConnection } from \"@server/database\";\n```\n\n### Output\n\n```javascript\nimport { debounce, reduce } from \"lodash\";\nimport React, {\n ChangeEvent,\n FC,\n KeyboardEvent,\n useEffect,\n useRef,\n} from \"react\";\n\nimport { createConnection } from \"@server/database\";\nimport { createServer } from \"@server/node\";\n\nimport { initializeApp } from \"@core/app\";\nimport { logger } from \"@core/logger\";\n\nimport { Alert } from \"@ui/Alert\";\nimport { Popup } from \"@ui/Popup\";\n\nimport { Message } from \"../Message\";\nimport { add, filter, repeat } from \"../utils\";\n```\n", + "currentMemberCount": 34, + "maxMemberCount": 39, + "enrollmentEndDate": "2022-07-11", + "startDate": "2022-07-20", + "endDate": "2022-08-30", + "owner": { + "id": 1, + "username": "jaejae-yoo", + "imageUrl": "https://picsum.photos/id/46/200/300", + "profileUrl": "https://github.com/user/jaejae,-yoo", + "participationDate": "2022-08-10" + }, + "members": [ + { + "id": 1, + "username": "yL0ovS", + "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" + }, + { + "id": 2, + "username": "lVfUgH", + "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" + }, + { + "id": 3, + "username": "xkEYIs", + "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" }, { "id": 4, - "name": "Spring", - "description": "스프링", - "category": { "id": 3, "name": "subject" } + "username": "oJ9BGE", + "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" }, { "id": 5, - "name": "TS", - "description": "타입스크립트", - "category": { "id": 3, "name": "subject" } + "username": "Rebgpq", + "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" }, { "id": 6, - "name": "JPA", - "description": "jpa", - "category": { "id": 3, "name": "subject" } + "username": "cydH_r", + "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" }, { "id": 7, - "name": "TDD", - "description": "테스트", - "category": { "id": 3, "name": "subject" } + "username": "4F0ATs", + "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" }, { "id": 8, - "name": "Alg", - "description": "알고리즘", - "category": { "id": 3, "name": "subject" } + "username": "dJA484", + "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" }, { "id": 9, - "name": "Book", - "description": "독서", - "category": { "id": 3, "name": "subject" } - } - ], - "createdDate": "2022-07-21" - }, - { - "id": 62039149, - "title": "2022-woowa-retrospect", - "excerpt": "우아한테크코스 4기 프론트엔드 과정 회고", - "thumbnail": "https://picsum.photos/id/71/200/300", - "recruitmentStatus": "RECRUITMENT_END", - "description": "# Prettier plugin sort imports\n\nA prettier plugin to sort import declarations by provided Regular Expression order.\n\n**Note: If you are migrating from v2.x.x to v3.x.x, [Please Read Migration Guidelines](./docs/MIGRATION.md)**\n\n### Input\n\n```javascript\nimport React, {\n FC,\n useEffect,\n useRef,\n ChangeEvent,\n KeyboardEvent,\n} from \"react\";\nimport { logger } from \"@core/logger\";\nimport { reduce, debounce } from \"lodash\";\nimport { Message } from \"../Message\";\nimport { createServer } from \"@server/node\";\nimport { Alert } from \"@ui/Alert\";\nimport { repeat, filter, add } from \"../utils\";\nimport { initializeApp } from \"@core/app\";\nimport { Popup } from \"@ui/Popup\";\nimport { createConnection } from \"@server/database\";\n```\n\n### Output\n\n```javascript\nimport { debounce, reduce } from \"lodash\";\nimport React, {\n ChangeEvent,\n FC,\n KeyboardEvent,\n useEffect,\n useRef,\n} from \"react\";\n\nimport { createConnection } from \"@server/database\";\nimport { createServer } from \"@server/node\";\n\nimport { initializeApp } from \"@core/app\";\nimport { logger } from \"@core/logger\";\n\nimport { Alert } from \"@ui/Alert\";\nimport { Popup } from \"@ui/Popup\";\n\nimport { Message } from \"../Message\";\nimport { add, filter, repeat } from \"../utils\";\n```\n", - "currentMemberCount": 20, - "maxMemberCount": 29, - "createdAt": "createdAt", - "enrollmentEndDate": "2022-07-26", - "startDate": "2022-07-29", - "endDate": "2022-08-19", - "owner": { - "id": 1, - "username": "jaejae-yoo", - "imageUrl": "https://picsum.photos/id/46/200/300", - "profileUrl": "https://github.com/user/jaejae-yoo" - }, - "members": [ + "username": "Dfs_Qh", + "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" + }, + { + "id": 10, + "username": "FlneSA", + "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" + }, + { + "id": 11, + "username": "UiS-7b", + "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" + }, + { + "id": 12, + "username": "zlBq4B", + "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" + }, + { + "id": 13, + "username": "NpO35j", + "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" + }, + { + "id": 14, + "username": "maoiuw", + "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" + }, { - "id": "1", - "username": "jmv7s2", + "id": 15, + "username": "UhJF8o", "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" }, { - "id": "2", - "username": "eTGQ3I", + "id": 16, + "username": "axRnEJ", "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" }, { - "id": "3", - "username": "WvOL0N", + "id": 17, + "username": "xScFMA", "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" }, { - "id": "4", - "username": "VfNI2S", + "id": 18, + "username": "OVn_ne", "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" }, { - "id": "5", - "username": "F1hht2", + "id": 19, + "username": "6tTuBY", "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" }, { - "id": "6", - "username": "Zy4o5p", + "id": 20, + "username": "kUwXSJ", "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" }, { - "id": "7", - "username": "mHR8vI", + "id": 21, + "username": "w5jlUD", "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" }, { - "id": "8", - "username": "Q3_Ym7", + "id": 22, + "username": "PrezMh", "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" }, { - "id": "9", - "username": "qCSOvw", + "id": "23", + "username": "yply-z", "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" }, { - "id": "10", - "username": "ZB-S9d", + "id": "24", + "username": "q3uhAe", "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" }, { - "id": "11", - "username": "y43Kqx", + "id": "25", + "username": "3X1KkO", "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" }, { - "id": "12", - "username": "xYcrjr", + "id": "26", + "username": "-J2wiF", "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" }, { - "id": "13", - "username": "j3ECJI", + "id": "27", + "username": "UAVjfR", "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" }, { - "id": "14", - "username": "aMHaL_", + "id": "28", + "username": "gmd5et", "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" }, { - "id": "15", - "username": "Y5hxB4", + "id": "29", + "username": "jflYFg", "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" }, { - "id": "16", - "username": "n5i62d", + "id": "30", + "username": "EiWv_v", "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" }, { - "id": "17", - "username": "5mzb7R", + "id": "31", + "username": "q3hIG5", "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" }, { - "id": "18", - "username": "r7WjLl", + "id": "32", + "username": "gIUzVJ", "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" }, { - "id": "19", - "username": "24Sg31", + "id": "33", + "username": "7TmGEj", "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" }, { - "id": "20", - "username": "ZoEbs9", + "id": "34", + "username": "T8qDnA", "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" } ], "tags": [ @@ -3161,209 +670,104 @@ "name": "Alg", "description": "알고리즘", "category": { "id": 3, "name": "subject" } + }, + { + "id": 9, + "name": "Book", + "description": "독서", + "category": { "id": 3, "name": "subject" } + }, + { + "id": 10, + "name": "Health", + "description": "운동", + "category": { "id": 3, "name": "subject" } } ], "createdDate": "2022-07-21" }, { - "id": 30417305, - "title": "2022-http-network-basic-study", - "excerpt": "우아한테크코스 4기 Http&Network Basic 스터디", - "thumbnail": "https://picsum.photos/id/28/200/300", + "id": 28409308, + "title": "2022-looking-for-the-sound-of-an-object", + "excerpt": "객체의 소리를 찾아서 ", + "thumbnail": "https://picsum.photos/id/70/200/300", "recruitmentStatus": "RECRUITMENT_END", "description": "# Prettier plugin sort imports\n\nA prettier plugin to sort import declarations by provided Regular Expression order.\n\n**Note: If you are migrating from v2.x.x to v3.x.x, [Please Read Migration Guidelines](./docs/MIGRATION.md)**\n\n### Input\n\n```javascript\nimport React, {\n FC,\n useEffect,\n useRef,\n ChangeEvent,\n KeyboardEvent,\n} from \"react\";\nimport { logger } from \"@core/logger\";\nimport { reduce, debounce } from \"lodash\";\nimport { Message } from \"../Message\";\nimport { createServer } from \"@server/node\";\nimport { Alert } from \"@ui/Alert\";\nimport { repeat, filter, add } from \"../utils\";\nimport { initializeApp } from \"@core/app\";\nimport { Popup } from \"@ui/Popup\";\nimport { createConnection } from \"@server/database\";\n```\n\n### Output\n\n```javascript\nimport { debounce, reduce } from \"lodash\";\nimport React, {\n ChangeEvent,\n FC,\n KeyboardEvent,\n useEffect,\n useRef,\n} from \"react\";\n\nimport { createConnection } from \"@server/database\";\nimport { createServer } from \"@server/node\";\n\nimport { initializeApp } from \"@core/app\";\nimport { logger } from \"@core/logger\";\n\nimport { Alert } from \"@ui/Alert\";\nimport { Popup } from \"@ui/Popup\";\n\nimport { Message } from \"../Message\";\nimport { add, filter, repeat } from \"../utils\";\n```\n", - "currentMemberCount": 30, - "maxMemberCount": 40, - "createdAt": "createdAt", - "enrollmentEndDate": "2022-07-16", - "startDate": "2022-07-25", - "endDate": "2022-08-24", + "currentMemberCount": 9, + "maxMemberCount": 20, + "enrollmentEndDate": "2022-07-27", + "startDate": "2022-07-02", + "endDate": "2022-08-20", "owner": { "id": 1, "username": "jaejae-yoo", "imageUrl": "https://picsum.photos/id/46/200/300", - "profileUrl": "https://github.com/user/jaejae-yoo" + "profileUrl": "https://github.com/user/jaejae,-yoo", + "participationDate": "2022-08-10" }, "members": [ { - "id": "1", - "username": "-bzjLk", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "2", - "username": "RjLohK", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "3", - "username": "A__hK0", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "4", - "username": "5yLnQq", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "5", - "username": "WB81Sd", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "6", - "username": "aLqysJ", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "7", - "username": "u19U4v", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "8", - "username": "QZXbe9", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "9", - "username": "2r1Gtw", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "10", - "username": "OtKRoF", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "11", - "username": "Q4LcZN", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "12", - "username": "_NE6sa", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "13", - "username": "1GB7Jv", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "14", - "username": "1Ct-oS", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "15", - "username": "pJuNxz", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "16", - "username": "72xCz7", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "17", - "username": "ax4HBJ", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "18", - "username": "uGMqXh", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "19", - "username": "-Ksx2B", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "20", - "username": "7FgbtP", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "21", - "username": "jfeepj", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "22", - "username": "LGtmWb", + "id": 1, + "username": "x95RXT", "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" }, { - "id": "23", - "username": "dn2VPO", + "id": 2, + "username": "YCA10T", "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" }, { - "id": "24", - "username": "ccdcvC", + "id": 3, + "username": "_ws6Lx", "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" }, { - "id": "25", - "username": "8voCRp", + "id": 4, + "username": "cfqVdQ", "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" }, { - "id": "26", - "username": "S7fOG_", + "id": 5, + "username": "BVKAeK", "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" }, { - "id": "27", - "username": "yvfq8f", + "id": 6, + "username": "qBuxOB", "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" }, { - "id": "28", - "username": "FSyDCw", + "id": 7, + "username": "w7-Alk", "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" }, { - "id": "29", - "username": "alqSGO", + "id": 8, + "username": "M2sXAn", "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" }, { - "id": "30", - "username": "nsZDeV", + "id": 9, + "username": "W9YJOX", "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" } ], "tags": [ @@ -3432,59 +836,189 @@ "name": "Network", "description": "네트워크", "category": { "id": 3, "name": "subject" } + }, + { + "id": 12, + "name": "CS", + "description": "컴퓨터 과학", + "category": { "id": 3, "name": "subject" } } ], "createdDate": "2022-07-21" }, { - "id": 77183556, - "title": "2022-lv2-effective-java-interview", - "excerpt": "2022 우아한테크코스 BE Level2 이펙티브자바 인터뷰 스터디", - "thumbnail": "https://picsum.photos/id/75/200/300", - "recruitmentStatus": "RECRUITMENT_END", + "id": 31449331, + "title": "2022-kotudy", + "excerpt": "우아한테크코스 4기 😋 코틀린 스터디", + "thumbnail": "https://picsum.photos/id/88/200/300", + "recruitmentStatus": "RECRUITMENT_START", "description": "# Prettier plugin sort imports\n\nA prettier plugin to sort import declarations by provided Regular Expression order.\n\n**Note: If you are migrating from v2.x.x to v3.x.x, [Please Read Migration Guidelines](./docs/MIGRATION.md)**\n\n### Input\n\n```javascript\nimport React, {\n FC,\n useEffect,\n useRef,\n ChangeEvent,\n KeyboardEvent,\n} from \"react\";\nimport { logger } from \"@core/logger\";\nimport { reduce, debounce } from \"lodash\";\nimport { Message } from \"../Message\";\nimport { createServer } from \"@server/node\";\nimport { Alert } from \"@ui/Alert\";\nimport { repeat, filter, add } from \"../utils\";\nimport { initializeApp } from \"@core/app\";\nimport { Popup } from \"@ui/Popup\";\nimport { createConnection } from \"@server/database\";\n```\n\n### Output\n\n```javascript\nimport { debounce, reduce } from \"lodash\";\nimport React, {\n ChangeEvent,\n FC,\n KeyboardEvent,\n useEffect,\n useRef,\n} from \"react\";\n\nimport { createConnection } from \"@server/database\";\nimport { createServer } from \"@server/node\";\n\nimport { initializeApp } from \"@core/app\";\nimport { logger } from \"@core/logger\";\n\nimport { Alert } from \"@ui/Alert\";\nimport { Popup } from \"@ui/Popup\";\n\nimport { Message } from \"../Message\";\nimport { add, filter, repeat } from \"../utils\";\n```\n", - "currentMemberCount": 5, - "maxMemberCount": 37, - "createdAt": "createdAt", - "enrollmentEndDate": "2022-07-20", - "startDate": "2022-07-06", - "endDate": "2022-08-22", + "currentMemberCount": 22, + "maxMemberCount": 28, + "enrollmentEndDate": "2022-07-08", + "startDate": "2022-07-03", + "endDate": "2022-08-17", "owner": { "id": 1, "username": "jaejae-yoo", "imageUrl": "https://picsum.photos/id/46/200/300", - "profileUrl": "https://github.com/user/jaejae-yoo" + "profileUrl": "https://github.com/user/jaejae,-yoo", + "participationDate": "2022-08-10" }, "members": [ { - "id": "1", - "username": "pO0gJT", + "id": 1, + "username": "ls3jCf", + "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" + }, + { + "id": 2, + "username": "wZAre5", + "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" + }, + { + "id": 3, + "username": "WEnmAX", + "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" + }, + { + "id": 4, + "username": "7cOCQu", + "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" + }, + { + "id": 5, + "username": "BcKhmv", + "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" + }, + { + "id": 6, + "username": "Voo449", + "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" + }, + { + "id": 7, + "username": "2RoP9-", + "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" + }, + { + "id": 8, + "username": "UbqZjR", + "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" + }, + { + "id": 9, + "username": "hp0jNp", + "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" + }, + { + "id": 10, + "username": "22CmFK", + "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" + }, + { + "id": 11, + "username": "hMXSv8", + "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" + }, + { + "id": 12, + "username": "QvSvWX", + "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" + }, + { + "id": 13, + "username": "EsWFF2", + "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" + }, + { + "id": 14, + "username": "2bySu5", + "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" + }, + { + "id": 15, + "username": "yCdgB2", + "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" + }, + { + "id": 16, + "username": "qtVI_Z", + "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" + }, + { + "id": 17, + "username": "HfWCVi", + "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" + }, + { + "id": 18, + "username": "80tGvC", "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" }, { - "id": "2", - "username": "aQgXad", + "id": 19, + "username": "2TR80R", "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" }, { - "id": "3", - "username": "WpGlff", + "id": 20, + "username": "v7VtDk", "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" }, { - "id": "4", - "username": "H5qsSZ", + "id": 21, + "username": "Tv3m-j", "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" }, { - "id": "5", - "username": "zSGERL", + "id": 22, + "username": "l6iC23", "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" } ], "tags": [ @@ -3493,233 +1027,232 @@ "name": "JS", "description": "자바스크립트", "category": { "id": 3, "name": "subject" } - }, - { - "id": 2, - "name": "Java", - "description": "자바", - "category": { "id": 3, "name": "subject" } - }, - { - "id": 3, - "name": "React", - "description": "리액트", - "category": { "id": 3, "name": "subject" } } ], "createdDate": "2022-07-21" }, { - "id": 85392540, - "title": "2022-book-muscle", - "excerpt": "프론트엔드 독서 클럽", - "thumbnail": "https://picsum.photos/id/92/200/300", + "id": 76240041, + "title": "2022-no-posting-you-die", + "excerpt": "'강제 블로그 포스팅 연합' 스터디입니다", + "thumbnail": "https://picsum.photos/id/27/200/300", "recruitmentStatus": "RECRUITMENT_START", "description": "# Prettier plugin sort imports\n\nA prettier plugin to sort import declarations by provided Regular Expression order.\n\n**Note: If you are migrating from v2.x.x to v3.x.x, [Please Read Migration Guidelines](./docs/MIGRATION.md)**\n\n### Input\n\n```javascript\nimport React, {\n FC,\n useEffect,\n useRef,\n ChangeEvent,\n KeyboardEvent,\n} from \"react\";\nimport { logger } from \"@core/logger\";\nimport { reduce, debounce } from \"lodash\";\nimport { Message } from \"../Message\";\nimport { createServer } from \"@server/node\";\nimport { Alert } from \"@ui/Alert\";\nimport { repeat, filter, add } from \"../utils\";\nimport { initializeApp } from \"@core/app\";\nimport { Popup } from \"@ui/Popup\";\nimport { createConnection } from \"@server/database\";\n```\n\n### Output\n\n```javascript\nimport { debounce, reduce } from \"lodash\";\nimport React, {\n ChangeEvent,\n FC,\n KeyboardEvent,\n useEffect,\n useRef,\n} from \"react\";\n\nimport { createConnection } from \"@server/database\";\nimport { createServer } from \"@server/node\";\n\nimport { initializeApp } from \"@core/app\";\nimport { logger } from \"@core/logger\";\n\nimport { Alert } from \"@ui/Alert\";\nimport { Popup } from \"@ui/Popup\";\n\nimport { Message } from \"../Message\";\nimport { add, filter, repeat } from \"../utils\";\n```\n", - "currentMemberCount": 32, - "maxMemberCount": 34, - "createdAt": "createdAt", - "enrollmentEndDate": "2022-07-31", - "startDate": "2022-07-11", - "endDate": "2022-08-13", + "currentMemberCount": 29, + "maxMemberCount": 39, + "enrollmentEndDate": "2022-07-16", + "startDate": "2022-07-18", + "endDate": "2022-08-29", "owner": { "id": 1, "username": "jaejae-yoo", "imageUrl": "https://picsum.photos/id/46/200/300", - "profileUrl": "https://github.com/user/jaejae-yoo" + "profileUrl": "https://github.com/user/jaejae,-yoo", + "participationDate": "2022-08-10" }, "members": [ { - "id": "1", - "username": "BzM2ka", + "id": 1, + "username": "o2KP49", "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" }, { - "id": "2", - "username": "aXDTfN", + "id": 2, + "username": "KsbIKf", "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" }, { - "id": "3", - "username": "YPYYsi", + "id": 3, + "username": "i8Gxro", "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" }, { - "id": "4", - "username": "74KnBp", + "id": 4, + "username": "ub_d3J", "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" }, { - "id": "5", - "username": "ZGeFll", + "id": 5, + "username": "k2HsHB", "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" }, { - "id": "6", - "username": "rmlEya", + "id": 6, + "username": "A67zDp", "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" }, { - "id": "7", - "username": "c7PyWP", + "id": 7, + "username": "SU33By", "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" }, { - "id": "8", - "username": "4tkhAF", + "id": 8, + "username": "Q4kL9E", "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" }, { - "id": "9", - "username": "aYCVB_", + "id": 9, + "username": "fsu41-", "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" }, { - "id": "10", - "username": "jp7LD9", + "id": 10, + "username": "NotLZH", "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" }, { - "id": "11", - "username": "psH9mf", + "id": 11, + "username": "2U8goX", "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" }, { - "id": "12", - "username": "25SXeA", + "id": 12, + "username": "UIeMmS", "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" }, { - "id": "13", - "username": "KaBol0", + "id": 13, + "username": "NPVgvy", "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" }, { - "id": "14", - "username": "wkhvTR", + "id": 14, + "username": "VNCjEh", "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" }, { - "id": "15", - "username": "F5K2_p", + "id": 15, + "username": "ap2JFi", "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" }, { - "id": "16", - "username": "ZPR2C9", + "id": 16, + "username": "t_30Aq", "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" }, { - "id": "17", - "username": "n-kGW_", + "id": 17, + "username": "fHe7pr", "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" }, { - "id": "18", - "username": "S40sGc", + "id": 18, + "username": "OUiw7g", "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" }, { - "id": "19", - "username": "agZXL0", + "id": 19, + "username": "dJRytJ", "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" }, { - "id": "20", - "username": "Ro9Jv7", + "id": 20, + "username": "LQbQZi", "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" }, { - "id": "21", - "username": "jISLTq", + "id": 21, + "username": "WNMJr2", "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" }, { - "id": "22", - "username": "6izMsI", + "id": 22, + "username": "ss2A4w", "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" }, { "id": "23", - "username": "ep5YVY", + "username": "gnQmnS", "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" }, { "id": "24", - "username": "4VqpY7", + "username": "IHU6Jg", "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" }, { "id": "25", - "username": "5HHB1Y", + "username": "osRHjF", "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" }, { "id": "26", - "username": "BRvfY-", + "username": "BWerqd", "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" }, { "id": "27", - "username": "WorZUt", + "username": "Cy8DjP", "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" }, { "id": "28", - "username": "lQhRRo", + "username": "boAipa", "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" }, { "id": "29", - "username": "JdnVUa", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "30", - "username": "hoc8cF", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "31", - "username": "g3iv3C", - "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" - }, - { - "id": "32", - "username": "K1TBpd", + "username": "9CyEVU", "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" } ], "tags": [ @@ -3770,149 +1303,187 @@ "name": "Alg", "description": "알고리즘", "category": { "id": 3, "name": "subject" } + }, + { + "id": 9, + "name": "Book", + "description": "독서", + "category": { "id": 3, "name": "subject" } + }, + { + "id": 10, + "name": "Health", + "description": "운동", + "category": { "id": 3, "name": "subject" } + }, + { + "id": 11, + "name": "Network", + "description": "네트워크", + "category": { "id": 3, "name": "subject" } } ], "createdDate": "2022-07-21" }, { - "id": 9841859, - "title": "2022-woowahan-everybook", - "excerpt": "안녕하세요 '리버'네 독서천국입니다😊", - "thumbnail": "https://picsum.photos/id/88/200/300", + "id": 55695590, + "title": "2022-Real-MySQL", + "excerpt": "⚡️토르⚡️의 짜릿한 Real MySQL 뽀개기 🔨", + "thumbnail": "https://picsum.photos/id/25/200/300", "recruitmentStatus": "RECRUITMENT_END", "description": "# Prettier plugin sort imports\n\nA prettier plugin to sort import declarations by provided Regular Expression order.\n\n**Note: If you are migrating from v2.x.x to v3.x.x, [Please Read Migration Guidelines](./docs/MIGRATION.md)**\n\n### Input\n\n```javascript\nimport React, {\n FC,\n useEffect,\n useRef,\n ChangeEvent,\n KeyboardEvent,\n} from \"react\";\nimport { logger } from \"@core/logger\";\nimport { reduce, debounce } from \"lodash\";\nimport { Message } from \"../Message\";\nimport { createServer } from \"@server/node\";\nimport { Alert } from \"@ui/Alert\";\nimport { repeat, filter, add } from \"../utils\";\nimport { initializeApp } from \"@core/app\";\nimport { Popup } from \"@ui/Popup\";\nimport { createConnection } from \"@server/database\";\n```\n\n### Output\n\n```javascript\nimport { debounce, reduce } from \"lodash\";\nimport React, {\n ChangeEvent,\n FC,\n KeyboardEvent,\n useEffect,\n useRef,\n} from \"react\";\n\nimport { createConnection } from \"@server/database\";\nimport { createServer } from \"@server/node\";\n\nimport { initializeApp } from \"@core/app\";\nimport { logger } from \"@core/logger\";\n\nimport { Alert } from \"@ui/Alert\";\nimport { Popup } from \"@ui/Popup\";\n\nimport { Message } from \"../Message\";\nimport { add, filter, repeat } from \"../utils\";\n```\n", "currentMemberCount": 20, - "maxMemberCount": 38, - "createdAt": "createdAt", - "enrollmentEndDate": "2022-07-18", - "startDate": "2022-07-05", - "endDate": "2022-08-17", + "maxMemberCount": 39, + "enrollmentEndDate": "2022-07-20", + "startDate": "2022-07-15", + "endDate": "2022-08-05", "owner": { "id": 1, "username": "jaejae-yoo", "imageUrl": "https://picsum.photos/id/46/200/300", - "profileUrl": "https://github.com/user/jaejae-yoo" + "profileUrl": "https://github.com/user/jaejae,-yoo", + "participationDate": "2022-08-10" }, "members": [ { - "id": "1", - "username": "DkZ74t", + "id": 1, + "username": "XEaojp", "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" }, { - "id": "2", - "username": "3DCRsR", + "id": 2, + "username": "kssaAC", "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" }, { - "id": "3", - "username": "8aCut6", + "id": 3, + "username": "a1Odtq", "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" }, { - "id": "4", - "username": "LG4752", + "id": 4, + "username": "ydqFR1", "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" }, { - "id": "5", - "username": "9HMi4i", + "id": 5, + "username": "nikhhf", "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" }, { - "id": "6", - "username": "r2FqpB", + "id": 6, + "username": "S4K87p", "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" }, { - "id": "7", - "username": "zCt-Ha", + "id": 7, + "username": "PqJicY", "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" }, { - "id": "8", - "username": "4QsEYM", + "id": 8, + "username": "ASPPzr", "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" }, { - "id": "9", - "username": "BfHDeS", + "id": 9, + "username": "H27a0Q", "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" }, { - "id": "10", - "username": "0vyszz", + "id": 10, + "username": "m_ukzu", "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" }, { - "id": "11", - "username": "YUyq8T", + "id": 11, + "username": "W2HZDJ", "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" }, { - "id": "12", - "username": "Tu_AGN", + "id": 12, + "username": "2VF7p6", "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" }, { - "id": "13", - "username": "3DkcF1", + "id": 13, + "username": "8nvdxo", "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" }, { - "id": "14", - "username": "jUN-Gi", + "id": 14, + "username": "QYk4LP", "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" }, { - "id": "15", - "username": "fMVv7A", + "id": 15, + "username": "NPu4f4", "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" }, { - "id": "16", - "username": "bnUKMu", + "id": 16, + "username": "A0SG6x", "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" }, { - "id": "17", - "username": "ukoVM_", + "id": 17, + "username": "tdswHF", "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" }, { - "id": "18", - "username": "mtKrJN", + "id": 18, + "username": "mk0duP", "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" }, { - "id": "19", - "username": "RoJsE9", + "id": 19, + "username": "eAnCYr", "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" + "profileUrl": "https://github.com/airman5573", + "participationDate": "2022-08-10" }, { - "id": "20", - "username": "MAp6TZ", + "id": 20, + "username": "e-iqV9", "imageUrl": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80", - "profileUrl": "https://github.com/airman5573" + "profileUrl": "https://github.com/airman5573,", + "participationDate": "2022-08-10" } ], "tags": [ diff --git a/frontend/src/mocks/tags.json b/frontend/src/mocks/tags.json index 05cfa9c0e..73e32eb6b 100644 --- a/frontend/src/mocks/tags.json +++ b/frontend/src/mocks/tags.json @@ -107,6 +107,15 @@ "id": 3, "name": "subject" } + }, + { + "id": 9, + "name": "Etc", + "description": "기타", + "category": { + "id": 3, + "name": "subject" + } } ] } diff --git a/frontend/src/pages/create-study-page/CreateStudyPage.tsx b/frontend/src/pages/create-study-page/CreateStudyPage.tsx index 39535c07c..064b1b104 100644 --- a/frontend/src/pages/create-study-page/CreateStudyPage.tsx +++ b/frontend/src/pages/create-study-page/CreateStudyPage.tsx @@ -8,7 +8,7 @@ import DescriptionTab from '@create-study-page/components/description-tab/Descri import EnrollmentEndDate from '@create-study-page/components/enrollment-end-date/EnrollmentEndDate'; import Excerpt from '@create-study-page/components/excerpt/Excerpt'; import MaxMemberCount from '@create-study-page/components/max-member-count/MaxMemberCount'; -import Period from '@create-study-page/components/period/Peroid'; +import Period from '@create-study-page/components/period/Period'; import Publish from '@create-study-page/components/publish/Publish'; import Subject from '@create-study-page/components/subject/Subject'; import Title from '@create-study-page/components/title/Title'; diff --git a/frontend/src/pages/create-study-page/components/category/Category.tsx b/frontend/src/pages/create-study-page/components/category/Category.tsx index 884279ab8..5aee4e64b 100644 --- a/frontend/src/pages/create-study-page/components/category/Category.tsx +++ b/frontend/src/pages/create-study-page/components/category/Category.tsx @@ -1,6 +1,6 @@ import tw from '@utils/tw'; -import type { Tag } from '@custom-types'; +import type { StudyDetail, Tag } from '@custom-types'; import { useGetTags } from '@api/tags'; @@ -13,6 +13,8 @@ import MetaBox from '@create-study-page/components/meta-box/MetaBox'; export type CategoryProps = { className?: string; + originalGeneration?: Tag; + originalAreas?: StudyDetail['tags']; }; const getClassifiedTags = (tags: Array) => { @@ -28,7 +30,7 @@ const getClassifiedTags = (tags: Array) => { }; }; -const Category = ({ className }: CategoryProps) => { +const Category: React.FC = ({ className, originalGeneration, originalAreas }) => { const { register } = useFormContext(); const { data, isLoading, isError, isSuccess } = useGetTags(); @@ -47,7 +49,7 @@ const Category = ({ className }: CategoryProps) => { <> 기수 : - + {generations.map(({ id, name }) => (