diff --git a/frontend/.gitignore b/frontend/.gitignore
index a660195f7..764ced914 100644
--- a/frontend/.gitignore
+++ b/frontend/.gitignore
@@ -16,4 +16,6 @@ build-storybook.log
*.csr
*storybook.log
-coverage/
\ No newline at end of file
+coverage/
+# Sentry Config File
+.env.sentry-build-plugin
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index e58c4e009..5a4edb8cb 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -10,6 +10,8 @@
"license": "ISC",
"dependencies": {
"@channel.io/channel-web-sdk-loader": "^2.0.0",
+ "@sentry/react": "^9.18.0",
+ "@sentry/webpack-plugin": "^3.4.0",
"@tanstack/react-query": "^5.66.0",
"date-fns": "^4.1.0",
"dotenv-webpack": "^8.1.0",
@@ -89,7 +91,6 @@
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
"integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==",
- "dev": true,
"license": "Apache-2.0",
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.5",
@@ -103,7 +104,6 @@
"version": "7.26.2",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz",
"integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-validator-identifier": "^7.25.9",
@@ -118,7 +118,6 @@
"version": "7.26.5",
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.5.tgz",
"integrity": "sha512-XvcZi1KWf88RVbF9wn8MN6tYFloU5qX8KjuF3E1PVBmJ9eypXfs4GRiJwLuTZL0iSnJUKn1BFPa5BPZZJyFzPg==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@@ -128,7 +127,6 @@
"version": "7.26.0",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.0.tgz",
"integrity": "sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@ampproject/remapping": "^2.2.0",
@@ -159,7 +157,6 @@
"version": "7.26.5",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.5.tgz",
"integrity": "sha512-2caSP6fN9I7HOe6nqhtft7V4g7/V/gfDsC3Ag4W7kEzzvRGKqiv0pu0HogPiZ3KaVSoNDhUws6IJjDjpfmYIXw==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.26.5",
@@ -176,7 +173,6 @@
"version": "7.26.5",
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.26.5.tgz",
"integrity": "sha512-IXuyn5EkouFJscIDuFF5EsiSolseme1s0CZB+QxVugqJLYmKdxI1VfIBOst0SUu4rnk2Z7kqTwmoO1lp3HIfnA==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@babel/compat-data": "^7.26.5",
@@ -193,7 +189,6 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz",
"integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@babel/traverse": "^7.25.9",
@@ -207,7 +202,6 @@
"version": "7.26.0",
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz",
"integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-module-imports": "^7.25.9",
@@ -235,7 +229,6 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz",
"integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@@ -245,7 +238,6 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz",
"integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@@ -255,7 +247,6 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz",
"integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@@ -265,7 +256,6 @@
"version": "7.26.0",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.0.tgz",
"integrity": "sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@babel/template": "^7.25.9",
@@ -279,7 +269,6 @@
"version": "7.26.5",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.5.tgz",
"integrity": "sha512-SRJ4jYmXRqV1/Xc+TIVG84WjHBXKlxO9sHQnA2Pf12QQEAp1LOh6kDzNHXcUnbH1QI0FDoPPVOt+vyUDucxpaw==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@babel/types": "^7.26.5"
@@ -547,7 +536,6 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz",
"integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.25.9",
@@ -562,7 +550,6 @@
"version": "7.26.5",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.5.tgz",
"integrity": "sha512-rkOSPOw+AXbgtwUga3U4u8RpoK9FEFWBNAlTpcnkLFjL5CT+oyHNuUUC/xx6XefEJ16r38r8Bc/lfp6rYuHeJQ==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.26.2",
@@ -581,7 +568,6 @@
"version": "11.12.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
"integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=4"
@@ -591,7 +577,6 @@
"version": "7.26.5",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.5.tgz",
"integrity": "sha512-L6mZmwFDK6Cjh1nRCLXpa6no13ZIioJDz7mdkzHv399pThrTa/k0nUlNaenOeh2kWu/iaOQYElEpKPUswUa9Vg==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-string-parser": "^7.25.9",
@@ -2060,6 +2045,451 @@
"integrity": "sha512-iMH3amHthJZ9x3gGmBPmdfim7wLGygC2GciIkw2A6SO8giSn8PHYtRT8OKNH4V+k3SZ6RSnYHcTQxBA7pSWZ3Q==",
"license": "MIT"
},
+ "node_modules/@sentry-internal/browser-utils": {
+ "version": "9.18.0",
+ "resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-9.18.0.tgz",
+ "integrity": "sha512-TwSlmgYpHhe55JpOcVApkM0XcXZh1/cYuEPKPFgeaaPD8BrQrLJJvwKxnonSWXOhdnkJxi4GgK7j7mw57PS4aA==",
+ "license": "MIT",
+ "dependencies": {
+ "@sentry/core": "9.18.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@sentry-internal/feedback": {
+ "version": "9.18.0",
+ "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-9.18.0.tgz",
+ "integrity": "sha512-QlrB8oQK+5bfhbgK6yHF6rLwLNJ9XuGblTc51yVkm4d4jn4W/HDyaNqMfQF+JXdTiFatl8oz2xdKR8kGK8kXyg==",
+ "license": "MIT",
+ "dependencies": {
+ "@sentry/core": "9.18.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@sentry-internal/replay": {
+ "version": "9.18.0",
+ "resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-9.18.0.tgz",
+ "integrity": "sha512-2A32FFwrlZtdpBruvpcLEfucu6BpyqOk3F4Bo5smM/5q7u0pa7q5d9FSY5l3nwKEAFAoLGv3hcCb+8wxMm50xA==",
+ "license": "MIT",
+ "dependencies": {
+ "@sentry-internal/browser-utils": "9.18.0",
+ "@sentry/core": "9.18.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@sentry-internal/replay-canvas": {
+ "version": "9.18.0",
+ "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-9.18.0.tgz",
+ "integrity": "sha512-3DEyQLmHcYgcwJ8n8eMhI6bhhawPuMc2xTT+Az8gXMqCO/X9ZACpipAmhXFjYP9Ptl+w0Vh3nllJw+gXc/DOsg==",
+ "license": "MIT",
+ "dependencies": {
+ "@sentry-internal/replay": "9.18.0",
+ "@sentry/core": "9.18.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@sentry/babel-plugin-component-annotate": {
+ "version": "3.4.0",
+ "resolved": "https://registry.npmjs.org/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-3.4.0.tgz",
+ "integrity": "sha512-tSzfc3aE7m0PM0Aj7HBDet5llH9AB9oc+tBQ8AvOqUSnWodLrNCuWeQszJ7mIBovD3figgCU3h0cvI6U5cDtsg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/@sentry/browser": {
+ "version": "9.18.0",
+ "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-9.18.0.tgz",
+ "integrity": "sha512-0SWfp4J2+mH4lZOcHfyIwt9VoGD7yCGQE1cm0BPcLwKnrVQeXHtUXNYNy8HTHSjTGyoFDhEAYelE/tdA3OLcWQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@sentry-internal/browser-utils": "9.18.0",
+ "@sentry-internal/feedback": "9.18.0",
+ "@sentry-internal/replay": "9.18.0",
+ "@sentry-internal/replay-canvas": "9.18.0",
+ "@sentry/core": "9.18.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@sentry/bundler-plugin-core": {
+ "version": "3.4.0",
+ "resolved": "https://registry.npmjs.org/@sentry/bundler-plugin-core/-/bundler-plugin-core-3.4.0.tgz",
+ "integrity": "sha512-X1Q41AsQ6xcT6hB4wYmBDBukndKM/inT4IsR7pdKLi7ICpX2Qq6lisamBAEPCgEvnLpazSFguaiC0uiwMKAdqw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.18.5",
+ "@sentry/babel-plugin-component-annotate": "3.4.0",
+ "@sentry/cli": "2.42.2",
+ "dotenv": "^16.3.1",
+ "find-up": "^5.0.0",
+ "glob": "^9.3.2",
+ "magic-string": "0.30.8",
+ "unplugin": "1.0.1"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/@sentry/bundler-plugin-core/node_modules/brace-expansion": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
+ "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/@sentry/bundler-plugin-core/node_modules/find-up": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+ "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+ "license": "MIT",
+ "dependencies": {
+ "locate-path": "^6.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@sentry/bundler-plugin-core/node_modules/glob": {
+ "version": "9.3.5",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-9.3.5.tgz",
+ "integrity": "sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q==",
+ "license": "ISC",
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "minimatch": "^8.0.2",
+ "minipass": "^4.2.4",
+ "path-scurry": "^1.6.1"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/@sentry/bundler-plugin-core/node_modules/locate-path": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+ "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+ "license": "MIT",
+ "dependencies": {
+ "p-locate": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@sentry/bundler-plugin-core/node_modules/magic-string": {
+ "version": "0.30.8",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.8.tgz",
+ "integrity": "sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.4.15"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@sentry/bundler-plugin-core/node_modules/minimatch": {
+ "version": "8.0.4",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-8.0.4.tgz",
+ "integrity": "sha512-W0Wvr9HyFXZRGIDgCicunpQ299OKXs9RgZfaukz4qAW/pJhcpUfupc9c+OObPOFueNy8VSrZgEmDtk6Kh4WzDA==",
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/@sentry/bundler-plugin-core/node_modules/p-limit": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+ "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+ "license": "MIT",
+ "dependencies": {
+ "yocto-queue": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@sentry/bundler-plugin-core/node_modules/p-locate": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+ "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+ "license": "MIT",
+ "dependencies": {
+ "p-limit": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@sentry/bundler-plugin-core/node_modules/unplugin": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.0.1.tgz",
+ "integrity": "sha512-aqrHaVBWW1JVKBHmGo33T5TxeL0qWzfvjWokObHA9bYmN7eNDkwOxmLjhioHl9878qDFMAaT51XNroRyuz7WxA==",
+ "license": "MIT",
+ "dependencies": {
+ "acorn": "^8.8.1",
+ "chokidar": "^3.5.3",
+ "webpack-sources": "^3.2.3",
+ "webpack-virtual-modules": "^0.5.0"
+ }
+ },
+ "node_modules/@sentry/bundler-plugin-core/node_modules/webpack-virtual-modules": {
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.5.0.tgz",
+ "integrity": "sha512-kyDivFZ7ZM0BVOUteVbDFhlRt7Ah/CSPwJdi8hBpkK7QLumUqdLtVfm/PX/hkcnrvr0i77fO5+TjZ94Pe+C9iw==",
+ "license": "MIT"
+ },
+ "node_modules/@sentry/cli": {
+ "version": "2.42.2",
+ "resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-2.42.2.tgz",
+ "integrity": "sha512-spb7S/RUumCGyiSTg8DlrCX4bivCNmU/A1hcfkwuciTFGu8l5CDc2I6jJWWZw8/0enDGxuj5XujgXvU5tr4bxg==",
+ "hasInstallScript": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "https-proxy-agent": "^5.0.0",
+ "node-fetch": "^2.6.7",
+ "progress": "^2.0.3",
+ "proxy-from-env": "^1.1.0",
+ "which": "^2.0.2"
+ },
+ "bin": {
+ "sentry-cli": "bin/sentry-cli"
+ },
+ "engines": {
+ "node": ">= 10"
+ },
+ "optionalDependencies": {
+ "@sentry/cli-darwin": "2.42.2",
+ "@sentry/cli-linux-arm": "2.42.2",
+ "@sentry/cli-linux-arm64": "2.42.2",
+ "@sentry/cli-linux-i686": "2.42.2",
+ "@sentry/cli-linux-x64": "2.42.2",
+ "@sentry/cli-win32-i686": "2.42.2",
+ "@sentry/cli-win32-x64": "2.42.2"
+ }
+ },
+ "node_modules/@sentry/cli-darwin": {
+ "version": "2.42.2",
+ "resolved": "https://registry.npmjs.org/@sentry/cli-darwin/-/cli-darwin-2.42.2.tgz",
+ "integrity": "sha512-GtJSuxER7Vrp1IpxdUyRZzcckzMnb4N5KTW7sbTwUiwqARRo+wxS+gczYrS8tdgtmXs5XYhzhs+t4d52ITHMIg==",
+ "license": "BSD-3-Clause",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@sentry/cli-linux-arm": {
+ "version": "2.42.2",
+ "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm/-/cli-linux-arm-2.42.2.tgz",
+ "integrity": "sha512-7udCw+YL9lwq+9eL3WLspvnuG+k5Icg92YE7zsteTzWLwgPVzaxeZD2f8hwhsu+wmL+jNqbpCRmktPteh3i2mg==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "BSD-3-Clause",
+ "optional": true,
+ "os": [
+ "linux",
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@sentry/cli-linux-arm64": {
+ "version": "2.42.2",
+ "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.42.2.tgz",
+ "integrity": "sha512-BOxzI7sgEU5Dhq3o4SblFXdE9zScpz6EXc5Zwr1UDZvzgXZGosUtKVc7d1LmkrHP8Q2o18HcDWtF3WvJRb5Zpw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "BSD-3-Clause",
+ "optional": true,
+ "os": [
+ "linux",
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@sentry/cli-linux-i686": {
+ "version": "2.42.2",
+ "resolved": "https://registry.npmjs.org/@sentry/cli-linux-i686/-/cli-linux-i686-2.42.2.tgz",
+ "integrity": "sha512-Sw/dQp5ZPvKnq3/y7wIJyxTUJYPGoTX/YeMbDs8BzDlu9to2LWV3K3r7hE7W1Lpbaw4tSquUHiQjP5QHCOS7aQ==",
+ "cpu": [
+ "x86",
+ "ia32"
+ ],
+ "license": "BSD-3-Clause",
+ "optional": true,
+ "os": [
+ "linux",
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@sentry/cli-linux-x64": {
+ "version": "2.42.2",
+ "resolved": "https://registry.npmjs.org/@sentry/cli-linux-x64/-/cli-linux-x64-2.42.2.tgz",
+ "integrity": "sha512-mU4zUspAal6TIwlNLBV5oq6yYqiENnCWSxtSQVzWs0Jyq97wtqGNG9U+QrnwjJZ+ta/hvye9fvL2X25D/RxHQw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "BSD-3-Clause",
+ "optional": true,
+ "os": [
+ "linux",
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@sentry/cli-win32-i686": {
+ "version": "2.42.2",
+ "resolved": "https://registry.npmjs.org/@sentry/cli-win32-i686/-/cli-win32-i686-2.42.2.tgz",
+ "integrity": "sha512-iHvFHPGqgJMNqXJoQpqttfsv2GI3cGodeTq4aoVLU/BT3+hXzbV0x1VpvvEhncJkDgDicJpFLM8sEPHb3b8abw==",
+ "cpu": [
+ "x86",
+ "ia32"
+ ],
+ "license": "BSD-3-Clause",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@sentry/cli-win32-x64": {
+ "version": "2.42.2",
+ "resolved": "https://registry.npmjs.org/@sentry/cli-win32-x64/-/cli-win32-x64-2.42.2.tgz",
+ "integrity": "sha512-vPPGHjYoaGmfrU7xhfFxG7qlTBacroz5NdT+0FmDn6692D8IvpNXl1K+eV3Kag44ipJBBeR8g1HRJyx/F/9ACw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "BSD-3-Clause",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@sentry/core": {
+ "version": "9.18.0",
+ "resolved": "https://registry.npmjs.org/@sentry/core/-/core-9.18.0.tgz",
+ "integrity": "sha512-kRVH8BqMiaU2FTHYa68zNlAloS43jl4XtIEHkLKVH/7gUtwRmM4Gqj8P7RTrZdO1Lo7ksYnGj+AG05Z09CRbOw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@sentry/react": {
+ "version": "9.18.0",
+ "resolved": "https://registry.npmjs.org/@sentry/react/-/react-9.18.0.tgz",
+ "integrity": "sha512-1cCLYZrZ2gu6H8eE83DC47kLf+pzD1Rim3dDoOEvwt1F5cD3K/DBeIhoHZaXqBeQxuVyHXOOLXSAC/CIuas5Aw==",
+ "license": "MIT",
+ "dependencies": {
+ "@sentry/browser": "9.18.0",
+ "@sentry/core": "9.18.0",
+ "hoist-non-react-statics": "^3.3.2"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "react": "^16.14.0 || 17.x || 18.x || 19.x"
+ }
+ },
+ "node_modules/@sentry/webpack-plugin": {
+ "version": "3.4.0",
+ "resolved": "https://registry.npmjs.org/@sentry/webpack-plugin/-/webpack-plugin-3.4.0.tgz",
+ "integrity": "sha512-i+nAxxniJV5ovijojjTF5n+Yj08Xk8my+vm8+oo0C0I7xcnI2gOKft6B0sJOq01CNbo85X5m/3/edL0PKoWE9w==",
+ "license": "MIT",
+ "dependencies": {
+ "@sentry/bundler-plugin-core": "3.4.0",
+ "unplugin": "1.0.1",
+ "uuid": "^9.0.0"
+ },
+ "engines": {
+ "node": ">= 14"
+ },
+ "peerDependencies": {
+ "webpack": ">=4.40.0"
+ }
+ },
+ "node_modules/@sentry/webpack-plugin/node_modules/unplugin": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.0.1.tgz",
+ "integrity": "sha512-aqrHaVBWW1JVKBHmGo33T5TxeL0qWzfvjWokObHA9bYmN7eNDkwOxmLjhioHl9878qDFMAaT51XNroRyuz7WxA==",
+ "license": "MIT",
+ "dependencies": {
+ "acorn": "^8.8.1",
+ "chokidar": "^3.5.3",
+ "webpack-sources": "^3.2.3",
+ "webpack-virtual-modules": "^0.5.0"
+ }
+ },
+ "node_modules/@sentry/webpack-plugin/node_modules/uuid": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
+ "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
+ "funding": [
+ "https://github.com/sponsors/broofa",
+ "https://github.com/sponsors/ctavan"
+ ],
+ "license": "MIT",
+ "bin": {
+ "uuid": "dist/bin/uuid"
+ }
+ },
+ "node_modules/@sentry/webpack-plugin/node_modules/webpack-virtual-modules": {
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.5.0.tgz",
+ "integrity": "sha512-kyDivFZ7ZM0BVOUteVbDFhlRt7Ah/CSPwJdi8hBpkK7QLumUqdLtVfm/PX/hkcnrvr0i77fO5+TjZ94Pe+C9iw==",
+ "license": "MIT"
+ },
"node_modules/@sinclair/typebox": {
"version": "0.27.8",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz",
@@ -4932,7 +5362,6 @@
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
- "dev": true,
"license": "MIT",
"dependencies": {
"debug": "4"
@@ -5090,7 +5519,6 @@
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
"integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
- "dev": true,
"license": "ISC",
"dependencies": {
"normalize-path": "^3.0.0",
@@ -5443,7 +5871,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
- "dev": true,
"license": "MIT"
},
"node_modules/base64-arraybuffer": {
@@ -5546,7 +5973,6 @@
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
"integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
@@ -5630,7 +6056,6 @@
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
- "dev": true,
"license": "MIT",
"dependencies": {
"fill-range": "^7.1.1"
@@ -5986,7 +6411,6 @@
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
- "dev": true,
"license": "MIT",
"dependencies": {
"anymatch": "~3.1.2",
@@ -6303,7 +6727,6 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
- "dev": true,
"license": "MIT"
},
"node_modules/cookie": {
@@ -7422,12 +7845,15 @@
}
},
"node_modules/dotenv": {
- "version": "8.6.0",
- "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.6.0.tgz",
- "integrity": "sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==",
+ "version": "16.5.0",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz",
+ "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==",
"license": "BSD-2-Clause",
"engines": {
- "node": ">=10"
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://dotenvx.com"
}
},
"node_modules/dotenv-defaults": {
@@ -7439,6 +7865,15 @@
"dotenv": "^8.2.0"
}
},
+ "node_modules/dotenv-defaults/node_modules/dotenv": {
+ "version": "8.6.0",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.6.0.tgz",
+ "integrity": "sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/dotenv-webpack": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/dotenv-webpack/-/dotenv-webpack-8.1.0.tgz",
@@ -9075,7 +9510,6 @@
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
- "dev": true,
"license": "MIT",
"dependencies": {
"to-regex-range": "^5.0.1"
@@ -9331,14 +9765,12 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
- "dev": true,
"license": "ISC"
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
- "dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
@@ -9394,7 +9826,6 @@
"version": "1.0.0-beta.2",
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
"integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@@ -9529,7 +9960,6 @@
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
- "dev": true,
"license": "ISC",
"dependencies": {
"is-glob": "^4.0.1"
@@ -9879,6 +10309,15 @@
"he": "bin/he"
}
},
+ "node_modules/hoist-non-react-statics": {
+ "version": "3.3.2",
+ "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
+ "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "react-is": "^16.7.0"
+ }
+ },
"node_modules/hpack.js": {
"version": "2.1.6",
"resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz",
@@ -10157,7 +10596,6 @@
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
"integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
- "dev": true,
"license": "MIT",
"dependencies": {
"agent-base": "6",
@@ -10455,7 +10893,6 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
- "dev": true,
"license": "MIT",
"dependencies": {
"binary-extensions": "^2.0.0"
@@ -10575,7 +11012,6 @@
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
@@ -10640,7 +11076,6 @@
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
- "dev": true,
"license": "MIT",
"dependencies": {
"is-extglob": "^2.1.1"
@@ -10708,7 +11143,6 @@
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=0.12.0"
@@ -10949,7 +11383,6 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
- "dev": true,
"license": "ISC"
},
"node_modules/isobject": {
@@ -12177,7 +12610,6 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
- "dev": true,
"license": "MIT"
},
"node_modules/js-yaml": {
@@ -12290,7 +12722,6 @@
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
"integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
- "dev": true,
"license": "MIT",
"bin": {
"jsesc": "bin/jsesc"
@@ -12329,7 +12760,6 @@
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
- "dev": true,
"license": "MIT",
"bin": {
"json5": "lib/cli.js"
@@ -12561,7 +12991,6 @@
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
"integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
- "dev": true,
"license": "ISC",
"dependencies": {
"yallist": "^3.0.2"
@@ -13751,6 +14180,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/minipass": {
+ "version": "4.2.8",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-4.2.8.tgz",
+ "integrity": "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/mitt": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz",
@@ -13849,7 +14287,6 @@
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
- "dev": true,
"license": "MIT",
"dependencies": {
"whatwg-url": "^5.0.0"
@@ -13893,7 +14330,6 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
@@ -14349,7 +14785,6 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
@@ -14382,6 +14817,37 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/path-scurry": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
+ "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "lru-cache": "^10.2.0",
+ "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/path-scurry/node_modules/lru-cache": {
+ "version": "10.4.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
+ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
+ "license": "ISC"
+ },
+ "node_modules/path-scurry/node_modules/minipass": {
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
+ "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ }
+ },
"node_modules/path-to-regexp": {
"version": "0.1.12",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
@@ -14436,7 +14902,6 @@
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=8.6"
@@ -15199,6 +15664,15 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/progress": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
+ "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
"node_modules/promise-polyfill": {
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-8.3.0.tgz",
@@ -15266,6 +15740,12 @@
"node": ">= 0.10"
}
},
+ "node_modules/proxy-from-env": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
+ "license": "MIT"
+ },
"node_modules/psl": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz",
@@ -15485,7 +15965,6 @@
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
- "dev": true,
"license": "MIT"
},
"node_modules/react-markdown": {
@@ -15583,7 +16062,6 @@
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
- "dev": true,
"license": "MIT",
"dependencies": {
"picomatch": "^2.2.1"
@@ -16176,7 +16654,6 @@
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
- "dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
@@ -17462,7 +17939,6 @@
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
- "dev": true,
"license": "MIT",
"dependencies": {
"is-number": "^7.0.0"
@@ -17511,7 +17987,6 @@
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
- "dev": true,
"license": "MIT"
},
"node_modules/tree-dump": {
@@ -18274,7 +18749,6 @@
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
- "dev": true,
"license": "BSD-2-Clause"
},
"node_modules/webpack": {
@@ -18685,7 +19159,6 @@
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
- "dev": true,
"license": "MIT",
"dependencies": {
"tr46": "~0.0.3",
@@ -18696,7 +19169,6 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
- "dev": true,
"license": "ISC",
"dependencies": {
"isexe": "^2.0.0"
@@ -18912,7 +19384,6 @@
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
- "dev": true,
"license": "ISC"
},
"node_modules/yaml": {
@@ -18958,7 +19429,6 @@
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
diff --git a/frontend/package.json b/frontend/package.json
index 1adda2f86..40059cf38 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -21,6 +21,8 @@
"description": "",
"dependencies": {
"@channel.io/channel-web-sdk-loader": "^2.0.0",
+ "@sentry/react": "^9.18.0",
+ "@sentry/webpack-plugin": "^3.4.0",
"@tanstack/react-query": "^5.66.0",
"date-fns": "^4.1.0",
"dotenv-webpack": "^8.1.0",
diff --git a/frontend/src/assets/images/icons/sns/instagram_icon.png b/frontend/src/assets/images/icons/sns/instagram_icon.png
new file mode 100644
index 000000000..87bf66c30
Binary files /dev/null and b/frontend/src/assets/images/icons/sns/instagram_icon.png differ
diff --git a/frontend/src/assets/images/icons/sns/x_icon.svg b/frontend/src/assets/images/icons/sns/x_icon.svg
new file mode 100644
index 000000000..56368dd4b
--- /dev/null
+++ b/frontend/src/assets/images/icons/sns/x_icon.svg
@@ -0,0 +1,3 @@
+
diff --git a/frontend/src/assets/images/icons/sns/youtube_icon.svg b/frontend/src/assets/images/icons/sns/youtube_icon.svg
new file mode 100644
index 000000000..0b6492b62
--- /dev/null
+++ b/frontend/src/assets/images/icons/sns/youtube_icon.svg
@@ -0,0 +1,3 @@
+
diff --git a/frontend/src/components/common/Button/Button.tsx b/frontend/src/components/common/Button/Button.tsx
index 94037751a..e1e4c3d8c 100644
--- a/frontend/src/components/common/Button/Button.tsx
+++ b/frontend/src/components/common/Button/Button.tsx
@@ -4,7 +4,8 @@ import styled, { keyframes, css } from 'styled-components';
export interface ButtonProps {
width?: string;
children: React.ReactNode;
- onClick: () => void;
+ type?: string;
+ onClick?: () => void;
animated?: boolean;
}
@@ -44,9 +45,10 @@ const Button = ({
width,
children,
onClick,
+ type,
animated = false,
}: ButtonProps) => (
-
+
{children}
);
diff --git a/frontend/src/components/common/InputField/InputField.styles.ts b/frontend/src/components/common/InputField/InputField.styles.ts
index 8cebe69e1..42dc0655c 100644
--- a/frontend/src/components/common/InputField/InputField.styles.ts
+++ b/frontend/src/components/common/InputField/InputField.styles.ts
@@ -19,20 +19,22 @@ export const InputWrapper = styled.div`
align-items: center;
`;
-export const Input = styled.input`
+export const Input = styled.input<{ hasError?: boolean }>`
flex: 1;
height: 45px;
padding: 12px 80px 12px 18px;
- border: 1px solid #c5c5c5;
+ border: 1px solid ${({ hasError }) => (hasError ? 'red' : '#c5c5c5')};
border-radius: 6px;
outline: none;
font-size: 1.125rem;
letter-spacing: 0;
- color: rgba(0, 0, 0, 0.5);
+ color: rgba(0, 0, 0, 0.8);
&:focus {
- border-color: #007bff;
- box-shadow: 0 0 3px rgba(0, 123, 255, 0.5);
+ border-color: ${({ hasError }) => (hasError ? 'red' : '#007bff')};
+ box-shadow: 0 0 3px
+ ${({ hasError }) =>
+ hasError ? 'rgba(255, 0, 0, 0.5)' : 'rgba(0, 123, 255, 0.5)'};
}
${({ disabled }) =>
@@ -40,6 +42,9 @@ export const Input = styled.input`
`
background-color: rgba(0, 0, 0, 0.05);
`}
+ &::placeholder {
+ color: rgba(0, 0, 0, 0.3);
+ }
`;
export const ClearButton = styled.button`
@@ -85,3 +90,15 @@ export const CharCount = styled.span`
font-size: 12px;
letter-spacing: -0.96px;
`;
+
+export const HelperText = styled.div`
+ position: absolute;
+ left: 0;
+ top: 100%;
+ font-size: 0.75rem;
+ color: red;
+ margin-top: 4px;
+ pointer-events: none;
+ white-space: nowrap;
+ z-index: 1;
+`;
diff --git a/frontend/src/components/common/InputField/InputField.tsx b/frontend/src/components/common/InputField/InputField.tsx
index b2a843744..16e507cde 100644
--- a/frontend/src/components/common/InputField/InputField.tsx
+++ b/frontend/src/components/common/InputField/InputField.tsx
@@ -14,6 +14,8 @@ interface CustomInputProps {
value?: string;
onChange?: (e: React.ChangeEvent) => void;
onClear?: () => void;
+ isError?: boolean;
+ helperText?: string;
}
const InputField = ({
@@ -28,6 +30,8 @@ const InputField = ({
value = '',
onChange,
onClear,
+ isError,
+ helperText,
}: CustomInputProps) => {
const [isPasswordVisible, setIsPasswordVisible] = useState(false);
@@ -64,6 +68,7 @@ const InputField = ({
placeholder={placeholder}
maxLength={maxLength}
disabled={disabled}
+ hasError={isError}
/>
{showClearButton && !disabled && (
@@ -81,6 +86,9 @@ const InputField = ({
)}
+ {isError && helperText && (
+ {helperText}
+ )}
);
};
diff --git a/frontend/src/constants/snsConfig.ts b/frontend/src/constants/snsConfig.ts
new file mode 100644
index 000000000..2d85fcd17
--- /dev/null
+++ b/frontend/src/constants/snsConfig.ts
@@ -0,0 +1,24 @@
+import youtube_icon from '@/assets/images/icons/sns/youtube_icon.svg';
+import instagram_icon from '@/assets/images/icons/sns/instagram_icon.png';
+import x_icon from '@/assets/images/icons/sns/x_icon.svg';
+
+export const SNS_CONFIG = {
+ instagram: {
+ label: '인스타그램',
+ placeholder: 'https://www.instagram.com/id',
+ regex: /^https:\/\/(www\.)?instagram\.com\/[A-Za-z0-9._%-]+\/?$/,
+ icon: instagram_icon,
+ },
+ youtube: {
+ label: '유튜브',
+ placeholder: 'https://www.youtube.com/@id',
+ regex: /^https:\/\/(www\.)?youtube\.com\/(channel\/|@)[A-Za-z0-9._%-]+\/?$/,
+ icon: youtube_icon,
+ },
+ x: {
+ label: 'X',
+ placeholder: 'https://x.com/id',
+ regex: /^https:\/\/(www\.)?x\.com\/[A-Za-z0-9._%-]+\/?$/,
+ icon: x_icon,
+ },
+} as const;
diff --git a/frontend/src/hooks/useTrackPageView.ts b/frontend/src/hooks/useTrackPageView.ts
index 42ac533b7..b64a7ddbb 100644
--- a/frontend/src/hooks/useTrackPageView.ts
+++ b/frontend/src/hooks/useTrackPageView.ts
@@ -1,33 +1,33 @@
-import { useEffect } from 'react';
+import { useEffect, useRef } from 'react';
import { useLocation } from 'react-router-dom';
import mixpanel from 'mixpanel-browser';
-const useTrackPageView = (pageName: string) => {
+const useTrackPageView = (pageName: string, clubName?: string) => {
const location = useLocation();
+ const isTracked = useRef(false);
+ const startTime = useRef(Date.now());
useEffect(() => {
- const startTime = Date.now();
-
- // 페이지 방문 이벤트
mixpanel.track(`${pageName} Visited`, {
url: window.location.href,
- timestamp: startTime,
+ timestamp: startTime.current,
referrer: document.referrer || 'direct',
+ clubName,
});
const trackPageDuration = () => {
- const duration = Date.now() - startTime;
+ if (isTracked.current) return;
+ const duration = Date.now() - startTime.current;
mixpanel.track(`${pageName} Duration`, {
url: window.location.href,
- duration: duration, // milliseconds
- duration_seconds: Math.round(duration / 1000), // Convert to seconds
+ duration: duration,
+ duration_seconds: Math.round(duration / 1000),
+ clubName,
});
+ isTracked.current = true;
};
- // 사용자가 페이지를 떠날 때 (페이지 종료 또는 새 페이지 이동)
window.addEventListener('beforeunload', trackPageDuration);
-
- // 사용자가 탭을 변경하거나 백그라운드로 이동할 때
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
trackPageDuration();
@@ -35,10 +35,11 @@ const useTrackPageView = (pageName: string) => {
});
return () => {
+ trackPageDuration();
window.removeEventListener('beforeunload', trackPageDuration);
document.removeEventListener('visibilitychange', trackPageDuration);
};
- }, [location.pathname]);
+ }, [location.pathname, clubName]);
};
export default useTrackPageView;
diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx
index 6e2cf0baf..20342c892 100644
--- a/frontend/src/index.tsx
+++ b/frontend/src/index.tsx
@@ -3,6 +3,7 @@ import ReactDOM from 'react-dom/client';
import App from './App';
import mixpanel from 'mixpanel-browser';
import * as ChannelService from '@channel.io/channel-web-sdk-loader';
+import * as Sentry from '@sentry/react';
if (process.env.REACT_APP_MIXPANEL_TOKEN) {
mixpanel.init(process.env.REACT_APP_MIXPANEL_TOKEN, {
@@ -22,6 +23,13 @@ if (process.env.CHANNEL_PLUGIN_KEY) {
});
}
+Sentry.init({
+ dsn: process.env.SENTRY_DSN,
+ sendDefaultPii: false,
+ release: process.env.SENTRY_RELEASE,
+ tracesSampleRate: 0.1,
+});
+
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement,
);
diff --git a/frontend/src/pages/AdminPage/auth/LoginTab/LoginTab.styles.ts b/frontend/src/pages/AdminPage/auth/LoginTab/LoginTab.styles.ts
index f142a2a4b..564715a50 100644
--- a/frontend/src/pages/AdminPage/auth/LoginTab/LoginTab.styles.ts
+++ b/frontend/src/pages/AdminPage/auth/LoginTab/LoginTab.styles.ts
@@ -36,6 +36,12 @@ export const Title = styled.h2`
align-self: flex-start;
`;
+export const LoginForm = styled.form`
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+`;
+
export const InputFieldsContainer = styled.div`
width: 100%;
display: flex;
diff --git a/frontend/src/pages/AdminPage/auth/LoginTab/LoginTab.tsx b/frontend/src/pages/AdminPage/auth/LoginTab/LoginTab.tsx
index 4b5541b89..bfb31901a 100644
--- a/frontend/src/pages/AdminPage/auth/LoginTab/LoginTab.tsx
+++ b/frontend/src/pages/AdminPage/auth/LoginTab/LoginTab.tsx
@@ -49,28 +49,35 @@ const LoginTab = () => {
Log in
-
- setUserId(e.target.value)}
- />
- setPassword(e.target.value)}
- />
-
+ {
+ e.preventDefault();
+ handleLogin();
+ }}
+ >
+
+ setUserId(e.target.value)}
+ />
+ setPassword(e.target.value)}
+ />
+
-
-
-
+
+
+
+
{
alert(
'해당 기능은 아직 준비 중이에요.\n필요하신 경우 관리자에게 문의해주세요☺',
)
- }>
+ }
+ >
회원가입
|
@@ -89,7 +97,8 @@ const LoginTab = () => {
alert(
'해당 기능은 아직 준비 중이에요.\n필요하신 경우 관리자에게 문의해주세요☺',
)
- }>
+ }
+ >
아이디 찾기
|
@@ -99,7 +108,8 @@ const LoginTab = () => {
alert(
'해당 기능은 아직 준비 중이에요.\n필요하신 경우 관리자에게 문의해주세요☺',
)
- }>
+ }
+ >
비밀번호 찾기
diff --git a/frontend/src/pages/AdminPage/tabs/ClubInfoEditTab/ClubInfoEditTab.styles.ts b/frontend/src/pages/AdminPage/tabs/ClubInfoEditTab/ClubInfoEditTab.styles.ts
index 7177019ad..645d6a935 100644
--- a/frontend/src/pages/AdminPage/tabs/ClubInfoEditTab/ClubInfoEditTab.styles.ts
+++ b/frontend/src/pages/AdminPage/tabs/ClubInfoEditTab/ClubInfoEditTab.styles.ts
@@ -31,4 +31,27 @@ export const TagEditGroup = styled.div`
display: flex;
flex-direction: column;
gap: 30px;
+ margin-bottom: 120px;
+`;
+
+export const SNSInputGroup = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: 30px;
+ margin-top: 30px;
+`;
+
+export const SNSRow = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ position: relative;
+ align-items: flex-start;
+`;
+
+export const SNSCheckboxLabel = styled.label`
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ font-weight: 600;
`;
diff --git a/frontend/src/pages/AdminPage/tabs/ClubInfoEditTab/ClubInfoEditTab.tsx b/frontend/src/pages/AdminPage/tabs/ClubInfoEditTab/ClubInfoEditTab.tsx
index c3557f364..175970aac 100644
--- a/frontend/src/pages/AdminPage/tabs/ClubInfoEditTab/ClubInfoEditTab.tsx
+++ b/frontend/src/pages/AdminPage/tabs/ClubInfoEditTab/ClubInfoEditTab.tsx
@@ -1,13 +1,16 @@
import React, { useState, useEffect } from 'react';
-import * as Styled from './ClubInfoEditTab.styles';
-import InputField from '@/components/common/InputField/InputField';
-import SelectTags from '@/pages/AdminPage/components/SelectTags/SelectTags';
-import MakeTags from '@/pages/AdminPage/components/MakeTags/MakeTags';
-import Button from '@/components/common/Button/Button';
import { useOutletContext } from 'react-router-dom';
+import { useQueryClient } from '@tanstack/react-query';
import { ClubDetail } from '@/types/club';
+import { SNSPlatform } from '@/types/club';
import { useUpdateClubDetail } from '@/hooks/queries/club/useUpdateClubDetail';
-import { useQueryClient } from '@tanstack/react-query';
+import { validateSocialLink } from '@/utils/validateSocialLink';
+import { SNS_CONFIG } from '@/constants/snsConfig';
+import InputField from '@/components/common/InputField/InputField';
+import Button from '@/components/common/Button/Button';
+import SelectTags from '@/pages/AdminPage/tabs/ClubInfoEditTab/components/SelectTags/SelectTags';
+import MakeTags from '@/pages/AdminPage/tabs/ClubInfoEditTab/components/MakeTags/MakeTags';
+import * as Styled from './ClubInfoEditTab.styles';
const ClubInfoEditTab = () => {
const clubDetail = useOutletContext();
@@ -20,7 +23,18 @@ const ClubInfoEditTab = () => {
const [selectedDivision, setSelectedDivision] = useState('');
const [selectedCategory, setSelectedCategory] = useState('');
const [clubTags, setClubTags] = useState(() => ['', '']);
- const [recruitmentForm] = useState('');
+ const [socialLinks, setSocialLinks] = useState>({
+ instagram: '',
+ youtube: '',
+ x: '',
+ });
+
+ const [snsErrors, setSnsErrors] = useState>({
+ instagram: '',
+ youtube: '',
+ x: '',
+ });
+
const queryClient = useQueryClient();
const divisions = ['중동', '과동'];
const categories = ['봉사', '종교', '취미교양', '학술', '운동', '공연'];
@@ -39,8 +53,21 @@ const ClubInfoEditTab = () => {
: [...clubDetail.tags, ''],
);
}
+ if (clubDetail?.socialLinks) {
+ setSocialLinks({
+ instagram: clubDetail.socialLinks.instagram || '',
+ youtube: clubDetail.socialLinks.youtube || '',
+ x: clubDetail.socialLinks.x || '',
+ });
+ }
}, [clubDetail]);
+ const handleSocialLinkChange = (key: SNSPlatform, value: string) => {
+ const errorMsg = validateSocialLink(key, value);
+ setSnsErrors((prev) => ({ ...prev, [key]: errorMsg }));
+ setSocialLinks((prev) => ({ ...prev, [key]: value }));
+ };
+
const handleUpdateClub = () => {
if (!clubDetail || !clubDetail.id) {
alert('클럽 정보가 로드되지 않았습니다.');
@@ -51,6 +78,12 @@ const ClubInfoEditTab = () => {
return;
}
+ const hasSnsErrors = Object.values(snsErrors).some((error) => error !== '');
+ if (hasSnsErrors) {
+ alert('SNS 링크에 오류가 있어요. 수정 후 다시 시도해주세요!');
+ return;
+ }
+
const updatedData = {
id: clubDetail.id,
name: clubName,
@@ -60,7 +93,7 @@ const ClubInfoEditTab = () => {
introduction: introduction,
presidentName: clubPresidentName,
presidentPhoneNumber: telephoneNumber,
- recruitmentForm: recruitmentForm,
+ socialLinks: socialLinks,
};
updateClub(updatedData, {
@@ -149,6 +182,30 @@ const ClubInfoEditTab = () => {
+
+ 동아리 SNS 링크
+
+ {Object.entries(SNS_CONFIG).map(([rawKey, { label, placeholder }]) => {
+ const key = rawKey as SNSPlatform;
+
+ return (
+
+ {label}
+ handleSocialLinkChange(key, e.target.value)}
+ onClear={() => {
+ setSocialLinks((prev) => ({ ...prev, [key]: '' }));
+ setSnsErrors((prev) => ({ ...prev, [key]: '' }));
+ }}
+ isError={snsErrors[key] !== ''}
+ helperText={snsErrors[key]}
+ />
+
+ );
+ })}
+
>
);
};
diff --git a/frontend/src/pages/AdminPage/components/MakeTags/MakeTags.styles.ts b/frontend/src/pages/AdminPage/tabs/ClubInfoEditTab/components/MakeTags/MakeTags.styles.ts
similarity index 100%
rename from frontend/src/pages/AdminPage/components/MakeTags/MakeTags.styles.ts
rename to frontend/src/pages/AdminPage/tabs/ClubInfoEditTab/components/MakeTags/MakeTags.styles.ts
diff --git a/frontend/src/pages/AdminPage/components/MakeTags/MakeTags.tsx b/frontend/src/pages/AdminPage/tabs/ClubInfoEditTab/components/MakeTags/MakeTags.tsx
similarity index 100%
rename from frontend/src/pages/AdminPage/components/MakeTags/MakeTags.tsx
rename to frontend/src/pages/AdminPage/tabs/ClubInfoEditTab/components/MakeTags/MakeTags.tsx
diff --git a/frontend/src/pages/AdminPage/components/SelectTags/SelectTags.styles.ts b/frontend/src/pages/AdminPage/tabs/ClubInfoEditTab/components/SelectTags/SelectTags.styles.ts
similarity index 100%
rename from frontend/src/pages/AdminPage/components/SelectTags/SelectTags.styles.ts
rename to frontend/src/pages/AdminPage/tabs/ClubInfoEditTab/components/SelectTags/SelectTags.styles.ts
diff --git a/frontend/src/pages/AdminPage/components/SelectTags/SelectTags.tsx b/frontend/src/pages/AdminPage/tabs/ClubInfoEditTab/components/SelectTags/SelectTags.tsx
similarity index 92%
rename from frontend/src/pages/AdminPage/components/SelectTags/SelectTags.tsx
rename to frontend/src/pages/AdminPage/tabs/ClubInfoEditTab/components/SelectTags/SelectTags.tsx
index 435bfb865..57b98c94a 100644
--- a/frontend/src/pages/AdminPage/components/SelectTags/SelectTags.tsx
+++ b/frontend/src/pages/AdminPage/tabs/ClubInfoEditTab/components/SelectTags/SelectTags.tsx
@@ -17,7 +17,8 @@ const SelectTags = ({ label, tags, selected, onChange }: SelectTagsProps) => {
onChange(tag)}
- selected={selected === tag}>
+ selected={selected === tag}
+ >
#{tag}
))}
diff --git a/frontend/src/pages/AdminPage/tabs/PhotoEditTab/PhotoEditTab.tsx b/frontend/src/pages/AdminPage/tabs/PhotoEditTab/PhotoEditTab.tsx
index 4b1663eed..883b89ac2 100644
--- a/frontend/src/pages/AdminPage/tabs/PhotoEditTab/PhotoEditTab.tsx
+++ b/frontend/src/pages/AdminPage/tabs/PhotoEditTab/PhotoEditTab.tsx
@@ -1,8 +1,8 @@
import React, { useEffect, useState } from 'react';
import { useOutletContext } from 'react-router-dom';
import * as Styled from './PhotoEditTab.styles';
-import ImageUpload from '@/pages/AdminPage/components/ImageUpload/ImageUpload';
-import { ImagePreview } from '@/pages/AdminPage/components/ImagePreview/ImagePreview';
+import ImageUpload from '@/pages/AdminPage/tabs/PhotoEditTab/components/ImageUpload/ImageUpload';
+import { ImagePreview } from '@/pages/AdminPage/tabs/PhotoEditTab/components/ImagePreview/ImagePreview';
import useUpdateFeedImages from '@/hooks/queries/club/useUpdateFeedImages';
import { ClubDetail } from '@/types/club';
diff --git a/frontend/src/pages/AdminPage/components/ImagePreview/ImagePreview.styles.ts b/frontend/src/pages/AdminPage/tabs/PhotoEditTab/components/ImagePreview/ImagePreview.styles.ts
similarity index 100%
rename from frontend/src/pages/AdminPage/components/ImagePreview/ImagePreview.styles.ts
rename to frontend/src/pages/AdminPage/tabs/PhotoEditTab/components/ImagePreview/ImagePreview.styles.ts
diff --git a/frontend/src/pages/AdminPage/components/ImagePreview/ImagePreview.tsx b/frontend/src/pages/AdminPage/tabs/PhotoEditTab/components/ImagePreview/ImagePreview.tsx
similarity index 100%
rename from frontend/src/pages/AdminPage/components/ImagePreview/ImagePreview.tsx
rename to frontend/src/pages/AdminPage/tabs/PhotoEditTab/components/ImagePreview/ImagePreview.tsx
diff --git a/frontend/src/pages/AdminPage/components/ImageUpload/ImageUpload.styles.ts b/frontend/src/pages/AdminPage/tabs/PhotoEditTab/components/ImageUpload/ImageUpload.styles.ts
similarity index 100%
rename from frontend/src/pages/AdminPage/components/ImageUpload/ImageUpload.styles.ts
rename to frontend/src/pages/AdminPage/tabs/PhotoEditTab/components/ImageUpload/ImageUpload.styles.ts
diff --git a/frontend/src/pages/AdminPage/components/ImageUpload/ImageUpload.tsx b/frontend/src/pages/AdminPage/tabs/PhotoEditTab/components/ImageUpload/ImageUpload.tsx
similarity index 95%
rename from frontend/src/pages/AdminPage/components/ImageUpload/ImageUpload.tsx
rename to frontend/src/pages/AdminPage/tabs/PhotoEditTab/components/ImageUpload/ImageUpload.tsx
index 3fc1a8aff..57379f4c9 100644
--- a/frontend/src/pages/AdminPage/components/ImageUpload/ImageUpload.tsx
+++ b/frontend/src/pages/AdminPage/tabs/PhotoEditTab/components/ImageUpload/ImageUpload.tsx
@@ -1,6 +1,5 @@
import React, { useRef } from 'react';
import * as Styled from './ImageUpload.styles';
-// import UploadAddIcon from '@/assets/images/upload-add.png';
import useCreateFeedImage from '@/hooks/queries/club/useCreateFeedImage';
import Button from '@/components/common/Button/Button';
import { MAX_FILE_SIZE } from '@/constants/uploadLimit';
@@ -52,7 +51,8 @@ export const ImageUpload = ({
return;
}
inputRef.current?.click();
- }}>
+ }}
+ >
이미지 업로드
{/*
*/}
diff --git a/frontend/src/pages/AdminPage/tabs/RecruitEditTab/RecruitEditTab.tsx b/frontend/src/pages/AdminPage/tabs/RecruitEditTab/RecruitEditTab.tsx
index 4cc3792fd..d3d1fa353 100644
--- a/frontend/src/pages/AdminPage/tabs/RecruitEditTab/RecruitEditTab.tsx
+++ b/frontend/src/pages/AdminPage/tabs/RecruitEditTab/RecruitEditTab.tsx
@@ -1,14 +1,14 @@
import React, { useState, useEffect } from 'react';
import { useOutletContext } from 'react-router-dom';
import * as Styled from './RecruitEditTab.styles';
-import Calendar from '@/pages/AdminPage/components/Calendar/Calendar';
+import Calendar from '@/pages/AdminPage/tabs/RecruitEditTab/components/Calendar/Calendar';
import Button from '@/components/common/Button/Button';
import InputField from '@/components/common/InputField/InputField';
import { useUpdateClubDescription } from '@/hooks/queries/club/useUpdateClubDescription';
import { parseRecruitmentPeriod } from '@/utils/stringToDate';
import { ClubDetail } from '@/types/club';
import { useQueryClient } from '@tanstack/react-query';
-import MarkdownEditor from '@/pages/AdminPage/components/MarkdownEditor/MarkdownEditor';
+import MarkdownEditor from '@/pages/AdminPage/tabs/RecruitEditTab/components/MarkdownEditor/MarkdownEditor';
const RecruitEditTab = () => {
const clubDetail = useOutletContext();
diff --git a/frontend/src/pages/AdminPage/components/Calendar/Calendar.styles.ts b/frontend/src/pages/AdminPage/tabs/RecruitEditTab/components/Calendar/Calendar.styles.ts
similarity index 100%
rename from frontend/src/pages/AdminPage/components/Calendar/Calendar.styles.ts
rename to frontend/src/pages/AdminPage/tabs/RecruitEditTab/components/Calendar/Calendar.styles.ts
diff --git a/frontend/src/pages/AdminPage/components/Calendar/Calendar.tsx b/frontend/src/pages/AdminPage/tabs/RecruitEditTab/components/Calendar/Calendar.tsx
similarity index 100%
rename from frontend/src/pages/AdminPage/components/Calendar/Calendar.tsx
rename to frontend/src/pages/AdminPage/tabs/RecruitEditTab/components/Calendar/Calendar.tsx
diff --git a/frontend/src/pages/AdminPage/components/MarkdownEditor/MarkdownEditor.styles.ts b/frontend/src/pages/AdminPage/tabs/RecruitEditTab/components/MarkdownEditor/MarkdownEditor.styles.ts
similarity index 100%
rename from frontend/src/pages/AdminPage/components/MarkdownEditor/MarkdownEditor.styles.ts
rename to frontend/src/pages/AdminPage/tabs/RecruitEditTab/components/MarkdownEditor/MarkdownEditor.styles.ts
diff --git a/frontend/src/pages/AdminPage/components/MarkdownEditor/MarkdownEditor.tsx b/frontend/src/pages/AdminPage/tabs/RecruitEditTab/components/MarkdownEditor/MarkdownEditor.tsx
similarity index 100%
rename from frontend/src/pages/AdminPage/components/MarkdownEditor/MarkdownEditor.tsx
rename to frontend/src/pages/AdminPage/tabs/RecruitEditTab/components/MarkdownEditor/MarkdownEditor.tsx
diff --git a/frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx b/frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx
index 97be1498a..84938ef74 100644
--- a/frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx
+++ b/frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx
@@ -30,7 +30,7 @@ const ClubDetailPage = () => {
return () => window.removeEventListener('resize', handleResize);
}, []);
- useTrackPageView(`ClubDetailPage ${clubDetail?.name || ''}`);
+ useTrackPageView(`ClubDetailPage`, clubDetail?.name);
if (!clubDetail) {
return null;
diff --git a/frontend/src/pages/ClubDetailPage/components/ClubProfile/ClubProfile.tsx b/frontend/src/pages/ClubDetailPage/components/ClubProfile/ClubProfile.tsx
index 143436e5d..1608a26bf 100644
--- a/frontend/src/pages/ClubDetailPage/components/ClubProfile/ClubProfile.tsx
+++ b/frontend/src/pages/ClubDetailPage/components/ClubProfile/ClubProfile.tsx
@@ -25,13 +25,19 @@ const ClubProfile = ({
{name}
- {division}
- {category}
- {tags.map((tag) => (
-
- {tag}
-
- ))}
+
+ {division}
+
+
+ {category}
+
+ {tags
+ .filter((tag) => tag.trim())
+ .map((tag) => (
+
+ {tag}
+
+ ))}
diff --git a/frontend/src/pages/ClubDetailPage/components/InfoBox/InfoBox.styles.ts b/frontend/src/pages/ClubDetailPage/components/InfoBox/InfoBox.styles.ts
index d4f9b1d99..d7f65efac 100644
--- a/frontend/src/pages/ClubDetailPage/components/InfoBox/InfoBox.styles.ts
+++ b/frontend/src/pages/ClubDetailPage/components/InfoBox/InfoBox.styles.ts
@@ -9,14 +9,14 @@ export const InfoBoxWrapper = styled.div`
@media (max-width: 500px) {
flex-direction: column;
- gap: 0px;
+ gap: 0;
margin-top: 40px;
}
`;
export const InfoBox = styled.div`
width: 573px;
- height: 164px;
+ height: 197px;
border-radius: 18px;
border: 1px solid #dcdcdc;
padding: 30px;
@@ -52,11 +52,15 @@ export const DescriptionWrapper = styled.div`
align-items: center;
gap: 50px;
font-size: 14px;
+ @media (max-width: 500px) {
+ gap: 40px;
+ }
`;
export const LeftText = styled.p`
color: #9d9d9d;
white-space: nowrap;
+ width: 30px;
`;
export const RightText = styled.p`
diff --git a/frontend/src/pages/ClubDetailPage/components/InfoBox/InfoBox.tsx b/frontend/src/pages/ClubDetailPage/components/InfoBox/InfoBox.tsx
index 5ffdc0a2c..0b0f7cf12 100644
--- a/frontend/src/pages/ClubDetailPage/components/InfoBox/InfoBox.tsx
+++ b/frontend/src/pages/ClubDetailPage/components/InfoBox/InfoBox.tsx
@@ -2,10 +2,12 @@ import React from 'react';
import * as Styled from './InfoBox.styles';
import { ClubDetail } from '@/types/club';
import { INFOTABS_SCROLL_INDEX } from '@/constants/scrollSections';
+import SnsLinkIcons from '@/pages/ClubDetailPage/components/SnsLinkIcons/SnsLinkIcons';
interface ClubInfoItem {
label: string;
- value: string;
+ value?: string;
+ render?: React.ReactNode;
}
interface ClubInfoSection {
@@ -37,6 +39,10 @@ const InfoBox = ({ sectionRefs, clubDetail }: InfoBoxProps) => {
descriptions: [
{ label: '회장이름', value: clubDetail.presidentName },
{ label: '전화번호', value: clubDetail.presidentPhoneNumber },
+ {
+ label: 'SNS',
+ render: ,
+ },
],
refIndex: INFOTABS_SCROLL_INDEX.CLUB_INFO_TAB,
},
@@ -49,13 +55,14 @@ const InfoBox = ({ sectionRefs, clubDetail }: InfoBoxProps) => {
key={info.refIndex}
ref={(el) => {
sectionRefs.current[info.refIndex] = el;
- }}>
+ }}
+ >
{info.title}
{info.descriptions.map((desc, idx) => (
{desc.label}
- {desc.value}
+ {desc.render ?? desc.value}
))}
diff --git a/frontend/src/pages/ClubDetailPage/components/SnsLinkIcons/SnsLinkIcons.styles.ts b/frontend/src/pages/ClubDetailPage/components/SnsLinkIcons/SnsLinkIcons.styles.ts
new file mode 100644
index 000000000..f2b70da71
--- /dev/null
+++ b/frontend/src/pages/ClubDetailPage/components/SnsLinkIcons/SnsLinkIcons.styles.ts
@@ -0,0 +1,25 @@
+import styled from 'styled-components';
+
+export const SnsIconGroup = styled.div`
+ display: flex;
+ gap: 8px;
+ align-items: center;
+`;
+
+export const SnsIcon = styled.img`
+ width: 24px;
+ height: 24px;
+ cursor: pointer;
+ opacity: 0.9;
+ &:hover {
+ opacity: 1;
+ }
+ &:active {
+ transform: scale(0.98);
+ }
+`;
+
+export const SnsLink = styled.a`
+ display: inline-block;
+ line-height: 0;
+`;
diff --git a/frontend/src/pages/ClubDetailPage/components/SnsLinkIcons/SnsLinkIcons.tsx b/frontend/src/pages/ClubDetailPage/components/SnsLinkIcons/SnsLinkIcons.tsx
new file mode 100644
index 000000000..3b5716d47
--- /dev/null
+++ b/frontend/src/pages/ClubDetailPage/components/SnsLinkIcons/SnsLinkIcons.tsx
@@ -0,0 +1,32 @@
+import React from 'react';
+import * as Styled from './SnsLinkIcons.styles';
+import { SNS_CONFIG } from '@/constants/snsConfig';
+import { SNSPlatform } from '@/types/club';
+
+interface SnsLinkIconsProps {
+ apiSocialLinks: Partial>;
+}
+
+const SnsLinkIcons = ({ apiSocialLinks }: SnsLinkIconsProps) => {
+ return (
+
+ {Object.entries(apiSocialLinks).map(([platform, url]) => {
+ if (!url) return null;
+
+ const config = SNS_CONFIG[platform as SNSPlatform];
+ return (
+
+
+
+ );
+ })}
+
+ );
+};
+
+export default SnsLinkIcons;
diff --git a/frontend/src/pages/MainPage/components/Banner/Banner.tsx b/frontend/src/pages/MainPage/components/Banner/Banner.tsx
index 39668bba3..1bd1ed406 100644
--- a/frontend/src/pages/MainPage/components/Banner/Banner.tsx
+++ b/frontend/src/pages/MainPage/components/Banner/Banner.tsx
@@ -17,23 +17,33 @@ const Banner = ({ desktopBanners, mobileBanners }: BannerComponentProps) => {
const [isMobile, setIsMobile] = useState(window.innerWidth <= 500);
const [currentSlideIndex, setCurrentSlideIndex] = useState(1);
const [slideWidth, setSlideWidth] = useState(0);
- const [isAnimating, setIsAnimating] = useState(true);
+ const [isAnimating, setIsAnimating] = useState(false);
const [isTransitioning, setIsTransitioning] = useState(false);
+ const [isReady, setIsReady] = useState(false);
const banners = isMobile ? mobileBanners : desktopBanners;
const extendedBanners = [banners[banners.length - 1], ...banners, banners[0]];
const updateSlideWidth = useCallback(() => {
if (slideRef.current) {
- setSlideWidth(slideRef.current.offsetWidth);
- slideRef.current.style.transform = `translateX(-${currentSlideIndex * slideRef.current.offsetWidth}px)`;
+ const width = slideRef.current.offsetWidth;
+ setSlideWidth(width);
+ if (width > 0) {
+ slideRef.current.style.transform = `translateX(-${currentSlideIndex * width}px)`;
+ if (!isReady) {
+ setIsReady(true);
+ setIsAnimating(true);
+ }
+ }
}
- }, [currentSlideIndex]);
+ }, [currentSlideIndex, isReady]);
useEffect(() => {
updateSlideWidth();
const handleResize = debounce(() => {
setIsMobile(window.innerWidth <= 500);
+ setIsReady(false);
+ setIsAnimating(false);
updateSlideWidth();
}, 200);
@@ -44,7 +54,7 @@ const Banner = ({ desktopBanners, mobileBanners }: BannerComponentProps) => {
}, [updateSlideWidth]);
useEffect(() => {
- if (!slideRef.current) return;
+ if (!slideRef.current || slideWidth === 0) return;
if (isAnimating) {
slideRef.current.style.transform = `translateX(-${currentSlideIndex * slideWidth}px)`;
@@ -77,27 +87,28 @@ const Banner = ({ desktopBanners, mobileBanners }: BannerComponentProps) => {
}, [currentSlideIndex, slideWidth, banners.length, isAnimating]);
const moveToNextSlide = useCallback(() => {
- if (isTransitioning) return;
+ if (isTransitioning || !isReady) return;
setIsTransitioning(true);
setIsAnimating(true);
setCurrentSlideIndex((prev) => prev + 1);
- }, [isTransitioning]);
+ }, [isTransitioning, isReady]);
const moveToPrevSlide = useCallback(() => {
- if (isTransitioning) return;
+ if (isTransitioning || !isReady) return;
setIsTransitioning(true);
setIsAnimating(true);
setCurrentSlideIndex((prev) => prev - 1);
- }, [isTransitioning]);
+ }, [isTransitioning, isReady]);
useEffect(() => {
- if (slideWidth === 0) return;
+ if (slideWidth === 0 || !isReady) return;
+
const interval = setInterval(() => {
moveToNextSlide();
}, 3000);
return () => clearInterval(interval);
- }, [moveToNextSlide, slideWidth]);
+ }, [moveToNextSlide, slideWidth, isReady]);
return (
diff --git a/frontend/src/pages/MainPage/components/ClubCard/ClubCard.tsx b/frontend/src/pages/MainPage/components/ClubCard/ClubCard.tsx
index 5443e709d..78c400bc0 100644
--- a/frontend/src/pages/MainPage/components/ClubCard/ClubCard.tsx
+++ b/frontend/src/pages/MainPage/components/ClubCard/ClubCard.tsx
@@ -40,13 +40,15 @@ const ClubCard = ({ club }: { club: Club }) => {
{club.introduction}
-
-
- {club.tags.map((tag) => (
-
- {tag}
-
- ))}
+
+
+ {club.tags
+ .filter((tag) => tag.trim())
+ .map((tag) => (
+
+ {tag}
+
+ ))}
);
diff --git a/frontend/src/types/club.ts b/frontend/src/types/club.ts
index b377843ea..8d17560a7 100644
--- a/frontend/src/types/club.ts
+++ b/frontend/src/types/club.ts
@@ -1,3 +1,5 @@
+import { SNS_CONFIG } from '@/constants/snsConfig';
+
export interface Club {
id: string;
name: string;
@@ -9,6 +11,8 @@ export interface Club {
introduction: string;
}
+export type SNSPlatform = keyof typeof SNS_CONFIG;
+
export interface ClubDetail extends Club {
description: string;
state: string;
@@ -18,6 +22,7 @@ export interface ClubDetail extends Club {
recruitmentForm: string;
recruitmentPeriod: string;
recruitmentTarget: string;
+ socialLinks: Record;
}
export interface ClubDescription {
diff --git a/frontend/src/utils/validateSocialLink.test.ts b/frontend/src/utils/validateSocialLink.test.ts
new file mode 100644
index 000000000..6a79aa8de
--- /dev/null
+++ b/frontend/src/utils/validateSocialLink.test.ts
@@ -0,0 +1,75 @@
+import { validateSocialLink } from '@/utils/validateSocialLink';
+import { SNS_CONFIG } from '@/constants/snsConfig';
+import { SNSPlatform } from '@/types/club';
+
+type LinkSet = Record;
+
+const validLinks: LinkSet = {
+ instagram: 'https://www.instagram.com/valid_id',
+ youtube: 'https://www.youtube.com/@validchannel',
+ x: 'https://x.com/valid_user',
+};
+
+const invalidLinks: LinkSet = {
+ instagram: 'https://instagram.com',
+ youtube: 'youtube.com/123',
+ x: 'https://x.com/',
+};
+
+const phishingLinks: LinkSet = {
+ instagram: 'https://www.instagram.evil.com/id',
+ youtube: 'https://www.y0utube.com/@channel',
+ x: 'https://x.co/valid_user',
+};
+
+const platforms = Object.keys(SNS_CONFIG) as SNSPlatform[];
+
+describe('validateSocialLink - SNS 링크 유효성 검사', () => {
+ const expectNoError = (platform: SNSPlatform, link: string) => {
+ expect(validateSocialLink(platform, link)).toBe('');
+ };
+
+ const expectError = (platform: SNSPlatform, link: string) => {
+ expect(validateSocialLink(platform, link)).toBe(
+ `${SNS_CONFIG[platform].label} 링크 형식이 조금 이상해요. 다시 한 번 확인해주세요!`,
+ );
+ };
+
+ it('유효한 링크는 에러 메시지를 반환하지 않는다', () => {
+ platforms.forEach((platform) =>
+ expectNoError(platform, validLinks[platform]),
+ );
+ });
+
+ it('잘못된 형식의 링크는 에러 메시지를 반환한다', () => {
+ platforms.forEach((platform) =>
+ expectError(platform, invalidLinks[platform]),
+ );
+ });
+
+ it('입력값이 비어있으면 에러 메시지를 반환하지 않는다', () => {
+ platforms.forEach((platform) => expectNoError(platform, ''));
+ });
+
+ it('피싱 가능성이 있는 링크는 에러 메시지를 반환한다', () => {
+ platforms.forEach((platform) =>
+ expectError(platform, phishingLinks[platform]),
+ );
+ });
+
+ it('http 링크는 허용되지 않아야 한다', () => {
+ expectError('x', 'http://x.com/valid_id');
+ });
+
+ it('유니코드 기반 도메인 위장은 허용되지 않아야 한다', () => {
+ // 러시아어 소문자 о (U+043E)
+ const spoofedUrl = 'https://www.yоutube.com/@fake';
+ expectError('youtube', spoofedUrl);
+ });
+
+ it('redirect 파라미터로 외부 사이트 유도 시 에러 메시지를 반환해야 한다', () => {
+ const redirectUrl =
+ 'https://www.instagram.com/redirect?to=https://attacker.com/steal?cookie=document.cookie';
+ expectError('instagram', redirectUrl);
+ });
+});
diff --git a/frontend/src/utils/validateSocialLink.ts b/frontend/src/utils/validateSocialLink.ts
new file mode 100644
index 000000000..750fe2823
--- /dev/null
+++ b/frontend/src/utils/validateSocialLink.ts
@@ -0,0 +1,10 @@
+import { SNS_CONFIG } from '@/constants/snsConfig';
+import { SNSPlatform } from '@/types/club';
+
+export const validateSocialLink = (key: SNSPlatform, value: string): string => {
+ const { regex, label } = SNS_CONFIG[key];
+ if (value && !regex.test(value)) {
+ return `${label} 링크 형식이 조금 이상해요. 다시 한 번 확인해주세요!`;
+ }
+ return '';
+};