diff --git a/.env.production b/.env.production index d657c94..d430a89 100644 --- a/.env.production +++ b/.env.production @@ -1 +1 @@ -VITE_KAKAO_REDIRECT_URI=https://d3741u3vzg4n3d.cloudfront.net/loginSuccess \ No newline at end of file +VITE_KAKAO_REDIRECT_URI=https://dx44qcj8tqeon.cloudfront.net/loginSuccess \ No newline at end of file diff --git a/.github/workflows/build-and-deploy.yaml b/.github/workflows/build-and-deploy.yaml index 276b226..da20d12 100644 --- a/.github/workflows/build-and-deploy.yaml +++ b/.github/workflows/build-and-deploy.yaml @@ -33,13 +33,23 @@ jobs: echo "VITE_API_URL=${{ secrets.VITE_API_URL }}" >> .env echo "VITE_KAKAO_JS_KEY=${{ secrets.VITE_KAKAO_JS_KEY }}" >> .env echo "VITE_SIGNATURE_ENCRYPTION_KEY=${{ secrets.VITE_SIGNATURE_ENCRYPTION_KEY }}" >> .env + echo "VITE_FIREBASE_API_KEY=${{ secrets.VITE_FIREBASE_API_KEY }}" >> .env + echo "VITE_FIREBASE_AUTH_DOMAIN=${{ secrets.VITE_FIREBASE_AUTH_DOMAIN }}" >> .env + echo "VITE_FIREBASE_PROJECT_ID=${{ secrets.VITE_FIREBASE_PROJECT_ID }}" >> .env + echo "VITE_FIREBASE_STORAGE_BUCKET=${{ secrets.VITE_FIREBASE_STORAGE_BUCKET }}" >> .env + echo "VITE_FIREBASE_MESSAGING_SENDER_ID=${{ secrets.VITE_FIREBASE_MESSAGING_SENDER_ID }}" >> .env + echo "VITE_FIREBASE_APP_ID=${{ secrets.VITE_FIREBASE_APP_ID }}" >> .env + echo "VITE_FIREBASE_MEASUREMENT_ID=${{ secrets.VITE_FIREBASE_MEASUREMENT_ID }}" >> .env + echo "VITE_FIREBASE_VAPID_KEY=${{ secrets.VITE_FIREBASE_VAPID_KEY }}" >> .env + echo "VITE_ADMIN_API_URL=${{ secrets.VITE_ADMIN_API_URL }}" >> .env + echo "VITE_TOSS_CLIENT_KEY=${{ secrets.VITE_TOSS_CLIENT_KEY }}" >> .env if [ "${{ github.ref }}" = "refs/heads/main" ]; then - echo "VITE_KAKAO_REDIRECT_URI=${{ secrets.VITE_KAKAO_REDIRECT_URI_PROD }}" >> .env.production + echo "VITE_KAKAO_REDIRECT_URI=${{ secrets.VITE_KAKAO_REDIRECT_URI_PROD }}" >> .env elif [ "${{ github.ref }}" = "refs/heads/test" ]; then - echo "VITE_KAKAO_REDIRECT_URI=${{ secrets.VITE_KAKAO_REDIRECT_URI_TEST }}" >> .env.test + echo "VITE_KAKAO_REDIRECT_URI=${{ secrets.VITE_KAKAO_REDIRECT_URI_TEST }}" >> .env else - echo "VITE_KAKAO_REDIRECT_URI=${{ secrets.VITE_KAKAO_REDIRECT_URI_DEV }}" >> .env.development + echo "VITE_KAKAO_REDIRECT_URI=${{ secrets.VITE_KAKAO_REDIRECT_URI_DEV }}" >> .env fi - name: Build React app diff --git a/dev-dist/sw.js b/dev-dist/sw.js index 460c12e..0e08011 100644 --- a/dev-dist/sw.js +++ b/dev-dist/sw.js @@ -82,7 +82,7 @@ define(['./workbox-86c9b217'], (function (workbox) { 'use strict'; "revision": "3ca0b8505b4bec776b69afdba2768812" }, { "url": "index.html", - "revision": "0.b814jr1kg9o" + "revision": "0.f8meplpigvg" }], {}); workbox.cleanupOutdatedCaches(); workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), { diff --git a/package.json b/package.json index b60967d..d531481 100644 --- a/package.json +++ b/package.json @@ -18,12 +18,14 @@ "dependencies": { "@hookform/resolvers": "^5.0.1", "@lottiefiles/react-lottie-player": "^3.6.0", + "@tosspayments/tosspayments-sdk": "^2.3.5", "axios": "^1.8.4", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "crypto-js": "^4.2.0", "date-fns": "^4.1.0", "dayjs": "^1.11.13", + "firebase": "^11.8.1", "framer-motion": "^12.9.1", "html5-qrcode": "^2.3.8", "lodash.isequal": "^4.5.0", @@ -36,6 +38,7 @@ "react-dropzone": "^14.3.8", "react-hook-form": "^7.55.0", "react-intersection-observer": "^9.16.0", + "react-ios-pwa-prompt": "^2.0.6", "react-router-dom": "^7.3.0", "react-signature-canvas": "1.1.0-alpha.2", "react-toastify": "^11.0.5", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b36dc5b..7fef892 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,6 +14,9 @@ importers: '@lottiefiles/react-lottie-player': specifier: ^3.6.0 version: 3.6.0(react@19.1.0) + '@tosspayments/tosspayments-sdk': + specifier: ^2.3.5 + version: 2.3.5 axios: specifier: ^1.8.4 version: 1.9.0 @@ -32,6 +35,9 @@ importers: dayjs: specifier: ^1.11.13 version: 1.11.13 + firebase: + specifier: ^11.8.1 + version: 11.8.1 framer-motion: specifier: ^12.9.1 version: 12.15.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -68,6 +74,9 @@ importers: react-intersection-observer: specifier: ^9.16.0 version: 9.16.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + react-ios-pwa-prompt: + specifier: ^2.0.6 + version: 2.0.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0) react-router-dom: specifier: ^7.3.0 version: 7.6.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -876,6 +885,225 @@ packages: resolution: {integrity: sha512-0J+zgWxHN+xXONWIyPWKFMgVuJoZuGiIFu8yxk7RJjxkzpGmyja5wRFqZIVtjDVOQpV+Rw0iOAjYPE2eQyjr0w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@firebase/ai@1.3.0': + resolution: {integrity: sha512-qBxJTtl9hpgZr050kVFTRADX6I0Ss6mEQyp/JEkBgKwwxixKnaRNqEDGFba4OKNL7K8E4Y7LlA/ZW6L8aCKH4A==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@firebase/app': 0.x + '@firebase/app-types': 0.x + + '@firebase/analytics-compat@0.2.22': + resolution: {integrity: sha512-VogWHgwkdYhjWKh8O1XU04uPrRaiDihkWvE/EMMmtWtaUtVALnpLnUurc3QtSKdPnvTz5uaIGKlW84DGtSPFbw==} + peerDependencies: + '@firebase/app-compat': 0.x + + '@firebase/analytics-types@0.8.3': + resolution: {integrity: sha512-VrIp/d8iq2g501qO46uGz3hjbDb8xzYMrbu8Tp0ovzIzrvJZ2fvmj649gTjge/b7cCCcjT0H37g1gVtlNhnkbg==} + + '@firebase/analytics@0.10.16': + resolution: {integrity: sha512-cMtp19He7Fd6uaj/nDEul+8JwvJsN8aRSJyuA1QN3QrKvfDDp+efjVurJO61sJpkVftw9O9nNMdhFbRcTmTfRQ==} + peerDependencies: + '@firebase/app': 0.x + + '@firebase/app-check-compat@0.3.25': + resolution: {integrity: sha512-3zrsPZWAKfV7DVC20T2dgfjzjtQnSJS65OfMOiddMUtJL1S5i0nAZKsdX0bOEvvrd0SBIL8jYnfpfDeQRnhV3w==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@firebase/app-compat': 0.x + + '@firebase/app-check-interop-types@0.3.3': + resolution: {integrity: sha512-gAlxfPLT2j8bTI/qfe3ahl2I2YcBQ8cFIBdhAQA4I2f3TndcO+22YizyGYuttLHPQEpWkhmpFW60VCFEPg4g5A==} + + '@firebase/app-check-types@0.5.3': + resolution: {integrity: sha512-hyl5rKSj0QmwPdsAxrI5x1otDlByQ7bvNvVt8G/XPO2CSwE++rmSVf3VEhaeOR4J8ZFaF0Z0NDSmLejPweZ3ng==} + + '@firebase/app-check@0.10.0': + resolution: {integrity: sha512-AZlRlVWKcu8BH4Yf8B5EI8sOi2UNGTS8oMuthV45tbt6OVUTSQwFPIEboZzhNJNKY+fPsg7hH8vixUWFZ3lrhw==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@firebase/app': 0.x + + '@firebase/app-compat@0.4.0': + resolution: {integrity: sha512-LjLUrzbUgTa/sCtPoLKT2C7KShvLVHS3crnU1Du02YxnGVLE0CUBGY/NxgfR/Zg84mEbj1q08/dgesojxjn0dA==} + engines: {node: '>=18.0.0'} + + '@firebase/app-types@0.9.3': + resolution: {integrity: sha512-kRVpIl4vVGJ4baogMDINbyrIOtOxqhkZQg4jTq3l8Lw6WSk0xfpEYzezFu+Kl4ve4fbPl79dvwRtaFqAC/ucCw==} + + '@firebase/app@0.13.0': + resolution: {integrity: sha512-Vj3MST245nq+V5UmmfEkB3isIgPouyUr8yGJlFeL9Trg/umG5ogAvrjAYvQ8gV7daKDoQSRnJKWI2JFpQqRsuQ==} + engines: {node: '>=18.0.0'} + + '@firebase/auth-compat@0.5.26': + resolution: {integrity: sha512-4baB7tR0KukyGzrlD25aeO4t0ChLifwvDQXTBiVJE9WWwJEOjkZpHmoU9Iww0+Vdalsq4sZ3abp6YTNjHyB1dA==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@firebase/app-compat': 0.x + + '@firebase/auth-interop-types@0.2.4': + resolution: {integrity: sha512-JPgcXKCuO+CWqGDnigBtvo09HeBs5u/Ktc2GaFj2m01hLarbxthLNm7Fk8iOP1aqAtXV+fnnGj7U28xmk7IwVA==} + + '@firebase/auth-types@0.13.0': + resolution: {integrity: sha512-S/PuIjni0AQRLF+l9ck0YpsMOdE8GO2KU6ubmBB7P+7TJUCQDa3R1dlgYm9UzGbbePMZsp0xzB93f2b/CgxMOg==} + peerDependencies: + '@firebase/app-types': 0.x + '@firebase/util': 1.x + + '@firebase/auth@1.10.6': + resolution: {integrity: sha512-cFbo2FymQltog4atI9cKTO6CxKxS0dOMXslTQrlNZRH7qhDG44/d7QeI6GXLweFZtrnlecf52ESnNz1DU6ek8w==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@firebase/app': 0.x + '@react-native-async-storage/async-storage': ^1.18.1 + peerDependenciesMeta: + '@react-native-async-storage/async-storage': + optional: true + + '@firebase/component@0.6.17': + resolution: {integrity: sha512-M6DOg7OySrKEFS8kxA3MU5/xc37fiOpKPMz6cTsMUcsuKB6CiZxxNAvgFta8HGRgEpZbi8WjGIj6Uf+TpOhyzg==} + engines: {node: '>=18.0.0'} + + '@firebase/data-connect@0.3.9': + resolution: {integrity: sha512-B5tGEh5uQrQeH0i7RvlU8kbZrKOJUmoyxVIX4zLA8qQJIN6A7D+kfBlGXtSwbPdrvyaejcRPcbOtqsDQ9HPJKw==} + peerDependencies: + '@firebase/app': 0.x + + '@firebase/database-compat@2.0.10': + resolution: {integrity: sha512-3sjl6oGaDDYJw/Ny0E5bO6v+KM3KoD4Qo/sAfHGdRFmcJ4QnfxOX9RbG9+ce/evI3m64mkPr24LlmTDduqMpog==} + engines: {node: '>=18.0.0'} + + '@firebase/database-types@1.0.14': + resolution: {integrity: sha512-8a0Q1GrxM0akgF0RiQHliinhmZd+UQPrxEmUv7MnQBYfVFiLtKOgs3g6ghRt/WEGJHyQNslZ+0PocIwNfoDwKw==} + + '@firebase/database@1.0.19': + resolution: {integrity: sha512-khE+MIYK+XlIndVn/7mAQ9F1fwG5JHrGKaG72hblCC6JAlUBDd3SirICH6SMCf2PQ0iYkruTECth+cRhauacyQ==} + engines: {node: '>=18.0.0'} + + '@firebase/firestore-compat@0.3.51': + resolution: {integrity: sha512-E5iubPhS6aAM7oSsHMx/FGBwfA2nbEHaK/hCs+MD3l3N7rHKnq4SYCGmVu/AraSJaMndZR1I37N9A/BH7aCq5A==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@firebase/app-compat': 0.x + + '@firebase/firestore-types@3.0.3': + resolution: {integrity: sha512-hD2jGdiWRxB/eZWF89xcK9gF8wvENDJkzpVFb4aGkzfEaKxVRD1kjz1t1Wj8VZEp2LCB53Yx1zD8mrhQu87R6Q==} + peerDependencies: + '@firebase/app-types': 0.x + '@firebase/util': 1.x + + '@firebase/firestore@4.7.16': + resolution: {integrity: sha512-5OpvlwYVUTLEnqewOlXmtIpH8t2ISlZHDW0NDbKROM2D0ATMqFkMHdvl+/wz9zOAcb8GMQYlhCihOnVAliUbpQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@firebase/app': 0.x + + '@firebase/functions-compat@0.3.25': + resolution: {integrity: sha512-V0JKUw5W/7aznXf9BQ8LIYHCX6zVCM8Hdw7XUQ/LU1Y9TVP8WKRCnPB/qdPJ0xGjWWn7fhtwIYbgEw/syH4yTQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@firebase/app-compat': 0.x + + '@firebase/functions-types@0.6.3': + resolution: {integrity: sha512-EZoDKQLUHFKNx6VLipQwrSMh01A1SaL3Wg6Hpi//x6/fJ6Ee4hrAeswK99I5Ht8roiniKHw4iO0B1Oxj5I4plg==} + + '@firebase/functions@0.12.8': + resolution: {integrity: sha512-p+ft6dQW0CJ3BLLxeDb5Hwk9ARw01kHTZjLqiUdPRzycR6w7Z75ThkegNmL6gCss3S0JEpldgvehgZ3kHybVhA==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@firebase/app': 0.x + + '@firebase/installations-compat@0.2.17': + resolution: {integrity: sha512-J7afeCXB7yq25FrrJAgbx8mn1nG1lZEubOLvYgG7ZHvyoOCK00sis5rj7TgDrLYJgdj/SJiGaO1BD3BAp55TeA==} + peerDependencies: + '@firebase/app-compat': 0.x + + '@firebase/installations-types@0.5.3': + resolution: {integrity: sha512-2FJI7gkLqIE0iYsNQ1P751lO3hER+Umykel+TkLwHj6plzWVxqvfclPUZhcKFVQObqloEBTmpi2Ozn7EkCABAA==} + peerDependencies: + '@firebase/app-types': 0.x + + '@firebase/installations@0.6.17': + resolution: {integrity: sha512-zfhqCNJZRe12KyADtRrtOj+SeSbD1H/K8J24oQAJVv/u02eQajEGlhZtcx9Qk7vhGWF5z9dvIygVDYqLL4o1XQ==} + peerDependencies: + '@firebase/app': 0.x + + '@firebase/logger@0.4.4': + resolution: {integrity: sha512-mH0PEh1zoXGnaR8gD1DeGeNZtWFKbnz9hDO91dIml3iou1gpOnLqXQ2dJfB71dj6dpmUjcQ6phY3ZZJbjErr9g==} + engines: {node: '>=18.0.0'} + + '@firebase/messaging-compat@0.2.21': + resolution: {integrity: sha512-1yMne+4BGLbHbtyu/VyXWcLiefUE1+K3ZGfVTyKM4BH4ZwDFRGoWUGhhx+tKRX4Tu9z7+8JN67SjnwacyNWK5g==} + peerDependencies: + '@firebase/app-compat': 0.x + + '@firebase/messaging-interop-types@0.2.3': + resolution: {integrity: sha512-xfzFaJpzcmtDjycpDeCUj0Ge10ATFi/VHVIvEEjDNc3hodVBQADZ7BWQU7CuFpjSHE+eLuBI13z5F/9xOoGX8Q==} + + '@firebase/messaging@0.12.21': + resolution: {integrity: sha512-bYJ2Evj167Z+lJ1ach6UglXz5dUKY1zrJZd15GagBUJSR7d9KfiM1W8dsyL0lDxcmhmA/sLaBYAAhF1uilwN0g==} + peerDependencies: + '@firebase/app': 0.x + + '@firebase/performance-compat@0.2.19': + resolution: {integrity: sha512-4cU0T0BJ+LZK/E/UwFcvpBCVdkStgBMQwBztM9fJPT6udrEUk3ugF5/HT+E2Z22FCXtIaXDukJbYkE/c3c6IHw==} + peerDependencies: + '@firebase/app-compat': 0.x + + '@firebase/performance-types@0.2.3': + resolution: {integrity: sha512-IgkyTz6QZVPAq8GSkLYJvwSLr3LS9+V6vNPQr0x4YozZJiLF5jYixj0amDtATf1X0EtYHqoPO48a9ija8GocxQ==} + + '@firebase/performance@0.7.6': + resolution: {integrity: sha512-AsOz74dSTlyQGlnnbLWXiHFAsrxhpssPOsFFi4HgOJ5DjzkK7ZdZ/E9uMPrwFoXJyMVoybGRuqsL/wkIbFITsA==} + peerDependencies: + '@firebase/app': 0.x + + '@firebase/remote-config-compat@0.2.17': + resolution: {integrity: sha512-KelsBD0sXSC0u3esr/r6sJYGRN6pzn3bYuI/6pTvvmZbjBlxQkRabHAVH6d+YhLcjUXKIAYIjZszczd1QJtOyA==} + peerDependencies: + '@firebase/app-compat': 0.x + + '@firebase/remote-config-types@0.4.0': + resolution: {integrity: sha512-7p3mRE/ldCNYt8fmWMQ/MSGRmXYlJ15Rvs9Rk17t8p0WwZDbeK7eRmoI1tvCPaDzn9Oqh+yD6Lw+sGLsLg4kKg==} + + '@firebase/remote-config@0.6.4': + resolution: {integrity: sha512-ZyLJRT46wtycyz2+opEkGaoFUOqRQjt/0NX1WfUISOMCI/PuVoyDjqGpq24uK+e8D5NknyTpiXCVq5dowhScmg==} + peerDependencies: + '@firebase/app': 0.x + + '@firebase/storage-compat@0.3.22': + resolution: {integrity: sha512-29j6JgXTjQ76sOIkxmTNHQfYA/hDTeV9qGbn0jolynPXSg/AmzCB0CpCoCYrS0ja0Flgmy1hkA3XYDZ/eiV1Cg==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@firebase/app-compat': 0.x + + '@firebase/storage-types@0.8.3': + resolution: {integrity: sha512-+Muk7g9uwngTpd8xn9OdF/D48uiQ7I1Fae7ULsWPuKoCH3HU7bfFPhxtJYzyhjdniowhuDpQcfPmuNRAqZEfvg==} + peerDependencies: + '@firebase/app-types': 0.x + '@firebase/util': 1.x + + '@firebase/storage@0.13.12': + resolution: {integrity: sha512-5JmoFS01MYjW1XMQa5F5rD/kvMwBN10QF03bmcuJWq4lg+BJ3nRgL3sscWnyJPhwM/ZCyv2eRwcfzESVmsYkdQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@firebase/app': 0.x + + '@firebase/util@1.12.0': + resolution: {integrity: sha512-Z4rK23xBCwgKDqmzGVMef+Vb4xso2j5Q8OG0vVL4m4fA5ZjPMYQazu8OJJC3vtQRC3SQ/Pgx/6TPNVsCd70QRw==} + engines: {node: '>=18.0.0'} + + '@firebase/webchannel-wrapper@1.0.3': + resolution: {integrity: sha512-2xCRM9q9FlzGZCdgDMJwc0gyUkWFtkosy7Xxr6sFgQwn+wMNIWd7xIvYNauU1r64B5L5rsGKy/n9TKJ0aAFeqQ==} + + '@grpc/grpc-js@1.9.15': + resolution: {integrity: sha512-nqE7Hc0AzI+euzUwDAy0aY5hCp10r734gMGRdU+qOPX0XSceI2ULrcXB5U2xSc5VkWwalCj4M7GzCAygZl2KoQ==} + engines: {node: ^8.13.0 || >=10.10.0} + + '@grpc/proto-loader@0.7.15': + resolution: {integrity: sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==} + engines: {node: '>=6'} + hasBin: true + '@hookform/resolvers@5.0.1': resolution: {integrity: sha512-u/+Jp83luQNx9AdyW2fIPGY6Y7NG68eN2ZW8FOJYL+M0i4s49+refdJdOp/A9n9HFQtQs3HIDHQvX3ZET2o7YA==} peerDependencies: @@ -966,6 +1194,36 @@ packages: resolution: {integrity: sha512-ROFF39F6ZrnzSUEmQQZUar0Jt4xVoP9WnDRdWwF4NNcXs3xBTLgBUDoOwW141y1jP+S8nahIbdxbFC7IShw9Iw==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + '@protobufjs/aspromise@1.1.2': + resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} + + '@protobufjs/base64@1.1.2': + resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} + + '@protobufjs/codegen@2.0.4': + resolution: {integrity: sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==} + + '@protobufjs/eventemitter@1.1.0': + resolution: {integrity: sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==} + + '@protobufjs/fetch@1.1.0': + resolution: {integrity: sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==} + + '@protobufjs/float@1.0.2': + resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} + + '@protobufjs/inquire@1.1.0': + resolution: {integrity: sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==} + + '@protobufjs/path@1.1.2': + resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} + + '@protobufjs/pool@1.1.0': + resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} + + '@protobufjs/utf8@1.1.0': + resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==} + '@rolldown/pluginutils@1.0.0-beta.9': resolution: {integrity: sha512-e9MeMtVWo186sgvFFJOPGy7/d2j2mZhLJIdVW0C/xDluuOvymEATqz6zKsP0ZmXGzQtqlyjz5sC1sYQUoJG98w==} @@ -1374,6 +1632,9 @@ packages: peerDependencies: '@testing-library/dom': '>=7.21.4' + '@tosspayments/tosspayments-sdk@2.3.5': + resolution: {integrity: sha512-XHi6e9LrF02vm4vuhrt8B4BaDUuyL0sQtdp21HITCUFdyRRB+lQKLZSRcBb8VTpymrCISlg/j8mT2r/QlJmipg==} + '@types/aria-query@5.0.4': resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} @@ -1746,6 +2007,10 @@ packages: class-variance-authority@0.7.1: resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + clsx@2.1.1: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} @@ -2080,6 +2345,10 @@ packages: fastq@1.19.1: resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} + faye-websocket@0.11.4: + resolution: {integrity: sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==} + engines: {node: '>=0.8.0'} + fdir@6.4.4: resolution: {integrity: sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==} peerDependencies: @@ -2107,6 +2376,9 @@ packages: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} + firebase@11.8.1: + resolution: {integrity: sha512-oetXhPCvJZM4DVL/n/06442emMU+KzM0JLZjszpwlU6mqdFZqBwumBxn6hQkLukJyU5wsjihZHUY8HEAE2micg==} + flat-cache@4.0.1: resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} engines: {node: '>=16'} @@ -2178,6 +2450,10 @@ packages: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + get-intrinsic@1.3.0: resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} engines: {node: '>= 0.4'} @@ -2268,6 +2544,9 @@ packages: html5-qrcode@2.3.8: resolution: {integrity: sha512-jsr4vafJhwoLVEDW3n1KvPnCCXWaQfRng0/EEYk1vNcQGcG/htAdhJX0be8YyqMoSz7+hZvOZSTAepsabiuhiQ==} + http-parser-js@0.5.10: + resolution: {integrity: sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==} + idb@7.1.1: resolution: {integrity: sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==} @@ -2537,6 +2816,9 @@ packages: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} + lodash.camelcase@4.3.0: + resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} + lodash.debounce@4.0.8: resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} @@ -2553,6 +2835,9 @@ packages: lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + long@5.3.2: + resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} + loose-envify@1.4.0: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true @@ -2868,6 +3153,10 @@ packages: prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + protobufjs@7.5.3: + resolution: {integrity: sha512-sildjKwVqOI2kmFDiXQ6aEB0fjYTafpEvIBs8tOR8qI4spuL9OPROLVu2qZqi/xgCfsHIwVqlaF8JBjWFHnKbw==} + engines: {node: '>=12.0.0'} + proxy-from-env@1.1.0: resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} @@ -2936,6 +3225,12 @@ packages: react-dom: optional: true + react-ios-pwa-prompt@2.0.6: + resolution: {integrity: sha512-slRvFlcYsOL01D8gbJW4agOQ64pk7V+0EBk+d8z8wQ40vtcaoSw/JJkh+fJQrWSfWFVvZESu3FHP0+un4YUjJg==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} @@ -3024,6 +3319,10 @@ packages: resolution: {integrity: sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ==} hasBin: true + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + require-from-string@2.0.2: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} @@ -3477,12 +3776,23 @@ packages: warning@4.0.3: resolution: {integrity: sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==} + web-vitals@4.2.4: + resolution: {integrity: sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==} + webidl-conversions@4.0.2: resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==} webpack-virtual-modules@0.6.2: resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} + websocket-driver@0.7.4: + resolution: {integrity: sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==} + engines: {node: '>=0.8.0'} + + websocket-extensions@0.1.4: + resolution: {integrity: sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==} + engines: {node: '>=0.8.0'} + whatwg-url@7.1.0: resolution: {integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==} @@ -3583,6 +3893,10 @@ packages: utf-8-validate: optional: true + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} @@ -3591,6 +3905,14 @@ packages: engines: {node: '>= 14.6'} hasBin: true + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} @@ -4390,6 +4712,336 @@ snapshots: '@eslint/core': 0.14.0 levn: 0.4.1 + '@firebase/ai@1.3.0(@firebase/app-types@0.9.3)(@firebase/app@0.13.0)': + dependencies: + '@firebase/app': 0.13.0 + '@firebase/app-check-interop-types': 0.3.3 + '@firebase/app-types': 0.9.3 + '@firebase/component': 0.6.17 + '@firebase/logger': 0.4.4 + '@firebase/util': 1.12.0 + tslib: 2.8.1 + + '@firebase/analytics-compat@0.2.22(@firebase/app-compat@0.4.0)(@firebase/app@0.13.0)': + dependencies: + '@firebase/analytics': 0.10.16(@firebase/app@0.13.0) + '@firebase/analytics-types': 0.8.3 + '@firebase/app-compat': 0.4.0 + '@firebase/component': 0.6.17 + '@firebase/util': 1.12.0 + tslib: 2.8.1 + transitivePeerDependencies: + - '@firebase/app' + + '@firebase/analytics-types@0.8.3': {} + + '@firebase/analytics@0.10.16(@firebase/app@0.13.0)': + dependencies: + '@firebase/app': 0.13.0 + '@firebase/component': 0.6.17 + '@firebase/installations': 0.6.17(@firebase/app@0.13.0) + '@firebase/logger': 0.4.4 + '@firebase/util': 1.12.0 + tslib: 2.8.1 + + '@firebase/app-check-compat@0.3.25(@firebase/app-compat@0.4.0)(@firebase/app@0.13.0)': + dependencies: + '@firebase/app-check': 0.10.0(@firebase/app@0.13.0) + '@firebase/app-check-types': 0.5.3 + '@firebase/app-compat': 0.4.0 + '@firebase/component': 0.6.17 + '@firebase/logger': 0.4.4 + '@firebase/util': 1.12.0 + tslib: 2.8.1 + transitivePeerDependencies: + - '@firebase/app' + + '@firebase/app-check-interop-types@0.3.3': {} + + '@firebase/app-check-types@0.5.3': {} + + '@firebase/app-check@0.10.0(@firebase/app@0.13.0)': + dependencies: + '@firebase/app': 0.13.0 + '@firebase/component': 0.6.17 + '@firebase/logger': 0.4.4 + '@firebase/util': 1.12.0 + tslib: 2.8.1 + + '@firebase/app-compat@0.4.0': + dependencies: + '@firebase/app': 0.13.0 + '@firebase/component': 0.6.17 + '@firebase/logger': 0.4.4 + '@firebase/util': 1.12.0 + tslib: 2.8.1 + + '@firebase/app-types@0.9.3': {} + + '@firebase/app@0.13.0': + dependencies: + '@firebase/component': 0.6.17 + '@firebase/logger': 0.4.4 + '@firebase/util': 1.12.0 + idb: 7.1.1 + tslib: 2.8.1 + + '@firebase/auth-compat@0.5.26(@firebase/app-compat@0.4.0)(@firebase/app-types@0.9.3)(@firebase/app@0.13.0)': + dependencies: + '@firebase/app-compat': 0.4.0 + '@firebase/auth': 1.10.6(@firebase/app@0.13.0) + '@firebase/auth-types': 0.13.0(@firebase/app-types@0.9.3)(@firebase/util@1.12.0) + '@firebase/component': 0.6.17 + '@firebase/util': 1.12.0 + tslib: 2.8.1 + transitivePeerDependencies: + - '@firebase/app' + - '@firebase/app-types' + - '@react-native-async-storage/async-storage' + + '@firebase/auth-interop-types@0.2.4': {} + + '@firebase/auth-types@0.13.0(@firebase/app-types@0.9.3)(@firebase/util@1.12.0)': + dependencies: + '@firebase/app-types': 0.9.3 + '@firebase/util': 1.12.0 + + '@firebase/auth@1.10.6(@firebase/app@0.13.0)': + dependencies: + '@firebase/app': 0.13.0 + '@firebase/component': 0.6.17 + '@firebase/logger': 0.4.4 + '@firebase/util': 1.12.0 + tslib: 2.8.1 + + '@firebase/component@0.6.17': + dependencies: + '@firebase/util': 1.12.0 + tslib: 2.8.1 + + '@firebase/data-connect@0.3.9(@firebase/app@0.13.0)': + dependencies: + '@firebase/app': 0.13.0 + '@firebase/auth-interop-types': 0.2.4 + '@firebase/component': 0.6.17 + '@firebase/logger': 0.4.4 + '@firebase/util': 1.12.0 + tslib: 2.8.1 + + '@firebase/database-compat@2.0.10': + dependencies: + '@firebase/component': 0.6.17 + '@firebase/database': 1.0.19 + '@firebase/database-types': 1.0.14 + '@firebase/logger': 0.4.4 + '@firebase/util': 1.12.0 + tslib: 2.8.1 + + '@firebase/database-types@1.0.14': + dependencies: + '@firebase/app-types': 0.9.3 + '@firebase/util': 1.12.0 + + '@firebase/database@1.0.19': + dependencies: + '@firebase/app-check-interop-types': 0.3.3 + '@firebase/auth-interop-types': 0.2.4 + '@firebase/component': 0.6.17 + '@firebase/logger': 0.4.4 + '@firebase/util': 1.12.0 + faye-websocket: 0.11.4 + tslib: 2.8.1 + + '@firebase/firestore-compat@0.3.51(@firebase/app-compat@0.4.0)(@firebase/app-types@0.9.3)(@firebase/app@0.13.0)': + dependencies: + '@firebase/app-compat': 0.4.0 + '@firebase/component': 0.6.17 + '@firebase/firestore': 4.7.16(@firebase/app@0.13.0) + '@firebase/firestore-types': 3.0.3(@firebase/app-types@0.9.3)(@firebase/util@1.12.0) + '@firebase/util': 1.12.0 + tslib: 2.8.1 + transitivePeerDependencies: + - '@firebase/app' + - '@firebase/app-types' + + '@firebase/firestore-types@3.0.3(@firebase/app-types@0.9.3)(@firebase/util@1.12.0)': + dependencies: + '@firebase/app-types': 0.9.3 + '@firebase/util': 1.12.0 + + '@firebase/firestore@4.7.16(@firebase/app@0.13.0)': + dependencies: + '@firebase/app': 0.13.0 + '@firebase/component': 0.6.17 + '@firebase/logger': 0.4.4 + '@firebase/util': 1.12.0 + '@firebase/webchannel-wrapper': 1.0.3 + '@grpc/grpc-js': 1.9.15 + '@grpc/proto-loader': 0.7.15 + tslib: 2.8.1 + + '@firebase/functions-compat@0.3.25(@firebase/app-compat@0.4.0)(@firebase/app@0.13.0)': + dependencies: + '@firebase/app-compat': 0.4.0 + '@firebase/component': 0.6.17 + '@firebase/functions': 0.12.8(@firebase/app@0.13.0) + '@firebase/functions-types': 0.6.3 + '@firebase/util': 1.12.0 + tslib: 2.8.1 + transitivePeerDependencies: + - '@firebase/app' + + '@firebase/functions-types@0.6.3': {} + + '@firebase/functions@0.12.8(@firebase/app@0.13.0)': + dependencies: + '@firebase/app': 0.13.0 + '@firebase/app-check-interop-types': 0.3.3 + '@firebase/auth-interop-types': 0.2.4 + '@firebase/component': 0.6.17 + '@firebase/messaging-interop-types': 0.2.3 + '@firebase/util': 1.12.0 + tslib: 2.8.1 + + '@firebase/installations-compat@0.2.17(@firebase/app-compat@0.4.0)(@firebase/app-types@0.9.3)(@firebase/app@0.13.0)': + dependencies: + '@firebase/app-compat': 0.4.0 + '@firebase/component': 0.6.17 + '@firebase/installations': 0.6.17(@firebase/app@0.13.0) + '@firebase/installations-types': 0.5.3(@firebase/app-types@0.9.3) + '@firebase/util': 1.12.0 + tslib: 2.8.1 + transitivePeerDependencies: + - '@firebase/app' + - '@firebase/app-types' + + '@firebase/installations-types@0.5.3(@firebase/app-types@0.9.3)': + dependencies: + '@firebase/app-types': 0.9.3 + + '@firebase/installations@0.6.17(@firebase/app@0.13.0)': + dependencies: + '@firebase/app': 0.13.0 + '@firebase/component': 0.6.17 + '@firebase/util': 1.12.0 + idb: 7.1.1 + tslib: 2.8.1 + + '@firebase/logger@0.4.4': + dependencies: + tslib: 2.8.1 + + '@firebase/messaging-compat@0.2.21(@firebase/app-compat@0.4.0)(@firebase/app@0.13.0)': + dependencies: + '@firebase/app-compat': 0.4.0 + '@firebase/component': 0.6.17 + '@firebase/messaging': 0.12.21(@firebase/app@0.13.0) + '@firebase/util': 1.12.0 + tslib: 2.8.1 + transitivePeerDependencies: + - '@firebase/app' + + '@firebase/messaging-interop-types@0.2.3': {} + + '@firebase/messaging@0.12.21(@firebase/app@0.13.0)': + dependencies: + '@firebase/app': 0.13.0 + '@firebase/component': 0.6.17 + '@firebase/installations': 0.6.17(@firebase/app@0.13.0) + '@firebase/messaging-interop-types': 0.2.3 + '@firebase/util': 1.12.0 + idb: 7.1.1 + tslib: 2.8.1 + + '@firebase/performance-compat@0.2.19(@firebase/app-compat@0.4.0)(@firebase/app@0.13.0)': + dependencies: + '@firebase/app-compat': 0.4.0 + '@firebase/component': 0.6.17 + '@firebase/logger': 0.4.4 + '@firebase/performance': 0.7.6(@firebase/app@0.13.0) + '@firebase/performance-types': 0.2.3 + '@firebase/util': 1.12.0 + tslib: 2.8.1 + transitivePeerDependencies: + - '@firebase/app' + + '@firebase/performance-types@0.2.3': {} + + '@firebase/performance@0.7.6(@firebase/app@0.13.0)': + dependencies: + '@firebase/app': 0.13.0 + '@firebase/component': 0.6.17 + '@firebase/installations': 0.6.17(@firebase/app@0.13.0) + '@firebase/logger': 0.4.4 + '@firebase/util': 1.12.0 + tslib: 2.8.1 + web-vitals: 4.2.4 + + '@firebase/remote-config-compat@0.2.17(@firebase/app-compat@0.4.0)(@firebase/app@0.13.0)': + dependencies: + '@firebase/app-compat': 0.4.0 + '@firebase/component': 0.6.17 + '@firebase/logger': 0.4.4 + '@firebase/remote-config': 0.6.4(@firebase/app@0.13.0) + '@firebase/remote-config-types': 0.4.0 + '@firebase/util': 1.12.0 + tslib: 2.8.1 + transitivePeerDependencies: + - '@firebase/app' + + '@firebase/remote-config-types@0.4.0': {} + + '@firebase/remote-config@0.6.4(@firebase/app@0.13.0)': + dependencies: + '@firebase/app': 0.13.0 + '@firebase/component': 0.6.17 + '@firebase/installations': 0.6.17(@firebase/app@0.13.0) + '@firebase/logger': 0.4.4 + '@firebase/util': 1.12.0 + tslib: 2.8.1 + + '@firebase/storage-compat@0.3.22(@firebase/app-compat@0.4.0)(@firebase/app-types@0.9.3)(@firebase/app@0.13.0)': + dependencies: + '@firebase/app-compat': 0.4.0 + '@firebase/component': 0.6.17 + '@firebase/storage': 0.13.12(@firebase/app@0.13.0) + '@firebase/storage-types': 0.8.3(@firebase/app-types@0.9.3)(@firebase/util@1.12.0) + '@firebase/util': 1.12.0 + tslib: 2.8.1 + transitivePeerDependencies: + - '@firebase/app' + - '@firebase/app-types' + + '@firebase/storage-types@0.8.3(@firebase/app-types@0.9.3)(@firebase/util@1.12.0)': + dependencies: + '@firebase/app-types': 0.9.3 + '@firebase/util': 1.12.0 + + '@firebase/storage@0.13.12(@firebase/app@0.13.0)': + dependencies: + '@firebase/app': 0.13.0 + '@firebase/component': 0.6.17 + '@firebase/util': 1.12.0 + tslib: 2.8.1 + + '@firebase/util@1.12.0': + dependencies: + tslib: 2.8.1 + + '@firebase/webchannel-wrapper@1.0.3': {} + + '@grpc/grpc-js@1.9.15': + dependencies: + '@grpc/proto-loader': 0.7.15 + '@types/node': 22.15.21 + + '@grpc/proto-loader@0.7.15': + dependencies: + lodash.camelcase: 4.3.0 + long: 5.3.2 + protobufjs: 7.5.3 + yargs: 17.7.2 + '@hookform/resolvers@5.0.1(react-hook-form@7.56.4(react@19.1.0))': dependencies: '@standard-schema/utils': 0.3.0 @@ -4476,6 +5128,29 @@ snapshots: '@pkgr/core@0.2.4': {} + '@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': + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/inquire': 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': {} + '@rolldown/pluginutils@1.0.0-beta.9': {} '@rollup/plugin-babel@5.3.1(@babel/core@7.27.3)(@types/babel__core@7.20.5)(rollup@2.79.2)': @@ -4887,6 +5562,8 @@ snapshots: dependencies: '@testing-library/dom': 10.4.0 + '@tosspayments/tosspayments-sdk@2.3.5': {} + '@types/aria-query@5.0.4': {} '@types/babel__core@7.20.5': @@ -5343,6 +6020,12 @@ snapshots: dependencies: clsx: 2.1.1 + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + clsx@2.1.1: {} color-convert@2.0.1: @@ -5773,6 +6456,10 @@ snapshots: dependencies: reusify: 1.1.0 + faye-websocket@0.11.4: + dependencies: + websocket-driver: 0.7.4 + fdir@6.4.4(picomatch@4.0.2): optionalDependencies: picomatch: 4.0.2 @@ -5798,6 +6485,39 @@ snapshots: locate-path: 6.0.0 path-exists: 4.0.0 + firebase@11.8.1: + dependencies: + '@firebase/ai': 1.3.0(@firebase/app-types@0.9.3)(@firebase/app@0.13.0) + '@firebase/analytics': 0.10.16(@firebase/app@0.13.0) + '@firebase/analytics-compat': 0.2.22(@firebase/app-compat@0.4.0)(@firebase/app@0.13.0) + '@firebase/app': 0.13.0 + '@firebase/app-check': 0.10.0(@firebase/app@0.13.0) + '@firebase/app-check-compat': 0.3.25(@firebase/app-compat@0.4.0)(@firebase/app@0.13.0) + '@firebase/app-compat': 0.4.0 + '@firebase/app-types': 0.9.3 + '@firebase/auth': 1.10.6(@firebase/app@0.13.0) + '@firebase/auth-compat': 0.5.26(@firebase/app-compat@0.4.0)(@firebase/app-types@0.9.3)(@firebase/app@0.13.0) + '@firebase/data-connect': 0.3.9(@firebase/app@0.13.0) + '@firebase/database': 1.0.19 + '@firebase/database-compat': 2.0.10 + '@firebase/firestore': 4.7.16(@firebase/app@0.13.0) + '@firebase/firestore-compat': 0.3.51(@firebase/app-compat@0.4.0)(@firebase/app-types@0.9.3)(@firebase/app@0.13.0) + '@firebase/functions': 0.12.8(@firebase/app@0.13.0) + '@firebase/functions-compat': 0.3.25(@firebase/app-compat@0.4.0)(@firebase/app@0.13.0) + '@firebase/installations': 0.6.17(@firebase/app@0.13.0) + '@firebase/installations-compat': 0.2.17(@firebase/app-compat@0.4.0)(@firebase/app-types@0.9.3)(@firebase/app@0.13.0) + '@firebase/messaging': 0.12.21(@firebase/app@0.13.0) + '@firebase/messaging-compat': 0.2.21(@firebase/app-compat@0.4.0)(@firebase/app@0.13.0) + '@firebase/performance': 0.7.6(@firebase/app@0.13.0) + '@firebase/performance-compat': 0.2.19(@firebase/app-compat@0.4.0)(@firebase/app@0.13.0) + '@firebase/remote-config': 0.6.4(@firebase/app@0.13.0) + '@firebase/remote-config-compat': 0.2.17(@firebase/app-compat@0.4.0)(@firebase/app@0.13.0) + '@firebase/storage': 0.13.12(@firebase/app@0.13.0) + '@firebase/storage-compat': 0.3.22(@firebase/app-compat@0.4.0)(@firebase/app-types@0.9.3)(@firebase/app@0.13.0) + '@firebase/util': 1.12.0 + transitivePeerDependencies: + - '@react-native-async-storage/async-storage' + flat-cache@4.0.1: dependencies: flatted: 3.3.3 @@ -5861,6 +6581,8 @@ snapshots: gensync@1.0.0-beta.2: {} + get-caller-file@2.0.5: {} + get-intrinsic@1.3.0: dependencies: call-bind-apply-helpers: 1.0.2 @@ -5958,6 +6680,8 @@ snapshots: html5-qrcode@2.3.8: {} + http-parser-js@0.5.10: {} + idb@7.1.1: {} ignore@5.3.2: {} @@ -6208,6 +6932,8 @@ snapshots: dependencies: p-locate: 5.0.0 + lodash.camelcase@4.3.0: {} + lodash.debounce@4.0.8: {} lodash.isequal@4.5.0: {} @@ -6218,6 +6944,8 @@ snapshots: lodash@4.17.21: {} + long@5.3.2: {} + loose-envify@1.4.0: dependencies: js-tokens: 4.0.0 @@ -6497,6 +7225,21 @@ snapshots: object-assign: 4.1.1 react-is: 16.13.1 + protobufjs@7.5.3: + 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': 22.15.21 + long: 5.3.2 + proxy-from-env@1.1.0: {} punycode@2.3.1: {} @@ -6567,6 +7310,11 @@ snapshots: optionalDependencies: react-dom: 19.1.0(react@19.1.0) + react-ios-pwa-prompt@2.0.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0): + dependencies: + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + react-is@16.13.1: {} react-is@17.0.2: {} @@ -6667,6 +7415,8 @@ snapshots: dependencies: jsesc: 3.0.2 + require-directory@2.1.1: {} + require-from-string@2.0.2: {} resolve-from@4.0.0: {} @@ -7183,10 +7933,20 @@ snapshots: dependencies: loose-envify: 1.4.0 + web-vitals@4.2.4: {} + webidl-conversions@4.0.2: {} webpack-virtual-modules@0.6.2: {} + websocket-driver@0.7.4: + dependencies: + http-parser-js: 0.5.10 + safe-buffer: 5.2.1 + websocket-extensions: 0.1.4 + + websocket-extensions@0.1.4: {} + whatwg-url@7.1.0: dependencies: lodash.sortby: 4.7.0 @@ -7369,10 +8129,24 @@ snapshots: ws@8.18.2: {} + y18n@5.0.8: {} + yallist@3.1.1: {} yaml@2.8.0: {} + yargs-parser@21.1.1: {} + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + yocto-queue@0.1.0: {} zod@3.25.30: {} diff --git a/public/firebase-messaging-sw.js b/public/firebase-messaging-sw.js new file mode 100644 index 0000000..b55694a --- /dev/null +++ b/public/firebase-messaging-sw.js @@ -0,0 +1,76 @@ +// public/firebase-messaging-sw.js + +self.addEventListener("install", (e) => { + console.log("[FCM SW] Install"); + self.skipWaiting(); +}); + +self.addEventListener("activate", (e) => { + console.log("[FCM SW] Activate"); +}); + +self.addEventListener("push", (event) => { + if (!event.data) { + console.error("[FCM SW] Push event has no data."); + return; + } + + const data = event.data.json(); + console.log("[FCM SW] Push received:", data); + + const unreadCount = Number(data.data?.unread_count || 0); + const clickAction = data.data?.click_action || "/"; + + const notificationPromise = self.registration.showNotification( + data.notification.title, + { + body: data.notification.body, + image: data.notification?.image, + icon: data.notification.icon || "/logo-192x192.png", // fallback icon + badge: data.notification.badge || "/logo-192x192.png", + data: { click_action: clickAction }, + }, + ); + + const updateBadgePromise = + "setAppBadge" in navigator + ? navigator.setAppBadge(unreadCount).catch(console.error) + : Promise.resolve(); + + const postMessagePromise = self.clients + .matchAll({ type: "window", includeUncontrolled: true }) + .then((clients) => + clients.forEach((client) => + client.postMessage({ type: "updateBadge", count: unreadCount }), + ), + ); + + event.waitUntil( + Promise.all([notificationPromise, updateBadgePromise, postMessagePromise]), + ); +}); + +self.addEventListener("notificationclick", (event) => { + const clickURL = event.notification.data?.click_action; + + event.notification.close(); + + if (!clickURL) { + return; + } + + event.waitUntil( + self.clients + .matchAll({ type: "window", includeUncontrolled: true }) + .then((clientList) => { + for (const client of clientList) { + if (client.url === clickURL && "focus" in client) { + return client.focus(); + } + } + if (self.clients.openWindow) { + return self.clients.openWindow(clickURL); + } + }), + ); +}); diff --git a/src/App.tsx b/src/App.tsx index 0c4c38d..ed5300b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -16,13 +16,19 @@ import RoleRoute from "./routes/RoleRoute.tsx"; import FullScreenLoading from "./components/common/FullScreenLoading.tsx"; import HomeBoss from "./pages/home/boss/HomeBoss.tsx"; import HomeStaff from "./pages/home/staff/HomeStaff.tsx"; +import HomeAdmin from "./pages/home/admin/HomeAdmin.tsx"; +import UnifiedPWAPrompt from "./libs/fcm/UnifiedPWAPrompt.tsx"; +import PdfViewerPage from "./pages/PdfViewerPage.tsx"; +import SubscribePage from "./pages/mypage/boss/SubscribePage.tsx"; +import SuccessPage from "./pages/mypage/boss/SuccessPage.tsx"; +import FailPage from "./pages/mypage/boss/FailPage.tsx"; // Lazy-loaded components (boss) const Landing = lazy(() => import("./pages/landing/Landing.tsx")); const Schedule = lazy(() => import("./pages/schedule/boss/Schedule.tsx")); const Employees = lazy(() => import("./pages/employee/boss/Employees.tsx")); -const Task = lazy(() => import("./pages/Task")); -const Store = lazy(() => import("./pages/store/boss/Store.tsx")); +const Task = lazy(() => import("./pages/task/boss/TaskPage.tsx")); +const Store = lazy(() => import("./pages/mypage/boss/BossStorePage.tsx")); const StoreRegisterBossPage = lazy( () => import("./pages/store/boss/StoreRegisterBossPage.tsx"), ); @@ -33,13 +39,13 @@ const StoreInfoEditPage = lazy( () => import("./pages/store/boss/StoreInfoEditPage.tsx"), ); const AttendanceSettingPage = lazy( - () => import("./pages/store/boss/AttendanceSettingPage.tsx"), + () => import("./pages/mypage/boss/AttendanceSettingPage.tsx"), ); -const SalarySettingPage = lazy( - () => import("./pages/store/boss/SalarySettingPage.tsx"), +const PayrollSettingPage = lazy( + () => import("./pages/mypage/boss/PayrollSettingPage.tsx"), ); const NotificationSettingPage = lazy( - () => import("./pages/store/boss/NotificationSettingPage.tsx"), + () => import("./pages/mypage/boss/NotificationSettingPage.tsx"), ); const ContractViewPage = lazy( () => import("./pages/contract/boss/ContractViewPage.tsx"), @@ -65,14 +71,37 @@ const ContractTemplateEditPage = lazy( const BossPayrollPage = lazy( () => import("./pages/payroll/boss/BossPayrollPage.tsx"), ); -const BossPayrollEditPage = lazy( - () => import("./pages/payroll/boss/BossPayrollEditPage.tsx"), +const BossAutoTransferEditPage = lazy( + () => + import("./pages/payroll/boss/autoTransfer/BossAutoTransferEditPage.tsx"), +); +const BossPayslipPage = lazy( + () => import("./pages/payroll/boss/history/BossPayslipPage.tsx"), +); +const AccountRegisterPage = lazy( + () => import("./pages/mypage/boss/AccountRegisterPage.tsx"), +); +const BossAlarmPage = lazy( + () => import("./pages/alarm/boss/BossAlarmPage.tsx"), +); +const TaskBossDetailPage = lazy( + () => import("./pages/task/boss/checklist/TaskDetailPage.tsx"), +); +const TaskRoutinePage = lazy( + () => import("./pages/task/boss/checklist/TaskRoutinePage.tsx"), +); +const BossReportDetailPage = lazy( + () => import("./pages/task/boss/report/BossReportDetailPage.tsx"), ); // Lazy-loaded components (staff) const ScheduleStaff = lazy( () => import("./pages/schedule/staff/ScheduleStaff.tsx"), ); +const TaskStaff = lazy(() => import("./pages/task/staff/StaffTaskPage.tsx")); +const StaffChecklistDetailPage = lazy( + () => import("./pages/task/staff/checklist/StaffChecklistDetailPage.tsx"), +); const StoreRegisterIntro = lazy( () => import("./pages/store/staff/StoreRegisterIntro.tsx"), ); @@ -89,6 +118,21 @@ const StaffMyPage = lazy(() => import("./pages/mypage/staff/StaffMyPage.tsx")); const StaffDocumentPage = lazy( () => import("./pages/document/staff/StaffDocumentPage.tsx"), ); +const StaffPayslipPage = lazy( + () => import("./pages/payroll/staff/StaffPayslipPage.tsx"), +); +const StaffPayrollPage = lazy( + () => import("./pages/payroll/staff/StaffPayrollPage.tsx"), +); +const StaffAccountRegisterPage = lazy( + () => import("./pages/mypage/staff/StaffAccountRegisterPage.tsx"), +); +const StaffAlarmPage = lazy( + () => import("./pages/alarm/staff/StaffAlarmPage.tsx"), +); +const StaffReportDetailPage = lazy( + () => import("./pages/task/staff/report/StaffReportDetailPage.tsx"), +); function App() { return ( @@ -103,6 +147,7 @@ function App() { draggable theme="light" /> + }> } /> @@ -118,6 +163,7 @@ function App() { {/* COMMON Routes */} } /> } /> + } /> {/* BOSS Routes */} }> } /> @@ -128,6 +174,15 @@ function App() { element={} /> } /> + } + /> + } /> + } + /> } /> } /> } + path="boss/store/payroll-setting" + element={} + /> + } /> } /> } /> + } + /> } + element={} /> + } /> + } /> + } + /> + } /> {/* STAFF Routes */} }> } /> } /> - } /> - } /> + } /> + } + /> + } + /> + } /> + } + /> } /> } /> } /> + } + /> } @@ -202,6 +288,12 @@ function App() { path="staff/attendance" element={} /> + } /> + + + {/* ADMIN Routes */} + }> + } /> diff --git a/src/api/admin/admin.ts b/src/api/admin/admin.ts new file mode 100644 index 0000000..9bf3dd5 --- /dev/null +++ b/src/api/admin/admin.ts @@ -0,0 +1,12 @@ +import axiosAdmin from "../common/axiosAdmin.ts"; +import { + BossStatisticsItem, + BossStatisticsResponse, +} from "../../types/admin.ts"; + +export const fetchBossStatistics = async (): Promise => { + const response = await axiosAdmin.get( + "/api/admin/dashboard/statistics/boss", + ); + return response.data.result; +}; diff --git a/src/api/boss/alarm.ts b/src/api/boss/alarm.ts new file mode 100644 index 0000000..2969d81 --- /dev/null +++ b/src/api/boss/alarm.ts @@ -0,0 +1,106 @@ +import { + AttendanceEditRequest, + SubstituteRequest, + NotificationItem, +} from "../../types/notification"; +import axiosAuth from "../common/axiosAuth.ts"; + +/** + * 대타 근무 요청 목록 조회 (사장님용) + * @param storeId 매장 ID + * @returns 대타 요청 리스트 + */ +export const fetchSubstituteRequests = async ( + storeId: number, +): Promise => { + const res = await axiosAuth.get( + `/api/boss/stores/${storeId}/schedules/substitutions`, + ); + return res.data.result; +}; + +/** + * 대타 근무 요청 승인 (사장님용) + * @param storeId 매장 ID + * @param substitutionId 대타 요청 ID + * @returns 성공 여부 + */ +export const approveSubstituteRequest = async ( + storeId: number, + substitutionId: number, +): Promise => { + await axiosAuth.post( + `/api/boss/stores/${storeId}/schedules/substitutions/${substitutionId}/approve`, + ); +}; + +/** + * 대타 근무 요청 거절 (사장님용) + * @param storeId 매장 ID + * @param substitutionId 대타 요청 ID + * @returns 성공 여부 + */ +export const rejectSubstituteRequest = async ( + storeId: number, + substitutionId: number, +): Promise => { + await axiosAuth.post( + `/api/boss/stores/${storeId}/schedules/substitutions/${substitutionId}/reject`, + ); +}; + +/** + * 근태 수정 요청 목록 조회 (사장님용) + * @param storeId 매장 ID + * @returns 근태 수정 요청 리스트 + */ +export const fetchAttendanceEditRequests = async ( + storeId: number, +): Promise => { + const res = await axiosAuth.get( + `/api/boss/stores/${storeId}/schedules/attendance-edits`, + ); + return res.data.result; +}; + +/** + * 근태 수정 요청 승인 (사장님용) + * @param storeId 매장 ID + * @param editId 근태 수정 요청 ID + * @returns 성공 여부 + */ +export const approveAttendanceEditRequest = async ( + storeId: number, + editId: number, +): Promise => { + await axiosAuth.post( + `/api/boss/stores/${storeId}/schedules/attendance-edits/${editId}/approve`, + ); +}; + +/** + * 근태 수정 요청 거절 (사장님용) + * @param storeId 매장 ID + * @param editId 근태 수정 요청 ID + * @returns 성공 여부 + */ +export const rejectAttendanceEditRequest = async ( + storeId: number, + editId: number, +): Promise => { + await axiosAuth.post( + `/api/boss/stores/${storeId}/schedules/attendance-edits/${editId}/reject`, + ); +}; + +/** + * 사장 알림 목록 조회 + * @param storeId 매장 ID + * @returns 알림 리스트 + */ +export const fetchBossNotifications = async ( + storeId: number, +): Promise => { + const res = await axiosAuth.get(`/api/boss/stores/${storeId}/notifications`); + return res.data.result; +}; diff --git a/src/api/boss/payment.ts b/src/api/boss/payment.ts new file mode 100644 index 0000000..db959e7 --- /dev/null +++ b/src/api/boss/payment.ts @@ -0,0 +1,28 @@ +// src/api/payment.ts +import axiosAuth from "../common/axiosAuth.ts"; + +export const registerBillingKey = async ( + authKey: string, + customerKey: string, +) => { + return axiosAuth.post("/api/payments/billing/issue", { + authKey, + customerKey, + }); +}; + +export const fetchCustomerKey = async () => { + const response = await axiosAuth.get("/api/payments/customer-key"); + return response.data; +}; + +export const fetchPaymentMethod = async () => { + const response = await axiosAuth.get<{ + cardCompany: string | null; + cardNumber: string | null; + cardType: "체크" | "신용" | null; + ownerType: "개인" | "법인" | null; + }>("/api/payments"); + + return response.data; +}; diff --git a/src/api/boss/payroll.ts b/src/api/boss/payroll.ts index 73cf17d..ad158cc 100644 --- a/src/api/boss/payroll.ts +++ b/src/api/boss/payroll.ts @@ -1,18 +1,79 @@ -import axiosAuth from "../common/axiosAuth.ts"; +import axiosAuth from "../common/axiosAuth"; import { - ConfirmPayrollTransfersResponse, - PayrollSettingsResponse, - StaffPayroll, -} from "../../types/payroll.ts"; + AccountVerificationRequest, + AccountVerificationResponse, + ConfirmedTransferItem, + ConfirmPayrollTargetsRequest, + EstimatedPayrollItem, + MonthlyPayrollItem, + PayrollDetailResponse, + PayrollSettingsRequest, + BossPayrollSettingsResponse, + PayslipDownloadLink, + StaffHourlyWage, + StaffWithholdingItem, + UpdateHourlyWageRequest, + UpdateWithholdingRequest, +} from "../../types/payroll"; /** - * 전체 알바생 예상 월급 목록 조회 (사장님용) - * @param storeId 매장 ID - * @returns 알바생들의 급여 정보 목록 + * 계좌 정보 본인 인증 (등록) + * POST /api/boss/stores/{storeId}/payrolls/account-verification */ -export const fetchStaffPayrolls = async ( +export const verifyAccountInfo = async ( storeId: number, -): Promise => { + payload: AccountVerificationRequest, +): Promise => { + const response = await axiosAuth.post( + `/api/boss/stores/${storeId}/payrolls/account-verification`, + payload, + ); + return response.data; +}; + +/** + * 계좌 정보 삭제 + * DELETE /api/boss/stores/{storeId}/payrolls/account + */ +export const deleteAccountInfo = async (storeId: number): Promise => { + await axiosAuth.delete(`/api/boss/stores/${storeId}/payrolls/account`); +}; + +/** + * 매장의 급여 정보 설정 + * POST /api/boss/stores/{storeId}/payrolls/settings + */ +export const updatePayrollSettings = async ( + storeId: number, + payload: PayrollSettingsRequest, +): Promise => { + await axiosAuth.post( + `/api/boss/stores/${storeId}/payrolls/settings`, + payload, + ); +}; + +/** + * 급여 설정 정보 조회 + * GET /api/boss/stores/{storeId}/payrolls/settings + */ +export const fetchPayrollSettings = async ( + storeId: number, +): Promise => { + const response = await axiosAuth.get( + `/api/boss/stores/${storeId}/payrolls/settings`, + ); + return response.data; +}; + +// 자동송금 페이지 +/** + * 전체 알바생 예상월급 목록 조회 + * GET /api/boss/stores/{storeId}/payrolls/staffs + */ +export const fetchEstimatedPayrolls = async ( + storeId: number, +): Promise => { const response = await axiosAuth.get( `/api/boss/stores/${storeId}/payrolls/staffs`, ); @@ -20,46 +81,133 @@ export const fetchStaffPayrolls = async ( }; /** - * 급여 설정 정보 조회 (사장님용) - * @param storeId 매장 ID - * @returns 급여 설정 데이터 + * 지급 대상 확정 + * POST /api/boss/stores/{storeId}/payrolls/staffs */ -export const getPayrollSettings = async ( +export const confirmPayrollTargets = async ( storeId: number, -): Promise => { - const res = await axiosAuth.get( - `/api/boss/stores/${storeId}/payrolls/settings`, + payload: ConfirmPayrollTargetsRequest, +): Promise => { + await axiosAuth.post(`/api/boss/stores/${storeId}/payrolls/staffs`, payload); +}; + +/** + * 확정된 송금 목록 조회 + * GET /api/boss/stores/{storeId}/payrolls/confirm + */ +export const fetchConfirmedTransfers = async ( + storeId: number, +): Promise => { + const response = await axiosAuth.get( + `/api/boss/stores/${storeId}/payrolls/confirm`, ); - return res.data; + return response.data.result; }; /** - * 알바생 송금 확정 요청 - * @param storeId 매장 ID - * @param payrollKeys 확정할 급여 key 리스트 - * @returns 확정된 알바생 급여 목록 + * 급여내역 목록 조회 (월별) + * GET /api/boss/stores/{storeId}/payrolls?month=YYYY-MM */ -export const confirmPayrollTransfers = async ( +export const fetchMonthlyPayrolls = async ( storeId: number, - payrollKeys: string[], -): Promise => { - const response = await axiosAuth.post( - `/api/boss/stores/${storeId}/payrolls/staffs`, - { payrollKeys }, + month: string, // 예: "2025-03" +): Promise => { + const response = await axiosAuth.get(`/api/boss/stores/${storeId}/payrolls`, { + params: { month }, + }); + return response.data.result; +}; + +// 확정된 급여명세서 조회 +export const fetchPayrollDetail = async ( + storeId: number, + payrollId: number, +): Promise => { + const response = await axiosAuth.get( + `/api/boss/stores/${storeId}/payrolls/${payrollId}`, + ); + return response.data; +}; + +// 비송금 대상 급여명세서 조회 +export const fetchUnconfirmedPayrollDetail = async ( + storeId: number, + staffId: number, + month: string, +): Promise => { + const response = await axiosAuth.get( + `/api/boss/stores/${storeId}/payrolls/staffs/${staffId}`, + { params: { month } }, + ); + return response.data; +}; + +/** + * 급여명세서 다운로드 링크 조회 + * GET /api/boss/stores/{storeId}/payrolls/payslip/{payslipId} + */ +export const fetchPayslipDownloadLink = async ( + storeId: number, + payslipId: number, +): Promise => { + const response = await axiosAuth.get( + `/api/boss/stores/${storeId}/payrolls/payslip/${payslipId}`, ); return response.data; }; /** - * 확정된 알바생 송금 목록 조회 - * @param storeId 매장 ID - * @returns 확정된 급여 정보 배열 + * 알바생 공제항목 목록 조회 + * GET /api/boss/stores/{storeId}/staffs/withholding */ -export const fetchConfirmedPayrolls = async ( +export const getStaffWithholdingList = async ( storeId: number, -): Promise => { +): Promise => { + const res = await axiosAuth.get( + `/api/boss/stores/${storeId}/staffs/withholding`, + ); + return res.data.result; +}; + +/** + * 알바생 공제항목 수정 + * PUT /api/boss/stores/{storeId}/staffs/{staffId}/withholding + */ +export const updateStaffWithholding = async ( + storeId: number, + staffId: number, + payload: UpdateWithholdingRequest, +): Promise => { + await axiosAuth.put( + `/api/boss/stores/${storeId}/staffs/${staffId}/withholding`, + payload, + ); +}; + +/** + * 알바생 시급 목록 조회 + * GET /api/boss/stores/{storeId}/staffs/hourly-wage + */ +export const fetchHourlyWageList = async ( + storeId: number, +): Promise => { const response = await axiosAuth.get( - `/api/boss/stores/${storeId}/payrolls/staffs/confirm`, + `/api/boss/stores/${storeId}/staffs/hourly-wage`, ); return response.data.result; }; + +/** + * 알바생 시급 설정 + * PUT /api/boss/stores/{storeId}/staffs/{staffId}/hourly-wage + */ +export const updateStaffHourlyWage = async ( + storeId: number, + staffId: number, + payload: UpdateHourlyWageRequest, +): Promise => { + await axiosAuth.put( + `/api/boss/stores/${storeId}/staffs/${staffId}/hourly-wage`, + payload, + ); +}; diff --git a/src/api/boss/report.ts b/src/api/boss/report.ts new file mode 100644 index 0000000..54c40d2 --- /dev/null +++ b/src/api/boss/report.ts @@ -0,0 +1,25 @@ +import { WorkReportItem } from "../../types/report.ts"; +import axiosAuth from "../common/axiosAuth.ts"; + +export const getBpssWorkReportsByDate = async ( + storeId: number, + date: string, // YYYY-MM-DD +): Promise => { + const response = await axiosAuth.get( + `/api/boss/stores/${storeId}/work-reports`, + { + params: { date }, + }, + ); + return response.data.result; +}; + +export const getBossWorkReportDetail = async ( + storeId: number, + workReportId: number, +): Promise => { + const response = await axiosAuth.get( + `/api/boss/stores/${storeId}/work-reports/${workReportId}`, + ); + return response.data; +}; diff --git a/src/api/boss/staff.ts b/src/api/boss/staff.ts index 5e09038..956fd94 100644 --- a/src/api/boss/staff.ts +++ b/src/api/boss/staff.ts @@ -74,3 +74,14 @@ export const getStaffAttendancesList = async ( ); return response.data.result; }; + +// 알바생 삭제 (퇴사 처리) +export const deleteStaff = async ( + storeId: number, + staffId: number, +): Promise => { + const response = await axiosAuth.delete( + `/api/boss/stores/${storeId}/staffs/${staffId}`, + ); + return response.data; +}; diff --git a/src/api/boss/store.ts b/src/api/boss/store.ts index f31d732..bee3ee0 100644 --- a/src/api/boss/store.ts +++ b/src/api/boss/store.ts @@ -6,7 +6,6 @@ import { GpsSettings, QrCodeSettings, ReissueInviteCodeResponse, - StoreInfo, StoreSummaryBoss, UpdateStoreInfoRequest, } from "../../types/store.ts"; @@ -27,12 +26,21 @@ export const registerStore = async (store: { latitude: number; longitude: number; }; -}) => { - return await axiosAuth.post("/api/boss/stores", store); +}): Promise<{ storeId: number }> => { + const response = await axiosAuth.post("/api/boss/stores", store); + return response.data; +}; + +// 매장 삭제 +export const deleteStore = async (storeId: number): Promise => { + const response = await axiosAuth.delete(`/api/boss/stores/${storeId}`); + return response.data; }; //특정 매장 정보 가져오기 -export const getStoreInfo = async (storeId: number): Promise => { +export const getStoreInfo = async ( + storeId: number, +): Promise => { const response = await axiosAuth.get( `/api/boss/stores/${storeId}/store-info`, ); @@ -131,3 +139,13 @@ export const updateGpsSettings = async ( ); return response.data; }; + +import { StoreRequestSummary } from "../../types/store.ts"; + +// 매장 요청수 및 알바 프로필 이미지 일부 조회 +export const getStoreRequestSummary = async ( + storeId: number, +): Promise => { + const response = await axiosAuth.get(`/api/boss/stores/${storeId}/requests`); + return response.data; +}; diff --git a/src/api/boss/subscription.ts b/src/api/boss/subscription.ts new file mode 100644 index 0000000..87bb9c0 --- /dev/null +++ b/src/api/boss/subscription.ts @@ -0,0 +1,28 @@ +// src/api/subscription.ts +import axiosAuth from "../common/axiosAuth.ts"; +import { + SubscriptionInfo, + SubscriptionOrderHistory, +} from "../../types/subscription.ts"; + +export const fetchSubscriptionInfo = async (): Promise => { + const response = await axiosAuth.get("/api/subscription"); + return response.data; +}; + +export const createSubscription = async (planType: "PREMIUM") => { + return axiosAuth.post("/api/subscription", { planType }); +}; + +export const deleteSubscription = async () => { + return axiosAuth.delete("/api/subscription"); +}; + +export const fetchSubscriptionOrderHistory = async (): Promise< + SubscriptionOrderHistory[] +> => { + const response = await axiosAuth.get<{ result: SubscriptionOrderHistory[] }>( + "/api/subscription/order-history", + ); + return response.data.result; +}; diff --git a/src/api/boss/task.ts b/src/api/boss/task.ts new file mode 100644 index 0000000..79be800 --- /dev/null +++ b/src/api/boss/task.ts @@ -0,0 +1,110 @@ +import axiosAuth from "../common/axiosAuth"; +import { + SingleTaskRequest, + TaskRoutineRequest, + ReferenceImageUploadUrlResponse, + TaskRoutine, + TaskStatus, +} from "../../types/task"; + +export const BossTaskAPI = { + // 단일 업무 생성 + createSingleTask: async ( + storeId: number, + taskData: SingleTaskRequest, + ): Promise => { + await axiosAuth.post(`/api/boss/stores/${storeId}/tasks`, taskData); + }, + + // 반복 업무 생성 + createTaskRoutine: async ( + storeId: number, + taskData: TaskRoutineRequest, + ): Promise => { + await axiosAuth.post( + `/api/boss/stores/${storeId}/tasks/task-routines`, + taskData, + ); + }, + + // 참고 사진 업로드 presignedURL 요청 + getReferenceImageUploadUrl: async ( + storeId: number, + extension: string, + contentType: string, + ): Promise => { + const response = await axiosAuth.get( + `/api/boss/stores/${storeId}/tasks/reference-image/upload-url`, + { + params: { + extension, + contentType, + }, + }, + ); + return response.data; + }, + + // 참고 사진 업로드 + uploadReferenceImage: async ( + uploadUrl: string, + file: File, + ): Promise => { + await fetch(uploadUrl, { + method: "PUT", + body: file, + headers: { + "Content-Type": file.type, + }, + }); + }, + + // 날짜별 업무 상태 조회 + getTasksByDate: async ( + storeId: number, + date: string, + ): Promise => { + const response = await axiosAuth.get(`/api/boss/stores/${storeId}/tasks`, { + params: { date }, + }); + return response.data.result; + }, + + // 업무 상세 조회 + getTaskDetail: async ( + storeId: number, + taskId: number, + ): Promise => { + const response = await axiosAuth.get( + `/api/boss/stores/${storeId}/tasks/${taskId}`, + ); + return response.data; + }, + + // 반복 업무 목록 조회 + getTaskRoutines: async (storeId: number): Promise => { + const response = await axiosAuth.get( + `/api/boss/stores/${storeId}/tasks/task-routines`, + ); + return response.data.result; + }, + + // 반복 업무 삭제 + deleteTaskRoutine: async ( + storeId: number, + taskRoutineId: number, + deleteOption: "ALL" | "PENDING", + ): Promise => { + await axiosAuth.delete( + `/api/boss/stores/${storeId}/tasks/task-routines/${taskRoutineId}`, + { + params: { deleteOption }, + }, + ); + }, + + // 특정 업무 삭제 + deleteTask: async (storeId: number, taskId: number): Promise => { + await axiosAuth.delete(`/api/boss/stores/${storeId}/tasks/${taskId}`); + }, +}; diff --git a/src/api/common/auth.ts b/src/api/common/auth.ts index 52a139d..355aae6 100644 --- a/src/api/common/auth.ts +++ b/src/api/common/auth.ts @@ -23,3 +23,11 @@ export const signup = async ( }); return res.data; }; + +export const logoutFromServer = async (refreshToken: string): Promise => { + await axiosAuth.post("/api/auth/logout", null, { + headers: { + "X-Refresh-Token": refreshToken, + }, + }); +}; diff --git a/src/api/common/axiosAdmin.ts b/src/api/common/axiosAdmin.ts new file mode 100644 index 0000000..addc35a --- /dev/null +++ b/src/api/common/axiosAdmin.ts @@ -0,0 +1,39 @@ +// src/api/axiosAdmin.ts +import axios from "axios"; +import { toast } from "react-toastify"; + +const axiosAdmin = axios.create({ + baseURL: import.meta.env.VITE_ADMIN_API_URL, + headers: { + "Content-Type": "application/json", + }, + timeout: 5000, +}); + +axiosAdmin.interceptors.request.use((config) => { + const token = localStorage.getItem("accessToken"); + if (token && config.headers) { + config.headers.Authorization = `Bearer ${token}`; + } + return config; +}); + +axiosAdmin.interceptors.response.use( + (response) => response, + (error) => { + if (axios.isAxiosError(error)) { + const message = error.response?.data?.message; + if (message) { + toast.error(message); + } else { + toast.error("예상치 못한 오류가 발생했습니다."); + } + } else { + toast.error("네트워크 오류 또는 알 수 없는 에러입니다."); + } + + return Promise.reject(error); + }, +); + +export default axiosAdmin; diff --git a/src/api/common/user.ts b/src/api/common/user.ts index d816e9e..0844855 100644 --- a/src/api/common/user.ts +++ b/src/api/common/user.ts @@ -6,3 +6,14 @@ export const fetchUserProfile = async (): Promise => { const res = await axiosAuth.get("/api/users/me"); return res.data; }; + +/** + * FCM 토큰 등록 (알림 허용 후) + * @param fcmToken FCM 디바이스 토큰 + * @returns 성공 여부 + */ +export const registerFcmToken = async (fcmToken: string): Promise => { + await axiosAuth.post("/api/users/device-token", { + fcmToken, + }); +}; diff --git a/src/api/staff/alarm.ts b/src/api/staff/alarm.ts new file mode 100644 index 0000000..d12c73f --- /dev/null +++ b/src/api/staff/alarm.ts @@ -0,0 +1,46 @@ +import { + AttendanceEditRequest, + NotificationItem, + SubstituteRequest, +} from "../../types/notification.ts"; +import axiosAuth from "../common/axiosAuth.ts"; + +/** + * 알바생 알림 조회 + * @param storeId 매장 ID + * @returns 알림 리스트 + */ +export const fetchStaffNotifications = async ( + storeId: number, +): Promise => { + const res = await axiosAuth.get(`/api/staff/stores/${storeId}/notifications`); + return res.data.result; +}; + +/** + * 본인의 근태 수정 요청 목록 조회 (알바생용) + * @param storeId 매장 ID + * @returns 근태 수정 요청 리스트 + */ +export const fetchOwnAttendanceEditRequests = async ( + storeId: number, +): Promise => { + const res = await axiosAuth.get( + `/api/staff/stores/${storeId}/schedules/attendance-edits`, + ); + return res.data.result; +}; + +/** + * 알바생 대타 근무 요청 목록 조회 (내가 요청한 + 받은 요청 모두) + * @param storeId 매장 ID + * @returns 대타 요청 리스트 + */ +export const fetchOwnSubstituteRequests = async ( + storeId: number, +): Promise => { + const res = await axiosAuth.get( + `/api/staff/stores/${storeId}/schedules/substitutions`, + ); + return res.data.result; +}; diff --git a/src/api/staff/attendance.ts b/src/api/staff/attendance.ts index b5bf2de..d266e6f 100644 --- a/src/api/staff/attendance.ts +++ b/src/api/staff/attendance.ts @@ -1,6 +1,8 @@ import axiosAuth from "../common/axiosAuth.ts"; import { ClockInRequest, + StaffAttendanceEditRequest, + StaffAttendanceRecord, TodayScheduleWithAttendance, } from "../../types/attendance.ts"; @@ -47,3 +49,38 @@ export const getTodayScheduleAndAttendance = async ( ); return res.data.result; }; + +/** + * 알바생 본인의 월별 근태기록 조회 + * GET /api/staff/stores/{storeId}/schedules/attendances?start=YYYY-MM-DD&end=YYYY-MM-DD + */ +export const getStaffAttendanceRecords = async ( + storeId: number, + start: string, + end: string, +): Promise => { + const response = await axiosAuth.get( + `/api/staff/stores/${storeId}/schedules/attendances`, + { + params: { start, end }, + }, + ); + return response.data.result; +}; + +/** + * 알바생 근태 수정 요청 + * @param storeId 매장 ID + * @param scheduleId 스케줄 ID + * @param body 근태 수정 요청 정보 + */ +export const requestAttendanceEdit = async ( + storeId: number, + scheduleId: number, + body: StaffAttendanceEditRequest, +): Promise => { + await axiosAuth.post( + `/api/staff/stores/${storeId}/schedules/${scheduleId}/attendance-edits`, + body, + ); +}; diff --git a/src/api/staff/payroll.ts b/src/api/staff/payroll.ts index e69de29..94babb7 100644 --- a/src/api/staff/payroll.ts +++ b/src/api/staff/payroll.ts @@ -0,0 +1,105 @@ +import axiosAuth from "../common/axiosAuth.ts"; +import { + PayslipDownloadResponse, + StaffAccountInfo, + StaffPayrollBriefInfo, + StaffPayrollResponse, + StaffPayrollSettingsResponse, + VerifyAccountRequest, + VerifyAccountResponse, +} from "../../types/payroll.ts"; + +/** + * 알바생 본인의 해당 월 급여 내역 조회 + * GET /api/staff/stores/{storeId}/payrolls?month=YYYY-MM + */ +export const getStaffPayroll = async ( + storeId: number, + month: string, +): Promise => { + const response = await axiosAuth.get( + `/api/staff/stores/${storeId}/payrolls`, + { + params: { month }, + }, + ); + return response.data; +}; + +/** + * 알바생 본인의 급여명세서 PDF 다운로드 URL 조회 + * GET /api/staff/stores/{storeId}/payrolls/payslip/{payslipId} + */ +export const fetchPayslipDownloadLink = async ( + storeId: number, + payslipId: number, +): Promise => { + const response = await axiosAuth.get( + `/api/staff/stores/${storeId}/payrolls/payslip/${payslipId}`, + ); + return response.data; +}; + +/** + * 가게 급여 설정 정보 조회 + * GET /api/staff/stores/{storeId}/payrolls/settings + */ +export const getPayrollSettings = async ( + storeId: number, +): Promise => { + const response = await axiosAuth.get( + `/api/staff/stores/${storeId}/payrolls/settings`, + ); + return response.data; +}; + +/** + * 알바생 계좌 등록 (실명 인증) + * POST /api/staff/stores/{storeId}/staffs/account-verification + */ +export const verifyStaffAccount = async ( + storeId: number, + payload: VerifyAccountRequest, +): Promise => { + const response = await axiosAuth.post( + `/api/staff/stores/${storeId}/staffs/account-verification`, + payload, + ); + return response.data; +}; + +/** + * 알바생 계좌 정보 조회 + * GET /api/staff/stores/{storeId}/staffs/account + */ +export const getStaffAccount = async ( + storeId: number, +): Promise => { + const response = await axiosAuth.get( + `/api/staff/stores/${storeId}/staffs/account`, + ); + return response.data; +}; + +/** + * 알바생의 시급 및 세금유형 조회 + * GET /api/staff/stores/{storeId}/staffs/me + */ +export const getStaffPayrollInfo = async ( + storeId: number, +): Promise => { + const response = await axiosAuth.get( + `/api/staff/stores/${storeId}/staffs/my`, + ); + return response.data; +}; + +/** + * 알바생 계좌 정보 삭제 + * DELETE /api/staff/stores/{storeId}/staffs/account + */ +export const deleteStaffAccountInfo = async ( + storeId: number, +): Promise => { + await axiosAuth.delete(`/api/staff/stores/${storeId}/staffs/account`); +}; diff --git a/src/api/staff/report.ts b/src/api/staff/report.ts new file mode 100644 index 0000000..15661d9 --- /dev/null +++ b/src/api/staff/report.ts @@ -0,0 +1,68 @@ +import { + CreateWorkReportRequest, + WorkReportImageUploadUrlResponse, + WorkReportItem, +} from "../../types/report.ts"; +import axiosAuth from "../common/axiosAuth.ts"; + +// 일별 조회 +export const getStaffWorkReportsByDate = async ( + storeId: number, + date: string, +): Promise => { + const res = await axiosAuth.get(`/api/staff/stores/${storeId}/work-reports`, { + params: { date }, + }); + return res.data.result; +}; + +// 작성 +export const createWorkReport = async ( + storeId: number, + data: CreateWorkReportRequest, +): Promise => { + const res = await axiosAuth.post( + `/api/staff/stores/${storeId}/work-reports`, + data, + ); + return res.data; +}; + +// 보고사항 상세 조회 (알바용) +export const getStaffWorkReportDetail = async ( + storeId: number, + workReportId: number, +): Promise => { + const res = await axiosAuth.get( + `/api/staff/stores/${storeId}/work-reports/${workReportId}`, + ); + return res.data; +}; + +// 보고사항 이미지 presigned URL 발급 +export const getWorkReportImageUploadUrl = async ( + storeId: number, + extension: string, + contentType: string, +): Promise => { + const res = await axiosAuth.get( + `/api/staff/stores/${storeId}/work-reports/work-report-image/upload-url`, + { + params: { extension, contentType }, + }, + ); + return res.data; +}; + +export const uploadImageToPresignedUrl = async ( + uploadUrl: string, + file: File, +): Promise => { + await fetch(uploadUrl, { + method: "PUT", + headers: { + "Content-Type": file.type, + }, + body: file, + }); +}; diff --git a/src/api/staff/schedule.ts b/src/api/staff/schedule.ts index e69de29..74cffb3 100644 --- a/src/api/staff/schedule.ts +++ b/src/api/staff/schedule.ts @@ -0,0 +1,38 @@ +import axiosAuth from "../common/axiosAuth.ts"; +import { + StaffSubstitutionRequest, + SubstituteCandidate, +} from "../../types/schedule.ts"; + +/** + * 알바생 대타 근무 요청 + * @param storeId 매장 ID + * @param scheduleId 요청 대상 스케줄 ID + * @param body 대타 요청 정보 + */ +export const requestSubstitution = async ( + storeId: number, + scheduleId: number, + body: StaffSubstitutionRequest, +): Promise => { + await axiosAuth.post( + `/api/staff/stores/${storeId}/schedules/${scheduleId}/substitutions`, + body, + ); +}; + +/** + * 대타 근무 요청 후보 알바생 목록 조회 + * @param storeId 매장 ID + * @param scheduleId 스케줄 ID + * @returns 대타 근무 가능 여부와 알바생 정보 목록 + */ +export const fetchSubstituteCandidates = async ( + storeId: number, + scheduleId: number, +): Promise => { + const response = await axiosAuth.get( + `/api/staff/stores/${storeId}/schedules/${scheduleId}/substitute-candidates`, + ); + return response.data.result; +}; diff --git a/src/api/staff/store.ts b/src/api/staff/store.ts index d6e13cc..b49119e 100644 --- a/src/api/staff/store.ts +++ b/src/api/staff/store.ts @@ -19,15 +19,3 @@ export const fetchStaffStores = async (): Promise => { ); return response.data.result; }; - -/** - * 알바생 - 특정 매장의 정보 조회 - * @param storeId 매장 ID - * @returns StaffStore (storeId, storeName, address, storeType, attendanceMethod) - */ -export const getStaffStoreInfo = async ( - storeId: number, -): Promise => { - const res = await axiosAuth.get(`/api/staff/stores/${storeId}/store-info`); - return res.data; -}; diff --git a/src/api/staff/task.ts b/src/api/staff/task.ts new file mode 100644 index 0000000..03fe5b9 --- /dev/null +++ b/src/api/staff/task.ts @@ -0,0 +1,80 @@ +import { + TaskStatus, + StaffTaskListResponse, + TaskUploadUrlResponse, +} from "../../types/task"; +import axiosAuth from "../common/axiosAuth"; + +// 날짜별 업무 목록 조회 +export const getTasksByDate = async ( + storeId: number, + date: string, +): Promise => { + const response = await axiosAuth.get( + `/api/staff/stores/${storeId}/tasks?date=${date}`, + ); + return response.data.result; +}; + +// 업무 세부 조회 +export const getTaskDetail = async ( + storeId: number, + taskId: number, +): Promise => { + const response = await axiosAuth.get( + `/api/staff/stores/${storeId}/tasks/${taskId}`, + ); + return response.data; +}; + +export const completeTask = async ( + storeId: number, + taskId: number, + reportImageUrl: string | null, +): Promise => { + await axiosAuth.post( + `/api/staff/stores/${storeId}/tasks/${taskId}/complete`, + { reportImageUrl }, // ✅ 필수 요청 바디 추가 + ); +}; + +// 업무 완료 취소 +export const cancelTaskCompletion = async ( + storeId: number, + taskId: number, +): Promise => { + await axiosAuth.delete( + `/api/staff/stores/${storeId}/tasks/${taskId}/completion`, + ); +}; + +// 업무 보고 사진 업로드용 presignedUrl 발급 +export const getTaskLogImageUploadUrl = async ( + storeId: number, + extension: string, + contentType: string, +): Promise => { + const response = await axiosAuth.get( + `/api/staff/stores/${storeId}/tasks/task-log-image/upload-url`, + { + params: { + extension, + contentType, + }, + }, + ); + return response.data; +}; + +export const uploadReferenceImage = async ( + uploadUrl: string, + file: File, +): Promise => { + await fetch(uploadUrl, { + method: "PUT", + body: file, + headers: { + "Content-Type": file.type, + }, + }); +}; diff --git a/src/assets/NHBankIcon.png b/src/assets/NHBankIcon.png new file mode 100644 index 0000000..bbece5b Binary files /dev/null and b/src/assets/NHBankIcon.png differ diff --git a/src/components/common/Button.tsx b/src/components/common/Button.tsx index 93e604a..1220612 100644 --- a/src/components/common/Button.tsx +++ b/src/components/common/Button.tsx @@ -9,7 +9,7 @@ const buttonVariants = cva( xl: "h-14 px-6 py-[0.906rem] title-1", lg: "h-14 px-6 py-[0.906rem] body-1", md: "h-12 px-5 py-3 title-2", - sm: "h-11 px-4 py-2.5 body-2", + sm: "h-11 px-2 py-1 body-2", icon_xl: "h-14 w-14 p-4 [&_#icon]:h-6 [&_#icon]:w-6", icon_lg: "h-14 w-14 p-4 [&_#icon]:h-6 [&_#icon]:w-6", icon_md: "h-12 w-12 p-3 [&_#icon]:h-6 [&_#icon]:w-6", @@ -27,13 +27,14 @@ const buttonVariants = cva( ghost2: "", outline: "", text: "", + danger: "", }, }, compoundVariants: [ //state == disabled { state: "disabled", - theme: ["secondary", "primary"], + theme: ["secondary", "primary", "danger"], className: "bg-grayscale-300 text-white", }, { @@ -57,6 +58,11 @@ const buttonVariants = cva( theme: "secondary", className: "bg-secondary-900 text-white", }, + { + state: "active", + theme: "danger", + className: "bg-red-800 text-white", + }, { state: "active", theme: "ghost", @@ -89,6 +95,11 @@ const buttonVariants = cva( theme: "secondary", className: "bg-secondary-900 text-white", }, + { + state: "default", + theme: "danger", + className: "bg-red-600 text-white", + }, { state: "default", theme: "ghost", @@ -124,7 +135,14 @@ export interface ButtonProps extends React.ComponentProps<"button"> { | "icon_md" | "icon_sm"; state?: "default" | "active" | "disabled"; - theme?: "primary" | "secondary" | "ghost" | "ghost2" | "outline" | "text"; + theme?: + | "primary" + | "secondary" + | "ghost" + | "ghost2" + | "outline" + | "text" + | "danger"; icon?: React.ReactNode; } diff --git a/src/components/common/Checkbox.tsx b/src/components/common/Checkbox.tsx index e7dec66..578e76a 100644 --- a/src/components/common/Checkbox.tsx +++ b/src/components/common/Checkbox.tsx @@ -1,14 +1,16 @@ import { cn } from "../../libs"; import { CheckboxFilled, CheckboxOn } from "../icons/CheckboxIcon.tsx"; +import Spinner from "../common/Spinner.tsx"; interface CheckboxProps extends React.ComponentProps<"div"> { - checked?: boolean; // ✅ boolean | undefined 허용 + checked?: boolean; onChange: (e: React.ChangeEvent) => void; label: string; description?: string; required?: boolean; optional?: boolean; icon?: React.ReactNode; + isLoading?: boolean; } const Checkbox: React.FC = ({ @@ -20,6 +22,7 @@ const Checkbox: React.FC = ({ optional, icon, description, + isLoading = false, // 기본값 false ...props }) => { return ( @@ -32,16 +35,21 @@ const Checkbox: React.FC = ({ onChange={onChange} className="sr-only" /> - {checked ? ( + + {isLoading ? ( + + ) : checked ? ( ) : ( )} + {label} + {required && ( (필수) )} @@ -53,6 +61,7 @@ const Checkbox: React.FC = ({ {icon} + {description && ( {description} diff --git a/src/components/common/FileDropzone.tsx b/src/components/common/FileDropzone.tsx index f540d09..7f0f5f3 100644 --- a/src/components/common/FileDropzone.tsx +++ b/src/components/common/FileDropzone.tsx @@ -1,11 +1,11 @@ import { useCallback } from "react"; import { useDropzone } from "react-dropzone"; -import { UploadCloud } from "lucide-react"; +import { UploadCloud, X } from "lucide-react"; import { cn } from "../../libs"; interface FileDropzoneProps { file: File | null; - onChange: (file: File) => void; + onChange: (file: File | null) => void; // 삭제를 위해 null 허용 placeholder?: string; } @@ -22,26 +22,55 @@ const FileDropzone = ({ file, onChange, placeholder }: FileDropzoneProps) => { const { getRootProps, getInputProps, isDragActive } = useDropzone({ onDrop, multiple: false, + maxFiles: 1, + noClick: !!file, // 파일이 있으면 클릭 방지 + noDrag: !!file, // 파일이 있으면 드래그 방지 }); + const isImage = file?.type.startsWith("image/"); + return ( -
- - - {file ? ( -

{file.name}

- ) : ( -

- {placeholder ?? "여기에 파일을 드래그하거나 클릭해서 업로드하세요."} -

+
+
+ + {file ? ( + isImage ? ( + preview + ) : ( +

{file.name}

+ ) + ) : ( + <> + +

+ {placeholder ?? + "여기에 파일을 드래그하거나 클릭해서 업로드하세요."} +

+ + )} +
+ + {file && ( + )}
); diff --git a/src/components/common/Label.tsx b/src/components/common/Label.tsx index bd78c56..8e4203f 100644 --- a/src/components/common/Label.tsx +++ b/src/components/common/Label.tsx @@ -9,13 +9,14 @@ const labelVariants = cva( theme: { indigo: "bg-secondary-900 text-white", solid: "bg-primary-600 text-white", - lightsolid: "bg-primary-100 text-primary-600", + lightsolid: "bg-white text-primary-900 border-2 border-primary-300", + lightsecond: "bg-white text-secondary-900 border border-secondary-300", graysolid: "bg-grayscale-100 text-grayscale-900", - ghost: "border border-grayscale-500 text-grayscale-900", + ghost: "border border-grayscale-500 text-grayscale-800", }, size: { lg: "h-8 px-3 py-[0.438rem] text-sm font-medium", - sm: "h-6 px-2 py-1 text-xs font-medium", + sm: "h-6 px-2 py-1 body-3", }, }, defaultVariants: { @@ -26,7 +27,13 @@ const labelVariants = cva( ); export interface LabelProps extends ComponentProps<"div"> { - theme?: "indigo" | "solid" | "lightsolid" | "graysolid" | "ghost"; + theme?: + | "indigo" + | "solid" + | "lightsolid" + | "lightsecond" + | "graysolid" + | "ghost"; size?: "lg" | "sm"; icon?: ReactNode; } @@ -40,7 +47,11 @@ export default function Label({ ...props }: LabelProps) { return ( -
+
{icon && {icon}}

{children}

diff --git a/src/components/common/Modal.tsx b/src/components/common/Modal.tsx index ff2a6ec..1a0afc7 100644 --- a/src/components/common/Modal.tsx +++ b/src/components/common/Modal.tsx @@ -56,9 +56,9 @@ export default function Modal() { ref={modalRef} className="max-h-[90vh] w-[94vw] max-w-[28rem] rounded-2xl bg-white p-6 pr-5 tablet:w-[37.5rem] tablet:max-w-none" > -
+
{title ? ( -

{title}

+

{title}

) : ( )} diff --git a/src/components/common/MonthPicker.tsx b/src/components/common/MonthPicker.tsx new file mode 100644 index 0000000..6f23718 --- /dev/null +++ b/src/components/common/MonthPicker.tsx @@ -0,0 +1,89 @@ +import { useRef, useState } from "react"; +import { ChevronDown } from "lucide-react"; +import useClickOutside from "../../hooks/useClickOutside.ts"; + +interface MonthPickerProps { + value: string; // YYYY-MM + onChange: (value: string) => void; + min?: string; + max?: string; + disabled?: boolean; +} + +const getMonthLabel = (yearMonth: string) => { + const [year, month] = yearMonth.split("-"); + return `${year}년 ${parseInt(month)}월`; +}; + +const generateMonths = (min: string, max: string): string[] => { + const [minY, minM] = min.split("-").map(Number); + const [maxY, maxM] = max.split("-").map(Number); + const months: string[] = []; + + for (let y = minY; y <= maxY; y++) { + const startM = y === minY ? minM : 1; + const endM = y === maxY ? maxM : 12; + for (let m = startM; m <= endM; m++) { + months.push(`${y}-${String(m).padStart(2, "0")}`); + } + } + + return months.reverse(); // 최신 월이 위에 +}; + +const MonthPicker = ({ + value, + onChange, + min = "2000-01", + max, + disabled = false, +}: MonthPickerProps) => { + const [open, setOpen] = useState(false); + const ref = useRef(null); + useClickOutside(ref, () => setOpen(false)); + + const monthOptions = generateMonths(min, max ?? value); + + return ( +
+ + + {open && !disabled && ( +
+ {monthOptions.map((month) => ( +
{ + onChange(month); + setOpen(false); + }} + className={`px-4 py-3 cursor-pointer text-sm text-gray-800 hover:bg-gray-100 ${ + month === value ? "bg-gray-100 font-semibold" : "" + }`} + > + {getMonthLabel(month)} +
+ ))} +
+ )} +
+ ); +}; + +export default MonthPicker; diff --git a/src/components/common/RangeDatePicker.tsx b/src/components/common/RangeDatePicker.tsx index a2e464d..e72bd40 100644 --- a/src/components/common/RangeDatePicker.tsx +++ b/src/components/common/RangeDatePicker.tsx @@ -88,7 +88,7 @@ export default function RangeDatePicker({
{open && ( -
+
void; } @@ -35,7 +37,9 @@ export default function SelectField({ description, size = "md", required = false, + disabled = false, options, + icon, value, onChange, }: SelectFieldProps) { @@ -55,12 +59,17 @@ export default function SelectField({
{title && (
-
+
{required && *} + {icon && ( +
+ {icon} +
+ )}
{description && ( - + )}
)} @@ -78,10 +87,11 @@ export default function SelectField({ )} type="button" onClick={() => setIsOpen((prev) => !prev)} + disabled={disabled} > @@ -97,7 +107,7 @@ export default function SelectField({ - {isOpen && ( + {isOpen && !disabled && ( handleSelect(opt.value)} - className="body-2 cursor-pointer px-4 py-3 hover:bg-grayscale-100" + className="body-3 cursor-pointer px-4 py-3 hover:bg-grayscale-100" > {opt.label} diff --git a/src/components/common/SingleDatePicker.tsx b/src/components/common/SingleDatePicker.tsx index 4c03bd5..c7eae2c 100644 --- a/src/components/common/SingleDatePicker.tsx +++ b/src/components/common/SingleDatePicker.tsx @@ -1,7 +1,7 @@ import { useState, useRef, useEffect } from "react"; import Calendar, { CalendarProps } from "react-calendar"; import { formatFullDate } from "../../utils/date"; -import { getKSTDate } from "../../libs/date"; // ✅ KST 기준 유틸 +import { getKSTDate } from "../../libs/date"; import dayjs from "dayjs"; import timezone from "dayjs/plugin/timezone"; import utc from "dayjs/plugin/utc"; @@ -74,7 +74,7 @@ export default function SingleDatePicker({ /> {open && ( -
+
{ +interface SpinnerProps { + className?: string; +} + +const Spinner: React.FC = ({ className }) => { return ( -
-
+
+
); }; diff --git a/src/components/common/TextField.tsx b/src/components/common/TextField.tsx index 7cd30f2..8110c4b 100644 --- a/src/components/common/TextField.tsx +++ b/src/components/common/TextField.tsx @@ -15,7 +15,7 @@ const textFieldVariants = cva( active: "[&_input]:text-grayscale-900 [&_input]:placeholder:text-grayscale-500 [&_input]:border-grayscale-500", disable: - "[&_input]:text-grayscale-600 [&_input]:placeholder:text-grayscale-600 [&_input]:border-grayscale-300", + "[&_input]:text-grayscale-500 [&_input]:placeholder:text-grayscale-500 [&_input]:border-grayscale-300 [&_input]:bg-gray-50", }, size: { lg: "[&_input]:h-14 body-1 [&_input]:px-6", @@ -92,9 +92,15 @@ const TextField = React.forwardRef( className={cn( "w-full rounded-lg border px-5 py-3 body-1 transition-colors", suffix && "pr-12", - state === "disable" && "overflow-x-auto", + state === "disable" && + "overflow-x-auto cursor-text touch-pan-x", // 필수 inputClassName, )} + style={ + state === "disable" + ? { WebkitOverflowScrolling: "touch" } + : undefined + } readOnly={state === "disable"} placeholder={placeholder} required={required} diff --git a/src/components/common/TimeInput.tsx b/src/components/common/TimeInput.tsx new file mode 100644 index 0000000..55cb5d0 --- /dev/null +++ b/src/components/common/TimeInput.tsx @@ -0,0 +1,113 @@ +import { useEffect, useRef, useState } from "react"; +import { ChevronDown } from "lucide-react"; +import { cn } from "../../libs"; + +interface TimeInputProps { + value: string; // "09:00" + onChange: (val: string) => void; + disabled?: boolean; + step?: number; + error?: boolean; +} + +const TimeInput = ({ + value, + onChange, + disabled = false, + step = 10, + error = false, +}: TimeInputProps) => { + const ref = useRef(null); + const [open, setOpen] = useState(false); + const [tempHour, tempMinute] = value.split(":"); + + const [selectedHour, setSelectedHour] = useState(tempHour || "00"); + const [selectedMinute, setSelectedMinute] = useState(tempMinute || "00"); + + useEffect(() => { + const handleMouseDown = (e: MouseEvent) => { + if (ref.current && !ref.current.contains(e.target as Node)) { + setTimeout(() => setOpen(false), 100); // 살짝 딜레이 주면 선택 실수 방지 + } + }; + + document.addEventListener("mousedown", handleMouseDown); + return () => document.removeEventListener("mousedown", handleMouseDown); + }, []); + + const hours = Array.from({ length: 24 }, (_, i) => + String(i).padStart(2, "0"), + ); + const minutes = Array.from({ length: 60 / step }, (_, i) => + String(i * step).padStart(2, "0"), + ); + + const handleSelectHour = (h: string) => { + setSelectedHour(h); + if (selectedMinute) { + onChange(`${h}:${selectedMinute}`); + setOpen(false); + } + }; + + const handleSelectMinute = (m: string) => { + setSelectedMinute(m); + if (selectedHour) { + onChange(`${selectedHour}:${m}`); + setOpen(false); + } + }; + + return ( +
+ + + {open && ( +
+
+ {hours.map((h) => ( +
handleSelectHour(h)} + > + {h} +
+ ))} +
+
+ {minutes.map((m) => ( +
handleSelectMinute(m)} + > + {m} +
+ ))} +
+
+ )} +
+ ); +}; + +export default TimeInput; diff --git a/src/components/icons/CanceledIcon.tsx b/src/components/icons/CanceledIcon.tsx new file mode 100644 index 0000000..14221d6 --- /dev/null +++ b/src/components/icons/CanceledIcon.tsx @@ -0,0 +1,21 @@ +export default function CanceledIcon({ + className, + fill = "#2BC566", + ...props +}: React.ComponentProps<"svg">) { + return ( + + + + + + ); +} diff --git a/src/components/icons/DeleteIcon.tsx b/src/components/icons/DeleteIcon.tsx new file mode 100644 index 0000000..060082d --- /dev/null +++ b/src/components/icons/DeleteIcon.tsx @@ -0,0 +1,22 @@ +export default function DeleteIcon({ + className, + fill = "#939393", + ...props +}: React.ComponentProps<"svg">) { + return ( + + + + ); +} diff --git a/src/components/icons/ErrorIcon.tsx b/src/components/icons/ErrorIcon.tsx index 14553c2..d0a76e7 100644 --- a/src/components/icons/ErrorIcon.tsx +++ b/src/components/icons/ErrorIcon.tsx @@ -8,15 +8,15 @@ export default function ErrorIcon({ return ( diff --git a/src/components/layouts/BottomNav.tsx b/src/components/layouts/BottomNav.tsx index 58576c3..0e40431 100644 --- a/src/components/layouts/BottomNav.tsx +++ b/src/components/layouts/BottomNav.tsx @@ -84,7 +84,7 @@ const BottomNav = () => { return (