diff --git a/.github/workflows/ci-workflow.yml b/.github/workflows/ci-workflow.yml index 1c5ebd62..cd74749d 100644 --- a/.github/workflows/ci-workflow.yml +++ b/.github/workflows/ci-workflow.yml @@ -44,6 +44,17 @@ jobs: NEXT_PUBLIC_GOOGLE_VERIFICATION=${{ secrets.NEXT_PUBLIC_GOOGLE_VERIFICATION }} NEXT_PUBLIC_NAVER_VERIFICATION=${{ secrets.NEXT_PUBLIC_NAVER_VERIFICATION }} NEXT_PUBLIC_SSE=${{ secrets.NEXT_PUBLIC_SSE }} + NEXT_PUBLIC_FIREBASE_API_KEY=${{ secrets.NEXT_PUBLIC_FIREBASE_API_KEY }} + NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=${{ secrets.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN }} + NEXT_PUBLIC_FIREBASE_PROJECT_ID=${{ secrets.NEXT_PUBLIC_FIREBASE_PROJECT_ID }} + NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET=${{ secrets.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET }} + NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER=${{ secrets.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER }} + NEXT_PUBLIC_FIREBASE_APP_ID=${{ secrets.NEXT_PUBLIC_FIREBASE_APP_ID }} + NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID=${{ secrets.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID }} + NEXT_PUBLIC_FIREBASE_MESSAGING_VAPID_KEY=${{ secrets.NEXT_PUBLIC_FIREBASE_MESSAGING_VAPID_KEY }} + FIREBASE_PRIVATE_KEY=${{ secrets.FIREBASE_PRIVATE_KEY }} + FIREBASE_CLIENT_EMAIL=${{ secrets.FIREBASE_CLIENT_EMAIL }} + NEXT_PUBLIC_FIREBASE_ACCESS_KEY=${{ secrets.NEXT_PUBLIC_FIREBASE_ACCESS_KEY }} notify: needs: build if: github.repository == 'KernelSquare/Frontend' diff --git a/Dockerfile b/Dockerfile index 6e829224..d6bf7314 100644 --- a/Dockerfile +++ b/Dockerfile @@ -23,7 +23,18 @@ NEXT_PUBLIC_SENTRY_DSN \ NEXT_PUBLIC_SENTRY_ACTIVE \ NEXT_PUBLIC_GOOGLE_VERIFICATION \ NEXT_PUBLIC_NAVER_VERIFICATION \ -NEXT_PUBLIC_SSE +NEXT_PUBLIC_SSE \ +NEXT_PUBLIC_FIREBASE_API_KEY \ +NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN \ +NEXT_PUBLIC_FIREBASE_PROJECT_ID \ +NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET \ +NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER \ +NEXT_PUBLIC_FIREBASE_APP_ID \ +NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID \ +NEXT_PUBLIC_FIREBASE_MESSAGING_VAPID_KEY \ +FIREBASE_PRIVATE_KEY \ +FIREBASE_CLIENT_EMAIL \ +NEXT_PUBLIC_FIREBASE_ACCESS_KEY ENV NEXT_PUBLIC_API_MOCKING=$NEXT_PUBLIC_API_MOCKING \ NEXT_PUBLIC_SITE_URL=$NEXT_PUBLIC_SITE_URL \ @@ -39,7 +50,18 @@ NEXT_PUBLIC_SENTRY_DSN=$NEXT_PUBLIC_SENTRY_DSN \ NEXT_PUBLIC_SENTRY_ACTIVE=$NEXT_PUBLIC_SENTRY_ACTIVE \ NEXT_PUBLIC_GOOGLE_VERIFICATION=$NEXT_PUBLIC_GOOGLE_VERIFICATION \ NEXT_PUBLIC_NAVER_VERIFICATION=$NEXT_PUBLIC_NAVER_VERIFICATION \ -NEXT_PUBLIC_SSE=$NEXT_PUBLIC_SSE +NEXT_PUBLIC_SSE=$NEXT_PUBLIC_SSE \ +NEXT_PUBLIC_FIREBASE_API_KEY=$NEXT_PUBLIC_FIREBASE_API_KEY \ +NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=$NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN \ +NEXT_PUBLIC_FIREBASE_PROJECT_ID=$NEXT_PUBLIC_FIREBASE_PROJECT_ID \ +NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET=$NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET \ +NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER=$NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER \ +NEXT_PUBLIC_FIREBASE_APP_ID=$NEXT_PUBLIC_FIREBASE_APP_ID \ +NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID=$NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID \ +NEXT_PUBLIC_FIREBASE_MESSAGING_VAPID_KEY=$NEXT_PUBLIC_FIREBASE_MESSAGING_VAPID_KEY \ +FIREBASE_PRIVATE_KEY=$FIREBASE_PRIVATE_KEY \ +FIREBASE_CLIENT_EMAIL=$FIREBASE_CLIENT_EMAIL \ +NEXT_PUBLIC_FIREBASE_ACCESS_KEY=$NEXT_PUBLIC_FIREBASE_ACCESS_KEY RUN pnpm run build diff --git a/package.json b/package.json index c7f4a3c2..52a60d50 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,8 @@ "embla-carousel-react": "^8.0.0", "event-source-polyfill": "^1.0.31", "express": "^4.18.2", + "firebase": "^10.12.2", + "firebase-admin": "^12.1.1", "jsonwebtoken": "^9.0.2", "lodash-es": "^4.17.21", "lottie-react": "^2.4.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index df17cfa6..cf0a036d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -110,6 +110,12 @@ dependencies: express: specifier: ^4.18.2 version: 4.18.2 + firebase: + specifier: ^10.12.2 + version: 10.12.2 + firebase-admin: + specifier: ^12.1.1 + version: 12.1.1 jsonwebtoken: specifier: ^9.0.2 version: 9.0.2 @@ -529,6 +535,470 @@ packages: engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: true + /@fastify/busboy@2.1.1: + resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==} + engines: {node: '>=14'} + dev: false + + /@firebase/analytics-compat@0.2.10(@firebase/app-compat@0.2.35)(@firebase/app@0.10.5): + resolution: {integrity: sha512-ia68RcLQLLMFWrM10JfmFod7eJGwqr4/uyrtzHpTDnxGX/6gNCBTOuxdAbyWIqXI5XmcMQdz9hDijGKOHgDfPw==} + peerDependencies: + '@firebase/app-compat': 0.x + dependencies: + '@firebase/analytics': 0.10.4(@firebase/app@0.10.5) + '@firebase/analytics-types': 0.8.2 + '@firebase/app-compat': 0.2.35 + '@firebase/component': 0.6.7 + '@firebase/util': 1.9.6 + tslib: 2.6.2 + transitivePeerDependencies: + - '@firebase/app' + dev: false + + /@firebase/analytics-types@0.8.2: + resolution: {integrity: sha512-EnzNNLh+9/sJsimsA/FGqzakmrAUKLeJvjRHlg8df1f97NLUlFidk9600y0ZgWOp3CAxn6Hjtk+08tixlUOWyw==} + dev: false + + /@firebase/analytics@0.10.4(@firebase/app@0.10.5): + resolution: {integrity: sha512-OJEl/8Oye/k+vJ1zV/1L6eGpc1XzAj+WG2TPznJ7PszL7sOFLBXkL9IjHfOCGDGpXeO3btozy/cYUqv4zgNeHg==} + peerDependencies: + '@firebase/app': 0.x + dependencies: + '@firebase/app': 0.10.5 + '@firebase/component': 0.6.7 + '@firebase/installations': 0.6.7(@firebase/app@0.10.5) + '@firebase/logger': 0.4.2 + '@firebase/util': 1.9.6 + tslib: 2.6.2 + dev: false + + /@firebase/app-check-compat@0.3.11(@firebase/app-compat@0.2.35)(@firebase/app@0.10.5): + resolution: {integrity: sha512-t01zaH3RJpKEey0nGduz3Is+uSz7Sj4U5nwOV6lWb+86s5xtxpIvBJzu/lKxJfYyfZ29eJwpdjEgT1/lm4iQyA==} + peerDependencies: + '@firebase/app-compat': 0.x + dependencies: + '@firebase/app-check': 0.8.4(@firebase/app@0.10.5) + '@firebase/app-check-types': 0.5.2 + '@firebase/app-compat': 0.2.35 + '@firebase/component': 0.6.7 + '@firebase/logger': 0.4.2 + '@firebase/util': 1.9.6 + tslib: 2.6.2 + transitivePeerDependencies: + - '@firebase/app' + dev: false + + /@firebase/app-check-interop-types@0.3.2: + resolution: {integrity: sha512-LMs47Vinv2HBMZi49C09dJxp0QT5LwDzFaVGf/+ITHe3BlIhUiLNttkATSXplc89A2lAaeTqjgqVkiRfUGyQiQ==} + dev: false + + /@firebase/app-check-types@0.5.2: + resolution: {integrity: sha512-FSOEzTzL5bLUbD2co3Zut46iyPWML6xc4x+78TeaXMSuJap5QObfb+rVvZJtla3asN4RwU7elaQaduP+HFizDA==} + dev: false + + /@firebase/app-check@0.8.4(@firebase/app@0.10.5): + resolution: {integrity: sha512-2tjRDaxcM5G7BEpytiDcIl+NovV99q8yEqRMKDbn4J4i/XjjuThuB4S+4PkmTnZiCbdLXQiBhkVxNlUDcfog5Q==} + peerDependencies: + '@firebase/app': 0.x + dependencies: + '@firebase/app': 0.10.5 + '@firebase/component': 0.6.7 + '@firebase/logger': 0.4.2 + '@firebase/util': 1.9.6 + tslib: 2.6.2 + dev: false + + /@firebase/app-compat@0.2.35: + resolution: {integrity: sha512-vgay/WRjeH0r97/Q6L6df2CMx7oyNFDsE5yPQ9oR1G+zx2eT0s8vNNh0WlKqQxUEWaOLRnXhQ8gy7uu0cBgTRg==} + dependencies: + '@firebase/app': 0.10.5 + '@firebase/component': 0.6.7 + '@firebase/logger': 0.4.2 + '@firebase/util': 1.9.6 + tslib: 2.6.2 + dev: false + + /@firebase/app-types@0.9.2: + resolution: {integrity: sha512-oMEZ1TDlBz479lmABwWsWjzHwheQKiAgnuKxE0pz0IXCVx7/rtlkx1fQ6GfgK24WCrxDKMplZrT50Kh04iMbXQ==} + dev: false + + /@firebase/app@0.10.5: + resolution: {integrity: sha512-iY/fNot+hWPk9sTX8aHMqlcX9ynRvpGkskWAdUZ2eQQdLo8d1hSFYcYNwPv0Q/frGMasw8udKWMcFOEpC9fG8g==} + dependencies: + '@firebase/component': 0.6.7 + '@firebase/logger': 0.4.2 + '@firebase/util': 1.9.6 + idb: 7.1.1 + tslib: 2.6.2 + dev: false + + /@firebase/auth-compat@0.5.9(@firebase/app-compat@0.2.35)(@firebase/app-types@0.9.2)(@firebase/app@0.10.5): + resolution: {integrity: sha512-RX8Zh/3zz2CsVbmYfgHkfUm4fAEPCl+KHVIImNygV5jTGDF6oKOhBIpf4Yigclyu8ESQKZ4elyN0MBYm9/7zGw==} + peerDependencies: + '@firebase/app-compat': 0.x + dependencies: + '@firebase/app-compat': 0.2.35 + '@firebase/auth': 1.7.4(@firebase/app@0.10.5) + '@firebase/auth-types': 0.12.2(@firebase/app-types@0.9.2)(@firebase/util@1.9.6) + '@firebase/component': 0.6.7 + '@firebase/util': 1.9.6 + tslib: 2.6.2 + undici: 5.28.4 + transitivePeerDependencies: + - '@firebase/app' + - '@firebase/app-types' + - '@react-native-async-storage/async-storage' + dev: false + + /@firebase/auth-interop-types@0.2.3: + resolution: {integrity: sha512-Fc9wuJGgxoxQeavybiuwgyi+0rssr76b+nHpj+eGhXFYAdudMWyfBHvFL/I5fEHniUM/UQdFzi9VXJK2iZF7FQ==} + dev: false + + /@firebase/auth-types@0.12.2(@firebase/app-types@0.9.2)(@firebase/util@1.9.6): + resolution: {integrity: sha512-qsEBaRMoGvHO10unlDJhaKSuPn4pyoTtlQuP1ghZfzB6rNQPuhp/N/DcFZxm9i4v0SogjCbf9reWupwIvfmH6w==} + peerDependencies: + '@firebase/app-types': 0.x + '@firebase/util': 1.x + dependencies: + '@firebase/app-types': 0.9.2 + '@firebase/util': 1.9.6 + dev: false + + /@firebase/auth@1.7.4(@firebase/app@0.10.5): + resolution: {integrity: sha512-d2Fw17s5QesojwebrA903el20Li9/YGgkoOGJjagM4I1qAT36APa/FcZ+OX86KxbYKCtQKTMqraU8pxG7C2JWA==} + peerDependencies: + '@firebase/app': 0.x + '@react-native-async-storage/async-storage': ^1.18.1 + peerDependenciesMeta: + '@react-native-async-storage/async-storage': + optional: true + dependencies: + '@firebase/app': 0.10.5 + '@firebase/component': 0.6.7 + '@firebase/logger': 0.4.2 + '@firebase/util': 1.9.6 + tslib: 2.6.2 + undici: 5.28.4 + dev: false + + /@firebase/component@0.6.7: + resolution: {integrity: sha512-baH1AA5zxfaz4O8w0vDwETByrKTQqB5CDjRls79Sa4eAGAoERw4Tnung7XbMl3jbJ4B/dmmtsMrdki0KikwDYA==} + dependencies: + '@firebase/util': 1.9.6 + tslib: 2.6.2 + dev: false + + /@firebase/database-compat@1.0.5: + resolution: {integrity: sha512-NDSMaDjQ+TZEMDMmzJwlTL05kh1+0Y84C+kVMaOmNOzRGRM7VHi29I6YUhCetXH+/b1Wh4ZZRyp1CuWkd8s6hg==} + dependencies: + '@firebase/component': 0.6.7 + '@firebase/database': 1.0.5 + '@firebase/database-types': 1.0.3 + '@firebase/logger': 0.4.2 + '@firebase/util': 1.9.6 + tslib: 2.6.2 + dev: false + + /@firebase/database-types@1.0.3: + resolution: {integrity: sha512-39V/Riv2R3O/aUjYKh0xypj7NTNXNAK1bcgY5Kx+hdQPRS/aPTS8/5c0CGFYKgVuFbYlnlnhrCTYsh2uNhGwzA==} + dependencies: + '@firebase/app-types': 0.9.2 + '@firebase/util': 1.9.6 + dev: false + + /@firebase/database@1.0.5: + resolution: {integrity: sha512-cAfwBqMQuW6HbhwI3Cb/gDqZg7aR0OmaJ85WUxlnoYW2Tm4eR0hFl5FEijI3/gYPUiUcUPQvTkGV222VkT7KPw==} + dependencies: + '@firebase/app-check-interop-types': 0.3.2 + '@firebase/auth-interop-types': 0.2.3 + '@firebase/component': 0.6.7 + '@firebase/logger': 0.4.2 + '@firebase/util': 1.9.6 + faye-websocket: 0.11.4 + tslib: 2.6.2 + dev: false + + /@firebase/firestore-compat@0.3.32(@firebase/app-compat@0.2.35)(@firebase/app-types@0.9.2)(@firebase/app@0.10.5): + resolution: {integrity: sha512-at71mwK7a/mUXH0OgyY0+gUzedm/EUydDFYSFsBoO8DYowZ23Mgd6P4Rzq/Ll3zI/3xJN7LGe7Qp4iE/V/3Arg==} + peerDependencies: + '@firebase/app-compat': 0.x + dependencies: + '@firebase/app-compat': 0.2.35 + '@firebase/component': 0.6.7 + '@firebase/firestore': 4.6.3(@firebase/app@0.10.5) + '@firebase/firestore-types': 3.0.2(@firebase/app-types@0.9.2)(@firebase/util@1.9.6) + '@firebase/util': 1.9.6 + tslib: 2.6.2 + transitivePeerDependencies: + - '@firebase/app' + - '@firebase/app-types' + dev: false + + /@firebase/firestore-types@3.0.2(@firebase/app-types@0.9.2)(@firebase/util@1.9.6): + resolution: {integrity: sha512-wp1A+t5rI2Qc/2q7r2ZpjUXkRVPtGMd6zCLsiWurjsQpqPgFin3AhNibKcIzoF2rnToNa/XYtyWXuifjOOwDgg==} + peerDependencies: + '@firebase/app-types': 0.x + '@firebase/util': 1.x + dependencies: + '@firebase/app-types': 0.9.2 + '@firebase/util': 1.9.6 + dev: false + + /@firebase/firestore@4.6.3(@firebase/app@0.10.5): + resolution: {integrity: sha512-d/+N2iUsiJ/Dc7fApdpdmmTXzwuTCromsdA1lKwYfZtMIOd1fI881NSLwK2wV4I38wkLnvfKJUV6WpU1f3/ONg==} + engines: {node: '>=10.10.0'} + peerDependencies: + '@firebase/app': 0.x + dependencies: + '@firebase/app': 0.10.5 + '@firebase/component': 0.6.7 + '@firebase/logger': 0.4.2 + '@firebase/util': 1.9.6 + '@firebase/webchannel-wrapper': 1.0.0 + '@grpc/grpc-js': 1.9.15 + '@grpc/proto-loader': 0.7.13 + tslib: 2.6.2 + undici: 5.28.4 + dev: false + + /@firebase/functions-compat@0.3.11(@firebase/app-compat@0.2.35)(@firebase/app@0.10.5): + resolution: {integrity: sha512-Qn+ts/M6Lj2/6i1cp5V5TRR+Hi9kyXyHbo+w9GguINJ87zxrCe6ulx3TI5AGQkoQa8YFHUhT3DMGmLFiJjWTSQ==} + peerDependencies: + '@firebase/app-compat': 0.x + dependencies: + '@firebase/app-compat': 0.2.35 + '@firebase/component': 0.6.7 + '@firebase/functions': 0.11.5(@firebase/app@0.10.5) + '@firebase/functions-types': 0.6.2 + '@firebase/util': 1.9.6 + tslib: 2.6.2 + transitivePeerDependencies: + - '@firebase/app' + dev: false + + /@firebase/functions-types@0.6.2: + resolution: {integrity: sha512-0KiJ9lZ28nS2iJJvimpY4nNccV21rkQyor5Iheu/nq8aKXJqtJdeSlZDspjPSBBiHRzo7/GMUttegnsEITqR+w==} + dev: false + + /@firebase/functions@0.11.5(@firebase/app@0.10.5): + resolution: {integrity: sha512-qrHJ+l62mZiU5UZiVi84t/iLXZlhRuSvBQsa2qvNLgPsEWR7wdpWhRmVdB7AU8ndkSHJjGlMICqrVnz47sgU7Q==} + peerDependencies: + '@firebase/app': 0.x + dependencies: + '@firebase/app': 0.10.5 + '@firebase/app-check-interop-types': 0.3.2 + '@firebase/auth-interop-types': 0.2.3 + '@firebase/component': 0.6.7 + '@firebase/messaging-interop-types': 0.2.2 + '@firebase/util': 1.9.6 + tslib: 2.6.2 + undici: 5.28.4 + dev: false + + /@firebase/installations-compat@0.2.7(@firebase/app-compat@0.2.35)(@firebase/app-types@0.9.2)(@firebase/app@0.10.5): + resolution: {integrity: sha512-RPcbD+3nqHbnhVjIOpWK2H5qzZ8pAAAScceiWph0VNTqpKyPQ5tDcp4V5fS0ELpfgsHYvroMLDKfeHxpfvm8cw==} + peerDependencies: + '@firebase/app-compat': 0.x + dependencies: + '@firebase/app-compat': 0.2.35 + '@firebase/component': 0.6.7 + '@firebase/installations': 0.6.7(@firebase/app@0.10.5) + '@firebase/installations-types': 0.5.2(@firebase/app-types@0.9.2) + '@firebase/util': 1.9.6 + tslib: 2.6.2 + transitivePeerDependencies: + - '@firebase/app' + - '@firebase/app-types' + dev: false + + /@firebase/installations-types@0.5.2(@firebase/app-types@0.9.2): + resolution: {integrity: sha512-que84TqGRZJpJKHBlF2pkvc1YcXrtEDOVGiDjovP/a3s6W4nlbohGXEsBJo0JCeeg/UG9A+DEZVDUV9GpklUzA==} + peerDependencies: + '@firebase/app-types': 0.x + dependencies: + '@firebase/app-types': 0.9.2 + dev: false + + /@firebase/installations@0.6.7(@firebase/app@0.10.5): + resolution: {integrity: sha512-i6iGoXRu5mX4rTsiMSSKrgh9pSEzD4hwBEzRh5kEhOTr8xN/wvQcCPZDSMVYKwM2XyCPBLVq0JzjyerwL0Rihg==} + peerDependencies: + '@firebase/app': 0.x + dependencies: + '@firebase/app': 0.10.5 + '@firebase/component': 0.6.7 + '@firebase/util': 1.9.6 + idb: 7.1.1 + tslib: 2.6.2 + dev: false + + /@firebase/logger@0.4.2: + resolution: {integrity: sha512-Q1VuA5M1Gjqrwom6I6NUU4lQXdo9IAQieXlujeHZWvRt1b7qQ0KwBaNAjgxG27jgF9/mUwsNmO8ptBCGVYhB0A==} + dependencies: + tslib: 2.6.2 + dev: false + + /@firebase/messaging-compat@0.2.9(@firebase/app-compat@0.2.35)(@firebase/app@0.10.5): + resolution: {integrity: sha512-5jN6wyhwPgBH02zOtmmoOeyfsmoD7ty48D1m0vVPsFg55RqN2Z3Q9gkZ5GmPklFPjTPLcxB1ObcHOZvThTkm7g==} + peerDependencies: + '@firebase/app-compat': 0.x + dependencies: + '@firebase/app-compat': 0.2.35 + '@firebase/component': 0.6.7 + '@firebase/messaging': 0.12.9(@firebase/app@0.10.5) + '@firebase/util': 1.9.6 + tslib: 2.6.2 + transitivePeerDependencies: + - '@firebase/app' + dev: false + + /@firebase/messaging-interop-types@0.2.2: + resolution: {integrity: sha512-l68HXbuD2PPzDUOFb3aG+nZj5KA3INcPwlocwLZOzPp9rFM9yeuI9YLl6DQfguTX5eAGxO0doTR+rDLDvQb5tA==} + dev: false + + /@firebase/messaging@0.12.9(@firebase/app@0.10.5): + resolution: {integrity: sha512-IH+JJmzbFGZXV3+TDyKdqqKPVfKRqBBg2BfYYOy7cm7J+SwV+uJMe8EnDKYeQLEQhtpwciPfJ3qQXJs2lbxDTw==} + peerDependencies: + '@firebase/app': 0.x + dependencies: + '@firebase/app': 0.10.5 + '@firebase/component': 0.6.7 + '@firebase/installations': 0.6.7(@firebase/app@0.10.5) + '@firebase/messaging-interop-types': 0.2.2 + '@firebase/util': 1.9.6 + idb: 7.1.1 + tslib: 2.6.2 + dev: false + + /@firebase/performance-compat@0.2.7(@firebase/app-compat@0.2.35)(@firebase/app@0.10.5): + resolution: {integrity: sha512-cb8ge/5iTstxfIGW+iiY+7l3FtN8gobNh9JSQNZgLC9xmcfBYWEs8IeEWMI6S8T+At0oHc3lv+b2kpRMUWr8zQ==} + peerDependencies: + '@firebase/app-compat': 0.x + dependencies: + '@firebase/app-compat': 0.2.35 + '@firebase/component': 0.6.7 + '@firebase/logger': 0.4.2 + '@firebase/performance': 0.6.7(@firebase/app@0.10.5) + '@firebase/performance-types': 0.2.2 + '@firebase/util': 1.9.6 + tslib: 2.6.2 + transitivePeerDependencies: + - '@firebase/app' + dev: false + + /@firebase/performance-types@0.2.2: + resolution: {integrity: sha512-gVq0/lAClVH5STrIdKnHnCo2UcPLjJlDUoEB/tB4KM+hAeHUxWKnpT0nemUPvxZ5nbdY/pybeyMe8Cs29gEcHA==} + dev: false + + /@firebase/performance@0.6.7(@firebase/app@0.10.5): + resolution: {integrity: sha512-d+Q4ltjdJZqjzcdms5i0UC9KLYX7vKGcygZ+7zHA/Xk+bAbMD2CPU0nWTnlNFWifZWIcXZ/2mAMvaGMW3lypUA==} + peerDependencies: + '@firebase/app': 0.x + dependencies: + '@firebase/app': 0.10.5 + '@firebase/component': 0.6.7 + '@firebase/installations': 0.6.7(@firebase/app@0.10.5) + '@firebase/logger': 0.4.2 + '@firebase/util': 1.9.6 + tslib: 2.6.2 + dev: false + + /@firebase/remote-config-compat@0.2.7(@firebase/app-compat@0.2.35)(@firebase/app@0.10.5): + resolution: {integrity: sha512-Fq0oneQ4SluLnfr5/HfzRS1TZf1ANj1rWbCCW3+oC98An3nE+sCdp+FSuHsEVNwgMg4Tkwx9Oom2lkKeU+Vn+w==} + peerDependencies: + '@firebase/app-compat': 0.x + dependencies: + '@firebase/app-compat': 0.2.35 + '@firebase/component': 0.6.7 + '@firebase/logger': 0.4.2 + '@firebase/remote-config': 0.4.7(@firebase/app@0.10.5) + '@firebase/remote-config-types': 0.3.2 + '@firebase/util': 1.9.6 + tslib: 2.6.2 + transitivePeerDependencies: + - '@firebase/app' + dev: false + + /@firebase/remote-config-types@0.3.2: + resolution: {integrity: sha512-0BC4+Ud7y2aPTyhXJTMTFfrGGLqdYXrUB9sJVAB8NiqJswDTc4/2qrE/yfUbnQJhbSi6ZaTTBKyG3n1nplssaA==} + dev: false + + /@firebase/remote-config@0.4.7(@firebase/app@0.10.5): + resolution: {integrity: sha512-5oPNrPFLsbsjpq0lUEIXoDF2eJK7vAbyXe/DEuZQxnwJlfR7aQbtUlEkRgQWcicXpyDmAmDLo7q7lDbCYa6CpA==} + peerDependencies: + '@firebase/app': 0.x + dependencies: + '@firebase/app': 0.10.5 + '@firebase/component': 0.6.7 + '@firebase/installations': 0.6.7(@firebase/app@0.10.5) + '@firebase/logger': 0.4.2 + '@firebase/util': 1.9.6 + tslib: 2.6.2 + dev: false + + /@firebase/storage-compat@0.3.8(@firebase/app-compat@0.2.35)(@firebase/app-types@0.9.2)(@firebase/app@0.10.5): + resolution: {integrity: sha512-qDfY9kMb6Ch2hZb40sBjDQ8YPxbjGOxuT+gU1Z0iIVSSpSX0f4YpGJCypUXiA0T11n6InCXB+T/Dknh2yxVTkg==} + peerDependencies: + '@firebase/app-compat': 0.x + dependencies: + '@firebase/app-compat': 0.2.35 + '@firebase/component': 0.6.7 + '@firebase/storage': 0.12.5(@firebase/app@0.10.5) + '@firebase/storage-types': 0.8.2(@firebase/app-types@0.9.2)(@firebase/util@1.9.6) + '@firebase/util': 1.9.6 + tslib: 2.6.2 + transitivePeerDependencies: + - '@firebase/app' + - '@firebase/app-types' + dev: false + + /@firebase/storage-types@0.8.2(@firebase/app-types@0.9.2)(@firebase/util@1.9.6): + resolution: {integrity: sha512-0vWu99rdey0g53lA7IShoA2Lol1jfnPovzLDUBuon65K7uKG9G+L5uO05brD9pMw+l4HRFw23ah3GwTGpEav6g==} + peerDependencies: + '@firebase/app-types': 0.x + '@firebase/util': 1.x + dependencies: + '@firebase/app-types': 0.9.2 + '@firebase/util': 1.9.6 + dev: false + + /@firebase/storage@0.12.5(@firebase/app@0.10.5): + resolution: {integrity: sha512-nGWBOGFNr10j0LA4NJ3/Yh3us/lb0Q1xSIKZ38N6FcS+vY54nqJ7k3zE3PENregHC8+8txRow++A568G3v8hOA==} + peerDependencies: + '@firebase/app': 0.x + dependencies: + '@firebase/app': 0.10.5 + '@firebase/component': 0.6.7 + '@firebase/util': 1.9.6 + tslib: 2.6.2 + undici: 5.28.4 + dev: false + + /@firebase/util@1.9.6: + resolution: {integrity: sha512-IBr1MZbp4d5MjBCXL3TW1dK/PDXX4yOGbiwRNh1oAbE/+ci5Uuvy9KIrsFYY80as1I0iOaD5oOMA9Q8j4TJWcw==} + dependencies: + tslib: 2.6.2 + dev: false + + /@firebase/vertexai-preview@0.0.2(@firebase/app-types@0.9.2)(@firebase/app@0.10.5): + resolution: {integrity: sha512-NOOL63kFQRq45ioi5P+hlqj/4LNmvn1URhGjQdvyV54c1Irvoq26aW861PRRLjrSMIeNeiLtCLD5pe+ediepAg==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@firebase/app': 0.x + '@firebase/app-types': 0.x + dependencies: + '@firebase/app': 0.10.5 + '@firebase/app-check-interop-types': 0.3.2 + '@firebase/app-types': 0.9.2 + '@firebase/component': 0.6.7 + '@firebase/logger': 0.4.2 + '@firebase/util': 1.9.6 + tslib: 2.6.2 + dev: false + + /@firebase/webchannel-wrapper@1.0.0: + resolution: {integrity: sha512-zuWxyfXNbsKbm96HhXzainONPFqRcoZblQ++e9cAIGUuHfl2cFSBzW01jtesqWG/lqaUyX3H8O1y9oWboGNQBA==} + dev: false + /@floating-ui/core@1.6.0: resolution: {integrity: sha512-PcF++MykgmTj3CIyOQbKA/hDzOAiqI3mhuoN44WRCopIs1sgoDoU4oty4Jtqaj/y3oDU6fnVSm4QG0a3t5i0+g==} dependencies: @@ -557,6 +1027,100 @@ packages: resolution: {integrity: sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q==} dev: false + /@google-cloud/firestore@7.8.0: + resolution: {integrity: sha512-m21BWVZLz7H7NF8HZ5hCGUSCEJKNwYB5yzQqDTuE9YUzNDRMDei3BwVDht5k4xF636sGlnobyBL+dcbthSGONg==} + engines: {node: '>=14.0.0'} + requiresBuild: true + dependencies: + fast-deep-equal: 3.1.3 + functional-red-black-tree: 1.0.1 + google-gax: 4.3.6 + protobufjs: 7.3.2 + transitivePeerDependencies: + - encoding + - supports-color + dev: false + optional: true + + /@google-cloud/paginator@5.0.2: + resolution: {integrity: sha512-DJS3s0OVH4zFDB1PzjxAsHqJT6sKVbRwwML0ZBP9PbU7Yebtu/7SWMRzvO2J3nUi9pRNITCfu4LJeooM2w4pjg==} + engines: {node: '>=14.0.0'} + requiresBuild: true + dependencies: + arrify: 2.0.1 + extend: 3.0.2 + dev: false + optional: true + + /@google-cloud/projectify@4.0.0: + resolution: {integrity: sha512-MmaX6HeSvyPbWGwFq7mXdo0uQZLGBYCwziiLIGq5JVX+/bdI3SAq6bP98trV5eTWfLuvsMcIC1YJOF2vfteLFA==} + engines: {node: '>=14.0.0'} + requiresBuild: true + dev: false + optional: true + + /@google-cloud/promisify@4.0.0: + resolution: {integrity: sha512-Orxzlfb9c67A15cq2JQEyVc7wEsmFBmHjZWZYQMUyJ1qivXyMwdyNOs9odi79hze+2zqdTtu1E19IM/FtqZ10g==} + engines: {node: '>=14'} + requiresBuild: true + dev: false + optional: true + + /@google-cloud/storage@7.11.2: + resolution: {integrity: sha512-jJOrKyOdujfrSF8EJODW9yY6hqO4jSTk6eVITEj2gsD43BSXuDlnMlLOaBUQhXL29VGnSkxDgYl5tlFhA6LKSA==} + engines: {node: '>=14'} + requiresBuild: true + dependencies: + '@google-cloud/paginator': 5.0.2 + '@google-cloud/projectify': 4.0.0 + '@google-cloud/promisify': 4.0.0 + abort-controller: 3.0.0 + async-retry: 1.3.3 + duplexify: 4.1.3 + fast-xml-parser: 4.4.0 + gaxios: 6.6.0 + google-auth-library: 9.11.0 + html-entities: 2.5.2 + mime: 3.0.0 + p-limit: 3.1.0 + retry-request: 7.0.2 + teeny-request: 9.0.0 + uuid: 8.3.2 + transitivePeerDependencies: + - encoding + - supports-color + dev: false + optional: true + + /@grpc/grpc-js@1.10.9: + resolution: {integrity: sha512-5tcgUctCG0qoNyfChZifz2tJqbRbXVO9J7X6duFcOjY3HUNCxg5D0ZCK7EP9vIcZ0zRpLU9bWkyCqVCLZ46IbQ==} + engines: {node: '>=12.10.0'} + requiresBuild: true + dependencies: + '@grpc/proto-loader': 0.7.13 + '@js-sdsl/ordered-map': 4.4.2 + dev: false + optional: true + + /@grpc/grpc-js@1.9.15: + resolution: {integrity: sha512-nqE7Hc0AzI+euzUwDAy0aY5hCp10r734gMGRdU+qOPX0XSceI2ULrcXB5U2xSc5VkWwalCj4M7GzCAygZl2KoQ==} + engines: {node: ^8.13.0 || >=10.10.0} + dependencies: + '@grpc/proto-loader': 0.7.13 + '@types/node': 20.10.4 + dev: false + + /@grpc/proto-loader@0.7.13: + resolution: {integrity: sha512-AiXO/bfe9bmxBjxxtYxFAXGZvMaN5s8kO+jBHAJCON8rJoB5YS/D6X7ZNc6XQkuHNmyl4CYaMI1fJ/Gn27RGGw==} + engines: {node: '>=6'} + hasBin: true + dependencies: + lodash.camelcase: 4.3.0 + long: 5.2.3 + protobufjs: 7.3.2 + yargs: 17.7.2 + dev: false + /@humanwhocodes/config-array@0.11.13: resolution: {integrity: sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==} engines: {node: '>=10.10.0'} @@ -790,6 +1354,12 @@ packages: '@jridgewell/resolve-uri': 3.1.1 '@jridgewell/sourcemap-codec': 1.4.15 + /@js-sdsl/ordered-map@4.4.2: + resolution: {integrity: sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==} + requiresBuild: true + dev: false + optional: true + /@mswjs/cookies@1.1.0: resolution: {integrity: sha512-0ZcCVQxifZmhwNBoQIrystCb+2sWBY2Zw8lpfJBPCHGCA/HWqehITeCRVIv4VMy8MPlaHo2w2pTHFV2pFfqKPw==} engines: {node: '>=18'} @@ -942,6 +1512,49 @@ packages: resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==} dev: true + /@protobufjs/aspromise@1.1.2: + resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} + dev: false + + /@protobufjs/base64@1.1.2: + resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} + dev: false + + /@protobufjs/codegen@2.0.4: + resolution: {integrity: sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==} + dev: false + + /@protobufjs/eventemitter@1.1.0: + resolution: {integrity: sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==} + dev: false + + /@protobufjs/fetch@1.1.0: + resolution: {integrity: sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==} + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/inquire': 1.1.0 + dev: false + + /@protobufjs/float@1.0.2: + resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} + dev: false + + /@protobufjs/inquire@1.1.0: + resolution: {integrity: sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==} + dev: false + + /@protobufjs/path@1.1.2: + resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} + dev: false + + /@protobufjs/pool@1.1.0: + resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} + dev: false + + /@protobufjs/utf8@1.1.0: + resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==} + dev: false + /@radix-ui/number@1.0.1: resolution: {integrity: sha512-T5gIdVO2mmPW3NNhjNgEP3cqMXjXL9UbO0BzWcXfvdBs+BohbQxvd/K5hSVKmn9/lbTdsQVKbUcP5WLCwvUbBg==} dependencies: @@ -2236,10 +2849,36 @@ packages: react: 18.2.0 dev: false + /@tootallnate/once@2.0.0: + resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==} + engines: {node: '>= 10'} + requiresBuild: true + dev: false + optional: true + /@types/aos@3.0.7: resolution: {integrity: sha512-sEhyFqvKauUJZDbvAB3Pggynrq6g+2PS4XB3tmUr+mDL1gfDJnwslUC4QQ7/l8UD+LWpr3RxZVR/rHoZrLqZVg==} dev: true + /@types/body-parser@1.19.5: + resolution: {integrity: sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==} + dependencies: + '@types/connect': 3.4.38 + '@types/node': 20.10.4 + dev: false + + /@types/caseless@0.12.5: + resolution: {integrity: sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg==} + requiresBuild: true + dev: false + optional: true + + /@types/connect@3.4.38: + resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + dependencies: + '@types/node': 20.10.4 + dev: false + /@types/cookie@0.4.1: resolution: {integrity: sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==} dev: true @@ -2256,12 +2895,34 @@ packages: resolution: {integrity: sha512-iaiDuDI2aIFft7XkcwMzDWLqo7LVDixd2sR6B4wxJut9xcp/Ev9bO4EFg4rm6S9QxATLBj5OPxdeocgmhjwKaw==} dev: true + /@types/express-serve-static-core@4.19.3: + resolution: {integrity: sha512-KOzM7MhcBFlmnlr/fzISFF5vGWVSvN6fTd4T+ExOt08bA/dA5kpSzY52nMsI1KDFmUREpJelPYyuslLRSjjgCg==} + dependencies: + '@types/node': 20.10.4 + '@types/qs': 6.9.15 + '@types/range-parser': 1.2.7 + '@types/send': 0.17.4 + dev: false + + /@types/express@4.17.21: + resolution: {integrity: sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==} + dependencies: + '@types/body-parser': 1.19.5 + '@types/express-serve-static-core': 4.19.3 + '@types/qs': 6.9.15 + '@types/serve-static': 1.15.7 + dev: false + /@types/hast@2.3.10: resolution: {integrity: sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==} dependencies: '@types/unist': 2.0.10 dev: false + /@types/http-errors@2.0.4: + resolution: {integrity: sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==} + dev: false + /@types/js-levenshtein@1.1.3: resolution: {integrity: sha512-jd+Q+sD20Qfu9e2aEXogiO3vpOC1PYJOUdyN9gvs4Qrvkg4wF43L5OhqrPeokdv8TL0/mXoYfpkcoGZMNN2pkQ==} dev: true @@ -2274,7 +2935,6 @@ packages: resolution: {integrity: sha512-VRLSGzik+Unrup6BsouBeHsf4d1hOEgYWTm/7Nmw1sXoN1+tRly/Gy/po3yeahnP4jfnQWWAhQAqcNfH7ngOkA==} dependencies: '@types/node': 20.10.4 - dev: true /@types/lodash-es@4.17.12: resolution: {integrity: sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==} @@ -2291,11 +2951,20 @@ packages: /@types/lodash@4.14.202: resolution: {integrity: sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ==} + /@types/long@4.0.2: + resolution: {integrity: sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==} + requiresBuild: true + dev: false + optional: true + + /@types/mime@1.3.5: + resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} + dev: false + /@types/node@20.10.4: resolution: {integrity: sha512-D08YG6rr8X90YB56tSIuBaddy/UXAA9RKJoFvrsnogAum/0pmjkgi4+2nx96A330FmioegBWmEYQ+syqCFaveg==} dependencies: undici-types: 5.26.5 - dev: true /@types/parse-json@4.0.2: resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==} @@ -2308,6 +2977,14 @@ packages: /@types/prop-types@15.7.11: resolution: {integrity: sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==} + /@types/qs@6.9.15: + resolution: {integrity: sha512-uXHQKES6DQKKCLh441Xv/dwxOq1TVS3JPUMlEqoEglvlhR6Mxnlew/Xq/LRVHpLyk7iK3zODe1qYHIMltO7XGg==} + dev: false + + /@types/range-parser@1.2.7: + resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} + dev: false + /@types/react-dom@18.2.17: resolution: {integrity: sha512-rvrT/M7Df5eykWFxn6MYt5Pem/Dbyc1N8Y0S9Mrkw2WFCRiqUgw9P7ul2NpwsXCSM1DVdENzdG9J5SreqfAIWg==} dependencies: @@ -2338,9 +3015,35 @@ packages: '@types/scheduler': 0.16.8 csstype: 3.1.3 + /@types/request@2.48.12: + resolution: {integrity: sha512-G3sY+NpsA9jnwm0ixhAFQSJ3Q9JkpLZpJbI3GMv0mIAT0y3mRabYeINzal5WOChIiaTEGQYlHOKgkaM9EisWHw==} + requiresBuild: true + dependencies: + '@types/caseless': 0.12.5 + '@types/node': 20.10.4 + '@types/tough-cookie': 4.0.5 + form-data: 2.5.1 + dev: false + optional: true + /@types/scheduler@0.16.8: resolution: {integrity: sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==} + /@types/send@0.17.4: + resolution: {integrity: sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==} + dependencies: + '@types/mime': 1.3.5 + '@types/node': 20.10.4 + dev: false + + /@types/serve-static@1.15.7: + resolution: {integrity: sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==} + dependencies: + '@types/http-errors': 2.0.4 + '@types/node': 20.10.4 + '@types/send': 0.17.4 + dev: false + /@types/sockjs-client@1.5.4: resolution: {integrity: sha512-zk+uFZeWyvJ5ZFkLIwoGA/DfJ+pYzcZ8eH4H/EILCm2OBZyHH6Hkdna1/UWL/CFruh5wj6ES7g75SvUB0VsH5w==} dev: true @@ -2355,6 +3058,12 @@ packages: resolution: {integrity: sha512-eqNDvZsCNY49OAXB0Firg/Sc2BgoWsntsLUdybGFOhAfCD6QJ2n9HXUIHGqt5qjrxmMv4wS8WLAw43ZkKcJ8Pw==} dev: true + /@types/tough-cookie@4.0.5: + resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==} + requiresBuild: true + dev: false + optional: true + /@types/unist@2.0.10: resolution: {integrity: sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==} dev: false @@ -2440,6 +3149,15 @@ packages: resolution: {integrity: sha512-+i7+JmNiE/3c9FKxzWFi2IjRJ+KzZl1QPu6QNrsgaa2MuBgXvUy4gA1TVzf/JMdIIloB76xSKikTWuyYAIVLww==} dev: false + /abort-controller@3.0.0: + resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} + engines: {node: '>=6.5'} + requiresBuild: true + dependencies: + event-target-shim: 5.0.1 + dev: false + optional: true + /accepts@1.3.8: resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} engines: {node: '>= 0.6'} @@ -2471,6 +3189,17 @@ packages: - supports-color dev: false + /agent-base@7.1.1: + resolution: {integrity: sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==} + engines: {node: '>= 14'} + requiresBuild: true + dependencies: + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + dev: false + optional: true + /ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} dependencies: @@ -2490,7 +3219,6 @@ packages: /ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} - dev: true /ansi-styles@3.2.1: resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} @@ -2624,6 +3352,13 @@ packages: is-shared-array-buffer: 1.0.2 dev: true + /arrify@2.0.1: + resolution: {integrity: sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==} + engines: {node: '>=8'} + requiresBuild: true + dev: false + optional: true + /ast-types-flow@0.0.8: resolution: {integrity: sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==} dev: true @@ -2632,6 +3367,14 @@ packages: resolution: {integrity: sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==} dev: false + /async-retry@1.3.3: + resolution: {integrity: sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==} + requiresBuild: true + dependencies: + retry: 0.13.1 + dev: false + optional: true + /asynciterator.prototype@1.0.0: resolution: {integrity: sha512-wwHYEIS0Q80f5mosx3L/dfG5t5rjEa9Ft51GTaNt862EnpyGHpgz2RkZvLPp1oF5TnAiTohkEKVEu8pQPJI7Vg==} dependencies: @@ -2698,7 +3441,12 @@ packages: /base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - dev: true + + /bignumber.js@9.1.2: + resolution: {integrity: sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==} + requiresBuild: true + dev: false + optional: true /binary-extensions@2.2.0: resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} @@ -2710,7 +3458,6 @@ packages: buffer: 5.7.1 inherits: 2.0.4 readable-stream: 3.6.2 - dev: true /body-parser@1.20.1: resolution: {integrity: sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==} @@ -2790,7 +3537,6 @@ packages: dependencies: base64-js: 1.5.1 ieee754: 1.2.1 - dev: true /busboy@1.6.0: resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} @@ -2877,6 +3623,10 @@ packages: optionalDependencies: fsevents: 2.3.3 + /chownr@1.1.4: + resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} + dev: false + /class-variance-authority@0.7.0: resolution: {integrity: sha512-jFI8IQw4hczaL4ALINxqLEXQbWcNjoSkloa4IaufXCJr6QawJyw7tuRysRsrE8w2p/4gGaxKIt/hX3qz/IbD1A==} dependencies: @@ -2919,7 +3669,6 @@ packages: string-width: 4.2.3 strip-ansi: 6.0.1 wrap-ansi: 7.0.0 - dev: true /clone@1.0.4: resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} @@ -3143,6 +3892,18 @@ packages: dependencies: ms: 2.1.2 + /decompress-response@6.0.0: + resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} + engines: {node: '>=10'} + dependencies: + mimic-response: 3.1.0 + dev: false + + /deep-extend@0.6.0: + resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} + engines: {node: '>=4.0.0'} + dev: false + /deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} dev: true @@ -3230,6 +3991,17 @@ packages: resolution: {integrity: sha512-kxxKlPEDa6Nc5WJi+qRgPbOAbgTpSULL+vI3NUXsZMlkJxTqYI9wg5ZTay2sFrdZRWHPWNi+EdAhcJf81WtoMQ==} dev: false + /duplexify@4.1.3: + resolution: {integrity: sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==} + requiresBuild: true + dependencies: + end-of-stream: 1.4.4 + inherits: 2.0.4 + readable-stream: 3.6.2 + stream-shift: 1.0.3 + dev: false + optional: true + /ecdsa-sig-formatter@1.0.11: resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} dependencies: @@ -3276,7 +4048,6 @@ packages: /emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} - dev: true /emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} @@ -3287,6 +4058,12 @@ packages: engines: {node: '>= 0.8'} dev: false + /end-of-stream@1.4.4: + resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} + dependencies: + once: 1.4.0 + dev: false + /enhanced-resolve@5.15.0: resolution: {integrity: sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==} engines: {node: '>=10.13.0'} @@ -3392,7 +4169,6 @@ packages: /escalade@3.1.1: resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} engines: {node: '>=6'} - dev: true /escape-html@1.0.3: resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} @@ -3694,11 +4470,23 @@ packages: resolution: {integrity: sha512-4IJSItgS/41IxN5UVAVuAyczwZF7ZIEsM1XAoUzIHA6A+xzusEZUutdXz2Nr+MQPLxfTiCvqE79/C8HT8fKFvA==} dev: false + /event-target-shim@5.0.1: + resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} + engines: {node: '>=6'} + requiresBuild: true + dev: false + optional: true + /eventsource@2.0.2: resolution: {integrity: sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA==} engines: {node: '>=12.0.0'} dev: false + /expand-template@2.0.3: + resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} + engines: {node: '>=6'} + dev: false + /express@4.18.2: resolution: {integrity: sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==} engines: {node: '>= 0.10.0'} @@ -3738,6 +4526,12 @@ packages: - supports-color dev: false + /extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + requiresBuild: true + dev: false + optional: true + /external-editor@3.1.0: resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==} engines: {node: '>=4'} @@ -3747,9 +4541,17 @@ packages: tmp: 0.0.33 dev: true + /farmhash@3.3.1: + resolution: {integrity: sha512-XUizHanzlr/v7suBr/o85HSakOoWh6HKXZjFYl5C2+Gj0f0rkw+XTUZzrd9odDsgI9G5tRUcF4wSbKaX04T0DQ==} + engines: {node: '>=10'} + requiresBuild: true + dependencies: + node-addon-api: 5.1.0 + prebuild-install: 7.1.2 + dev: false + /fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - dev: true /fast-glob@3.3.2: resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} @@ -3769,6 +4571,15 @@ packages: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} dev: true + /fast-xml-parser@4.4.0: + resolution: {integrity: sha512-kLY3jFlwIYwBNDojclKsNAC12sfD6NwW74QB2CoNGPvtVxjliYehVunB3HYyNi+n4Tt1dAcgwYvmKF/Z18flqg==} + hasBin: true + requiresBuild: true + dependencies: + strnum: 1.0.5 + dev: false + optional: true + /fastq@1.15.0: resolution: {integrity: sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==} dependencies: @@ -3834,6 +4645,62 @@ packages: path-exists: 4.0.0 dev: true + /firebase-admin@12.1.1: + resolution: {integrity: sha512-Nuoxk//gaYrspS7TvwBINdGvFhh2QeiaWpRW6+PJ+tWyn2/CugBc7jKa1NaBg0AvhGSOXFOCIsXhzCzHA47Rew==} + engines: {node: '>=14'} + dependencies: + '@fastify/busboy': 2.1.1 + '@firebase/database-compat': 1.0.5 + '@firebase/database-types': 1.0.3 + '@types/node': 20.10.4 + farmhash: 3.3.1 + jsonwebtoken: 9.0.2 + jwks-rsa: 3.1.0 + long: 5.2.3 + node-forge: 1.3.1 + uuid: 9.0.1 + optionalDependencies: + '@google-cloud/firestore': 7.8.0 + '@google-cloud/storage': 7.11.2 + transitivePeerDependencies: + - encoding + - supports-color + dev: false + + /firebase@10.12.2: + resolution: {integrity: sha512-ZxEdtSvP1I9su1yf32D8TIdgxtPgxwr6z3jYAR1TXS/t+fVfpoPc/N1/N2bxOco9mNjUoc+od34v5Fn4GeKs6Q==} + dependencies: + '@firebase/analytics': 0.10.4(@firebase/app@0.10.5) + '@firebase/analytics-compat': 0.2.10(@firebase/app-compat@0.2.35)(@firebase/app@0.10.5) + '@firebase/app': 0.10.5 + '@firebase/app-check': 0.8.4(@firebase/app@0.10.5) + '@firebase/app-check-compat': 0.3.11(@firebase/app-compat@0.2.35)(@firebase/app@0.10.5) + '@firebase/app-compat': 0.2.35 + '@firebase/app-types': 0.9.2 + '@firebase/auth': 1.7.4(@firebase/app@0.10.5) + '@firebase/auth-compat': 0.5.9(@firebase/app-compat@0.2.35)(@firebase/app-types@0.9.2)(@firebase/app@0.10.5) + '@firebase/database': 1.0.5 + '@firebase/database-compat': 1.0.5 + '@firebase/firestore': 4.6.3(@firebase/app@0.10.5) + '@firebase/firestore-compat': 0.3.32(@firebase/app-compat@0.2.35)(@firebase/app-types@0.9.2)(@firebase/app@0.10.5) + '@firebase/functions': 0.11.5(@firebase/app@0.10.5) + '@firebase/functions-compat': 0.3.11(@firebase/app-compat@0.2.35)(@firebase/app@0.10.5) + '@firebase/installations': 0.6.7(@firebase/app@0.10.5) + '@firebase/installations-compat': 0.2.7(@firebase/app-compat@0.2.35)(@firebase/app-types@0.9.2)(@firebase/app@0.10.5) + '@firebase/messaging': 0.12.9(@firebase/app@0.10.5) + '@firebase/messaging-compat': 0.2.9(@firebase/app-compat@0.2.35)(@firebase/app@0.10.5) + '@firebase/performance': 0.6.7(@firebase/app@0.10.5) + '@firebase/performance-compat': 0.2.7(@firebase/app-compat@0.2.35)(@firebase/app@0.10.5) + '@firebase/remote-config': 0.4.7(@firebase/app@0.10.5) + '@firebase/remote-config-compat': 0.2.7(@firebase/app-compat@0.2.35)(@firebase/app@0.10.5) + '@firebase/storage': 0.12.5(@firebase/app@0.10.5) + '@firebase/storage-compat': 0.3.8(@firebase/app-compat@0.2.35)(@firebase/app-types@0.9.2)(@firebase/app@0.10.5) + '@firebase/util': 1.9.6 + '@firebase/vertexai-preview': 0.0.2(@firebase/app-types@0.9.2)(@firebase/app@0.10.5) + transitivePeerDependencies: + - '@react-native-async-storage/async-storage' + dev: false + /flat-cache@3.2.0: resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} engines: {node: ^10.12.0 || >=12.0.0} @@ -3863,6 +4730,17 @@ packages: is-callable: 1.2.7 dev: true + /form-data@2.5.1: + resolution: {integrity: sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==} + engines: {node: '>= 0.12'} + requiresBuild: true + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + mime-types: 2.1.35 + dev: false + optional: true + /form-data@4.0.0: resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==} engines: {node: '>= 6'} @@ -3891,6 +4769,10 @@ packages: engines: {node: '>= 0.6'} dev: false + /fs-constants@1.0.0: + resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + dev: false + /fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} @@ -3914,10 +4796,45 @@ packages: functions-have-names: 1.2.3 dev: true + /functional-red-black-tree@1.0.1: + resolution: {integrity: sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==} + requiresBuild: true + dev: false + optional: true + /functions-have-names@1.2.3: resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} dev: true + /gaxios@6.6.0: + resolution: {integrity: sha512-bpOZVQV5gthH/jVCSuYuokRo2bTKOcuBiVWpjmTn6C5Agl5zclGfTljuGsQZxwwDBkli+YhZhP4TdlqTnhOezQ==} + engines: {node: '>=14'} + requiresBuild: true + dependencies: + extend: 3.0.2 + https-proxy-agent: 7.0.4 + is-stream: 2.0.1 + node-fetch: 2.7.0 + uuid: 9.0.1 + transitivePeerDependencies: + - encoding + - supports-color + dev: false + optional: true + + /gcp-metadata@6.1.0: + resolution: {integrity: sha512-Jh/AIwwgaxan+7ZUUmRLCjtchyDiqh4KjBJ5tW3plBZb5iL/BPcso8A5DlzeD9qlw0duCamnNdpFjxwaT0KyKg==} + engines: {node: '>=14'} + requiresBuild: true + dependencies: + gaxios: 6.6.0 + json-bigint: 1.0.0 + transitivePeerDependencies: + - encoding + - supports-color + dev: false + optional: true + /generic-pool@3.9.0: resolution: {integrity: sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==} engines: {node: '>= 4'} @@ -3926,7 +4843,6 @@ packages: /get-caller-file@2.0.5: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} - dev: true /get-intrinsic@1.2.2: resolution: {integrity: sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==} @@ -3962,6 +4878,10 @@ packages: lodash.memoize: 4.1.2 dev: false + /github-from-package@0.0.0: + resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} + dev: false + /glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -4047,6 +4967,46 @@ packages: slash: 3.0.0 dev: true + /google-auth-library@9.11.0: + resolution: {integrity: sha512-epX3ww/mNnhl6tL45EQ/oixsY8JLEgUFoT4A5E/5iAR4esld9Kqv6IJGk7EmGuOgDvaarwF95hU2+v7Irql9lw==} + engines: {node: '>=14'} + requiresBuild: true + dependencies: + base64-js: 1.5.1 + ecdsa-sig-formatter: 1.0.11 + gaxios: 6.6.0 + gcp-metadata: 6.1.0 + gtoken: 7.1.0 + jws: 4.0.0 + transitivePeerDependencies: + - encoding + - supports-color + dev: false + optional: true + + /google-gax@4.3.6: + resolution: {integrity: sha512-z3MR+pE6WqU+tnKtkJl4c723EYY7Il4fcSNgEbehzUJpcNWkca9AyoC2pdBWmEa0cda21VRpUBb4s6VSATiUKg==} + engines: {node: '>=14'} + requiresBuild: true + dependencies: + '@grpc/grpc-js': 1.10.9 + '@grpc/proto-loader': 0.7.13 + '@types/long': 4.0.2 + abort-controller: 3.0.0 + duplexify: 4.1.3 + google-auth-library: 9.11.0 + node-fetch: 2.7.0 + object-hash: 3.0.0 + proto3-json-serializer: 2.0.2 + protobufjs: 7.3.0 + retry-request: 7.0.2 + uuid: 9.0.1 + transitivePeerDependencies: + - encoding + - supports-color + dev: false + optional: true + /gopd@1.0.1: resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} dependencies: @@ -4064,6 +5024,19 @@ packages: engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} dev: true + /gtoken@7.1.0: + resolution: {integrity: sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==} + engines: {node: '>=14.0.0'} + requiresBuild: true + dependencies: + gaxios: 6.6.0 + jws: 4.0.0 + transitivePeerDependencies: + - encoding + - supports-color + dev: false + optional: true + /hamt_plus@1.0.2: resolution: {integrity: sha512-t2JXKaehnMb9paaYA7J0BX8QQAY8lwfQ9Gjf4pg/mk4krt+cmwmU652HOoWonf+7+EQV97ARPMhhVgU1ra2GhA==} dev: false @@ -4139,6 +5112,12 @@ packages: react-is: 16.13.1 dev: false + /html-entities@2.5.2: + resolution: {integrity: sha512-K//PSRMQk4FZ78Kyau+mZurHn3FH0Vwr+H36eE0rPbeYkRRi9YxceYPhuN60UwWorxyKHhqoAJl2OFKa4BVtaA==} + requiresBuild: true + dev: false + optional: true + /http-errors@2.0.0: resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} engines: {node: '>= 0.8'} @@ -4154,6 +5133,19 @@ packages: resolution: {integrity: sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==} dev: false + /http-proxy-agent@5.0.0: + resolution: {integrity: sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==} + engines: {node: '>= 6'} + requiresBuild: true + dependencies: + '@tootallnate/once': 2.0.0 + agent-base: 6.0.2 + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + dev: false + optional: true + /https-proxy-agent@5.0.1: resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} engines: {node: '>= 6'} @@ -4164,15 +5156,30 @@ packages: - supports-color dev: false + /https-proxy-agent@7.0.4: + resolution: {integrity: sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==} + engines: {node: '>= 14'} + requiresBuild: true + dependencies: + agent-base: 7.1.1 + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + dev: false + optional: true + /iconv-lite@0.4.24: resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} engines: {node: '>=0.10.0'} dependencies: safer-buffer: 2.1.2 + /idb@7.1.1: + resolution: {integrity: sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==} + dev: false + /ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} - dev: true /ignore@5.3.0: resolution: {integrity: sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==} @@ -4204,6 +5211,10 @@ packages: /inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + /ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + dev: false + /inquirer@8.2.6: resolution: {integrity: sha512-M1WuAmb7pn9zdFRtQYk26ZBoY043Sse0wVDdk4Bppr+JOXyQYybdtvK+l9wUibhtjdjvtoiNy8tk+EgsYIUqKg==} engines: {node: '>=12.0.0'} @@ -4333,7 +5344,6 @@ packages: /is-fullwidth-code-point@3.0.0: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} - dev: true /is-generator-function@1.0.10: resolution: {integrity: sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==} @@ -4410,6 +5420,13 @@ packages: call-bind: 1.0.5 dev: true + /is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + requiresBuild: true + dev: false + optional: true + /is-string@1.0.7: resolution: {integrity: sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==} engines: {node: '>= 0.4'} @@ -4474,6 +5491,10 @@ packages: resolution: {integrity: sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==} hasBin: true + /jose@4.15.5: + resolution: {integrity: sha512-jc7BFxgKPKi94uOvEmzlSWFFe2+vASyXaKUpdQKatWAESU2MWjDfFf0fdfc83CDKcA5QecabZeNLyfhe3yKNkg==} + dev: false + /js-levenshtein@1.1.6: resolution: {integrity: sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==} engines: {node: '>=0.10.0'} @@ -4489,6 +5510,14 @@ packages: argparse: 2.0.1 dev: true + /json-bigint@1.0.0: + resolution: {integrity: sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==} + requiresBuild: true + dependencies: + bignumber.js: 9.1.2 + dev: false + optional: true + /json-buffer@3.0.1: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} dev: true @@ -4546,6 +5575,30 @@ packages: safe-buffer: 5.2.1 dev: false + /jwa@2.0.0: + resolution: {integrity: sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==} + requiresBuild: true + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + dev: false + optional: true + + /jwks-rsa@3.1.0: + resolution: {integrity: sha512-v7nqlfezb9YfHHzYII3ef2a2j1XnGeSE/bK3WfumaYCqONAIstJbrEGapz4kadScZzEt7zYCN7bucj8C0Mv/Rg==} + engines: {node: '>=14'} + dependencies: + '@types/express': 4.17.21 + '@types/jsonwebtoken': 9.0.5 + debug: 4.3.4 + jose: 4.15.5 + limiter: 1.1.5 + lru-memoizer: 2.3.0 + transitivePeerDependencies: + - supports-color + dev: false + /jws@3.2.2: resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==} dependencies: @@ -4553,6 +5606,15 @@ packages: safe-buffer: 5.2.1 dev: false + /jws@4.0.0: + resolution: {integrity: sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==} + requiresBuild: true + dependencies: + jwa: 2.0.0 + safe-buffer: 5.2.1 + dev: false + optional: true + /kakao.maps.d.ts@0.1.39: resolution: {integrity: sha512-KXENJ8hHYtjb5G+0vf8TXx/PwWW4j5ndDiQTSMvGtF7EFWu2P3N/+Zivcj9/UKn3j29Iz/sIUaA7WL8Ug3IDGQ==} @@ -4595,6 +5657,10 @@ packages: resolution: {integrity: sha512-K2U4W2Ff5ibV7j7ydLr+zLAkIg5JJ4lPn1Ltsdt+Tz/IjQ8buJ55pZAxoP34lqIiwtF9iAvtLv3JGv7CAyAg+g==} engines: {node: '>=14'} + /limiter@1.1.5: + resolution: {integrity: sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==} + dev: false + /line-height@0.3.1: resolution: {integrity: sha512-YExecgqPwnp5gplD2+Y8e8A5+jKpr25+DzMbFdI1/1UAr0FJrTFv4VkHLf8/6B590i1wUPJWMKKldkd/bdQ//w==} engines: {node: '>= 4.0.0'} @@ -4622,6 +5688,14 @@ packages: resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==} dev: false + /lodash.camelcase@4.3.0: + resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} + dev: false + + /lodash.clonedeep@4.5.0: + resolution: {integrity: sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==} + dev: false + /lodash.debounce@4.0.8: resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} dev: false @@ -4678,6 +5752,10 @@ packages: is-unicode-supported: 0.1.0 dev: true + /long@5.2.3: + resolution: {integrity: sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==} + dev: false + /loose-envify@1.4.0: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true @@ -4712,6 +5790,13 @@ packages: dependencies: yallist: 4.0.0 + /lru-memoizer@2.3.0: + resolution: {integrity: sha512-GXn7gyHAMhO13WSKrIiNfztwxodVsP8IoZ3XfrJV4yH2x0/OeTO/FIaAHTY5YekdGgW94njfuKmyyt1E0mR6Ug==} + dependencies: + lodash.clonedeep: 4.5.0 + lru-cache: 6.0.0 + dev: false + /lucide-react@0.312.0(react@18.2.0): resolution: {integrity: sha512-3UZsqyswRXjW4t+nw+InICewSimjPKHuSxiFYqTshv9xkK3tPPntXk/lvXc9pKlXIxm3v9WKyoxcrB6YHhP+dg==} peerDependencies: @@ -4778,11 +5863,24 @@ packages: hasBin: true dev: false + /mime@3.0.0: + resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==} + engines: {node: '>=10.0.0'} + hasBin: true + requiresBuild: true + dev: false + optional: true + /mimic-fn@2.1.0: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} engines: {node: '>=6'} dev: true + /mimic-response@3.1.0: + resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} + engines: {node: '>=10'} + dev: false + /minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} dependencies: @@ -4798,6 +5896,10 @@ packages: /minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + /mkdirp-classic@0.5.3: + resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} + dev: false + /mkdirp@0.5.6: resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} hasBin: true @@ -4866,6 +5968,10 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + /napi-build-utils@1.0.2: + resolution: {integrity: sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==} + dev: false + /natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} dev: true @@ -4915,6 +6021,17 @@ packages: - babel-plugin-macros dev: false + /node-abi@3.65.0: + resolution: {integrity: sha512-ThjYBfoDNr08AWx6hGaRbfPwxKV9kVzAzOzlLKbk2CuqXE2xnCh+cbAGnwM3t8Lq4v9rUB7VfondlkBckcJrVA==} + engines: {node: '>=10'} + dependencies: + semver: 7.5.4 + dev: false + + /node-addon-api@5.1.0: + resolution: {integrity: sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==} + dev: false + /node-fetch@2.7.0: resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} engines: {node: 4.x || >=6.0.0} @@ -4927,6 +6044,11 @@ packages: whatwg-url: 5.0.0 dev: false + /node-forge@1.3.1: + resolution: {integrity: sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==} + engines: {node: '>= 6.13.0'} + dev: false + /node-releases@2.0.14: resolution: {integrity: sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==} dev: true @@ -5073,7 +6195,6 @@ packages: engines: {node: '>=10'} dependencies: yocto-queue: 0.1.0 - dev: true /p-locate@5.0.0: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} @@ -5230,6 +6351,25 @@ packages: picocolors: 1.0.0 source-map-js: 1.0.2 + /prebuild-install@7.1.2: + resolution: {integrity: sha512-UnNke3IQb6sgarcZIDU3gbMeTp/9SSU1DAIkil7PrqG1vZlBtY5msYccSKSHDqa3hNg436IXK+SNImReuA1wEQ==} + engines: {node: '>=10'} + hasBin: true + dependencies: + detect-libc: 2.0.2 + expand-template: 2.0.3 + github-from-package: 0.0.0 + minimist: 1.2.8 + mkdirp-classic: 0.5.3 + napi-build-utils: 1.0.2 + node-abi: 3.65.0 + pump: 3.0.0 + rc: 1.2.8 + simple-get: 4.0.1 + tar-fs: 2.1.1 + tunnel-agent: 0.6.0 + dev: false + /prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} @@ -5325,6 +6465,54 @@ packages: prosemirror-transform: 1.8.0 dev: false + /proto3-json-serializer@2.0.2: + resolution: {integrity: sha512-SAzp/O4Yh02jGdRc+uIrGoe87dkN/XtwxfZ4ZyafJHymd79ozp5VG5nyZ7ygqPM5+cpLDjjGnYFUkngonyDPOQ==} + engines: {node: '>=14.0.0'} + requiresBuild: true + dependencies: + protobufjs: 7.3.2 + dev: false + optional: true + + /protobufjs@7.3.0: + resolution: {integrity: sha512-YWD03n3shzV9ImZRX3ccbjqLxj7NokGN0V/ESiBV5xWqrommYHYiihuIyavq03pWSGqlyvYUFmfoMKd+1rPA/g==} + engines: {node: '>=12.0.0'} + requiresBuild: true + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/base64': 1.1.2 + '@protobufjs/codegen': 2.0.4 + '@protobufjs/eventemitter': 1.1.0 + '@protobufjs/fetch': 1.1.0 + '@protobufjs/float': 1.0.2 + '@protobufjs/inquire': 1.1.0 + '@protobufjs/path': 1.1.2 + '@protobufjs/pool': 1.1.0 + '@protobufjs/utf8': 1.1.0 + '@types/node': 20.10.4 + long: 5.2.3 + dev: false + optional: true + + /protobufjs@7.3.2: + resolution: {integrity: sha512-RXyHaACeqXeqAKGLDl68rQKbmObRsTIn4TYVUUug1KfS47YWCo5MacGITEryugIgZqORCvJWEk4l449POg5Txg==} + engines: {node: '>=12.0.0'} + requiresBuild: true + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/base64': 1.1.2 + '@protobufjs/codegen': 2.0.4 + '@protobufjs/eventemitter': 1.1.0 + '@protobufjs/fetch': 1.1.0 + '@protobufjs/float': 1.0.2 + '@protobufjs/inquire': 1.1.0 + '@protobufjs/path': 1.1.2 + '@protobufjs/pool': 1.1.0 + '@protobufjs/utf8': 1.1.0 + '@types/node': 20.10.4 + long: 5.2.3 + dev: false + /proxy-addr@2.0.7: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} @@ -5337,6 +6525,13 @@ packages: resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} dev: false + /pump@3.0.0: + resolution: {integrity: sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==} + dependencies: + end-of-stream: 1.4.4 + once: 1.4.0 + dev: false + /punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -5464,6 +6659,16 @@ packages: react-is: 18.2.0 dev: false + /rc@1.2.8: + resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} + hasBin: true + dependencies: + deep-extend: 0.6.0 + ini: 1.3.8 + minimist: 1.2.8 + strip-json-comments: 2.0.1 + dev: false + /react-calendar@4.7.0(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-Sb+oRRbwBo1bzDByIEqMCXOh5JwklLPn3inibzvLKKXDokHo23rqtH2A9vz8LxNMJpzuPjGJ4OSuOJyhia3x5g==} peerDependencies: @@ -5693,7 +6898,6 @@ packages: inherits: 2.0.4 string_decoder: 1.3.0 util-deprecate: 1.0.2 - dev: true /readdirp@3.6.0: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} @@ -5764,7 +6968,6 @@ packages: /require-directory@2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} - dev: true /requires-port@1.0.0: resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} @@ -5807,6 +7010,27 @@ packages: signal-exit: 3.0.7 dev: true + /retry-request@7.0.2: + resolution: {integrity: sha512-dUOvLMJ0/JJYEn8NrpOaGNE7X3vpI5XlZS/u0ANjqtcZVKnIxP7IgCFwrKTxENw29emmwug53awKtaMm4i9g5w==} + engines: {node: '>=14'} + requiresBuild: true + dependencies: + '@types/request': 2.48.12 + extend: 3.0.2 + teeny-request: 9.0.0 + transitivePeerDependencies: + - encoding + - supports-color + dev: false + optional: true + + /retry@0.13.1: + resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==} + engines: {node: '>= 4'} + requiresBuild: true + dev: false + optional: true + /reusify@1.0.4: resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} @@ -5996,6 +7220,18 @@ packages: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} dev: true + /simple-concat@1.0.1: + resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} + dev: false + + /simple-get@4.0.1: + resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} + dependencies: + decompress-response: 6.0.0 + once: 1.4.0 + simple-concat: 1.0.1 + dev: false + /simple-swizzle@0.2.2: resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} dependencies: @@ -6066,6 +7302,20 @@ packages: - utf-8-validate dev: false + /stream-events@1.0.5: + resolution: {integrity: sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==} + requiresBuild: true + dependencies: + stubs: 3.0.0 + dev: false + optional: true + + /stream-shift@1.0.3: + resolution: {integrity: sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==} + requiresBuild: true + dev: false + optional: true + /streamsearch@1.1.0: resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} engines: {node: '>=10.0.0'} @@ -6082,7 +7332,6 @@ packages: emoji-regex: 8.0.0 is-fullwidth-code-point: 3.0.0 strip-ansi: 6.0.1 - dev: true /string.prototype.matchall@4.0.10: resolution: {integrity: sha512-rGXbGmOEosIQi6Qva94HUjgPs9vKW+dkG7Y8Q5O2OYkWL6wFaTRZO8zM4mhP94uX55wgyrXzfS2aGtGzUL7EJQ==} @@ -6127,25 +7376,40 @@ packages: resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} dependencies: safe-buffer: 5.2.1 - dev: true /strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} dependencies: ansi-regex: 5.0.1 - dev: true /strip-bom@3.0.0: resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} engines: {node: '>=4'} dev: true + /strip-json-comments@2.0.1: + resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} + engines: {node: '>=0.10.0'} + dev: false + /strip-json-comments@3.1.1: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} dev: true + /strnum@1.0.5: + resolution: {integrity: sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==} + requiresBuild: true + dev: false + optional: true + + /stubs@3.0.0: + resolution: {integrity: sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==} + requiresBuild: true + dev: false + optional: true + /styled-jsx@5.1.1(react@18.2.0): resolution: {integrity: sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==} engines: {node: '>= 12.0.0'} @@ -6246,6 +7510,42 @@ packages: engines: {node: '>=6'} dev: true + /tar-fs@2.1.1: + resolution: {integrity: sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==} + dependencies: + chownr: 1.1.4 + mkdirp-classic: 0.5.3 + pump: 3.0.0 + tar-stream: 2.2.0 + dev: false + + /tar-stream@2.2.0: + resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} + engines: {node: '>=6'} + dependencies: + bl: 4.1.0 + end-of-stream: 1.4.4 + fs-constants: 1.0.0 + inherits: 2.0.4 + readable-stream: 3.6.2 + dev: false + + /teeny-request@9.0.0: + resolution: {integrity: sha512-resvxdc6Mgb7YEThw6G6bExlXKkv6+YbuzGg9xuXxSgxJF7Ozs+o8Y9+2R3sArdWdW8nOokoQb1yrpFB0pQK2g==} + engines: {node: '>=14'} + requiresBuild: true + dependencies: + http-proxy-agent: 5.0.0 + https-proxy-agent: 5.0.1 + node-fetch: 2.7.0 + stream-events: 1.0.5 + uuid: 9.0.1 + transitivePeerDependencies: + - encoding + - supports-color + dev: false + optional: true + /text-table@0.2.0: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} dev: true @@ -6332,6 +7632,12 @@ packages: resolution: {integrity: sha512-q5sE9NQ5NR9lYpilYjcI7Sdv0KCogo+W8fZY+AYTj/HYg+9fscYy3UuJ6UQiV1bF+ARCLwFRWC8UcOt9kuUctQ==} dev: false + /tunnel-agent@0.6.0: + resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} + dependencies: + safe-buffer: 5.2.1 + dev: false + /type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} @@ -6422,7 +7728,13 @@ packages: /undici-types@5.26.5: resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} - dev: true + + /undici@5.28.4: + resolution: {integrity: sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==} + engines: {node: '>=14.0'} + dependencies: + '@fastify/busboy': 2.1.1 + dev: false /unpipe@1.0.0: resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} @@ -6497,6 +7809,11 @@ packages: hasBin: true dev: false + /uuid@9.0.1: + resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} + hasBin: true + dev: false + /validator@13.11.0: resolution: {integrity: sha512-Ii+sehpSfZy+At5nPdnyMhx78fEoPDkR2XW/zimHEL3MyGJQOCQ7WeP20jPYRz7ZCpcKLB21NxuXHF3bxjStBQ==} engines: {node: '>= 0.10'} @@ -6626,7 +7943,6 @@ packages: ansi-styles: 4.3.0 string-width: 4.2.3 strip-ansi: 6.0.1 - dev: true /wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} @@ -6666,7 +7982,6 @@ packages: /y18n@5.0.8: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} - dev: true /yallist@4.0.0: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} @@ -6683,7 +7998,6 @@ packages: /yargs-parser@21.1.1: resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} engines: {node: '>=12'} - dev: true /yargs@17.7.2: resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} @@ -6696,9 +8010,7 @@ packages: string-width: 4.2.3 y18n: 5.0.8 yargs-parser: 21.1.1 - dev: true /yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} - dev: true diff --git a/public/firebase-messaging-sw.js b/public/firebase-messaging-sw.js new file mode 100644 index 00000000..f43e52cd --- /dev/null +++ b/public/firebase-messaging-sw.js @@ -0,0 +1,81 @@ +/** + * @typedef {Object} AppNotification + * @property {string} title - 알림 제목 + * @property {string} body - 알림 내용 + * @property {string} [image] - 알림에 표시할 이미지 주소 + * @property {string} [click_action] - (web push) 알림 클릭시 이동할 주소 + */ + +/** + * @typedef {Object} AppNotificationData + * @property {string} [postId] - 답변 알림시 질문 id + * @property {string} [questionAuthorId] - 질문 작성자 id + */ + +/** + * @typedef {Object} NotificationData + * @property {AppNotificationData} data - 알림 데이터 + * @property {string} fcmMessageId - fcm 메시지 id + * @property {AppNotification} notification - fcm 발신 id + * @property {string} priority - 알림 우선순위 + */ + +self.addEventListener("push", (event) => { + const canNotification = !!globalThis.Notification + + if (!canNotification) return + if (Notification.permission !== "granted") return + + /** @type {NotificationData} */ + const { data, notification } = event.data.json() + + /** @type {ServiceWorkerRegistration} */ + const registration = event.target.registration + + event.waitUntil( + registration.showNotification(notification.title, { + requireInteraction: true, + body: notification.body, + badge: "/icons/outline-badge-icon.png", + icon: "/icon.png", + actions: [ + { + action: "view", + title: "글 보기", + }, + { + action: "close", + title: "알림 닫기", + }, + ], + data, + }), + ) +}) + +/** + * @typedef {'view' | 'close'} AppNotificationAction + */ + +self.addEventListener("notificationclick", function (event) { + /** @type {AppNotificationAction} */ + const action = event.action + const notification = event.notification + + const data = notification.data + + const linkUrl = `${location.protocol}//${location.host}/question/${data.postId}` + + switch (action) { + case "view": + event.waitUntil(clients.openWindow(linkUrl)) + break + case "close": + break + default: + event.waitUntil(clients.openWindow(linkUrl)) + break + } + + notification.close() +}) diff --git a/public/icons/badge-icon.png b/public/icons/badge-icon.png new file mode 100644 index 00000000..195776ba Binary files /dev/null and b/public/icons/badge-icon.png differ diff --git a/public/icons/icon-128x128.png b/public/icons/icon-128x128.png new file mode 100644 index 00000000..38f2906f Binary files /dev/null and b/public/icons/icon-128x128.png differ diff --git a/public/icons/icon-144x144.png b/public/icons/icon-144x144.png new file mode 100644 index 00000000..2a65dde3 Binary files /dev/null and b/public/icons/icon-144x144.png differ diff --git a/public/icons/icon-152x152.png b/public/icons/icon-152x152.png new file mode 100644 index 00000000..0a341fec Binary files /dev/null and b/public/icons/icon-152x152.png differ diff --git a/public/icons/icon-192x192.png b/public/icons/icon-192x192.png new file mode 100644 index 00000000..477291e4 Binary files /dev/null and b/public/icons/icon-192x192.png differ diff --git a/public/icons/icon-36x36.png b/public/icons/icon-36x36.png new file mode 100644 index 00000000..3b73ae9d Binary files /dev/null and b/public/icons/icon-36x36.png differ diff --git a/public/icons/icon-384x384.png b/public/icons/icon-384x384.png new file mode 100644 index 00000000..3522c79e Binary files /dev/null and b/public/icons/icon-384x384.png differ diff --git a/public/icons/icon-48x48.png b/public/icons/icon-48x48.png new file mode 100644 index 00000000..1ea7189b Binary files /dev/null and b/public/icons/icon-48x48.png differ diff --git a/public/icons/icon-512x512.png b/public/icons/icon-512x512.png new file mode 100644 index 00000000..e0989892 Binary files /dev/null and b/public/icons/icon-512x512.png differ diff --git a/public/icons/icon-72x72.png b/public/icons/icon-72x72.png new file mode 100644 index 00000000..5001bbd4 Binary files /dev/null and b/public/icons/icon-72x72.png differ diff --git a/public/icons/icon-96x96.png b/public/icons/icon-96x96.png new file mode 100644 index 00000000..9d716e71 Binary files /dev/null and b/public/icons/icon-96x96.png differ diff --git a/public/icons/outline-badge-icon.png b/public/icons/outline-badge-icon.png new file mode 100644 index 00000000..bb23c6b4 Binary files /dev/null and b/public/icons/outline-badge-icon.png differ diff --git a/public/mockServiceWorker.js b/public/mockServiceWorker.js index 2f1d1d56..bd58ef42 100644 --- a/public/mockServiceWorker.js +++ b/public/mockServiceWorker.js @@ -8,19 +8,19 @@ * - Please do NOT serve this file on production. */ -const INTEGRITY_CHECKSUM = 'c5f7f8e188b673ea4e677df7ea3c5a39' -const IS_MOCKED_RESPONSE = Symbol('isMockedResponse') +const INTEGRITY_CHECKSUM = "c5f7f8e188b673ea4e677df7ea3c5a39" +const IS_MOCKED_RESPONSE = Symbol("isMockedResponse") const activeClientIds = new Set() -self.addEventListener('install', function () { +self.addEventListener("install", function () { self.skipWaiting() }) -self.addEventListener('activate', function (event) { +self.addEventListener("activate", function (event) { event.waitUntil(self.clients.claim()) }) -self.addEventListener('message', async function (event) { +self.addEventListener("message", async function (event) { const clientId = event.source.id if (!clientId || !self.clients) { @@ -34,41 +34,41 @@ self.addEventListener('message', async function (event) { } const allClients = await self.clients.matchAll({ - type: 'window', + type: "window", }) switch (event.data) { - case 'KEEPALIVE_REQUEST': { + case "KEEPALIVE_REQUEST": { sendToClient(client, { - type: 'KEEPALIVE_RESPONSE', + type: "KEEPALIVE_RESPONSE", }) break } - case 'INTEGRITY_CHECK_REQUEST': { + case "INTEGRITY_CHECK_REQUEST": { sendToClient(client, { - type: 'INTEGRITY_CHECK_RESPONSE', + type: "INTEGRITY_CHECK_RESPONSE", payload: INTEGRITY_CHECKSUM, }) break } - case 'MOCK_ACTIVATE': { + case "MOCK_ACTIVATE": { activeClientIds.add(clientId) sendToClient(client, { - type: 'MOCKING_ENABLED', + type: "MOCKING_ENABLED", payload: true, }) break } - case 'MOCK_DEACTIVATE': { + case "MOCK_DEACTIVATE": { activeClientIds.delete(clientId) break } - case 'CLIENT_CLOSED': { + case "CLIENT_CLOSED": { activeClientIds.delete(clientId) const remainingClients = allClients.filter((client) => { @@ -85,17 +85,17 @@ self.addEventListener('message', async function (event) { } }) -self.addEventListener('fetch', function (event) { +self.addEventListener("fetch", function (event) { const { request } = event // Bypass navigation requests. - if (request.mode === 'navigate') { + if (request.mode === "navigate") { return } // Opening the DevTools triggers the "only-if-cached" request // that cannot be handled by the worker. Bypass such requests. - if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') { + if (request.cache === "only-if-cached" && request.mode !== "same-origin") { return } @@ -125,7 +125,7 @@ async function handleRequest(event, requestId) { sendToClient( client, { - type: 'RESPONSE', + type: "RESPONSE", payload: { requestId, isMockedResponse: IS_MOCKED_RESPONSE in response, @@ -151,18 +151,18 @@ async function handleRequest(event, requestId) { async function resolveMainClient(event) { const client = await self.clients.get(event.clientId) - if (client?.frameType === 'top-level') { + if (client?.frameType === "top-level") { return client } const allClients = await self.clients.matchAll({ - type: 'window', + type: "window", }) return allClients .filter((client) => { // Get only those clients that are currently visible. - return client.visibilityState === 'visible' + return client.visibilityState === "visible" }) .find((client) => { // Find the client ID that's recorded in the @@ -184,7 +184,7 @@ async function getResponse(event, client, requestId) { // Remove internal MSW request header so the passthrough request // complies with any potential CORS preflight checks on the server. // Some servers forbid unknown request headers. - delete headers['x-msw-intention'] + delete headers["x-msw-intention"] return fetch(requestClone, { headers }) } @@ -204,8 +204,8 @@ async function getResponse(event, client, requestId) { // Bypass requests with the explicit bypass header. // Such requests can be issued by "ctx.fetch()". - const mswIntention = request.headers.get('x-msw-intention') - if (['bypass', 'passthrough'].includes(mswIntention)) { + const mswIntention = request.headers.get("x-msw-intention") + if (["bypass", "passthrough"].includes(mswIntention)) { return passthrough() } @@ -214,7 +214,7 @@ async function getResponse(event, client, requestId) { const clientMessage = await sendToClient( client, { - type: 'REQUEST', + type: "REQUEST", payload: { id: requestId, url: request.url, @@ -236,11 +236,11 @@ async function getResponse(event, client, requestId) { ) switch (clientMessage.type) { - case 'MOCK_RESPONSE': { + case "MOCK_RESPONSE": { return respondWithMock(clientMessage.data) } - case 'MOCK_NOT_FOUND': { + case "MOCK_NOT_FOUND": { return passthrough() } } diff --git a/public/splash_screens/10.2__iPad_landscape.png b/public/splash_screens/10.2__iPad_landscape.png new file mode 100644 index 00000000..968d0bbf Binary files /dev/null and b/public/splash_screens/10.2__iPad_landscape.png differ diff --git a/public/splash_screens/10.2__iPad_portrait.png b/public/splash_screens/10.2__iPad_portrait.png new file mode 100644 index 00000000..e4c39bca Binary files /dev/null and b/public/splash_screens/10.2__iPad_portrait.png differ diff --git a/public/splash_screens/10.5__iPad_Air_landscape.png b/public/splash_screens/10.5__iPad_Air_landscape.png new file mode 100644 index 00000000..d87722a7 Binary files /dev/null and b/public/splash_screens/10.5__iPad_Air_landscape.png differ diff --git a/public/splash_screens/10.5__iPad_Air_portrait.png b/public/splash_screens/10.5__iPad_Air_portrait.png new file mode 100644 index 00000000..f0d4e977 Binary files /dev/null and b/public/splash_screens/10.5__iPad_Air_portrait.png differ diff --git a/public/splash_screens/10.9__iPad_Air_landscape.png b/public/splash_screens/10.9__iPad_Air_landscape.png new file mode 100644 index 00000000..4422fe04 Binary files /dev/null and b/public/splash_screens/10.9__iPad_Air_landscape.png differ diff --git a/public/splash_screens/10.9__iPad_Air_portrait.png b/public/splash_screens/10.9__iPad_Air_portrait.png new file mode 100644 index 00000000..4933a150 Binary files /dev/null and b/public/splash_screens/10.9__iPad_Air_portrait.png differ diff --git a/public/splash_screens/11__iPad_Pro_M4_landscape.png b/public/splash_screens/11__iPad_Pro_M4_landscape.png new file mode 100644 index 00000000..8cb376cb Binary files /dev/null and b/public/splash_screens/11__iPad_Pro_M4_landscape.png differ diff --git a/public/splash_screens/11__iPad_Pro_M4_portrait.png b/public/splash_screens/11__iPad_Pro_M4_portrait.png new file mode 100644 index 00000000..2d2094a4 Binary files /dev/null and b/public/splash_screens/11__iPad_Pro_M4_portrait.png differ diff --git a/public/splash_screens/11__iPad_Pro__10.5__iPad_Pro_landscape.png b/public/splash_screens/11__iPad_Pro__10.5__iPad_Pro_landscape.png new file mode 100644 index 00000000..b42e952e Binary files /dev/null and b/public/splash_screens/11__iPad_Pro__10.5__iPad_Pro_landscape.png differ diff --git a/public/splash_screens/11__iPad_Pro__10.5__iPad_Pro_portrait.png b/public/splash_screens/11__iPad_Pro__10.5__iPad_Pro_portrait.png new file mode 100644 index 00000000..d478d21b Binary files /dev/null and b/public/splash_screens/11__iPad_Pro__10.5__iPad_Pro_portrait.png differ diff --git a/public/splash_screens/12.9__iPad_Pro_landscape.png b/public/splash_screens/12.9__iPad_Pro_landscape.png new file mode 100644 index 00000000..25ad1419 Binary files /dev/null and b/public/splash_screens/12.9__iPad_Pro_landscape.png differ diff --git a/public/splash_screens/12.9__iPad_Pro_portrait.png b/public/splash_screens/12.9__iPad_Pro_portrait.png new file mode 100644 index 00000000..95e740d1 Binary files /dev/null and b/public/splash_screens/12.9__iPad_Pro_portrait.png differ diff --git a/public/splash_screens/13__iPad_Pro_M4_landscape.png b/public/splash_screens/13__iPad_Pro_M4_landscape.png new file mode 100644 index 00000000..e1bf41d6 Binary files /dev/null and b/public/splash_screens/13__iPad_Pro_M4_landscape.png differ diff --git a/public/splash_screens/13__iPad_Pro_M4_portrait.png b/public/splash_screens/13__iPad_Pro_M4_portrait.png new file mode 100644 index 00000000..9ea60908 Binary files /dev/null and b/public/splash_screens/13__iPad_Pro_M4_portrait.png differ diff --git a/public/splash_screens/4__iPhone_SE__iPod_touch_5th_generation_and_later_landscape.png b/public/splash_screens/4__iPhone_SE__iPod_touch_5th_generation_and_later_landscape.png new file mode 100644 index 00000000..d9dfe5b9 Binary files /dev/null and b/public/splash_screens/4__iPhone_SE__iPod_touch_5th_generation_and_later_landscape.png differ diff --git a/public/splash_screens/4__iPhone_SE__iPod_touch_5th_generation_and_later_portrait.png b/public/splash_screens/4__iPhone_SE__iPod_touch_5th_generation_and_later_portrait.png new file mode 100644 index 00000000..260d475a Binary files /dev/null and b/public/splash_screens/4__iPhone_SE__iPod_touch_5th_generation_and_later_portrait.png differ diff --git a/public/splash_screens/8.3__iPad_Mini_landscape.png b/public/splash_screens/8.3__iPad_Mini_landscape.png new file mode 100644 index 00000000..ff8026e3 Binary files /dev/null and b/public/splash_screens/8.3__iPad_Mini_landscape.png differ diff --git a/public/splash_screens/8.3__iPad_Mini_portrait.png b/public/splash_screens/8.3__iPad_Mini_portrait.png new file mode 100644 index 00000000..c3214cde Binary files /dev/null and b/public/splash_screens/8.3__iPad_Mini_portrait.png differ diff --git a/public/splash_screens/9.7__iPad_Pro__7.9__iPad_mini__9.7__iPad_Air__9.7__iPad_landscape.png b/public/splash_screens/9.7__iPad_Pro__7.9__iPad_mini__9.7__iPad_Air__9.7__iPad_landscape.png new file mode 100644 index 00000000..01f79ddf Binary files /dev/null and b/public/splash_screens/9.7__iPad_Pro__7.9__iPad_mini__9.7__iPad_Air__9.7__iPad_landscape.png differ diff --git a/public/splash_screens/9.7__iPad_Pro__7.9__iPad_mini__9.7__iPad_Air__9.7__iPad_portrait.png b/public/splash_screens/9.7__iPad_Pro__7.9__iPad_mini__9.7__iPad_Air__9.7__iPad_portrait.png new file mode 100644 index 00000000..d5b9f31c Binary files /dev/null and b/public/splash_screens/9.7__iPad_Pro__7.9__iPad_mini__9.7__iPad_Air__9.7__iPad_portrait.png differ diff --git a/public/splash_screens/iPhone_11_Pro_Max__iPhone_XS_Max_landscape.png b/public/splash_screens/iPhone_11_Pro_Max__iPhone_XS_Max_landscape.png new file mode 100644 index 00000000..0a78afdc Binary files /dev/null and b/public/splash_screens/iPhone_11_Pro_Max__iPhone_XS_Max_landscape.png differ diff --git a/public/splash_screens/iPhone_11_Pro_Max__iPhone_XS_Max_portrait.png b/public/splash_screens/iPhone_11_Pro_Max__iPhone_XS_Max_portrait.png new file mode 100644 index 00000000..4564fdc8 Binary files /dev/null and b/public/splash_screens/iPhone_11_Pro_Max__iPhone_XS_Max_portrait.png differ diff --git a/public/splash_screens/iPhone_11__iPhone_XR_landscape.png b/public/splash_screens/iPhone_11__iPhone_XR_landscape.png new file mode 100644 index 00000000..bf87d83f Binary files /dev/null and b/public/splash_screens/iPhone_11__iPhone_XR_landscape.png differ diff --git a/public/splash_screens/iPhone_11__iPhone_XR_portrait.png b/public/splash_screens/iPhone_11__iPhone_XR_portrait.png new file mode 100644 index 00000000..182704ab Binary files /dev/null and b/public/splash_screens/iPhone_11__iPhone_XR_portrait.png differ diff --git a/public/splash_screens/iPhone_13_mini__iPhone_12_mini__iPhone_11_Pro__iPhone_XS__iPhone_X_landscape.png b/public/splash_screens/iPhone_13_mini__iPhone_12_mini__iPhone_11_Pro__iPhone_XS__iPhone_X_landscape.png new file mode 100644 index 00000000..b81eca9c Binary files /dev/null and b/public/splash_screens/iPhone_13_mini__iPhone_12_mini__iPhone_11_Pro__iPhone_XS__iPhone_X_landscape.png differ diff --git a/public/splash_screens/iPhone_13_mini__iPhone_12_mini__iPhone_11_Pro__iPhone_XS__iPhone_X_portrait.png b/public/splash_screens/iPhone_13_mini__iPhone_12_mini__iPhone_11_Pro__iPhone_XS__iPhone_X_portrait.png new file mode 100644 index 00000000..056d5b5f Binary files /dev/null and b/public/splash_screens/iPhone_13_mini__iPhone_12_mini__iPhone_11_Pro__iPhone_XS__iPhone_X_portrait.png differ diff --git a/public/splash_screens/iPhone_14_Plus__iPhone_13_Pro_Max__iPhone_12_Pro_Max_landscape.png b/public/splash_screens/iPhone_14_Plus__iPhone_13_Pro_Max__iPhone_12_Pro_Max_landscape.png new file mode 100644 index 00000000..2f27556c Binary files /dev/null and b/public/splash_screens/iPhone_14_Plus__iPhone_13_Pro_Max__iPhone_12_Pro_Max_landscape.png differ diff --git a/public/splash_screens/iPhone_14_Plus__iPhone_13_Pro_Max__iPhone_12_Pro_Max_portrait.png b/public/splash_screens/iPhone_14_Plus__iPhone_13_Pro_Max__iPhone_12_Pro_Max_portrait.png new file mode 100644 index 00000000..ed29abec Binary files /dev/null and b/public/splash_screens/iPhone_14_Plus__iPhone_13_Pro_Max__iPhone_12_Pro_Max_portrait.png differ diff --git a/public/splash_screens/iPhone_14__iPhone_13_Pro__iPhone_13__iPhone_12_Pro__iPhone_12_landscape.png b/public/splash_screens/iPhone_14__iPhone_13_Pro__iPhone_13__iPhone_12_Pro__iPhone_12_landscape.png new file mode 100644 index 00000000..eefa5cf5 Binary files /dev/null and b/public/splash_screens/iPhone_14__iPhone_13_Pro__iPhone_13__iPhone_12_Pro__iPhone_12_landscape.png differ diff --git a/public/splash_screens/iPhone_14__iPhone_13_Pro__iPhone_13__iPhone_12_Pro__iPhone_12_portrait.png b/public/splash_screens/iPhone_14__iPhone_13_Pro__iPhone_13__iPhone_12_Pro__iPhone_12_portrait.png new file mode 100644 index 00000000..afdf4ab1 Binary files /dev/null and b/public/splash_screens/iPhone_14__iPhone_13_Pro__iPhone_13__iPhone_12_Pro__iPhone_12_portrait.png differ diff --git a/public/splash_screens/iPhone_15_Pro_Max__iPhone_15_Plus__iPhone_14_Pro_Max_landscape.png b/public/splash_screens/iPhone_15_Pro_Max__iPhone_15_Plus__iPhone_14_Pro_Max_landscape.png new file mode 100644 index 00000000..db0e32b7 Binary files /dev/null and b/public/splash_screens/iPhone_15_Pro_Max__iPhone_15_Plus__iPhone_14_Pro_Max_landscape.png differ diff --git a/public/splash_screens/iPhone_15_Pro_Max__iPhone_15_Plus__iPhone_14_Pro_Max_portrait.png b/public/splash_screens/iPhone_15_Pro_Max__iPhone_15_Plus__iPhone_14_Pro_Max_portrait.png new file mode 100644 index 00000000..e594d94c Binary files /dev/null and b/public/splash_screens/iPhone_15_Pro_Max__iPhone_15_Plus__iPhone_14_Pro_Max_portrait.png differ diff --git a/public/splash_screens/iPhone_15_Pro__iPhone_15__iPhone_14_Pro_landscape.png b/public/splash_screens/iPhone_15_Pro__iPhone_15__iPhone_14_Pro_landscape.png new file mode 100644 index 00000000..254cc1d6 Binary files /dev/null and b/public/splash_screens/iPhone_15_Pro__iPhone_15__iPhone_14_Pro_landscape.png differ diff --git a/public/splash_screens/iPhone_15_Pro__iPhone_15__iPhone_14_Pro_portrait.png b/public/splash_screens/iPhone_15_Pro__iPhone_15__iPhone_14_Pro_portrait.png new file mode 100644 index 00000000..6aeb4d1e Binary files /dev/null and b/public/splash_screens/iPhone_15_Pro__iPhone_15__iPhone_14_Pro_portrait.png differ diff --git a/public/splash_screens/iPhone_8_Plus__iPhone_7_Plus__iPhone_6s_Plus__iPhone_6_Plus_landscape.png b/public/splash_screens/iPhone_8_Plus__iPhone_7_Plus__iPhone_6s_Plus__iPhone_6_Plus_landscape.png new file mode 100644 index 00000000..ff95a84d Binary files /dev/null and b/public/splash_screens/iPhone_8_Plus__iPhone_7_Plus__iPhone_6s_Plus__iPhone_6_Plus_landscape.png differ diff --git a/public/splash_screens/iPhone_8_Plus__iPhone_7_Plus__iPhone_6s_Plus__iPhone_6_Plus_portrait.png b/public/splash_screens/iPhone_8_Plus__iPhone_7_Plus__iPhone_6s_Plus__iPhone_6_Plus_portrait.png new file mode 100644 index 00000000..7b7fbe66 Binary files /dev/null and b/public/splash_screens/iPhone_8_Plus__iPhone_7_Plus__iPhone_6s_Plus__iPhone_6_Plus_portrait.png differ diff --git a/public/splash_screens/iPhone_8__iPhone_7__iPhone_6s__iPhone_6__4.7__iPhone_SE_landscape.png b/public/splash_screens/iPhone_8__iPhone_7__iPhone_6s__iPhone_6__4.7__iPhone_SE_landscape.png new file mode 100644 index 00000000..b57f86fd Binary files /dev/null and b/public/splash_screens/iPhone_8__iPhone_7__iPhone_6s__iPhone_6__4.7__iPhone_SE_landscape.png differ diff --git a/public/splash_screens/iPhone_8__iPhone_7__iPhone_6s__iPhone_6__4.7__iPhone_SE_portrait.png b/public/splash_screens/iPhone_8__iPhone_7__iPhone_6s__iPhone_6__4.7__iPhone_SE_portrait.png new file mode 100644 index 00000000..3f27d476 Binary files /dev/null and b/public/splash_screens/iPhone_8__iPhone_7__iPhone_6s__iPhone_6__4.7__iPhone_SE_portrait.png differ diff --git a/public/splash_screens/icon.png b/public/splash_screens/icon.png new file mode 100644 index 00000000..1b596ef5 Binary files /dev/null and b/public/splash_screens/icon.png differ diff --git a/src/app/api/fcm-token/[token]/route.ts b/src/app/api/fcm-token/[token]/route.ts new file mode 100644 index 00000000..44c35867 --- /dev/null +++ b/src/app/api/fcm-token/[token]/route.ts @@ -0,0 +1,73 @@ +import { FIREBASE_COLLECTIONS, store } from "@/firebase/firebase-app" +import { DeleteFcmTokenResponse } from "@/interfaces/dto/fcm/delete-fcm-token.dto" +import { FireStoreTokenCollection } from "@/interfaces/fcm" +import { HttpStatusCode } from "axios" +import { + collection, + deleteDoc, + getDocs, + query, + updateDoc, + where, +} from "firebase/firestore" +import { NextRequest, NextResponse } from "next/server" + +interface FcmTokenParams { + params: { + token: string + } +} + +export async function DELETE(request: NextRequest, { params }: FcmTokenParams) { + const auth = request.headers.get("Authorization") + if (!auth || auth !== process.env.NEXT_PUBLIC_FIREBASE_ACCESS_KEY) { + return NextResponse.json( + { + code: HttpStatusCode.Unauthorized, + msg: "인증되지 않았습니다", + }, + { status: HttpStatusCode.Unauthorized }, + ) + } + + const token = params.token + + const q = query( + collection(store, FIREBASE_COLLECTIONS.TOKEN), + where("tokenList", "array-contains", token), + ) + const snapshot = await getDocs(q) + + if (!snapshot.empty) { + const target = snapshot.docs[0] + + const tokenList = target.get( + "tokenList", + ) as FireStoreTokenCollection["tokenList"] + const tokenListPayload = tokenList.filter((_token) => _token !== token) + + if (!tokenListPayload.length) { + await deleteDoc(target.ref) + + return NextResponse.json( + { + code: HttpStatusCode.Ok, + msg: "FCM 토큰 삭제", + }, + { status: HttpStatusCode.Ok }, + ) + } + + await updateDoc(target.ref, { + tokenList: tokenListPayload, + }) + } + + return NextResponse.json( + { + code: HttpStatusCode.Ok, + msg: "FCM 토큰 삭제", + }, + { status: HttpStatusCode.Ok }, + ) +} diff --git a/src/app/api/fcm-token/route.ts b/src/app/api/fcm-token/route.ts new file mode 100644 index 00000000..56212d85 --- /dev/null +++ b/src/app/api/fcm-token/route.ts @@ -0,0 +1,73 @@ +import { FIREBASE_COLLECTIONS, store } from "@/firebase/firebase-app" +import { + RegisterFcmTokenRequest, + RegisterFcmTokenResponse, +} from "@/interfaces/dto/fcm/register-fcm-token.dto" +import { FireStoreTokenCollection } from "@/interfaces/fcm" +import { HttpStatusCode } from "axios" +import { collection, doc, setDoc, updateDoc, getDoc } from "firebase/firestore" +import { NextRequest, NextResponse } from "next/server" + +export async function POST(request: NextRequest) { + const auth = request.headers.get("Authorization") + if (!auth || auth !== process.env.NEXT_PUBLIC_FIREBASE_ACCESS_KEY) { + return NextResponse.json( + { + code: HttpStatusCode.Unauthorized, + msg: "인증되지 않았습니다", + }, + { status: HttpStatusCode.Unauthorized }, + ) + } + + const { user, token } = (await request.json()) as RegisterFcmTokenRequest + + const docRef = doc( + collection(store, FIREBASE_COLLECTIONS.TOKEN), + `${user.id}`, + ) + + const tokenDoc = await getDoc(docRef) + const tokenData = tokenDoc.data() + + if (!tokenData) { + const newToken: FireStoreTokenCollection = { + user: { + id: user.id, + }, + tokenList: [token], + } + + await setDoc(docRef, newToken) + + return NextResponse.json( + { + code: 201, + msg: "FCM 토큰 등록 성공", + }, + { + status: 201, + }, + ) + } + + const tokenList = tokenDoc.get( + "tokenList", + ) as FireStoreTokenCollection["tokenList"] + + if (!tokenList.find((_token) => _token === token)) { + await updateDoc(docRef, { + tokenList: [...tokenList, token], + }) + } + + return NextResponse.json( + { + code: 200, + msg: "FCM 토큰 등록 성공", + }, + { + status: 200, + }, + ) +} diff --git a/src/app/api/fcm-token/user/[id]/route.ts b/src/app/api/fcm-token/user/[id]/route.ts new file mode 100644 index 00000000..45529a3d --- /dev/null +++ b/src/app/api/fcm-token/user/[id]/route.ts @@ -0,0 +1,58 @@ +import { FIREBASE_COLLECTIONS, store } from "@/firebase/firebase-app" +import { GetUserFcmTokenResponse } from "@/interfaces/dto/fcm/get-user-fcm-token.dto" +import { FireStoreTokenCollection } from "@/interfaces/fcm" +import { HttpStatusCode } from "axios" +import { collection, doc, getDoc } from "firebase/firestore" +import { NextRequest, NextResponse } from "next/server" + +interface UserFcmTokenParams { + params: { + id: string + } +} + +export async function GET( + request: NextRequest, + { params }: UserFcmTokenParams, +) { + const auth = request.headers.get("Authorization") + if (!auth || auth !== process.env.NEXT_PUBLIC_FIREBASE_ACCESS_KEY) { + return NextResponse.json( + { + code: HttpStatusCode.Unauthorized, + msg: "인증되지 않았습니다", + }, + { status: HttpStatusCode.Unauthorized }, + ) + } + + const userId = params.id + + const docRef = doc(collection(store, FIREBASE_COLLECTIONS.TOKEN), userId) + + const tokenDoc = await getDoc(docRef) + const tokenData = tokenDoc.data() + + if (!tokenData) { + return NextResponse.json( + { + code: HttpStatusCode.NotFound, + msg: "토큰 없음", + data: { + tokenList: null, + }, + }, + { + status: HttpStatusCode.Ok, + }, + ) + } + + return NextResponse.json({ + code: HttpStatusCode.Ok, + msg: "토큰 조회 성공", + data: { + tokenList: tokenData.tokenList as FireStoreTokenCollection["tokenList"], + }, + }) +} diff --git a/src/app/api/send-fcm/route.ts b/src/app/api/send-fcm/route.ts new file mode 100644 index 00000000..9fdef53a --- /dev/null +++ b/src/app/api/send-fcm/route.ts @@ -0,0 +1,112 @@ +import { FIREBASE_COLLECTIONS, store } from "@/firebase/firebase-app" +import { + NotificationType, + SendFcmRequest, + SendFcmResponse, +} from "@/interfaces/dto/fcm/send-fcm.dto" +import { FireStoreTokenCollection } from "@/interfaces/fcm" +import { deleteFcmToken } from "@/service/fcm" +import { HttpStatusCode } from "axios" +import admin from "firebase-admin" +import { + FirebaseMessagingError, + MessagingClientErrorCode, +} from "firebase-admin/messaging" +import { collection, getDocs, query, where } from "firebase/firestore" +import { NextRequest, NextResponse } from "next/server" + +export async function POST(request: NextRequest) { + const auth = request.headers.get("Authorization") + if (!auth || auth !== process.env.NEXT_PUBLIC_FIREBASE_ACCESS_KEY) { + return NextResponse.json( + { + code: HttpStatusCode.Unauthorized, + msg: "인증되지 않았습니다", + }, + { status: HttpStatusCode.Unauthorized }, + ) + } + + const firebaseAdmin = admin.apps.length + ? admin.app() + : admin.initializeApp({ + credential: admin.credential.cert({ + projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID, + privateKey: process.env.FIREBASE_PRIVATE_KEY, + clientEmail: process.env.FIREBASE_CLIENT_EMAIL, + }), + }) + + const payload = await request.json() + + const type = payload.type as NotificationType + const notification = payload.notification + + if (type === "answer" && isAppNotificationPayload("answer", notification)) { + const { data, ...notificationPayload } = notification + + const { postId, questionAuthorId } = data + + const q = query( + collection(store, FIREBASE_COLLECTIONS.TOKEN), + where("user.id", "==", Number(questionAuthorId)), + ) + const snapshot = await getDocs(q) + + if (!snapshot.empty) { + const target = snapshot.docs[0] + const tokenList = target.get( + "tokenList", + ) as FireStoreTokenCollection["tokenList"] + + const invalidTokenList: FireStoreTokenCollection["tokenList"] = [] + + await Promise.allSettled( + tokenList.map((token) => + firebaseAdmin + .messaging() + .send({ + token, + notification: { + ...notificationPayload, + }, + data: { + postId: `${postId}`, + }, + }) + .catch((error) => { + if (error instanceof FirebaseMessagingError) { + if ( + error.code === + "messaging/registration-token-not-registered" || + error.code === MessagingClientErrorCode.INVALID_ARGUMENT.code + ) { + invalidTokenList.push(token) + } + } + }), + ), + ) + + if (invalidTokenList?.length) { + await Promise.allSettled( + invalidTokenList.map((invalidToken) => + deleteFcmToken({ token: invalidToken }), + ), + ) + } + } + } + + return NextResponse.json({ + code: HttpStatusCode.Ok, + msg: "ok", + }) +} + +function isAppNotificationPayload( + type: TData, + payload: any, +): payload is SendFcmRequest["notification"] { + return true +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 16855e2f..f52453a0 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,4 +1,4 @@ -import type { Metadata } from "next" +import type { Metadata, Viewport } from "next" import dynamicComponent from "next/dynamic" import "./globals.css" import "rc-dropdown/assets/index.css" @@ -17,9 +17,15 @@ import GoogleAnalyticsProvider from "@/google-analytics/GoogleAnalyticsProvider" import HistorySession from "@/components/history/HistorySession" import CodingMeetingFormProvider from "@/page/coding-meetings/create/CodingMeetingFormProvider" import { Slide } from "react-toastify" +import NotificationPermissionSnackBar from "../components/snack-bar/NotificationPermissionSnackBar" +import { APPLE_SPLASH_SCREENS } from "@/constants/layoutMeta" export const dynamic = "force-dynamic" +export const viewport: Viewport = { + themeColor: "#97e75c", +} + export const metadata: Metadata = { metadataBase: new URL("https://kernelsquare.live"), title: { @@ -48,6 +54,9 @@ export const metadata: Metadata = { alt: "Kernel Square", }, }, + appleWebApp: { + startupImage: APPLE_SPLASH_SCREENS, + }, } const PopupEventListener = dynamicComponent( @@ -66,6 +75,7 @@ export default function RootLayout({ + diff --git a/src/app/manifest.ts b/src/app/manifest.ts new file mode 100644 index 00000000..9a37decf --- /dev/null +++ b/src/app/manifest.ts @@ -0,0 +1,66 @@ +import { MetadataRoute } from "next" + +export default function manifest(): MetadataRoute.Manifest { + return { + name: "커널스퀘어", + short_name: "커널스퀘어", + description: "지속 가능한 성장을 위한 개발자 커뮤니티 커널스퀘어", + start_url: "/", + display: "standalone", + background_color: "#fff", + theme_color: "#97e75c", + lang: "ko-KR", + icons: [ + { + src: "/icons/icon-72x72.png", + sizes: "72x72", + type: "image/png", + purpose: "maskable", + }, + { + src: "/icons/icon-96x96.png", + sizes: "96x96", + type: "image/png", + purpose: "maskable", + }, + { + src: "/icons/icon-128x128.png", + sizes: "128x128", + type: "image/png", + purpose: "maskable", + }, + { + src: "/icons/icon-144x144.png", + sizes: "144x144", + type: "image/png", + purpose: "maskable", + }, + { + src: "/icons/icon-192x192.png", + sizes: "192x192", + type: "image/png", + purpose: "maskable", + }, + { + src: "/icons/icon-512x512.png", + sizes: "512x512", + type: "image/png", + purpose: "any", + }, + ], + shortcuts: [ + { + name: "질문 작성", + url: "/question", + icons: [ + { + src: "/icons/icon-96x96.png", + sizes: "96x96", + type: "image/png", + purpose: "maskable", + }, + ], + }, + ], + } +} diff --git a/src/components/snack-bar/NotificationPermissionSnackBar.tsx b/src/components/snack-bar/NotificationPermissionSnackBar.tsx new file mode 100644 index 00000000..676305c2 --- /dev/null +++ b/src/components/snack-bar/NotificationPermissionSnackBar.tsx @@ -0,0 +1,157 @@ +"use client" + +import Button from "@/components/shared/button/Button" +import { useClientSession } from "@/hooks/useClientSession" +import { useEffect, useState } from "react" +import { Icons } from "../icons/Icons" +import { useFCM } from "@/hooks/firebase/useFCM" +import { toast } from "react-toastify" +import { PERMISSION_MESSAGE } from "@/constants/message/permission" +import dayjs from "dayjs" + +function NotificationPermissionSnackBar() { + const { user } = useClientSession() + + const { registerToken, hasFcmToken } = useFCM() + + const [render, setRender] = useState(false) + const [isGranted, setIsGranted] = useState(false) + const [isDenied, setIsDenied] = useState(false) + + /** + - 권한을 승인한 동일 브라우저(디바이스)에서 다른 유저로 로그인 시 토큰을 DB에 저장 + - 동일 디바이스(브라우저)에서 알림 권한을 승인한 경우, + 다른 유저로 로그인해도 다시 권한을 물어보지 않아 + 해당 유저에 대한 알림을 푸시 할 수 없어서 구현 + */ + const registerFcmTokenFromSameDevice = async ({ + userId, + }: { + userId: number + }) => { + hasFcmToken({ userId }).then((userHasFcmToken) => { + if (userHasFcmToken === "error" || userHasFcmToken) return + + registerToken({ user: { id: userId } }) + }) + } + + const handlePermission = async (permission: NotificationPermission) => { + setTimeout(() => { + if (permission === "default") return + + toast.info( + PERMISSION_MESSAGE[permission] + `(${dayjs().format("YYYY.M.D")})`, + { + position: "bottom-center", + }, + ) + }, 0) + + if (permission === "granted") { + setIsGranted(true) + + setTimeout(async () => { + registerToken({ + user: { + id: user!.member_id, + }, + }) + }, 0) + + return + } + + if (permission === "denied") { + setIsDenied(true) + return + } + } + + useEffect(() => { + setRender(true) + }, []) /* eslint-disable-line */ + + useEffect(() => { + if (!user) return + if (!globalThis?.Notification) return + if (Notification.permission !== "granted") return + + registerFcmTokenFromSameDevice({ userId: user.member_id }) + }, [user]) /* eslint-disable-line */ + + if (!render) return null + if (!globalThis?.Notification) return null + if (!user) return null + + if (Notification.permission === "granted" || isGranted) return null + if (Notification.permission === "denied" || isDenied) { + return null + } + if (Notification.permission === "default") { + return ( + + + + ) + } +} + +export default NotificationPermissionSnackBar + +const Wrapper = ({ + open, + children, +}: { + open: boolean + children: React.ReactNode +}) => { + const [isOpen, setIsOpen] = useState(open) + + const close = () => { + setIsOpen(false) + } + + if (!isOpen) return null + + return ( +
+
+ {children} + {/* close button */} + +
+
+ ) +} + +const RequestPermissionContent = ({ + onPermission, +}: { + onPermission: (permission: NotificationPermission) => Promise +}) => { + const requestPermission = () => { + Notification?.requestPermission(async (permission) => { + onPermission(permission) + }) + } + + return ( + <> +
+
알림 권한을 허용해주세요
+
+ 사용목적: 푸시 알림 메시지 발송 +
+
+ + + ) +} diff --git a/src/constants/layoutMeta.ts b/src/constants/layoutMeta.ts index 1abaecd8..af380773 100644 --- a/src/constants/layoutMeta.ts +++ b/src/constants/layoutMeta.ts @@ -1,3 +1,5 @@ +import { AppleImage } from "next/dist/lib/metadata/types/extra-types" + export type LayoutMetaKey = | "/" | "qna" @@ -164,3 +166,196 @@ export const layoutMeta = { }, }, } satisfies Record + +export const APPLE_SPLASH_SCREENS: AppleImage[] = [ + { + url: "/splash_screens/iPhone_15_Pro_Max__iPhone_15_Plus__iPhone_14_Pro_Max_landscape.png", + media: + "screen and (device-width: 430px) and (device-height: 932px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)", + }, + { + url: "/splash_screens/iPhone_15_Pro__iPhone_15__iPhone_14_Pro_landscape.png", + media: + "screen and (device-width: 393px) and (device-height: 852px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)", + }, + { + url: "/splash_screens/iPhone_14_Plus__iPhone_13_Pro_Max__iPhone_12_Pro_Max_landscape.png", + media: + "screen and (device-width: 428px) and (device-height: 926px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)", + }, + { + url: "/splash_screens/iPhone_14__iPhone_13_Pro__iPhone_13__iPhone_12_Pro__iPhone_12_landscape.png", + media: + "screen and (device-width: 390px) and (device-height: 844px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)", + }, + { + url: "/splash_screens/iPhone_13_mini__iPhone_12_mini__iPhone_11_Pro__iPhone_XS__iPhone_X_landscape.png", + media: + "screen and (device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)", + }, + { + url: "/splash_screens/iPhone_11_Pro_Max__iPhone_XS_Max_landscape.png", + media: + "screen and (device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)", + }, + { + url: "/splash_screens/iPhone_11__iPhone_XR_landscape.png", + media: + "screen and (device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)", + }, + { + url: "/splash_screens/iPhone_8_Plus__iPhone_7_Plus__iPhone_6s_Plus__iPhone_6_Plus_landscape.png", + media: + "screen and (device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)", + }, + { + url: "/splash_screens/iPhone_8__iPhone_7__iPhone_6s__iPhone_6__4.7__iPhone_SE_landscape.png", + media: + "screen and (device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)", + }, + { + url: "/splash_screens/4__iPhone_SE__iPod_touch_5th_generation_and_later_landscape.png", + media: + "screen and (device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)", + }, + { + url: "/splash_screens/13__iPad_Pro_M4_landscape.png", + media: + "screen and (device-width: 1032px) and (device-height: 1376px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)", + }, + { + url: "/splash_screens/12.9__iPad_Pro_landscape.png", + media: + "screen and (device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)", + }, + { + url: "/splash_screens/11__iPad_Pro_M4_landscape.png", + media: + "screen and (device-width: 834px) and (device-height: 1210px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)", + }, + { + url: "/splash_screens/11__iPad_Pro__10.5__iPad_Pro_landscape.png", + media: + "screen and (device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)", + }, + { + url: "/splash_screens/10.9__iPad_Air_landscape.png", + media: + "screen and (device-width: 820px) and (device-height: 1180px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)", + }, + { + url: "/splash_screens/10.5__iPad_Air_landscape.png", + media: + "screen and (device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)", + }, + { + url: "/splash_screens/10.2__iPad_landscape.png", + media: + "screen and (device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)", + }, + { + url: "/splash_screens/9.7__iPad_Pro__7.9__iPad_mini__9.7__iPad_Air__9.7__iPad_landscape.png", + media: + "screen and (device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)", + }, + { + url: "/splash_screens/8.3__iPad_Mini_landscape.png", + media: + "screen and (device-width: 744px) and (device-height: 1133px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)", + }, + { + url: "/splash_screens/iPhone_15_Pro_Max__iPhone_15_Plus__iPhone_14_Pro_Max_portrait.png", + media: + "screen and (device-width: 430px) and (device-height: 932px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)", + }, + { + url: "/splash_screens/iPhone_15_Pro__iPhone_15__iPhone_14_Pro_portrait.png", + media: + "screen and (device-width: 393px) and (device-height: 852px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)", + }, + { + url: "/splash_screens/iPhone_14_Plus__iPhone_13_Pro_Max__iPhone_12_Pro_Max_portrait.png", + media: + "screen and (device-width: 428px) and (device-height: 926px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)", + }, + { + url: "/splash_screens/iPhone_14__iPhone_13_Pro__iPhone_13__iPhone_12_Pro__iPhone_12_portrait.png", + media: + "screen and (device-width: 390px) and (device-height: 844px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)", + }, + { + url: "/splash_screens/iPhone_13_mini__iPhone_12_mini__iPhone_11_Pro__iPhone_XS__iPhone_X_portrait.png", + media: + "screen and (device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)", + }, + { + url: "/splash_screens/iPhone_11_Pro_Max__iPhone_XS_Max_portrait.png", + media: + "screen and (device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)", + }, + { + url: "/splash_screens/iPhone_11__iPhone_XR_portrait.png", + media: + "screen and (device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)", + }, + { + url: "/splash_screens/iPhone_8_Plus__iPhone_7_Plus__iPhone_6s_Plus__iPhone_6_Plus_portrait.png", + media: + "screen and (device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)", + }, + { + url: "/splash_screens/iPhone_8__iPhone_7__iPhone_6s__iPhone_6__4.7__iPhone_SE_portrait.png", + media: + "screen and (device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)", + }, + { + url: "/splash_screens/4__iPhone_SE__iPod_touch_5th_generation_and_later_portrait.png", + media: + "screen and (device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)", + }, + { + url: "/splash_screens/13__iPad_Pro_M4_portrait.png", + media: + "screen and (device-width: 1032px) and (device-height: 1376px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)", + }, + { + url: "/splash_screens/12.9__iPad_Pro_portrait.png", + media: + "screen and (device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)", + }, + { + url: "/splash_screens/11__iPad_Pro_M4_portrait.png", + media: + "screen and (device-width: 834px) and (device-height: 1210px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)", + }, + { + url: "/splash_screens/11__iPad_Pro__10.5__iPad_Pro_portrait.png", + media: + "screen and (device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)", + }, + { + url: "/splash_screens/10.9__iPad_Air_portrait.png", + media: + "screen and (device-width: 820px) and (device-height: 1180px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)", + }, + { + url: "/splash_screens/10.5__iPad_Air_portrait.png", + media: + "screen and (device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)", + }, + { + url: "/splash_screens/10.2__iPad_portrait.png", + media: + "screen and (device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)", + }, + { + url: "/splash_screens/9.7__iPad_Pro__7.9__iPad_mini__9.7__iPad_Air__9.7__iPad_portrait.png", + media: + "screen and (device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)", + }, + { + url: "/splash_screens/8.3__iPad_Mini_portrait.png", + media: + "screen and (device-width: 744px) and (device-height: 1133px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)", + }, +] diff --git a/src/constants/message/permission.ts b/src/constants/message/permission.ts new file mode 100644 index 00000000..cd840426 --- /dev/null +++ b/src/constants/message/permission.ts @@ -0,0 +1,4 @@ +export const PERMISSION_MESSAGE = { + granted: "푸시 알림 수신을 허용하셨습니다", + denied: "푸시 알림 수신을 거부하셨습니다", +} satisfies Record, string> diff --git a/src/firebase/firebase-app.ts b/src/firebase/firebase-app.ts new file mode 100644 index 00000000..1034dd62 --- /dev/null +++ b/src/firebase/firebase-app.ts @@ -0,0 +1,30 @@ +import { initializeApp } from "firebase/app" +import { getMessaging } from "firebase/messaging" +import { getFirestore } from "firebase/firestore" + +const firebaseConfig = { + apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY, + authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN, + projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID, + storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET, + messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER, + appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID, + measurementId: process.env.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID, +} + +// Initialize Firebase +export const app = initializeApp(firebaseConfig) +export const messaging = getFirebaseMessaging() +export const store = getFirestore(app) + +export const FIREBASE_COLLECTIONS = { + TOKEN: "TOKEN", +} + +function getFirebaseMessaging() { + if (typeof window === "undefined" || !globalThis?.navigator) { + return null + } + + return getMessaging(app) +} diff --git a/src/hooks/firebase/useFCM.tsx b/src/hooks/firebase/useFCM.tsx new file mode 100644 index 00000000..4ef5fe62 --- /dev/null +++ b/src/hooks/firebase/useFCM.tsx @@ -0,0 +1,62 @@ +"use client" + +import { messaging } from "@/firebase/firebase-app" +import { GetUserFcmTokenRequest } from "@/interfaces/dto/fcm/get-user-fcm-token.dto" +import { RegisterFcmTokenRequest } from "@/interfaces/dto/fcm/register-fcm-token.dto" +import { + NotificationType, + SendFcmRequest, +} from "@/interfaces/dto/fcm/send-fcm.dto" +import { + hasFcmTokenAction, + registerFcmTokenAction, + sendFcmAction, +} from "@/util/actions/fcm" +import { getToken } from "firebase/messaging" + +export function useFCM() { + const registerToken = async ({ + user, + }: Pick) => { + if (messaging) { + const token = await getToken(messaging, { + vapidKey: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_VAPID_KEY, + }).catch((error) => { + return null + }) + + if (!token) return + + await registerFcmTokenAction({ + userId: user.id, + token, + }) + } + } + + const hasFcmToken = async ({ userId }: GetUserFcmTokenRequest) => { + if (messaging) { + const token = await getToken(messaging, { + vapidKey: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_VAPID_KEY, + }).catch((error) => { + return null + }) + + const res = await hasFcmTokenAction({ userId, token }) + return res.success === false ? "error" : res.hasFcmToken! + } + + return "error" + } + + const send = async ( + type: TData, + payload: SendFcmRequest["notification"], + ) => { + if (messaging) { + await sendFcmAction(type, payload) + } + } + + return { registerToken, hasFcmToken, send } +} diff --git a/src/interfaces/dto/fcm/delete-fcm-token.dto.ts b/src/interfaces/dto/fcm/delete-fcm-token.dto.ts new file mode 100644 index 00000000..b2ff7b28 --- /dev/null +++ b/src/interfaces/dto/fcm/delete-fcm-token.dto.ts @@ -0,0 +1,7 @@ +import { APIResponse } from "../api-response" + +export interface DeleteFcmTokenRequest { + token: string +} + +export interface DeleteFcmTokenResponse extends APIResponse {} diff --git a/src/interfaces/dto/fcm/get-user-fcm-token.dto.ts b/src/interfaces/dto/fcm/get-user-fcm-token.dto.ts new file mode 100644 index 00000000..e50095de --- /dev/null +++ b/src/interfaces/dto/fcm/get-user-fcm-token.dto.ts @@ -0,0 +1,13 @@ +import { FireStoreTokenCollection } from "@/interfaces/fcm" +import { APIResponse } from "../api-response" + +export interface GetUserFcmTokenRequest { + userId: number +} + +export interface GetUserFcmTokenPayload { + tokenList: FireStoreTokenCollection["tokenList"] | null +} + +export interface GetUserFcmTokenResponse + extends APIResponse {} diff --git a/src/interfaces/dto/fcm/register-fcm-token.dto.ts b/src/interfaces/dto/fcm/register-fcm-token.dto.ts new file mode 100644 index 00000000..dc340c71 --- /dev/null +++ b/src/interfaces/dto/fcm/register-fcm-token.dto.ts @@ -0,0 +1,9 @@ +import { FireStoreTokenCollection } from "@/interfaces/fcm" +import { APIResponse } from "../api-response" + +export interface RegisterFcmTokenRequest { + user: FireStoreTokenCollection["user"] + token: string +} + +export interface RegisterFcmTokenResponse extends APIResponse {} diff --git a/src/interfaces/dto/fcm/send-fcm.dto.ts b/src/interfaces/dto/fcm/send-fcm.dto.ts new file mode 100644 index 00000000..32cc30fd --- /dev/null +++ b/src/interfaces/dto/fcm/send-fcm.dto.ts @@ -0,0 +1,22 @@ +import { APIResponse } from "../api-response" + +// 'answer' | 'rank' | 'coffeeChatRequest' +// 현재는 answer만 가능 +export type NotificationType = "answer" + +export type FcmNotificationData = { + answer: { postId: string; questionAuthorId: string } +}[TData] + +export type FcmNotification = { + title: string + body: string + imageUrl?: string + data: FcmNotificationData +} + +export interface SendFcmRequest { + notification: FcmNotification +} + +export interface SendFcmResponse extends APIResponse {} diff --git a/src/interfaces/fcm.ts b/src/interfaces/fcm.ts new file mode 100644 index 00000000..122015d0 --- /dev/null +++ b/src/interfaces/fcm.ts @@ -0,0 +1,6 @@ +export interface FireStoreTokenCollection { + user: { + id: number + } + tokenList: string[] +} diff --git a/src/page/qna-detail/QnADetail.tsx b/src/page/qna-detail/QnADetail.tsx index 4bf980c6..30d3f265 100644 --- a/src/page/qna-detail/QnADetail.tsx +++ b/src/page/qna-detail/QnADetail.tsx @@ -44,7 +44,7 @@ const QnADetail: React.FC<{ id: string }> = ({ id }) => { diff --git a/src/page/qna-detail/components/Answers/CreateAnswer.tsx b/src/page/qna-detail/components/Answers/CreateAnswer.tsx index d66bdf9c..8fcf5cda 100644 --- a/src/page/qna-detail/components/Answers/CreateAnswer.tsx +++ b/src/page/qna-detail/components/Answers/CreateAnswer.tsx @@ -13,15 +13,16 @@ import UploadedAnswerImages from "@/components/shared/toast-ui-editor/editor/Upl import AnswerFormProvider from "../formProvider/AnswerFormProvider" import { useAnswerFormContext } from "@/hooks/editor/useAnswerFormContext" import CreateAnswerForm from "./form/CreateAnswerForm" +import { Question } from "@/interfaces/question" export interface MyAnswerProps { - questionId: number + question: Question list?: Answer[] isQuestionAuthor: boolean } const CreateAnswer: React.FC = ({ - questionId, + question, list, isQuestionAuthor, }) => { @@ -73,7 +74,7 @@ const CreateAnswer: React.FC = ({ - + ) diff --git a/src/page/qna-detail/components/Answers/form/CreateAnswerForm.tsx b/src/page/qna-detail/components/Answers/form/CreateAnswerForm.tsx index f7f4917d..a2d3caf9 100644 --- a/src/page/qna-detail/components/Answers/form/CreateAnswerForm.tsx +++ b/src/page/qna-detail/components/Answers/form/CreateAnswerForm.tsx @@ -1,3 +1,5 @@ +"use client" + import Button from "@/components/shared/button/Button" import buttonMessage from "@/constants/message/button" import { useAnswerFormContext } from "@/hooks/editor/useAnswerFormContext" @@ -11,16 +13,18 @@ import SuccessModalContent from "../../SuccessModalContent" import successMessage from "@/constants/message/success" import useModal from "@/hooks/useModal" import { pickFirstAnswerFormError } from "@/util/hook-form/error" +import { Question } from "@/interfaces/question" +import { useFCM } from "@/hooks/firebase/useFCM" interface CreateAnswerFormProps { - questionId: number + question: Question } const CreateAnswerEditor = lazy( () => import("../../Answers/editor/CreateAnswerEditor"), ) -function CreateAnswerForm({ questionId }: CreateAnswerFormProps) { +function CreateAnswerForm({ question }: CreateAnswerFormProps) { const { user } = useClientSession() const { openModal } = useModal() @@ -31,11 +35,22 @@ function CreateAnswerForm({ questionId }: CreateAnswerFormProps) { formReset, } = useAnswerFormContext() + const { send } = useFCM() + const { createAnswerApi, createAnswerApiStatus } = useCreateAnswer({ - questionId, + questionId: question.id, onSuccess() { formReset() + send("answer", { + title: "커널스퀘어 답변 알림", + body: `${question.title} 글에 ${user?.nickname} 님이 답변했습니다.`, + data: { + postId: `${question.id}`, + questionAuthorId: `${question.member_id}`, + }, + }) + setTimeout(() => { openModal({ content: ( @@ -50,7 +65,7 @@ function CreateAnswerForm({ questionId }: CreateAnswerFormProps) { if (createAnswerApiStatus === "pending") return createAnswerApi({ - questionId, + questionId: question.id, content: answer, member_id: user?.member_id ?? -1, ...(images.length && { image_url: images[0].uploadURL }), diff --git a/src/service/axios.ts b/src/service/axios.ts index c699695a..41d96480 100644 --- a/src/service/axios.ts +++ b/src/service/axios.ts @@ -66,5 +66,21 @@ const createAPIInstance = (domain?: "alert") => { return axiosInstance } +const createFireBaseApiInstance = () => { + const axiosInstance = axios.create({ + baseURL: process.env.NEXT_PUBLIC_SITE_URL, + withCredentials: true, + }) + + axiosInstance.interceptors.request.use((request) => { + request.headers.Authorization = process.env.NEXT_PUBLIC_FIREBASE_ACCESS_KEY + + return request + }) + + return axiosInstance +} + export const apiInstance = createAPIInstance() export const alertApiInstance = createAPIInstance("alert") +export const firebaseApiInstance = createFireBaseApiInstance() diff --git a/src/service/fcm.ts b/src/service/fcm.ts new file mode 100644 index 00000000..114da379 --- /dev/null +++ b/src/service/fcm.ts @@ -0,0 +1,67 @@ +import { + RegisterFcmTokenRequest, + RegisterFcmTokenResponse, +} from "@/interfaces/dto/fcm/register-fcm-token.dto" +import { + NotificationType, + SendFcmRequest, + SendFcmResponse, +} from "@/interfaces/dto/fcm/send-fcm.dto" +import { AxiosResponse } from "axios" +import { firebaseApiInstance } from "./axios" +import { + DeleteFcmTokenRequest, + DeleteFcmTokenResponse, +} from "@/interfaces/dto/fcm/delete-fcm-token.dto" +import { + GetUserFcmTokenRequest, + GetUserFcmTokenResponse, +} from "@/interfaces/dto/fcm/get-user-fcm-token.dto" + +export async function getUserFcmToken({ userId }: GetUserFcmTokenRequest) { + const res = await firebaseApiInstance.get( + `/api/fcm-token/user/${userId}`, + ) + + return res +} + +export async function registerFcmToken({ + user, + token, +}: RegisterFcmTokenRequest) { + const res = await firebaseApiInstance.post< + any, + AxiosResponse, + RegisterFcmTokenRequest + >("/api/fcm-token", { + user, + token, + }) + + return res +} + +export async function deleteFcmToken({ token }: DeleteFcmTokenRequest) { + const res = await firebaseApiInstance.delete( + `/api/fcm-token/${token}`, + ) + + return res +} + +export async function sendFcm( + type: TData, + payload: SendFcmRequest, +) { + const res = await firebaseApiInstance.post< + any, + AxiosResponse, + SendFcmRequest & { type: TData } + >("/api/send-fcm", { + type, + ...payload, + }) + + return res +} diff --git a/src/util/actions/fcm.ts b/src/util/actions/fcm.ts new file mode 100644 index 00000000..7767ab37 --- /dev/null +++ b/src/util/actions/fcm.ts @@ -0,0 +1,63 @@ +"use server" + +import { GetUserFcmTokenRequest } from "@/interfaces/dto/fcm/get-user-fcm-token.dto" +import { + NotificationType, + SendFcmRequest, +} from "@/interfaces/dto/fcm/send-fcm.dto" +import { getUserFcmToken, registerFcmToken, sendFcm } from "@/service/fcm" + +export async function hasFcmTokenAction({ + userId, + token, +}: GetUserFcmTokenRequest & { token: string | null }) { + try { + const res = await getUserFcmToken({ userId }) + + const tokenList = res.data.data?.tokenList + + return { + success: true, + hasFcmToken: + token && tokenList?.length ? tokenList.includes(token) : false, + } + } catch (error) { + return { success: false } + } +} + +export async function registerFcmTokenAction({ + userId, + token, +}: { + userId: number + token: string +}) { + try { + await registerFcmToken({ + user: { + id: userId, + }, + token, + }) + + return { success: true } + } catch (error) { + return { success: false } + } +} + +export async function sendFcmAction( + type: TData, + payload: SendFcmRequest["notification"], +) { + try { + await sendFcm(type, { + notification: { ...payload }, + }) + + return { success: true } + } catch (error) { + return { success: false } + } +} diff --git a/src/util/check.ts b/src/util/check.ts new file mode 100644 index 00000000..8e278226 --- /dev/null +++ b/src/util/check.ts @@ -0,0 +1,7 @@ +export function isSupportServiceWorker() { + return ( + !!globalThis?.Notification && + !!globalThis?.navigator && + "serviceWorker" in navigator + ) +}