From 40edfff65cae69959aff476c0bb698cd88ac16d3 Mon Sep 17 00:00:00 2001 From: JeongwooSeo Date: Sun, 17 Nov 2024 23:05:36 +0900 Subject: [PATCH 001/180] =?UTF-8?q?chore:=20lottie=20=EB=9D=BC=EC=9D=B4?= =?UTF-8?q?=EB=B8=8C=EB=9F=AC=EB=A6=AC=20=EC=84=A4=EC=B9=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/package.json | 1 + pnpm-lock.yaml | 53 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/frontend/package.json b/frontend/package.json index 943e72f1..d2f6d682 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -16,6 +16,7 @@ "react": "^18.3.1", "react-dom": "^18.3.1", "react-icons": "^5.3.0", + "react-lottie": "^1.2.7", "react-router-dom": "^6.27.0", "socket.io-client": "^4.8.1", "zustand": "^5.0.1" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 069a910e..f2c11137 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -129,6 +129,9 @@ importers: react-icons: specifier: ^5.3.0 version: 5.3.0(react@18.3.1) + react-lottie: + specifier: ^1.2.7 + version: 1.2.7(react@18.3.1) react-router-dom: specifier: ^6.27.0 version: 6.27.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -1401,6 +1404,9 @@ packages: peerDependencies: '@babel/core': ^7.0.0 + babel-runtime@6.26.0: + resolution: {integrity: sha512-ITKNuq2wKlW1fJg9sSW52eepoYgZBggvOAHC0u/CYu/qxQ9EVzThCgR69BnSXLHjy2f7SY5zaQ4yt7H9ZVxY2g==} + balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -1626,6 +1632,10 @@ packages: cookiejar@2.1.4: resolution: {integrity: sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==} + core-js@2.6.12: + resolution: {integrity: sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==} + deprecated: core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js. + core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} @@ -2656,6 +2666,9 @@ packages: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true + lottie-web@5.12.2: + resolution: {integrity: sha512-uvhvYPC8kGPjXT3MyKMrL3JitEAmDMp30lVkuq/590Mw9ok6pWcFCwXJveo0t5uqYw1UREQHofD+jVpdjBv8wg==} + lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} @@ -3021,6 +3034,9 @@ packages: resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} engines: {node: '>= 6'} + prop-types@15.8.1: + resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + proxy-addr@2.0.7: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} @@ -3060,9 +3076,17 @@ packages: peerDependencies: react: '*' + react-is@16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + react-is@18.3.1: resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + react-lottie@1.2.7: + resolution: {integrity: sha512-VVY/dL77jdpAY97LeAbPKExVqlKqZqqG0PtdCJh1NmChtfUlemUQXPkF6rrOw2cp3T+xmtcsFs+N9U+nXUzAqw==} + peerDependencies: + react: '>=15.0.0' + react-refresh@0.14.2: resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==} engines: {node: '>=0.10.0'} @@ -3109,6 +3133,9 @@ packages: reflect-metadata@0.2.2: resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==} + regenerator-runtime@0.11.1: + resolution: {integrity: sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==} + repeat-string@1.6.1: resolution: {integrity: sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==} engines: {node: '>=0.10'} @@ -5272,6 +5299,11 @@ snapshots: babel-plugin-jest-hoist: 29.6.3 babel-preset-current-node-syntax: 1.1.0(@babel/core@7.26.0) + babel-runtime@6.26.0: + dependencies: + core-js: 2.6.12 + regenerator-runtime: 0.11.1 + balanced-match@1.0.2: {} base64-js@1.5.1: {} @@ -5492,6 +5524,8 @@ snapshots: cookiejar@2.1.4: {} + core-js@2.6.12: {} + core-util-is@1.0.3: {} cors@2.8.5: @@ -6795,6 +6829,8 @@ snapshots: dependencies: js-tokens: 4.0.0 + lottie-web@5.12.2: {} + lru-cache@10.4.3: {} lru-cache@5.1.1: @@ -7100,6 +7136,12 @@ snapshots: kleur: 3.0.3 sisteransi: 1.0.5 + prop-types@15.8.1: + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + react-is: 16.13.1 + proxy-addr@2.0.7: dependencies: forwarded: 0.2.0 @@ -7138,8 +7180,17 @@ snapshots: dependencies: react: 18.3.1 + react-is@16.13.1: {} + react-is@18.3.1: {} + react-lottie@1.2.7(react@18.3.1): + dependencies: + babel-runtime: 6.26.0 + lottie-web: 5.12.2 + prop-types: 15.8.1 + react: 18.3.1 + react-refresh@0.14.2: {} react-router-dom@6.27.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): @@ -7190,6 +7241,8 @@ snapshots: reflect-metadata@0.2.2: {} + regenerator-runtime@0.11.1: {} + repeat-string@1.6.1: {} require-directory@2.1.1: {} From 501b0bfe1f30ec4682c7d2c5c31b4451df372b99 Mon Sep 17 00:00:00 2001 From: JeongwooSeo Date: Sun, 17 Nov 2024 23:49:19 +0900 Subject: [PATCH 002/180] =?UTF-8?q?chore:=20lottie=20=EB=9D=BC=EC=9D=B4?= =?UTF-8?q?=EB=B8=8C=EB=9F=AC=EB=A6=AC=20=ED=83=80=EC=9E=85=20=EC=84=A4?= =?UTF-8?q?=EC=B9=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/package.json | 1 + pnpm-lock.yaml | 10 ++++++++++ 2 files changed, 11 insertions(+) diff --git a/frontend/package.json b/frontend/package.json index d2f6d682..9a6cf26d 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -12,6 +12,7 @@ "test": "echo \"Error: no test specified\" && exit 1" }, "dependencies": { + "@types/react-lottie": "^1.2.10", "@types/socket.io-client": "^3.0.0", "react": "^18.3.1", "react-dom": "^18.3.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f2c11137..74f0d6cf 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -117,6 +117,9 @@ importers: frontend: dependencies: + '@types/react-lottie': + specifier: ^1.2.10 + version: 1.2.10 '@types/socket.io-client': specifier: ^3.0.0 version: 3.0.0 @@ -1110,6 +1113,9 @@ packages: '@types/react-dom@18.3.1': resolution: {integrity: sha512-qW1Mfv8taImTthu4KoXgDfLuk4bydU6Q/TkADnDWWHwi4NX4BR+LWfTp2sVmTqRrsHvyDDTelgelxJ+SsejKKQ==} + '@types/react-lottie@1.2.10': + resolution: {integrity: sha512-rCd1p3US4ELKJlqwVnP0h5b24zt5p9OCvKUoNpYExLqwbFZMWEiJ6EGLMmH7nmq5V7KomBIbWO2X/XRFsL0vCA==} + '@types/react@18.3.12': resolution: {integrity: sha512-D2wOSq/d6Agt28q7rSI3jhU7G6aiuzljDGZ2hTZHIkrTLUI+AF3WMeKkEZ9nN2fkBAlcktT6vcZjDFiIhMYEQw==} @@ -4860,6 +4866,10 @@ snapshots: dependencies: '@types/react': 18.3.12 + '@types/react-lottie@1.2.10': + dependencies: + '@types/react': 18.3.12 + '@types/react@18.3.12': dependencies: '@types/prop-types': 15.7.13 From 846fee6d32ec07261a4e6d1c37a86c9525e9cd69 Mon Sep 17 00:00:00 2001 From: JeongwooSeo Date: Sun, 17 Nov 2024 23:49:39 +0900 Subject: [PATCH 003/180] =?UTF-8?q?feat:=20=EC=97=90=EB=9F=AC=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 로티애니메이션 추가 - 변경 가능 --- frontend/public/assets/snowman.json | 1 + frontend/src/pages/ErrorPage.tsx | 31 +++++++++++++++++++++++++++++ frontend/src/routes.tsx | 3 ++- 3 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 frontend/public/assets/snowman.json create mode 100644 frontend/src/pages/ErrorPage.tsx diff --git a/frontend/public/assets/snowman.json b/frontend/public/assets/snowman.json new file mode 100644 index 00000000..1f196699 --- /dev/null +++ b/frontend/public/assets/snowman.json @@ -0,0 +1 @@ +{"v":"5.5.8","fr":30,"ip":0,"op":105,"w":1080,"h":1080,"nm":"Snowman_update","ddd":0,"assets":[{"id":"comp_0","layers":[{"ddd":0,"ind":1,"ty":3,"nm":"Null 1","sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":38,"s":[540,540,0],"to":[21.5,0,0],"ti":[19.667,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":50,"s":[669,540,0],"to":[-19.667,0,0],"ti":[41.167,0,0]},{"t":51,"s":[422,540,0],"h":1},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":52,"s":[422,540,0],"to":[19.667,0,0],"ti":[-19.667,0,0]},{"t":62,"s":[540,540,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"ip":0,"op":1800,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Hat","parent":15,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[545,393,0],"ix":2},"a":{"a":0,"k":[545,393,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[80.98,-13.698],[-6.125,9.801],[-3.465,-0.08],[-2.832,-51.339]],"o":[[0,0],[1.837,-2.939],[22.742,0.528],[0,0]],"v":[[-67.282,-1.197],[-60.863,-23.951],[-52.292,-28.537],[67.282,28.617]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2078,0.898,0.6039,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[546.288,426.457],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-10.216],[10.215,0],[0,10.216],[-10.216,0]],"o":[[0,10.216],[-10.216,0],[0,-10.216],[10.215,0]],"v":[[18.497,0],[0,18.497],[-18.498,0],[0,-18.497]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2078,0.898,0.6039,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0.31,"y":1},"o":{"x":0.333,"y":0},"t":20,"s":[510.064,349.065],"to":[0,2.532],"ti":[-4.616,5.917]},{"i":{"x":0.31,"y":1},"o":{"x":0.333,"y":0},"t":25.079,"s":[510.064,364.256],"to":[4.616,-5.917],"ti":[0,0]},{"i":{"x":1,"y":1},"o":{"x":0.557,"y":0},"t":30.159,"s":[537.759,313.561],"to":[0,0],"ti":[4.616,-5.917]},{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0},"t":47.937,"s":[510.064,364.256],"to":[-4.616,5.917],"ti":[0,2.532]},{"t":52,"s":[510.064,349.065]}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[-2.569,-75.222]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[-54.511,7.905],[-30.348,-7.11],[-33.973,-24.837],[-25.11,-42.564],[54.511,42.564]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":22.032,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[-2.569,-75.222]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[-54.511,7.905],[-30.348,-7.11],[-33.973,-24.837],[-30.11,-38.064],[54.511,42.564]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":26.604,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[-2.569,-75.222]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[-54.511,7.905],[-30.348,-7.11],[-22.473,-37.337],[-13.11,-42.564],[54.511,42.564]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":27.111,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[-2.569,-75.222]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[-54.511,7.905],[-30.348,-7.11],[-21.973,-43.837],[-7.61,-49.064],[54.511,42.564]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":27.619,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[-2.569,-75.222]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[-54.511,7.905],[-20.348,-21.61],[-15.473,-54.337],[-5.11,-57.064],[54.511,42.564]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":28.635,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[-2.569,-75.222]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[-54.511,7.905],[-20.348,-21.61],[-10.973,-61.837],[-2.11,-67.564],[54.511,42.564]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":39.302,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[-2.569,-75.222]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[-54.511,7.905],[-20.348,-21.61],[-10.973,-61.837],[-2.11,-67.564],[54.511,42.564]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40.317,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[-2.569,-75.222]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[-54.511,7.905],[-23.098,-17.86],[-20.223,-53.587],[-11.36,-60.814],[54.511,42.564]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":41.333,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[-2.569,-75.222]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[-54.511,7.905],[-25.848,-14.11],[-23.473,-49.337],[-14.61,-55.064],[54.511,42.564]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":43.873,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[-2.569,-75.222]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[-54.511,7.905],[-25.848,-14.11],[-27.973,-45.337],[-19.11,-51.064],[54.511,42.564]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":44.889,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[-2.569,-75.222]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[-54.511,7.905],[-30.348,-7.11],[-33.973,-24.837],[-25.11,-42.564],[54.511,42.564]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":46.921,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[-2.569,-75.222]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[-54.511,7.905],[-30.348,-7.11],[-33.973,-24.837],[-30.11,-38.064],[54.511,42.564]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":47.937,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[2.314,-82.879]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[-54.511,7.905],[-30.348,-7.11],[-33.973,-24.837],[-31.11,-33.964],[54.511,42.564]],"c":false}]},{"t":52,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[-2.569,-75.222]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[-54.511,7.905],[-30.348,-7.11],[-33.973,-24.837],[-25.11,-42.564],[54.511,42.564]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.668,0.1499,0.132,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[550.448,390.067],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":2,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"mask 3","td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.011,"y":1},"o":{"x":0.42,"y":0},"t":23,"s":[539,549,0],"to":[-1.08,-21.667,0],"ti":[1.08,21.667,0]},{"i":{"x":0.667,"y":0.667},"o":{"x":0.376,"y":0.376},"t":38,"s":[532.517,419,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.773,"y":0},"t":68,"s":[532.517,419,0],"to":[1.08,21.667,0],"ti":[-1.08,-21.667,0]},{"t":83,"s":[539,549,0]}],"ix":2},"a":{"a":0,"k":[539,549,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-42.938],[42.938,0],[0,42.938],[-42.938,0]],"o":[[0,42.938],[-42.938,0],[0,-42.938],[42.938,0]],"v":[[77.746,0],[0,77.746],[-77.746,0],[0,-77.746]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.9064,0.9136,0.9105,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[538.483,475.681],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"eyes 2","parent":1,"tt":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.773,"y":0},"t":68,"s":[-7.483,-121,0],"to":[1.08,21.667,0],"ti":[-1.08,-21.667,0]},{"t":83,"s":[-1,9,0]}],"ix":2},"a":{"a":0,"k":[539,549,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-4.054],[4.054,0],[0,4.053],[-4.054,0]],"o":[[0,4.053],[-4.054,0],[0,-4.054],[4.054,0]],"v":[[7.339,0],[0,7.339],[-7.34,0],[0,-7.339]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[560.644,450.986],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":90,"s":[100,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":100,"s":[100,0]},{"t":110,"s":[100,100]}],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-4.054],[4.053,0],[0,4.053],[-4.054,0]],"o":[[0,4.053],[-4.054,0],[0,-4.054],[4.053,0]],"v":[[7.34,0],[0,7.339],[-7.34,0],[0,-7.339]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[501.624,450.986],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":90,"s":[100,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":100,"s":[100,0]},{"t":110,"s":[100,100]}],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":51,"op":300,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"mask 2","td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.011,"y":1},"o":{"x":0.42,"y":0},"t":23,"s":[539,549,0],"to":[-1.08,-21.667,0],"ti":[1.08,21.667,0]},{"i":{"x":0.667,"y":0.667},"o":{"x":0.376,"y":0.376},"t":38,"s":[532.517,419,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.773,"y":0},"t":68,"s":[532.517,419,0],"to":[1.08,21.667,0],"ti":[-1.08,-21.667,0]},{"t":83,"s":[539,549,0]}],"ix":2},"a":{"a":0,"k":[539,549,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-42.938],[42.938,0],[0,42.938],[-42.938,0]],"o":[[0,42.938],[-42.938,0],[0,-42.938],[42.938,0]],"v":[[77.746,0],[0,77.746],[-77.746,0],[0,-77.746]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.9064,0.9136,0.9105,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[538.483,475.681],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":120,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"eyes","parent":1,"tt":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.011,"y":1},"o":{"x":0.42,"y":0},"t":23,"s":[-1,9,0],"to":[-1.08,-21.667,0],"ti":[1.08,21.667,0]},{"t":38,"s":[-7.483,-121,0]}],"ix":2},"a":{"a":0,"k":[539,549,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-4.054],[4.054,0],[0,4.053],[-4.054,0]],"o":[[0,4.053],[-4.054,0],[0,-4.054],[4.054,0]],"v":[[7.339,0],[0,7.339],[-7.34,0],[0,-7.339]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[560.644,450.986],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":10,"s":[100,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":20,"s":[100,0]},{"t":30,"s":[100,100]}],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-4.054],[4.053,0],[0,4.053],[-4.054,0]],"o":[[0,4.053],[-4.054,0],[0,-4.054],[4.053,0]],"v":[[7.34,0],[0,7.339],[-7.34,0],[0,-7.339]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[501.624,450.986],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":10,"s":[100,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":20,"s":[100,0]},{"t":30,"s":[100,100]}],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":50,"st":0,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":"Shall","parent":15,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[537,585,0],"ix":2},"a":{"a":0,"k":[537,585,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,-38.531],[18.349,6.728],[-22.629,67.276]],"o":[[0,0],[0,38.531],[-18.347,-6.727],[0,0]],"v":[[38.021,-60.753],[12.13,5.708],[0.509,54.025],[-15.392,-42.608]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.668,0.1499,0.132,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[611.434,549.125],"ix":2},"a":{"a":0,"k":[36.723,-59.332],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":23,"s":[0]},{"i":{"x":[0],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":38,"s":[31]},{"i":{"x":[0],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":45,"s":[33]},{"i":{"x":[0],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":60,"s":[31]},{"i":{"x":[0],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":75,"s":[33]},{"t":90,"s":[0]}],"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[9.285,8.593],[0.387,-9.764],[-3.869,-7.812],[-38.688,1.953],[-7.739,3.516],[5.417,4.296],[-2.321,5.078],[7.739,-7.421],[51.846,-0.437]],"o":[[0,0],[-0.386,9.765],[3.868,7.812],[38.689,-1.953],[7.737,-3.515],[-5.416,-4.297],[2.321,-5.078],[-7.737,7.422],[-46.427,0.391]],"v":[[-65.384,-26.365],[-73.509,-18.163],[-83.954,2.148],[-10.446,35.349],[80.086,9.96],[81.246,-6.054],[80.859,-20.506],[73.895,-29.881],[-2.321,-3.32]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.668,0.1499,0.132,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[534.881,538.192],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":8,"ty":4,"nm":"Nose 4","parent":15,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":0.639},"o":{"x":0.333,"y":0},"t":57,"s":[468,493,0],"to":[1.139,0,0],"ti":[-8.92,0,0]},{"i":{"x":0.667,"y":0.831},"o":{"x":0.333,"y":0.235},"t":61,"s":[494.601,493,0],"to":[4.961,0,0],"ti":[-3.37,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0.446},"t":63,"s":[515.059,493,0],"to":[5.631,0,0],"ti":[-1.788,0,0]},{"t":64,"s":[518,493,0]}],"ix":2},"a":{"a":0,"k":[518,493,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":57,"s":[20,73,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":58,"s":[28,100,100]},{"t":63,"s":[100,100,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":64,"s":[{"i":[[0,0],[0,0],[-5.105,-1.095],[0,0]],"o":[[0,0],[-5.105,1.095],[0,0],[0,0]],"v":[[37.463,-19.728],[-32.358,-4.737],[-32.358,4.738],[37.463,19.728]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":81,"s":[{"i":[[0,0],[0,0],[-5.105,-1.095],[0,0]],"o":[[0,0],[-5.105,1.095],[0,0],[0,0]],"v":[[37.463,-19.728],[-32.358,7.263],[-32.358,16.738],[37.463,19.728]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":82,"s":[{"i":[[0,0],[0,0],[-5.105,-1.095],[0,0]],"o":[[0,0],[-5.105,1.095],[0,0],[0,0]],"v":[[37.463,-19.728],[-32.358,-16.737],[-32.358,-7.262],[37.463,19.728]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":84,"s":[{"i":[[0,0],[0,0],[-5.105,-1.095],[0,0]],"o":[[0,0],[-5.105,1.095],[0,0],[0,0]],"v":[[37.463,-19.728],[-32.358,-8.737],[-32.358,0.738],[37.463,19.728]],"c":false}]},{"t":91,"s":[{"i":[[0,0],[0,0],[-5.105,-1.095],[0,0]],"o":[[0,0],[-5.105,1.095],[0,0],[0,0]],"v":[[37.463,-19.728],[-32.358,-4.737],[-32.358,4.738],[37.463,19.728]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.8706,0.3216,0.298,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[478.18,491.545],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":57,"op":123,"st":57,"bm":0},{"ddd":0,"ind":9,"ty":4,"nm":"Nose 3","parent":15,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":40,"s":[540,493,0],"to":[3.295,0,0],"ti":[-6.973,0,0]},{"i":{"x":0.667,"y":0.748},"o":{"x":0.333,"y":0},"t":42,"s":[565.005,493,0],"to":[17.375,0,0],"ti":[-8.558,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0.21},"t":48,"s":[610.012,493,0],"to":[1.906,0,0],"ti":[0,0,0]},{"t":49,"s":[619.005,493,0]}],"ix":2},"a":{"a":0,"k":[518,493,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":40,"s":[-18,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":42,"s":[-100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":48,"s":[-18,69,100]},{"t":49,"s":[0,100,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[-5.105,-1.095],[0,0]],"o":[[0,0],[-5.105,1.095],[0,0],[0,0]],"v":[[37.463,-19.728],[-32.358,-4.737],[-32.358,4.738],[37.463,19.728]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.8706,0.3216,0.298,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[478.18,491.545],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":40,"op":121,"st":0,"bm":0},{"ddd":0,"ind":10,"ty":4,"nm":"Nose 2","parent":15,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":38,"s":[518,493,0],"to":[3.667,0,0],"ti":[-3.667,0,0]},{"t":40,"s":[540,493,0]}],"ix":2},"a":{"a":0,"k":[518,493,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":38,"s":[100,100,100]},{"t":40,"s":[18,100,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[-5.105,-1.095],[0,0]],"o":[[0,0],[-5.105,1.095],[0,0],[0,0]],"v":[[37.463,-19.728],[-32.358,-4.737],[-32.358,4.738],[37.463,19.728]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.8706,0.3216,0.298,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[478.18,491.545],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":41,"st":0,"bm":0},{"ddd":0,"ind":11,"ty":4,"nm":"mask 4","td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.011,"y":1},"o":{"x":0.42,"y":0},"t":23,"s":[539,549,0],"to":[-1.08,-21.667,0],"ti":[1.08,21.667,0]},{"i":{"x":0.667,"y":0.667},"o":{"x":0.376,"y":0.376},"t":38,"s":[532.517,419,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.773,"y":0},"t":68,"s":[532.517,419,0],"to":[1.08,21.667,0],"ti":[-1.08,-21.667,0]},{"t":83,"s":[539,549,0]}],"ix":2},"a":{"a":0,"k":[539,549,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-42.938],[42.938,0],[0,42.938],[-42.938,0]],"o":[[0,42.938],[-42.938,0],[0,-42.938],[42.938,0]],"v":[[77.746,0],[0,77.746],[-77.746,0],[0,-77.746]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.9064,0.9136,0.9105,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[538.483,475.681],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":12,"ty":4,"nm":"Nose 5","parent":15,"tt":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":52,"s":[405,493,0],"to":[19.333,0,0],"ti":[-19.333,0,0]},{"t":64,"s":[521,493,0]}],"ix":2},"a":{"a":0,"k":[521,493,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-11.496],[11.496,0],[0,11.496],[-11.496,0]],"o":[[0,11.496],[-11.496,0],[0,-11.496],[11.496,0]],"v":[[20.815,-0.001],[0,20.814],[-20.815,-0.001],[0,-20.814]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.8706,0.3216,0.298,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[521.586,491.545],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":52,"op":302,"st":2,"bm":0},{"ddd":0,"ind":13,"ty":4,"nm":"mask","td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.011,"y":1},"o":{"x":0.42,"y":0},"t":23,"s":[539,549,0],"to":[-1.08,-21.667,0],"ti":[1.08,21.667,0]},{"i":{"x":0.667,"y":0.667},"o":{"x":0.376,"y":0.376},"t":38,"s":[532.517,419,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.773,"y":0},"t":68,"s":[532.517,419,0],"to":[1.08,21.667,0],"ti":[-1.08,-21.667,0]},{"t":83,"s":[539,549,0]}],"ix":2},"a":{"a":0,"k":[539,549,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-42.938],[42.938,0],[0,42.938],[-42.938,0]],"o":[[0,42.938],[-42.938,0],[0,-42.938],[42.938,0]],"v":[[77.746,0],[0,77.746],[-77.746,0],[0,-77.746]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.9064,0.9136,0.9105,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[538.483,475.681],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":14,"ty":4,"nm":"Nose","parent":15,"tt":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":38,"s":[521,493,0],"to":[20,0,0],"ti":[-20,0,0]},{"t":51,"s":[641,493,0]}],"ix":2},"a":{"a":0,"k":[521,493,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-11.496],[11.496,0],[0,11.496],[-11.496,0]],"o":[[0,11.496],[-11.496,0],[0,-11.496],[11.496,0]],"v":[[20.815,-0.001],[0,20.814],[-20.815,-0.001],[0,-20.814]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.8706,0.3216,0.298,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[521.586,491.545],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":51,"st":0,"bm":0},{"ddd":0,"ind":15,"ty":4,"nm":"Head","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.011,"y":1},"o":{"x":0.42,"y":0},"t":23,"s":[539,549,0],"to":[-1.08,-21.667,0],"ti":[1.08,21.667,0]},{"i":{"x":0.667,"y":0.667},"o":{"x":0.376,"y":0.376},"t":38,"s":[532.517,419,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.773,"y":0},"t":68,"s":[532.517,419,0],"to":[1.08,21.667,0],"ti":[-1.08,-21.667,0]},{"t":83,"s":[539,549,0]}],"ix":2},"a":{"a":0,"k":[539,549,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-42.938],[42.938,0],[0,42.938],[-42.938,0]],"o":[[0,42.938],[-42.938,0],[0,-42.938],[42.938,0]],"v":[[77.746,0],[0,77.746],[-77.746,0],[0,-77.746]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[538.483,475.681],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":2,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":300,"st":0,"bm":0}]},{"id":"comp_1","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Hand","parent":3,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.184],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[0]},{"i":{"x":[0.184],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":15,"s":[50]},{"i":{"x":[0.014],"y":[1]},"o":{"x":[0.396],"y":[0]},"t":30,"s":[-15]},{"t":60,"s":[0]}],"ix":10},"p":{"a":0,"k":[605,612,0],"ix":2},"a":{"a":0,"k":[605,612,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[10.12,-10.12],[0,0],[0,0],[0,0],[0,0],[5.023,11.04],[0,0],[-8.43,3.714],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[-7.84,9.256],[0,0],[-3.815,-8.383],[0,0],[0,0]],"v":[[57.14,-29.375],[35.906,-13.45],[57.14,-8.173],[50.236,-1.739],[24.132,-8.127],[-25.652,27],[-53.321,23.162],[-53.325,23.154],[-44.94,1.172],[49.241,-36.256]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.66,0.1573,0.14,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[655.759,580.235],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Body1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[493,626,0],"ix":2},"a":{"a":0,"k":[493,626,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-3.765],[3.764,0],[0,3.764],[-3.764,0]],"o":[[0,3.764],[-3.764,0],[0,-3.765],[3.764,0]],"v":[[6.815,0],[0,6.815],[-6.815,0],[0,-6.815]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[492.44,647.317],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-3.764],[3.763,0],[0,3.764],[-3.765,0]],"o":[[0,3.764],[-3.765,0],[0,-3.764],[3.763,0]],"v":[[6.815,0],[0,6.815],[-6.816,0],[0,-6.815]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[496.13,600.16],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Body","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[532,728,0],"ix":2},"a":{"a":0,"k":[532,728,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[2.956,0,0]},"t":0,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[4.157,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":9.868,"s":[100,90,100]},{"t":19.7373046875,"s":[100,100,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":9.868,"s":[{"i":[[0,-65.229],[65.229,0],[0,65.229],[-65.23,0]],"o":[[0,65.229],[-65.23,0],[0,-65.229],[65.229,0]],"v":[[118.109,0],[0.001,118.108],[-118.108,0],[0.001,-118.108]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":19.737,"s":[{"i":[[0,-65.229],[65.229,0],[0,65.229],[-65.23,0]],"o":[[0,65.229],[-65.23,0],[0,-65.229],[65.229,0]],"v":[[118.109,0],[0.001,118.108],[-118.108,0],[0.001,-140.108]],"c":true}]},{"t":25,"s":[{"i":[[0,-65.229],[65.229,0],[0,65.229],[-65.23,0]],"o":[[0,65.229],[-65.23,0],[0,-65.229],[65.229,0]],"v":[[118.109,0],[0.001,118.108],[-118.108,0],[0.001,-118.108]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[533.054,623.738],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"hand2","parent":3,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.252],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[0]},{"i":{"x":[0.781],"y":[1]},"o":{"x":[0.607],"y":[0]},"t":30,"s":[-25]},{"t":45,"s":[0]}],"ix":10},"p":{"a":0,"k":[452,591,0],"ix":2},"a":{"a":0,"k":[452,591,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[-7.682,-7.683],[0,0],[0,0],[0,0],[0,0],[-3.814,8.38],[0,0],[6.398,2.819],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[5.951,7.025],[0,0],[2.896,-6.364],[0,0],[0,0]],"v":[[-43.372,-22.297],[-27.255,-10.208],[-43.372,-6.203],[-38.132,-1.319],[-18.319,-6.168],[19.47,20.495],[40.473,17.582],[40.477,17.576],[34.111,0.89],[-37.377,-27.52]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.66,0.1573,0.14,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[415.829,571.499],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"Snow","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[543,722,0],"ix":2},"a":{"a":0,"k":[543,722,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[17.222,17.222],[32.212,-32.211],[15.179,-12.635],[-9.343,0],[0,0],[6.893,6.089],[12.612,-1.885],[38.43,-17.362]],"o":[[0,0],[-24.385,24.385],[-7.181,5.977],[0,0],[9.198,0],[-10.072,-8.895],[-50.355,7.531],[-30.492,13.778]],"v":[[-44.402,-2.578],[-116.358,-10.733],[-190.121,25.27],[-183.699,42.944],[183.763,42.944],[190.409,25.466],[154.38,9.87],[72.765,-5.841]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[539.967,707.057],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":300,"st":0,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":0,"nm":"head","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[540,540,0],"to":[0,3.167,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":11,"s":[540,559,0],"to":[0,0,0],"ti":[0,3.167,0]},{"t":16,"s":[540,540,0]}],"ix":2},"a":{"a":0,"k":[540,540,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"w":1080,"h":1080,"ip":-8,"op":1792,"st":-8,"bm":0},{"ddd":0,"ind":2,"ty":0,"nm":"Body","refId":"comp_1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[540,540,0],"ix":2},"a":{"a":0,"k":[540,540,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"w":1080,"h":1080,"ip":0,"op":1800,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/frontend/src/pages/ErrorPage.tsx b/frontend/src/pages/ErrorPage.tsx new file mode 100644 index 00000000..4139eb25 --- /dev/null +++ b/frontend/src/pages/ErrorPage.tsx @@ -0,0 +1,31 @@ +import Lottie from "react-lottie"; +import snowmanAnimate from "../../public/assets/snowman.json"; + +const ErrorPage = () => { + return ( +
+
+

404 Error

+

+ 이런! 요청하신 데이터를 찾을 수 없었어요! +

+
+ +
+
+
+ ); +}; + +export default ErrorPage; diff --git a/frontend/src/routes.tsx b/frontend/src/routes.tsx index 745fd0e0..72dc825c 100644 --- a/frontend/src/routes.tsx +++ b/frontend/src/routes.tsx @@ -2,6 +2,7 @@ import App from "./App.tsx"; import CreateSessionPage from "./pages/CreateSessionPage.tsx"; import SessionListPage from "./pages/SessionListPage.tsx"; import SessionPage from "./pages/SessionPage"; +import ErrorPage from "@/pages/ErrorPage.tsx"; export const routes = [ { @@ -25,7 +26,7 @@ export const routes = [ path: "/sessions/create", }, { - element: <>에러 페이지, + element: , path: "/*", }, ]; From 464b6be787538401c41a3bb71461f665abd62943 Mon Sep 17 00:00:00 2001 From: yiseungyun Date: Sun, 17 Nov 2024 23:50:29 +0900 Subject: [PATCH 004/180] =?UTF-8?q?feat:=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=EB=A5=BC=20=EC=9C=84=ED=95=9C=20Jest=20=EC=84=A4=EC=B9=98=20?= =?UTF-8?q?=EB=B0=8F=20=EA=B8=B0=EB=B3=B8=20=ED=99=98=EA=B2=BD=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/jest.config.ts | 23 ++ frontend/package.json | 11 +- frontend/src/setupTests.ts | 2 + frontend/tsconfig.json | 19 +- frontend/tsconfig.test.json | 6 + pnpm-lock.yaml | 574 ++++++++++++++++++++++++++++++++++++ 6 files changed, 633 insertions(+), 2 deletions(-) create mode 100644 frontend/jest.config.ts create mode 100644 frontend/src/setupTests.ts create mode 100644 frontend/tsconfig.test.json diff --git a/frontend/jest.config.ts b/frontend/jest.config.ts new file mode 100644 index 00000000..02175d0e --- /dev/null +++ b/frontend/jest.config.ts @@ -0,0 +1,23 @@ +import type { Config } from "jest"; + +const config: Config = { + preset: "ts-jest", + testEnvironment: "jsdom", + setupFilesAfterEnv: ["/src/setupTests.ts"], + moduleNameMapper: { + "^@/(.*)$": "/src/$1", + "^@components/(.*)$": "/src/components/$1", + "^@hooks/(.*)$": "/src/hooks/$1", + "^@stores/(.*)$": "/src/stores/$1", + }, + transform: { + "^.+\\.tsx?$": [ + "ts-jest", + { + tsconfig: "tsconfig.test.json", + }, + ], + }, +}; + +export default config; diff --git a/frontend/package.json b/frontend/package.json index 943e72f1..19a580c0 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -9,7 +9,8 @@ "lint": "eslint .", "format": "prettier --write \"**/*.{ts,tsx}\"", "preview": "vite preview", - "test": "echo \"Error: no test specified\" && exit 1" + "test": "jest", + "test:watch": "jest --watch" }, "dependencies": { "@types/socket.io-client": "^3.0.0", @@ -22,6 +23,11 @@ }, "devDependencies": { "@eslint/js": "^9.13.0", + "@testing-library/jest-dom": "^6.6.3", + "@testing-library/react": "^15.0.6", + "@testing-library/react-hooks": "^8.0.1", + "@testing-library/user-event": "^14.5.2", + "@types/jest": "^29.5.14", "@types/node": "^20.3.1", "@types/react": "^18.3.12", "@types/react-dom": "^18.3.1", @@ -31,8 +37,11 @@ "eslint-plugin-react-hooks": "^5.0.0", "eslint-plugin-react-refresh": "^0.4.14", "globals": "^15.11.0", + "jest": "^29.7.0", + "jest-environment-jsdom": "^29.7.0", "postcss": "^8.4.47", "tailwindcss": "^3.4.14", + "ts-jest": "^29.1.0", "typescript": "~5.6.2", "typescript-eslint": "^8.11.0", "vite": "^5.4.10", diff --git a/frontend/src/setupTests.ts b/frontend/src/setupTests.ts new file mode 100644 index 00000000..cb1b1281 --- /dev/null +++ b/frontend/src/setupTests.ts @@ -0,0 +1,2 @@ +import "@testing-library/jest-dom"; +import "jest-environment-jsdom"; diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index 65f670c8..526fcd05 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -7,5 +7,22 @@ { "path": "./tsconfig.node.json" } - ] + ], + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@/*": [ + "src/*" + ], + "@components/*": [ + "src/components/*" + ], + "@hooks/*": [ + "src/hooks/*" + ], + "@stores/*": [ + "src/stores/*" + ] + } + } } \ No newline at end of file diff --git a/frontend/tsconfig.test.json b/frontend/tsconfig.test.json new file mode 100644 index 00000000..d9b1b855 --- /dev/null +++ b/frontend/tsconfig.test.json @@ -0,0 +1,6 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "jsx": "react-jsx" + } +} \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 02443130..4c1e023f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -136,6 +136,21 @@ importers: '@eslint/js': specifier: ^9.13.0 version: 9.13.0 + '@testing-library/jest-dom': + specifier: ^6.6.3 + version: 6.6.3 + '@testing-library/react': + specifier: ^15.0.6 + version: 15.0.6(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@testing-library/react-hooks': + specifier: ^8.0.1 + version: 8.0.1(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@testing-library/user-event': + specifier: ^14.5.2 + version: 14.5.2(@testing-library/dom@10.4.0) + '@types/jest': + specifier: ^29.5.14 + version: 29.5.14 '@types/node': specifier: ^20.3.1 version: 20.17.4 @@ -163,12 +178,21 @@ importers: globals: specifier: ^15.11.0 version: 15.11.0 + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@20.17.4)(ts-node@10.9.2(@types/node@20.17.4)(typescript@5.6.3)) + jest-environment-jsdom: + specifier: ^29.7.0 + version: 29.7.0 postcss: specifier: ^8.4.47 version: 8.4.47 tailwindcss: specifier: ^3.4.14 version: 3.4.14(ts-node@10.9.2(@types/node@20.17.4)(typescript@5.6.3)) + ts-jest: + specifier: ^29.1.0 + version: 29.2.5(@babel/core@7.26.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.26.0))(jest@29.7.0(@types/node@20.17.4)(ts-node@10.9.2(@types/node@20.17.4)(typescript@5.6.3)))(typescript@5.6.3) typescript: specifier: ~5.6.2 version: 5.6.3 @@ -184,6 +208,9 @@ importers: packages: + '@adobe/css-tools@4.4.1': + resolution: {integrity: sha512-12WGKBQzjUAI4ayyF4IAtfw2QR/IDoqk6jTddXDhtYTJF9ASmoE1zst7cVtP0aL/F1jUJL5r+JxKXKEgHNbEUQ==} + '@alloc/quick-lru@5.2.0': resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} @@ -381,6 +408,10 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/runtime@7.26.0': + resolution: {integrity: sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==} + engines: {node: '>=6.9.0'} + '@babel/template@7.25.9': resolution: {integrity: sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==} engines: {node: '>=6.9.0'} @@ -1005,6 +1036,51 @@ packages: '@socket.io/component-emitter@3.1.2': resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==} + '@testing-library/dom@10.4.0': + resolution: {integrity: sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==} + engines: {node: '>=18'} + + '@testing-library/jest-dom@6.6.3': + resolution: {integrity: sha512-IteBhl4XqYNkM54f4ejhLRJiZNqcSCoXUOG2CPK7qbD322KjQozM4kHQOfkG2oln9b9HTYqs+Sae8vBATubxxA==} + engines: {node: '>=14', npm: '>=6', yarn: '>=1'} + + '@testing-library/react-hooks@8.0.1': + resolution: {integrity: sha512-Aqhl2IVmLt8IovEVarNDFuJDVWVvhnr9/GCU6UUnrYXwgDFF9h2L2o2P9KBni1AST5sT6riAyoukFLyjQUgD/g==} + engines: {node: '>=12'} + peerDependencies: + '@types/react': ^16.9.0 || ^17.0.0 + react: ^16.9.0 || ^17.0.0 + react-dom: ^16.9.0 || ^17.0.0 + react-test-renderer: ^16.9.0 || ^17.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + react-dom: + optional: true + react-test-renderer: + optional: true + + '@testing-library/react@15.0.6': + resolution: {integrity: sha512-UlbazRtEpQClFOiYp+1BapMT+xyqWMnE+hh9tn5DQ6gmlE7AIZWcGpzZukmDZuFk3By01oiqOf8lRedLS4k6xQ==} + engines: {node: '>=18'} + peerDependencies: + '@types/react': ^18.0.0 + react: ^18.0.0 + react-dom: ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + '@testing-library/user-event@14.5.2': + resolution: {integrity: sha512-YAh82Wh4TIrxYLmfGcixwD18oIjyC1pFQC2Y01F2lzV2HTMiYrI0nze0FD0ocB//CKS/7jIUgae+adPqxK5yCQ==} + engines: {node: '>=12', npm: '>=6'} + peerDependencies: + '@testing-library/dom': '>=7.21.4' + + '@tootallnate/once@2.0.0': + resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==} + engines: {node: '>= 10'} + '@tsconfig/node10@1.0.11': resolution: {integrity: sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==} @@ -1017,6 +1093,9 @@ packages: '@tsconfig/node16@1.0.4': resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} + '@types/aria-query@5.0.4': + resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} + '@types/babel__core@7.20.5': resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} @@ -1074,6 +1153,9 @@ packages: '@types/jest@29.5.14': resolution: {integrity: sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==} + '@types/jsdom@20.0.1': + resolution: {integrity: sha512-d0r18sZPmMQr1eG35u12FZfhIXNrnsPU/g5wvRKCUf/tOGilKKwYMYGqh33BNR6ba+2gkHw1EUiHoN3mn7E5IQ==} + '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} @@ -1120,6 +1202,9 @@ packages: '@types/supertest@6.0.2': resolution: {integrity: sha512-137ypx2lk/wTQbW6An6safu9hXmajAifU/s7szAHLN/FeIm5w7yR0Wkl9fdJMRSHwOn4HLAI0DaB2TOORuhPDg==} + '@types/tough-cookie@4.0.5': + resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==} + '@types/yargs-parser@21.0.3': resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} @@ -1247,10 +1332,17 @@ packages: resolution: {integrity: sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==} hasBin: true + abab@2.0.6: + resolution: {integrity: sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==} + deprecated: Use your platform's native atob() and btoa() methods instead + accepts@1.3.8: resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} engines: {node: '>= 0.6'} + acorn-globals@7.0.1: + resolution: {integrity: sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==} + acorn-import-attributes@1.9.5: resolution: {integrity: sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==} peerDependencies: @@ -1270,6 +1362,10 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + agent-base@6.0.2: + resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} + engines: {node: '>= 6.0.0'} + ajv-formats@2.1.1: resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} peerDependencies: @@ -1342,6 +1438,13 @@ packages: argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + aria-query@5.3.0: + resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} + + aria-query@5.3.2: + resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} + engines: {node: '>= 0.4'} + array-flatten@1.1.1: resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} @@ -1472,6 +1575,10 @@ packages: caniuse-lite@1.0.30001676: resolution: {integrity: sha512-Qz6zwGCiPghQXGJvgQAem79esjitvJ+CxSbSQkW9H/UX5hg8XM88d4lp2W+MEQ81j+Hip58Il+jGVdazk1z9cw==} + chalk@3.0.0: + resolution: {integrity: sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==} + engines: {node: '>=8'} + chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} @@ -1658,11 +1765,24 @@ packages: resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} engines: {node: '>= 8'} + css.escape@1.5.1: + resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} + cssesc@3.0.0: resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} engines: {node: '>=4'} hasBin: true + cssom@0.3.8: + resolution: {integrity: sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==} + + cssom@0.5.0: + resolution: {integrity: sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==} + + cssstyle@2.3.0: + resolution: {integrity: sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==} + engines: {node: '>=8'} + csstype@3.1.3: resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} @@ -1670,6 +1790,10 @@ packages: resolution: {integrity: sha512-wAV9QHOsNbwnWdNW2FYvE1P56wtgSbM+3SZcdGiWQILwVjACCXDCI3Ai8QlCjMDB8YK5zySiXZYBiwGmNY3lnw==} engines: {node: '>=12'} + data-urls@3.0.2: + resolution: {integrity: sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==} + engines: {node: '>=12'} + debug@2.6.9: resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} peerDependencies: @@ -1687,6 +1811,9 @@ packages: supports-color: optional: true + decimal.js@10.4.3: + resolution: {integrity: sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==} + dedent@1.5.3: resolution: {integrity: sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==} peerDependencies: @@ -1717,6 +1844,10 @@ packages: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + destroy@1.2.0: resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} @@ -1746,6 +1877,17 @@ packages: resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} engines: {node: '>=6.0.0'} + dom-accessibility-api@0.5.16: + resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} + + dom-accessibility-api@0.6.3: + resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==} + + domexception@4.0.0: + resolution: {integrity: sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==} + engines: {node: '>=12'} + deprecated: Use your platform's native DOMException instead + dot-prop@5.3.0: resolution: {integrity: sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==} engines: {node: '>=8'} @@ -1797,6 +1939,10 @@ packages: resolution: {integrity: sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==} engines: {node: '>=10.13.0'} + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + env-paths@2.2.1: resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} engines: {node: '>=6'} @@ -1839,6 +1985,11 @@ packages: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} + escodegen@2.1.0: + resolution: {integrity: sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==} + engines: {node: '>=6.0'} + hasBin: true + eslint-config-prettier@9.1.0: resolution: {integrity: sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==} hasBin: true @@ -2193,6 +2344,10 @@ packages: resolution: {integrity: sha512-qlspKUK7IlSQv2o+5I7yhUd7TxlOG2Vr5LTa3ve2XSNVKAL/n/u/7KLvKmFNimomDIKvZFXWHv0T12mv7rT8Aw==} engines: {node: '>=8'} + html-encoding-sniffer@3.0.0: + resolution: {integrity: sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==} + engines: {node: '>=12'} + html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} @@ -2200,6 +2355,14 @@ packages: resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} engines: {node: '>= 0.8'} + http-proxy-agent@5.0.0: + resolution: {integrity: sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==} + engines: {node: '>= 6'} + + https-proxy-agent@5.0.1: + resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} + engines: {node: '>= 6'} + human-signals@2.1.0: resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} engines: {node: '>=10.17.0'} @@ -2213,6 +2376,10 @@ packages: resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} engines: {node: '>=0.10.0'} + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} @@ -2236,6 +2403,10 @@ packages: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} + indent-string@4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} + inflight@1.0.6: resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. @@ -2302,6 +2473,9 @@ packages: resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} engines: {node: '>=8'} + is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + is-stream@2.0.1: resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} engines: {node: '>=8'} @@ -2398,6 +2572,15 @@ packages: resolution: {integrity: sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-environment-jsdom@29.7.0: + resolution: {integrity: sha512-k9iQbsf9OyOfdzWH8HDmrRT0gSIcX+FLNW7IQq94tFX0gynPwqDTW0Ho6iMVNjGz/nb+l/vW3dWM2bbLLpkbXA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + canvas: ^2.5.0 + peerDependenciesMeta: + canvas: + optional: true + jest-environment-node@29.7.0: resolution: {integrity: sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -2504,6 +2687,15 @@ packages: resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} hasBin: true + jsdom@20.0.3: + resolution: {integrity: sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ==} + engines: {node: '>=14'} + peerDependencies: + canvas: ^2.5.0 + peerDependenciesMeta: + canvas: + optional: true + jsesc@3.0.2: resolution: {integrity: sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==} engines: {node: '>=6'} @@ -2631,6 +2823,10 @@ packages: lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + lz-string@1.5.0: + resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} + hasBin: true + magic-string@0.30.8: resolution: {integrity: sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==} engines: {node: '>=12'} @@ -2697,6 +2893,10 @@ packages: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} engines: {node: '>=6'} + min-indent@1.0.1: + resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} + engines: {node: '>=4'} + minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} @@ -2787,6 +2987,9 @@ packages: resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} engines: {node: '>=8'} + nwsapi@2.2.13: + resolution: {integrity: sha512-cTGB9ptp9dY9A5VbMSe7fQBcl/tt22Vcqdq8+eN93rblOuE0aCFu4aZ2vMwct/2t+lFnosm8RkQW1I0Omb1UtQ==} + object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -2861,6 +3064,9 @@ packages: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} + parse5@7.2.1: + resolution: {integrity: sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==} + parseurl@1.3.3: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} @@ -2979,6 +3185,10 @@ packages: engines: {node: '>=14'} hasBin: true + pretty-format@27.5.1: + resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + pretty-format@29.7.0: resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -2994,6 +3204,9 @@ packages: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} + psl@1.10.0: + resolution: {integrity: sha512-KSKHEbjAnpUuAUserOq0FxGXCUrzC3WniuSJhvdbs102rL55266ZcHBqLWOsG30spQMlPdpy7icATiAQehg/iA==} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -3005,6 +3218,9 @@ packages: resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==} engines: {node: '>=0.6'} + querystringify@2.2.0: + resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} + queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} @@ -3024,11 +3240,20 @@ packages: peerDependencies: react: ^18.3.1 + react-error-boundary@3.1.4: + resolution: {integrity: sha512-uM9uPzZJTF6wRQORmSrvOIgt4lJ9MC1sNgEOj2XGsDTRE4kmpWxg7ENK9EWNKJRMAOY9z0MuF4yIfl6gp4sotA==} + engines: {node: '>=10', npm: '>=6'} + peerDependencies: + react: '>=16.13.1' + react-icons@5.3.0: resolution: {integrity: sha512-DnUk8aFbTyQPSkCfF8dbX6kQjXA9DktMeJqfjrg6cK9vwQVMxmcA3BfP4QoiztVmEHtwlTgLFsPuH2NskKT6eg==} peerDependencies: react: '*' + react-is@17.0.2: + resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + react-is@18.3.1: resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} @@ -3067,9 +3292,16 @@ packages: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} + redent@3.0.0: + resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} + engines: {node: '>=8'} + reflect-metadata@0.2.2: resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==} + regenerator-runtime@0.14.1: + resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} + repeat-string@1.6.1: resolution: {integrity: sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==} engines: {node: '>=0.10'} @@ -3082,6 +3314,9 @@ packages: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} + requires-port@1.0.0: + resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} + resolve-cwd@3.0.0: resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==} engines: {node: '>=8'} @@ -3143,6 +3378,10 @@ packages: safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + scheduler@0.23.2: resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} @@ -3297,6 +3536,10 @@ packages: resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} engines: {node: '>=6'} + strip-indent@3.0.0: + resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} + engines: {node: '>=8'} + strip-json-comments@3.1.1: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} @@ -3330,6 +3573,9 @@ packages: resolution: {integrity: sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==} engines: {node: '>=0.10'} + symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + synckit@0.9.2: resolution: {integrity: sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw==} engines: {node: ^14.18.0 || >=16.0.0} @@ -3403,9 +3649,17 @@ packages: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} + tough-cookie@4.1.4: + resolution: {integrity: sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==} + engines: {node: '>=6'} + tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + tr46@3.0.0: + resolution: {integrity: sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==} + engines: {node: '>=12'} + tree-kill@1.2.2: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true @@ -3531,6 +3785,10 @@ packages: resolution: {integrity: sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==} engines: {node: '>=18'} + universalify@0.2.0: + resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} + engines: {node: '>= 4.0.0'} + universalify@2.0.1: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} @@ -3548,6 +3806,9 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + url-parse@1.5.10: + resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} + util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} @@ -3600,6 +3861,10 @@ packages: terser: optional: true + w3c-xmlserializer@4.0.0: + resolution: {integrity: sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==} + engines: {node: '>=14'} + walker@1.0.8: resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} @@ -3613,6 +3878,10 @@ packages: webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + webidl-conversions@7.0.0: + resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} + engines: {node: '>=12'} + webpack-node-externals@3.0.0: resolution: {integrity: sha512-LnL6Z3GGDPht/AigwRh2dvL9PQPFQ8skEpVrWZXLWBYmqcaojHNN0onvHzie6rq7EWKrrBfPYqNEzTJgiwEQDQ==} engines: {node: '>=6'} @@ -3631,6 +3900,18 @@ packages: webpack-cli: optional: true + whatwg-encoding@2.0.0: + resolution: {integrity: sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==} + engines: {node: '>=12'} + + whatwg-mimetype@3.0.0: + resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==} + engines: {node: '>=12'} + + whatwg-url@11.0.0: + resolution: {integrity: sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==} + engines: {node: '>=12'} + whatwg-url@5.0.0: resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} @@ -3674,6 +3955,13 @@ packages: utf-8-validate: optional: true + xml-name-validator@4.0.0: + resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==} + engines: {node: '>=12'} + + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + xmlhttprequest-ssl@2.1.2: resolution: {integrity: sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==} engines: {node: '>=0.4.0'} @@ -3734,6 +4022,8 @@ packages: snapshots: + '@adobe/css-tools@4.4.1': {} + '@alloc/quick-lru@5.2.0': {} '@ampproject/remapping@2.3.0': @@ -3966,6 +4256,10 @@ snapshots: '@babel/core': 7.26.0 '@babel/helper-plugin-utils': 7.25.9 + '@babel/runtime@7.26.0': + dependencies: + regenerator-runtime: 0.14.1 + '@babel/template@7.25.9': dependencies: '@babel/code-frame': 7.26.2 @@ -4682,6 +4976,52 @@ snapshots: '@socket.io/component-emitter@3.1.2': {} + '@testing-library/dom@10.4.0': + dependencies: + '@babel/code-frame': 7.26.2 + '@babel/runtime': 7.26.0 + '@types/aria-query': 5.0.4 + aria-query: 5.3.0 + chalk: 4.1.2 + dom-accessibility-api: 0.5.16 + lz-string: 1.5.0 + pretty-format: 27.5.1 + + '@testing-library/jest-dom@6.6.3': + dependencies: + '@adobe/css-tools': 4.4.1 + aria-query: 5.3.2 + chalk: 3.0.0 + css.escape: 1.5.1 + dom-accessibility-api: 0.6.3 + lodash: 4.17.21 + redent: 3.0.0 + + '@testing-library/react-hooks@8.0.1(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@babel/runtime': 7.26.0 + react: 18.3.1 + react-error-boundary: 3.1.4(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.12 + react-dom: 18.3.1(react@18.3.1) + + '@testing-library/react@15.0.6(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@babel/runtime': 7.26.0 + '@testing-library/dom': 10.4.0 + '@types/react-dom': 18.3.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.12 + + '@testing-library/user-event@14.5.2(@testing-library/dom@10.4.0)': + dependencies: + '@testing-library/dom': 10.4.0 + + '@tootallnate/once@2.0.0': {} + '@tsconfig/node10@1.0.11': {} '@tsconfig/node12@1.0.11': {} @@ -4690,6 +5030,8 @@ snapshots: '@tsconfig/node16@1.0.4': {} + '@types/aria-query@5.0.4': {} + '@types/babel__core@7.20.5': dependencies: '@babel/parser': 7.26.2 @@ -4769,6 +5111,12 @@ snapshots: expect: 29.7.0 pretty-format: 29.7.0 + '@types/jsdom@20.0.1': + dependencies: + '@types/node': 20.17.4 + '@types/tough-cookie': 4.0.5 + parse5: 7.2.1 + '@types/json-schema@7.0.15': {} '@types/methods@1.1.4': {} @@ -4827,6 +5175,8 @@ snapshots: '@types/methods': 1.1.4 '@types/superagent': 8.1.9 + '@types/tough-cookie@4.0.5': {} + '@types/yargs-parser@21.0.3': {} '@types/yargs@17.0.33': @@ -5066,11 +5416,18 @@ snapshots: jsonparse: 1.3.1 through: 2.3.8 + abab@2.0.6: {} + accepts@1.3.8: dependencies: mime-types: 2.1.35 negotiator: 0.6.3 + acorn-globals@7.0.1: + dependencies: + acorn: 8.14.0 + acorn-walk: 8.3.4 + acorn-import-attributes@1.9.5(acorn@8.14.0): dependencies: acorn: 8.14.0 @@ -5085,6 +5442,12 @@ snapshots: acorn@8.14.0: {} + agent-base@6.0.2: + dependencies: + debug: 4.3.7 + transitivePeerDependencies: + - supports-color + ajv-formats@2.1.1(ajv@8.12.0): optionalDependencies: ajv: 8.12.0 @@ -5151,6 +5514,12 @@ snapshots: argparse@2.0.1: {} + aria-query@5.3.0: + dependencies: + dequal: 2.0.3 + + aria-query@5.3.2: {} + array-flatten@1.1.1: {} array-ify@1.0.0: {} @@ -5318,6 +5687,11 @@ snapshots: caniuse-lite@1.0.30001676: {} + chalk@3.0.0: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + chalk@4.1.2: dependencies: ansi-styles: 4.3.0 @@ -5501,12 +5875,28 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + css.escape@1.5.1: {} + cssesc@3.0.0: {} + cssom@0.3.8: {} + + cssom@0.5.0: {} + + cssstyle@2.3.0: + dependencies: + cssom: 0.3.8 + csstype@3.1.3: {} dargs@8.1.0: {} + data-urls@3.0.2: + dependencies: + abab: 2.0.6 + whatwg-mimetype: 3.0.0 + whatwg-url: 11.0.0 + debug@2.6.9: dependencies: ms: 2.0.0 @@ -5515,6 +5905,8 @@ snapshots: dependencies: ms: 2.1.3 + decimal.js@10.4.3: {} + dedent@1.5.3: {} deep-is@0.1.4: {} @@ -5535,6 +5927,8 @@ snapshots: depd@2.0.0: {} + dequal@2.0.3: {} + destroy@1.2.0: {} detect-newline@3.1.0: {} @@ -5556,6 +5950,14 @@ snapshots: dependencies: esutils: 2.0.3 + dom-accessibility-api@0.5.16: {} + + dom-accessibility-api@0.6.3: {} + + domexception@4.0.0: + dependencies: + webidl-conversions: 7.0.0 + dot-prop@5.3.0: dependencies: is-obj: 2.0.0 @@ -5616,6 +6018,8 @@ snapshots: graceful-fs: 4.2.11 tapable: 2.2.1 + entities@4.5.0: {} + env-paths@2.2.1: {} error-ex@1.3.2: @@ -5666,6 +6070,14 @@ snapshots: escape-string-regexp@4.0.0: {} + escodegen@2.1.0: + dependencies: + esprima: 4.0.1 + estraverse: 5.3.0 + esutils: 2.0.3 + optionalDependencies: + source-map: 0.6.1 + eslint-config-prettier@9.1.0(eslint@8.57.1): dependencies: eslint: 8.57.1 @@ -6130,6 +6542,10 @@ snapshots: hexoid@2.0.0: {} + html-encoding-sniffer@3.0.0: + dependencies: + whatwg-encoding: 2.0.0 + html-escaper@2.0.2: {} http-errors@2.0.0: @@ -6140,6 +6556,21 @@ snapshots: statuses: 2.0.1 toidentifier: 1.0.1 + http-proxy-agent@5.0.0: + dependencies: + '@tootallnate/once': 2.0.0 + agent-base: 6.0.2 + debug: 4.3.7 + transitivePeerDependencies: + - supports-color + + https-proxy-agent@5.0.1: + dependencies: + agent-base: 6.0.2 + debug: 4.3.7 + transitivePeerDependencies: + - supports-color + human-signals@2.1.0: {} husky@9.1.6: {} @@ -6148,6 +6579,10 @@ snapshots: dependencies: safer-buffer: 2.1.2 + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + ieee754@1.2.1: {} ignore@5.3.2: {} @@ -6166,6 +6601,8 @@ snapshots: imurmurhash@0.1.4: {} + indent-string@4.0.0: {} + inflight@1.0.6: dependencies: once: 1.4.0 @@ -6241,6 +6678,8 @@ snapshots: is-path-inside@3.0.3: {} + is-potential-custom-element-name@1.0.1: {} + is-stream@2.0.1: {} is-text-path@2.0.0: @@ -6410,6 +6849,21 @@ snapshots: jest-util: 29.7.0 pretty-format: 29.7.0 + jest-environment-jsdom@29.7.0: + dependencies: + '@jest/environment': 29.7.0 + '@jest/fake-timers': 29.7.0 + '@jest/types': 29.6.3 + '@types/jsdom': 20.0.1 + '@types/node': 20.17.4 + jest-mock: 29.7.0 + jest-util: 29.7.0 + jsdom: 20.0.3 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + jest-environment-node@29.7.0: dependencies: '@jest/environment': 29.7.0 @@ -6637,6 +7091,39 @@ snapshots: dependencies: argparse: 2.0.1 + jsdom@20.0.3: + dependencies: + abab: 2.0.6 + acorn: 8.14.0 + acorn-globals: 7.0.1 + cssom: 0.5.0 + cssstyle: 2.3.0 + data-urls: 3.0.2 + decimal.js: 10.4.3 + domexception: 4.0.0 + escodegen: 2.1.0 + form-data: 4.0.1 + html-encoding-sniffer: 3.0.0 + http-proxy-agent: 5.0.0 + https-proxy-agent: 5.0.1 + is-potential-custom-element-name: 1.0.1 + nwsapi: 2.2.13 + parse5: 7.2.1 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 4.1.4 + w3c-xmlserializer: 4.0.0 + webidl-conversions: 7.0.0 + whatwg-encoding: 2.0.0 + whatwg-mimetype: 3.0.0 + whatwg-url: 11.0.0 + ws: 8.17.1 + xml-name-validator: 4.0.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + jsesc@3.0.2: {} json-buffer@3.0.1: {} @@ -6733,6 +7220,8 @@ snapshots: dependencies: yallist: 3.1.1 + lz-string@1.5.0: {} + magic-string@0.30.8: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 @@ -6780,6 +7269,8 @@ snapshots: mimic-fn@2.1.0: {} + min-indent@1.0.1: {} + minimatch@3.1.2: dependencies: brace-expansion: 1.1.11 @@ -6854,6 +7345,8 @@ snapshots: dependencies: path-key: 3.1.1 + nwsapi@2.2.13: {} + object-assign@4.1.1: {} object-hash@3.0.0: {} @@ -6934,6 +7427,10 @@ snapshots: json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 + parse5@7.2.1: + dependencies: + entities: 4.5.0 + parseurl@1.3.3: {} path-exists@4.0.0: {} @@ -7019,6 +7516,12 @@ snapshots: prettier@3.3.3: {} + pretty-format@27.5.1: + dependencies: + ansi-regex: 5.0.1 + ansi-styles: 5.2.0 + react-is: 17.0.2 + pretty-format@29.7.0: dependencies: '@jest/schemas': 29.6.3 @@ -7037,6 +7540,10 @@ snapshots: forwarded: 0.2.0 ipaddr.js: 1.9.1 + psl@1.10.0: + dependencies: + punycode: 2.3.1 + punycode@2.3.1: {} pure-rand@6.1.0: {} @@ -7045,6 +7552,8 @@ snapshots: dependencies: side-channel: 1.0.6 + querystringify@2.2.0: {} + queue-microtask@1.2.3: {} randombytes@2.1.0: @@ -7066,10 +7575,17 @@ snapshots: react: 18.3.1 scheduler: 0.23.2 + react-error-boundary@3.1.4(react@18.3.1): + dependencies: + '@babel/runtime': 7.26.0 + react: 18.3.1 + react-icons@5.3.0(react@18.3.1): dependencies: react: 18.3.1 + react-is@17.0.2: {} + react-is@18.3.1: {} react-refresh@0.14.2: {} @@ -7114,14 +7630,23 @@ snapshots: dependencies: picomatch: 2.3.1 + redent@3.0.0: + dependencies: + indent-string: 4.0.0 + strip-indent: 3.0.0 + reflect-metadata@0.2.2: {} + regenerator-runtime@0.14.1: {} + repeat-string@1.6.1: {} require-directory@2.1.1: {} require-from-string@2.0.2: {} + requires-port@1.0.0: {} + resolve-cwd@3.0.0: dependencies: resolve-from: 5.0.0 @@ -7191,6 +7716,10 @@ snapshots: safer-buffer@2.1.2: {} + saxes@6.0.0: + dependencies: + xmlchars: 2.2.0 + scheduler@0.23.2: dependencies: loose-envify: 1.4.0 @@ -7390,6 +7919,10 @@ snapshots: strip-final-newline@2.0.0: {} + strip-indent@3.0.0: + dependencies: + min-indent: 1.0.1 + strip-json-comments@3.1.1: {} sucrase@3.35.0: @@ -7435,6 +7968,8 @@ snapshots: symbol-observable@4.0.0: {} + symbol-tree@3.2.4: {} + synckit@0.9.2: dependencies: '@pkgr/core': 0.1.1 @@ -7519,8 +8054,19 @@ snapshots: toidentifier@1.0.1: {} + tough-cookie@4.1.4: + dependencies: + psl: 1.10.0 + punycode: 2.3.1 + universalify: 0.2.0 + url-parse: 1.5.10 + tr46@0.0.3: {} + tr46@3.0.0: + dependencies: + punycode: 2.3.1 + tree-kill@1.2.2: {} ts-api-utils@1.4.0(typescript@5.6.3): @@ -7632,6 +8178,8 @@ snapshots: unicorn-magic@0.1.0: {} + universalify@0.2.0: {} + universalify@2.0.1: {} unpipe@1.0.0: {} @@ -7646,6 +8194,11 @@ snapshots: dependencies: punycode: 2.3.1 + url-parse@1.5.10: + dependencies: + querystringify: 2.2.0 + requires-port: 1.0.0 + util-deprecate@1.0.2: {} utils-merge@1.0.1: {} @@ -7672,6 +8225,10 @@ snapshots: fsevents: 2.3.3 terser: 5.36.0 + w3c-xmlserializer@4.0.0: + dependencies: + xml-name-validator: 4.0.0 + walker@1.0.8: dependencies: makeerror: 1.0.12 @@ -7687,6 +8244,8 @@ snapshots: webidl-conversions@3.0.1: {} + webidl-conversions@7.0.0: {} + webpack-node-externals@3.0.0: {} webpack-sources@3.2.3: {} @@ -7721,6 +8280,17 @@ snapshots: - esbuild - uglify-js + whatwg-encoding@2.0.0: + dependencies: + iconv-lite: 0.6.3 + + whatwg-mimetype@3.0.0: {} + + whatwg-url@11.0.0: + dependencies: + tr46: 3.0.0 + webidl-conversions: 7.0.0 + whatwg-url@5.0.0: dependencies: tr46: 0.0.3 @@ -7759,6 +8329,10 @@ snapshots: ws@8.17.1: {} + xml-name-validator@4.0.0: {} + + xmlchars@2.2.0: {} + xmlhttprequest-ssl@2.1.2: {} xtend@4.0.2: {} From a77cf584d08db34c1e0b59e631d0321a299f48f0 Mon Sep 17 00:00:00 2001 From: yiseungyun Date: Sun, 17 Nov 2024 23:55:05 +0900 Subject: [PATCH 005/180] =?UTF-8?q?refactor:=20=EC=84=B8=EC=85=98=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20useSession=20=ED=9B=85=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/hooks/useSession.ts | 254 +++++++++++++++++++++++++++++ frontend/src/pages/SessionPage.tsx | 251 ++-------------------------- 2 files changed, 266 insertions(+), 239 deletions(-) create mode 100644 frontend/src/hooks/useSession.ts diff --git a/frontend/src/hooks/useSession.ts b/frontend/src/hooks/useSession.ts new file mode 100644 index 00000000..ad94a513 --- /dev/null +++ b/frontend/src/hooks/useSession.ts @@ -0,0 +1,254 @@ +import { useCallback, useEffect, useRef, useState } from "react"; +import { useNavigate } from "react-router-dom"; +import useToast from "@/hooks/useToast"; +import useMediaDevices from "@/hooks/useMediaDevices"; +import usePeerConnection from "@/hooks/usePeerConnection"; +import useSocketStore from "@/stores/useSocketStore"; + +interface User { + id: string; + nickname: string; +} + +export const useSession = (sessionId: string | undefined) => { + const { socket, connect } = useSocketStore(); + const navigate = useNavigate(); + const toast = useToast(); + + const { + createPeerConnection, + closePeerConnection, + peers, + setPeers, + peerConnections, + } = usePeerConnection(socket!); + + const [nickname, setNickname] = useState(""); + const [reaction, setReaction] = useState(""); + const reactionTimeouts = useRef<{ + [key: string]: ReturnType; + }>({}); + + const { + userVideoDevices, + userAudioDevices, + selectedAudioDeviceId, + selectedVideoDeviceId, + stream, + isVideoOn, + isMicOn, + handleMicToggle, + handleVideoToggle, + setSelectedAudioDeviceId, + setSelectedVideoDeviceId, + getMedia, + } = useMediaDevices(); + + useEffect(() => { + if (!socket) connect(import.meta.env.VITE_SIGNALING_SERVER_URL); + const connections = peerConnections; + + return () => { + Object.values(connections.current).forEach((pc) => { + pc.ontrack = null; + pc.onicecandidate = null; + pc.oniceconnectionstatechange = null; + pc.onconnectionstatechange = null; + pc.close(); + }); + }; + }, []); + + const setupSocketListeners = useCallback(() => { + if (!socket || !stream) return; + + const handleAllUsers = (users: User[]) => { + Object.entries(users).forEach(([socketId, userInfo]) => { + createPeerConnection(socketId, userInfo.nickname, stream, true, { + nickname, + }); + }); + }; + + const handleGetOffer = async (data: { + sdp: RTCSessionDescription; + offerSendID: string; + offerSendNickname: string; + }) => { + const pc = createPeerConnection( + data.offerSendID, + data.offerSendNickname, + stream, + false, + { nickname } + ); + if (!pc) return; + + try { + await pc.setRemoteDescription(new RTCSessionDescription(data.sdp)); + const answer = await pc.createAnswer(); + await pc.setLocalDescription(answer); + + socket.emit("answer", { + answerReceiveID: data.offerSendID, + sdp: answer, + answerSendID: socket.id, + }); + } catch (error) { + console.error("Error handling offer:", error); + } + }; + + const handleGetAnswer = async (data: { + sdp: RTCSessionDescription; + answerSendID: string; + }) => { + const pc = peerConnections.current[data.answerSendID]; + if (!pc) return; + try { + await pc.setRemoteDescription(new RTCSessionDescription(data.sdp)); + } catch (error) { + console.error("Error handling answer:", error); + } + }; + + const handleGetCandidate = async (data: { + candidate: RTCIceCandidate; + candidateSendID: string; + }) => { + const pc = peerConnections.current[data.candidateSendID]; + if (!pc) return; + try { + await pc.addIceCandidate(new RTCIceCandidate(data.candidate)); + } catch (error) { + console.error("Error handling ICE candidate:", error); + } + }; + + const handleReaction = ({ + senderId, + reaction, + }: { + senderId: string; + reaction: string; + }) => { + if (reactionTimeouts.current[senderId]) { + clearTimeout(reactionTimeouts.current[senderId]); + } + + if (senderId === socket.id) { + setReaction(reaction); + reactionTimeouts.current[senderId] = setTimeout(() => { + setReaction(""); + delete reactionTimeouts.current[senderId]; + }, 3000); + } else { + addReaction(senderId, reaction); + reactionTimeouts.current[senderId] = setTimeout(() => { + addReaction(senderId, ""); + delete reactionTimeouts.current[senderId]; + }, 3000); + } + }; + + socket.on("all_users", handleAllUsers); + socket.on("getOffer", handleGetOffer); + socket.on("getAnswer", handleGetAnswer); + socket.on("getCandidate", handleGetCandidate); + socket.on("user_exit", ({ id }) => closePeerConnection(id)); + socket.on("room_full", () => { + toast.error("해당 세션은 이미 유저가 가득 찼습니다."); + navigate("/sessions"); + }); + socket.on("reaction", handleReaction); + + return () => { + socket.off("all_users", handleAllUsers); + socket.off("getOffer", handleGetOffer); + socket.off("getAnswer", handleGetAnswer); + socket.off("getCandidate", handleGetCandidate); + socket.off("user_exit"); + socket.off("room_full"); + socket.off("reaction", handleReaction); + + if (reactionTimeouts.current) { + Object.values(reactionTimeouts.current).forEach(clearTimeout); + } + }; + }, [socket, stream, nickname, createPeerConnection, closePeerConnection]); + + useEffect(() => { + const cleanup = setupSocketListeners(); + return () => cleanup?.(); + }, [setupSocketListeners]); + + useEffect(() => { + if (selectedAudioDeviceId || selectedVideoDeviceId) { + getMedia(); + } + }, [selectedAudioDeviceId, selectedVideoDeviceId]); + + useEffect(() => { + return () => { + if (stream) { + stream.getTracks().forEach((track) => track.stop()); + } + }; + }, [stream]); + + const joinRoom = async () => { + if (!socket || !sessionId || !nickname) { + toast.error("닉네임을 입력해주세요."); + return; + } + + const mediaStream = await getMedia(); + if (!mediaStream) { + toast.error( + "미디어 스트림을 가져오지 못했습니다. 미디어 장치를 확인 후 다시 시도해주세요." + ); + navigate("/sessions"); + return; + } + + socket.emit("join_room", { roomId: sessionId, nickname }); + }; + + const emitReaction = (reactionType: string) => { + if (socket) { + socket.emit("reaction", { + roomId: sessionId, + reaction: reactionType, + }); + } + }; + + const addReaction = useCallback( + (senderId: string, reactionType: string) => { + setPeers((prev) => + prev.map((peer) => + peer.peerId === senderId ? { ...peer, reaction: reactionType } : peer + ) + ); + }, + [setPeers] + ); + + return { + nickname, + setNickname, + reaction, + peers, + userVideoDevices, + userAudioDevices, + isVideoOn, + isMicOn, + stream, + handleMicToggle, + handleVideoToggle, + setSelectedAudioDeviceId, + setSelectedVideoDeviceId, + joinRoom, + emitReaction, + }; +}; diff --git a/frontend/src/pages/SessionPage.tsx b/frontend/src/pages/SessionPage.tsx index 67b5ee4b..a22f649f 100644 --- a/frontend/src/pages/SessionPage.tsx +++ b/frontend/src/pages/SessionPage.tsx @@ -1,260 +1,33 @@ -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { useMemo } from "react"; import VideoContainer from "@/components/session/VideoContainer.tsx"; -import { useNavigate, useParams } from "react-router-dom"; +import { useParams } from "react-router-dom"; import SessionSidebar from "@/components/session/SessionSidebar.tsx"; import SessionToolbar from "@/components/session/SessionToolbar.tsx"; -import useMediaDevices from "@/hooks/useMediaDevices.ts"; -import useToast from "@/hooks/useToast.ts"; -import usePeerConnection from "@/hooks/usePeerConnection.ts"; import useSocketStore from "@/stores/useSocketStore"; import useSessionFormStore from "@/stores/useSessionFormStore"; - -interface User { - id: string; - nickname: string; -} +import { useSession } from "@/hooks/useSession"; const SessionPage = () => { - const { socket, connect } = useSocketStore(); + const { socket } = useSocketStore(); const { sessionName } = useSessionFormStore(); - - const { - createPeerConnection, - closePeerConnection, - peers, - setPeers, - peerConnections, - } = usePeerConnection(socket!); const { sessionId } = useParams(); - const [nickname, setNickname] = useState(""); - const [reaction, setReaction] = useState(""); - const { + nickname, + setNickname, + reaction, + peers, userVideoDevices, userAudioDevices, - selectedAudioDeviceId, - selectedVideoDeviceId, - stream, isVideoOn, isMicOn, + stream, handleMicToggle, handleVideoToggle, setSelectedAudioDeviceId, setSelectedVideoDeviceId, - getMedia, - } = useMediaDevices(); - - const reactionTimeouts = useRef<{ - [key: string]: ReturnType; - }>({}); - const navigate = useNavigate(); - const toast = useToast(); - - useEffect(() => { - if (!socket) connect(import.meta.env.VITE_SIGNALING_SERVER_URL); - const connections = peerConnections; - - return () => { - Object.values(connections.current).forEach((pc) => { - pc.ontrack = null; - pc.onicecandidate = null; - pc.oniceconnectionstatechange = null; - pc.onconnectionstatechange = null; - pc.close(); - }); - }; - }, []); // eslint-disable-line react-hooks/exhaustive-deps - - useEffect(() => { - return () => { - if (stream) { - stream.getTracks().forEach((track) => track.stop()); - } - }; - }, [stream]); - - useEffect(() => { - if (selectedAudioDeviceId || selectedVideoDeviceId) { - getMedia(); - } - }, [selectedAudioDeviceId, selectedVideoDeviceId]); - - useEffect(() => { - if (!socket || !stream) return; - - console.log("Setting up socket event listeners"); - - const handleAllUsers = (users: User[]) => { - console.log("Received all_users:", users); - Object.entries(users).forEach(([socketId, userInfo]) => { - console.log("Creating peer connection for:", { - socketId, - nickname: userInfo.nickname, - }); - - createPeerConnection(socketId, userInfo.nickname, stream, true, { - nickname, - }); - }); - }; - - const handleGetOffer = async (data: { - sdp: RTCSessionDescription; - offerSendID: string; - offerSendNickname: string; - }) => { - console.log("Received offer from:", data.offerSendID); - const pc = createPeerConnection( - data.offerSendID, - data.offerSendNickname, - stream, - false, - { nickname } - ); - if (!pc) return; - - try { - await pc.setRemoteDescription(new RTCSessionDescription(data.sdp)); - const answer = await pc.createAnswer(); - await pc.setLocalDescription(answer); - - socket.emit("answer", { - answerReceiveID: data.offerSendID, - sdp: answer, - answerSendID: socket.id, - }); - } catch (error) { - console.error("Error handling offer:", error); - } - }; - - const handleGetAnswer = async (data: { - sdp: RTCSessionDescription; - answerSendID: string; - }) => { - console.log("Received answer from:", data.answerSendID); - const pc = peerConnections.current[data.answerSendID]; - if (!pc) return; - try { - await pc.setRemoteDescription(new RTCSessionDescription(data.sdp)); - } catch (error) { - console.error("Error handling answer:", error); - } - }; - - const handleGetCandidate = async (data: { - candidate: RTCIceCandidate; - candidateSendID: string; - }) => { - const pc = peerConnections.current[data.candidateSendID]; - if (!pc) return; - try { - await pc.addIceCandidate(new RTCIceCandidate(data.candidate)); - } catch (error) { - console.error("Error handling ICE candidate:", error); - } - }; - - const handleReaction = ({ - senderId, - reaction, - }: { - senderId: string; - reaction: string; - }) => { - if (reactionTimeouts.current[senderId]) { - clearTimeout(reactionTimeouts.current[senderId]); - } - - if (senderId === socket.id) { - setReaction(reaction); - reactionTimeouts.current[senderId] = setTimeout(() => { - setReaction(""); - delete reactionTimeouts.current[senderId]; - }, 3000); - } else { - addReaction(senderId, reaction); - reactionTimeouts.current[senderId] = setTimeout(() => { - addReaction(senderId, ""); - delete reactionTimeouts.current[senderId]; - }, 3000); - } - }; - - socket.on("all_users", handleAllUsers); - socket.on("getOffer", handleGetOffer); - socket.on("getAnswer", handleGetAnswer); - socket.on("getCandidate", handleGetCandidate); - socket.on("user_exit", ({ id }) => closePeerConnection(id)); - socket.on("room_full", () => { - toast.error("해당 세션은 이미 유저가 가득 찼습니다."); - navigate("/sessions"); - }); - socket.on("reaction", handleReaction); - - return () => { - console.log("Cleaning up socket event listeners"); - socket.off("all_users", handleAllUsers); - socket.off("getOffer", handleGetOffer); - socket.off("getAnswer", handleGetAnswer); - socket.off("getCandidate", handleGetCandidate); - socket.off("user_exit"); - socket.off("room_full"); - socket.off("reaction", handleReaction); - - if (reactionTimeouts.current) { - Object.values(reactionTimeouts.current).forEach((timeout) => { - clearTimeout(timeout); - }); - } - }; - }, [ - socket, - stream, - nickname, - createPeerConnection, - closePeerConnection, - peerConnections, - navigate, - toast, - ]); - - const emitReaction = (reactionType: string) => { - if (socket) { - socket.emit("reaction", { - roomId: sessionId, - reaction: reactionType, - }); - } - }; - - const joinRoom = async () => { - if (!socket || !sessionId || !nickname) { - toast.error("닉네임을 입력해주세요."); - return; - } - - const mediaStream = await getMedia(); - if (!mediaStream) { - toast.error( - "미디어 스트림을 가져오지 못했습니다. 미디어 장치를 확인 후 다시 시도해주세요." - ); - navigate("/sessions"); - return; - } - - console.log("Joining room:", sessionId); - socket.emit("join_room", { roomId: sessionId, nickname }); - }; - - const addReaction = useCallback( - (senderId: string, reactionType: string) => { - setPeers((prev) => - prev.map((peer) => - peer.peerId === senderId ? { ...peer, reaction: reactionType } : peer - ) - ); - }, []); // eslint-disable-line react-hooks/exhaustive-deps + joinRoom, + emitReaction + } = useSession(sessionId); return (
From 530b6a05e1ba41af5ad28db02c3b379e9a717a4a Mon Sep 17 00:00:00 2001 From: yiseungyun Date: Sun, 17 Nov 2024 23:58:19 +0900 Subject: [PATCH 006/180] =?UTF-8?q?test:=20useSession=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/hooks/__test__/useSession.test.ts | 285 ++++++++++++++++++ 1 file changed, 285 insertions(+) create mode 100644 frontend/src/hooks/__test__/useSession.test.ts diff --git a/frontend/src/hooks/__test__/useSession.test.ts b/frontend/src/hooks/__test__/useSession.test.ts new file mode 100644 index 00000000..6c11a280 --- /dev/null +++ b/frontend/src/hooks/__test__/useSession.test.ts @@ -0,0 +1,285 @@ +import { renderHook, act } from "@testing-library/react"; +import { useSession } from "../useSession"; +import useSocketStore from "@/stores/useSocketStore"; +import useMediaDevices from "@/hooks/useMediaDevices"; +import usePeerConnection from "@/hooks/usePeerConnection"; +import { useNavigate } from "react-router-dom"; +import useToast from "@/hooks/useToast"; +import { Socket } from "socket.io-client"; +import { create } from "zustand"; + +interface SocketStore { + socket: Socket | null; + connect: (url: string) => void; + disconnect: () => void; +} +// Store 모킹을 위한 타입 설정 +type MockStore = ReturnType>; + +const mockSocket = { + emit: jest.fn(), + on: jest.fn(), + off: jest.fn(), + id: "mock-socket-id", +}; +const mockMediaStream = { + getTracks: jest.fn().mockReturnValue([{ stop: jest.fn(), enabled: true }]), +}; +const mockNavigate = jest.fn(); +const mockToast = { success: jest.fn(), error: jest.fn() }; +let mockPeerConnections = { current: {} }; + +// jest.mock: 실제 모듈대신 mock 모듈을 사용하도록 설정 +jest.mock("@/stores/useSocketStore", () => ({ + _esModule: true, + default: jest.fn().mockImplementation(() => ({ + socket: mockSocket, + connect: jest.fn(), + disconnect: jest.fn(), + })), +})); +jest.mock("@/hooks/useMediaDevices"); +jest.mock("@/hooks/usePeerConnection"); +jest.mock("@/hooks/useToast"); +jest.mock("react-router-dom", () => ({ + useNavigate: jest.fn(), +})); + +describe("useSession Hook 테스트", () => { + let mockStore: jest.Mocked; + + beforeEach(() => { + jest.clearAllMocks(); + + mockPeerConnections = { + current: { + "peer-1": { + ontrack: null, + onicecandidate: null, + oniceconnectionstatechange: null, + onconnectionstatechange: null, + close: jest.fn(), + }, + }, + }; + + // mockImplementation: mock 함수 구현, 함수가 호출될 때 어떤 값을 반환할지 지정 + (useSocketStore as unknown as jest.Mock).mockImplementation( + () => mockStore + ); + + (useMediaDevices as jest.Mock).mockReturnValue({ + userAudioDevices: [], + userVideoDevices: [], + selectedAudioDeviceId: "", + selectedVideoDeviceId: "", + stream: mockMediaStream, + isVideoOn: true, + isMicOn: true, + videoLoading: false, + handleMicToggle: jest.fn(), + handleVideoToggle: jest.fn(), + setSelectedAudioDeviceId: jest.fn(), + setSelectedVideoDeviceId: jest.fn(), + getMedia: jest.fn().mockResolvedValue(mockMediaStream), + }); + + (usePeerConnection as jest.Mock).mockReturnValue({ + createPeerConnection: jest.fn(), + closePeerConnection: jest.fn(), + peers: [], + setPeers: jest.fn(), + peerConnections: mockPeerConnections, + }); + + (useNavigate as jest.Mock).mockReturnValue(mockNavigate); + (useToast as jest.Mock).mockReturnValue(mockToast); + }); + + describe("초기화 및 기본 동작 테스트", () => { + it("초기 상태 설정", () => { + const { result } = renderHook(() => useSession("test-session")); + + expect(result.current.nickname).toBe(""); + expect(result.current.reaction).toBe(""); + expect(result.current.isVideoOn).toBe(true); + expect(result.current.isMicOn).toBe(true); + }); + + it("마운트 시 소켓 연결", () => { + // TODO: 연결되지 않았을 때와 연결되었을 때 나누어 테스트 + // TODO: 이미 연결되었을 때 재연결하지 않는지 테스트 + // TODO: 연결 실패 시 에러 처리 테스트 + // TODO: 언마운트 시 소켓 정리 테스트 + renderHook(() => useSession("test-session")); + const connectFn = useSocketStore().connect; + + expect(connectFn).toHaveBeenCalledWith( + import.meta.env.VITE_SIGNALING_SERVER_URL + ); + }); + }); + + describe("스터디룸 입장 테스트", () => { + it("스터디룸 입장 성공", async () => { + const { result } = renderHook(() => useSession("test-session")); + + // 1. 닉네임 설정 + act(() => { + result.current.setNickname("test-user"); + }); + + // 2. 방 입장 시도 + await act(async () => { + await result.current.joinRoom(); + }); + + // 3. 미디어 스트림 요청 확인 + expect(useMediaDevices().getMedia).toHaveBeenCalled(); + + // 4. 소켓 이벤트 발생 확인 + expect(mockSocket.emit).toHaveBeenCalledWith("join_room", { + roomId: "test-session", + nickname: "test-user", + }); + + // 5. 성공 메시지 표시 + expect(mockToast.success).toHaveBeenCalled(); + }); + + it("닉네임 없이 스터디룸 입장", async () => { + const { result } = renderHook(() => useSession("test-session")); + + await act(async () => { + await result.current.joinRoom(); + }); + + expect(mockToast.error).toHaveBeenCalledWith("닉네임을 입력해주세요."); + expect(mockSocket.emit).not.toHaveBeenCalled(); + }); + + it("미디어 스트림 획득 실패 시 에러 처리", async () => { + (useMediaDevices as jest.Mock).mockReturnValue({ + ...useMediaDevices(), + getMedia: jest.fn().mockResolvedValue(null), + }); + + const { result } = renderHook(() => useSession("test-session")); + act(() => { + result.current.setNickname("test-user"); + }); + + await act(async () => { + await result.current.joinRoom(); + }); + + expect(mockToast.error).toHaveBeenCalledWith( + "미디어 스트림을 가져오지 못했습니다. 미디어 장치를 확인 후 다시 시도해주세요." + ); + expect(mockNavigate).toHaveBeenCalledWith("/sessions"); + }); + }); + + describe("리액션 기능 테스트", () => { + it("리액션 이벤트 발생", () => { + const { result } = renderHook(() => useSession("test-session")); + + act(() => { + result.current.emitReaction("👍"); + }); + + expect(mockSocket.emit).toHaveBeenCalledWith("reaction", { + roomId: "test-session", + reaction: "👍", + }); + }); + }); + + describe("소켓 이벤트 리스너 테스트", () => { + it("모든 소켓 이벤트 리스너가 등록", () => { + renderHook(() => useSession("test-session")); + + const expectedEvents = [ + "all_users", + "getOffer", + "getAnswer", + "getCandidate", + "user_exit", + "room_full", + "reaction", + ]; + + expectedEvents.forEach((event) => { + expect(mockSocket.on).toHaveBeenCalledWith(event, expect.any(Function)); + }); + }); + + it("room_full 이벤트 발생", () => { + renderHook(() => useSession("test-session")); + + // room_full 이벤트 핸들러 찾기 + const roomFullHandler = mockSocket.on.mock.calls.find( + ([event]) => event === "room_full" + )[1]; + + // 이벤트 핸들러 실행 + roomFullHandler(); + + expect(mockToast.error).toHaveBeenCalledWith( + "해당 세션은 이미 유저가 가득 찼습니다." + ); + expect(mockNavigate).toHaveBeenCalledWith("/sessions"); + }); + }); + + describe("정리(Cleanup) 테스트", () => { + it("언마운트 시 모든 리소스 정리", () => { + const { unmount } = renderHook(() => useSession("test-session")); + + unmount(); + + // 1. 소켓 이벤트 리스너 제거 + expect(mockSocket.off).toHaveBeenCalledWith( + "all_users", + expect.any(Function) + ); + expect(mockSocket.off).toHaveBeenCalledWith( + "getOffer", + expect.any(Function) + ); + expect(mockSocket.off).toHaveBeenCalledWith( + "getAnswer", + expect.any(Function) + ); + expect(mockSocket.off).toHaveBeenCalledWith( + "getCandidate", + expect.any(Function) + ); + expect(mockSocket.off).toHaveBeenCalledWith("user_exit"); + expect(mockSocket.off).toHaveBeenCalledWith("room_full"); + expect(mockSocket.off).toHaveBeenCalledWith( + "reaction", + expect.any(Function) + ); + + // 2. 미디어 트랙 정리 + expect(mockMediaStream.getTracks).toHaveBeenCalled(); + expect(mockMediaStream.getTracks()[0].stop).toHaveBeenCalled(); + + // 3. Peer Connection 정리 + expect(mockPeerConnections.current["peer-1"].close).toHaveBeenCalled(); + }); + + it("스트림이 없는 경우에도 정리 동작", () => { + (useMediaDevices as jest.Mock).mockReturnValue({ + ...useMediaDevices(), + stream: null, + }); + + const { unmount } = renderHook(() => useSession("test-session")); + unmount(); + + expect(mockSocket.off).toHaveBeenCalled(); + }); + }); +}); From 273ba3c1f2aa1eeed36bfd67482c382588da640f Mon Sep 17 00:00:00 2001 From: JeongwooSeo Date: Mon, 18 Nov 2024 01:12:45 +0900 Subject: [PATCH 007/180] =?UTF-8?q?feat:=20=EC=95=A0=EB=8B=88=EB=A9=94?= =?UTF-8?q?=EC=9D=B4=EC=85=98=20=EC=98=86=EC=97=90=204=202=EA=B0=9C=20?= =?UTF-8?q?=EB=B6=99=EC=9D=B4=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/pages/ErrorPage.tsx | 40 ++++++++++++++++++++++++++------ 1 file changed, 33 insertions(+), 7 deletions(-) diff --git a/frontend/src/pages/ErrorPage.tsx b/frontend/src/pages/ErrorPage.tsx index 4139eb25..d8b2c7df 100644 --- a/frontend/src/pages/ErrorPage.tsx +++ b/frontend/src/pages/ErrorPage.tsx @@ -1,18 +1,29 @@ import Lottie from "react-lottie"; import snowmanAnimate from "../../public/assets/snowman.json"; +import { useNavigate } from "react-router-dom"; const ErrorPage = () => { + const navigate = useNavigate(); return ( -
-
-

404 Error

-

- 이런! 요청하신 데이터를 찾을 수 없었어요! -

+
+
+ + 4 + { ariaRole={undefined} isClickToPauseDisabled={true} /> + + 4 +
+
); From 6560e754e17ee9899ebbdece3447828de3298908 Mon Sep 17 00:00:00 2001 From: yiseungyun Date: Mon, 18 Nov 2024 14:02:33 +0900 Subject: [PATCH 008/180] =?UTF-8?q?test:=20=EC=86=8C=EC=BC=93=20=EC=97=B0?= =?UTF-8?q?=EA=B2=B0,=20=EC=9D=B4=EB=AF=B8=20=EC=97=B0=EA=B2=B0=20?= =?UTF-8?q?=EB=90=90=EC=9D=84=20=EB=95=8C=20=EC=97=B0=EA=B2=B0=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/hooks/__test__/useSession.test.ts | 105 +++++++++++------- frontend/src/hooks/useSession.ts | 5 +- frontend/src/hooks/useSocket.ts | 9 +- frontend/src/hooks/useToast.ts | 2 +- frontend/tsconfig.test.json | 23 +++- 5 files changed, 93 insertions(+), 51 deletions(-) diff --git a/frontend/src/hooks/__test__/useSession.test.ts b/frontend/src/hooks/__test__/useSession.test.ts index 6c11a280..fb5491ef 100644 --- a/frontend/src/hooks/__test__/useSession.test.ts +++ b/frontend/src/hooks/__test__/useSession.test.ts @@ -1,55 +1,86 @@ -import { renderHook, act } from "@testing-library/react"; +import { renderHook } from "@testing-library/react"; import { useSession } from "../useSession"; import useSocketStore from "@/stores/useSocketStore"; import useMediaDevices from "@/hooks/useMediaDevices"; import usePeerConnection from "@/hooks/usePeerConnection"; import { useNavigate } from "react-router-dom"; -import useToast from "@/hooks/useToast"; import { Socket } from "socket.io-client"; -import { create } from "zustand"; -interface SocketStore { - socket: Socket | null; - connect: (url: string) => void; - disconnect: () => void; -} -// Store 모킹을 위한 타입 설정 -type MockStore = ReturnType>; +type MockSocket = Partial & { + emit: jest.Mock; + on: jest.Mock; + off: jest.Mock; + id: string; +}; -const mockSocket = { +const mockSocket: MockSocket = { emit: jest.fn(), on: jest.fn(), off: jest.fn(), id: "mock-socket-id", }; + +const mockSocketStore = { + socket: null as MockSocket | null, + connect: jest.fn(), + disconnect: jest.fn() +}; + const mockMediaStream = { getTracks: jest.fn().mockReturnValue([{ stop: jest.fn(), enabled: true }]), }; + const mockNavigate = jest.fn(); -const mockToast = { success: jest.fn(), error: jest.fn() }; let mockPeerConnections = { current: {} }; // jest.mock: 실제 모듈대신 mock 모듈을 사용하도록 설정 -jest.mock("@/stores/useSocketStore", () => ({ - _esModule: true, - default: jest.fn().mockImplementation(() => ({ - socket: mockSocket, - connect: jest.fn(), - disconnect: jest.fn(), - })), -})); jest.mock("@/hooks/useMediaDevices"); -jest.mock("@/hooks/usePeerConnection"); -jest.mock("@/hooks/useToast"); + +jest.mock("@/hooks/usePeerConnection", () => ({ + __esModule: true, + default: jest.fn().mockReturnValue({ + createPeerConnection: jest.fn(), + closePeerConnection: jest.fn(), + peers: [], + setPeers: jest.fn(), + peerConnections: { current: {} } + }) +})); + +jest.mock("@/hooks/useToast", () => ({ + __esModule: true, + default: () => ({ + error: jest.fn(), + success: jest.fn(), + }) +})); + jest.mock("react-router-dom", () => ({ useNavigate: jest.fn(), })); +jest.mock("@/stores/useSocketStore", () => ({ + __esModule: true, + default: jest.fn(() => mockSocketStore) +})); + +jest.mock("@/hooks/useSocket", () => ({ + __esModule: true, + default: () => { + const store = useSocketStore(); + if (!store.socket) { + store.connect('test-url'); + } + return { socket: store.socket }; + } +})); + describe("useSession Hook 테스트", () => { - let mockStore: jest.Mocked; beforeEach(() => { jest.clearAllMocks(); + mockSocketStore.socket = null; + mockSocketStore.connect = jest.fn(); mockPeerConnections = { current: { @@ -63,11 +94,6 @@ describe("useSession Hook 테스트", () => { }, }; - // mockImplementation: mock 함수 구현, 함수가 호출될 때 어떤 값을 반환할지 지정 - (useSocketStore as unknown as jest.Mock).mockImplementation( - () => mockStore - ); - (useMediaDevices as jest.Mock).mockReturnValue({ userAudioDevices: [], userVideoDevices: [], @@ -93,7 +119,6 @@ describe("useSession Hook 테스트", () => { }); (useNavigate as jest.Mock).mockReturnValue(mockNavigate); - (useToast as jest.Mock).mockReturnValue(mockToast); }); describe("초기화 및 기본 동작 테스트", () => { @@ -106,21 +131,21 @@ describe("useSession Hook 테스트", () => { expect(result.current.isMicOn).toBe(true); }); - it("마운트 시 소켓 연결", () => { - // TODO: 연결되지 않았을 때와 연결되었을 때 나누어 테스트 - // TODO: 이미 연결되었을 때 재연결하지 않는지 테스트 - // TODO: 연결 실패 시 에러 처리 테스트 - // TODO: 언마운트 시 소켓 정리 테스트 + it("소켓이 없는 경우: 연결 시도", () => { renderHook(() => useSession("test-session")); - const connectFn = useSocketStore().connect; - expect(connectFn).toHaveBeenCalledWith( - import.meta.env.VITE_SIGNALING_SERVER_URL - ); + expect(mockSocketStore.connect).toHaveBeenCalled(); + }); + + it("이미 소켓이 있는 경우: 연결 시도 X", () => { + mockSocketStore.socket = mockSocket; + + renderHook(() => useSession("test-session")); + expect(mockSocketStore.connect).not.toHaveBeenCalled(); }); }); - describe("스터디룸 입장 테스트", () => { + /*describe("스터디룸 입장 테스트", () => { it("스터디룸 입장 성공", async () => { const { result } = renderHook(() => useSession("test-session")); @@ -281,5 +306,5 @@ describe("useSession Hook 테스트", () => { expect(mockSocket.off).toHaveBeenCalled(); }); - }); + });*/ }); diff --git a/frontend/src/hooks/useSession.ts b/frontend/src/hooks/useSession.ts index ad94a513..8d1593bb 100644 --- a/frontend/src/hooks/useSession.ts +++ b/frontend/src/hooks/useSession.ts @@ -3,7 +3,7 @@ import { useNavigate } from "react-router-dom"; import useToast from "@/hooks/useToast"; import useMediaDevices from "@/hooks/useMediaDevices"; import usePeerConnection from "@/hooks/usePeerConnection"; -import useSocketStore from "@/stores/useSocketStore"; +import useSocket from "./useSocket"; interface User { id: string; @@ -11,7 +11,7 @@ interface User { } export const useSession = (sessionId: string | undefined) => { - const { socket, connect } = useSocketStore(); + const { socket } = useSocket(); const navigate = useNavigate(); const toast = useToast(); @@ -45,7 +45,6 @@ export const useSession = (sessionId: string | undefined) => { } = useMediaDevices(); useEffect(() => { - if (!socket) connect(import.meta.env.VITE_SIGNALING_SERVER_URL); const connections = peerConnections; return () => { diff --git a/frontend/src/hooks/useSocket.ts b/frontend/src/hooks/useSocket.ts index e102380b..1ae6527a 100644 --- a/frontend/src/hooks/useSocket.ts +++ b/frontend/src/hooks/useSocket.ts @@ -1,7 +1,9 @@ import { useEffect } from "react"; import useSocketStore from "../stores/useSocketStore"; -const useSocket = (socketURL: string) => { +const socketURL = import.meta.env.VITE_SIGNALING_SERVER; + +const useSocket = () => { const { socket, connect } = useSocketStore(); useEffect(() => { @@ -9,10 +11,9 @@ const useSocket = (socketURL: string) => { connect(socketURL); } // eslint-disable-next-line react-hooks/exhaustive-deps - - }, [socketURL]); + }, []); return { socket }; }; - + export default useSocket; diff --git a/frontend/src/hooks/useToast.ts b/frontend/src/hooks/useToast.ts index 0d2e1aa0..fe2ea392 100644 --- a/frontend/src/hooks/useToast.ts +++ b/frontend/src/hooks/useToast.ts @@ -1,4 +1,4 @@ -import useToastStore from "../stores/useToastStore.ts"; +import useToastStore from "@/stores/useToastStore"; import { useCallback } from "react"; const useToast = () => { diff --git a/frontend/tsconfig.test.json b/frontend/tsconfig.test.json index d9b1b855..f7f602ea 100644 --- a/frontend/tsconfig.test.json +++ b/frontend/tsconfig.test.json @@ -1,6 +1,23 @@ { "extends": "./tsconfig.json", - "compilerOptions": { - "jsx": "react-jsx" - } + "compilerOptions": { + "esModuleInterop": true, + "module": "esnext", + "jsx": "react-jsx", + "types": [ + "jest", + "node" + ], + "moduleResolution": "node", + "paths": { + "@/*": [ + "./src/*" + ] + } + }, + "include": [ + "src/**/*", + "src/**/*.ts", + "src/**/*.tsx" + ] } \ No newline at end of file From acd582834cf080955397385873ef034839d5e0db Mon Sep 17 00:00:00 2001 From: JeongwooSeo Date: Mon, 18 Nov 2024 17:06:31 +0900 Subject: [PATCH 009/180] =?UTF-8?q?feat:=20text=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/pages/ErrorPage.tsx | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/frontend/src/pages/ErrorPage.tsx b/frontend/src/pages/ErrorPage.tsx index d8b2c7df..11e40584 100644 --- a/frontend/src/pages/ErrorPage.tsx +++ b/frontend/src/pages/ErrorPage.tsx @@ -41,14 +41,23 @@ const ErrorPage = () => { 4
- +

+ 이런! 요청하신 데이터를 찾을 수 없었어요! +

+ +
); From 21a645c311f44e5fbb904c8d896e238cafb583a7 Mon Sep 17 00:00:00 2001 From: yiseungyun Date: Mon, 18 Nov 2024 18:26:32 +0900 Subject: [PATCH 010/180] =?UTF-8?q?test:=20=EC=8A=A4=ED=84=B0=EB=94=94?= =?UTF-8?q?=EB=A3=B8=20=EC=9E=85=EC=9E=A5=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/hooks/__test__/useSession.test.ts | 42 +++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/frontend/src/hooks/__test__/useSession.test.ts b/frontend/src/hooks/__test__/useSession.test.ts index fb5491ef..9cba1522 100644 --- a/frontend/src/hooks/__test__/useSession.test.ts +++ b/frontend/src/hooks/__test__/useSession.test.ts @@ -5,6 +5,7 @@ import useMediaDevices from "@/hooks/useMediaDevices"; import usePeerConnection from "@/hooks/usePeerConnection"; import { useNavigate } from "react-router-dom"; import { Socket } from "socket.io-client"; +import { act } from "react"; type MockSocket = Partial & { emit: jest.Mock; @@ -76,24 +77,13 @@ jest.mock("@/hooks/useSocket", () => ({ })); describe("useSession Hook 테스트", () => { + const mockGetMedia = jest.fn().mockResolvedValue(mockMediaStream); beforeEach(() => { jest.clearAllMocks(); mockSocketStore.socket = null; mockSocketStore.connect = jest.fn(); - mockPeerConnections = { - current: { - "peer-1": { - ontrack: null, - onicecandidate: null, - oniceconnectionstatechange: null, - onconnectionstatechange: null, - close: jest.fn(), - }, - }, - }; - (useMediaDevices as jest.Mock).mockReturnValue({ userAudioDevices: [], userVideoDevices: [], @@ -107,9 +97,21 @@ describe("useSession Hook 테스트", () => { handleVideoToggle: jest.fn(), setSelectedAudioDeviceId: jest.fn(), setSelectedVideoDeviceId: jest.fn(), - getMedia: jest.fn().mockResolvedValue(mockMediaStream), + getMedia: mockGetMedia, }); + mockPeerConnections = { + current: { + "peer-1": { + ontrack: null, + onicecandidate: null, + oniceconnectionstatechange: null, + onconnectionstatechange: null, + close: jest.fn(), + }, + }, + }; + (usePeerConnection as jest.Mock).mockReturnValue({ createPeerConnection: jest.fn(), closePeerConnection: jest.fn(), @@ -145,8 +147,9 @@ describe("useSession Hook 테스트", () => { }); }); - /*describe("스터디룸 입장 테스트", () => { + describe("스터디룸 입장 테스트", () => { it("스터디룸 입장 성공", async () => { + mockSocketStore.socket = mockSocket; const { result } = renderHook(() => useSession("test-session")); // 1. 닉네임 설정 @@ -160,19 +163,16 @@ describe("useSession Hook 테스트", () => { }); // 3. 미디어 스트림 요청 확인 - expect(useMediaDevices().getMedia).toHaveBeenCalled(); + expect(mockGetMedia).toHaveBeenCalled(); // 4. 소켓 이벤트 발생 확인 expect(mockSocket.emit).toHaveBeenCalledWith("join_room", { roomId: "test-session", nickname: "test-user", }); - - // 5. 성공 메시지 표시 - expect(mockToast.success).toHaveBeenCalled(); }); - it("닉네임 없이 스터디룸 입장", async () => { + /*it("닉네임 없이 스터디룸 입장", async () => { const { result } = renderHook(() => useSession("test-session")); await act(async () => { @@ -202,10 +202,10 @@ describe("useSession Hook 테스트", () => { "미디어 스트림을 가져오지 못했습니다. 미디어 장치를 확인 후 다시 시도해주세요." ); expect(mockNavigate).toHaveBeenCalledWith("/sessions"); - }); + });*/ }); - describe("리액션 기능 테스트", () => { + /*describe("리액션 기능 테스트", () => { it("리액션 이벤트 발생", () => { const { result } = renderHook(() => useSession("test-session")); From 8b07d78fa404d1b087281fd962b2aa9e09655588 Mon Sep 17 00:00:00 2001 From: yiseungyun Date: Mon, 18 Nov 2024 18:43:49 +0900 Subject: [PATCH 011/180] =?UTF-8?q?test:=20=EB=8B=89=EB=84=A4=EC=9E=84=20?= =?UTF-8?q?=EC=97=86=EC=9D=B4=20=EC=9E=85=EC=9E=A5=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/hooks/__test__/useSession.test.ts | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/frontend/src/hooks/__test__/useSession.test.ts b/frontend/src/hooks/__test__/useSession.test.ts index 9cba1522..d61895d4 100644 --- a/frontend/src/hooks/__test__/useSession.test.ts +++ b/frontend/src/hooks/__test__/useSession.test.ts @@ -1,11 +1,12 @@ import { renderHook } from "@testing-library/react"; -import { useSession } from "../useSession"; +import { useSession } from "@/hooks/useSession"; import useSocketStore from "@/stores/useSocketStore"; import useMediaDevices from "@/hooks/useMediaDevices"; import usePeerConnection from "@/hooks/usePeerConnection"; import { useNavigate } from "react-router-dom"; import { Socket } from "socket.io-client"; import { act } from "react"; +import useToast from "@/hooks/useToast"; type MockSocket = Partial & { emit: jest.Mock; @@ -31,6 +32,7 @@ const mockMediaStream = { getTracks: jest.fn().mockReturnValue([{ stop: jest.fn(), enabled: true }]), }; +const mockToast = { success: jest.fn(), error: jest.fn() }; const mockNavigate = jest.fn(); let mockPeerConnections = { current: {} }; @@ -50,10 +52,7 @@ jest.mock("@/hooks/usePeerConnection", () => ({ jest.mock("@/hooks/useToast", () => ({ __esModule: true, - default: () => ({ - error: jest.fn(), - success: jest.fn(), - }) + default: () => mockToast })); jest.mock("react-router-dom", () => ({ @@ -131,6 +130,9 @@ describe("useSession Hook 테스트", () => { expect(result.current.reaction).toBe(""); expect(result.current.isVideoOn).toBe(true); expect(result.current.isMicOn).toBe(true); + expect(result.current.roomMetadata).toBeNull(); + expect(result.current.isHost).toBe(false); + expect(result.current.participants).toEqual([{ nickname: "", isHost: false }]); }); it("소켓이 없는 경우: 연결 시도", () => { @@ -172,18 +174,21 @@ describe("useSession Hook 테스트", () => { }); }); - /*it("닉네임 없이 스터디룸 입장", async () => { + it("닉네임 없이 스터디룸 입장", async () => { const { result } = renderHook(() => useSession("test-session")); await act(async () => { await result.current.joinRoom(); }); + console.log(result.current.nickname === ""); // true + console.log(!result.current.nickname); // true + expect(mockToast.error).toHaveBeenCalledWith("닉네임을 입력해주세요."); expect(mockSocket.emit).not.toHaveBeenCalled(); }); - it("미디어 스트림 획득 실패 시 에러 처리", async () => { + /*it("미디어 스트림 획득 실패 시 에러 처리", async () => { (useMediaDevices as jest.Mock).mockReturnValue({ ...useMediaDevices(), getMedia: jest.fn().mockResolvedValue(null), From a0baf42502b2ad956e11d0b0c561b1d3c1ff99ed Mon Sep 17 00:00:00 2001 From: yiseungyun Date: Mon, 18 Nov 2024 18:47:57 +0900 Subject: [PATCH 012/180] =?UTF-8?q?fix:=20joinRoom=20=ED=86=A0=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=97=90=EB=9F=AC=20=EB=A9=94=EC=8B=9C=EC=A7=80=20?= =?UTF-8?q?=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 세션 id, 소켓 연결 없을 때 닉네임 입력해달라고 띄우는 오류 수정 - 모두 조건 분리해서 토스트 에러 띄우도록 수정 --- frontend/src/hooks/useSession.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/frontend/src/hooks/useSession.ts b/frontend/src/hooks/useSession.ts index 18132cd3..4a83892c 100644 --- a/frontend/src/hooks/useSession.ts +++ b/frontend/src/hooks/useSession.ts @@ -273,7 +273,17 @@ export const useSession = (sessionId: string | undefined) => { }, [setupSocketListeners]); const joinRoom = async () => { - if (!socket || !sessionId || !nickname) { + if (!socket) { + toast.error("소켓 연결이 필요합니다."); + return; + } + + if (!sessionId) { + toast.error("세션 ID가 필요합니다."); + return; + } + + if (!nickname) { toast.error("닉네임을 입력해주세요."); return; } From 2abe710e52bf5c281c3bc114b1542bd48a3080de Mon Sep 17 00:00:00 2001 From: yiseungyun Date: Mon, 18 Nov 2024 18:50:46 +0900 Subject: [PATCH 013/180] =?UTF-8?q?test:=20=EB=AF=B8=EB=94=94=EC=96=B4=20?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=EB=A6=BC=20=ED=9A=8D=EB=93=9D=20=EC=8B=A4?= =?UTF-8?q?=ED=8C=A8=20=EC=97=90=EB=9F=AC=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EB=B0=8F=20=EB=8B=89=EB=84=A4=EC=9E=84=20=EC=97=86=EC=9D=B4=20?= =?UTF-8?q?=EC=9E=85=EC=9E=A5=20=ED=85=8C=EC=8A=A4=ED=8A=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 --- frontend/src/hooks/__test__/useSession.test.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/frontend/src/hooks/__test__/useSession.test.ts b/frontend/src/hooks/__test__/useSession.test.ts index d61895d4..122e0fb9 100644 --- a/frontend/src/hooks/__test__/useSession.test.ts +++ b/frontend/src/hooks/__test__/useSession.test.ts @@ -175,20 +175,19 @@ describe("useSession Hook 테스트", () => { }); it("닉네임 없이 스터디룸 입장", async () => { + mockSocketStore.socket = mockSocket; const { result } = renderHook(() => useSession("test-session")); await act(async () => { await result.current.joinRoom(); }); - console.log(result.current.nickname === ""); // true - console.log(!result.current.nickname); // true - expect(mockToast.error).toHaveBeenCalledWith("닉네임을 입력해주세요."); expect(mockSocket.emit).not.toHaveBeenCalled(); }); - /*it("미디어 스트림 획득 실패 시 에러 처리", async () => { + it("미디어 스트림 획득 실패 시 에러 처리", async () => { + mockSocketStore.socket = mockSocket; (useMediaDevices as jest.Mock).mockReturnValue({ ...useMediaDevices(), getMedia: jest.fn().mockResolvedValue(null), @@ -207,7 +206,7 @@ describe("useSession Hook 테스트", () => { "미디어 스트림을 가져오지 못했습니다. 미디어 장치를 확인 후 다시 시도해주세요." ); expect(mockNavigate).toHaveBeenCalledWith("/sessions"); - });*/ + }); }); /*describe("리액션 기능 테스트", () => { From a22efd47fffad5577d7a373092e1f17b7a92bc6e Mon Sep 17 00:00:00 2001 From: yiseungyun Date: Mon, 18 Nov 2024 18:55:43 +0900 Subject: [PATCH 014/180] =?UTF-8?q?test:=20=EB=A6=AC=EC=95=A1=EC=85=98=20?= =?UTF-8?q?=EC=9D=B4=EB=B2=A4=ED=8A=B8=20=EB=B0=9C=EC=83=9D=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/hooks/__test__/useSession.test.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/frontend/src/hooks/__test__/useSession.test.ts b/frontend/src/hooks/__test__/useSession.test.ts index 122e0fb9..c2049bff 100644 --- a/frontend/src/hooks/__test__/useSession.test.ts +++ b/frontend/src/hooks/__test__/useSession.test.ts @@ -209,8 +209,9 @@ describe("useSession Hook 테스트", () => { }); }); - /*describe("리액션 기능 테스트", () => { + describe("리액션 기능 테스트", () => { it("리액션 이벤트 발생", () => { + mockSocketStore.socket = mockSocket; const { result } = renderHook(() => useSession("test-session")); act(() => { @@ -224,7 +225,7 @@ describe("useSession Hook 테스트", () => { }); }); - describe("소켓 이벤트 리스너 테스트", () => { + /*describe("소켓 이벤트 리스너 테스트", () => { it("모든 소켓 이벤트 리스너가 등록", () => { renderHook(() => useSession("test-session")); From ff5e7170782ed395793ce1dfdfb1feda795fef9b Mon Sep 17 00:00:00 2001 From: yiseungyun Date: Mon, 18 Nov 2024 19:25:36 +0900 Subject: [PATCH 015/180] =?UTF-8?q?test:=20=EC=86=8C=EC=BC=93=20=EC=9D=B4?= =?UTF-8?q?=EB=B2=A4=ED=8A=B8=20=EB=A6=AC=EC=8A=A4=EB=84=88=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/hooks/__test__/useSession.test.ts | 29 +++--- frontend/src/hooks/useSession.ts | 89 +++++++++++-------- frontend/src/hooks/useSocket.ts | 1 - frontend/src/pages/SessionPage.tsx | 14 ++- 4 files changed, 80 insertions(+), 53 deletions(-) diff --git a/frontend/src/hooks/__test__/useSession.test.ts b/frontend/src/hooks/__test__/useSession.test.ts index c2049bff..e3d3314c 100644 --- a/frontend/src/hooks/__test__/useSession.test.ts +++ b/frontend/src/hooks/__test__/useSession.test.ts @@ -6,7 +6,6 @@ import usePeerConnection from "@/hooks/usePeerConnection"; import { useNavigate } from "react-router-dom"; import { Socket } from "socket.io-client"; import { act } from "react"; -import useToast from "@/hooks/useToast"; type MockSocket = Partial & { emit: jest.Mock; @@ -25,7 +24,7 @@ const mockSocket: MockSocket = { const mockSocketStore = { socket: null as MockSocket | null, connect: jest.fn(), - disconnect: jest.fn() + disconnect: jest.fn(), }; const mockMediaStream = { @@ -46,13 +45,13 @@ jest.mock("@/hooks/usePeerConnection", () => ({ closePeerConnection: jest.fn(), peers: [], setPeers: jest.fn(), - peerConnections: { current: {} } - }) + peerConnections: { current: {} }, + }), })); jest.mock("@/hooks/useToast", () => ({ __esModule: true, - default: () => mockToast + default: () => mockToast, })); jest.mock("react-router-dom", () => ({ @@ -61,7 +60,7 @@ jest.mock("react-router-dom", () => ({ jest.mock("@/stores/useSocketStore", () => ({ __esModule: true, - default: jest.fn(() => mockSocketStore) + default: jest.fn(() => mockSocketStore), })); jest.mock("@/hooks/useSocket", () => ({ @@ -69,10 +68,10 @@ jest.mock("@/hooks/useSocket", () => ({ default: () => { const store = useSocketStore(); if (!store.socket) { - store.connect('test-url'); + store.connect("test-url"); } return { socket: store.socket }; - } + }, })); describe("useSession Hook 테스트", () => { @@ -132,7 +131,9 @@ describe("useSession Hook 테스트", () => { expect(result.current.isMicOn).toBe(true); expect(result.current.roomMetadata).toBeNull(); expect(result.current.isHost).toBe(false); - expect(result.current.participants).toEqual([{ nickname: "", isHost: false }]); + expect(result.current.participants).toEqual([ + { nickname: "", isHost: false }, + ]); }); it("소켓이 없는 경우: 연결 시도", () => { @@ -225,8 +226,9 @@ describe("useSession Hook 테스트", () => { }); }); - /*describe("소켓 이벤트 리스너 테스트", () => { - it("모든 소켓 이벤트 리스너가 등록", () => { + describe("소켓 이벤트 리스너 테스트", () => { + it("모든 소켓 이벤트 리스너 등록", () => { + mockSocketStore.socket = mockSocket; renderHook(() => useSession("test-session")); const expectedEvents = [ @@ -237,6 +239,8 @@ describe("useSession Hook 테스트", () => { "user_exit", "room_full", "reaction", + "master_changed", + "room_finished", ]; expectedEvents.forEach((event) => { @@ -245,6 +249,7 @@ describe("useSession Hook 테스트", () => { }); it("room_full 이벤트 발생", () => { + mockSocketStore.socket = mockSocket; renderHook(() => useSession("test-session")); // room_full 이벤트 핸들러 찾기 @@ -262,7 +267,7 @@ describe("useSession Hook 테스트", () => { }); }); - describe("정리(Cleanup) 테스트", () => { + /*describe("정리(Cleanup) 테스트", () => { it("언마운트 시 모든 리소스 정리", () => { const { unmount } = renderHook(() => useSession("test-session")); diff --git a/frontend/src/hooks/useSession.ts b/frontend/src/hooks/useSession.ts index 4a83892c..14e3a040 100644 --- a/frontend/src/hooks/useSession.ts +++ b/frontend/src/hooks/useSession.ts @@ -99,38 +99,47 @@ export const useSession = (sessionId: string | undefined) => { }; }, [stream]); - const handleUserExit = useCallback(({ socketId }: { socketId: string }) => { - toast.error("유저가 나갔습니다."); - closePeerConnection(socketId); - }, [toast, closePeerConnection]); + const handleUserExit = useCallback( + ({ socketId }: { socketId: string }) => { + toast.error("유저가 나갔습니다."); + closePeerConnection(socketId); + }, + [toast, closePeerConnection] + ); const handleRoomFinished = useCallback(() => { toast.error("방장이 세션을 종료했습니다."); navigate("/sessions"); }, [toast, navigate]); - const handleHostChange = useCallback((data: ResponseMasterChanged) => { - if (socket && data.masterSocketId === socket.id) { - setIsHost(true); - toast.success("당신이 호스트가 되었습니다."); - } else { - setPeers((prev) => - prev.map((peer) => - peer.peerId === data.masterSocketId - ? { ...peer, isHost: true } - : peer - ) - ); - toast.success(`${data.masterNickname}님이 호스트가 되었습니다.`); - } - }, [socket, toast, setPeers]); + const handleHostChange = useCallback( + (data: ResponseMasterChanged) => { + if (socket && data.masterSocketId === socket.id) { + setIsHost(true); + toast.success("당신이 호스트가 되었습니다."); + } else { + setPeers((prev) => + prev.map((peer) => + peer.peerId === data.masterSocketId + ? { ...peer, isHost: true } + : peer + ) + ); + toast.success(`${data.masterNickname}님이 호스트가 되었습니다.`); + } + }, + [socket, toast, setPeers] + ); const setupSocketListeners = useCallback(() => { if (!socket || !stream) return; const handleAllUsers = ({ roomMetadata, users }: AllUsersResponse) => { if (!roomMetadata || !users) { - console.error("Invalid data received from server:", { roomMetadata, users }); + console.error("Invalid data received from server:", { + roomMetadata, + users, + }); return; } @@ -264,7 +273,7 @@ export const useSession = (sessionId: string | undefined) => { toast, handleHostChange, handleUserExit, - handleRoomFinished + handleRoomFinished, ]); useEffect(() => { @@ -300,14 +309,17 @@ export const useSession = (sessionId: string | undefined) => { socket.emit("join_room", { roomId: sessionId, nickname }); }; - const emitReaction = useCallback((reactionType: string) => { - if (socket) { - socket.emit("reaction", { - roomId: sessionId, - reaction: reactionType, - }); - } - }, [socket, sessionId]); + const emitReaction = useCallback( + (reactionType: string) => { + if (socket) { + socket.emit("reaction", { + roomId: sessionId, + reaction: reactionType, + }); + } + }, + [socket, sessionId] + ); const addReaction = useCallback( (senderId: string, reactionType: string) => { @@ -320,13 +332,16 @@ export const useSession = (sessionId: string | undefined) => { [setPeers] ); - const participants: Participant[] = useMemo(() => [ - { nickname, isHost }, - ...peers.map((peer) => ({ - nickname: peer.peerNickname, - isHost: peer.isHost || false, - })) - ], [nickname, isHost, peers]); + const participants: Participant[] = useMemo( + () => [ + { nickname, isHost }, + ...peers.map((peer) => ({ + nickname: peer.peerNickname, + isHost: peer.isHost || false, + })), + ], + [nickname, isHost, peers] + ); return { nickname, @@ -348,4 +363,4 @@ export const useSession = (sessionId: string | undefined) => { joinRoom, emitReaction, }; -}; \ No newline at end of file +}; diff --git a/frontend/src/hooks/useSocket.ts b/frontend/src/hooks/useSocket.ts index 24f0acc2..1ae6527a 100644 --- a/frontend/src/hooks/useSocket.ts +++ b/frontend/src/hooks/useSocket.ts @@ -16,5 +16,4 @@ const useSocket = () => { return { socket }; }; - export default useSocket; diff --git a/frontend/src/pages/SessionPage.tsx b/frontend/src/pages/SessionPage.tsx index 2d38f66b..c13d08dc 100644 --- a/frontend/src/pages/SessionPage.tsx +++ b/frontend/src/pages/SessionPage.tsx @@ -48,8 +48,16 @@ const SessionPage = () => {
-
-
+
+
{
); }; -export default SessionPage; \ No newline at end of file +export default SessionPage; From cf7971da4831a12f78c1bc869a891a7290860001 Mon Sep 17 00:00:00 2001 From: yiseungyun Date: Mon, 18 Nov 2024 19:46:13 +0900 Subject: [PATCH 016/180] =?UTF-8?q?refactor:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=82=AD=EC=A0=9C=20=EB=B0=8F?= =?UTF-8?q?=20MockPeerConnections=20=ED=83=80=EC=9E=85=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/hooks/__test__/useSession.test.ts | 33 ++++++++++--------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/frontend/src/hooks/__test__/useSession.test.ts b/frontend/src/hooks/__test__/useSession.test.ts index e3d3314c..000bc5be 100644 --- a/frontend/src/hooks/__test__/useSession.test.ts +++ b/frontend/src/hooks/__test__/useSession.test.ts @@ -14,6 +14,20 @@ type MockSocket = Partial & { id: string; }; +interface MockPeerConnection { + ontrack: null | ((event: any) => void); + onicecandidate: null | ((event: any) => void); + oniceconnectionstatechange: null | (() => void); + onconnectionstatechange: null | (() => void); + close: jest.Mock; +} + +interface MockPeerConnections { + current: { + [key: string]: MockPeerConnection; + }; +} + const mockSocket: MockSocket = { emit: jest.fn(), on: jest.fn(), @@ -33,7 +47,7 @@ const mockMediaStream = { const mockToast = { success: jest.fn(), error: jest.fn() }; const mockNavigate = jest.fn(); -let mockPeerConnections = { current: {} }; +let mockPeerConnections: MockPeerConnections = { current: {} }; // jest.mock: 실제 모듈대신 mock 모듈을 사용하도록 설정 jest.mock("@/hooks/useMediaDevices"); @@ -267,8 +281,9 @@ describe("useSession Hook 테스트", () => { }); }); - /*describe("정리(Cleanup) 테스트", () => { + describe("정리(Cleanup) 테스트", () => { it("언마운트 시 모든 리소스 정리", () => { + mockSocketStore.socket = mockSocket; const { unmount } = renderHook(() => useSession("test-session")); unmount(); @@ -304,17 +319,5 @@ describe("useSession Hook 테스트", () => { // 3. Peer Connection 정리 expect(mockPeerConnections.current["peer-1"].close).toHaveBeenCalled(); }); - - it("스트림이 없는 경우에도 정리 동작", () => { - (useMediaDevices as jest.Mock).mockReturnValue({ - ...useMediaDevices(), - stream: null, - }); - - const { unmount } = renderHook(() => useSession("test-session")); - unmount(); - - expect(mockSocket.off).toHaveBeenCalled(); - }); - });*/ + }); }); From fc05b4a0eab1e2605c86491b75d33ed99e78a948 Mon Sep 17 00:00:00 2001 From: JeongwooSeo Date: Mon, 18 Nov 2024 22:40:57 +0900 Subject: [PATCH 017/180] =?UTF-8?q?chore:=20lottie=20player=20=EB=9D=BC?= =?UTF-8?q?=EC=9D=B4=EB=B8=8C=EB=9F=AC=EB=A6=AC=20=EC=84=A4=EC=B9=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/package.json | 1 + pnpm-lock.yaml | 425 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 426 insertions(+) diff --git a/frontend/package.json b/frontend/package.json index 9a6cf26d..8fa32edd 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -12,6 +12,7 @@ "test": "echo \"Error: no test specified\" && exit 1" }, "dependencies": { + "@dotlottie/react-player": "^1.6.19", "@types/react-lottie": "^1.2.10", "@types/socket.io-client": "^3.0.0", "react": "^18.3.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 74f0d6cf..25d0403b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -117,6 +117,9 @@ importers: frontend: dependencies: + '@dotlottie/react-player': + specifier: ^1.6.19 + version: 1.6.19(react@18.3.1) '@types/react-lottie': specifier: ^1.2.10 version: 1.2.10 @@ -485,6 +488,24 @@ packages: resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} + '@dotlottie/common@0.7.11': + resolution: {integrity: sha512-0ThV1uZVfKJ2pwYQy52EKH8edKnvEsSDGdxRf/n1Wz15xbqI+8qKs0BHSIQ7yLyaU5gFSaEooEgK1kTiKJaE4g==} + engines: {node: '>18.0.0'} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + + '@dotlottie/dotlottie-js@0.7.2': + resolution: {integrity: sha512-BIEFkaKCRzVSHgKoZ2d/Squ1Rw4U93/YmHvON04PCx2gOBRAM2//GYUPcVijYHT8dQtom07KoyTKpXh0cRzKKw==} + engines: {node: '>=18.0.0'} + + '@dotlottie/react-player@1.6.19': + resolution: {integrity: sha512-kXQ/BD3OnK70wIGOIy5DWg1jgfgb3AWaDLXwVnLtzSmTBcH2dFcU/XGXF+z4w8vm2/s2tdbNd7c4yq8twoIVLw==} + deprecated: This package is superceded by @lottiefiles/dotlottie-react + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + + '@emnapi/runtime@1.3.1': + resolution: {integrity: sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==} + '@esbuild/aix-ppc64@0.21.5': resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} engines: {node: '>=12'} @@ -690,6 +711,111 @@ packages: resolution: {integrity: sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==} engines: {node: '>=18.18'} + '@img/sharp-darwin-arm64@0.33.5': + resolution: {integrity: sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.33.5': + resolution: {integrity: sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-darwin-arm64@1.0.4': + resolution: {integrity: sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.0.4': + resolution: {integrity: sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.0.4': + resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linux-arm@1.0.5': + resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==} + cpu: [arm] + os: [linux] + + '@img/sharp-libvips-linux-s390x@1.0.4': + resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==} + cpu: [s390x] + os: [linux] + + '@img/sharp-libvips-linux-x64@1.0.4': + resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==} + cpu: [x64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-arm64@1.0.4': + resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-x64@1.0.4': + resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==} + cpu: [x64] + os: [linux] + + '@img/sharp-linux-arm64@0.33.5': + resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linux-arm@0.33.5': + resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] + + '@img/sharp-linux-s390x@0.33.5': + resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [s390x] + os: [linux] + + '@img/sharp-linux-x64@0.33.5': + resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-linuxmusl-arm64@0.33.5': + resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linuxmusl-x64@0.33.5': + resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-wasm32@0.33.5': + resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [wasm32] + + '@img/sharp-win32-ia32@0.33.5': + resolution: {integrity: sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.33.5': + resolution: {integrity: sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] + '@ioredis/commands@1.2.0': resolution: {integrity: sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==} @@ -914,10 +1040,25 @@ packages: resolution: {integrity: sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + '@preact/signals-core@1.8.0': + resolution: {integrity: sha512-OBvUsRZqNmjzCZXWLxkZfhcgT+Fk8DDcT/8vD6a1xhDemodyy87UJRJfASMuSD8FaAIeGgGm85ydXhm7lr4fyA==} + '@remix-run/router@1.20.0': resolution: {integrity: sha512-mUnk8rPJBI9loFDZ+YzPGdeniYK+FTmRD1TMCz7ev2SNIozyKKpnGgsxO34u6Z4z/t0ITuu7voi/AshfsGsgFg==} engines: {node: '>=14.0.0'} + '@rgba-image/common@0.1.13': + resolution: {integrity: sha512-AnOBmBpjSgcymTuVhTGy+RB4FfmEQqR2GeJY3d3xfvR9fl3HfhzwgVqopuh3bKSAT6KRpJr7wNmug0qr3oI7bA==} + + '@rgba-image/copy@0.1.3': + resolution: {integrity: sha512-fscJhpp8YtVELGIwQsv1Pj6BEN4PEWAlMJ6a/HWzYxzVr3y/dut4BUrqeWRKiKeRXAGqaV6QxkBxAgYMQYZEvw==} + + '@rgba-image/create-image@0.1.1': + resolution: {integrity: sha512-ndExUNyi9Ooa/OZqiJS53vYrQ48FX7MDmMrEslDxhsorDsXpeKI9w689r4AYhT9CF9KZlBe8SmI++3BwSvvwAQ==} + + '@rgba-image/lanczos@0.1.1': + resolution: {integrity: sha512-MSGGU7BZmEbg1xHtNp+StARoN7R38zJnFgSEvSzB710nXsHGEaJt//z2VnPfRQTtKSKUXEnp95JSuqDlXTBrYA==} + '@rollup/rollup-android-arm-eabi@4.24.3': resolution: {integrity: sha512-ufb2CH2KfBWPJok95frEZZ82LtDl0A6QKTa8MoM+cWwDZvVGl5/jNb79pIhRvAalUu+7LD91VYR0nwRD799HkQ==} cpu: [arm] @@ -1444,6 +1585,9 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} + browser-image-hash@0.0.5: + resolution: {integrity: sha512-j+rsA1L3vL8k8ji4pFPFAOU/wN/hegwk1eoMshFk3OtjzEzdDrT9Dz94OkLc43NhWGck2a9t5eQQok6zjJSPHQ==} + browserslist@4.24.2: resolution: {integrity: sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} @@ -1569,6 +1713,13 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + color-string@1.9.1: + resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} + + color@4.2.3: + resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} + engines: {node: '>=12.5.0'} + combined-stream@1.0.8: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} @@ -1716,6 +1867,9 @@ packages: supports-color: optional: true + decimal.js@10.4.3: + resolution: {integrity: sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==} + dedent@1.5.3: resolution: {integrity: sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==} peerDependencies: @@ -1754,6 +1908,10 @@ packages: resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + detect-libc@2.0.3: + resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==} + engines: {node: '>=8'} + detect-newline@3.1.0: resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} engines: {node: '>=8'} @@ -1841,6 +1999,9 @@ packages: error-ex@1.3.2: resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} + error-stack-parser@2.1.4: + resolution: {integrity: sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==} + es-define-property@1.0.0: resolution: {integrity: sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==} engines: {node: '>= 0.4'} @@ -2032,6 +2193,9 @@ packages: fb-watchman@2.0.2: resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} + fflate@0.8.2: + resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} + figures@3.2.0: resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==} engines: {node: '>=8'} @@ -2230,6 +2394,9 @@ packages: resolution: {integrity: sha512-qlspKUK7IlSQv2o+5I7yhUd7TxlOG2Vr5LTa3ve2XSNVKAL/n/u/7KLvKmFNimomDIKvZFXWHv0T12mv7rT8Aw==} engines: {node: '>=8'} + howler@2.2.4: + resolution: {integrity: sha512-iARIBPgcQrwtEr+tALF+rapJ8qSc+Set2GJQl7xT1MQzWaVkFebdJhR3alVlSiUf5U7nAANKuj3aWpwerocD5w==} + html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} @@ -2303,6 +2470,9 @@ packages: is-arrayish@0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + is-arrayish@0.3.2: + resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} + is-binary-path@2.1.0: resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} engines: {node: '>=8'} @@ -2896,6 +3066,10 @@ packages: resolution: {integrity: sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + p-map@2.1.0: + resolution: {integrity: sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==} + engines: {node: '>=6'} + p-try@2.2.0: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} engines: {node: '>=6'} @@ -3249,6 +3423,16 @@ packages: setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + sharp-phash@2.2.0: + resolution: {integrity: sha512-/gBoes9EycJkt/2aakKl9BeSGxFbPpZ5lCDtpRRlnU+vkqqcdXi8YLIlXnJsTtTBmaL82zh1bPkV7uZSSIsj8w==} + engines: {node: '>= 10'} + peerDependencies: + sharp: '>= 0.32.0' + + sharp@0.33.5: + resolution: {integrity: sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -3268,6 +3452,9 @@ packages: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} + simple-swizzle@0.2.2: + resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} + sisteransi@1.0.5: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} @@ -3304,6 +3491,10 @@ packages: source-map-support@0.5.21: resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + source-map@0.5.6: + resolution: {integrity: sha512-MjZkVp0NHr5+TPihLcadqnlVoGIoWo4IBHptutGh9wI3ttUYvCG26HkSuDi+K6lsZ25syXJXcctwgyVCt//xqA==} + engines: {node: '>=0.10.0'} + source-map@0.6.1: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} @@ -3319,10 +3510,22 @@ packages: sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + stack-generator@2.0.10: + resolution: {integrity: sha512-mwnua/hkqM6pF4k8SnmZ2zfETsRUpWXREfA/goT8SLCV4iOFa4bzOX2nDipWAZFPTjLvQB82f5yaodMVhK0yJQ==} + stack-utils@2.0.6: resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} engines: {node: '>=10'} + stackframe@1.3.4: + resolution: {integrity: sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==} + + stacktrace-gps@3.1.2: + resolution: {integrity: sha512-GcUgbO4Jsqqg6RxfyTHFiPxdPqF+3LFmQhm7MgCuYQOYuWyqxo5pwRPz5d/u6/WYJdEnWfK4r+jGbyD8TSggXQ==} + + stacktrace-js@2.0.2: + resolution: {integrity: sha512-Je5vBeY4S1r/RnLydLl0TBTi3F2qdfWmYsGvtfZgEI+SCprPppaIhQf5nGcal4gI4cGpCV/duLcAzT1np6sQqg==} + standard-as-callback@2.1.0: resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==} @@ -3637,6 +3840,9 @@ packages: resolution: {integrity: sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==} engines: {node: '>=10.12.0'} + valibot@0.13.1: + resolution: {integrity: sha512-SG2W1RHqE2LShl3p6tyERt6I+G6PQa9ZFVfkyNKXz01HBzL+tBeH5kXw/5AQeAzPJSjI3djVGBl1CyozA1kyBQ==} + vary@1.1.2: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} @@ -3678,6 +3884,9 @@ packages: walker@1.0.8: resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} + wasm-imagemagick@1.2.8: + resolution: {integrity: sha512-V7u80n7g+iAoV7sYgQKGSdG59J6/aSMGO0DDK0zxKnwOGjmVXyjP0yU4tX4cMrfC0t/Wk3I8TX7cmdbFQOYHpg==} + watchpack@2.4.2: resolution: {integrity: sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==} engines: {node: '>=10.13.0'} @@ -3753,6 +3962,9 @@ packages: resolution: {integrity: sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==} engines: {node: '>=0.4.0'} + xstate@4.38.3: + resolution: {integrity: sha512-SH7nAaaPQx57dx6qvfcIgqKRXIh4L0A1iYEqim4s1u7c9VoCgzZc+63FY90AKU4ZzOC2cfJzTnpO4zK7fCUzzw==} + xtend@4.0.2: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} @@ -4183,6 +4395,32 @@ snapshots: dependencies: '@jridgewell/trace-mapping': 0.3.9 + '@dotlottie/common@0.7.11': + dependencies: + '@dotlottie/dotlottie-js': 0.7.2 + '@preact/signals-core': 1.8.0 + howler: 2.2.4 + lottie-web: 5.12.2 + xstate: 4.38.3 + + '@dotlottie/dotlottie-js@0.7.2': + dependencies: + browser-image-hash: 0.0.5 + fflate: 0.8.2 + sharp: 0.33.5 + sharp-phash: 2.2.0(sharp@0.33.5) + valibot: 0.13.1 + + '@dotlottie/react-player@1.6.19(react@18.3.1)': + dependencies: + '@dotlottie/common': 0.7.11 + react: 18.3.1 + + '@emnapi/runtime@1.3.1': + dependencies: + tslib: 2.8.0 + optional: true + '@esbuild/aix-ppc64@0.21.5': optional: true @@ -4333,6 +4571,81 @@ snapshots: '@humanwhocodes/retry@0.3.1': {} + '@img/sharp-darwin-arm64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.0.4 + optional: true + + '@img/sharp-darwin-x64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.0.4 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.0.4': + optional: true + + '@img/sharp-libvips-darwin-x64@1.0.4': + optional: true + + '@img/sharp-libvips-linux-arm64@1.0.4': + optional: true + + '@img/sharp-libvips-linux-arm@1.0.5': + optional: true + + '@img/sharp-libvips-linux-s390x@1.0.4': + optional: true + + '@img/sharp-libvips-linux-x64@1.0.4': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.0.4': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.0.4': + optional: true + + '@img/sharp-linux-arm64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.0.4 + optional: true + + '@img/sharp-linux-arm@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.0.5 + optional: true + + '@img/sharp-linux-s390x@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.0.4 + optional: true + + '@img/sharp-linux-x64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.0.4 + optional: true + + '@img/sharp-linuxmusl-arm64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 + optional: true + + '@img/sharp-linuxmusl-x64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.0.4 + optional: true + + '@img/sharp-wasm32@0.33.5': + dependencies: + '@emnapi/runtime': 1.3.1 + optional: true + + '@img/sharp-win32-ia32@0.33.5': + optional: true + + '@img/sharp-win32-x64@0.33.5': + optional: true + '@ioredis/commands@1.2.0': {} '@isaacs/cliui@8.0.2': @@ -4691,8 +5004,26 @@ snapshots: '@pkgr/core@0.1.1': {} + '@preact/signals-core@1.8.0': {} + '@remix-run/router@1.20.0': {} + '@rgba-image/common@0.1.13': {} + + '@rgba-image/copy@0.1.3': + dependencies: + '@rgba-image/common': 0.1.13 + + '@rgba-image/create-image@0.1.1': + dependencies: + '@rgba-image/common': 0.1.13 + + '@rgba-image/lanczos@0.1.1': + dependencies: + '@rgba-image/common': 0.1.13 + '@rgba-image/copy': 0.1.3 + '@rgba-image/create-image': 0.1.1 + '@rollup/rollup-android-arm-eabi@4.24.3': optional: true @@ -5358,6 +5689,12 @@ snapshots: dependencies: fill-range: 7.1.1 + browser-image-hash@0.0.5: + dependencies: + '@rgba-image/lanczos': 0.1.1 + decimal.js: 10.4.3 + wasm-imagemagick: 1.2.8 + browserslist@4.24.2: dependencies: caniuse-lite: 1.0.30001676 @@ -5469,6 +5806,16 @@ snapshots: color-name@1.1.4: {} + color-string@1.9.1: + dependencies: + color-name: 1.1.4 + simple-swizzle: 0.2.2 + + color@4.2.3: + dependencies: + color-convert: 2.0.1 + color-string: 1.9.1 + combined-stream@1.0.8: dependencies: delayed-stream: 1.0.0 @@ -5605,6 +5952,8 @@ snapshots: dependencies: ms: 2.1.3 + decimal.js@10.4.3: {} + dedent@1.5.3: {} deep-is@0.1.4: {} @@ -5629,6 +5978,8 @@ snapshots: destroy@1.2.0: {} + detect-libc@2.0.3: {} + detect-newline@3.1.0: {} dezalgo@1.0.4: @@ -5716,6 +6067,10 @@ snapshots: dependencies: is-arrayish: 0.2.1 + error-stack-parser@2.1.4: + dependencies: + stackframe: 1.3.4 + es-define-property@1.0.0: dependencies: get-intrinsic: 1.2.4 @@ -6009,6 +6364,8 @@ snapshots: dependencies: bser: 2.1.1 + fflate@0.8.2: {} + figures@3.2.0: dependencies: escape-string-regexp: 1.0.5 @@ -6224,6 +6581,8 @@ snapshots: hexoid@2.0.0: {} + howler@2.2.4: {} + html-escaper@2.0.2: {} http-errors@2.0.0: @@ -6323,6 +6682,8 @@ snapshots: is-arrayish@0.2.1: {} + is-arrayish@0.3.2: {} + is-binary-path@2.1.0: dependencies: binary-extensions: 2.3.0 @@ -7033,6 +7394,8 @@ snapshots: dependencies: p-limit: 4.0.0 + p-map@2.1.0: {} + p-try@2.2.0: {} package-json-from-dist@1.0.1: {} @@ -7384,6 +7747,36 @@ snapshots: setprototypeof@1.2.0: {} + sharp-phash@2.2.0(sharp@0.33.5): + dependencies: + sharp: 0.33.5 + + sharp@0.33.5: + dependencies: + color: 4.2.3 + detect-libc: 2.0.3 + semver: 7.6.3 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.33.5 + '@img/sharp-darwin-x64': 0.33.5 + '@img/sharp-libvips-darwin-arm64': 1.0.4 + '@img/sharp-libvips-darwin-x64': 1.0.4 + '@img/sharp-libvips-linux-arm': 1.0.5 + '@img/sharp-libvips-linux-arm64': 1.0.4 + '@img/sharp-libvips-linux-s390x': 1.0.4 + '@img/sharp-libvips-linux-x64': 1.0.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 + '@img/sharp-libvips-linuxmusl-x64': 1.0.4 + '@img/sharp-linux-arm': 0.33.5 + '@img/sharp-linux-arm64': 0.33.5 + '@img/sharp-linux-s390x': 0.33.5 + '@img/sharp-linux-x64': 0.33.5 + '@img/sharp-linuxmusl-arm64': 0.33.5 + '@img/sharp-linuxmusl-x64': 0.33.5 + '@img/sharp-wasm32': 0.33.5 + '@img/sharp-win32-ia32': 0.33.5 + '@img/sharp-win32-x64': 0.33.5 + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 @@ -7401,6 +7794,10 @@ snapshots: signal-exit@4.1.0: {} + simple-swizzle@0.2.2: + dependencies: + is-arrayish: 0.3.2 + sisteransi@1.0.5: {} slash@3.0.0: {} @@ -7472,6 +7869,8 @@ snapshots: buffer-from: 1.1.2 source-map: 0.6.1 + source-map@0.5.6: {} + source-map@0.6.1: {} source-map@0.7.4: {} @@ -7480,10 +7879,27 @@ snapshots: sprintf-js@1.0.3: {} + stack-generator@2.0.10: + dependencies: + stackframe: 1.3.4 + stack-utils@2.0.6: dependencies: escape-string-regexp: 2.0.0 + stackframe@1.3.4: {} + + stacktrace-gps@3.1.2: + dependencies: + source-map: 0.5.6 + stackframe: 1.3.4 + + stacktrace-js@2.0.2: + dependencies: + error-stack-parser: 2.1.4 + stack-generator: 2.0.10 + stacktrace-gps: 3.1.2 + standard-as-callback@2.1.0: {} statuses@2.0.1: {} @@ -7797,6 +8213,8 @@ snapshots: '@types/istanbul-lib-coverage': 2.0.6 convert-source-map: 2.0.0 + valibot@0.13.1: {} + vary@1.1.2: {} vite-plugin-remove-console@2.2.0: {} @@ -7815,6 +8233,11 @@ snapshots: dependencies: makeerror: 1.0.12 + wasm-imagemagick@1.2.8: + dependencies: + p-map: 2.1.0 + stacktrace-js: 2.0.2 + watchpack@2.4.2: dependencies: glob-to-regexp: 0.4.1 @@ -7900,6 +8323,8 @@ snapshots: xmlhttprequest-ssl@2.1.2: {} + xstate@4.38.3: {} + xtend@4.0.2: {} y18n@5.0.8: {} From 2caeecd436ca93455f5de3c94b4cb9243ccc2dcf Mon Sep 17 00:00:00 2001 From: JeongwooSeo Date: Mon, 18 Nov 2024 22:41:29 +0900 Subject: [PATCH 018/180] =?UTF-8?q?chore:=20=ED=94=84=EB=A1=9C=EC=A0=9D?= =?UTF-8?q?=ED=8A=B8=EC=97=90=EC=84=9C=20lottie=20=ED=99=95=EC=9E=A5?= =?UTF-8?q?=EC=9E=90=EB=A5=BC=20=EC=9D=B8=EC=8B=9D=ED=95=A0=20=EC=88=98=20?= =?UTF-8?q?=EC=9E=88=EB=8F=84=EB=A1=9D=20=ED=83=80=EC=9E=85=20=EB=B0=8F=20?= =?UTF-8?q?assets=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/public/assets/noondeumyum.lottie | Bin 0 -> 4436 bytes frontend/src/types/declarations.d.ts | 4 ++++ frontend/tsconfig.app.json | 1 + frontend/vite.config.ts | 1 + 4 files changed, 6 insertions(+) create mode 100644 frontend/public/assets/noondeumyum.lottie create mode 100644 frontend/src/types/declarations.d.ts diff --git a/frontend/public/assets/noondeumyum.lottie b/frontend/public/assets/noondeumyum.lottie new file mode 100644 index 0000000000000000000000000000000000000000..d83a4ffc67a7718835d47d2e7c67a29b68349a90 GIT binary patch literal 4436 zcmbVQcQl+`w;wWkL>r>Fi0D09f~ZkP8AS9p$|y6&AVCnLh3K6Z(M1~sQKCztcSe-x zQG!gO1R=hW_q*S_?ppV*b=N(=_3Y=Iz0WS^_ut+II@f?8z|}%HBOkLy_}2jm0D!BH z60h^H^K^BBdZR>O-f+(VZeJ+U+ZFD~Ezd13BE~JmjfDET{;d!f;&y{R^@AfFz42DO z$<7Dm0!QNax^NWA6{_Lt4)w;XouQskq#f#y#SxDB+b9gXGT~Es*rD*@@ewQnxLqCb z&W>VsP7cyCPQntB4wAxB5@J%qva(P+VR2b|$-B_I4t6q<4tQ5bS0vQo%9UFlUx+sX z3dK9)cL;a8r+RQlC_a)W9O+@_j=$>;hau%x=8=fB~9t1!AS)GXHMx91GQh5C7l>W}*GpV3;qD9C!)F==DDR3$yQwZnf#-+bxS z@oAy*x4(C*`DoCIS6VMOSpD#Ryl48-qxi2|J3Wy4Q<0d&;>*J(bIYYAg`o)`?Myr$ zZLqXN&xI-~_4bJunuF)&s*)%iThT6Xsmz3OZvMTAyl7}`dzapC!}X0n$)`st1#oI2 zj@fC-xWi$HyX2xubvxJ=B2+oLl!s_#A_n_XImwRZOi( z7qsUuiyY%F&*KXui2M(XdZ8v@4sC((9r4hEv%w#B(;X?}7@&2GTKxE;%)+k~(cm+$ z66)NGz3;!@k~lUPrf-ZEx;?g8;F(H{2D=Q}1b@Pu=e@Rm6E3`2y76UD_YmmL{-CRag6571SZPhg_&}Xjrul?aHssSXS*G%0Y5l(Pg8J`E z!{6bWtJ*0|cYe_R%xcmybcv`u99uSWF6o|T^I5yqrcswYFRq978*MjJ3~X*DHdlhP zf9t3=Z<uNSRx=yqW*z9A5IKWFFg4$ZS4 zn2#*}YR9A(x#N7+em#tfwaC#^VD(?%Dy_z;NlXMLM>q1#VGxzy8*lNw?^3Cw^R5?% zU@QBzit%Zo?-8WBu*;yT(gj9aO#E}@*EpczQZp)Tg0v`_uL`U(2 zZ4+AVqIcmaV24LDqkwY7XrlIj{D;+y%&>oh2as%g(muqqouI|HQj`hQe!-XhH20(W zgjjmmGOb~+IzeCUw8`3xijAl-;UxLwK(Cg`usg|tiejRvOB^G(($&md?)r1By(P?% zMSbXsZyY)KjWdD7(KiwdJ|p$WFo!Jh2&M%ScQhnRDqFO?sgGGpCNAuFuEkMWUV!C= z1bC3bmd6zJIlch>{7GCic&66}GMxVn6i&jDqT^VE6_6iu`9V%hsOcFjo>$}BGdKL+ zMW!bB-b0aoq9#XcXpNc!OZ|gQ(Ha_^H!8dpHoCX3a8vd6(I+FVYpP$$qty*1(}9LM z+GQbP@@c^ceXDXoUV1!IW4P$Ik={W?q32$i%BuX!id~ho+Nz1!mJ*LI>?Q2*Xl9f! z{4|=VHz13CPKyG`u8C)d+0r$?zYUpgzsZp!mMxZT3CiBx=(zknTlBhi(q!nX6!=xX zSZ0`*EQ43>G-5YBxVS%Edz@@Y=nCmQLIJ1P7_K)4-CB}5i65BWakH2`+1O>+e)0I# zvrbV>0j|s+L29I>FYOza&v~@GD1s$+eO!;bc+Jeekp48{)!Uc|$tn^4A(hAo9)i)t)J@VMR&ByM?EbQXfL2%yczrW0KXzOmcLPTGU}Q z#{g2p64B%m9?{E6-T+B|@|mC*(D(=+0Kf=-;9czgf=3YHU_Ip3Y7DyLw?{oIPM0#9 zni+ywE$2GZF1{;eSRvuUA^_*B8!}385ugNdW|hOI*;7+T?RC;rYF?Lf11wZFLd`%v z>2>6&7y-)Y;z$$FHA;%a)({LszoAbWrH5e7s(gHMLGiBewQq!7Ck%x0&xGB`3r3&k zl&uPs8X(pevJz^;6DkNChm?K8o)ibhyU2t*v0B#_uUh4O?G==Ed&jG)%-)0xV|aUf z=-$(&s&7Vx#E)*zaLzUmfBAvrWd1ZD^hmpQSC(g8TT6t(8iLCT#e&l7OJ)V%_58Ac zNU1MGneX(n3iM&KLK$s9+Wa#+usAl7bAFz;p^eHs%Vp8BRZ$hp>4$f|Jz-}JFEpM= z;-|>Pc-55Ox=wEZqgi1H6XrLe$EHcYtjfB(EH;6Ll#}V5t2K^g3l<=Dqd>i*h%eLC zZAI$nQ)c?*ROHvlOk4&YB9XXb^84cJt&q}tjQ6F@rYMG7)p-^4Gr!%(vH9{$QJDdh zA0{bLh-E}id6f9dL6}JLP_Sa`dsoU2YA~Z4DT~L6?J4bBij)H0{0(o5jC(cSf1MBp z3%vrO$t4oXjO8TNxVexY=whc}5MTn_)&N~#LR=&5#Rub)tLtnGnMf&KT~LfCH1y${ zXo{!aD3a3AYe`Ng8@rdxm%JrKo~a$0%zks2G-kO6#3hGfrT{-V3I70xxZLEw#>bd^ zDwK1P+5Sldy_OSPr*11#3CLm3*7ge;S8I2q#f{_y0#2Gcb0{S06ns%hIku&2vv(;` z2v`^{%qx6-v`kyBj@z;tTM=tc(htt3tsEV{uIT9~D(h@qKMpsp*=;f5bLAZ)~Lu$hbH!)CM7`H!Xi5~i|z2UESN*G=0Nz?!0s zRZ%3hQpJqO4aVuv=}xS~O~rW_w~))^2<7)s*b1Aq-JzRVU~C*^82p0xq?>rh1D8r? zXAlU3^=2+Z)aTkFqbl&P`oY zPLe$Hno~}?L-#nmOk7hsooH$B{5|mF?xzPkspcnQaXDX^j9NCTO@=e z@PLAyVy3Rsl0L!b8LU>S`GLVeed=>si%qj3{!0c@X=%xO{A~-3yo&4Oj3+Fiv6RDI z1B1>EdM_!bp+E;JL5z4{`1l(;f;8usb~O7Y8Ly~W*jO0s1Ltl@^_F~~y|FsAYi99W zY-N#%K5d}1K>2!=@eRw5dhr!QP3q? z47cm7^+@;+ah@nSfKQj8n(WDR>kiu%MD~c2^QdN zEGaf-MChsCW^K_(C&pWo$J~W1xwZE}?_=!Z@3iK= zkk$_p&b|NXNVop!54Z2@T6)vc`hO~(BXpzKEE1OO)*PzSQS{_Q(_rncjae~j1N#;jOF|2@U>1@ zjsUQGdTYS8sYRbI4qf4sLQVYWr(7smIUT2>>ary8kC-7h6MPbDx*XLYdCU2$j)Lj~ z%T!wQtWQKpYia`|DxplmZeq7Eyd+8@T1tK@%GR29o#B%sUV!Z8v45v@Ws zjW}kHd<5y_W6~VO1q@-m=(VRQJ4Eh_y37d&=e> zT9Uqed+$>*jZCuUif@Ck#)X>4pQk2{KyQoy4+4UBO!>mQz(>_0S~LC0H0&nVb%W=- zCK^)br#DFjLdV~8nE;)QF@>*5@5xZb6?13g1VlxLG~iyBkp?lqtXZwvo&W-nfMZZz zf4p1$9THom@0s_TH3E$E8ocWtBQKG)5uME4a;~ILM6GA2MflliL+dv0t#Xaav>*B# zj7us$Rl!1>*o7q?%KkMTqG_(y(KPLY1ainw^cr?B2Q|r|lw5{B^S&YOi;<;`|O|Po|C~%MQVLon=W~~qCM`Ey>+qqBn zUqZge4Hfa_?oxXP>1G(v=G{EaGAS_&QF}SuK&n0Q4qb_!qPBo8(T7{C4Jv2h$`(`{ z_JbEHtf%g;2NmpCmbTrh4H0^mS9#e$_`DmhsBhRnj;=>x=G9lsPRvEfyKp|g_{^5~ z0ns_2S*_=Pgb(oyll76IwA{=mo%|dkYM(&r&%M@EDOJk;6{2Nf{B-fOnUiEzA2&-} z-y1kRirH2X)G3=Z_+TTLV@+Yv2ex*N?SefkmS?Ok`2-=g6}AaC=*xR}DJ0uH|8Wz=&pZA< W&AWjP5%E Date: Mon, 18 Nov 2024 22:41:42 +0900 Subject: [PATCH 019/180] =?UTF-8?q?feat:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20UI=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/pages/LoginPage.tsx | 110 +++++++++++++++++++++++++++++++ frontend/src/routes.tsx | 3 +- 2 files changed, 112 insertions(+), 1 deletion(-) create mode 100644 frontend/src/pages/LoginPage.tsx diff --git a/frontend/src/pages/LoginPage.tsx b/frontend/src/pages/LoginPage.tsx new file mode 100644 index 00000000..0f6a99fc --- /dev/null +++ b/frontend/src/pages/LoginPage.tsx @@ -0,0 +1,110 @@ +import { FaGithub, FaGoogle } from "react-icons/fa"; +import { DotLottiePlayer } from "@dotlottie/react-player"; + +import mainSnowman from "../../public/assets/noondeumyum.lottie"; + +const LoginPage = () => { + return ( +
+ {/* 전체 컨테이너 */} +
+ {/* 전체 콘텐츠를 감싸는 프레임 */} +
+ {/* 내부 콘텐츠 그리드 */} +
+ {/* 왼쪽 콘텐츠 영역 - 7칸 차지 */} +
+
+ +
+
+ + {/* 오른쪽 로그인 폼 - 5칸 차지 */} +
+

+ Preview +

+
+
+
+ +
+ +
+ +
+ + + +
+ + +
+ {/* 구분선 */} +
+
+
+
+
+ + 또는 + +
+
+ {/* OAuth 로그인 버튼 */} + + +
+
+
+
+
+
+
+ ); +}; + +export default LoginPage; diff --git a/frontend/src/routes.tsx b/frontend/src/routes.tsx index 72dc825c..bbf815be 100644 --- a/frontend/src/routes.tsx +++ b/frontend/src/routes.tsx @@ -3,6 +3,7 @@ import CreateSessionPage from "./pages/CreateSessionPage.tsx"; import SessionListPage from "./pages/SessionListPage.tsx"; import SessionPage from "./pages/SessionPage"; import ErrorPage from "@/pages/ErrorPage.tsx"; +import LoginPage from "@/pages/LoginPage.tsx"; export const routes = [ { @@ -18,7 +19,7 @@ export const routes = [ path: "/sessions", }, { - element: <>로그인 페이지, + element: , path: "/login", }, { From 70ab941e52e1d3f2b70bc72f8425308f587506a3 Mon Sep 17 00:00:00 2001 From: JeongwooSeo Date: Mon, 18 Nov 2024 23:17:30 +0900 Subject: [PATCH 020/180] =?UTF-8?q?chore:=20index.html=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=ED=83=80=EC=9D=B4=ED=8B=80=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/index.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/index.html b/frontend/index.html index e4b78eae..234dd5e9 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1,10 +1,10 @@ - + - Vite + React + TS + 면접 스터디 Preview
From ded03b78541ec26ca806f549028d4f7f70047ef1 Mon Sep 17 00:00:00 2001 From: JeongwooSeo Date: Mon, 18 Nov 2024 23:27:52 +0900 Subject: [PATCH 021/180] =?UTF-8?q?feat:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=95=A0=EB=8B=88=EB=A9=94?= =?UTF-8?q?=EC=9D=B4=EC=85=98=20=EC=84=B9=EC=85=98,=20=EB=B0=98=EC=9D=91?= =?UTF-8?q?=ED=98=95=EC=9C=BC=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 화면 너비 작아지면 애니메이션 섹션 hidden --- frontend/src/pages/LoginPage.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/pages/LoginPage.tsx b/frontend/src/pages/LoginPage.tsx index 0f6a99fc..5f8b2c14 100644 --- a/frontend/src/pages/LoginPage.tsx +++ b/frontend/src/pages/LoginPage.tsx @@ -13,7 +13,7 @@ const LoginPage = () => { {/* 내부 콘텐츠 그리드 */}
{/* 왼쪽 콘텐츠 영역 - 7칸 차지 */} -
+
{
{/* 오른쪽 로그인 폼 - 5칸 차지 */} -
+

Preview

From 6d053e6b268bdf8a4809c709fd3d0f0e2d29eb3e Mon Sep 17 00:00:00 2001 From: Chanwoo Kim Date: Mon, 18 Nov 2024 23:46:30 +0900 Subject: [PATCH 022/180] =?UTF-8?q?chore:=20`OAuth`=EC=97=90=20=ED=95=84?= =?UTF-8?q?=EC=9A=94=ED=95=9C=20=ED=8C=A8=ED=82=A4=EC=A7=80=20=EC=84=A4?= =?UTF-8?q?=EC=B9=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: twalla26 --- backend/package.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/backend/package.json b/backend/package.json index a3918ab8..96d9b5dc 100644 --- a/backend/package.json +++ b/backend/package.json @@ -22,11 +22,14 @@ "dependencies": { "@nestjs/common": "^10.0.0", "@nestjs/core": "^10.0.0", + "@nestjs/passport": "^10.0.3", "@nestjs/platform-express": "^10.0.0", "@nestjs/platform-socket.io": "^10.4.6", "@nestjs/websockets": "^10.4.6", "dotenv": "^16.4.5", "ioredis": "^5.4.1", + "passport": "^0.7.0", + "passport-github": "^1.1.0", "reflect-metadata": "^0.2.0", "rxjs": "^7.8.1", "socket.io": "^4.8.1" @@ -38,6 +41,7 @@ "@types/express": "^5.0.0", "@types/jest": "^29.5.2", "@types/node": "^20.3.1", + "@types/passport-github": "^1.1.12", "@types/supertest": "^6.0.0", "@typescript-eslint/eslint-plugin": "^8.0.0", "@typescript-eslint/parser": "^8.0.0", From 2ae27ba9acb61336cc058c6795b19d6d92d9a8a5 Mon Sep 17 00:00:00 2001 From: Chanwoo Kim Date: Mon, 18 Nov 2024 23:47:30 +0900 Subject: [PATCH 023/180] =?UTF-8?q?chore:=20`TypeORM`=EA=B3=BC=20=ED=8A=B8?= =?UTF-8?q?=EB=9E=9C=EC=9E=AD=EC=85=98=EC=97=90=20=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=ED=8C=A8=ED=82=A4=EC=A7=80=20=EC=84=A4=EC=B9=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: twalla26 --- backend/package.json | 7 +- pnpm-lock.yaml | 492 ++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 496 insertions(+), 3 deletions(-) diff --git a/backend/package.json b/backend/package.json index 96d9b5dc..66cc4b1f 100644 --- a/backend/package.json +++ b/backend/package.json @@ -25,14 +25,19 @@ "@nestjs/passport": "^10.0.3", "@nestjs/platform-express": "^10.0.0", "@nestjs/platform-socket.io": "^10.4.6", + "@nestjs/typeorm": "^10.0.2", "@nestjs/websockets": "^10.4.6", "dotenv": "^16.4.5", "ioredis": "^5.4.1", + "mysql2": "^3.11.4", "passport": "^0.7.0", "passport-github": "^1.1.0", "reflect-metadata": "^0.2.0", "rxjs": "^7.8.1", - "socket.io": "^4.8.1" + "socket.io": "^4.8.1", + "typeorm": "^0.3.20", + "typeorm-naming-strategies": "^4.1.0", + "typeorm-transactional": "^0.5.0" }, "devDependencies": { "@nestjs/cli": "^10.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 74f0d6cf..4ffe95ae 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -26,12 +26,18 @@ importers: '@nestjs/core': specifier: ^10.0.0 version: 10.4.6(@nestjs/common@10.4.6(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.6)(@nestjs/websockets@10.4.7)(reflect-metadata@0.2.2)(rxjs@7.8.1) + '@nestjs/passport': + specifier: ^10.0.3 + version: 10.0.3(@nestjs/common@10.4.6(reflect-metadata@0.2.2)(rxjs@7.8.1))(passport@0.7.0) '@nestjs/platform-express': specifier: ^10.0.0 version: 10.4.6(@nestjs/common@10.4.6(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.6) '@nestjs/platform-socket.io': specifier: ^10.4.6 version: 10.4.7(@nestjs/common@10.4.6(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/websockets@10.4.7)(rxjs@7.8.1) + '@nestjs/typeorm': + specifier: ^10.0.2 + version: 10.0.2(@nestjs/common@10.4.6(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.6(@nestjs/common@10.4.6(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.6)(@nestjs/websockets@10.4.7)(reflect-metadata@0.2.2)(rxjs@7.8.1))(reflect-metadata@0.2.2)(rxjs@7.8.1)(typeorm@0.3.20(ioredis@5.4.1)(mysql2@3.11.4)(ts-node@10.9.2(@types/node@20.17.4)(typescript@5.6.3))) '@nestjs/websockets': specifier: ^10.4.6 version: 10.4.7(@nestjs/common@10.4.6(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.6)(@nestjs/platform-socket.io@10.4.7)(reflect-metadata@0.2.2)(rxjs@7.8.1) @@ -41,6 +47,15 @@ importers: ioredis: specifier: ^5.4.1 version: 5.4.1 + mysql2: + specifier: ^3.11.4 + version: 3.11.4 + passport: + specifier: ^0.7.0 + version: 0.7.0 + passport-github: + specifier: ^1.1.0 + version: 1.1.0 reflect-metadata: specifier: ^0.2.0 version: 0.2.2 @@ -50,6 +65,15 @@ importers: socket.io: specifier: ^4.8.1 version: 4.8.1 + typeorm: + specifier: ^0.3.20 + version: 0.3.20(ioredis@5.4.1)(mysql2@3.11.4)(ts-node@10.9.2(@types/node@20.17.4)(typescript@5.6.3)) + typeorm-naming-strategies: + specifier: ^4.1.0 + version: 4.1.0(typeorm@0.3.20(ioredis@5.4.1)(mysql2@3.11.4)(ts-node@10.9.2(@types/node@20.17.4)(typescript@5.6.3))) + typeorm-transactional: + specifier: ^0.5.0 + version: 0.5.0(reflect-metadata@0.2.2)(typeorm@0.3.20(ioredis@5.4.1)(mysql2@3.11.4)(ts-node@10.9.2(@types/node@20.17.4)(typescript@5.6.3))) devDependencies: '@nestjs/cli': specifier: ^10.0.0 @@ -59,7 +83,7 @@ importers: version: 10.2.3(chokidar@3.6.0)(typescript@5.6.3) '@nestjs/testing': specifier: ^10.0.0 - version: 10.4.6(@nestjs/common@10.4.6(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.6)(@nestjs/platform-express@10.4.6) + version: 10.4.6(@nestjs/common@10.4.6(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.6(@nestjs/common@10.4.6(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.6)(@nestjs/websockets@10.4.7)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.6(@nestjs/common@10.4.6(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.6)) '@types/express': specifier: ^5.0.0 version: 5.0.0 @@ -69,6 +93,9 @@ importers: '@types/node': specifier: ^20.3.1 version: 20.17.4 + '@types/passport-github': + specifier: ^1.1.12 + version: 1.1.12 '@types/supertest': specifier: ^6.0.0 version: 6.0.2 @@ -846,6 +873,12 @@ packages: '@nestjs/websockets': optional: true + '@nestjs/passport@10.0.3': + resolution: {integrity: sha512-znJ9Y4S8ZDVY+j4doWAJ8EuuVO7SkQN3yOBmzxbGaXbvcSwFDAdGJ+OMCg52NdzIO4tQoN4pYKx8W6M0ArfFRQ==} + peerDependencies: + '@nestjs/common': ^8.0.0 || ^9.0.0 || ^10.0.0 + passport: ^0.4.0 || ^0.5.0 || ^0.6.0 || ^0.7.0 + '@nestjs/platform-express@10.4.6': resolution: {integrity: sha512-HcyCpAKccAasrLSGRTGWv5BKRs0rwTIFOSsk6laNyqfqvgvYcJQAedarnm4jmaemtmSJ0PFI9PmtEZADd2ahCg==} peerDependencies: @@ -877,6 +910,15 @@ packages: '@nestjs/platform-express': optional: true + '@nestjs/typeorm@10.0.2': + resolution: {integrity: sha512-H738bJyydK4SQkRCTeh1aFBxoO1E9xdL/HaLGThwrqN95os5mEyAtK7BLADOS+vldP4jDZ2VQPLj4epWwRqCeQ==} + peerDependencies: + '@nestjs/common': ^8.0.0 || ^9.0.0 || ^10.0.0 + '@nestjs/core': ^8.0.0 || ^9.0.0 || ^10.0.0 + reflect-metadata: ^0.1.13 || ^0.2.0 + rxjs: ^7.2.0 + typeorm: ^0.3.0 + '@nestjs/websockets@10.4.7': resolution: {integrity: sha512-ajuoptYLYm+l3+KtaA9Ed+cO9yB34PtBE8UObavRT8Euh/f7QfeJiKcrU3+BQSAiTWM3nF2qfuV4CfEkP9uKuw==} peerDependencies: @@ -1020,6 +1062,9 @@ packages: '@socket.io/component-emitter@3.1.2': resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==} + '@sqltools/formatter@1.2.5': + resolution: {integrity: sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==} + '@tsconfig/node10@1.0.11': resolution: {integrity: sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==} @@ -1047,6 +1092,9 @@ packages: '@types/body-parser@1.19.5': resolution: {integrity: sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==} + '@types/cls-hooked@4.3.9': + resolution: {integrity: sha512-CMtHMz6Q/dkfcHarq9nioXH8BDPP+v5xvd+N90lBQ2bdmu06UvnLDqxTKoOJzz4SzIwb/x9i4UXGAAcnUDuIvg==} + '@types/connect@3.4.38': resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} @@ -1101,6 +1149,18 @@ packages: '@types/node@20.17.4': resolution: {integrity: sha512-Fi1Bj8qTJr4f1FDdHFR7oMlOawEYSzkHNdBJK+aRjcDDNHwEV3jPPjuZP2Lh2QNgXeqzM8Y+U6b6urKAog2rZw==} + '@types/oauth@0.9.6': + resolution: {integrity: sha512-H9TRCVKBNOhZZmyHLqFt9drPM9l+ShWiqqJijU1B8P3DX3ub84NjxDuy+Hjrz+fEca5Kwip3qPMKNyiLgNJtIA==} + + '@types/passport-github@1.1.12': + resolution: {integrity: sha512-VJpMEIH+cOoXB694QgcxuvWy2wPd1Oq3gqrg2Y9DMVBYs9TmH9L14qnqPDZsNMZKBDH+SvqRsGZj9SgHYeDgcA==} + + '@types/passport-oauth2@1.4.17': + resolution: {integrity: sha512-ODiAHvso6JcWJ6ZkHHroVp05EHGhqQN533PtFNBkg8Fy5mERDqsr030AX81M0D69ZcaMvhF92SRckEk2B0HYYg==} + + '@types/passport@1.0.17': + resolution: {integrity: sha512-aciLyx+wDwT2t2/kJGJR2AEeBz0nJU4WuRX04Wu9Dqc5lSUtwu0WERPHYsLhF9PtseiAMPBGNUOtFjxZ56prsg==} + '@types/prop-types@15.7.13': resolution: {integrity: sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA==} @@ -1345,6 +1405,10 @@ packages: resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} engines: {node: '>= 8'} + app-root-path@3.1.0: + resolution: {integrity: sha512-biN3PwB2gUtjaYy/isrU3aNWI5w+fAfvHkSvCKeQGxhmYpwKFUxudR3Yya+KqVRHBmEDYh+/lTozYCFbmzX4nA==} + engines: {node: '>= 6.0.0'} + append-field@1.0.0: resolution: {integrity: sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==} @@ -1372,6 +1436,10 @@ packages: asap@2.0.6: resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} + async-hook-jl@1.7.6: + resolution: {integrity: sha512-gFaHkFfSxTjvoxDMYqDuGHlcRyUuamF8s+ZTtJdDzqjws4mCt7v0vuV79/E2Wr2/riMQgtG4/yUtXWs1gZ7JMg==} + engines: {node: ^4.7 || >=6.9 || >=7.3} + async@3.2.6: resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} @@ -1385,6 +1453,10 @@ packages: peerDependencies: postcss: ^8.1.0 + aws-ssl-profiles@1.1.2: + resolution: {integrity: sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==} + engines: {node: '>= 6.0.0'} + babel-jest@29.7.0: resolution: {integrity: sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -1423,6 +1495,10 @@ packages: resolution: {integrity: sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==} engines: {node: ^4.5.0 || >= 5.9} + base64url@3.0.1: + resolution: {integrity: sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==} + engines: {node: '>=6.0.0'} + binary-extensions@2.3.0: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} @@ -1462,6 +1538,9 @@ packages: buffer@5.7.1: resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + buffer@6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + busboy@1.6.0: resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} engines: {node: '>=10.16.0'} @@ -1527,6 +1606,11 @@ packages: resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} engines: {node: '>=8'} + cli-highlight@2.1.11: + resolution: {integrity: sha512-9KDcoEVwyUXrjcJNvHD0NFc/hiwe/WPVYIleQh2O1N2Zro5gWJZ/K+3DGn8w8P/F6FxOgzyC5bxDyHIgCSPhGg==} + engines: {node: '>=8.0.0', npm: '>=5.0.0'} + hasBin: true + cli-spinners@2.9.2: resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} engines: {node: '>=6'} @@ -1543,6 +1627,9 @@ packages: resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} engines: {node: '>= 12'} + cliui@7.0.4: + resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} + cliui@8.0.1: resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} engines: {node: '>=12'} @@ -1551,6 +1638,10 @@ packages: resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} engines: {node: '>=0.8'} + cls-hooked@4.2.2: + resolution: {integrity: sha512-J4Xj5f5wq/4jAvcdgoGsL3G103BtWpZrMo8NEinRltN+xpTZdI+M38pyQqhuFU/P792xkMFvnKSf+Lm81U1bxw==} + engines: {node: ^4.7 || >=6.9 || >=7.3 || >=8.2.1} + cluster-key-slot@1.1.2: resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==} engines: {node: '>=0.10.0'} @@ -1699,6 +1790,9 @@ packages: resolution: {integrity: sha512-wAV9QHOsNbwnWdNW2FYvE1P56wtgSbM+3SZcdGiWQILwVjACCXDCI3Ai8QlCjMDB8YK5zySiXZYBiwGmNY3lnw==} engines: {node: '>=12'} + dayjs@1.11.13: + resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==} + debug@2.6.9: resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} peerDependencies: @@ -1801,6 +1895,9 @@ packages: electron-to-chromium@1.5.49: resolution: {integrity: sha512-ZXfs1Of8fDb6z7WEYZjXpgIRF6MEu8JdeGA0A40aZq6OQbS+eJpnnV49epZRna2DU/YsEjSQuGtQPPtvt6J65A==} + emitter-listener@1.1.2: + resolution: {integrity: sha512-Bt1sBAGFHY9DKY+4/2cV6izcKJUf5T7/gkdmkxzX/qv9CcGH8xSwVRW5mtX03SWJtRTWSOpzCuWN9rBFYZepZQ==} + emittery@0.13.1: resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==} engines: {node: '>=12'} @@ -2125,6 +2222,9 @@ packages: function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + generate-function@2.3.1: + resolution: {integrity: sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==} + gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} @@ -2230,6 +2330,9 @@ packages: resolution: {integrity: sha512-qlspKUK7IlSQv2o+5I7yhUd7TxlOG2Vr5LTa3ve2XSNVKAL/n/u/7KLvKmFNimomDIKvZFXWHv0T12mv7rT8Aw==} engines: {node: '>=8'} + highlight.js@10.7.3: + resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==} + html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} @@ -2250,6 +2353,10 @@ packages: resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} engines: {node: '>=0.10.0'} + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} @@ -2343,6 +2450,9 @@ packages: resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} engines: {node: '>=8'} + is-property@1.0.2: + resolution: {integrity: sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==} + is-stream@2.0.1: resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} engines: {node: '>=8'} @@ -2668,6 +2778,9 @@ packages: resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} engines: {node: '>=10'} + long@5.2.3: + resolution: {integrity: sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==} + loose-envify@1.4.0: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true @@ -2681,6 +2794,14 @@ packages: lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + lru-cache@7.18.3: + resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} + engines: {node: '>=12'} + + lru.min@1.1.1: + resolution: {integrity: sha512-FbAj6lXil6t8z4z3j0E5mfRlPzxkySotzUHwRXjlpRh10vc6AI6WN62ehZj82VG7M20rqogJ0GLwar2Xa05a8Q==} + engines: {bun: '>=1.0.0', deno: '>=1.30.0', node: '>=8.0.0'} + magic-string@0.30.8: resolution: {integrity: sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==} engines: {node: '>=12'} @@ -2769,6 +2890,11 @@ packages: resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} hasBin: true + mkdirp@2.1.6: + resolution: {integrity: sha512-+hEnITedc8LAtIP9u3HJDFIdcLV2vXP33sqLLIzkv1Db1zO/1OxbvYf0Y1OC/S/Qo5dxHXepofhmxL02PsKe+A==} + engines: {node: '>=10'} + hasBin: true + ms@2.0.0: resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} @@ -2786,9 +2912,17 @@ packages: resolution: {integrity: sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + mysql2@3.11.4: + resolution: {integrity: sha512-Z2o3tY4Z8EvSRDwknaC40MdZ3+m0sKbpnXrShQLdxPrAvcNli7jLrD2Zd2IzsRMw4eK9Yle500FDmlkIqp+krg==} + engines: {node: '>= 8.0'} + mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + named-placeholders@1.1.3: + resolution: {integrity: sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==} + engines: {node: '>=12.0.0'} + nanoid@3.3.7: resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -2837,6 +2971,9 @@ packages: resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} engines: {node: '>=8'} + oauth@0.10.0: + resolution: {integrity: sha512-1orQ9MT1vHFGQxhuy7E/0gECD3fd2fCC+PIX+/jgmU/gI3EpRocXtmtvxCO5x3WZ443FLTLFWNDjl5MPJf9u+Q==} + object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -2911,10 +3048,35 @@ packages: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} + parse5-htmlparser2-tree-adapter@6.0.1: + resolution: {integrity: sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==} + + parse5@5.1.1: + resolution: {integrity: sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==} + + parse5@6.0.1: + resolution: {integrity: sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==} + parseurl@1.3.3: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} + passport-github@1.1.0: + resolution: {integrity: sha512-XARXJycE6fFh/dxF+Uut8OjlwbFEXgbPVj/+V+K7cvriRK7VcAOm+NgBmbiLM9Qv3SSxEAV+V6fIk89nYHXa8A==} + engines: {node: '>= 0.4.0'} + + passport-oauth2@1.8.0: + resolution: {integrity: sha512-cjsQbOrXIDE4P8nNb3FQRCCmJJ/utnFKEz2NX209f7KOHPoX18gF7gBzBbLLsj2/je4KrgiwLLGjf0lm9rtTBA==} + engines: {node: '>= 0.4.0'} + + passport-strategy@1.0.0: + resolution: {integrity: sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==} + engines: {node: '>= 0.4.0'} + + passport@0.7.0: + resolution: {integrity: sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==} + engines: {node: '>= 0.4.0'} + path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -2948,6 +3110,9 @@ packages: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} + pause@0.0.1: + resolution: {integrity: sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -3222,6 +3387,10 @@ packages: resolution: {integrity: sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==} engines: {node: '>= 10.13.0'} + semver@5.7.2: + resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} + hasBin: true + semver@6.3.1: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true @@ -3235,6 +3404,9 @@ packages: resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==} engines: {node: '>= 0.8.0'} + seq-queue@0.0.5: + resolution: {integrity: sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==} + serialize-javascript@6.0.2: resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} @@ -3249,6 +3421,10 @@ packages: setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + sha.js@2.4.11: + resolution: {integrity: sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==} + hasBin: true + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -3257,6 +3433,9 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} + shimmer@1.2.1: + resolution: {integrity: sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==} + side-channel@1.0.6: resolution: {integrity: sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==} engines: {node: '>= 0.4'} @@ -3319,6 +3498,13 @@ packages: sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + sqlstring@2.3.3: + resolution: {integrity: sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==} + engines: {node: '>= 0.6'} + + stack-chain@1.3.7: + resolution: {integrity: sha512-D8cWtWVdIe/jBA7v5p5Hwl5yOSOrmZPWDPe2KxQ5UAGD+nxbxU0lKXA4h85Ta6+qgdKVL3vUxsbIZjc1kBG7ug==} + stack-utils@2.0.6: resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} engines: {node: '>=10'} @@ -3576,6 +3762,76 @@ packages: typedarray@0.0.6: resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} + typeorm-naming-strategies@4.1.0: + resolution: {integrity: sha512-vPekJXzZOTZrdDvTl1YoM+w+sUIfQHG4kZTpbFYoTsufyv9NIBRe4Q+PdzhEAFA2std3D9LZHEb1EjE9zhRpiQ==} + peerDependencies: + typeorm: ^0.2.0 || ^0.3.0 + + typeorm-transactional@0.5.0: + resolution: {integrity: sha512-53/CwnXpOIJnWU3oVCNbhHB95FwciKSGbY+m/Hw4e2dBM2c4toiOHwf4pmk83Ne7guznmDgVr/5IUfbp+JTPCg==} + engines: {node: '>=12.0.0'} + peerDependencies: + reflect-metadata: '>= 0.1.12' + typeorm: '>= 0.2.8' + + typeorm@0.3.20: + resolution: {integrity: sha512-sJ0T08dV5eoZroaq9uPKBoNcGslHBR4E4y+EBHs//SiGbblGe7IeduP/IH4ddCcj0qp3PHwDwGnuvqEAnKlq/Q==} + engines: {node: '>=16.13.0'} + hasBin: true + peerDependencies: + '@google-cloud/spanner': ^5.18.0 + '@sap/hana-client': ^2.12.25 + better-sqlite3: ^7.1.2 || ^8.0.0 || ^9.0.0 + hdb-pool: ^0.1.6 + ioredis: ^5.0.4 + mongodb: ^5.8.0 + mssql: ^9.1.1 || ^10.0.1 + mysql2: ^2.2.5 || ^3.0.1 + oracledb: ^6.3.0 + pg: ^8.5.1 + pg-native: ^3.0.0 + pg-query-stream: ^4.0.0 + redis: ^3.1.1 || ^4.0.0 + sql.js: ^1.4.0 + sqlite3: ^5.0.3 + ts-node: ^10.7.0 + typeorm-aurora-data-api-driver: ^2.0.0 + peerDependenciesMeta: + '@google-cloud/spanner': + optional: true + '@sap/hana-client': + optional: true + better-sqlite3: + optional: true + hdb-pool: + optional: true + ioredis: + optional: true + mongodb: + optional: true + mssql: + optional: true + mysql2: + optional: true + oracledb: + optional: true + pg: + optional: true + pg-native: + optional: true + pg-query-stream: + optional: true + redis: + optional: true + sql.js: + optional: true + sqlite3: + optional: true + ts-node: + optional: true + typeorm-aurora-data-api-driver: + optional: true + typescript-eslint@8.12.2: resolution: {integrity: sha512-UbuVUWSrHVR03q9CWx+JDHeO6B/Hr9p4U5lRH++5tq/EbFq1faYZe50ZSBePptgfIKLEti0aPQ3hFgnPVcd8ZQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -3595,6 +3851,9 @@ packages: engines: {node: '>=14.17'} hasBin: true + uid2@0.0.4: + resolution: {integrity: sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA==} + uid@2.0.2: resolution: {integrity: sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==} engines: {node: '>=8'} @@ -3630,6 +3889,10 @@ packages: resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} engines: {node: '>= 0.4.0'} + uuid@9.0.1: + resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} + hasBin: true + v8-compile-cache-lib@3.0.1: resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} @@ -3769,10 +4032,18 @@ packages: engines: {node: '>= 14'} hasBin: true + yargs-parser@20.2.9: + resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} + engines: {node: '>=10'} + yargs-parser@21.1.1: resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} engines: {node: '>=12'} + yargs@16.2.0: + resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==} + engines: {node: '>=10'} + yargs@17.7.2: resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} engines: {node: '>=12'} @@ -4600,6 +4871,11 @@ snapshots: transitivePeerDependencies: - encoding + '@nestjs/passport@10.0.3(@nestjs/common@10.4.6(reflect-metadata@0.2.2)(rxjs@7.8.1))(passport@0.7.0)': + dependencies: + '@nestjs/common': 10.4.6(reflect-metadata@0.2.2)(rxjs@7.8.1) + passport: 0.7.0 + '@nestjs/platform-express@10.4.6(@nestjs/common@10.4.6(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.6)': dependencies: '@nestjs/common': 10.4.6(reflect-metadata@0.2.2)(rxjs@7.8.1) @@ -4646,7 +4922,7 @@ snapshots: transitivePeerDependencies: - chokidar - '@nestjs/testing@10.4.6(@nestjs/common@10.4.6(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.6)(@nestjs/platform-express@10.4.6)': + '@nestjs/testing@10.4.6(@nestjs/common@10.4.6(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.6(@nestjs/common@10.4.6(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.6)(@nestjs/websockets@10.4.7)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.6(@nestjs/common@10.4.6(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.6))': dependencies: '@nestjs/common': 10.4.6(reflect-metadata@0.2.2)(rxjs@7.8.1) '@nestjs/core': 10.4.6(@nestjs/common@10.4.6(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.6)(@nestjs/websockets@10.4.7)(reflect-metadata@0.2.2)(rxjs@7.8.1) @@ -4654,6 +4930,15 @@ snapshots: optionalDependencies: '@nestjs/platform-express': 10.4.6(@nestjs/common@10.4.6(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.6) + '@nestjs/typeorm@10.0.2(@nestjs/common@10.4.6(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.6(@nestjs/common@10.4.6(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.6)(@nestjs/websockets@10.4.7)(reflect-metadata@0.2.2)(rxjs@7.8.1))(reflect-metadata@0.2.2)(rxjs@7.8.1)(typeorm@0.3.20(ioredis@5.4.1)(mysql2@3.11.4)(ts-node@10.9.2(@types/node@20.17.4)(typescript@5.6.3)))': + dependencies: + '@nestjs/common': 10.4.6(reflect-metadata@0.2.2)(rxjs@7.8.1) + '@nestjs/core': 10.4.6(@nestjs/common@10.4.6(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.6)(@nestjs/websockets@10.4.7)(reflect-metadata@0.2.2)(rxjs@7.8.1) + reflect-metadata: 0.2.2 + rxjs: 7.8.1 + typeorm: 0.3.20(ioredis@5.4.1)(mysql2@3.11.4)(ts-node@10.9.2(@types/node@20.17.4)(typescript@5.6.3)) + uuid: 9.0.1 + '@nestjs/websockets@10.4.7(@nestjs/common@10.4.6(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.6)(@nestjs/platform-socket.io@10.4.7)(reflect-metadata@0.2.2)(rxjs@7.8.1)': dependencies: '@nestjs/common': 10.4.6(reflect-metadata@0.2.2)(rxjs@7.8.1) @@ -4759,6 +5044,8 @@ snapshots: '@socket.io/component-emitter@3.1.2': {} + '@sqltools/formatter@1.2.5': {} + '@tsconfig/node10@1.0.11': {} '@tsconfig/node12@1.0.11': {} @@ -4793,6 +5080,10 @@ snapshots: '@types/connect': 3.4.38 '@types/node': 20.17.4 + '@types/cls-hooked@4.3.9': + dependencies: + '@types/node': 20.17.4 + '@types/connect@3.4.38': dependencies: '@types/node': 20.17.4 @@ -4856,6 +5147,26 @@ snapshots: dependencies: undici-types: 6.19.8 + '@types/oauth@0.9.6': + dependencies: + '@types/node': 20.17.4 + + '@types/passport-github@1.1.12': + dependencies: + '@types/express': 5.0.0 + '@types/passport': 1.0.17 + '@types/passport-oauth2': 1.4.17 + + '@types/passport-oauth2@1.4.17': + dependencies: + '@types/express': 5.0.0 + '@types/oauth': 0.9.6 + '@types/passport': 1.0.17 + + '@types/passport@1.0.17': + dependencies: + '@types/express': 5.0.0 + '@types/prop-types@15.7.13': {} '@types/qs@6.9.16': {} @@ -5220,6 +5531,8 @@ snapshots: normalize-path: 3.0.0 picomatch: 2.3.1 + app-root-path@3.1.0: {} + append-field@1.0.0: {} arg@4.1.3: {} @@ -5240,6 +5553,10 @@ snapshots: asap@2.0.6: {} + async-hook-jl@1.7.6: + dependencies: + stack-chain: 1.3.7 + async@3.2.6: {} asynckit@0.4.0: {} @@ -5254,6 +5571,8 @@ snapshots: postcss: 8.4.47 postcss-value-parser: 4.2.0 + aws-ssl-profiles@1.1.2: {} + babel-jest@29.7.0(@babel/core@7.26.0): dependencies: '@babel/core': 7.26.0 @@ -5320,6 +5639,8 @@ snapshots: base64id@2.0.0: {} + base64url@3.0.1: {} + binary-extensions@2.3.0: {} bl@4.1.0: @@ -5380,6 +5701,11 @@ snapshots: base64-js: 1.5.1 ieee754: 1.2.1 + buffer@6.0.3: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + busboy@1.6.0: dependencies: streamsearch: 1.1.0 @@ -5437,6 +5763,15 @@ snapshots: dependencies: restore-cursor: 3.1.0 + cli-highlight@2.1.11: + dependencies: + chalk: 4.1.2 + highlight.js: 10.7.3 + mz: 2.7.0 + parse5: 5.1.1 + parse5-htmlparser2-tree-adapter: 6.0.1 + yargs: 16.2.0 + cli-spinners@2.9.2: {} cli-table3@0.6.5: @@ -5449,6 +5784,12 @@ snapshots: cli-width@4.1.0: {} + cliui@7.0.4: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + cliui@8.0.1: dependencies: string-width: 4.2.3 @@ -5457,6 +5798,12 @@ snapshots: clone@1.0.4: {} + cls-hooked@4.2.2: + dependencies: + async-hook-jl: 1.7.6 + emitter-listener: 1.1.2 + semver: 5.7.2 + cluster-key-slot@1.1.2: {} co@4.6.0: {} @@ -5597,6 +5944,8 @@ snapshots: dargs@8.1.0: {} + dayjs@1.11.13: {} + debug@2.6.9: dependencies: ms: 2.0.0 @@ -5664,6 +6013,10 @@ snapshots: electron-to-chromium@1.5.49: {} + emitter-listener@1.1.2: + dependencies: + shimmer: 1.2.1 + emittery@0.13.1: {} emoji-regex@8.0.0: {} @@ -6125,6 +6478,10 @@ snapshots: function-bind@1.1.2: {} + generate-function@2.3.1: + dependencies: + is-property: 1.0.2 + gensync@1.0.0-beta.2: {} get-caller-file@2.0.5: {} @@ -6224,6 +6581,8 @@ snapshots: hexoid@2.0.0: {} + highlight.js@10.7.3: {} + html-escaper@2.0.2: {} http-errors@2.0.0: @@ -6242,6 +6601,10 @@ snapshots: dependencies: safer-buffer: 2.1.2 + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + ieee754@1.2.1: {} ignore@5.3.2: {} @@ -6349,6 +6712,8 @@ snapshots: is-path-inside@3.0.3: {} + is-property@1.0.2: {} + is-stream@2.0.1: {} is-text-path@2.0.0: @@ -6835,6 +7200,8 @@ snapshots: chalk: 4.1.2 is-unicode-supported: 0.1.0 + long@5.2.3: {} + loose-envify@1.4.0: dependencies: js-tokens: 4.0.0 @@ -6847,6 +7214,10 @@ snapshots: dependencies: yallist: 3.1.1 + lru-cache@7.18.3: {} + + lru.min@1.1.1: {} + magic-string@0.30.8: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 @@ -6914,6 +7285,8 @@ snapshots: dependencies: minimist: 1.2.8 + mkdirp@2.1.6: {} + ms@2.0.0: {} ms@2.1.3: {} @@ -6932,12 +7305,28 @@ snapshots: mute-stream@1.0.0: {} + mysql2@3.11.4: + dependencies: + aws-ssl-profiles: 1.1.2 + denque: 2.1.0 + generate-function: 2.3.1 + iconv-lite: 0.6.3 + long: 5.2.3 + lru.min: 1.1.1 + named-placeholders: 1.1.3 + seq-queue: 0.0.5 + sqlstring: 2.3.3 + mz@2.7.0: dependencies: any-promise: 1.3.0 object-assign: 4.1.1 thenify-all: 1.6.0 + named-placeholders@1.1.3: + dependencies: + lru-cache: 7.18.3 + nanoid@3.3.7: {} natural-compare@1.4.0: {} @@ -6968,6 +7357,8 @@ snapshots: dependencies: path-key: 3.1.1 + oauth@0.10.0: {} + object-assign@4.1.1: {} object-hash@3.0.0: {} @@ -7048,8 +7439,36 @@ snapshots: json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 + parse5-htmlparser2-tree-adapter@6.0.1: + dependencies: + parse5: 6.0.1 + + parse5@5.1.1: {} + + parse5@6.0.1: {} + parseurl@1.3.3: {} + passport-github@1.1.0: + dependencies: + passport-oauth2: 1.8.0 + + passport-oauth2@1.8.0: + dependencies: + base64url: 3.0.1 + oauth: 0.10.0 + passport-strategy: 1.0.0 + uid2: 0.0.4 + utils-merge: 1.0.1 + + passport-strategy@1.0.0: {} + + passport@0.7.0: + dependencies: + passport-strategy: 1.0.0 + pause: 0.0.1 + utils-merge: 1.0.1 + path-exists@4.0.0: {} path-exists@5.0.0: {} @@ -7071,6 +7490,8 @@ snapshots: path-type@4.0.0: {} + pause@0.0.1: {} + picocolors@1.1.1: {} picomatch@2.3.1: {} @@ -7338,6 +7759,8 @@ snapshots: ajv: 6.12.6 ajv-keywords: 3.5.2(ajv@6.12.6) + semver@5.7.2: {} + semver@6.3.1: {} semver@7.6.3: {} @@ -7360,6 +7783,8 @@ snapshots: transitivePeerDependencies: - supports-color + seq-queue@0.0.5: {} + serialize-javascript@6.0.2: dependencies: randombytes: 2.1.0 @@ -7384,12 +7809,19 @@ snapshots: setprototypeof@1.2.0: {} + sha.js@2.4.11: + dependencies: + inherits: 2.0.4 + safe-buffer: 5.2.1 + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 shebang-regex@3.0.0: {} + shimmer@1.2.1: {} + side-channel@1.0.6: dependencies: call-bind: 1.0.7 @@ -7480,6 +7912,10 @@ snapshots: sprintf-js@1.0.3: {} + sqlstring@2.3.3: {} + + stack-chain@1.3.7: {} + stack-utils@2.0.6: dependencies: escape-string-regexp: 2.0.0 @@ -7748,6 +8184,42 @@ snapshots: typedarray@0.0.6: {} + typeorm-naming-strategies@4.1.0(typeorm@0.3.20(ioredis@5.4.1)(mysql2@3.11.4)(ts-node@10.9.2(@types/node@20.17.4)(typescript@5.6.3))): + dependencies: + typeorm: 0.3.20(ioredis@5.4.1)(mysql2@3.11.4)(ts-node@10.9.2(@types/node@20.17.4)(typescript@5.6.3)) + + typeorm-transactional@0.5.0(reflect-metadata@0.2.2)(typeorm@0.3.20(ioredis@5.4.1)(mysql2@3.11.4)(ts-node@10.9.2(@types/node@20.17.4)(typescript@5.6.3))): + dependencies: + '@types/cls-hooked': 4.3.9 + cls-hooked: 4.2.2 + reflect-metadata: 0.2.2 + semver: 7.6.3 + typeorm: 0.3.20(ioredis@5.4.1)(mysql2@3.11.4)(ts-node@10.9.2(@types/node@20.17.4)(typescript@5.6.3)) + + typeorm@0.3.20(ioredis@5.4.1)(mysql2@3.11.4)(ts-node@10.9.2(@types/node@20.17.4)(typescript@5.6.3)): + dependencies: + '@sqltools/formatter': 1.2.5 + app-root-path: 3.1.0 + buffer: 6.0.3 + chalk: 4.1.2 + cli-highlight: 2.1.11 + dayjs: 1.11.13 + debug: 4.3.7 + dotenv: 16.4.5 + glob: 10.4.5 + mkdirp: 2.1.6 + reflect-metadata: 0.2.2 + sha.js: 2.4.11 + tslib: 2.8.0 + uuid: 9.0.1 + yargs: 17.7.2 + optionalDependencies: + ioredis: 5.4.1 + mysql2: 3.11.4 + ts-node: 10.9.2(@types/node@20.17.4)(typescript@5.6.3) + transitivePeerDependencies: + - supports-color + typescript-eslint@8.12.2(eslint@9.13.0(jiti@1.21.6))(typescript@5.6.3): dependencies: '@typescript-eslint/eslint-plugin': 8.12.2(@typescript-eslint/parser@8.12.2(eslint@9.13.0(jiti@1.21.6))(typescript@5.6.3))(eslint@9.13.0(jiti@1.21.6))(typescript@5.6.3) @@ -7763,6 +8235,8 @@ snapshots: typescript@5.6.3: {} + uid2@0.0.4: {} + uid@2.0.2: dependencies: '@lukeed/csprng': 1.1.0 @@ -7789,6 +8263,8 @@ snapshots: utils-merge@1.0.1: {} + uuid@9.0.1: {} + v8-compile-cache-lib@3.0.1: {} v8-to-istanbul@9.3.0: @@ -7908,8 +8384,20 @@ snapshots: yaml@2.6.0: {} + yargs-parser@20.2.9: {} + yargs-parser@21.1.1: {} + yargs@16.2.0: + dependencies: + cliui: 7.0.4 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 20.2.9 + yargs@17.7.2: dependencies: cliui: 8.0.1 From 7b893e2eb8e9a932a80e51e15a317c75a4f4ffdb Mon Sep 17 00:00:00 2001 From: Chanwoo Kim Date: Mon, 18 Nov 2024 23:50:28 +0900 Subject: [PATCH 024/180] =?UTF-8?q?feat:=20`Nest.js`=EC=99=80=20`TypeORM`?= =?UTF-8?q?=20=EB=AA=A8=EB=93=88=20=EC=97=B0=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `typeorm-transactional` 을 이용하여 선언형으로 트랜잭션 적용 - `MySQL` 사용 - `synchronize : true` 옵션을 사용하여 테이블이 없을 경우 생성 (프로덕션 시 해당 옵션 삭제 요망) Co-authored-by: twalla26 --- backend/src/app.module.ts | 40 ++++++++++++++++++++++++++++++++++++++- backend/src/main.ts | 9 +++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index 2e4f16bc..90b88f32 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -1,14 +1,52 @@ import { Module } from "@nestjs/common"; + import { AppController } from "./app.controller"; import { AppService } from "./app.service"; + import { SocketModule } from "./signaling-server/socket.module"; import { RoomModule } from "./room/room.module"; import { RedisModule } from "./redis/redis.module"; +import { AuthModule } from "./auth/auth.module"; +import { UserModule } from "./user/user.module"; +import { TypeOrmModule } from "@nestjs/typeorm"; + +import { SnakeNamingStrategy } from "typeorm-naming-strategies"; + +import { User } from "./user/user.entity"; import "dotenv/config"; +import { addTransactionalDataSource } from "typeorm-transactional"; +import { DataSource } from "typeorm"; @Module({ - imports: [SocketModule, RoomModule, RedisModule], + imports: [ + TypeOrmModule.forRootAsync({ + useFactory() { + return { + type: "mysql", + host: process.env.MYSQL_HOST, + port: parseInt(process.env.MYSQL_PORT) ?? 3306, + username: process.env.MYSQL_USERNAME, + password: process.env.MYSQL_PASSWORD, + database: process.env.MYSQL_DATABASE, + entities: [User], + namingStrategy: new SnakeNamingStrategy(), + synchronize: true, + }; + }, + async dataSourceFactory(options) { + if (!options) { + throw new Error("Invalid options passed"); + } + return addTransactionalDataSource(new DataSource(options)); + }, + }), + SocketModule, + RoomModule, + RedisModule, + AuthModule, + UserModule, + ], controllers: [AppController], providers: [AppService], }) diff --git a/backend/src/main.ts b/backend/src/main.ts index b5c531a1..f8245896 100644 --- a/backend/src/main.ts +++ b/backend/src/main.ts @@ -1,8 +1,17 @@ import { NestFactory } from "@nestjs/core"; import { AppModule } from "./app.module"; +import { + initializeTransactionalContext, + StorageDriver, +} from "typeorm-transactional"; async function bootstrap() { + initializeTransactionalContext({ + storageDriver: StorageDriver.AUTO, // 명시적으로 storage driver 설정 + }); + const app = await NestFactory.create(AppModule); await app.listen(process.env.PORT ?? 3000); } + bootstrap(); From d7145580242e7272f0ea7e294e4ce5a58b2e7dd8 Mon Sep 17 00:00:00 2001 From: Chanwoo Kim Date: Mon, 18 Nov 2024 23:51:54 +0900 Subject: [PATCH 025/180] =?UTF-8?q?feat:=20`UserRepository`=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `github id` 번호로 유저 테이블에서 유저 읽기 가능 - `github id` 로 필요할 경우 유저 생성 가능 Co-authored-by: twalla26 --- backend/src/user/dto/create-user.dto.ts | 6 ++++++ backend/src/user/user.entity.ts | 23 +++++++++++++++++++++++ backend/src/user/user.module.ts | 7 +++++++ backend/src/user/user.repository.ts | 21 +++++++++++++++++++++ 4 files changed, 57 insertions(+) create mode 100644 backend/src/user/dto/create-user.dto.ts create mode 100644 backend/src/user/user.entity.ts create mode 100644 backend/src/user/user.module.ts create mode 100644 backend/src/user/user.repository.ts diff --git a/backend/src/user/dto/create-user.dto.ts b/backend/src/user/dto/create-user.dto.ts new file mode 100644 index 00000000..61c14de2 --- /dev/null +++ b/backend/src/user/dto/create-user.dto.ts @@ -0,0 +1,6 @@ +export interface CreateUserDto { + loginId?: string; + passwordHash?: string; + githubId?: number; + username: string; +} diff --git a/backend/src/user/user.entity.ts b/backend/src/user/user.entity.ts new file mode 100644 index 00000000..719366e9 --- /dev/null +++ b/backend/src/user/user.entity.ts @@ -0,0 +1,23 @@ +import { Entity, PrimaryGeneratedColumn, Column } from "typeorm"; + +@Entity() +export class User { + private static LOGIN_ID_MAX_LEN = 20; + private static PASSWORD_HASH_MAX_LEN = 256; + private static USERNAME_MAX_LEN = 20; + + @PrimaryGeneratedColumn() + id: number; + + @Column({ length: User.LOGIN_ID_MAX_LEN, nullable: true, unique: true }) + loginId: string; + + @Column({ length: User.PASSWORD_HASH_MAX_LEN, nullable: true }) + passwordHash: string; + + @Column({ length: User.USERNAME_MAX_LEN, unique: true }) + username: string; + + @Column({ nullable: true, unique: true }) + githubId: number; +} diff --git a/backend/src/user/user.module.ts b/backend/src/user/user.module.ts new file mode 100644 index 00000000..196852ba --- /dev/null +++ b/backend/src/user/user.module.ts @@ -0,0 +1,7 @@ +import { Module } from "@nestjs/common"; +import { UserRepository } from "./user.repository"; + +@Module({ + providers: [UserRepository], +}) +export class UserModule {} diff --git a/backend/src/user/user.repository.ts b/backend/src/user/user.repository.ts new file mode 100644 index 00000000..98dd2e5d --- /dev/null +++ b/backend/src/user/user.repository.ts @@ -0,0 +1,21 @@ +import { Injectable } from "@nestjs/common"; +import { User } from "./user.entity"; +import { DataSource } from "typeorm"; +import { CreateUserDto } from "./dto/create-user.dto"; + +@Injectable() +export class UserRepository { + constructor(private dataSource: DataSource) {} + + getUserByGithubId(githubId: number) { + return this.dataSource + .getRepository(User) + .createQueryBuilder("user") + .where("user.github_id = :id", { id: githubId }) + .getOne(); + } + + createUser(createUserDto: CreateUserDto) { + return this.dataSource.getRepository(User).save(createUserDto); + } +} From caf02144631d87d2be970a9e3b7887ecdb816192 Mon Sep 17 00:00:00 2001 From: Chanwoo Kim Date: Mon, 18 Nov 2024 23:54:27 +0900 Subject: [PATCH 026/180] =?UTF-8?q?feat:=20=EA=B9=83=ED=97=88=EB=B8=8C=20O?= =?UTF-8?q?Auth=20=EC=97=B0=EB=8F=99=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `passport` 를 통해 `accessToken`으로부터 자동적으로 유저의 깃허브 프로필 확인 - `validate` 함수를 통해 유저의 프로필 정보를 통해 유저 테이블에 접근 - `/api/github`와 `/api/github/login` API를 생성 - 해당 API를 접근할 때, `AuthGuard` 를 통하여 `github.strategy.ts` 파일 내 유저 인증 미들웨어에 접근 가능 Co-authored-by: twalla26 --- backend/src/auth/auth.controller.spec.ts | 18 ++++++++++ backend/src/auth/auth.controller.ts | 24 +++++++++++++ backend/src/auth/auth.module.ts | 11 ++++++ backend/src/auth/auth.service.spec.ts | 18 ++++++++++ backend/src/auth/auth.service.ts | 46 ++++++++++++++++++++++++ backend/src/config/github.strategy.ts | 43 ++++++++++++++++++++++ 6 files changed, 160 insertions(+) create mode 100644 backend/src/auth/auth.controller.spec.ts create mode 100644 backend/src/auth/auth.controller.ts create mode 100644 backend/src/auth/auth.module.ts create mode 100644 backend/src/auth/auth.service.spec.ts create mode 100644 backend/src/auth/auth.service.ts create mode 100644 backend/src/config/github.strategy.ts diff --git a/backend/src/auth/auth.controller.spec.ts b/backend/src/auth/auth.controller.spec.ts new file mode 100644 index 00000000..901d3e88 --- /dev/null +++ b/backend/src/auth/auth.controller.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from "@nestjs/testing"; +import { AuthController } from "./auth.controller"; + +describe("AuthController", () => { + let controller: AuthController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [AuthController], + }).compile(); + + controller = module.get(AuthController); + }); + + it("should be defined", () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/backend/src/auth/auth.controller.ts b/backend/src/auth/auth.controller.ts new file mode 100644 index 00000000..d87f20d0 --- /dev/null +++ b/backend/src/auth/auth.controller.ts @@ -0,0 +1,24 @@ +import { Controller, Get, Redirect, Req, Res, UseGuards } from "@nestjs/common"; +import { AuthGuard } from "@nestjs/passport"; + +@Controller("auth") +export class AuthController { + @Get("github") + @UseGuards(AuthGuard("github")) + async githubLogin(): Promise {} + + @Get("github/login") + @UseGuards(AuthGuard("github")) + @Redirect() + async githubLoginCallback(@Req() req) { + const username: string = req.user.username; + if (username) return { url: "/login/success/" + username }; + return { url: "/login/failure" }; + } + + @Get("protected") + @UseGuards(AuthGuard("jwt")) + protectedResource() { + return "JWT is working!"; + } +} diff --git a/backend/src/auth/auth.module.ts b/backend/src/auth/auth.module.ts new file mode 100644 index 00000000..1f9253ec --- /dev/null +++ b/backend/src/auth/auth.module.ts @@ -0,0 +1,11 @@ +import { Module } from "@nestjs/common"; +import { AuthController } from "./auth.controller"; +import { AuthService } from "./auth.service"; +import { GithubStrategy } from "../config/github.strategy"; +import { UserRepository } from "../user/user.repository"; + +@Module({ + controllers: [AuthController], + providers: [AuthService, GithubStrategy, UserRepository], +}) +export class AuthModule {} diff --git a/backend/src/auth/auth.service.spec.ts b/backend/src/auth/auth.service.spec.ts new file mode 100644 index 00000000..e5f2ecf6 --- /dev/null +++ b/backend/src/auth/auth.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from "@nestjs/testing"; +import { AuthService } from "./auth.service"; + +describe("AuthService", () => { + let service: AuthService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [AuthService], + }).compile(); + + service = module.get(AuthService); + }); + + it("should be defined", () => { + expect(service).toBeDefined(); + }); +}); diff --git a/backend/src/auth/auth.service.ts b/backend/src/auth/auth.service.ts new file mode 100644 index 00000000..373f3785 --- /dev/null +++ b/backend/src/auth/auth.service.ts @@ -0,0 +1,46 @@ +import { Injectable } from "@nestjs/common"; +import { UserRepository } from "../user/user.repository"; +import { Profile } from "passport-github"; +import { Transactional } from "typeorm-transactional"; + +@Injectable() +export class AuthService { + constructor(private readonly userRepository: UserRepository) {} + + @Transactional() + public async githubLogin(profile: Profile) { + const user = await this.userRepository.getUserByGithubId( + parseInt(profile.id) + ); + + if (!user) + return await this.userRepository.createUser({ + githubId: parseInt(profile.id), + username: `camper_${profile.id}`, + }); + + return user; + } + + // public oauthLogin(code: string) { + // // 1. GitHub OAuth 토큰 획득 + // const accessToken = githubClient.getAccessToken(code); + // + // // 1. User DB를 토큰이나, 깃허브 사용자 정보를 통해서 재구성하는 행위 + // // 2. nickname => 캠퍼_{github_username} + // // 3. id => AUTO INCREMENT, password => null + // // 4. user_id => null + // + // // 2. GitHub API로 사용자 정보 조회 + // const userInfo = githubClient.getUserInfo(accessToken); + // + // // 3. 자체 DB에 사용자 정보 저장 + // const user = userRepository.findByGithubId(userInfo.getId()) + // .orElseGet(() -> createUser(userInfo)); + // + // // 4. JWT 발급 (자체 서비스용) + // const jwt = jwtProvider.generateToken(user); + // + // return new UserResponse(user, jwt); + // } +} diff --git a/backend/src/config/github.strategy.ts b/backend/src/config/github.strategy.ts new file mode 100644 index 00000000..e3990baa --- /dev/null +++ b/backend/src/config/github.strategy.ts @@ -0,0 +1,43 @@ +import { Injectable } from "@nestjs/common"; +import { PassportStrategy } from "@nestjs/passport"; +import "dotenv/config"; +import { Profile, Strategy } from "passport-github"; +import { AuthService } from "../auth/auth.service"; + +@Injectable() +export class GithubStrategy extends PassportStrategy(Strategy, "github") { + constructor(private readonly authService: AuthService) { + super({ + clientID: process.env.OAUTH_GITHUB_ID, // CLIENT_ID + clientSecret: process.env.OAUTH_GITHUB_SECRET, // CLIENT_SECRET + callbackURL: process.env.OAUTH_GITHUB_CALLBACK, // redirect_uri + passReqToCallback: true, + scope: ["profile"], // 가져올 정보들 + }); + } + + /** + * GitHub에서 반환된 프로필 데이터를 가공 + * @param request + * @param accessToken + * @param refreshToken + * @param profile + * @param done + */ + async validate( + request: any, + accessToken: string, + refreshToken: string, + profile: Profile, + done: (error: any, user?: any) => void + ) { + try { + const user = await this.authService.githubLogin(profile); + console.log(user); + done(null, user); + } catch (err) { + console.error(err); + done(err, false); + } + } +} From 3b232bc0aeed1bbf55b52da027ded99bf02fe3bc Mon Sep 17 00:00:00 2001 From: JeongwooSeo Date: Tue, 19 Nov 2024 00:36:36 +0900 Subject: [PATCH 027/180] =?UTF-8?q?feat:=20sidebar=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/common/Sidebar.tsx | 113 +++++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 frontend/src/components/common/Sidebar.tsx diff --git a/frontend/src/components/common/Sidebar.tsx b/frontend/src/components/common/Sidebar.tsx new file mode 100644 index 00000000..bcc08c8e --- /dev/null +++ b/frontend/src/components/common/Sidebar.tsx @@ -0,0 +1,113 @@ +import { Link } from "react-router-dom"; +import { ReactElement, useEffect, useState } from "react"; +import { FaClipboardList, FaHome, FaLayerGroup } from "react-icons/fa"; +import { MdLogout } from "react-icons/md"; +import { FaRegCircleUser } from "react-icons/fa6"; + +const Sidebar = () => { + const routes = [ + { + path: "/", + label: "홈", + icon: , + }, + { + path: "/questions", + label: "질문지 리스트", + icon: , + }, + { + path: "/sessions", + label: "스터디 세션 목록", + icon: , + }, + { + path: "/mypage", + label: "마이페이지", + icon: , + }, + { + path: "/logout", + label: "로그아웃", + icon: , + }, + ]; + + const [selected, setSelected] = useState(""); + + useEffect(() => { + setSelected(window.location.pathname); + }, []); + return ( + + ); +}; + +interface SidebarMenuProps { + path: string; + label: string; + icon?: ReactElement; + isSelected?: boolean; +} + +const SidebarMenu = ({ + path, + label, + icon, + isSelected = false, +}: SidebarMenuProps) => { + const activeClass = isSelected + ? "bg-green-100 text-white" + : "bg-transparent "; + + return ( +
  • + + {icon} + {label} + +
  • + ); +}; + +export default Sidebar; From 22a353125f4672ed16312969fc6e0b0759b1e51a Mon Sep 17 00:00:00 2001 From: JeongwooSeo Date: Tue, 19 Nov 2024 00:36:51 +0900 Subject: [PATCH 028/180] =?UTF-8?q?feat:=20=EC=84=B8=EC=85=98=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=EC=97=90=20=EC=82=AC=EC=9D=B4=EB=93=9C?= =?UTF-8?q?=EB=B0=94=EB=A5=BC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/pages/SessionListPage.tsx | 112 +++++++++++++------------ 1 file changed, 58 insertions(+), 54 deletions(-) diff --git a/frontend/src/pages/SessionListPage.tsx b/frontend/src/pages/SessionListPage.tsx index 8f335487..3e588e61 100644 --- a/frontend/src/pages/SessionListPage.tsx +++ b/frontend/src/pages/SessionListPage.tsx @@ -5,6 +5,7 @@ import { useNavigate } from "react-router-dom"; import { IoMdAdd } from "react-icons/io"; import SearchBar from "@/components/common/SearchBar.tsx"; import useToast from "@/hooks/useToast"; +import Sidebar from "@components/common/Sidebar.tsx"; interface Session { id: number; @@ -84,65 +85,68 @@ const SessionListPage = () => { }; return ( -
    -
    -

    스터디 세션 목록

    -
    - -
    - + + + + + + +
    +
    -
    -
    -
    -

    공개된 세션 목록

    -
      - {listLoading ? ( - <>loading - ) : ( - <> - {sessionList.length <= 0 ? ( -
    • 아직 아무도 세션을 열지 않았어요..!
    • - ) : ( - renderSessionList(SessionStatus.OPEN) - )} - - )} -
    -
    -
    -

    진행 중인 세션 목록

    -
      - {listLoading ? ( - <>loading - ) : ( - <> - {sessionList.length <= 0 ? ( -
    • 아직 아무도 세션을 열지 않았어요..!
    • - ) : ( - renderSessionList(SessionStatus.CLOSE) - )} - - )} -
    +
    +

    공개된 세션 목록

    +
      + {listLoading ? ( + <>loading + ) : ( + <> + {sessionList.length <= 0 ? ( +
    • 아직 아무도 세션을 열지 않았어요..!
    • + ) : ( + renderSessionList(SessionStatus.OPEN) + )} + + )} +
    +
    +
    +

    진행 중인 세션 목록

    +
      + {listLoading ? ( + <>loading + ) : ( + <> + {sessionList.length <= 0 ? ( +
    • 아직 아무도 세션을 열지 않았어요..!
    • + ) : ( + renderSessionList(SessionStatus.CLOSE) + )} + + )} +
    +
    ); From 205277a457ad9fae6718e835adc3813b30e7dd44 Mon Sep 17 00:00:00 2001 From: JeongwooSeo Date: Tue, 19 Nov 2024 00:38:31 +0900 Subject: [PATCH 029/180] =?UTF-8?q?fix:=20=EB=A7=88=EC=9D=B4=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EB=88=84=EB=A5=BC=20=EC=8B=9C=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=20=ED=8E=98=EC=9D=B4=EC=A7=80=EB=A1=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/common/Sidebar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/common/Sidebar.tsx b/frontend/src/components/common/Sidebar.tsx index bcc08c8e..3bc841ac 100644 --- a/frontend/src/components/common/Sidebar.tsx +++ b/frontend/src/components/common/Sidebar.tsx @@ -22,7 +22,7 @@ const Sidebar = () => { icon: , }, { - path: "/mypage", + path: "/login", label: "마이페이지", icon: , }, From eceb3efa913cf4b7f0f5b521e7502136fce6e446 Mon Sep 17 00:00:00 2001 From: JeongwooSeo Date: Tue, 19 Nov 2024 00:41:44 +0900 Subject: [PATCH 030/180] =?UTF-8?q?feat:=20=EB=A1=9C=EA=B3=A0=20=ED=98=B8?= =?UTF-8?q?=EB=B2=84=EC=8B=9C=20=EC=95=A0=EB=8B=88=EB=A9=94=EC=9D=B4?= =?UTF-8?q?=EC=85=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/common/Sidebar.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/common/Sidebar.tsx b/frontend/src/components/common/Sidebar.tsx index 3bc841ac..fc23ea3a 100644 --- a/frontend/src/components/common/Sidebar.tsx +++ b/frontend/src/components/common/Sidebar.tsx @@ -46,7 +46,9 @@ const Sidebar = () => { >
    Preview
    From eb7fc1be5344916f4b829ebfdb821b08e7e18f38 Mon Sep 17 00:00:00 2001 From: JeongwooSeo Date: Tue, 19 Nov 2024 00:45:53 +0900 Subject: [PATCH 031/180] =?UTF-8?q?style:=20=ED=98=B8=EB=B2=84=EC=8B=9C=20?= =?UTF-8?q?=EB=B0=B0=EA=B2=BD=EC=83=89=20=ED=8A=B8=EB=9E=9C=EC=A7=80?= =?UTF-8?q?=EC=85=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/common/Sidebar.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/common/Sidebar.tsx b/frontend/src/components/common/Sidebar.tsx index fc23ea3a..78247a83 100644 --- a/frontend/src/components/common/Sidebar.tsx +++ b/frontend/src/components/common/Sidebar.tsx @@ -94,14 +94,14 @@ const SidebarMenu = ({ }: SidebarMenuProps) => { const activeClass = isSelected ? "bg-green-100 text-white" - : "bg-transparent "; + : "bg-transparent transition-color duration-300 hover:bg-gray-200/30"; return (
  • From 4b57597bf1dc2baa33d602031adba91adb9b3f2e Mon Sep 17 00:00:00 2001 From: JeongwooSeo Date: Tue, 19 Nov 2024 00:51:01 +0900 Subject: [PATCH 032/180] =?UTF-8?q?style:=20repository=EB=A1=9C=20?= =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=95=98=EB=8A=94=20=EB=A7=81=ED=81=AC=20?= =?UTF-8?q?=EC=98=86=EC=97=90=20=EC=95=84=EC=9D=B4=EC=BD=98=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/common/Sidebar.tsx | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/common/Sidebar.tsx b/frontend/src/components/common/Sidebar.tsx index 78247a83..d562492d 100644 --- a/frontend/src/components/common/Sidebar.tsx +++ b/frontend/src/components/common/Sidebar.tsx @@ -1,6 +1,11 @@ import { Link } from "react-router-dom"; import { ReactElement, useEffect, useState } from "react"; -import { FaClipboardList, FaHome, FaLayerGroup } from "react-icons/fa"; +import { + FaClipboardList, + FaExternalLinkAlt, + FaHome, + FaLayerGroup, +} from "react-icons/fa"; import { MdLogout } from "react-icons/md"; import { FaRegCircleUser } from "react-icons/fa6"; @@ -73,7 +78,9 @@ const Sidebar = () => { aria-label={"리포지토리 링크"} target={"_blank"} > - BOOSKIT + + BOOSKIT + ); @@ -98,7 +105,7 @@ const SidebarMenu = ({ return (
  • Date: Tue, 19 Nov 2024 00:52:18 +0900 Subject: [PATCH 033/180] =?UTF-8?q?feat:=20=EC=A0=91=EA=B7=BC=EC=84=B1=20a?= =?UTF-8?q?ria-label=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/common/Sidebar.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/frontend/src/components/common/Sidebar.tsx b/frontend/src/components/common/Sidebar.tsx index d562492d..e9511af7 100644 --- a/frontend/src/components/common/Sidebar.tsx +++ b/frontend/src/components/common/Sidebar.tsx @@ -58,7 +58,10 @@ const Sidebar = () => { Preview
    -
      +
        {routes.map((route) => { return ( - + {icon} {label} From 231b438b32f7a36248cbc59ef45e2834b0447636 Mon Sep 17 00:00:00 2001 From: yiseungyun Date: Tue, 19 Nov 2024 01:20:14 +0900 Subject: [PATCH 034/180] =?UTF-8?q?test:=20=EC=84=B8=EC=85=98=20id=20?= =?UTF-8?q?=EC=97=86=EC=9D=B4=20=EC=8A=A4=ED=84=B0=EB=94=94=EB=A3=B8=20?= =?UTF-8?q?=EC=9E=85=EC=9E=A5=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/hooks/__test__/useSession.test.ts | 64 ++++++++----------- frontend/src/hooks/type/session.test.d.ts | 20 ++++++ 2 files changed, 46 insertions(+), 38 deletions(-) create mode 100644 frontend/src/hooks/type/session.test.d.ts diff --git a/frontend/src/hooks/__test__/useSession.test.ts b/frontend/src/hooks/__test__/useSession.test.ts index 000bc5be..8baba6d5 100644 --- a/frontend/src/hooks/__test__/useSession.test.ts +++ b/frontend/src/hooks/__test__/useSession.test.ts @@ -4,29 +4,8 @@ import useSocketStore from "@/stores/useSocketStore"; import useMediaDevices from "@/hooks/useMediaDevices"; import usePeerConnection from "@/hooks/usePeerConnection"; import { useNavigate } from "react-router-dom"; -import { Socket } from "socket.io-client"; import { act } from "react"; - -type MockSocket = Partial & { - emit: jest.Mock; - on: jest.Mock; - off: jest.Mock; - id: string; -}; - -interface MockPeerConnection { - ontrack: null | ((event: any) => void); - onicecandidate: null | ((event: any) => void); - oniceconnectionstatechange: null | (() => void); - onconnectionstatechange: null | (() => void); - close: jest.Mock; -} - -interface MockPeerConnections { - current: { - [key: string]: MockPeerConnection; - }; -} +import { MockPeerConnections, MockSocket } from "../type/session.test"; const mockSocket: MockSocket = { emit: jest.fn(), @@ -46,8 +25,20 @@ const mockMediaStream = { }; const mockToast = { success: jest.fn(), error: jest.fn() }; + const mockNavigate = jest.fn(); -let mockPeerConnections: MockPeerConnections = { current: {} }; + +const mockPeerConnections: MockPeerConnections = { + current: { + "peer-1": { + ontrack: null, + onicecandidate: null, + oniceconnectionstatechange: null, + onconnectionstatechange: null, + close: jest.fn(), + }, + }, +}; // jest.mock: 실제 모듈대신 mock 모듈을 사용하도록 설정 jest.mock("@/hooks/useMediaDevices"); @@ -93,8 +84,6 @@ describe("useSession Hook 테스트", () => { beforeEach(() => { jest.clearAllMocks(); - mockSocketStore.socket = null; - mockSocketStore.connect = jest.fn(); (useMediaDevices as jest.Mock).mockReturnValue({ userAudioDevices: [], @@ -112,18 +101,6 @@ describe("useSession Hook 테스트", () => { getMedia: mockGetMedia, }); - mockPeerConnections = { - current: { - "peer-1": { - ontrack: null, - onicecandidate: null, - oniceconnectionstatechange: null, - onconnectionstatechange: null, - close: jest.fn(), - }, - }, - }; - (usePeerConnection as jest.Mock).mockReturnValue({ createPeerConnection: jest.fn(), closePeerConnection: jest.fn(), @@ -189,6 +166,17 @@ describe("useSession Hook 테스트", () => { }); }); + it("세션 ID가 없이 스터디룸 입장", async () => { + mockSocketStore.socket = mockSocket; + const { result } = renderHook(() => useSession(undefined)); + + await act(async () => { + await result.current.joinRoom(); + }); + + expect(mockToast.error).toHaveBeenCalledWith("세션 ID가 필요합니다."); + }); + it("닉네임 없이 스터디룸 입장", async () => { mockSocketStore.socket = mockSocket; const { result } = renderHook(() => useSession("test-session")); @@ -268,7 +256,7 @@ describe("useSession Hook 테스트", () => { // room_full 이벤트 핸들러 찾기 const roomFullHandler = mockSocket.on.mock.calls.find( - ([event]) => event === "room_full" + ([event]: [string]) => event === "room_full" )[1]; // 이벤트 핸들러 실행 diff --git a/frontend/src/hooks/type/session.test.d.ts b/frontend/src/hooks/type/session.test.d.ts new file mode 100644 index 00000000..298dc018 --- /dev/null +++ b/frontend/src/hooks/type/session.test.d.ts @@ -0,0 +1,20 @@ +export type MockSocket = Partial & { + emit: jest.Mock; + on: jest.Mock; + off: jest.Mock; + id: string; +}; + +interface MockPeerConnection { + ontrack: null | ((event: any) => void); + onicecandidate: null | ((event: any) => void); + oniceconnectionstatechange: null | (() => void); + onconnectionstatechange: null | (() => void); + close: jest.Mock; +} + +export interface MockPeerConnections { + current: { + [key: string]: MockPeerConnection; + }; +} \ No newline at end of file From 1689a65601c38d89936f36236ab968a62756c12f Mon Sep 17 00:00:00 2001 From: yiseungyun Date: Tue, 19 Nov 2024 01:39:00 +0900 Subject: [PATCH 035/180] =?UTF-8?q?test:=20=EB=A6=AC=EC=95=A1=EC=85=98=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=ED=83=80=EC=9D=B4=EB=A8=B8=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/hooks/__test__/useSession.test.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/frontend/src/hooks/__test__/useSession.test.ts b/frontend/src/hooks/__test__/useSession.test.ts index 8baba6d5..34d8ebbf 100644 --- a/frontend/src/hooks/__test__/useSession.test.ts +++ b/frontend/src/hooks/__test__/useSession.test.ts @@ -213,8 +213,16 @@ describe("useSession Hook 테스트", () => { }); describe("리액션 기능 테스트", () => { - it("리액션 이벤트 발생", () => { + beforeEach(() => { + jest.useFakeTimers(); mockSocketStore.socket = mockSocket; + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + it("리액션 이벤트 발생 및 타이머 동작", () => { const { result } = renderHook(() => useSession("test-session")); act(() => { @@ -225,6 +233,11 @@ describe("useSession Hook 테스트", () => { roomId: "test-session", reaction: "👍", }); + + act(() => { + jest.advanceTimersByTime(3000); + }); + expect(result.current.reaction).toBe(""); }); }); From 77122362ed3d3fbb783139a633516a7582bd99b5 Mon Sep 17 00:00:00 2001 From: yiseungyun Date: Tue, 19 Nov 2024 01:39:30 +0900 Subject: [PATCH 036/180] =?UTF-8?q?refactor:=20useSession=20=ED=83=80?= =?UTF-8?q?=EC=9E=85=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/hooks/type/session.d.ts | 30 +++++++++++++++++++++++++++ frontend/src/hooks/useSession.ts | 31 +--------------------------- 2 files changed, 31 insertions(+), 30 deletions(-) create mode 100644 frontend/src/hooks/type/session.d.ts diff --git a/frontend/src/hooks/type/session.d.ts b/frontend/src/hooks/type/session.d.ts new file mode 100644 index 00000000..3f540163 --- /dev/null +++ b/frontend/src/hooks/type/session.d.ts @@ -0,0 +1,30 @@ +export type RoomStatus = "PUBLIC" | "PRIVATE"; + +export interface RoomMetadata { + title: string; + status: RoomStatus; + maxParticipants: number; + createdAt: number; + host: string; +} + +export interface AllUsersResponse { + roomMetadata: RoomMetadata; + users: { + [socketId: string]: { + joinTime: number; + nickname: string; + isHost: boolean; + }; + }; +} + +export interface ResponseMasterChanged { + masterNickname: string; + masterSocketId: string; +} + +export interface Participant { + nickname: string; + isHost: boolean; +} \ No newline at end of file diff --git a/frontend/src/hooks/useSession.ts b/frontend/src/hooks/useSession.ts index 14e3a040..e20741b7 100644 --- a/frontend/src/hooks/useSession.ts +++ b/frontend/src/hooks/useSession.ts @@ -4,36 +4,7 @@ import useToast from "@/hooks/useToast"; import useMediaDevices from "@/hooks/useMediaDevices"; import usePeerConnection from "@/hooks/usePeerConnection"; import useSocket from "./useSocket"; - -export type RoomStatus = "PUBLIC" | "PRIVATE"; -export interface RoomMetadata { - title: string; - status: RoomStatus; - maxParticipants: number; - createdAt: number; - host: string; -} - -interface AllUsersResponse { - roomMetadata: RoomMetadata; - users: { - [socketId: string]: { - joinTime: number; - nickname: string; - isHost: boolean; - }; - }; -} - -interface ResponseMasterChanged { - masterNickname: string; - masterSocketId: string; -} - -interface Participant { - nickname: string; - isHost: boolean; -} +import { AllUsersResponse, Participant, ResponseMasterChanged, RoomMetadata } from "./type/session"; export const useSession = (sessionId: string | undefined) => { const { socket } = useSocket(); From bb9e89c02c631e73098454f75e846ba2b6187376 Mon Sep 17 00:00:00 2001 From: yiseungyun Date: Tue, 19 Nov 2024 11:08:01 +0900 Subject: [PATCH 037/180] =?UTF-8?q?refactor:=20useSession=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20mock=20=EA=B0=9D?= =?UTF-8?q?=EC=B2=B4=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hooks/__test__/mocks/useSession.mock.ts | 34 +++++++++++ .../src/hooks/__test__/useSession.test.ts | 57 +++++-------------- frontend/src/hooks/type/session.d.ts | 2 +- frontend/src/hooks/type/session.test.d.ts | 2 +- frontend/src/hooks/useSession.ts | 7 ++- 5 files changed, 57 insertions(+), 45 deletions(-) create mode 100644 frontend/src/hooks/__test__/mocks/useSession.mock.ts diff --git a/frontend/src/hooks/__test__/mocks/useSession.mock.ts b/frontend/src/hooks/__test__/mocks/useSession.mock.ts new file mode 100644 index 00000000..a5996b2c --- /dev/null +++ b/frontend/src/hooks/__test__/mocks/useSession.mock.ts @@ -0,0 +1,34 @@ +import { MockPeerConnections, MockSocket } from "../../type/session.test"; + +export const mockSocket: MockSocket = { + emit: jest.fn(), + on: jest.fn(), + off: jest.fn(), + id: "mock-socket-id", +}; + +export const mockSocketStore = { + socket: null as MockSocket | null, + connect: jest.fn(), + disconnect: jest.fn(), +}; + +export const mockMediaStream = { + getTracks: jest.fn().mockReturnValue([{ stop: jest.fn(), enabled: true }]), +}; + +export const mockToast = { success: jest.fn(), error: jest.fn() }; + +export const mockNavigate = jest.fn(); + +export const mockPeerConnections: MockPeerConnections = { + current: { + "peer-1": { + ontrack: null, + onicecandidate: null, + oniceconnectionstatechange: null, + onconnectionstatechange: null, + close: jest.fn(), + }, + }, +}; diff --git a/frontend/src/hooks/__test__/useSession.test.ts b/frontend/src/hooks/__test__/useSession.test.ts index 34d8ebbf..e2b37f1c 100644 --- a/frontend/src/hooks/__test__/useSession.test.ts +++ b/frontend/src/hooks/__test__/useSession.test.ts @@ -5,40 +5,14 @@ import useMediaDevices from "@/hooks/useMediaDevices"; import usePeerConnection from "@/hooks/usePeerConnection"; import { useNavigate } from "react-router-dom"; import { act } from "react"; -import { MockPeerConnections, MockSocket } from "../type/session.test"; - -const mockSocket: MockSocket = { - emit: jest.fn(), - on: jest.fn(), - off: jest.fn(), - id: "mock-socket-id", -}; - -const mockSocketStore = { - socket: null as MockSocket | null, - connect: jest.fn(), - disconnect: jest.fn(), -}; - -const mockMediaStream = { - getTracks: jest.fn().mockReturnValue([{ stop: jest.fn(), enabled: true }]), -}; - -const mockToast = { success: jest.fn(), error: jest.fn() }; - -const mockNavigate = jest.fn(); - -const mockPeerConnections: MockPeerConnections = { - current: { - "peer-1": { - ontrack: null, - onicecandidate: null, - oniceconnectionstatechange: null, - onconnectionstatechange: null, - close: jest.fn(), - }, - }, -}; +import { + mockMediaStream, + mockNavigate, + mockPeerConnections, + mockSocket, + mockSocketStore, + mockToast, +} from "./mocks/useSession.mock"; // jest.mock: 실제 모듈대신 mock 모듈을 사용하도록 설정 jest.mock("@/hooks/useMediaDevices"); @@ -142,8 +116,11 @@ describe("useSession Hook 테스트", () => { }); describe("스터디룸 입장 테스트", () => { - it("스터디룸 입장 성공", async () => { + beforeEach(() => { mockSocketStore.socket = mockSocket; + }); + + it("스터디룸 입장 성공", async () => { const { result } = renderHook(() => useSession("test-session")); // 1. 닉네임 설정 @@ -167,7 +144,6 @@ describe("useSession Hook 테스트", () => { }); it("세션 ID가 없이 스터디룸 입장", async () => { - mockSocketStore.socket = mockSocket; const { result } = renderHook(() => useSession(undefined)); await act(async () => { @@ -178,7 +154,6 @@ describe("useSession Hook 테스트", () => { }); it("닉네임 없이 스터디룸 입장", async () => { - mockSocketStore.socket = mockSocket; const { result } = renderHook(() => useSession("test-session")); await act(async () => { @@ -190,7 +165,6 @@ describe("useSession Hook 테스트", () => { }); it("미디어 스트림 획득 실패 시 에러 처리", async () => { - mockSocketStore.socket = mockSocket; (useMediaDevices as jest.Mock).mockReturnValue({ ...useMediaDevices(), getMedia: jest.fn().mockResolvedValue(null), @@ -242,10 +216,12 @@ describe("useSession Hook 테스트", () => { }); describe("소켓 이벤트 리스너 테스트", () => { - it("모든 소켓 이벤트 리스너 등록", () => { + beforeEach(() => { mockSocketStore.socket = mockSocket; renderHook(() => useSession("test-session")); + }); + it("모든 소켓 이벤트 리스너 등록", () => { const expectedEvents = [ "all_users", "getOffer", @@ -264,9 +240,6 @@ describe("useSession Hook 테스트", () => { }); it("room_full 이벤트 발생", () => { - mockSocketStore.socket = mockSocket; - renderHook(() => useSession("test-session")); - // room_full 이벤트 핸들러 찾기 const roomFullHandler = mockSocket.on.mock.calls.find( ([event]: [string]) => event === "room_full" diff --git a/frontend/src/hooks/type/session.d.ts b/frontend/src/hooks/type/session.d.ts index 3f540163..e0281334 100644 --- a/frontend/src/hooks/type/session.d.ts +++ b/frontend/src/hooks/type/session.d.ts @@ -27,4 +27,4 @@ export interface ResponseMasterChanged { export interface Participant { nickname: string; isHost: boolean; -} \ No newline at end of file +} diff --git a/frontend/src/hooks/type/session.test.d.ts b/frontend/src/hooks/type/session.test.d.ts index 298dc018..fc14108a 100644 --- a/frontend/src/hooks/type/session.test.d.ts +++ b/frontend/src/hooks/type/session.test.d.ts @@ -17,4 +17,4 @@ export interface MockPeerConnections { current: { [key: string]: MockPeerConnection; }; -} \ No newline at end of file +} diff --git a/frontend/src/hooks/useSession.ts b/frontend/src/hooks/useSession.ts index e20741b7..19bf748d 100644 --- a/frontend/src/hooks/useSession.ts +++ b/frontend/src/hooks/useSession.ts @@ -4,7 +4,12 @@ import useToast from "@/hooks/useToast"; import useMediaDevices from "@/hooks/useMediaDevices"; import usePeerConnection from "@/hooks/usePeerConnection"; import useSocket from "./useSocket"; -import { AllUsersResponse, Participant, ResponseMasterChanged, RoomMetadata } from "./type/session"; +import { + AllUsersResponse, + Participant, + ResponseMasterChanged, + RoomMetadata, +} from "./type/session"; export const useSession = (sessionId: string | undefined) => { const { socket } = useSocket(); From 5b4514cd7b272652db9bb8c05f2d21bc56ce4a55 Mon Sep 17 00:00:00 2001 From: yiseungyun Date: Tue, 19 Nov 2024 14:08:45 +0900 Subject: [PATCH 038/180] =?UTF-8?q?test:=20=EC=A0=95=EB=A6=AC=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=EC=9D=98=20master=5Fchanged,=20room=5Ffinish?= =?UTF-8?q?ed=20=EB=A6=AC=EC=8A=A4=EB=84=88=20=EC=A0=9C=EA=B1=B0=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 --- frontend/src/hooks/__test__/useSession.test.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/frontend/src/hooks/__test__/useSession.test.ts b/frontend/src/hooks/__test__/useSession.test.ts index e2b37f1c..79000726 100644 --- a/frontend/src/hooks/__test__/useSession.test.ts +++ b/frontend/src/hooks/__test__/useSession.test.ts @@ -229,9 +229,9 @@ describe("useSession Hook 테스트", () => { "getCandidate", "user_exit", "room_full", - "reaction", "master_changed", "room_finished", + "reaction", ]; expectedEvents.forEach((event) => { @@ -255,7 +255,7 @@ describe("useSession Hook 테스트", () => { }); }); - describe("정리(Cleanup) 테스트", () => { + describe("정리(Clean up) 테스트", () => { it("언마운트 시 모든 리소스 정리", () => { mockSocketStore.socket = mockSocket; const { unmount } = renderHook(() => useSession("test-session")); @@ -281,6 +281,14 @@ describe("useSession Hook 테스트", () => { ); expect(mockSocket.off).toHaveBeenCalledWith("user_exit"); expect(mockSocket.off).toHaveBeenCalledWith("room_full"); + expect(mockSocket.off).toHaveBeenCalledWith( + "master_changed", + expect.any(Function) + ); + expect(mockSocket.off).toHaveBeenCalledWith( + "room_finished", + expect.any(Function) + ); expect(mockSocket.off).toHaveBeenCalledWith( "reaction", expect.any(Function) From d9afbb8f9eaf1888d41259e08cb8a094e5e9b652 Mon Sep 17 00:00:00 2001 From: Chanwoo Kim Date: Tue, 19 Nov 2024 14:15:33 +0900 Subject: [PATCH 039/180] =?UTF-8?q?refactor:=20`github.strategy.ts`=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=20=EC=9C=84=EC=B9=98=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `auth` 와 관련되며, 여러 `strategy` 가 생길 수 있으므로, `auth/strategy` 에 위치 --- backend/src/auth/auth.module.ts | 2 +- backend/src/{config => auth/strategy}/github.strategy.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename backend/src/{config => auth/strategy}/github.strategy.ts (96%) diff --git a/backend/src/auth/auth.module.ts b/backend/src/auth/auth.module.ts index 1f9253ec..d3ec5168 100644 --- a/backend/src/auth/auth.module.ts +++ b/backend/src/auth/auth.module.ts @@ -1,7 +1,7 @@ import { Module } from "@nestjs/common"; import { AuthController } from "./auth.controller"; import { AuthService } from "./auth.service"; -import { GithubStrategy } from "../config/github.strategy"; +import { GithubStrategy } from "./strategy/github.strategy"; import { UserRepository } from "../user/user.repository"; @Module({ diff --git a/backend/src/config/github.strategy.ts b/backend/src/auth/strategy/github.strategy.ts similarity index 96% rename from backend/src/config/github.strategy.ts rename to backend/src/auth/strategy/github.strategy.ts index e3990baa..52bd3544 100644 --- a/backend/src/config/github.strategy.ts +++ b/backend/src/auth/strategy/github.strategy.ts @@ -2,7 +2,7 @@ import { Injectable } from "@nestjs/common"; import { PassportStrategy } from "@nestjs/passport"; import "dotenv/config"; import { Profile, Strategy } from "passport-github"; -import { AuthService } from "../auth/auth.service"; +import { AuthService } from "../auth.service"; @Injectable() export class GithubStrategy extends PassportStrategy(Strategy, "github") { From 8bad16ccb6d8a49af5daec86313eefcc482b8f5c Mon Sep 17 00:00:00 2001 From: JeongwooSeo Date: Tue, 19 Nov 2024 14:58:11 +0900 Subject: [PATCH 040/180] =?UTF-8?q?refactor:=20=EC=82=AC=EC=9D=B4=EB=93=9C?= =?UTF-8?q?=EB=B0=94=20=EB=A0=88=ED=8F=AC=EC=A7=80=ED=86=A0=EB=A6=AC=20?= =?UTF-8?q?=EC=9D=B4=EB=8F=99=20=EB=B2=84=ED=8A=BC=20=EC=98=86=EC=97=90=20?= =?UTF-8?q?=EA=B9=83=ED=97=88=EB=B8=8C=EC=95=84=EC=9D=B4=EC=BD=98=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/common/Sidebar.tsx | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/frontend/src/components/common/Sidebar.tsx b/frontend/src/components/common/Sidebar.tsx index e9511af7..8a5afa6e 100644 --- a/frontend/src/components/common/Sidebar.tsx +++ b/frontend/src/components/common/Sidebar.tsx @@ -1,14 +1,9 @@ import { Link } from "react-router-dom"; import { ReactElement, useEffect, useState } from "react"; -import { - FaClipboardList, - FaExternalLinkAlt, - FaHome, - FaLayerGroup, -} from "react-icons/fa"; +import { FaClipboardList, FaHome, FaLayerGroup } from "react-icons/fa"; import { MdLogout } from "react-icons/md"; import { FaRegCircleUser } from "react-icons/fa6"; - +import { FaGithub } from "react-icons/fa6"; const Sidebar = () => { const routes = [ { @@ -82,7 +77,7 @@ const Sidebar = () => { target={"_blank"} > - BOOSKIT + BOOSKIT From e19199caa8cc19fe9e2f81e0dc0995033a919ab3 Mon Sep 17 00:00:00 2001 From: JeongwooSeo Date: Tue, 19 Nov 2024 14:58:38 +0900 Subject: [PATCH 041/180] =?UTF-8?q?style:=20=EC=82=AC=EC=9D=B4=EB=93=9C?= =?UTF-8?q?=EB=B0=94=20=EB=A1=9C=EA=B3=A0=20=ED=8F=B0=ED=8A=B8=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/common/Sidebar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/common/Sidebar.tsx b/frontend/src/components/common/Sidebar.tsx index 8a5afa6e..822e0e5e 100644 --- a/frontend/src/components/common/Sidebar.tsx +++ b/frontend/src/components/common/Sidebar.tsx @@ -47,7 +47,7 @@ const Sidebar = () => {
        Preview From 623b6962a2d934eca0d9eb8632038c3b83648ef4 Mon Sep 17 00:00:00 2001 From: JeongwooSeo Date: Tue, 19 Nov 2024 15:26:19 +0900 Subject: [PATCH 042/180] =?UTF-8?q?style:=20=EC=82=AC=EC=9D=B4=EB=93=9C?= =?UTF-8?q?=EB=B0=94=20=EB=8B=A4=ED=81=AC=EB=AA=A8=EB=93=9C=20=EC=A7=80?= =?UTF-8?q?=EC=9B=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/common/Sidebar.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/common/Sidebar.tsx b/frontend/src/components/common/Sidebar.tsx index 822e0e5e..f1b2e32c 100644 --- a/frontend/src/components/common/Sidebar.tsx +++ b/frontend/src/components/common/Sidebar.tsx @@ -41,18 +41,18 @@ const Sidebar = () => { return ( ); }; @@ -98,8 +114,8 @@ const SidebarMenu = ({ isSelected = false, }: SidebarMenuProps) => { const activeClass = isSelected - ? "bg-green-100 text-white" - : "bg-transparent transition-color duration-300 hover:bg-gray-200/30"; + ? "bg-green-100 dark:text-black text-white" + : "bg-transparent dark:text-white text-black transition-color duration-300 hover:bg-gray-200/30"; return (
      • Date: Tue, 19 Nov 2024 16:21:38 +0900 Subject: [PATCH 046/180] =?UTF-8?q?feat:=20typeorm=20=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=20=EB=B6=84=EB=A6=AC,=20strategy=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/app.module.ts | 29 +++---------------- backend/src/auth/auth.module.ts | 2 +- .../strategy}/github.strategy.ts | 2 +- backend/src/config/typeorm.config.ts | 28 ++++++++++++++++++ 4 files changed, 34 insertions(+), 27 deletions(-) rename backend/src/{config => auth/strategy}/github.strategy.ts (96%) create mode 100644 backend/src/config/typeorm.config.ts diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index 90b88f32..deb2f538 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -10,36 +10,15 @@ import { AuthModule } from "./auth/auth.module"; import { UserModule } from "./user/user.module"; import { TypeOrmModule } from "@nestjs/typeorm"; -import { SnakeNamingStrategy } from "typeorm-naming-strategies"; - -import { User } from "./user/user.entity"; - import "dotenv/config"; -import { addTransactionalDataSource } from "typeorm-transactional"; -import { DataSource } from "typeorm"; + +import { createDataSource, typeOrmConfig } from "./config/typeorm.config"; @Module({ imports: [ TypeOrmModule.forRootAsync({ - useFactory() { - return { - type: "mysql", - host: process.env.MYSQL_HOST, - port: parseInt(process.env.MYSQL_PORT) ?? 3306, - username: process.env.MYSQL_USERNAME, - password: process.env.MYSQL_PASSWORD, - database: process.env.MYSQL_DATABASE, - entities: [User], - namingStrategy: new SnakeNamingStrategy(), - synchronize: true, - }; - }, - async dataSourceFactory(options) { - if (!options) { - throw new Error("Invalid options passed"); - } - return addTransactionalDataSource(new DataSource(options)); - }, + useFactory: async () => typeOrmConfig, // 설정 객체를 직접 반환 + dataSourceFactory: async () => await createDataSource(), // 분리된 데이터소스 생성 함수 사용 }), SocketModule, RoomModule, diff --git a/backend/src/auth/auth.module.ts b/backend/src/auth/auth.module.ts index 1f9253ec..d3ec5168 100644 --- a/backend/src/auth/auth.module.ts +++ b/backend/src/auth/auth.module.ts @@ -1,7 +1,7 @@ import { Module } from "@nestjs/common"; import { AuthController } from "./auth.controller"; import { AuthService } from "./auth.service"; -import { GithubStrategy } from "../config/github.strategy"; +import { GithubStrategy } from "./strategy/github.strategy"; import { UserRepository } from "../user/user.repository"; @Module({ diff --git a/backend/src/config/github.strategy.ts b/backend/src/auth/strategy/github.strategy.ts similarity index 96% rename from backend/src/config/github.strategy.ts rename to backend/src/auth/strategy/github.strategy.ts index e3990baa..52bd3544 100644 --- a/backend/src/config/github.strategy.ts +++ b/backend/src/auth/strategy/github.strategy.ts @@ -2,7 +2,7 @@ import { Injectable } from "@nestjs/common"; import { PassportStrategy } from "@nestjs/passport"; import "dotenv/config"; import { Profile, Strategy } from "passport-github"; -import { AuthService } from "../auth/auth.service"; +import { AuthService } from "../auth.service"; @Injectable() export class GithubStrategy extends PassportStrategy(Strategy, "github") { diff --git a/backend/src/config/typeorm.config.ts b/backend/src/config/typeorm.config.ts new file mode 100644 index 00000000..92dce832 --- /dev/null +++ b/backend/src/config/typeorm.config.ts @@ -0,0 +1,28 @@ +import { DataSource, DataSourceOptions } from "typeorm"; +import { SnakeNamingStrategy } from "typeorm-naming-strategies"; +import { User } from "../user/user.entity"; // 엔티티 경로를 수정하세요. +import "dotenv/config"; +import { addTransactionalDataSource } from "typeorm-transactional"; + +export const typeOrmConfig: DataSourceOptions = { + type: "mysql", + host: process.env.MYSQL_HOST, + port: parseInt(process.env.MYSQL_PORT) ?? 3306, + username: process.env.MYSQL_USERNAME, + password: process.env.MYSQL_PASSWORD, + database: process.env.MYSQL_DATABASE, + entities: [User], + namingStrategy: new SnakeNamingStrategy(), + synchronize: true, +}; + +let transactionalDataSource: DataSource | null = null; + +export const createDataSource = async (): Promise => { + if (!transactionalDataSource) { + transactionalDataSource = addTransactionalDataSource( + new DataSource(typeOrmConfig) + ); + } + return transactionalDataSource; +}; From 1df3b3fe48015e154d976f43d1bb9c2ebedc9721 Mon Sep 17 00:00:00 2001 From: JeongwooSeo Date: Tue, 19 Nov 2024 16:29:24 +0900 Subject: [PATCH 047/180] =?UTF-8?q?feat:=20=EC=A7=88=EB=AC=B8=EC=A7=80=20?= =?UTF-8?q?=EB=A6=AC=EC=8A=A4=ED=8A=B8=20=ED=8E=98=EC=9D=B4=EC=A7=80=20rou?= =?UTF-8?q?te=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/routes.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/frontend/src/routes.tsx b/frontend/src/routes.tsx index bbf815be..4f188f19 100644 --- a/frontend/src/routes.tsx +++ b/frontend/src/routes.tsx @@ -4,12 +4,14 @@ import SessionListPage from "./pages/SessionListPage.tsx"; import SessionPage from "./pages/SessionPage"; import ErrorPage from "@/pages/ErrorPage.tsx"; import LoginPage from "@/pages/LoginPage.tsx"; +import QuestionListPage from "@/pages/QuestionListPage.tsx"; export const routes = [ { element: , path: "/", }, + { element: , path: "/session/:sessionId", @@ -18,6 +20,10 @@ export const routes = [ element: , path: "/sessions", }, + { + element: , + path: "/questions", + }, { element: , path: "/login", From fbbd7384c2071c742e4b4aac5283f646aa7bb618 Mon Sep 17 00:00:00 2001 From: twalla26 Date: Tue, 19 Nov 2024 16:29:39 +0900 Subject: [PATCH 048/180] =?UTF-8?q?chore:=20question=20module=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20=EB=B0=8F=20=EC=B4=88=EA=B8=B0=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/app.module.ts | 2 ++ .../src/question/question.controller.spec.ts | 18 ++++++++++++++++++ backend/src/question/question.controller.ts | 4 ++++ backend/src/question/question.entity.ts | 0 backend/src/question/question.module.ts | 9 +++++++++ backend/src/question/question.repository.ts | 0 backend/src/question/question.service.spec.ts | 18 ++++++++++++++++++ backend/src/question/question.service.ts | 4 ++++ 8 files changed, 55 insertions(+) create mode 100644 backend/src/question/question.controller.spec.ts create mode 100644 backend/src/question/question.controller.ts create mode 100644 backend/src/question/question.entity.ts create mode 100644 backend/src/question/question.module.ts create mode 100644 backend/src/question/question.repository.ts create mode 100644 backend/src/question/question.service.spec.ts create mode 100644 backend/src/question/question.service.ts diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index deb2f538..34bc05cf 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -13,6 +13,7 @@ import { TypeOrmModule } from "@nestjs/typeorm"; import "dotenv/config"; import { createDataSource, typeOrmConfig } from "./config/typeorm.config"; +import { QuestionModule } from './question/question.module'; @Module({ imports: [ @@ -25,6 +26,7 @@ import { createDataSource, typeOrmConfig } from "./config/typeorm.config"; RedisModule, AuthModule, UserModule, + QuestionModule, ], controllers: [AppController], providers: [AppService], diff --git a/backend/src/question/question.controller.spec.ts b/backend/src/question/question.controller.spec.ts new file mode 100644 index 00000000..dcb3df1b --- /dev/null +++ b/backend/src/question/question.controller.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from "@nestjs/testing"; +import { QuestionController } from "./question.controller"; + +describe("QuestionController", () => { + let controller: QuestionController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [QuestionController], + }).compile(); + + controller = module.get(QuestionController); + }); + + it("should be defined", () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/backend/src/question/question.controller.ts b/backend/src/question/question.controller.ts new file mode 100644 index 00000000..a919f400 --- /dev/null +++ b/backend/src/question/question.controller.ts @@ -0,0 +1,4 @@ +import { Controller } from "@nestjs/common"; + +@Controller("question") +export class QuestionController {} diff --git a/backend/src/question/question.entity.ts b/backend/src/question/question.entity.ts new file mode 100644 index 00000000..e69de29b diff --git a/backend/src/question/question.module.ts b/backend/src/question/question.module.ts new file mode 100644 index 00000000..c43f5da7 --- /dev/null +++ b/backend/src/question/question.module.ts @@ -0,0 +1,9 @@ +import { Module } from "@nestjs/common"; +import { QuestionController } from "./question.controller"; +import { QuestionService } from "./question.service"; + +@Module({ + controllers: [QuestionController], + providers: [QuestionService], +}) +export class QuestionModule {} diff --git a/backend/src/question/question.repository.ts b/backend/src/question/question.repository.ts new file mode 100644 index 00000000..e69de29b diff --git a/backend/src/question/question.service.spec.ts b/backend/src/question/question.service.spec.ts new file mode 100644 index 00000000..f0174489 --- /dev/null +++ b/backend/src/question/question.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from "@nestjs/testing"; +import { QuestionService } from "./question.service"; + +describe("QuestionService", () => { + let service: QuestionService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [QuestionService], + }).compile(); + + service = module.get(QuestionService); + }); + + it("should be defined", () => { + expect(service).toBeDefined(); + }); +}); diff --git a/backend/src/question/question.service.ts b/backend/src/question/question.service.ts new file mode 100644 index 00000000..062c5d3c --- /dev/null +++ b/backend/src/question/question.service.ts @@ -0,0 +1,4 @@ +import { Injectable } from "@nestjs/common"; + +@Injectable() +export class QuestionService {} From 27efdfd20c1df5eb4b0896392067fe60a713a24f Mon Sep 17 00:00:00 2001 From: JeongwooSeo Date: Tue, 19 Nov 2024 17:08:40 +0900 Subject: [PATCH 049/180] =?UTF-8?q?style:=20=EC=82=AC=EC=9D=B4=EB=93=9C?= =?UTF-8?q?=EB=B0=94=20=EC=83=89=EC=83=81=20=EC=88=98=EC=A0=95=20=EB=B0=8F?= =?UTF-8?q?=20=EC=A0=84=EC=B2=B4=20=EB=B0=B0=EA=B2=BD=EC=83=89=20=EB=8B=A4?= =?UTF-8?q?=ED=81=AC=EB=AA=A8=EB=93=9C=20=EC=A7=80=EC=9B=90=ED=95=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/common/Sidebar.tsx | 4 ++-- frontend/src/index.css | 8 +++++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/common/Sidebar.tsx b/frontend/src/components/common/Sidebar.tsx index c2805303..3884da8d 100644 --- a/frontend/src/components/common/Sidebar.tsx +++ b/frontend/src/components/common/Sidebar.tsx @@ -43,7 +43,7 @@ const Sidebar = () => { return (
      • -
        +
          Date: Tue, 19 Nov 2024 17:09:39 +0900 Subject: [PATCH 050/180] =?UTF-8?q?feat:=20=EC=A7=88=EB=AC=B8=EC=A7=80=20?= =?UTF-8?q?=EB=A6=AC=EC=8A=A4=ED=8A=B8=20=ED=8E=98=EC=9D=B4=EC=A7=80=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 리스트 페이지 구현 - 질문지 미리보기 컴포넌트 구현 - 다크모드 지원 - 반응형 레이아웃 지원 --- .../questions/QuestionsPreviewCard.tsx | 52 +++++++++++ frontend/src/pages/QuestionListPage.tsx | 93 +++++++++++++++++++ 2 files changed, 145 insertions(+) create mode 100644 frontend/src/components/questions/QuestionsPreviewCard.tsx create mode 100644 frontend/src/pages/QuestionListPage.tsx diff --git a/frontend/src/components/questions/QuestionsPreviewCard.tsx b/frontend/src/components/questions/QuestionsPreviewCard.tsx new file mode 100644 index 00000000..3ddb166a --- /dev/null +++ b/frontend/src/components/questions/QuestionsPreviewCard.tsx @@ -0,0 +1,52 @@ +import { FaStar, FaUsers } from "react-icons/fa6"; + +interface QuestionCardProps { + id: number; + title: string; + questionCount: number; + usage: number; + isStarred: boolean; + category: string; +} + +const QuestionCard = ({ + title, + questionCount, + usage, + isStarred, + category, +}: QuestionCardProps) => { + return ( +
          +
          + + {category} + + +
          + +

          + {title} +

          + +
          +
          + {questionCount} + 문항 +
          +
          + + {usage} +
          +
          +
          + ); +}; + +export default QuestionCard; diff --git a/frontend/src/pages/QuestionListPage.tsx b/frontend/src/pages/QuestionListPage.tsx new file mode 100644 index 00000000..98c630a3 --- /dev/null +++ b/frontend/src/pages/QuestionListPage.tsx @@ -0,0 +1,93 @@ +import { FaPlus } from "react-icons/fa6"; +import Sidebar from "@components/common/Sidebar.tsx"; +import SearchBar from "@components/common/SearchBar.tsx"; +import QuestionsPreviewCard from "@components/questions/QuestionsPreviewCard.tsx"; + +const QuestionList = () => { + // 더미 데이터 + const questionLists = [ + { + id: 1, + title: "프론트엔드 기술 면접", + questionCount: 25, + usage: 128, + isStarred: true, + category: "Frontend", + }, + { + id: 2, + title: "React 심화 면접 질문", + questionCount: 30, + usage: 89, + isStarred: false, + category: "React", + }, + { + id: 3, + title: "JavaScript 핵심 개념", + questionCount: 40, + usage: 156, + isStarred: true, + category: "JavaScript", + }, + { + id: 4, + title: "웹 성능 최적화", + questionCount: 20, + usage: 67, + isStarred: false, + category: "Performance", + }, + { + id: 5, + title: "CSS 레이아웃 마스터", + questionCount: 15, + usage: 45, + isStarred: false, + category: "CSS", + }, + { + id: 6, + title: "웹 접근성과 SEO", + questionCount: 35, + usage: 92, + isStarred: true, + category: "Accessibility", + }, + ]; + + return ( +
          + +
          +
          +

          + 질문지 목록 +

          +
          + + +
          +
          + +
          + {questionLists.map((list) => ( + + ))} +
          +
          +
          + ); +}; + +export default QuestionList; From a16e4922b776ac5848f69df14f447b03a1187dfc Mon Sep 17 00:00:00 2001 From: JeongwooSeo Date: Tue, 19 Nov 2024 17:14:40 +0900 Subject: [PATCH 051/180] =?UTF-8?q?style:=20=EC=84=9C=EC=B9=98=EB=B0=94=20?= =?UTF-8?q?=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20margin=20bottom=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/common/SearchBar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/common/SearchBar.tsx b/frontend/src/components/common/SearchBar.tsx index ad104517..44b07dd7 100644 --- a/frontend/src/components/common/SearchBar.tsx +++ b/frontend/src/components/common/SearchBar.tsx @@ -5,7 +5,7 @@ interface Props { const SearchBar = ({ text }: Props) => { return ( -
          +
          Date: Tue, 19 Nov 2024 17:20:33 +0900 Subject: [PATCH 052/180] =?UTF-8?q?feat:=20=EA=B3=B5=ED=86=B5=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20Select=20=EB=B6=84=EB=A6=AC=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - SearchBar 컴포넌트 처럼 여러번 재사용 가능성이 있는 컴포넌트 --- frontend/src/components/common/Select.tsx | 30 +++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 frontend/src/components/common/Select.tsx diff --git a/frontend/src/components/common/Select.tsx b/frontend/src/components/common/Select.tsx new file mode 100644 index 00000000..4b958d22 --- /dev/null +++ b/frontend/src/components/common/Select.tsx @@ -0,0 +1,30 @@ +import { IoChevronDownSharp } from "react-icons/io5"; + +type Option = { + label: string; + value: string; +}; + +interface SelectProps { + options: Option[]; + backgroundColor?: string; +} + +const Select = ({ options, backgroundColor = "bg-green-200" }: SelectProps) => { + return ( +
          + + + + +
          + ); +}; + +export default Select; From c82d13b5f3260f8e6544e68b336fe15340232ed1 Mon Sep 17 00:00:00 2001 From: JeongwooSeo Date: Tue, 19 Nov 2024 17:21:18 +0900 Subject: [PATCH 053/180] =?UTF-8?q?refactor:=20=EA=B8=B0=EC=A1=B4=20select?= =?UTF-8?q?=20=ED=83=9C=EA=B7=B8=EB=A5=BC=20Select=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=EB=A5=BC=20=EC=82=AC=EC=9A=A9=ED=95=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/pages/QuestionListPage.tsx | 14 +++++++++++--- frontend/src/pages/SessionListPage.tsx | 22 ++++++++-------------- 2 files changed, 19 insertions(+), 17 deletions(-) diff --git a/frontend/src/pages/QuestionListPage.tsx b/frontend/src/pages/QuestionListPage.tsx index 98c630a3..7c8e6764 100644 --- a/frontend/src/pages/QuestionListPage.tsx +++ b/frontend/src/pages/QuestionListPage.tsx @@ -2,6 +2,7 @@ import { FaPlus } from "react-icons/fa6"; import Sidebar from "@components/common/Sidebar.tsx"; import SearchBar from "@components/common/SearchBar.tsx"; import QuestionsPreviewCard from "@components/questions/QuestionsPreviewCard.tsx"; +import Select from "@components/common/Select.tsx"; const QuestionList = () => { // 더미 데이터 @@ -64,10 +65,17 @@ const QuestionList = () => {

          질문지 목록

          -
          +
          -
          + + + {options.map((option) => ( + + ))} + + + + +
          + ); +}; + +export default CategorySelector; diff --git a/frontend/src/components/sessions/create/SessionForm/CategorySection/index.tsx b/frontend/src/components/sessions/create/SessionForm/CategorySection/index.tsx index c89c046d..078fda6c 100644 --- a/frontend/src/components/sessions/create/SessionForm/CategorySection/index.tsx +++ b/frontend/src/components/sessions/create/SessionForm/CategorySection/index.tsx @@ -1,33 +1,30 @@ -import { IoChevronDownSharp } from "react-icons/io5"; import SelectTitle from "@/components/common/SelectTitle"; import useSessionFormStore from "@/stores/useSessionFormStore"; +import CategorySelector from "@/components/common/CategorySelector"; + +const options = [ + { + value: "프론트엔드", + label: "프론트엔드", + }, + { + value: "백엔드", + label: "백엔드", + }, +]; const CategorySection = () => { const { category, setCategory } = useSessionFormStore(); - const changeHandler = (event: React.ChangeEvent) => { - setCategory(event.target.value); - }; - return (
          -
          - - - - -
          +
          ); }; From 89b7e412cbb8d4ca26785a182b8ade767825291d Mon Sep 17 00:00:00 2001 From: yiseungyun Date: Tue, 19 Nov 2024 21:43:28 +0900 Subject: [PATCH 065/180] =?UTF-8?q?feat:=20=EC=83=9D=EC=84=B1=20=ED=8F=BC?= =?UTF-8?q?=20=ED=83=80=EC=9D=B4=ED=8B=80=20=EC=9E=85=EB=A0=A5=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=EB=A5=BC=20=EA=B3=B5=ED=86=B5=20?= =?UTF-8?q?=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=EB=A1=9C=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/CategorySelector/index.tsx | 4 +-- .../components/common/TitleInput/index.tsx | 25 +++++++++++++++++++ .../SessionForm/AccessSection/index.tsx | 10 +++++--- .../ListSelectModal/CategoryTab/Category.tsx | 5 ++-- .../create/SessionForm/NameSection/index.tsx | 12 +++------ 5 files changed, 39 insertions(+), 17 deletions(-) create mode 100644 frontend/src/components/common/TitleInput/index.tsx diff --git a/frontend/src/components/common/CategorySelector/index.tsx b/frontend/src/components/common/CategorySelector/index.tsx index 3ec52453..84b524a7 100644 --- a/frontend/src/components/common/CategorySelector/index.tsx +++ b/frontend/src/components/common/CategorySelector/index.tsx @@ -5,7 +5,7 @@ interface Option { label: string; } -interface CategoryOptions { +interface CategoryProps { title: string; options: Option[]; value: string; @@ -17,7 +17,7 @@ const CategorySelector = ({ options, value, onChange, -}: CategoryOptions) => { +}: CategoryProps) => { const changeHandler = (event: React.ChangeEvent) => { onChange(event.target.value); }; diff --git a/frontend/src/components/common/TitleInput/index.tsx b/frontend/src/components/common/TitleInput/index.tsx new file mode 100644 index 00000000..ea1cd271 --- /dev/null +++ b/frontend/src/components/common/TitleInput/index.tsx @@ -0,0 +1,25 @@ +interface TitleProps { + placeholder: string; + onChange: (title: string) => void; +} + +const TitleInput = ({ + placeholder, + onChange +}: TitleProps) => { + const changeHandler = (event: React.ChangeEvent) => { + onChange(event.target.value); + }; + + return ( + + ); +}; + +export default TitleInput; diff --git a/frontend/src/components/sessions/create/SessionForm/AccessSection/index.tsx b/frontend/src/components/sessions/create/SessionForm/AccessSection/index.tsx index cc8aed87..384083e9 100644 --- a/frontend/src/components/sessions/create/SessionForm/AccessSection/index.tsx +++ b/frontend/src/components/sessions/create/SessionForm/AccessSection/index.tsx @@ -10,20 +10,22 @@ const AccessSection = () => {
          + +
          + ); +}; + +export default AccessButton; \ No newline at end of file diff --git a/frontend/src/components/sessions/create/SessionForm/AccessSection/index.tsx b/frontend/src/components/sessions/create/SessionForm/AccessSection/index.tsx index 384083e9..b8292366 100644 --- a/frontend/src/components/sessions/create/SessionForm/AccessSection/index.tsx +++ b/frontend/src/components/sessions/create/SessionForm/AccessSection/index.tsx @@ -1,5 +1,6 @@ import useSessionFormStore from "@/stores/useSessionFormStore"; import SelectTitle from "@/components/common/SelectTitle"; +import AccessButton from "@/components/common/AccessButton"; const AccessSection = () => { const { access, setAccess } = useSessionFormStore(); @@ -7,30 +8,10 @@ const AccessSection = () => { return (
          -
          - - -
          +
          ); }; diff --git a/frontend/src/components/sessions/create/SessionForm/NameSection/index.tsx b/frontend/src/components/sessions/create/SessionForm/TitleSection/index.tsx similarity index 88% rename from frontend/src/components/sessions/create/SessionForm/NameSection/index.tsx rename to frontend/src/components/sessions/create/SessionForm/TitleSection/index.tsx index 5e4d8582..49735660 100644 --- a/frontend/src/components/sessions/create/SessionForm/NameSection/index.tsx +++ b/frontend/src/components/sessions/create/SessionForm/TitleSection/index.tsx @@ -2,7 +2,7 @@ import useSessionFormStore from "@/stores/useSessionFormStore"; import SelectTitle from "@/components/common/SelectTitle"; import TitleInput from "@/components/common/TitleInput"; -const NameSection = () => { +const TitleSection = () => { const { setSessionName } = useSessionFormStore(); return ( @@ -16,4 +16,4 @@ const NameSection = () => { ); }; -export default NameSection; +export default TitleSection; diff --git a/frontend/src/components/sessions/create/SessionForm/index.tsx b/frontend/src/components/sessions/create/SessionForm/index.tsx index aceeca8a..7bf73fa8 100644 --- a/frontend/src/components/sessions/create/SessionForm/index.tsx +++ b/frontend/src/components/sessions/create/SessionForm/index.tsx @@ -1,7 +1,7 @@ import AccessSection from "./AccessSection"; import CategorySection from "./CategorySection"; import ParticipantSection from "./ParticipantSection"; -import NameSection from "./NameSection"; +import TitleSection from "./TitleSection"; import QuestionListSection from "./QuestionListSection"; import ListSelectModal from "./ListSelectModal"; import useSessionFormStore from "@/stores/useSessionFormStore"; @@ -70,7 +70,7 @@ const SessionForm = () => {
          - + diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 43d2bbc7..458184c8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2542,13 +2542,6 @@ packages: resolution: {integrity: sha512-qlspKUK7IlSQv2o+5I7yhUd7TxlOG2Vr5LTa3ve2XSNVKAL/n/u/7KLvKmFNimomDIKvZFXWHv0T12mv7rT8Aw==} engines: {node: '>=8'} - howler@2.2.4: - resolution: {integrity: sha512-iARIBPgcQrwtEr+tALF+rapJ8qSc+Set2GJQl7xT1MQzWaVkFebdJhR3alVlSiUf5U7nAANKuj3aWpwerocD5w==} - - html-encoding-sniffer@3.0.0: - resolution: {integrity: sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==} - engines: {node: '>=12'} - html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} @@ -3475,12 +3468,6 @@ packages: peerDependencies: react: '*' - react-is@16.13.1: - resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} - - react-is@17.0.2: - resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} - react-is@18.3.1: resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} @@ -3524,10 +3511,6 @@ packages: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} - redent@3.0.0: - resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} - engines: {node: '>=8'} - redis-errors@1.2.0: resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==} engines: {node: '>=4'} @@ -3539,12 +3522,6 @@ packages: reflect-metadata@0.2.2: resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==} - regenerator-runtime@0.11.1: - resolution: {integrity: sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==} - - regenerator-runtime@0.14.1: - resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} - repeat-string@1.6.1: resolution: {integrity: sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==} engines: {node: '>=0.10'} @@ -8062,10 +8039,6 @@ snapshots: dependencies: react: 18.3.1 - react-is@16.13.1: {} - - react-is@17.0.2: {} - react-is@18.3.1: {} react-lottie@1.2.7(react@18.3.1): @@ -8117,11 +8090,6 @@ snapshots: dependencies: picomatch: 2.3.1 - redent@3.0.0: - dependencies: - indent-string: 4.0.0 - strip-indent: 3.0.0 - redis-errors@1.2.0: {} redis-parser@3.0.0: @@ -8130,10 +8098,6 @@ snapshots: reflect-metadata@0.2.2: {} - regenerator-runtime@0.11.1: {} - - regenerator-runtime@0.14.1: {} - repeat-string@1.6.1: {} require-directory@2.1.1: {} From c44c76cfa654a6f7ce6fd90342a7f43cd35adbf5 Mon Sep 17 00:00:00 2001 From: JeongwooSeo Date: Tue, 19 Nov 2024 22:23:52 +0900 Subject: [PATCH 067/180] =?UTF-8?q?refactor:=20=EA=B7=B8=EB=A6=AC=EB=93=9C?= =?UTF-8?q?=20=EC=B5=9C=EB=8C=80=203=EC=97=B4=EB=A1=9C=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/QuestionListPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/pages/QuestionListPage.tsx b/frontend/src/pages/QuestionListPage.tsx index 98ae68bf..01554271 100644 --- a/frontend/src/pages/QuestionListPage.tsx +++ b/frontend/src/pages/QuestionListPage.tsx @@ -108,7 +108,7 @@ const QuestionList = () => {
          -
          +
          {questionList.map((list) => ( Date: Tue, 19 Nov 2024 22:34:52 +0900 Subject: [PATCH 068/180] =?UTF-8?q?feat:=20=EC=A7=88=EB=AC=B8=EC=A7=80=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../QuestionForm/AccessSection/index.tsx | 19 ++++++++++ .../QuestionForm/CategorySection/data.ts | 14 +++++++ .../QuestionForm/CategorySection/index.tsx | 22 +++++++++++ .../QuestionForm/TitleSection/index.tsx | 19 ++++++++++ .../questions/create/QuestionForm/index.tsx | 16 ++++++++ frontend/src/pages/CreateQuestionPage.tsx | 2 + frontend/src/routes.tsx | 2 +- frontend/src/stores/useQuestionFormStore.ts | 38 +++++++++++++++++++ 8 files changed, 131 insertions(+), 1 deletion(-) create mode 100644 frontend/src/components/questions/create/QuestionForm/AccessSection/index.tsx create mode 100644 frontend/src/components/questions/create/QuestionForm/CategorySection/data.ts create mode 100644 frontend/src/components/questions/create/QuestionForm/CategorySection/index.tsx create mode 100644 frontend/src/components/questions/create/QuestionForm/TitleSection/index.tsx create mode 100644 frontend/src/components/questions/create/QuestionForm/index.tsx create mode 100644 frontend/src/stores/useQuestionFormStore.ts diff --git a/frontend/src/components/questions/create/QuestionForm/AccessSection/index.tsx b/frontend/src/components/questions/create/QuestionForm/AccessSection/index.tsx new file mode 100644 index 00000000..6159c9fa --- /dev/null +++ b/frontend/src/components/questions/create/QuestionForm/AccessSection/index.tsx @@ -0,0 +1,19 @@ +import SelectTitle from "@/components/common/SelectTitle"; +import AccessButton from "@/components/common/AccessButton"; +import useQuestionFormStore from "@/stores/useQuestionFormStore"; + +const AccessSection = () => { + const { access, setAccess } = useQuestionFormStore(); + + return ( +
          + + +
          + ); +}; + +export default AccessSection; diff --git a/frontend/src/components/questions/create/QuestionForm/CategorySection/data.ts b/frontend/src/components/questions/create/QuestionForm/CategorySection/data.ts new file mode 100644 index 00000000..1df01956 --- /dev/null +++ b/frontend/src/components/questions/create/QuestionForm/CategorySection/data.ts @@ -0,0 +1,14 @@ +export const options = [ + { + value: "운영체제", + label: "운영체제", + }, + { + value: "네트워크", + label: "네트워크", + }, + { + value: "프론트엔드", + label: "프론트엔드" + } +]; diff --git a/frontend/src/components/questions/create/QuestionForm/CategorySection/index.tsx b/frontend/src/components/questions/create/QuestionForm/CategorySection/index.tsx new file mode 100644 index 00000000..2ddebccf --- /dev/null +++ b/frontend/src/components/questions/create/QuestionForm/CategorySection/index.tsx @@ -0,0 +1,22 @@ +import SelectTitle from "@/components/common/SelectTitle"; +import CategorySelector from "@/components/common/CategorySelector"; +import { options } from "./data"; +import useQuestionFormStore from "@/stores/useQuestionFormStore"; + +const CategorySection = () => { + const { category, setCategory } = useQuestionFormStore(); + + return ( +
          + + +
          + ); +}; + +export default CategorySection; diff --git a/frontend/src/components/questions/create/QuestionForm/TitleSection/index.tsx b/frontend/src/components/questions/create/QuestionForm/TitleSection/index.tsx new file mode 100644 index 00000000..d71d7de4 --- /dev/null +++ b/frontend/src/components/questions/create/QuestionForm/TitleSection/index.tsx @@ -0,0 +1,19 @@ +import SelectTitle from "@/components/common/SelectTitle"; +import TitleInput from "@/components/common/TitleInput"; +import useQuestionFormStore from "@/stores/useQuestionFormStore"; + +const TitleSection = () => { + const { setQuestionTitle } = useQuestionFormStore(); + + return ( +
          + + +
          + ); +}; + +export default TitleSection; diff --git a/frontend/src/components/questions/create/QuestionForm/index.tsx b/frontend/src/components/questions/create/QuestionForm/index.tsx new file mode 100644 index 00000000..27c824e4 --- /dev/null +++ b/frontend/src/components/questions/create/QuestionForm/index.tsx @@ -0,0 +1,16 @@ +import AccessSection from "./AccessSection"; +import CategorySection from "./CategorySection"; +import TitleSection from "./TitleSection"; + +const QuestionForm = () => { + return ( +
          + + + + +
          + ); +}; + +export default QuestionForm; \ No newline at end of file diff --git a/frontend/src/pages/CreateQuestionPage.tsx b/frontend/src/pages/CreateQuestionPage.tsx index 11ea65dc..49887976 100644 --- a/frontend/src/pages/CreateQuestionPage.tsx +++ b/frontend/src/pages/CreateQuestionPage.tsx @@ -1,3 +1,4 @@ +import QuestionForm from "@/components/questions/create/QuestionForm"; import { IoArrowBackSharp } from "react-icons/io5"; import { useNavigate } from "react-router-dom"; @@ -17,6 +18,7 @@ const CreateQuestionPage = () => {

          면접 스터디를 위한 새로운 질문지를 생성합니다.

          +
          ); }; diff --git a/frontend/src/routes.tsx b/frontend/src/routes.tsx index 5e9701fe..2e8e3302 100644 --- a/frontend/src/routes.tsx +++ b/frontend/src/routes.tsx @@ -29,7 +29,7 @@ export const routes = [ }, { element: , - path: "/questions/create" + path: "/questions/create", }, { element: , diff --git a/frontend/src/stores/useQuestionFormStore.ts b/frontend/src/stores/useQuestionFormStore.ts new file mode 100644 index 00000000..1c1538e2 --- /dev/null +++ b/frontend/src/stores/useQuestionFormStore.ts @@ -0,0 +1,38 @@ +import { create } from "zustand"; + +interface QuestionState { + category: string; + questionTitle: string; + access: "PRIVATE" | "PUBLIC"; + + setCategory: (category: string) => void; + setQuestionTitle: (name: string) => void; + setAccess: (access: "PRIVATE" | "PUBLIC") => void; + resetForm: () => void; + isFormValid: () => boolean; +} + +const initialState = { + category: "", + questionTitle: "", + access: "PUBLIC" as const, +}; + +const useQuestionFormStore = create((set, get) => ({ + ...initialState, + + setCategory: (category) => set({ category }), + setQuestionTitle: (title) => set({ questionTitle: title }), + setAccess: (access) => set({ access }), + + resetForm: () => set(initialState), + isFormValid: () => { + const state = get(); + return ( + state.category.trim() !== "" && + state.questionTitle.trim() !== "" + ); + }, +})); + +export default useQuestionFormStore; From 8957c1bbecf9b70b819f965e64d2d95f8b1c5a11 Mon Sep 17 00:00:00 2001 From: yiseungyun Date: Tue, 19 Nov 2024 22:53:13 +0900 Subject: [PATCH 069/180] =?UTF-8?q?feat:=20=EC=84=B8=EC=85=98=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1,=20=EC=A7=88=EB=AC=B8=20=EC=83=9D=EC=84=B1=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=ED=83=80=EC=9D=B4=ED=8B=80=20=EC=B5=9C=EC=86=8C=20?= =?UTF-8?q?=EA=B8=80=EC=9E=90=20=EC=88=98=20=EA=B2=80=EC=82=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../questions/create/QuestionForm/index.tsx | 16 +++++++++++++++- frontend/src/stores/useQuestionFormStore.ts | 3 ++- frontend/src/stores/useSessionFormStore.ts | 1 + 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/questions/create/QuestionForm/index.tsx b/frontend/src/components/questions/create/QuestionForm/index.tsx index 27c824e4..875c6adc 100644 --- a/frontend/src/components/questions/create/QuestionForm/index.tsx +++ b/frontend/src/components/questions/create/QuestionForm/index.tsx @@ -1,14 +1,28 @@ +import useQuestionFormStore from "@/stores/useQuestionFormStore"; import AccessSection from "./AccessSection"; import CategorySection from "./CategorySection"; import TitleSection from "./TitleSection"; const QuestionForm = () => { + const isValid = useQuestionFormStore((state) => state.isFormValid()); + const submitHandler = () => { + + } + return (
          - +
          ); }; diff --git a/frontend/src/stores/useQuestionFormStore.ts b/frontend/src/stores/useQuestionFormStore.ts index 1c1538e2..3a8c1cc2 100644 --- a/frontend/src/stores/useQuestionFormStore.ts +++ b/frontend/src/stores/useQuestionFormStore.ts @@ -30,7 +30,8 @@ const useQuestionFormStore = create((set, get) => ({ const state = get(); return ( state.category.trim() !== "" && - state.questionTitle.trim() !== "" + state.questionTitle.trim() !== "" && + state.questionTitle.trim().length >= 5 ); }, })); diff --git a/frontend/src/stores/useSessionFormStore.ts b/frontend/src/stores/useSessionFormStore.ts index 16e7287e..4e2df899 100644 --- a/frontend/src/stores/useSessionFormStore.ts +++ b/frontend/src/stores/useSessionFormStore.ts @@ -52,6 +52,7 @@ const useSessionFormStore = create((set, get) => ({ return ( state.category.trim() !== "" && state.sessionName.trim() !== "" && + state.sessionName.trim().length > 5 && state.questionId > 0 && state.questionTitle.trim() !== "" ); From 1d3c9489b32ff2c6b7e2061c74f0d72d3c8477a2 Mon Sep 17 00:00:00 2001 From: JeongwooSeo Date: Tue, 19 Nov 2024 23:37:43 +0900 Subject: [PATCH 070/180] =?UTF-8?q?refactor:=20=ED=86=A0=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=20=EC=8A=A4=ED=83=9D=20=EC=88=9C=EC=84=9C=20=EB=B0=98=EB=8C=80?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/stores/useToastStore.ts | 2 +- pnpm-lock.yaml | 32 +++++++++++++++++++++++++--- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/frontend/src/stores/useToastStore.ts b/frontend/src/stores/useToastStore.ts index a361bf77..d10645e2 100644 --- a/frontend/src/stores/useToastStore.ts +++ b/frontend/src/stores/useToastStore.ts @@ -17,7 +17,7 @@ const useToastStore = create((set) => ({ toasts: [] as Toast[], // 액션 createToast: (toast: Toast) => - set((state) => ({ toasts: [...state.toasts, toast] })), + set((state) => ({ toasts: [toast, ...state.toasts] })), removeToast: (id: number) => set((state) => ({ toasts: state.toasts.filter((toast: Toast) => toast.id !== id), diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6bdc9306..43d2bbc7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2541,7 +2541,14 @@ packages: hexoid@2.0.0: resolution: {integrity: sha512-qlspKUK7IlSQv2o+5I7yhUd7TxlOG2Vr5LTa3ve2XSNVKAL/n/u/7KLvKmFNimomDIKvZFXWHv0T12mv7rT8Aw==} engines: {node: '>=8'} - + + howler@2.2.4: + resolution: {integrity: sha512-iARIBPgcQrwtEr+tALF+rapJ8qSc+Set2GJQl7xT1MQzWaVkFebdJhR3alVlSiUf5U7nAANKuj3aWpwerocD5w==} + + html-encoding-sniffer@3.0.0: + resolution: {integrity: sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==} + engines: {node: '>=12'} + html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} @@ -3468,6 +3475,9 @@ packages: peerDependencies: react: '*' + react-is@16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + react-is@17.0.2: resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} @@ -3514,6 +3524,10 @@ packages: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} + redent@3.0.0: + resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} + engines: {node: '>=8'} + redis-errors@1.2.0: resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==} engines: {node: '>=4'} @@ -3525,6 +3539,9 @@ packages: reflect-metadata@0.2.2: resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==} + regenerator-runtime@0.11.1: + resolution: {integrity: sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==} + regenerator-runtime@0.14.1: resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} @@ -6971,12 +6988,12 @@ snapshots: hexoid@2.0.0: {} + howler@2.2.4: {} + html-encoding-sniffer@3.0.0: dependencies: whatwg-encoding: 2.0.0 - howler@2.2.4: {} - html-escaper@2.0.2: {} http-errors@2.0.0: @@ -8045,6 +8062,8 @@ snapshots: dependencies: react: 18.3.1 + react-is@16.13.1: {} + react-is@17.0.2: {} react-is@18.3.1: {} @@ -8098,6 +8117,11 @@ snapshots: dependencies: picomatch: 2.3.1 + redent@3.0.0: + dependencies: + indent-string: 4.0.0 + strip-indent: 3.0.0 + redis-errors@1.2.0: {} redis-parser@3.0.0: @@ -8106,6 +8130,8 @@ snapshots: reflect-metadata@0.2.2: {} + regenerator-runtime@0.11.1: {} + regenerator-runtime@0.14.1: {} repeat-string@1.6.1: {} From 1fe8316268ef1f5b5608281cc52b4c3e55b97bed Mon Sep 17 00:00:00 2001 From: yiseungyun Date: Tue, 19 Nov 2024 23:44:44 +0900 Subject: [PATCH 071/180] =?UTF-8?q?feat:=20=EC=A7=88=EB=AC=B8=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20=ED=8F=BC=EC=97=90=EC=84=9C=20=EC=9E=85=EB=A0=A5=20?= =?UTF-8?q?=EC=A7=88=EB=AC=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 20개까지 제한 - 질문의 글자 수 제한 - 질문 개수 카운팅 --- .../QuestionInputSection/QuestionInput.tsx | 28 +++++++++++++++++++ .../QuestionInputSection/QuestionList.tsx | 16 +++++++++++ .../QuestionInputSection/QustionItem.tsx | 24 ++++++++++++++++ .../QuestionInputSection/index.tsx | 21 ++++++++++++++ frontend/src/stores/useQuestionFormStore.ts | 14 +++++++++- frontend/tailwind.config.js | 1 + 6 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 frontend/src/components/questions/create/QuestionForm/QuestionInputSection/QuestionInput.tsx create mode 100644 frontend/src/components/questions/create/QuestionForm/QuestionInputSection/QuestionList.tsx create mode 100644 frontend/src/components/questions/create/QuestionForm/QuestionInputSection/QustionItem.tsx create mode 100644 frontend/src/components/questions/create/QuestionForm/QuestionInputSection/index.tsx diff --git a/frontend/src/components/questions/create/QuestionForm/QuestionInputSection/QuestionInput.tsx b/frontend/src/components/questions/create/QuestionForm/QuestionInputSection/QuestionInput.tsx new file mode 100644 index 00000000..6b734b06 --- /dev/null +++ b/frontend/src/components/questions/create/QuestionForm/QuestionInputSection/QuestionInput.tsx @@ -0,0 +1,28 @@ +import { useState } from "react"; +import useQuestionFormStore from "@/stores/useQuestionFormStore"; + +const QuestionInput = () => { + const [inputValue, setInputValue] = useState(""); + const addQuestion = useQuestionFormStore((state) => state.addQuestion); + + const enterHandler = (event: React.KeyboardEvent) => { + if (event.key === "Enter" && inputValue.trim().length >= 10) { + addQuestion(inputValue.trim()); + setInputValue(""); + } + }; + + return ( + <> + setInputValue(e.target.value)} + onKeyDown={enterHandler} + /> + + ); +}; + +export default QuestionInput; diff --git a/frontend/src/components/questions/create/QuestionForm/QuestionInputSection/QuestionList.tsx b/frontend/src/components/questions/create/QuestionForm/QuestionInputSection/QuestionList.tsx new file mode 100644 index 00000000..2b86331e --- /dev/null +++ b/frontend/src/components/questions/create/QuestionForm/QuestionInputSection/QuestionList.tsx @@ -0,0 +1,16 @@ +import useQuestionFormStore from "@/stores/useQuestionFormStore"; +import QuestionItem from "./QustionItem"; + +const QuestionList = () => { + const questions = useQuestionFormStore((state) => state.questionList); + + return ( +
          + {questions.map((question, index) => ( + + ))} +
          + ); +}; + +export default QuestionList; diff --git a/frontend/src/components/questions/create/QuestionForm/QuestionInputSection/QustionItem.tsx b/frontend/src/components/questions/create/QuestionForm/QuestionInputSection/QustionItem.tsx new file mode 100644 index 00000000..e750e604 --- /dev/null +++ b/frontend/src/components/questions/create/QuestionForm/QuestionInputSection/QustionItem.tsx @@ -0,0 +1,24 @@ +import { MdEdit } from "react-icons/md"; +import { RiDeleteBin6Fill } from "react-icons/ri"; + +interface ItemProps { + content: string; +} + +const QuestionItem = ({ content }: ItemProps) => { + return ( +
          + {content} +
          + + +
          +
          + ); +}; + +export default QuestionItem; diff --git a/frontend/src/components/questions/create/QuestionForm/QuestionInputSection/index.tsx b/frontend/src/components/questions/create/QuestionForm/QuestionInputSection/index.tsx new file mode 100644 index 00000000..f92cd87b --- /dev/null +++ b/frontend/src/components/questions/create/QuestionForm/QuestionInputSection/index.tsx @@ -0,0 +1,21 @@ +import SelectTitle from "@/components/common/SelectTitle"; +import QuestionInput from "./QuestionInput"; +import QuestionList from "./QuestionList"; +import useQuestionFormStore from "@/stores/useQuestionFormStore"; + +const QuestionInputSection = () => { + const questionList = useQuestionFormStore((state) => state.questionList); + + return ( +
          +
          + + {questionList.length}/20 +
          + + +
          + ); +}; + +export default QuestionInputSection; diff --git a/frontend/src/stores/useQuestionFormStore.ts b/frontend/src/stores/useQuestionFormStore.ts index 3a8c1cc2..d23c3133 100644 --- a/frontend/src/stores/useQuestionFormStore.ts +++ b/frontend/src/stores/useQuestionFormStore.ts @@ -4,10 +4,12 @@ interface QuestionState { category: string; questionTitle: string; access: "PRIVATE" | "PUBLIC"; + questionList: string[]; setCategory: (category: string) => void; setQuestionTitle: (name: string) => void; setAccess: (access: "PRIVATE" | "PUBLIC") => void; + addQuestion: (question: string) => void; resetForm: () => void; isFormValid: () => boolean; } @@ -16,6 +18,7 @@ const initialState = { category: "", questionTitle: "", access: "PUBLIC" as const, + questionList: [], }; const useQuestionFormStore = create((set, get) => ({ @@ -24,6 +27,14 @@ const useQuestionFormStore = create((set, get) => ({ setCategory: (category) => set({ category }), setQuestionTitle: (title) => set({ questionTitle: title }), setAccess: (access) => set({ access }), + addQuestion: (question: string) => + set((state) => { + const currentQuestions = state.questionList; + if (currentQuestions.length < 20) { + return { questionList: [...currentQuestions, question] }; + } + return state; + }), resetForm: () => set(initialState), isFormValid: () => { @@ -31,7 +42,8 @@ const useQuestionFormStore = create((set, get) => ({ return ( state.category.trim() !== "" && state.questionTitle.trim() !== "" && - state.questionTitle.trim().length >= 5 + state.questionTitle.trim().length >= 5 && + state.questionList.length >= 5 ); }, })); diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js index 3ee18428..03a8bc38 100644 --- a/frontend/tailwind.config.js +++ b/frontend/tailwind.config.js @@ -42,6 +42,7 @@ export default { }, boxShadow: { 8: "0 0 0.2rem 0.125rem rgba(182, 182, 182, 0.08)", + 16: "0 0 0.125rem 0.075rem rgba(182, 182, 182, 0.16)", }, fontSize: { // Bold(700) sizes From 7408aa6dea127d77c78b12afd64cbbf53a9bc9c3 Mon Sep 17 00:00:00 2001 From: Chanwoo Kim Date: Tue, 19 Nov 2024 23:45:05 +0900 Subject: [PATCH 072/180] =?UTF-8?q?test:=20=EC=8A=A4=ED=84=B0=EB=94=94?= =?UTF-8?q?=EC=84=B8=EC=85=98=20=EA=B8=B0=EB=8A=A5=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 단순 `Repository` 는 작성하지 않음 - 비즈니스 로직이 필요한 부분에서는 작성 완료함 - 단순히 호출을 하는지와 로직만 확인하므로, 자세한 테스트 코드는 다시 직접 작성해야함 --- backend/src/room/room.controller.spec.ts | 62 ++++++++++- backend/src/room/room.service.spec.ts | 126 ++++++++++++++++++++++- 2 files changed, 181 insertions(+), 7 deletions(-) diff --git a/backend/src/room/room.controller.spec.ts b/backend/src/room/room.controller.spec.ts index 7536f2ec..51f10620 100644 --- a/backend/src/room/room.controller.spec.ts +++ b/backend/src/room/room.controller.spec.ts @@ -1,18 +1,76 @@ import { Test, TestingModule } from "@nestjs/testing"; import { RoomController } from "./room.controller"; +import { RoomService } from "./room.service"; describe("RoomController", () => { let controller: RoomController; + let roomService: RoomService; + + // RoomService Mock 생성 + const mockRoomService = { + getPublicRoom: jest.fn(), + }; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ controllers: [RoomController], + providers: [ + { + provide: RoomService, + useValue: mockRoomService, + }, + ], }).compile(); controller = module.get(RoomController); + roomService = module.get(RoomService); }); - it("should be defined", () => { - expect(controller).toBeDefined(); + describe("getPublicRooms", () => { + it("공개방 목록을 반환해야 한다", async () => { + // Given + const mockRooms = { + room1: { + title: "Room 1", + status: "PUBLIC", + maxParticipants: 5, + }, + room2: { + title: "Room 2", + status: "PUBLIC", + maxParticipants: 3, + }, + }; + mockRoomService.getPublicRoom.mockResolvedValue(mockRooms); + + // When + const result = await controller.getPublicRooms(); + + // Then + expect(roomService.getPublicRoom).toHaveBeenCalled(); + expect(result).toEqual(mockRooms); + }); + + it("빈 방 목록을 반환해야 한다", async () => { + // Given + const mockEmptyRooms = {}; + mockRoomService.getPublicRoom.mockResolvedValue(mockEmptyRooms); + + // When + const result = await controller.getPublicRooms(); + + // Then + expect(roomService.getPublicRoom).toHaveBeenCalled(); + expect(result).toEqual({}); + }); + + it("서비스 에러 발생 시 예외를 던져야 한다", async () => { + // Given + const error = new Error("Service error"); + mockRoomService.getPublicRoom.mockRejectedValue(error); + + // When & Then + await expect(controller.getPublicRooms()).rejects.toThrow(error); + }); }); }); diff --git a/backend/src/room/room.service.spec.ts b/backend/src/room/room.service.spec.ts index 0ab7dff4..630e7dd1 100644 --- a/backend/src/room/room.service.spec.ts +++ b/backend/src/room/room.service.spec.ts @@ -1,18 +1,134 @@ import { Test, TestingModule } from "@nestjs/testing"; import { RoomService } from "./room.service"; +import { RoomRepository } from "./room.repository"; +import { CreateRoomDto } from "./dto/create-room.dto"; describe("RoomService", () => { - let service: RoomService; + let roomService: RoomService; + + // Mock Repository 생성 + const mockRoomRepository = { + getAllRoom: jest.fn(), + findMyRoomId: jest.fn(), + createRoom: jest.fn(), + addUser: jest.fn(), + getRoomById: jest.fn(), + getRoomMemberConnection: jest.fn(), + checkHost: jest.fn(), + deleteUser: jest.fn(), + getRoomMemberCount: jest.fn(), + getNewHost: jest.fn(), + setNewHost: jest.fn(), + deleteRoom: jest.fn(), + }; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - providers: [RoomService], + providers: [ + RoomService, + { + provide: RoomRepository, + useValue: mockRoomRepository, + }, + ], }).compile(); - service = module.get(RoomService); + roomService = module.get(RoomService); + }); + + describe("getPublicRoom", () => { + it("공개방만 반환해야 한다", async () => { + const mockRooms = { + room1: { status: "PUBLIC", title: "Room 1" }, + room2: { status: "PRIVATE", title: "Room 2" }, + }; + mockRoomRepository.getAllRoom.mockResolvedValue(mockRooms); + + const result = await roomService.getPublicRoom(); + + expect(result.room1).toBeDefined(); + expect(result.room2).toBeUndefined(); + }); + }); + + describe("createRoom", () => { + it("새로운 방을 생성해야 한다", async () => { + const createRoomDto: CreateRoomDto = { + status: "PUBLIC", + title: "Test Room", + socketId: "socket-123", + nickname: "User1", + }; + const mockRoomId = "room-123"; + + mockRoomRepository.createRoom.mockResolvedValue(mockRoomId); + + const result = await roomService.createRoom(createRoomDto); + + expect(result.roomId).toBe(mockRoomId); + expect(result.roomMetadata.title).toBe(createRoomDto.title); + expect(mockRoomRepository.addUser).toHaveBeenCalled(); + }); }); - it("should be defined", () => { - expect(service).toBeDefined(); + describe("joinRoom", () => { + it("존재하는 방에 참가할 수 있어야 한다", async () => { + const mockRoom = { id: "room-123", title: "Test Room" }; + mockRoomRepository.getRoomById.mockResolvedValue(mockRoom); + + const result = await roomService.joinRoom( + "socket-123", + "room-123", + "User1" + ); + + expect(result).toBe(mockRoom); + expect(mockRoomRepository.addUser).toHaveBeenCalled(); + }); + + it("존재하지 않는 방에 참가할 수 없어야 한다", async () => { + mockRoomRepository.getRoomById.mockResolvedValue(null); + + const result = await roomService.joinRoom( + "socket-123", + "invalid-room", + "User1" + ); + + expect(result).toBeNull(); + }); + }); + + describe("leaveRoom", () => { + it("방을 떠날 수 있어야 한다", async () => { + const mockRoomId = "room-123"; + mockRoomRepository.findMyRoomId.mockResolvedValue(mockRoomId); + mockRoomRepository.getRoomMemberCount.mockResolvedValue(2); + + const result = await roomService.leaveRoom("socket-123"); + + expect(mockRoomRepository.deleteUser).toHaveBeenCalled(); + expect(result).toBe(2); + }); + }); + + describe("getRoomMemberConnection", () => { + it("자신을 제외한 멤버 연결 정보를 반환해야 한다", async () => { + const mockConnections = { + "socket-123": { nickname: "User1" }, + "socket-456": { nickname: "User2" }, + }; + mockRoomRepository.getRoomMemberConnection.mockResolvedValue( + mockConnections + ); + + const result = await roomService.getRoomMemberConnection( + "socket-123", + "room-123" + ); + + expect(result["socket-123"]).toBeUndefined(); + expect(result["socket-456"]).toBeDefined(); + }); }); }); From 31d8e41790c8e2ed183609691ea6573bf683d0c1 Mon Sep 17 00:00:00 2001 From: JeongwooSeo Date: Tue, 19 Nov 2024 23:45:16 +0900 Subject: [PATCH 073/180] =?UTF-8?q?refactor:=20=EC=8B=9C=EA=B0=84=20?= =?UTF-8?q?=ED=83=80=EC=9E=84=EC=8A=A4=ED=83=AC=ED=94=84=EC=97=90=200~200?= =?UTF-8?q?=EC=82=AC=EC=9D=B4=EC=9D=98=20=EC=88=98=EB=A5=BC=20=EB=8D=94?= =?UTF-8?q?=ED=95=9C=20=EA=B0=92=EC=9D=84=20=EC=95=84=EC=9D=B4=EB=94=94?= =?UTF-8?q?=EB=A1=9C=20=EC=82=AC=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/hooks/useToast.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/hooks/useToast.ts b/frontend/src/hooks/useToast.ts index fe2ea392..b422dd09 100644 --- a/frontend/src/hooks/useToast.ts +++ b/frontend/src/hooks/useToast.ts @@ -8,7 +8,7 @@ const useToast = () => { const toast = useCallback( (message: string, type: "success" | "error") => { const newToast = { - id: new Date().getTime(), + id: new Date().getTime() + Math.floor(Math.random() * 200), message, type, duration: DURATION || 5000, From 8c79e0ba1cd397c976b824d8f5862bc22be43042 Mon Sep 17 00:00:00 2001 From: Chanwoo Kim Date: Tue, 19 Nov 2024 23:45:40 +0900 Subject: [PATCH 074/180] =?UTF-8?q?fix:=20=EC=8A=A4=ED=84=B0=EB=94=94=20?= =?UTF-8?q?=EB=A0=88=ED=8F=AC=EC=A7=80=ED=86=A0=EB=A6=AC=20=EB=B2=84?= =?UTF-8?q?=EA=B7=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 방 리스트가 없을 경우 빈 객체 반환하도록 수정 --- backend/src/room/room.repository.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/backend/src/room/room.repository.ts b/backend/src/room/room.repository.ts index 52c0ecfb..113ded64 100644 --- a/backend/src/room/room.repository.ts +++ b/backend/src/room/room.repository.ts @@ -17,6 +17,8 @@ export class RoomRepository { const redisMap = await this.redisService.getMap("room:*"); console.log(redisMap); + if (!redisMap) return {}; + return Object.entries(redisMap).reduce( (acc, [roomId, room]) => { acc[roomId.split(":")[1]] = room as Room; From e325ef47bf43b8f9636796b03f7e309a6a98feb6 Mon Sep 17 00:00:00 2001 From: JeongwooSeo Date: Tue, 19 Nov 2024 23:45:43 +0900 Subject: [PATCH 075/180] =?UTF-8?q?fix:=20=ED=99=94=EB=A9=B4=20=EC=8A=A4?= =?UTF-8?q?=ED=81=AC=EB=A1=A4=EC=8B=9C=20=ED=86=A0=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=ED=94=84=EB=A1=9C=EB=B0=94=EC=9D=B4=EB=8D=94=EA=B0=80=20?= =?UTF-8?q?=EA=B0=80=EB=A0=A4=EC=A7=80=EB=8D=98=20=EB=AC=B8=EC=A0=9C?= =?UTF-8?q?=EB=A5=BC=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - position: fixed 사용 --- frontend/src/components/common/ToastProvider.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/common/ToastProvider.tsx b/frontend/src/components/common/ToastProvider.tsx index 6486404e..3c36cce7 100644 --- a/frontend/src/components/common/ToastProvider.tsx +++ b/frontend/src/components/common/ToastProvider.tsx @@ -11,7 +11,7 @@ const ToastProvider = () => { const { toasts, removeToast } = useToastStore(); return ( -
          +
          {toasts.map((toast: Toast) => { return ( Date: Tue, 19 Nov 2024 23:45:54 +0900 Subject: [PATCH 076/180] =?UTF-8?q?test:=20=EC=8A=A4=ED=84=B0=EB=94=94=20?= =?UTF-8?q?=EC=84=B8=EC=85=98=20=EC=9B=B9=EC=86=8C=EC=BC=93=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/room/room.gateway.spec.ts | 164 +++++++++++++++++++++++++- 1 file changed, 161 insertions(+), 3 deletions(-) diff --git a/backend/src/room/room.gateway.spec.ts b/backend/src/room/room.gateway.spec.ts index 446b302b..52ab83da 100644 --- a/backend/src/room/room.gateway.spec.ts +++ b/backend/src/room/room.gateway.spec.ts @@ -1,18 +1,176 @@ import { Test, TestingModule } from "@nestjs/testing"; import { RoomGateway } from "./room.gateway"; +import { RoomService } from "./room.service"; describe("RoomGateway", () => { let gateway: RoomGateway; + let roomService: RoomService; + + // Mock Socket.io 서버와 클라이언트 + let mockServer: any; + let mockClient: any; beforeEach(async () => { + // Mock Service 생성 + const mockRoomService = { + createRoom: jest.fn(), + joinRoom: jest.fn(), + getRoomId: jest.fn(), + checkHost: jest.fn(), + leaveRoom: jest.fn(), + deleteRoom: jest.fn(), + delegateHost: jest.fn(), + finishRoom: jest.fn(), + checkAvailable: jest.fn(), + getRoomMemberConnection: jest.fn(), + }; + const module: TestingModule = await Test.createTestingModule({ - providers: [RoomGateway], + providers: [ + RoomGateway, + { + provide: RoomService, + useValue: mockRoomService, + }, + ], }).compile(); gateway = module.get(RoomGateway); + roomService = module.get(RoomService); + + // Mock Socket.io 서버 설정 + mockServer = { + to: jest.fn().mockReturnThis(), + emit: jest.fn(), + }; + gateway.server = mockServer as any; + + // Mock 클라이언트 설정 + mockClient = { + id: "test-client-id", + join: jest.fn(), + emit: jest.fn(), + }; + }); + + describe("handleCreateRoom", () => { + it("방 생성 성공시 이벤트를 발생시켜야 한다", async () => { + const createRoomData = { + title: "Test Room", + nickname: "Test User", + status: "PUBLIC", + maxParticipants: 5, + }; + + const mockRoomData = { + roomId: "test-room-id", + roomMetadata: { + title: createRoomData.title, + status: createRoomData.status, + maxParticipants: createRoomData.maxParticipants, + }, + }; + + roomService.createRoom = jest.fn().mockResolvedValue(mockRoomData); + + await gateway.handleCreateRoom(mockClient, createRoomData); + + expect(mockClient.join).toHaveBeenCalledWith(mockRoomData.roomId); + expect(mockServer.to).toHaveBeenCalledWith(mockRoomData.roomId); + expect(mockServer.emit).toHaveBeenCalledWith( + "room_created", + mockRoomData + ); + }); }); - it("should be defined", () => { - expect(gateway).toBeDefined(); + describe("handleJoinRoom", () => { + it("방이 가득 찼을 경우 ROOM_FULL 이벤트를 발생시켜야 한다", async () => { + const joinRoomData = { + roomId: "test-room-id", + nickname: "Test User", + }; + + roomService.checkAvailable = jest.fn().mockResolvedValue(false); + + await gateway.handleJoinRoom(mockClient, joinRoomData); + + expect(mockClient.emit).toHaveBeenCalledWith("room_full"); + }); + + it("방 참가 성공시 ALL_USERS 이벤트를 발생시켜야 한다", async () => { + const joinRoomData = { + roomId: "test-room-id", + nickname: "Test User", + }; + + const mockRoom = { + id: "test-room-id", + title: "Test Room", + }; + + const mockUsers = { + "user-1": { nickname: "User 1" }, + }; + + roomService.checkAvailable = jest.fn().mockResolvedValue(true); + roomService.joinRoom = jest.fn().mockResolvedValue(mockRoom); + roomService.getRoomMemberConnection = jest + .fn() + .mockResolvedValue(mockUsers); + + await gateway.handleJoinRoom(mockClient, joinRoomData); + + expect(mockClient.join).toHaveBeenCalledWith(joinRoomData.roomId); + expect(mockClient.emit).toHaveBeenCalledWith("all_users", { + roomMetadata: mockRoom, + users: mockUsers, + }); + }); + }); + + describe("handleLeaveRoom", () => { + it("마지막 사용자가 나갈 경우 방을 삭제해야 한다", async () => { + roomService.getRoomId = jest.fn().mockResolvedValue("test-room-id"); + roomService.checkHost = jest.fn().mockResolvedValue(false); + roomService.leaveRoom = jest.fn().mockResolvedValue(0); + + await gateway.handleLeaveRoom(mockClient); + + expect(roomService.deleteRoom).toHaveBeenCalled(); + }); + + it("호스트가 나갈 경우 새로운 호스트를 지정해야 한다", async () => { + const mockRoomId = "test-room-id"; + const mockNewHost = { + socketId: "new-host-id", + nickname: "New Host", + }; + + roomService.getRoomId = jest.fn().mockResolvedValue(mockRoomId); + roomService.checkHost = jest.fn().mockResolvedValue(true); + roomService.leaveRoom = jest.fn().mockResolvedValue(1); + roomService.delegateHost = jest.fn().mockResolvedValue(mockNewHost); + + await gateway.handleLeaveRoom(mockClient); + + expect(mockServer.to).toHaveBeenCalledWith(mockRoomId); + expect(mockServer.emit).toHaveBeenCalledWith("master_changed", { + masterSocketId: mockNewHost.socketId, + masterNickname: mockNewHost.nickname, + }); + }); + }); + + describe("handleFinishRoom", () => { + it("방 종료시 ROOM_FINISHED 이벤트를 발생시켜야 한다", async () => { + const mockRoomId = "test-room-id"; + roomService.finishRoom = jest.fn().mockResolvedValue(mockRoomId); + + await gateway.handleFinishRoom(mockClient); + + expect(mockServer.to).toHaveBeenCalledWith(mockRoomId); + expect(mockServer.emit).toHaveBeenCalledWith("room_finished"); + }); }); }); From 286321a753609bd5a16eb3c567c80f615a613353 Mon Sep 17 00:00:00 2001 From: Chanwoo Kim Date: Tue, 19 Nov 2024 23:46:03 +0900 Subject: [PATCH 077/180] =?UTF-8?q?test:=20=EB=A0=88=EB=94=94=EC=8A=A4=20?= =?UTF-8?q?=EC=84=9C=EB=B9=84=EC=8A=A4=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/redis/redis.service.spec.ts | 241 +++++++++++++++++++++++- 1 file changed, 237 insertions(+), 4 deletions(-) diff --git a/backend/src/redis/redis.service.spec.ts b/backend/src/redis/redis.service.spec.ts index 19faa0c1..66a5e277 100644 --- a/backend/src/redis/redis.service.spec.ts +++ b/backend/src/redis/redis.service.spec.ts @@ -1,18 +1,251 @@ import { Test, TestingModule } from "@nestjs/testing"; import { RedisService } from "./redis.service"; +// Mock 함수들 생성 +const mockSet = jest.fn().mockResolvedValue("OK"); +const mockGet = jest.fn(); +const mockTtl = jest.fn(); +const mockExpire = jest.fn(); +const mockHget = jest.fn(); +const mockHset = jest.fn(); +const mockDel = jest.fn(); + +const mockScan = jest.fn().mockImplementation(() => { + return Promise.resolve(["0", ["key1", "key2"]]); // 배열 형태로 반환 +}); + +const mockMget = jest.fn().mockImplementation(() => { + return Promise.resolve(["value1", "value2"]); // 배열 형태로 반환 +}); + +// Redis 모듈 모킹 +jest.mock("ioredis", () => { + return { + default: jest.fn().mockImplementation(() => ({ + set: mockSet, + get: mockGet, + ttl: mockTtl, + expire: mockExpire, + scan: mockScan, + hget: mockHget, + hset: mockHset, + del: mockDel, + mget: mockMget, + })), + }; +}); + describe("RedisService", () => { - let service: RedisService; + let redisService: RedisService; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [RedisService], }).compile(); - service = module.get(RedisService); + redisService = module.get(RedisService); + + // 각 테스트 전에 모든 mock 함수 초기화 + jest.clearAllMocks(); + }); + + describe("set", () => { + it("문자열 값을 저장해야 한다", async () => { + const key = "test-key"; + const value = "test-value"; + const ttl = 3600; + + await redisService.set(key, value, ttl); + + expect(mockSet).toHaveBeenCalledWith(key, value, "KEEPTTL"); + expect(mockExpire).toHaveBeenCalledWith(key, ttl); + }); + + it("객체를 JSON 문자열로 변환하여 저장해야 한다", async () => { + const key = "test-key"; + const value = { name: "test" }; + const ttl = 3600; + + await redisService.set(key, value, ttl); + + expect(mockSet).toHaveBeenCalledWith( + key, + JSON.stringify(value), + "KEEPTTL" + ); + expect(mockExpire).toHaveBeenCalledWith(key, ttl); + }); + }); + + describe("get", () => { + it("저장된 값을 조회해야 한다", async () => { + const key = "test-key"; + const value = "test-value"; + mockGet.mockResolvedValue(value); + + const result = await redisService.get(key); + + expect(mockGet).toHaveBeenCalledWith(key); + expect(result).toBe(value); + }); + }); + + describe("getTTL", () => { + it("키의 TTL을 반환해야 한다", async () => { + const key = "test-key"; + const ttlValue = 3600; + mockTtl.mockResolvedValue(ttlValue); + + const result = await redisService.getTTL(key); + + expect(mockTtl).toHaveBeenCalledWith(key); + expect(result).toBe(ttlValue); + }); + }); + + describe("getKeys", () => { + it("패턴에 맞는 모든 키를 반환해야 한다", async () => { + const query = "test*"; + mockScan + .mockResolvedValueOnce(["1", ["key1", "key2"]]) + .mockResolvedValueOnce(["0", ["key3"]]); + + const result = await redisService.getKeys(query); + + expect(result).toEqual(["key1", "key2", "key3"]); + expect(mockScan).toHaveBeenCalledWith( + "0", + "MATCH", + query, + "COUNT", + "100" + ); + }); + }); + + describe("getHashValueByField", () => { + it("해시 필드의 값을 반환해야 한다", async () => { + const key = "hash-key"; + const field = "field1"; + const value = "value1"; + mockHget.mockResolvedValue(value); + + const result = await redisService.getHashValueByField(key, field); + + expect(mockHget).toHaveBeenCalledWith(key, field); + expect(result).toBe(value); + }); }); - it("should be defined", () => { - expect(service).toBeDefined(); + describe("setHashValueByField", () => { + it("해시 필드에 문자열 값을 저장해야 한다", async () => { + const key = "hash-key"; + const field = "field1"; + const value = "test-value"; + + await redisService.setHashValueByField(key, field, value); + + expect(mockHset).toHaveBeenCalledWith(key, field, value); + }); + + it("해시 필드에 객체를 JSON 문자열로 변환하여 저장해야 한다", async () => { + const key = "hash-key"; + const field = "field1"; + const value = { test: "value" }; + + await redisService.setHashValueByField(key, field, value); + + expect(mockHset).toHaveBeenCalledWith( + key, + field, + JSON.stringify(value) + ); + }); + }); + + describe("delete", () => { + it("키들을 삭제해야 한다", async () => { + const keys = ["key1", "key2"]; + + await redisService.delete(...keys); + + expect(mockDel).toHaveBeenCalledWith(...keys); + }); + }); + + describe("getValues", () => { + it("키 패턴에 해당하는 모든 값을 반환해야 한다", async () => { + const query = "test*"; + const keys = ["key1", "key2"]; + const values = ["value1", "value2"]; + + mockScan.mockResolvedValueOnce(["0", keys]); + mockMget.mockResolvedValue(values); + + const result = await redisService.getValues(query); + + expect(result).toEqual(values); + }); + + it("키가 없을 경우 null을 반환해야 한다", async () => { + const query = "test*"; + mockScan.mockResolvedValueOnce(["0", []]); + + const result = await redisService.getValues(query); + + expect(result).toBeNull(); + }); + }); + + describe("getMap", () => { + it("객체 타입으로 맵을 반환해야 한다", async () => { + const query = "test*"; + const keys = ["key1", "key2"]; + const values = ['{"value":1}', '{"value":2}']; + + // getKeys 모킹 + mockScan.mockResolvedValueOnce(["0", keys]); + // getValues를 위한 mget 모킹 + mockMget.mockResolvedValueOnce(values); + + const result = await redisService.getMap(query); + + expect(result).toEqual({ + key1: { value: 1 }, + key2: { value: 2 }, + }); + }); + + it("primitive 타입으로 맵을 반환해야 한다", async () => { + const query = "test*"; + const keys = ["key1", "key2"]; + const values = ["value1", "value2"]; + + // getKeys 모킹 + mockScan.mockResolvedValueOnce(["0", keys]); + // getValues를 위한 mget 모킹 + mockMget.mockResolvedValueOnce(values); + + const result = await redisService.getMap(query, "primitive"); + + expect(result).toEqual({ + key1: "value1", + key2: "value2", + }); + }); + + it("값이 없을 경우 null을 반환해야 한다", async () => { + const query = "test*"; + + // getKeys가 빈 배열을 반환하도록 모킹 + mockScan.mockResolvedValueOnce(["0", []]); + + // getValues가 null을 반환하도록 모킹 + mockMget.mockResolvedValueOnce(null); + + const result = await redisService.getMap(query); + + expect(result).toBeNull(); + }); }); }); From 53e4614d60ac2116a93a34c0f8643570e3988930 Mon Sep 17 00:00:00 2001 From: Chanwoo Kim Date: Tue, 19 Nov 2024 23:46:21 +0900 Subject: [PATCH 078/180] =?UTF-8?q?test:=20`OAuth`=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/auth/auth.service.spec.ts | 121 ++++++++++++++++++++++++-- 1 file changed, 116 insertions(+), 5 deletions(-) diff --git a/backend/src/auth/auth.service.spec.ts b/backend/src/auth/auth.service.spec.ts index e5f2ecf6..042f61ea 100644 --- a/backend/src/auth/auth.service.spec.ts +++ b/backend/src/auth/auth.service.spec.ts @@ -1,18 +1,129 @@ import { Test, TestingModule } from "@nestjs/testing"; import { AuthService } from "./auth.service"; +import { UserRepository } from "../user/user.repository"; +import { Profile } from "passport-github"; + +// typeorm-transactional 모킹 +jest.mock("typeorm-transactional", () => ({ + Transactional: () => () => ({}), + runOnTransactionCommit: () => () => ({}), + runOnTransactionRollback: () => () => ({}), + runOnTransactionComplete: () => () => ({}), + initializeTransactionalContext: () => ({}), +})); describe("AuthService", () => { - let service: AuthService; + let authService: AuthService; + let userRepository: UserRepository; + + // Mock GitHub 프로필 데이터 + const mockGithubProfile: Profile = { + id: "12345", + displayName: "Test User", + username: "testuser", + profileUrl: "https://abcd/", + photos: [], + provider: "github", + _raw: "", + _json: {}, + }; + + // Mock 유저 데이터 + const mockUser = { + id: 1, + loginId: null, + passwordHash: null, + githubId: 12345, + username: "camper_12345", + }; beforeEach(async () => { + // Mock Repository 생성 + const mockUserRepository = { + getUserByGithubId: jest.fn(), + createUser: jest.fn(), + }; + const module: TestingModule = await Test.createTestingModule({ - providers: [AuthService], + providers: [ + AuthService, + { + provide: UserRepository, + useValue: mockUserRepository, + }, + ], }).compile(); - service = module.get(AuthService); + authService = module.get(AuthService); + userRepository = module.get(UserRepository); }); - it("should be defined", () => { - expect(service).toBeDefined(); + describe("githubLogin", () => { + it("기존 사용자가 있을 경우 해당 사용자를 반환해야 한다", async () => { + // Given + jest.spyOn(userRepository, "getUserByGithubId").mockResolvedValue( + mockUser + ); + + // When + const result = await authService.githubLogin(mockGithubProfile); + + // Then + expect(userRepository.getUserByGithubId).toHaveBeenCalledWith( + parseInt(mockGithubProfile.id) + ); + expect(result).toEqual(mockUser); + expect(userRepository.createUser).not.toHaveBeenCalled(); + }); + + it("새로운 사용자의 경우 새 계정을 생성해야 한다", async () => { + // Given + jest.spyOn(userRepository, "getUserByGithubId").mockResolvedValue( + null + ); + jest.spyOn(userRepository, "createUser").mockResolvedValue( + mockUser + ); + + // When + const result = await authService.githubLogin(mockGithubProfile); + + // Then + expect(userRepository.getUserByGithubId).toHaveBeenCalledWith( + parseInt(mockGithubProfile.id) + ); + expect(userRepository.createUser).toHaveBeenCalledWith({ + githubId: parseInt(mockGithubProfile.id), + username: `camper_${mockGithubProfile.id}`, + }); + expect(result).toEqual(mockUser); + }); + + it("getUserByGithubId 에러 발생 시 예외를 던져야 한다", async () => { + // Given + const error = new Error("Database error"); + jest.spyOn(userRepository, "getUserByGithubId").mockRejectedValue( + error + ); + + // When & Then + await expect( + authService.githubLogin(mockGithubProfile) + ).rejects.toThrow(error); + }); + + it("createUser 에러 발생 시 예외를 던져야 한다", async () => { + // Given + const error = new Error("Database error"); + jest.spyOn(userRepository, "getUserByGithubId").mockResolvedValue( + null + ); + jest.spyOn(userRepository, "createUser").mockRejectedValue(error); + + // When & Then + await expect( + authService.githubLogin(mockGithubProfile) + ).rejects.toThrow(error); + }); }); }); From f3148b0ed180174e453642a24860c496f6d4d59f Mon Sep 17 00:00:00 2001 From: twalla26 Date: Tue, 19 Nov 2024 23:51:35 +0900 Subject: [PATCH 079/180] =?UTF-8?q?refactor:=20controller=EC=97=90?= =?UTF-8?q?=EC=84=9C=20service=EB=A1=9C=20DTO=20=EB=84=98=EA=B2=A8?= =?UTF-8?q?=EC=A3=BC=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/create-question-list.dto.ts | 2 +- .../question-list/dto/create-question.dto.ts | 5 ++- backend/src/question-list/dto/question.dto.ts | 5 +++ .../question-list/question-list.controller.ts | 25 +++++++++------ .../src/question-list/question-list.entity.ts | 8 ++++- .../question-list/question-list.repository.ts | 8 ++--- .../question-list/question-list.service.ts | 32 +++++++++---------- 7 files changed, 50 insertions(+), 35 deletions(-) create mode 100644 backend/src/question-list/dto/question.dto.ts diff --git a/backend/src/question-list/dto/create-question-list.dto.ts b/backend/src/question-list/dto/create-question-list.dto.ts index 6b58cf9a..7f627dbe 100644 --- a/backend/src/question-list/dto/create-question-list.dto.ts +++ b/backend/src/question-list/dto/create-question-list.dto.ts @@ -1,4 +1,4 @@ -export interface CreateQuestionListDto { +export class CreateQuestionListDto { title: string; isPublic: boolean; userId: number; diff --git a/backend/src/question-list/dto/create-question.dto.ts b/backend/src/question-list/dto/create-question.dto.ts index 6b275d76..e4cef9f8 100644 --- a/backend/src/question-list/dto/create-question.dto.ts +++ b/backend/src/question-list/dto/create-question.dto.ts @@ -1,5 +1,4 @@ -export interface CreateQuestionDto { - content: string; - index: number; +export class CreateQuestionDto { + contents: string[]; questionListId: number; } diff --git a/backend/src/question-list/dto/question.dto.ts b/backend/src/question-list/dto/question.dto.ts new file mode 100644 index 00000000..4b9e9b8e --- /dev/null +++ b/backend/src/question-list/dto/question.dto.ts @@ -0,0 +1,5 @@ +export class QuestionDto { + content: string; + index: number; + questionListId: number; +} diff --git a/backend/src/question-list/question-list.controller.ts b/backend/src/question-list/question-list.controller.ts index 44c389fb..20d98b6b 100644 --- a/backend/src/question-list/question-list.controller.ts +++ b/backend/src/question-list/question-list.controller.ts @@ -2,6 +2,8 @@ import { Body, Controller, Post, Req, UseGuards } from "@nestjs/common"; import { AuthGuard } from "@nestjs/passport"; import { QuestionListService } from "./question-list.service"; import { UserRepository } from "../user/user.repository"; +import { CreateQuestionListDto } from "./dto/create-question-list.dto"; +import { CreateQuestionDto } from "./dto/create-question.dto"; @Controller("question-list") export class QuestionListController { @@ -13,21 +15,26 @@ export class QuestionListController { @UseGuards(AuthGuard("github")) async createQuestionList( @Req() req, - @Body() body: { title: string; questions: string[]; isPublic: boolean } + @Body() body: { title: string; contents: string[]; isPublic: boolean } ) { - const { title, questions, isPublic } = body; + const { title, contents, isPublic } = body; const user = await this.userRepository.getUserByGithubId(req.user.id); + const createQuestionListDto = new CreateQuestionListDto(); + createQuestionListDto.title = title; + createQuestionListDto.isPublic = isPublic; + createQuestionListDto.userId = user.id; + const createdQuestionList = await this.questionService.createQuestionList( - title, - isPublic, - user.id + createQuestionListDto ); - await this.questionService.createQuestions( - createdQuestionList.id, - questions - ); + + const createQuestionDto = new CreateQuestionDto(); + createQuestionDto.contents = contents; + createQuestionDto.questionListId = createdQuestionList.id; + + await this.questionService.createQuestions(createQuestionDto); return createdQuestionList; } diff --git a/backend/src/question-list/question-list.entity.ts b/backend/src/question-list/question-list.entity.ts index ca81e37f..fa037545 100644 --- a/backend/src/question-list/question-list.entity.ts +++ b/backend/src/question-list/question-list.entity.ts @@ -1,4 +1,10 @@ -import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, OneToMany } from "typeorm"; +import { + Entity, + PrimaryGeneratedColumn, + Column, + ManyToOne, + OneToMany, +} from "typeorm"; import { User } from "../user/user.entity"; import { Question } from "./question.entity"; diff --git a/backend/src/question-list/question-list.repository.ts b/backend/src/question-list/question-list.repository.ts index 4decc09b..09a7b632 100644 --- a/backend/src/question-list/question-list.repository.ts +++ b/backend/src/question-list/question-list.repository.ts @@ -1,9 +1,9 @@ import { Injectable } from "@nestjs/common"; import { DataSource } from "typeorm"; -import { CreateQuestionListDto } from "./dto/create-question-list.dto"; -import { CreateQuestionDto } from "./dto/create-question.dto"; import { QuestionList } from "./question-list.entity"; import { Question } from "./question.entity"; +import { CreateQuestionListDto } from "./dto/create-question-list.dto"; +import { QuestionDto } from "./dto/question.dto"; @Injectable() export class QuestionListRepository { @@ -15,7 +15,7 @@ export class QuestionListRepository { .save(createQuestionListDto); } - async createQuestions(createQuestionDtos: CreateQuestionDto[]) { - return this.dataSource.getRepository(Question).save(createQuestionDtos); + async createQuestions(questionDtos: QuestionDto[]) { + return this.dataSource.getRepository(Question).save(questionDtos); } } diff --git a/backend/src/question-list/question-list.service.ts b/backend/src/question-list/question-list.service.ts index 30cd967b..958dc78a 100644 --- a/backend/src/question-list/question-list.service.ts +++ b/backend/src/question-list/question-list.service.ts @@ -1,34 +1,32 @@ import { Injectable } from "@nestjs/common"; -import { QuestionList } from "./question-list.entity"; -import { Question } from "./question.entity"; import { QuestionListRepository } from "./question-list.repository"; +import { CreateQuestionListDto } from "./dto/create-question-list.dto"; +import { CreateQuestionDto } from "./dto/create-question.dto"; +import { QuestionDto } from "./dto/question.dto"; @Injectable() export class QuestionListService { constructor(private readonly questionRepository: QuestionListRepository) {} - // 질문 생성 메서드 - async createQuestionList(title: string, isPublic: boolean, userId: number) { - const questionList = new QuestionList(); - questionList.title = title; - questionList.isPublic = isPublic; - questionList.userId = userId; - - return this.questionRepository.createQuestionList(questionList); + async createQuestionList(createQuestionListDto: CreateQuestionListDto) { + return this.questionRepository.createQuestionList( + createQuestionListDto + ); } - async createQuestions(questionListId: number, questions: string[]) { - let index = 0; - const questionEntities = questions.map((questionContent) => { - const question = new Question(); - question.content = questionContent; - question.index = index++; + async createQuestions(createQuestionDto: CreateQuestionDto) { + const { contents, questionListId } = createQuestionDto; + + const questionDtos = contents.map((content, index) => { + const question = new QuestionDto(); + question.content = content; + question.index = index; question.questionListId = questionListId; return question; }); - await this.questionRepository.createQuestions(questionEntities); + return await this.questionRepository.createQuestions(questionDtos); } } From 04c7bcf9be6a75d2936c6dee143770e6ee8a3a22 Mon Sep 17 00:00:00 2001 From: twalla26 Date: Wed, 20 Nov 2024 00:00:28 +0900 Subject: [PATCH 080/180] =?UTF-8?q?refactor:=20=ED=81=B4=EB=9D=BC=EC=9D=B4?= =?UTF-8?q?=EC=96=B8=ED=8A=B8=EC=97=90=EA=B2=8C=20=EC=A7=88=EB=AC=B8?= =?UTF-8?q?=EC=A7=80=20=EC=83=9D=EC=84=B1=20=EC=84=B1=EA=B3=B5=20=EC=97=AC?= =?UTF-8?q?=EB=B6=80=20=EC=A0=84=EB=8B=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../question-list/question-list.controller.ts | 52 +++++++++++++------ 1 file changed, 37 insertions(+), 15 deletions(-) diff --git a/backend/src/question-list/question-list.controller.ts b/backend/src/question-list/question-list.controller.ts index 20d98b6b..a9e72036 100644 --- a/backend/src/question-list/question-list.controller.ts +++ b/backend/src/question-list/question-list.controller.ts @@ -17,25 +17,47 @@ export class QuestionListController { @Req() req, @Body() body: { title: string; contents: string[]; isPublic: boolean } ) { - const { title, contents, isPublic } = body; - const user = await this.userRepository.getUserByGithubId(req.user.id); + try { + const { title, contents, isPublic } = body; + const user = await this.userRepository.getUserByGithubId( + req.user.id + ); - const createQuestionListDto = new CreateQuestionListDto(); - createQuestionListDto.title = title; - createQuestionListDto.isPublic = isPublic; - createQuestionListDto.userId = user.id; + // 질문지 DTO 준비 + const createQuestionListDto = new CreateQuestionListDto(); + createQuestionListDto.title = title; + createQuestionListDto.isPublic = isPublic; + createQuestionListDto.userId = user.id; - const createdQuestionList = - await this.questionService.createQuestionList( - createQuestionListDto - ); + // 질문지 생성 + const createdQuestionList = + await this.questionService.createQuestionList( + createQuestionListDto + ); - const createQuestionDto = new CreateQuestionDto(); - createQuestionDto.contents = contents; - createQuestionDto.questionListId = createdQuestionList.id; + // 질문 DTO 준비 + const createQuestionDto = new CreateQuestionDto(); + createQuestionDto.contents = contents; + createQuestionDto.questionListId = createdQuestionList.id; - await this.questionService.createQuestions(createQuestionDto); + // 질문 생성 + const createdQuestions = + await this.questionService.createQuestions(createQuestionDto); - return createdQuestionList; + return { + success: true, + message: "Question list created successfully.", + data: { + createdQuestionList, + createdQuestions, + }, + }; + } catch (error) { + return { + success: false, + message: "Failed to create question list.", + error: error.message, + }; + } } } From 9bd89f49aea75e915446c9ac3dced52c0ed7f233 Mon Sep 17 00:00:00 2001 From: twalla26 Date: Wed, 20 Nov 2024 00:02:14 +0900 Subject: [PATCH 081/180] =?UTF-8?q?refactor:=20dataSource=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/user/user.repository.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/src/user/user.repository.ts b/backend/src/user/user.repository.ts index 0973faf0..98dd2e5d 100644 --- a/backend/src/user/user.repository.ts +++ b/backend/src/user/user.repository.ts @@ -5,7 +5,6 @@ import { CreateUserDto } from "./dto/create-user.dto"; @Injectable() export class UserRepository { - private static dataSource: DataSource; constructor(private dataSource: DataSource) {} getUserByGithubId(githubId: number) { From d09b45392f1ffd2dc6f40b3677515950ef445b8c Mon Sep 17 00:00:00 2001 From: JeongwooSeo Date: Wed, 20 Nov 2024 00:05:21 +0900 Subject: [PATCH 082/180] =?UTF-8?q?refactor:=20=EC=A0=91=EA=B7=BC=EC=84=B1?= =?UTF-8?q?=20=ED=83=9C=EA=B7=B8=EB=A5=BC=20=EC=82=AC=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/common/Toast.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/common/Toast.tsx b/frontend/src/components/common/Toast.tsx index ef98581f..00a301e6 100644 --- a/frontend/src/components/common/Toast.tsx +++ b/frontend/src/components/common/Toast.tsx @@ -11,14 +11,19 @@ const Toast = ({ message, type, removeToast }: ToastProps) => { return (
          -

          +

          {message}

          From 7c7628eb6b40c863459406e9a7a30fe32e5b6796 Mon Sep 17 00:00:00 2001 From: JeongwooSeo Date: Wed, 20 Nov 2024 00:05:51 +0900 Subject: [PATCH 083/180] =?UTF-8?q?refactor:=20timeout=20=ED=81=B4?= =?UTF-8?q?=EB=A6=B0=EC=97=85=ED=95=A8=EC=88=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/hooks/useToast.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/frontend/src/hooks/useToast.ts b/frontend/src/hooks/useToast.ts index b422dd09..7657df5e 100644 --- a/frontend/src/hooks/useToast.ts +++ b/frontend/src/hooks/useToast.ts @@ -1,9 +1,10 @@ import useToastStore from "@/stores/useToastStore"; -import { useCallback } from "react"; +import { useCallback, useEffect, useRef } from "react"; const useToast = () => { const { createToast, removeToast } = useToastStore(); const DURATION = 5000; + const timerIDs = useRef[]>([]); const toast = useCallback( (message: string, type: "success" | "error") => { @@ -15,11 +16,21 @@ const useToast = () => { }; createToast(newToast); - setTimeout(() => removeToast(newToast.id), DURATION); + const id = setTimeout(() => { + removeToast(newToast.id); + timerIDs.current = timerIDs.current.filter((timerId) => timerId !== id); + }, DURATION); + timerIDs.current.push(id); }, [createToast, removeToast] ); + useEffect(() => { + return () => { + timerIDs.current.forEach((id) => clearTimeout(id)); + }; + }, []); + const success = useCallback( (message: string) => { toast(message, "success"); From 35a1f13c1b92d3e25b5dccff02d2a1d0f9a4f543 Mon Sep 17 00:00:00 2001 From: twalla26 Date: Wed, 20 Nov 2024 00:16:24 +0900 Subject: [PATCH 084/180] =?UTF-8?q?refactor:=20dto=EB=A5=BC=20interface?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/create-question-list.dto.ts | 2 +- .../question-list/dto/create-question.dto.ts | 2 +- backend/src/question-list/dto/question.dto.ts | 2 +- .../question-list/question-list.controller.ts | 17 ++++++++++------- .../src/question-list/question-list.service.ts | 9 +++++---- 5 files changed, 18 insertions(+), 14 deletions(-) diff --git a/backend/src/question-list/dto/create-question-list.dto.ts b/backend/src/question-list/dto/create-question-list.dto.ts index 7f627dbe..6b58cf9a 100644 --- a/backend/src/question-list/dto/create-question-list.dto.ts +++ b/backend/src/question-list/dto/create-question-list.dto.ts @@ -1,4 +1,4 @@ -export class CreateQuestionListDto { +export interface CreateQuestionListDto { title: string; isPublic: boolean; userId: number; diff --git a/backend/src/question-list/dto/create-question.dto.ts b/backend/src/question-list/dto/create-question.dto.ts index e4cef9f8..3916d486 100644 --- a/backend/src/question-list/dto/create-question.dto.ts +++ b/backend/src/question-list/dto/create-question.dto.ts @@ -1,4 +1,4 @@ -export class CreateQuestionDto { +export interface CreateQuestionDto { contents: string[]; questionListId: number; } diff --git a/backend/src/question-list/dto/question.dto.ts b/backend/src/question-list/dto/question.dto.ts index 4b9e9b8e..a90f7512 100644 --- a/backend/src/question-list/dto/question.dto.ts +++ b/backend/src/question-list/dto/question.dto.ts @@ -1,4 +1,4 @@ -export class QuestionDto { +export interface QuestionDto { content: string; index: number; questionListId: number; diff --git a/backend/src/question-list/question-list.controller.ts b/backend/src/question-list/question-list.controller.ts index a9e72036..ca7f35f0 100644 --- a/backend/src/question-list/question-list.controller.ts +++ b/backend/src/question-list/question-list.controller.ts @@ -24,10 +24,12 @@ export class QuestionListController { ); // 질문지 DTO 준비 - const createQuestionListDto = new CreateQuestionListDto(); - createQuestionListDto.title = title; - createQuestionListDto.isPublic = isPublic; - createQuestionListDto.userId = user.id; + // const createQuestionListDto = new CreateQuestionListDto(); + const createQuestionListDto: CreateQuestionListDto = { + title, + isPublic, + userId: user.id, + }; // 질문지 생성 const createdQuestionList = @@ -36,9 +38,10 @@ export class QuestionListController { ); // 질문 DTO 준비 - const createQuestionDto = new CreateQuestionDto(); - createQuestionDto.contents = contents; - createQuestionDto.questionListId = createdQuestionList.id; + const createQuestionDto: CreateQuestionDto = { + contents, + questionListId: createdQuestionList.id, + }; // 질문 생성 const createdQuestions = diff --git a/backend/src/question-list/question-list.service.ts b/backend/src/question-list/question-list.service.ts index 958dc78a..adce4959 100644 --- a/backend/src/question-list/question-list.service.ts +++ b/backend/src/question-list/question-list.service.ts @@ -19,10 +19,11 @@ export class QuestionListService { const { contents, questionListId } = createQuestionDto; const questionDtos = contents.map((content, index) => { - const question = new QuestionDto(); - question.content = content; - question.index = index; - question.questionListId = questionListId; + const question: QuestionDto = { + content, + index, + questionListId, + }; return question; }); From 4c1e5ab4ac5c23485fd2f04579a14efe5ef7156a Mon Sep 17 00:00:00 2001 From: yiseungyun Date: Wed, 20 Nov 2024 00:43:32 +0900 Subject: [PATCH 085/180] =?UTF-8?q?feat:=20=EC=A7=88=EB=AC=B8=20=EA=B8=80?= =?UTF-8?q?=EC=9E=90=EC=88=98=2010=EC=9E=90=20=EB=AF=B8=EB=A7=8C=20?= =?UTF-8?q?=EC=8B=9C=20=ED=86=A0=EC=8A=A4=ED=8A=B8=20=EC=97=90=EB=9F=AC=20?= =?UTF-8?q?=EB=B0=8F=20=EC=A7=88=EB=AC=B8=20=EA=B3=A0=EC=9C=A0=20id=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 - 고유 id 추가로 중간 질문 삭제 시에도 리액트에서 변경사항 추적할 수 있게 구현 --- .../QuestionInputSection/QuestionInput.tsx | 15 +++++++++++---- .../QuestionInputSection/QuestionList.tsx | 4 ++-- .../QuestionForm/QuestionInputSection/index.tsx | 4 ++-- frontend/src/stores/useQuestionFormStore.ts | 17 +++++++++++++---- 4 files changed, 28 insertions(+), 12 deletions(-) diff --git a/frontend/src/components/questions/create/QuestionForm/QuestionInputSection/QuestionInput.tsx b/frontend/src/components/questions/create/QuestionForm/QuestionInputSection/QuestionInput.tsx index 6b734b06..3181c848 100644 --- a/frontend/src/components/questions/create/QuestionForm/QuestionInputSection/QuestionInput.tsx +++ b/frontend/src/components/questions/create/QuestionForm/QuestionInputSection/QuestionInput.tsx @@ -1,15 +1,21 @@ import { useState } from "react"; import useQuestionFormStore from "@/stores/useQuestionFormStore"; +import useToast from "@/hooks/useToast"; const QuestionInput = () => { + const toast = useToast(); const [inputValue, setInputValue] = useState(""); const addQuestion = useQuestionFormStore((state) => state.addQuestion); const enterHandler = (event: React.KeyboardEvent) => { - if (event.key === "Enter" && inputValue.trim().length >= 10) { - addQuestion(inputValue.trim()); - setInputValue(""); - } + if (event.key === "Enter") { + if (inputValue.trim().length >= 10) { + addQuestion(inputValue.trim()); + setInputValue(""); + } else { + toast.error("질문은 10자 이상 입력해주세요."); + } + }; }; return ( @@ -20,6 +26,7 @@ const QuestionInput = () => { value={inputValue} onChange={(e) => setInputValue(e.target.value)} onKeyDown={enterHandler} + maxLength={100} /> ); diff --git a/frontend/src/components/questions/create/QuestionForm/QuestionInputSection/QuestionList.tsx b/frontend/src/components/questions/create/QuestionForm/QuestionInputSection/QuestionList.tsx index 2b86331e..97b5617d 100644 --- a/frontend/src/components/questions/create/QuestionForm/QuestionInputSection/QuestionList.tsx +++ b/frontend/src/components/questions/create/QuestionForm/QuestionInputSection/QuestionList.tsx @@ -6,8 +6,8 @@ const QuestionList = () => { return (
          - {questions.map((question, index) => ( - + {questions.map((question) => ( + ))}
          ); diff --git a/frontend/src/components/questions/create/QuestionForm/QuestionInputSection/index.tsx b/frontend/src/components/questions/create/QuestionForm/QuestionInputSection/index.tsx index f92cd87b..3a84e547 100644 --- a/frontend/src/components/questions/create/QuestionForm/QuestionInputSection/index.tsx +++ b/frontend/src/components/questions/create/QuestionForm/QuestionInputSection/index.tsx @@ -8,9 +8,9 @@ const QuestionInputSection = () => { return (
          -
          +
          - {questionList.length}/20 + {questionList.length}/20
          diff --git a/frontend/src/stores/useQuestionFormStore.ts b/frontend/src/stores/useQuestionFormStore.ts index d23c3133..b3cb84ca 100644 --- a/frontend/src/stores/useQuestionFormStore.ts +++ b/frontend/src/stores/useQuestionFormStore.ts @@ -1,15 +1,20 @@ import { create } from "zustand"; +interface Question { + id: string; + content: string; +} + interface QuestionState { category: string; questionTitle: string; access: "PRIVATE" | "PUBLIC"; - questionList: string[]; + questionList: Question[]; setCategory: (category: string) => void; setQuestionTitle: (name: string) => void; setAccess: (access: "PRIVATE" | "PUBLIC") => void; - addQuestion: (question: string) => void; + addQuestion: (content: string) => void; resetForm: () => void; isFormValid: () => boolean; } @@ -27,11 +32,15 @@ const useQuestionFormStore = create((set, get) => ({ setCategory: (category) => set({ category }), setQuestionTitle: (title) => set({ questionTitle: title }), setAccess: (access) => set({ access }), - addQuestion: (question: string) => + addQuestion: (content: string) => set((state) => { const currentQuestions = state.questionList; if (currentQuestions.length < 20) { - return { questionList: [...currentQuestions, question] }; + const newQuestion: Question = { + id: crypto.randomUUID(), + content + } + return { questionList: [...currentQuestions, newQuestion] }; } return state; }), From 9ea6d8ef6135655a1b36a88e2c4060869e371387 Mon Sep 17 00:00:00 2001 From: twalla26 Date: Wed, 20 Nov 2024 00:58:22 +0900 Subject: [PATCH 086/180] =?UTF-8?q?chore:=20=EB=9D=BC=EC=9D=B4=EB=B8=8C?= =?UTF-8?q?=EB=9F=AC=EB=A6=AC=20=EC=84=A4=EC=B9=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/package.json b/backend/package.json index 66cc4b1f..fb9df0b4 100644 --- a/backend/package.json +++ b/backend/package.json @@ -22,6 +22,7 @@ "dependencies": { "@nestjs/common": "^10.0.0", "@nestjs/core": "^10.0.0", + "@nestjs/jwt": "^10.2.0", "@nestjs/passport": "^10.0.3", "@nestjs/platform-express": "^10.0.0", "@nestjs/platform-socket.io": "^10.4.6", From 7eb4e86a8da4c8767aec4ae29a973ae752159fda Mon Sep 17 00:00:00 2001 From: yiseungyun Date: Wed, 20 Nov 2024 02:41:26 +0900 Subject: [PATCH 087/180] =?UTF-8?q?feat:=20=EC=A7=88=EB=AC=B8=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20=ED=8F=BC=EC=97=90=EC=84=9C=20=EC=A7=88=EB=AC=B8=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EC=B6=94=EA=B0=80=EB=90=9C=20?= =?UTF-8?q?=EC=A7=88=EB=AC=B8=20=EC=82=AD=EC=A0=9C/=EC=88=98=EC=A0=95=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../QuestionInputSection/EditInput.tsx | 29 ++++++++++++ .../QuestionInputSection/QuestionInput.tsx | 30 ++++++------ .../QuestionInputSection/QuestionList.tsx | 46 +++++++++++++++++-- .../QuestionInputSection/QustionItem.tsx | 14 +++--- .../QuestionInputSection/index.tsx | 4 +- frontend/src/stores/useQuestionFormStore.ts | 19 +++++++- pnpm-lock.yaml | 36 +++++++++++++++ 7 files changed, 153 insertions(+), 25 deletions(-) create mode 100644 frontend/src/components/questions/create/QuestionForm/QuestionInputSection/EditInput.tsx diff --git a/frontend/src/components/questions/create/QuestionForm/QuestionInputSection/EditInput.tsx b/frontend/src/components/questions/create/QuestionForm/QuestionInputSection/EditInput.tsx new file mode 100644 index 00000000..a5275380 --- /dev/null +++ b/frontend/src/components/questions/create/QuestionForm/QuestionInputSection/EditInput.tsx @@ -0,0 +1,29 @@ +interface EditInputProps { + value: string; + onChange: (value: string) => void; + onSave: () => void; + onCancel: () => void; +} + +const EditInput = ({ value, onChange, onSave, onCancel }: EditInputProps) => { + return ( +
          + onChange(e.target.value)} + maxLength={100} + /> +
          + + +
          +
          + ); +}; + +export default EditInput; diff --git a/frontend/src/components/questions/create/QuestionForm/QuestionInputSection/QuestionInput.tsx b/frontend/src/components/questions/create/QuestionForm/QuestionInputSection/QuestionInput.tsx index 3181c848..48d2fb07 100644 --- a/frontend/src/components/questions/create/QuestionForm/QuestionInputSection/QuestionInput.tsx +++ b/frontend/src/components/questions/create/QuestionForm/QuestionInputSection/QuestionInput.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useEffect, useState } from "react"; import useQuestionFormStore from "@/stores/useQuestionFormStore"; import useToast from "@/hooks/useToast"; @@ -6,29 +6,33 @@ const QuestionInput = () => { const toast = useToast(); const [inputValue, setInputValue] = useState(""); const addQuestion = useQuestionFormStore((state) => state.addQuestion); + const questionList = useQuestionFormStore((state) => state.questionList); const enterHandler = (event: React.KeyboardEvent) => { if (event.key === "Enter") { + event.preventDefault(); + if (inputValue.trim().length >= 10) { addQuestion(inputValue.trim()); - setInputValue(""); } else { toast.error("질문은 10자 이상 입력해주세요."); } - }; + } }; + useEffect(() => { + setInputValue(""); + }, [questionList]); + return ( - <> - setInputValue(e.target.value)} - onKeyDown={enterHandler} - maxLength={100} - /> - + setInputValue(e.target.value)} + onKeyUp={enterHandler} + maxLength={100} + /> ); }; diff --git a/frontend/src/components/questions/create/QuestionForm/QuestionInputSection/QuestionList.tsx b/frontend/src/components/questions/create/QuestionForm/QuestionInputSection/QuestionList.tsx index 97b5617d..e82b7b06 100644 --- a/frontend/src/components/questions/create/QuestionForm/QuestionInputSection/QuestionList.tsx +++ b/frontend/src/components/questions/create/QuestionForm/QuestionInputSection/QuestionList.tsx @@ -1,13 +1,53 @@ +import { useState } from "react"; import useQuestionFormStore from "@/stores/useQuestionFormStore"; import QuestionItem from "./QustionItem"; +import EditInput from "./EditInput"; const QuestionList = () => { - const questions = useQuestionFormStore((state) => state.questionList); + const questionList = useQuestionFormStore((state) => state.questionList); + const deleteQuestion = useQuestionFormStore((state) => state.deleteQuestion); + const updateQuestion = useQuestionFormStore((state) => state.updateQuestion); + + const [editingId, setEditingId] = useState(""); + const [editValue, setEditValue] = useState(""); + + const editHandler = (id: string, content: string) => { + setEditingId(id); + setEditValue(content); + }; + + const handleSave = () => { + if (editingId && editValue.trim()) { + updateQuestion(editingId, editValue.trim()); + setEditingId(""); + setEditValue(""); + } + }; + + const handleCancel = () => { + setEditingId(""); + setEditValue(""); + }; return (
          - {questions.map((question) => ( - + {questionList.map((question) => ( +
          + {editingId === question.id ? ( + + ) : ( + deleteQuestion(question.id)} + onEdit={() => editHandler(question.id, question.content)} + /> + )} +
          ))}
          ); diff --git a/frontend/src/components/questions/create/QuestionForm/QuestionInputSection/QustionItem.tsx b/frontend/src/components/questions/create/QuestionForm/QuestionInputSection/QustionItem.tsx index e750e604..357d6c38 100644 --- a/frontend/src/components/questions/create/QuestionForm/QuestionInputSection/QustionItem.tsx +++ b/frontend/src/components/questions/create/QuestionForm/QuestionInputSection/QustionItem.tsx @@ -3,18 +3,20 @@ import { RiDeleteBin6Fill } from "react-icons/ri"; interface ItemProps { content: string; + onDelete: () => void; + onEdit: () => void; } -const QuestionItem = ({ content }: ItemProps) => { +const QuestionItem = ({ content, onDelete, onEdit }: ItemProps) => { return (
          {content} -
          - -
          diff --git a/frontend/src/components/questions/create/QuestionForm/QuestionInputSection/index.tsx b/frontend/src/components/questions/create/QuestionForm/QuestionInputSection/index.tsx index 3a84e547..d4fe71bb 100644 --- a/frontend/src/components/questions/create/QuestionForm/QuestionInputSection/index.tsx +++ b/frontend/src/components/questions/create/QuestionForm/QuestionInputSection/index.tsx @@ -10,7 +10,9 @@ const QuestionInputSection = () => {
          - {questionList.length}/20 + + {questionList.length}/20 +
          diff --git a/frontend/src/stores/useQuestionFormStore.ts b/frontend/src/stores/useQuestionFormStore.ts index b3cb84ca..fa441b23 100644 --- a/frontend/src/stores/useQuestionFormStore.ts +++ b/frontend/src/stores/useQuestionFormStore.ts @@ -15,6 +15,8 @@ interface QuestionState { setQuestionTitle: (name: string) => void; setAccess: (access: "PRIVATE" | "PUBLIC") => void; addQuestion: (content: string) => void; + deleteQuestion: (id: string) => void; + updateQuestion: (id: string, newContent: string) => void; resetForm: () => void; isFormValid: () => boolean; } @@ -32,19 +34,32 @@ const useQuestionFormStore = create((set, get) => ({ setCategory: (category) => set({ category }), setQuestionTitle: (title) => set({ questionTitle: title }), setAccess: (access) => set({ access }), + addQuestion: (content: string) => set((state) => { const currentQuestions = state.questionList; if (currentQuestions.length < 20) { const newQuestion: Question = { id: crypto.randomUUID(), - content - } + content, + }; return { questionList: [...currentQuestions, newQuestion] }; } return state; }), + deleteQuestion: (id: string) => + set((state) => ({ + questionList: state.questionList.filter((question) => question.id !== id), + })), + + updateQuestion: (id: string, newContent: string) => + set((state) => ({ + questionList: state.questionList.map((question) => + question.id === id ? { ...question, content: newContent } : question + ), + })), + resetForm: () => set(initialState), isFormValid: () => { const state = get(); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 458184c8..43d2bbc7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2542,6 +2542,13 @@ packages: resolution: {integrity: sha512-qlspKUK7IlSQv2o+5I7yhUd7TxlOG2Vr5LTa3ve2XSNVKAL/n/u/7KLvKmFNimomDIKvZFXWHv0T12mv7rT8Aw==} engines: {node: '>=8'} + howler@2.2.4: + resolution: {integrity: sha512-iARIBPgcQrwtEr+tALF+rapJ8qSc+Set2GJQl7xT1MQzWaVkFebdJhR3alVlSiUf5U7nAANKuj3aWpwerocD5w==} + + html-encoding-sniffer@3.0.0: + resolution: {integrity: sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==} + engines: {node: '>=12'} + html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} @@ -3468,6 +3475,12 @@ packages: peerDependencies: react: '*' + react-is@16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + + react-is@17.0.2: + resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + react-is@18.3.1: resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} @@ -3511,6 +3524,10 @@ packages: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} + redent@3.0.0: + resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} + engines: {node: '>=8'} + redis-errors@1.2.0: resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==} engines: {node: '>=4'} @@ -3522,6 +3539,12 @@ packages: reflect-metadata@0.2.2: resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==} + regenerator-runtime@0.11.1: + resolution: {integrity: sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==} + + regenerator-runtime@0.14.1: + resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} + repeat-string@1.6.1: resolution: {integrity: sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==} engines: {node: '>=0.10'} @@ -8039,6 +8062,10 @@ snapshots: dependencies: react: 18.3.1 + react-is@16.13.1: {} + + react-is@17.0.2: {} + react-is@18.3.1: {} react-lottie@1.2.7(react@18.3.1): @@ -8090,6 +8117,11 @@ snapshots: dependencies: picomatch: 2.3.1 + redent@3.0.0: + dependencies: + indent-string: 4.0.0 + strip-indent: 3.0.0 + redis-errors@1.2.0: {} redis-parser@3.0.0: @@ -8098,6 +8130,10 @@ snapshots: reflect-metadata@0.2.2: {} + regenerator-runtime@0.11.1: {} + + regenerator-runtime@0.14.1: {} + repeat-string@1.6.1: {} require-directory@2.1.1: {} From 10f84227fc65c4f1e591da79e631cc005b6e840a Mon Sep 17 00:00:00 2001 From: yiseungyun Date: Wed, 20 Nov 2024 03:46:01 +0900 Subject: [PATCH 088/180] =?UTF-8?q?feat:=20=EC=A7=88=EB=AC=B8=EC=A7=80=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=ED=8F=BC=EC=97=90=EC=84=9C=20=EC=A7=88?= =?UTF-8?q?=EB=AC=B8=20=EC=9E=85=EB=A0=A5=EA=B3=BC=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?=EC=8B=9C=20=EC=9E=85=EB=A0=A5=20=EA=B0=92=EC=97=90=20=EB=94=B0?= =?UTF-8?q?=EB=9D=BC=20=EB=86=92=EC=9D=B4=20=EC=A1=B0=EC=A0=88=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../QuestionInputSection/EditInput.tsx | 46 ++++++++++++++--- .../QuestionInputSection/QuestionInput.tsx | 49 +++++++++++++++---- .../QuestionInputSection/QustionItem.tsx | 2 +- 3 files changed, 80 insertions(+), 17 deletions(-) diff --git a/frontend/src/components/questions/create/QuestionForm/QuestionInputSection/EditInput.tsx b/frontend/src/components/questions/create/QuestionForm/QuestionInputSection/EditInput.tsx index a5275380..d2ae63ba 100644 --- a/frontend/src/components/questions/create/QuestionForm/QuestionInputSection/EditInput.tsx +++ b/frontend/src/components/questions/create/QuestionForm/QuestionInputSection/EditInput.tsx @@ -1,3 +1,5 @@ +import { useEffect, useRef } from "react"; + interface EditInputProps { value: string; onChange: (value: string) => void; @@ -6,19 +8,51 @@ interface EditInputProps { } const EditInput = ({ value, onChange, onSave, onCancel }: EditInputProps) => { + const textareaRef = useRef(null); + + const adjustHeight = () => { + const textarea = textareaRef.current; + if (textarea) { + textarea.style.height = "auto"; + + const defaultHeight = 44; + const scrollHeight = textarea.scrollHeight; + + textarea.style.height = value + ? `${Math.max(defaultHeight, scrollHeight)}px` + : `${defaultHeight}px`; + } + }; + + const changeHandler = (e: React.ChangeEvent) => { + const newValue = e.target.value.slice(0, 100); + onChange(newValue); + }; + + useEffect(() => { + adjustHeight(); + }, [value]); + return (
          - onChange(e.target.value)} - maxLength={100} + onChange={changeHandler} + rows={1} />
          - -
          diff --git a/frontend/src/components/questions/create/QuestionForm/QuestionInputSection/QuestionInput.tsx b/frontend/src/components/questions/create/QuestionForm/QuestionInputSection/QuestionInput.tsx index 48d2fb07..799387bc 100644 --- a/frontend/src/components/questions/create/QuestionForm/QuestionInputSection/QuestionInput.tsx +++ b/frontend/src/components/questions/create/QuestionForm/QuestionInputSection/QuestionInput.tsx @@ -1,38 +1,67 @@ -import { useEffect, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import useQuestionFormStore from "@/stores/useQuestionFormStore"; import useToast from "@/hooks/useToast"; const QuestionInput = () => { const toast = useToast(); + const textareaRef = useRef(null); const [inputValue, setInputValue] = useState(""); const addQuestion = useQuestionFormStore((state) => state.addQuestion); const questionList = useQuestionFormStore((state) => state.questionList); - const enterHandler = (event: React.KeyboardEvent) => { + const adjustHeight = () => { + const textarea = textareaRef.current; + if (textarea) { + textarea.style.height = "auto"; + + const defaultHeight = 44; + const scrollHeight = textarea.scrollHeight; + + textarea.style.height = inputValue + ? `${Math.max(defaultHeight, scrollHeight)}px` + : `${defaultHeight}px`; + } + }; + + const changeHandler = (e: React.ChangeEvent) => { + const newValue = e.target.value.slice(0, 100); + setInputValue(newValue); + }; + + const enterHandler = (event: React.KeyboardEvent) => { if (event.key === "Enter") { event.preventDefault(); if (inputValue.trim().length >= 10) { addQuestion(inputValue.trim()); + setInputValue(""); } else { toast.error("질문은 10자 이상 입력해주세요."); } } }; + useEffect(() => { + adjustHeight(); + }, [inputValue]); + useEffect(() => { setInputValue(""); }, [questionList]); return ( - setInputValue(e.target.value)} - onKeyUp={enterHandler} - maxLength={100} - /> +
          +