diff --git a/package-lock.json b/package-lock.json index d2ff10b6..47d92a6a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,13 +8,14 @@ "name": "frontend", "version": "0.0.0", "dependencies": { + "@tanstack/react-query": "^5.90.5", "axios": "^1.6.8", + "jotai": "^2.15.0", "lucide-react": "^0.522.0", "phaser": "^3.80.0", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-router-dom": "^6.22.1", - "zustand": "^4.5.2" + "react-router-dom": "^6.22.1" }, "devDependencies": { "@chromatic-com/storybook": "^4.1.1", @@ -88,7 +89,7 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", @@ -103,7 +104,7 @@ "version": "7.28.4", "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.4.tgz", "integrity": "sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -113,7 +114,7 @@ "version": "7.28.4", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz", "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", @@ -144,7 +145,7 @@ "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, + "devOptional": true, "bin": { "semver": "bin/semver.js" } @@ -153,7 +154,7 @@ "version": "7.28.3", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz", "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/parser": "^7.28.3", @@ -170,7 +171,7 @@ "version": "7.27.2", "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/compat-data": "^7.27.2", @@ -187,7 +188,7 @@ "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, + "devOptional": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -197,7 +198,7 @@ "version": "7.28.0", "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -207,7 +208,7 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/traverse": "^7.27.1", @@ -221,7 +222,7 @@ "version": "7.28.3", "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-module-imports": "^7.27.1", @@ -248,7 +249,7 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -258,7 +259,7 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -268,7 +269,7 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -278,7 +279,7 @@ "version": "7.28.4", "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/template": "^7.27.2", @@ -292,7 +293,7 @@ "version": "7.28.4", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz", "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/types": "^7.28.4" @@ -378,7 +379,7 @@ "version": "7.27.2", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", @@ -393,7 +394,7 @@ "version": "7.28.4", "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.4.tgz", "integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", @@ -412,7 +413,7 @@ "version": "7.28.4", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz", "integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", @@ -1127,7 +1128,7 @@ "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", @@ -1138,7 +1139,7 @@ "version": "2.3.5", "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", @@ -1149,7 +1150,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, + "devOptional": true, "engines": { "node": ">=6.0.0" } @@ -1170,14 +1171,14 @@ "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.31", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -1787,6 +1788,32 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@tanstack/query-core": { + "version": "5.90.5", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.5.tgz", + "integrity": "sha512-wLamYp7FaDq6ZnNehypKI5fNvxHPfTYylE0m/ZpuuzJfJqhR5Pxg9gvGBHZx4n7J+V5Rg5mZxHHTlv25Zt5u+w==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.90.5", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.5.tgz", + "integrity": "sha512-pN+8UWpxZkEJ/Rnnj2v2Sxpx1WFlaa9L6a4UO89p6tTQbeo+m0MS8oYDjbggrR8QcTyjKoYWKS3xJQGr3ExT8Q==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.90.5" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, "node_modules/@testing-library/dom": { "version": "10.4.1", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", @@ -3051,7 +3078,7 @@ "version": "2.8.7", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.7.tgz", "integrity": "sha512-bxxN2M3a4d1CRoQC//IqsR5XrLh0IJ8TCv2x6Y9N0nckNz/rTjZB3//GGscZziZOxmjP55rzxg/ze7usFI9FqQ==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "bin": { "baseline-browser-mapping": "dist/cli.js" @@ -3095,7 +3122,7 @@ "version": "4.26.2", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.2.tgz", "integrity": "sha512-ECFzp6uFOSB+dcZ5BK/IBaGWssbSYBHvuMeMt3MMFyhI0Z8SqGgEkBLARgpRH3hutIgPVsALcMwbDrJqPxQ65A==", - "dev": true, + "devOptional": true, "funding": [ { "type": "opencollective", @@ -3174,7 +3201,7 @@ "version": "1.0.30001745", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001745.tgz", "integrity": "sha512-ywt6i8FzvdgrrrGbr1jZVObnVv6adj+0if2/omv9cmR2oiZs30zL4DIyaptKcbOrBdOIc74QTMoJvSE2QHh5UQ==", - "dev": true, + "devOptional": true, "funding": [ { "type": "opencollective", @@ -3297,7 +3324,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true + "devOptional": true }, "node_modules/core-js-pure": { "version": "3.36.0", @@ -3372,7 +3399,7 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -3536,7 +3563,7 @@ "version": "1.5.224", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.224.tgz", "integrity": "sha512-kWAoUu/bwzvnhpdZSIc6KUyvkI1rbRXMT0Eq8pKReyOyaPZcctMli+EgvcN1PAvwVc7Tdo4Fxi2PsLNDU05mdg==", - "dev": true, + "devOptional": true, "license": "ISC" }, "node_modules/emoji-regex": { @@ -3736,7 +3763,7 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=6" @@ -4638,7 +4665,7 @@ "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, + "devOptional": true, "engines": { "node": ">=6.9.0" } @@ -5340,6 +5367,35 @@ "integrity": "sha512-JVAfqNPTvNq3sB/VHQJAFxN/sPgKnsKrCwyRt15zwNCdrMMJDdcEOdubuy+DuJYYdm0ox1J4uzEuYKkN+9yhVg==", "dev": true }, + "node_modules/jotai": { + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/jotai/-/jotai-2.15.0.tgz", + "integrity": "sha512-nbp/6jN2Ftxgw0VwoVnOg0m5qYM1rVcfvij+MZx99Z5IK13eGve9FJoCwGv+17JvVthTjhSmNtT5e1coJnr6aw==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0", + "@babel/template": ">=7.0.0", + "@types/react": ">=17.0.0", + "react": ">=17.0.0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@babel/template": { + "optional": true + }, + "@types/react": { + "optional": true + }, + "react": { + "optional": true + } + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -5361,7 +5417,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "dev": true, + "devOptional": true, "license": "MIT", "bin": { "jsesc": "bin/jsesc" @@ -5392,7 +5448,7 @@ "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, + "devOptional": true, "bin": { "json5": "lib/cli.js" }, @@ -5533,7 +5589,7 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, + "devOptional": true, "license": "ISC", "dependencies": { "yallist": "^3.0.2" @@ -5722,7 +5778,7 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/nanoid": { @@ -5753,7 +5809,7 @@ "version": "2.0.21", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.21.tgz", "integrity": "sha512-5b0pgg78U3hwXkCM8Z9b2FJdPZlr9Psr9V2gQPESdGHqbntyFJKFW4r5TeWGFzafGY3hzs1JC62VEQMbl1JFkw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/object-assign": { @@ -6055,7 +6111,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, + "devOptional": true, "license": "ISC" }, "node_modules/picomatch": { @@ -7501,7 +7557,7 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", - "dev": true, + "devOptional": true, "funding": [ { "type": "opencollective", @@ -7537,14 +7593,6 @@ "punycode": "^2.1.0" } }, - "node_modules/use-sync-external-store": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", - "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, "node_modules/vite": { "version": "5.1.4", "resolved": "https://registry.npmjs.org/vite/-/vite-5.1.4.tgz", @@ -7961,7 +8009,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true, + "devOptional": true, "license": "ISC" }, "node_modules/yocto-queue": { @@ -7975,33 +8023,6 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } - }, - "node_modules/zustand": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.2.tgz", - "integrity": "sha512-2cN1tPkDVkwCy5ickKrI7vijSjPksFRfqS6237NzT0vqSsztTNnQdHw9mmN7uBdk3gceVXU0a+21jFzFzAc9+g==", - "dependencies": { - "use-sync-external-store": "1.2.0" - }, - "engines": { - "node": ">=12.7.0" - }, - "peerDependencies": { - "@types/react": ">=16.8", - "immer": ">=9.0.6", - "react": ">=16.8" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "immer": { - "optional": true - }, - "react": { - "optional": true - } - } } } } diff --git a/package.json b/package.json index d443313b..223a946c 100644 --- a/package.json +++ b/package.json @@ -12,13 +12,14 @@ "build-storybook": "storybook build" }, "dependencies": { + "@tanstack/react-query": "^5.90.5", "axios": "^1.6.8", + "jotai": "^2.15.0", "lucide-react": "^0.522.0", "phaser": "^3.80.0", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-router-dom": "^6.22.1", - "zustand": "^4.5.2" + "react-router-dom": "^6.22.1" }, "devDependencies": { "@chromatic-com/storybook": "^4.1.1", diff --git a/src/app/index.tsx b/src/app/index.tsx index 5795af09..d680d108 100644 --- a/src/app/index.tsx +++ b/src/app/index.tsx @@ -1,18 +1,22 @@ +import { QueryClientProvider } from '@tanstack/react-query'; import { RouterProvider } from 'react-router-dom'; import router from './router'; import FullScreenPrompt from '@/components/prompt'; -import { useSoundSetting } from '@/features/sound'; +import { useBackgroundSound } from '@/features/sound'; +import { queryClient } from '@/shared/api'; const App = () => { - const { playBgm } = useSoundSetting(); + const { playBackgroundSound } = useBackgroundSound(); return ( -
+
- + + +
); }; diff --git a/src/app/router.tsx b/src/app/router.tsx index 6caccfbd..c44821ea 100644 --- a/src/app/router.tsx +++ b/src/app/router.tsx @@ -1,28 +1,26 @@ import { createBrowserRouter, Navigate, Outlet } from 'react-router-dom'; +import BasicContentFrame from '@/components/frame/with-buttons'; import Collection from '@/pages/collection'; import Error from '@/pages/error'; +import Finder, { getRoomList } from '@/pages/finder'; import Home from '@/pages/home'; import Landing from '@/pages/landing'; import LandingLayout from '@/pages/landing/layout'; -import LogIn from '@/pages/landing/logIn'; -import Finder from '@/pages/play/finder'; +import LogIn from '@/pages/logIn'; import Game from '@/pages/play/game'; -import Lobby from '@/pages/play/lobby'; +import LobbyPage, { getLobbyInfo } from '@/pages/play/lobby'; import Mode from '@/pages/play/mode'; import Ranking from '@/pages/ranking'; -import Result from '@/pages/result'; +import Result, { getGameResult } from '@/pages/result'; import Settings from '@/pages/settings'; import Tutorial from '@/pages/tutorial'; -import { checkToken, getUser } from '@/services/auth'; -import { getRoomList } from '@/services/finder'; -import { getLobbyInfo } from '@/services/lobby'; +import { checkToken } from '@/services/auth'; import { getRank } from '@/services/rank'; -import { getGameResult } from '@/services/result'; -import { ROUTE } from '@/constants/routes'; +import { ROUTE } from '@/shared/constants'; const router = createBrowserRouter([ { @@ -40,73 +38,79 @@ const router = createBrowserRouter([ ], }, { - element: , - errorElement: , - loader: checkToken, + element: ( + + + + ), children: [ { - path: `${ROUTE.tutorial}`, - element: , - }, - { - path: `${ROUTE.home}`, - element: , - loader: getUser, - }, - { - path: `${ROUTE.play}`, - errorElement: , + element: , + errorElement: , + loader: checkToken, children: [ { - index: true, - element: , - loader: getUser, + path: ROUTE.tutorial, + element: , + }, + { + path: ROUTE.home, + element: , + }, + { + path: ROUTE.play, + errorElement: , + children: [ + { + index: true, + element: , + }, + { + path: `${ROUTE.lobby}/:roomId`, + element: , + loader: ({ params }) => getLobbyInfo(params.roomId), + }, + { + path: ROUTE.finder, + element: , + loader: () => getRoomList(1), + }, + ], }, { - path: `${ROUTE.lobby}/:roomId`, - element: , - loader: ({ params }) => getLobbyInfo(params.roomId), + path: ROUTE.ranking, + element: , + loader: getRank, }, { - path: `${ROUTE.finder}`, - element: , - loader: () => getRoomList(1), + path: ROUTE.result, + element: , + errorElement: , + loader: getGameResult, }, { - path: `${ROUTE.game}`, - element: , + path: ROUTE.setting, + element: , + }, + { + path: ROUTE.collection, + element: , }, ], }, { - path: `${ROUTE.ranking}`, - element: , - loader: getRank, - }, - { - path: `${ROUTE.result}`, - element: , - errorElement: , - loader: getGameResult, + path: `${ROUTE.error}/:code`, + element: , }, { - path: `${ROUTE.setting}`, - element: , - }, - { - path: `${ROUTE.collection}`, - element: , - loader: getUser, + path: '/*', + element: , }, ], }, { - path: `${ROUTE.error}/:code`, - element: , - }, - { - path: '/*', - element: , + path: ROUTE.game, + element: , }, ]); diff --git a/src/components/button/ColoredButton/index.tsx b/src/components/button/ColoredButton/index.tsx index bb5b2a14..1852ae22 100644 --- a/src/components/button/ColoredButton/index.tsx +++ b/src/components/button/ColoredButton/index.tsx @@ -5,13 +5,14 @@ import type { coloredButtonSizeType, } from '@/components/button/types'; -import useEffectSoundStore from "@/states/effect"; +import { useEffectSound } from '@/features/sound'; interface Props extends ColorButtonProps { size: coloredButtonSizeType; } + const ColoredButton = ({ text, color, size, onClick }: Props) => { - const { playEffectSound } = useEffectSoundStore(); + const { playEffectSound } = useEffectSound(); const handleClick = () => { playEffectSound(); diff --git a/src/components/button/ColoredIconButton/index.tsx b/src/components/button/ColoredIconButton/index.tsx index 043c12c4..04c4b701 100644 --- a/src/components/button/ColoredIconButton/index.tsx +++ b/src/components/button/ColoredIconButton/index.tsx @@ -5,7 +5,7 @@ import type { coloredIconButtonSizeType, } from '@/components/button/types'; -import useEffectSoundStore from "@/states/effect"; +import { useEffectSound } from '@/features/sound'; export interface ColoredIconButtonProps extends ColorButtonProps { icon: string; @@ -19,7 +19,7 @@ const ColoredIconButton = ({ size, onClick, }: ColoredIconButtonProps) => { - const { playEffectSound } = useEffectSoundStore(); + const { playEffectSound } = useEffectSound(); const handleClick = () => { playEffectSound(); diff --git a/src/components/button/SettingNavigationButton/index.css.ts b/src/components/button/SettingNavigationButton/index.css.ts index c4fdf761..98d95192 100644 --- a/src/components/button/SettingNavigationButton/index.css.ts +++ b/src/components/button/SettingNavigationButton/index.css.ts @@ -4,7 +4,7 @@ import { recipe } from '@vanilla-extract/recipes'; import { sprinkles } from '@/styles/sprinkles.css'; import { vars } from '@/styles/vars.css'; -export const positionVariants = { +const position = { leftTop: { top: 0, left: 0, @@ -21,6 +21,11 @@ export const positionVariants = { }, }; +const usage = { + frame: {}, + modal: {}, +}; + export const button = recipe({ base: style([ sprinkles({ @@ -42,6 +47,23 @@ export const button = recipe({ }, ]), variants: { - position: positionVariants, + position, + usage, }, + compoundVariants: [ + { + variants: { + position: 'leftTop', + usage: 'frame', + }, + style: style({ + '@media': { + 'screen and (min-height: 463px)': { + top: '-16px', + left: '-16px', + }, + }, + }), + }, + ], }); diff --git a/src/components/button/SettingNavigationButton/index.tsx b/src/components/button/SettingNavigationButton/index.tsx index 328383c5..05a6afd4 100644 --- a/src/components/button/SettingNavigationButton/index.tsx +++ b/src/components/button/SettingNavigationButton/index.tsx @@ -1,24 +1,21 @@ -import * as styles from './index.css'; +import { type RecipeVariants } from '@vanilla-extract/recipes'; -import type { positionType } from '@/components/button/types'; +import * as styles from './index.css'; -import useEffectSoundStore from "@/states/effect"; +import { useEffectSound } from '@/features/sound'; -interface Props { +type Props = { label: string; onClick: () => void; - position: positionType; -} - -const buttonLabel = (label: string, position: positionType) => { - if (position === 'leftTop') { - return `< ${label}`; - } - return `${label} X`; -}; +} & RecipeVariants; -const SettingNavigationButton = ({ label, onClick, position }: Props) => { - const { playEffectSound } = useEffectSoundStore(); +const SettingNavigationButton = ({ + label, + onClick, + position, + usage = 'modal', +}: Props) => { + const { playEffectSound } = useEffectSound(); const handleClick = () => { playEffectSound(); @@ -28,10 +25,10 @@ const SettingNavigationButton = ({ label, onClick, position }: Props) => { return ( ); }; diff --git a/src/components/button/SettingTextButton/index.tsx b/src/components/button/SettingTextButton/index.tsx index 9f97e95d..921c2250 100644 --- a/src/components/button/SettingTextButton/index.tsx +++ b/src/components/button/SettingTextButton/index.tsx @@ -2,7 +2,7 @@ import { ReactNode } from 'react'; import * as styles from './index.css'; -import useEffectSoundStore from '@/states/effect'; +import { useEffectSound } from '@/features/sound'; interface Props { children: ReactNode; @@ -16,7 +16,7 @@ const SettingTextButton = ({ onClick, disabled, }: Props & styles.ButtonVariantsProps) => { - const { playEffectSound } = useEffectSoundStore(); + const { playEffectSound } = useEffectSound(); const handleClick = () => { playEffectSound(); diff --git a/src/components/button/types.d.ts b/src/components/button/types.d.ts index 80389f1c..123d7ea7 100644 --- a/src/components/button/types.d.ts +++ b/src/components/button/types.d.ts @@ -1,6 +1,5 @@ import * as constants from './constants'; -import { positionVariants } from '@/components/button/SettingNavigationButton/index.css'; import { buttonVariants } from '@/components/button/SettingTextButton/index.css'; type colorType = (typeof constants.BUTTON_COLOR)[number]; @@ -10,8 +9,6 @@ type coloredButtonSizeType = keyof typeof constants.COLORED_BUTTON_SIZE_PIXEL; type coloredIconButtonSizeType = keyof typeof constants.COLORED_ICON_BUTTON_SIZE_PIXEL; -type positionType = keyof typeof positionVariants; - interface ColorButtonProps { text: string; color: colorType; diff --git a/src/components/frame/constants.ts b/src/components/frame/constants.ts index d38d7e17..248156a1 100644 --- a/src/components/frame/constants.ts +++ b/src/components/frame/constants.ts @@ -1,6 +1,6 @@ import { vars } from '@/styles/vars.css'; -import { IPHONE_14_PRO_MAX, IPHONE_SE } from '@/constants/screen'; +import { IPHONE_14_PRO_MAX, IPHONE_SE } from '@/shared/constants'; export const FRAME_STYLE = { width: { diff --git a/src/components/frame/with-buttons/audio-button/index.tsx b/src/components/frame/with-buttons/audio-button/index.tsx index 378bd3e1..c5485dff 100644 --- a/src/components/frame/with-buttons/audio-button/index.tsx +++ b/src/components/frame/with-buttons/audio-button/index.tsx @@ -1,11 +1,12 @@ import { Volume2, VolumeX } from 'lucide-react'; +import { memo } from 'react'; import { button } from '../button.css'; import * as styles from './index.css'; import { useSoundToggle } from '@/features/sound'; -const AudioButton = () => { +const AudioButton = memo(() => { const { backgroundSound, toggleSound } = useSoundToggle(); return ( @@ -17,6 +18,6 @@ const AudioButton = () => { )} ); -}; +}); export default AudioButton; diff --git a/src/components/frame/with-buttons/index.tsx b/src/components/frame/with-buttons/index.tsx index b37f8aed..b0d7935d 100644 --- a/src/components/frame/with-buttons/index.tsx +++ b/src/components/frame/with-buttons/index.tsx @@ -1,5 +1,5 @@ -import { ReactNode } from 'react'; -import { useNavigate } from 'react-router-dom'; +import { type ReactNode } from 'react'; +import { useLocation, useNavigate } from 'react-router-dom'; import AudioButton from './audio-button'; import * as styles from './index.css'; @@ -7,28 +7,46 @@ import MenuButton from './menu-button'; import SettingNavigationButton from '@/components/button/SettingNavigationButton/index'; -interface Props { - children: ReactNode; - leftButton?: { - label: string; - navigateTo?: string; - }; - rightButtonsDisabled?: boolean; - menuButtonDisabled?: boolean; -} - -const BasicContentFrame = ({ - children, - leftButton, - rightButtonsDisabled, - menuButtonDisabled, -}: Props) => { +import { ROUTE } from '@/shared/constants'; + +const ALLOWED_PATHS = new Set([ + ROUTE.collection, + ROUTE.ranking, + ROUTE.play, + ROUTE.finder, + ROUTE.setting, +]); + +const BackButton = ({ pathname }: { pathname: string }) => { const navigate = useNavigate(); - const handleNavigate = () => - leftButton?.navigateTo - ? navigate(leftButton.navigateTo, { replace: true }) - : navigate(-1); + const allowed = ALLOWED_PATHS.has(pathname); + + if (!allowed) { + return null; + } + + return ( + navigate(-1)} + position='leftTop' + /> + ); +}; + +const DISABLED = { + rightButtons: new Set([ROUTE.error, ROUTE.result]), + menuButton: new Set([ROUTE.tutorial]), +}; + +const BasicContentFrame = ({ children }: { children: ReactNode }) => { + const { pathname } = useLocation(); + + const rightButtonsDisabled = DISABLED.rightButtons.has(pathname); + + const menuButtonDisabled = DISABLED.menuButton.has(pathname); return (
@@ -39,16 +57,7 @@ const BasicContentFrame = ({
)}
- {leftButton && ( -
- -
- )} - + {children}
diff --git a/src/components/frame/with-buttons/menu-button/account-setting-button.tsx b/src/components/frame/with-buttons/menu-button/account-setting-button.tsx index ba29b2ad..6b0d5840 100644 --- a/src/components/frame/with-buttons/menu-button/account-setting-button.tsx +++ b/src/components/frame/with-buttons/menu-button/account-setting-button.tsx @@ -2,7 +2,7 @@ import { useLocation, useNavigate } from 'react-router-dom'; import SettingTextButton from '@/components/button/SettingTextButton'; -import { ROUTE } from '@/constants/routes'; +import { ROUTE } from '@/shared/constants'; interface Props { onClick?: () => void; diff --git a/src/components/frame/with-buttons/menu-button/constants.ts b/src/components/frame/with-buttons/menu-button/constants.ts deleted file mode 100644 index d7dd5918..00000000 --- a/src/components/frame/with-buttons/menu-button/constants.ts +++ /dev/null @@ -1,8 +0,0 @@ -export const MENU_ID = { - home: 'menu-home', - gameInfo: 'game-info', - guest: 'become-member', -}; - -export const NOTICE_URL = - 'https://nocolored.notion.site/'; diff --git a/src/components/frame/with-buttons/menu-button/game-info/GameRules.tsx b/src/components/frame/with-buttons/menu-button/game-info/GameRules.tsx index 8592ea63..95e6b8ce 100644 --- a/src/components/frame/with-buttons/menu-button/game-info/GameRules.tsx +++ b/src/components/frame/with-buttons/menu-button/game-info/GameRules.tsx @@ -1,9 +1,7 @@ import { useState } from 'react'; import * as styles from './index.css'; -import { indexProps } from './types'; -import SettingNavigationButton from '@/components/button/SettingNavigationButton'; import SettingTextButton from '@/components/button/SettingTextButton'; import { MAX_PAGE_SIZE } from '@/pages/tutorial/constants'; @@ -11,7 +9,7 @@ import Info from '@/pages/tutorial/Info'; const $MAX_PAGE_SIZE = MAX_PAGE_SIZE - 1; -const GameRules = ({ onBack, onClose }: indexProps) => { +const GameRules = () => { const [page, setPage] = useState(0); const prevPage = () => { @@ -33,39 +31,27 @@ const GameRules = ({ onBack, onClose }: indexProps) => { }; return ( - <> - - -
- -
- 0 ? 'black' : 'gray'} - onClick={prevPage} - > - {`<`} - -
-
- - {`>`} - -
+
+ +
+ 0 ? 'black' : 'gray'} + onClick={prevPage} + > + {`<`} +
- +
+ + {`>`} + +
+
); }; diff --git a/src/components/frame/with-buttons/menu-button/game-info/ItemInfo.tsx b/src/components/frame/with-buttons/menu-button/game-info/ItemInfo.tsx index 20c8859d..49b3c7a1 100644 --- a/src/components/frame/with-buttons/menu-button/game-info/ItemInfo.tsx +++ b/src/components/frame/with-buttons/menu-button/game-info/ItemInfo.tsx @@ -2,12 +2,10 @@ import { useState } from 'react'; import { ITEMS } from './constants'; import * as styles from './index.css'; -import { indexProps } from './types'; -import SettingNavigationButton from '@/components/button/SettingNavigationButton'; import RoundCornerImageBox from '@/components/image-box'; -const ItemInfo = ({ onBack, onClose }: indexProps) => { +const ItemInfo = () => { const [idx, setIdx] = useState(0); const imgUrl = `/images/items/item-${ITEMS[idx].name}-h32w32.png`; @@ -22,16 +20,6 @@ const ItemInfo = ({ onBack, onClose }: indexProps) => { return ( <> - -
아이템
{ITEMS[idx].title}
diff --git a/src/components/frame/with-buttons/menu-button/game-info/MainInfo.tsx b/src/components/frame/with-buttons/menu-button/game-info/MainInfo.tsx deleted file mode 100644 index 24e34072..00000000 --- a/src/components/frame/with-buttons/menu-button/game-info/MainInfo.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import * as styles from './index.css'; - -import type { infoType } from './types'; - -import ColoredButton from '@/components/button/ColoredButton'; -import SettingTextButton from '@/components/button/SettingTextButton'; - -interface mainProps { - onClose?: () => void; - onNavigate: (view: infoType) => void; -} - -const MainInfo = ({ onClose = () => {}, onNavigate }: mainProps) => { - return ( - <> -
-
게임 정보
-
-
- onNavigate('game')} - size='medium' - colorStyle='black' - > - 게임 방식 - - onNavigate('item')} - size='medium' - colorStyle='black' - > - 아이템 - - onNavigate('tier')} - size='medium' - colorStyle='black' - > - 티어 - -
-
- -
- - ); -}; - -export default MainInfo; diff --git a/src/components/frame/with-buttons/menu-button/game-info/TierInfo.tsx b/src/components/frame/with-buttons/menu-button/game-info/TierInfo.tsx index 705c8a44..3ef69f44 100644 --- a/src/components/frame/with-buttons/menu-button/game-info/TierInfo.tsx +++ b/src/components/frame/with-buttons/menu-button/game-info/TierInfo.tsx @@ -1,29 +1,17 @@ import { HIGH_TIER_INFO, LOW_TIER_INFO } from './constants'; import * as styles from './index.css'; -import { indexProps } from './types'; -import SettingNavigationButton from '@/components/button/SettingNavigationButton'; -import TierBox from '@/components/tier'; +import TierBox from '@/models/tier'; -const TierInfo = ({ onBack, onClose }: indexProps) => { +const TierInfo = () => { return ( <> - -
티어
{Object.entries(LOW_TIER_INFO).map(([tier, { description, score }]) => (
- +
{description}
{score}
@@ -35,7 +23,7 @@ const TierInfo = ({ onBack, onClose }: indexProps) => { ([tier, { description, score }]) => (
- +
{description}
{score}
diff --git a/src/components/frame/with-buttons/menu-button/game-info/constants/index.ts b/src/components/frame/with-buttons/menu-button/game-info/constants/index.ts index 29a5d8de..496ddfde 100644 --- a/src/components/frame/with-buttons/menu-button/game-info/constants/index.ts +++ b/src/components/frame/with-buttons/menu-button/game-info/constants/index.ts @@ -1,4 +1,2 @@ -export const INFO_TYPE = ['main', 'item', 'game', 'tier'] as const; - export { ITEMS } from './item'; export { LOW_TIER_INFO, HIGH_TIER_INFO } from './tier'; diff --git a/src/components/frame/with-buttons/menu-button/game-info/index.tsx b/src/components/frame/with-buttons/menu-button/game-info/index.tsx index a20bb2d0..217d6584 100644 --- a/src/components/frame/with-buttons/menu-button/game-info/index.tsx +++ b/src/components/frame/with-buttons/menu-button/game-info/index.tsx @@ -1,33 +1,76 @@ import { useState } from 'react'; import GameRules from './GameRules'; +import * as styles from './index.css'; import ItemInfo from './ItemInfo'; -import MainInfo from './MainInfo'; import TierInfo from './TierInfo'; -import type { infoType } from './types'; +import ColoredButton from '@/components/button/ColoredButton'; +import SettingNavigationButton from '@/components/button/SettingNavigationButton'; +import SettingTextButton from '@/components/button/SettingTextButton'; + +const menu = { + game: ['게임 방식', GameRules], + item: ['아이템', ItemInfo], + tier: ['티어', TierInfo], +} satisfies Record React.ReactNode]>; + +type infoType = keyof typeof menu; interface Props { onClose: () => void; } const GameInfo = ({ onClose }: Props) => { - const [currentView, setCurrentView] = useState('main'); + const [currentView, setCurrentView] = useState('main'); - const handleBack = () => { - setCurrentView('main'); - }; - - if (currentView === 'game') { - return ; - } - if (currentView === 'item') { - return ; + if (currentView === 'main') { + return ( + <> +
+
게임 정보
+
+
+ {(Object.keys(menu) as infoType[]).map((key) => ( + setCurrentView(key)} + size='medium' + colorStyle='black' + > + {menu[key][0]} + + ))} +
+
+ +
+ + ); } - if (currentView === 'tier') { - return ; - } - return ; + + const InfoComponent = menu[currentView][1]; + + return ( + <> + setCurrentView('main')} + position='leftTop' + /> + + + + ); }; export default GameInfo; diff --git a/src/components/frame/with-buttons/menu-button/game-info/types.d.ts b/src/components/frame/with-buttons/menu-button/game-info/types.d.ts deleted file mode 100644 index 225f79cb..00000000 --- a/src/components/frame/with-buttons/menu-button/game-info/types.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { INFO_TYPE } from './constants'; - -export interface indexProps { - onClose: () => void; - onBack: () => void; -} - -type infoType = (typeof INFO_TYPE)[number]; diff --git a/src/components/frame/with-buttons/menu-button/store.ts b/src/components/frame/with-buttons/menu-button/store.ts deleted file mode 100644 index 0ca2e924..00000000 --- a/src/components/frame/with-buttons/menu-button/store.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { create } from 'zustand'; - -import { MENU_ID } from './constants'; - -interface MenuState { - id: string; -} - -interface MenuActions { - setMenuId: (id: string) => void; -} - -type MenuStore = MenuState & MenuActions; - -export const useMenuStore = create((set) => ({ - id: MENU_ID.home, - setMenuId: (id: string) => set({ id }), -})); diff --git a/src/components/frame/with-buttons/menu-button/ui.tsx b/src/components/frame/with-buttons/menu-button/ui.tsx index 37d20f3e..a4710266 100644 --- a/src/components/frame/with-buttons/menu-button/ui.tsx +++ b/src/components/frame/with-buttons/menu-button/ui.tsx @@ -1,17 +1,22 @@ import { Menu as MenuIcon } from 'lucide-react'; +import { useCallback, useMemo, useState } from 'react'; import { button } from '../button.css'; import AccountSettingButton from './account-setting-button'; -import { MENU_ID, NOTICE_URL } from './constants'; import GameInfo from './game-info'; -import { useMenuStore } from './store'; import ColoredButton from '@/components/button/ColoredButton'; import SettingTextButton from '@/components/button/SettingTextButton'; import Modal, { useModal } from '@/components/modal'; import SignUp from '@/features/sign-up'; -import { useUserStatus } from '@/features/user'; +import { useUserStatus } from '@/models/user'; + +const MENU_ID = { + home: 'menu-home', + gameInfo: 'game-info', + guest: 'become-member', +}; const MenuItem = (props: { onClick: () => void; @@ -19,14 +24,21 @@ const MenuItem = (props: { children: React.ReactNode; }) => ; -const Menu = ({ closeModal }: { closeModal: () => void }) => { - const { setMenuId } = useMenuStore.getState(); +const Menu = ({ + closeModal, + setMenuId, +}: { + closeModal: () => void; + setMenuId: (id: string) => void; +}) => { const { isGuest, isMember } = useUserStatus(); return ( <>

메뉴

- window.open(NOTICE_URL, '_blank')}> + window.open('https://nocolored.notion.site/', '_blank')} + > 공지 사항 setMenuId(MENU_ID.gameInfo)}>게임 정보 @@ -44,44 +56,32 @@ const Menu = ({ closeModal }: { closeModal: () => void }) => { ); }; -const ModalItem = ({ - id, - children, -}: { - id: string; - children: React.ReactNode; -}) => { - const openId = useMenuStore((state) => state.id); - - if (id === openId) { - return children; - } +const MenuButton = () => { + const [menuId, setMenuId] = useState(MENU_ID.home); - return null; -}; + const onClose = useCallback(() => setMenuId(MENU_ID.home), []); -const MenuButton = () => { - const { setMenuId } = useMenuStore.getState(); const { modalRef, openModal, closeModal } = useModal({ - onClose: () => setMenuId(MENU_ID.home), + onClose, }); + const ModalContent = useMemo(() => { + switch (menuId) { + case MENU_ID.gameInfo: + return ; + case MENU_ID.guest: + return ; + default: + return ; + } + }, [menuId]); + return ( <> - - - - - - - - - - - + {ModalContent} ); }; diff --git a/src/components/image-box/index.stories.tsx b/src/components/image-box/index.stories.tsx index bb2302a6..b1ffe323 100644 --- a/src/components/image-box/index.stories.tsx +++ b/src/components/image-box/index.stories.tsx @@ -6,7 +6,7 @@ import type { Meta, StoryObj } from '@storybook/react-vite'; import Chip from '@/components/chip'; -import * as styles from '@/pages/collection/index.css'; +import * as styles from '@/pages/collection/ui/menu/index.css'; const meta = { title: 'components/ImageBox', diff --git a/src/components/image-box/index.tsx b/src/components/image-box/index.tsx index a5995d19..6672233a 100644 --- a/src/components/image-box/index.tsx +++ b/src/components/image-box/index.tsx @@ -33,7 +33,7 @@ const RoundCornerImageBox = ({ backgroundColor, })} > - {alt} + {imgSrc && {alt}} {children &&
{children}
}
); diff --git a/src/components/modal/ui.tsx b/src/components/modal/ui.tsx index cd1dac46..514c4882 100644 --- a/src/components/modal/ui.tsx +++ b/src/components/modal/ui.tsx @@ -1,4 +1,4 @@ -import { forwardRef, type ReactNode } from 'react'; +import { forwardRef, memo, type ReactNode } from 'react'; import { createPortal } from 'react-dom'; import * as styles from './index.css'; @@ -7,13 +7,15 @@ interface Props { children: ReactNode; } -const Modal = forwardRef(({ children }, ref) => { - return createPortal( - -
{children}
-
, - document.getElementById('modal') as HTMLDivElement, - ); -}); +const Modal = memo( + forwardRef(({ children }, ref) => { + return createPortal( + +
{children}
+
, + document.getElementById('modal') as HTMLDivElement, + ); + }), +); export default Modal; diff --git a/src/components/player-info/index.stories.tsx b/src/components/player-info/index.stories.tsx index acf87485..a09ba0eb 100644 --- a/src/components/player-info/index.stories.tsx +++ b/src/components/player-info/index.stories.tsx @@ -4,7 +4,7 @@ import type { Meta, StoryObj } from '@storybook/react-vite'; import RankingComponent from '@/components/ranking'; -import ResultInfoBox from '@/pages/result/ResultInfoBox'; +import ResultInfoBox from '@/pages/result/ui/ResultInfoBox'; const meta = { title: 'components/PlayerInfo', @@ -70,13 +70,16 @@ export const GameResult: Story = { render: (args) => { return ( ); }, @@ -86,13 +89,16 @@ export const MyGameResult: Story = { render: (args) => { return ( ); }, diff --git a/src/components/prompt/index.css.ts b/src/components/prompt/index.css.ts index 42705f72..ffcb10a1 100644 --- a/src/components/prompt/index.css.ts +++ b/src/components/prompt/index.css.ts @@ -2,7 +2,7 @@ import { style } from '@vanilla-extract/css'; import { sprinkles } from '@/styles/sprinkles.css'; -import { IPHONE_SE } from '@/constants/screen'; +import { IPHONE_SE } from '@/shared/constants'; export const fullscreenPromptStyle = style([ sprinkles({ diff --git a/src/components/ranking/ui.tsx b/src/components/ranking/ui.tsx index 6ad4961b..cb8ac0da 100644 --- a/src/components/ranking/ui.tsx +++ b/src/components/ranking/ui.tsx @@ -1,12 +1,11 @@ import * as styles from './index.css'; -import type { RankPlayer } from '@/types/rank'; - import PlayerInfo from '@/components/player-info'; -import TierBox from '@/components/tier'; + +import TierBox from '@/models/tier'; interface Props { - player: RankPlayer; + player: Profile; guest?: boolean; myRank?: boolean; } diff --git a/src/constants/routes.ts b/src/constants/routes.ts deleted file mode 100644 index cbc0754f..00000000 --- a/src/constants/routes.ts +++ /dev/null @@ -1,17 +0,0 @@ -export const ROUTE = { - main: '/', - login: '/login', - tutorial: '/tutorial', - play: '/play', - finder: '/play/finder', - game: '/play/game', - lobby: '/play/lobby', - ranking: '/ranking', - collection: '/collection', - label: '/label', - achievement: '/achievement', - home: '/home', - result: '/result', - setting: '/settings', - error: '/error', -}; diff --git a/src/features/api/constants.ts b/src/features/api/constants.ts deleted file mode 100644 index 1b098b18..00000000 --- a/src/features/api/constants.ts +++ /dev/null @@ -1 +0,0 @@ -export const API_BASE_URL = import.meta.env.VITE_API_BASE_URL; diff --git a/src/features/auth/api.ts b/src/features/auth/api.ts new file mode 100644 index 00000000..d7e8e596 --- /dev/null +++ b/src/features/auth/api.ts @@ -0,0 +1,29 @@ +import { client } from '@/shared/api'; + +const setToken = (token: string, status: number) => { + if (status === 200) { + localStorage.setItem('token', token); + return true; + } + return false; +}; + +export const loginAsMember = async (account: Account) => { + const { data, status } = await client.post(`/user/login`, account, { + headers: { + 'X-Bypass-Authorization': true, + }, + }); + return setToken(data, status); +}; + +export const loginAsGuest = async () => { + return client + .get('/user/guest', { + headers: { + 'X-Bypass-Authorization': true, + }, + }) + .then(({ data, status }) => setToken(data, status)) + .catch(() => false); +}; diff --git a/src/features/auth/hooks.ts b/src/features/auth/hooks.ts new file mode 100644 index 00000000..d7c102f4 --- /dev/null +++ b/src/features/auth/hooks.ts @@ -0,0 +1,63 @@ +import { type AxiosError } from 'axios'; +import { useNavigate } from 'react-router-dom'; + +import { loginAsGuest, loginAsMember } from './api'; + +import { removeUserQuery } from '@/models/user'; +import { ROUTE } from '@/shared/constants'; +import { setFullScreen } from '@/shared/utils'; + +export const useLogout = () => { + const navigate = useNavigate(); + + const logout = () => { + window.localStorage.removeItem('token'); + removeUserQuery(); + navigate('/'); + }; + + return { logout }; +}; + +export const useLogin = () => { + const navigate = useNavigate(); + + const login = async (account: Account) => { + removeUserQuery(); + + return loginAsMember(account) + .then((isSuccess) => { + if (isSuccess) { + navigate(ROUTE.home, { replace: true }); + setFullScreen(); + } + return isSuccess; + }) + .catch(({ response }: AxiosError) => { + console.debug('login error:', response); + if (!response || response.status >= 500) { + navigate(`${ROUTE.error}/500`); + } + return false; + }); + }; + + return { login }; +}; + +export const useGuestLogin = () => { + const navigate = useNavigate(); + + const login = async () => { + removeUserQuery(); + + const isSuccess = await loginAsGuest(); + if (!isSuccess) { + return navigate(`${ROUTE.error}/500`); + } + setFullScreen(); + return navigate(ROUTE.tutorial, { replace: true }); + }; + + return { guestLogin: login }; +}; diff --git a/src/features/auth/index.ts b/src/features/auth/index.ts new file mode 100644 index 00000000..4cc90d02 --- /dev/null +++ b/src/features/auth/index.ts @@ -0,0 +1 @@ +export * from './hooks'; diff --git a/src/features/auth/types.d.ts b/src/features/auth/types.d.ts new file mode 100644 index 00000000..06422cd6 --- /dev/null +++ b/src/features/auth/types.d.ts @@ -0,0 +1,4 @@ +type Account = { + id: string; + password: string; +}; diff --git a/src/features/game/api.ts b/src/features/game/api.ts new file mode 100644 index 00000000..43695b56 --- /dev/null +++ b/src/features/game/api.ts @@ -0,0 +1,8 @@ +import { client } from '@/shared/api'; + +export const getGameReady = async () => { + return client + .get('/ingame/ready') + .then(({ data }) => data) + .catch(() => null); +}; diff --git a/src/features/game/object/Character.ts b/src/features/game/object/Character.ts index 8922542c..78a34144 100644 --- a/src/features/game/object/Character.ts +++ b/src/features/game/object/Character.ts @@ -1,5 +1,3 @@ -import { characterInfo } from '@/types/ingame'; - import * as constants from '@/features/game/constants'; export class Character extends Phaser.Physics.Arcade.Sprite { diff --git a/src/features/game/scene/GameScene.ts b/src/features/game/scene/GameScene.ts index ad60e2fa..300d4b19 100644 --- a/src/features/game/scene/GameScene.ts +++ b/src/features/game/scene/GameScene.ts @@ -1,5 +1,6 @@ import Phaser from 'phaser'; +import { getGameReady } from '../api'; import * as constants from '../constants'; import { Background } from '../map/Background'; import { Map } from '../map/Map'; @@ -19,16 +20,11 @@ import { EffectUtils } from './EffectUtils'; import GameOver from './GameOver'; import LoadingUtils from './LoadingUtils'; -import { characterInfo, IngameReady } from '@/types/ingame'; - -import { getIngameReady } from '@/services/ingame'; - -import { useWebSocketStore } from '@/features/websocket'; import { characterInfoList, currentScore, effectList, - GameSocket, + type GameSocket, showItem, showRealSkin, timeLeft, @@ -38,7 +34,7 @@ import { export default class GameScene extends Phaser.Scene { private socket: GameSocket; - private gameData: IngameReady | null; + private gameData: GameData | null; private gameState: 'loading' | 'ready' | 'countDown' | 'playing' | 'end' = 'loading'; @@ -68,17 +64,12 @@ export default class GameScene extends Phaser.Scene { private setIsActive: (isActive: boolean) => void; - constructor( - setIsActive: (isActive: boolean) => void, - onDisconnect: () => void, - ) { + constructor(setIsActive: (isActive: boolean) => void, webSocket: GameSocket) { super({ key: 'GameScene' }); this.setIsActive = setIsActive; // WebSocket - const { webSocket } = useWebSocketStore.getState(); this.socket = webSocket; this.socket.useMessageQueue(); - this.socket.inGameUnconnected(onDisconnect); // api 데이터 초기화 this.gameData = null; @@ -104,7 +95,7 @@ export default class GameScene extends Phaser.Scene { // 게임 초기 데이터 콜 로직 private setGameReady = async () => { - this.gameData = await getIngameReady(); + this.gameData = await getGameReady(); // 게임 데이터 없을 시 오류 if (this.gameData === null) { diff --git a/src/features/game/scene/config.ts b/src/features/game/scene/config.ts index 48775550..0060cb45 100644 --- a/src/features/game/scene/config.ts +++ b/src/features/game/scene/config.ts @@ -19,7 +19,7 @@ export const config: Phaser.Types.Core.GameConfig = { export const scenesConfig = ( setIsActive: (isActive: boolean) => void, - onDisconnect: () => void, + webSocket: GameSocket, ) => { - return [LoadPreLoadingScene, new GameScene(setIsActive, onDisconnect)]; + return [LoadPreLoadingScene, new GameScene(setIsActive, webSocket)]; }; diff --git a/src/types/ingame.d.ts b/src/features/game/types.d.ts similarity index 63% rename from src/types/ingame.d.ts rename to src/features/game/types.d.ts index fea1e0dd..b2f121a3 100644 --- a/src/types/ingame.d.ts +++ b/src/features/game/types.d.ts @@ -1,17 +1,17 @@ -export interface IngameReady { +interface GameData { mapId: number; floorList: number[][]; skins: string[]; } -export interface characterInfo { +interface characterInfo { x: number; y: number; velX: number; velY: number; } -export interface characterInfoIndex { +interface characterInfoIndex { index: number; characterInfo: characterInfo; } diff --git a/src/features/room-setting/api.ts b/src/features/room-setting/api.ts new file mode 100644 index 00000000..fb3805f5 --- /dev/null +++ b/src/features/room-setting/api.ts @@ -0,0 +1,14 @@ +import { client } from '@/shared/api'; + +export const createRoom = async (setting: RoomSetting) => { + return client.post('play/friendly', setting).then(({ data }) => data); +}; + +export const updateRoom = async (setting: RoomSetting) => { + return client + .post('/play/friendly/renew', setting) + .then(({ data }) => { + console.log('room update:', data); + return ''; + }); +}; diff --git a/src/pages/play/finder/Modal/index.css.ts b/src/features/room-setting/index.css.ts similarity index 56% rename from src/pages/play/finder/Modal/index.css.ts rename to src/features/room-setting/index.css.ts index ea82af82..82d06be8 100644 --- a/src/pages/play/finder/Modal/index.css.ts +++ b/src/features/room-setting/index.css.ts @@ -1,11 +1,10 @@ import { style } from '@vanilla-extract/css'; -import { recipe } from '@vanilla-extract/recipes'; - -import * as constants from '@/pages/play/finder/constants'; import { borderLightOptions, flexOptions } from '@/styles/common.css'; import { sprinkles } from '@/styles/sprinkles.css'; +import { MAP_ID_LIST } from '@/models/map'; + export const modalTwoButtonWrapper = style([ flexOptions({ option: 'rowCenter', @@ -15,53 +14,6 @@ export const modalTwoButtonWrapper = style([ }), ]); -export const contentBox = style([ - sprinkles({ - display: 'flex', - flexDirection: 'column', - alignItems: 'center', - }), -]); - -export const text = style([ - sprinkles({ - textSize: '1.5x', - lineHeight: '2x', - }), -]); - -export const messageModalWrapper = sprinkles({ - display: 'flex', - flexDirection: 'column', - alignItems: 'center', - justifyContent: 'spaceBetween', -}); - -export const messageText = recipe({ - base: [ - { - whiteSpace: 'pre-wrap', - textAlign: 'center', - }, - flexOptions({ option: 'center' }), - sprinkles({ marginBottom: '2x' }), - ], - variants: { - messageType: { - main: [ - sprinkles({ - fontSize: '2x', - lineHeight: '3x', - }), - ], - sub: sprinkles({ - textSize: '0.75x', - color: 'gray', - }), - }, - }, -}); - export const createLobbyTextGrid = style([ { display: 'grid', @@ -88,13 +40,15 @@ export const createLobbySelectMap = style([ }), ]); +const MAP_ITEM_HEIGHT = '100px'; + export const createLobbyMapList = style([ sprinkles({ gap: '3x', }), { display: 'grid', - gridTemplateColumns: `repeat(${constants.MAPS.length}, ${constants.MAP_ITEM_HEIGHT})`, + gridTemplateColumns: `repeat(${MAP_ID_LIST.length}, ${MAP_ITEM_HEIGHT})`, gridTemplateRows: '1fr', width: '400px', overflowX: 'auto', @@ -112,7 +66,7 @@ export const mapItemWrapper = style([ backgroundColor: 'white', }), { - height: constants.MAP_ITEM_HEIGHT, + height: MAP_ITEM_HEIGHT, ':hover': { cursor: 'pointer', diff --git a/src/components/tier/index.tsx b/src/features/room-setting/index.tsx similarity index 100% rename from src/components/tier/index.tsx rename to src/features/room-setting/index.tsx diff --git a/src/features/room-setting/type.d.ts b/src/features/room-setting/type.d.ts new file mode 100644 index 00000000..1e6c93a4 --- /dev/null +++ b/src/features/room-setting/type.d.ts @@ -0,0 +1 @@ +type RoomSetting = Omit; diff --git a/src/pages/play/finder/Modal/CreateLobby/ModalContent.tsx b/src/features/room-setting/ui.tsx similarity index 63% rename from src/pages/play/finder/Modal/CreateLobby/ModalContent.tsx rename to src/features/room-setting/ui.tsx index de368e18..3d861767 100644 --- a/src/pages/play/finder/Modal/CreateLobby/ModalContent.tsx +++ b/src/features/room-setting/ui.tsx @@ -1,36 +1,40 @@ import React, { useState } from 'react'; import { useNavigate } from 'react-router-dom'; -import type { CreateRoom } from '@/types/play'; +import { createRoom, updateRoom } from './api'; +import * as styles from './index.css'; import ColoredButton from '@/components/button/ColoredButton'; import Input from '@/components/input'; -import * as constants from '@/pages/play/finder/constants'; -import MapItem from '@/pages/play/finder/Modal/CreateLobby/MapItem'; -import * as styles from '@/pages/play/finder/Modal/index.css'; +import Map, { MAP_ID_LIST } from '@/models/map'; +import { ROUTE } from '@/shared/constants'; -import { ROUTE } from '@/constants/routes'; +const API_MAP = { + create: createRoom, + update: updateRoom, +} as const; interface Props { roomTitle: string; roomPassword: string; - mapId: number; + mapId: MapId; closeModal: () => void; - api: (roomRequest: CreateRoom) => Promise; + type: keyof typeof API_MAP; buttonText: string; } -const ModalContent = ({ +const RoomSettingForm = ({ roomTitle, roomPassword, mapId, closeModal, - api, + type, buttonText, }: Props) => { + const api = API_MAP[type]; const navigate = useNavigate(); - const [createRoomInfo, setCreateRoomInfo] = useState({ + const [roomSetting, setRoomSetting] = useState({ roomTitle, roomPassword, mapId, @@ -41,22 +45,22 @@ const ModalContent = ({ const { name, value } = event.target; if (name === 'roomTitle' && value.length > 9) return; if (name === 'roomPassword' && value.length > 4) return; - setCreateRoomInfo((info) => ({ + setRoomSetting((info) => ({ ...info, [name]: value, })); }; const handleClickCreateButton = async () => { - if (!createRoomInfo.roomTitle || createRoomInfo.roomPassword.length !== 4) { + if (!roomSetting.roomTitle || roomSetting.roomPassword.length !== 4) { setIsValidInfo(false); return; } - await api(createRoomInfo).then((roomId) => { + await api(roomSetting).then((roomId) => { closeModal(); if (roomId) { - navigate(`${ROUTE.lobby}/${roomId}`); + navigate(`${ROUTE.lobby}/${roomId}`, { replace: true }); } }); }; @@ -71,7 +75,7 @@ const ModalContent = ({ placeholder='몇 글자 가능할까요?' size='widthFull' type='text' - value={createRoomInfo.roomTitle} + value={roomSetting.roomTitle} onChange={handleChange} /> @@ -80,7 +84,7 @@ const ModalContent = ({ placeholder='숫자 4자리' size='widthFull' type='text' - value={createRoomInfo.roomPassword} + value={roomSetting.roomPassword} onChange={handleChange} />
@@ -88,19 +92,15 @@ const ModalContent = ({
- {constants.MAPS.map((item) => ( - { - setCreateRoomInfo((prev) => ({ - ...prev, - mapId: item.mapId, - })); - }} - /> + {MAP_ID_LIST.map((id) => ( + ))}
@@ -120,10 +120,10 @@ const ModalContent = ({ />
{!isValidInfo && ( -
{constants.ALERT_MESSAGE}
+
정보를 모두 입력해주세요.
)} ); }; -export default ModalContent; +export default RoomSettingForm; diff --git a/src/features/sign-up/api.ts b/src/features/sign-up/api.ts new file mode 100644 index 00000000..9ba0ddb1 --- /dev/null +++ b/src/features/sign-up/api.ts @@ -0,0 +1,42 @@ +import type { AxiosError } from 'axios'; + +import { invalidateUserQuery } from '@/models/user'; +import { client } from '@/shared/api'; + +const axiosConfig = { + headers: { + 'X-Bypass-Authorization': true, + }, +}; + +export const checkIdDuplicate = async (id: SignUpForm['id']) => { + // 중복이 안되면 false. true면 오류 + return client + .get(`/user/dup/${id}`, axiosConfig) + .then(({ data }) => data) + .catch(() => true); +}; + +export const upgradeToMember = async (formData: SignUpForm) => { + return client + .post('/user/guest', formData) + .then(({ data }) => { + console.debug(data); + invalidateUserQuery(); + return true; + }) + .catch(({ response }: AxiosError) => { + console.debug(response); + return false; + }); +}; + +export const registerMember = async (formData: SignUpForm) => { + return client + .post('/user/signup', formData, axiosConfig) + .then(({ data, status }) => { + localStorage.setItem('token', data); + return status === 200; + }) + .catch(() => false); +}; diff --git a/src/features/sign-up/types.d.ts b/src/features/sign-up/types.d.ts new file mode 100644 index 00000000..565e49a9 --- /dev/null +++ b/src/features/sign-up/types.d.ts @@ -0,0 +1,4 @@ +type SignUpForm = Account & { + passwordConfirm: Account['password']; + nickname: User['nickname']; +}; diff --git a/src/features/sign-up/ui.tsx b/src/features/sign-up/ui.tsx index 016793c0..0ef2b38a 100644 --- a/src/features/sign-up/ui.tsx +++ b/src/features/sign-up/ui.tsx @@ -1,33 +1,26 @@ import React, { useState } from 'react'; import { useNavigate } from 'react-router-dom'; -import { checkSignUpInfo } from './utils'; +import { checkIdDuplicate, registerMember, upgradeToMember } from './api'; +import { validateSignUpForm } from './utils'; -import { SignUpInfo } from '@/types/auth'; - -import ColoredButton from '@/components/button/ColoredButton/index'; +import ColoredButton from '@/components/button/ColoredButton'; import Input from '@/components/input'; import { buttonWrapper } from '@/pages/landing/index.css'; -import * as constants from '@/pages/landing/logIn/constants'; - -import { getIdCheck, postGuestSignUp, postSignUp } from '@/services/auth'; -import { setFullScreen } from '@/services/landing'; -import { ROUTE } from '@/constants/routes'; -import { USER_STATUS, useUserStore } from '@/features/user'; +import { ERROR_MESSAGE, ROUTE } from '@/shared/constants'; +import { setFullScreen } from '@/shared/utils'; interface Props { closeModal: () => void; + isGuest?: boolean; } -const SignUp = ({ closeModal }: Props) => { - const { loginStatus } = useUserStore.getState(); +const SignUp = ({ closeModal, isGuest }: Props) => { const navigate = useNavigate(); - const [errorMessage, setErrorMessage] = useState( - constants.ERROR_MESSAGE.welcome, - ); - const [signUpInfo, setSignUpInfo] = useState({ + const [errorMessage, setErrorMessage] = useState(ERROR_MESSAGE.welcome); + const [form, setForm] = useState({ id: '', password: '', passwordConfirm: '', @@ -36,43 +29,35 @@ const SignUp = ({ closeModal }: Props) => { const handleChange = (event: React.ChangeEvent) => { const { name, value } = event.target; - setSignUpInfo((info) => ({ - ...info, + setForm((data) => ({ + ...data, [name]: value, })); setErrorMessage(''); }; const clickSignUp = async () => { - const checkId = await getIdCheck(signUpInfo.id); - if (checkId) { - setErrorMessage(constants.ERROR_MESSAGE.sameId); + const isDuplicate = await checkIdDuplicate(form.id); + if (isDuplicate) { + setErrorMessage(ERROR_MESSAGE.sameId); return; } - const errorInfo = checkSignUpInfo(signUpInfo); - if (errorInfo.length >= 1) { - setErrorMessage(errorInfo); + const error = validateSignUpForm(form); + if (error) { + setErrorMessage(error); return; } - if (loginStatus === USER_STATUS.guest) { - await postGuestSignUp(signUpInfo).then((isSuccess) => { - if (isSuccess) { - closeModal(); - navigate(ROUTE.home); - } - }); + const signUp = isGuest ? upgradeToMember : registerMember; + const isSuccess = await signUp(form); + if (!isSuccess) { return; } - - if (loginStatus === USER_STATUS.notLoggedIn) { - await postSignUp(signUpInfo).then((isSuccess) => { - if (isSuccess) { - closeModal(); - navigate(ROUTE.tutorial); - setFullScreen(); - } - }); + closeModal(); + const to = isGuest ? ROUTE.home : ROUTE.tutorial; + navigate(to, { replace: true }); + if (!isGuest) { + setFullScreen(); } }; @@ -83,7 +68,7 @@ const SignUp = ({ closeModal }: Props) => { type='text' placeholder='아이디 (최소 6 ~ 20자)' size='medium' - value={signUpInfo.id} + value={form.id} onChange={handleChange} /> { type='password' placeholder='비밀번호 (숫자 6자)' size='medium' - value={signUpInfo.password} + value={form.password} onChange={handleChange} /> { type='password' placeholder='비밀번호 확인' size='medium' - value={signUpInfo.passwordConfirm} + value={form.passwordConfirm} onChange={handleChange} /> { type='text' placeholder='닉네임 (최소 2 ~ 9자)' size='medium' - value={signUpInfo.nickname} + value={form.nickname} onChange={handleChange} />
{errorMessage} diff --git a/src/features/sign-up/utils.ts b/src/features/sign-up/utils.ts index e4b2d736..3502b02e 100644 --- a/src/features/sign-up/utils.ts +++ b/src/features/sign-up/utils.ts @@ -1,20 +1,22 @@ -import { SignUpInfo } from '@/types/auth'; -import * as constants from '@/pages/landing/logIn/constants'; - -export const checkSignUpInfo = (signUpInfo: SignUpInfo) => { - const { id, password, passwordConfirm, nickname } = signUpInfo; +import { ERROR_MESSAGE } from '@/shared/constants'; +export const validateSignUpForm = ({ + id, + password, + passwordConfirm, + nickname, +}: SignUpForm) => { if ( id.trim() === '' || password.trim() === '' || passwordConfirm.trim() === '' || nickname.trim() === '' ) { - return constants.ERROR_MESSAGE.blank; + return ERROR_MESSAGE.blank; } if (password !== passwordConfirm) { - return constants.ERROR_MESSAGE.notSamePassword; + return ERROR_MESSAGE.notSamePassword; } if ( @@ -25,7 +27,7 @@ export const checkSignUpInfo = (signUpInfo: SignUpInfo) => { nickname.length < 2 || nickname.length > 9 ) { - return constants.ERROR_MESSAGE.invalidInput; + return ERROR_MESSAGE.invalidInput; } return ''; diff --git a/src/features/sound/constants.ts b/src/features/sound/constants.ts new file mode 100644 index 00000000..14036aad --- /dev/null +++ b/src/features/sound/constants.ts @@ -0,0 +1,6 @@ +export const SETTING_KEY = { + bgm: 'bgm-muted', + sfx: 'sfx-muted', +}; + +export const MUTED = true; diff --git a/src/features/sound/hooks.ts b/src/features/sound/hooks.ts index 60e0fd95..8df7598a 100644 --- a/src/features/sound/hooks.ts +++ b/src/features/sound/hooks.ts @@ -1,55 +1,36 @@ -import { useState } from 'react'; +import { useAtom, useSetAtom } from 'jotai'; -import { useBgmStore } from './store/music'; +import { MUTED } from './constants'; +import { playSfxAudioAtom, sfxSettingMutedAtom } from './store/effect'; +import { + bgmSettingMutedAtom, + playBgmAudioAtom, + updateBgmVolumeAtom, +} from './store/music'; -const SETTING_KEY = { - bgm: 'backgroundSound', - sfx: 'effectSound', -}; - -export const useSoundSetting = () => { - const isBgmMuted = localStorage.getItem(SETTING_KEY.bgm) === 'false'; - const isSfxMuted = localStorage.getItem(SETTING_KEY.sfx) === 'false'; - - const playBgm = () => { - if (isBgmMuted) { - return; - } - - const { isPlaying, playBackgroundSound } = useBgmStore.getState(); - if (!isPlaying) { - playBackgroundSound(); - } - }; +export const useBackgroundSound = () => { + const playBackgroundSound = useSetAtom(playBgmAudioAtom); - return { isBgmMuted, isSfxMuted, playBgm }; + return { playBackgroundSound }; }; export const useSoundToggle = () => { - const { isBgmMuted } = useSoundSetting(); - const [setting, setSetting] = useState(!isBgmMuted); - const isBgmPlaying = useBgmStore((state) => state.isPlaying); - const bgm = useBgmStore((state) => state.audio); + const [isBgmSettingMuted, setBgmSettingMuted] = useAtom(bgmSettingMutedAtom); + const [_, setSfxSettingMuted] = useAtom(sfxSettingMutedAtom); + const setVolume = useSetAtom(updateBgmVolumeAtom); const toggleSound = () => { - localStorage.setItem(SETTING_KEY.bgm, (!setting).toString()); - localStorage.setItem(SETTING_KEY.sfx, (!setting).toString()); - setSetting((isSettingOn) => { - if (isSettingOn) { - // turn off sound - bgm.volume = 0; - return false; - } - - // turn on sound - if (isBgmPlaying) { - bgm.volume = 1; - } else { - useBgmStore.getState().playBackgroundSound(); - } - return true; - }); + const nextSetting = !isBgmSettingMuted; + setBgmSettingMuted(nextSetting); + setSfxSettingMuted(nextSetting); + setVolume(nextSetting === MUTED ? 0 : 1); }; - return { backgroundSound: setting, toggleSound }; + return { backgroundSound: !isBgmSettingMuted, toggleSound }; +}; + +export const useEffectSound = () => { + const playEffectSound = useSetAtom(playSfxAudioAtom); + + return { playEffectSound }; }; diff --git a/src/features/sound/index.ts b/src/features/sound/index.ts index 8c220a86..4cc90d02 100644 --- a/src/features/sound/index.ts +++ b/src/features/sound/index.ts @@ -1 +1 @@ -export { useSoundSetting, useSoundToggle } from './hooks'; +export * from './hooks'; diff --git a/src/features/sound/store/effect.ts b/src/features/sound/store/effect.ts new file mode 100644 index 00000000..01a3b66f --- /dev/null +++ b/src/features/sound/store/effect.ts @@ -0,0 +1,16 @@ +import { atom } from 'jotai'; +import { atomWithStorage } from 'jotai/utils'; + +import { SETTING_KEY } from '../constants'; + +const sfxAudioAtom = atom(new Audio('/music/blop.mp3')); + +export const sfxSettingMutedAtom = atomWithStorage(SETTING_KEY.sfx, false); + +export const playSfxAudioAtom = atom(null, (get) => { + const isMuted = get(sfxSettingMutedAtom); + if (!isMuted) { + const audio = get(sfxAudioAtom); + audio.play().catch((error) => console.error('SFX 오류', error)); + } +}); diff --git a/src/features/sound/store/music.ts b/src/features/sound/store/music.ts index cdfdeda4..5fae594b 100644 --- a/src/features/sound/store/music.ts +++ b/src/features/sound/store/music.ts @@ -1,19 +1,28 @@ -import { create } from 'zustand'; +import { atom } from 'jotai'; +import { atomWithStorage } from 'jotai/utils'; -interface BgmState { - isPlaying: boolean; - audio: HTMLAudioElement; - playBackgroundSound: () => void; -} +import { SETTING_KEY } from '../constants'; -export const useBgmStore = create((set) => ({ - isPlaying: false, - audio: new Audio('/music/8-bit-game.mp3'), +const bgmAudioAtom = atom(new Audio('/music/8-bit-game.mp3')); - playBackgroundSound: () => - set((state) => { - state.audio.loop = true; - state.audio.play().catch((error) => console.error('음악 없음', error)); - return { isPlaying: true }; - }), -})); +const bgmPlayingAtom = atom(false); + +export const bgmSettingMutedAtom = atomWithStorage(SETTING_KEY.bgm, false); + +export const playBgmAudioAtom = atom(null, (get, set) => { + const isSettingMuted = get(bgmSettingMutedAtom); + const isPlaying = get(bgmPlayingAtom); + if (isSettingMuted || isPlaying) { + return; + } + + const audio = get(bgmAudioAtom); + audio.loop = true; + audio.play().catch((error) => console.error('BGM 오류', error)); + set(bgmPlayingAtom, true); +}); + +export const updateBgmVolumeAtom = atom(null, (get, _, volume: number) => { + const audio = get(bgmAudioAtom); + audio.volume = volume; +}); diff --git a/src/features/user/constants.ts b/src/features/user/constants.ts deleted file mode 100644 index 34eb86bb..00000000 --- a/src/features/user/constants.ts +++ /dev/null @@ -1,5 +0,0 @@ -export const USER_STATUS = { - notLoggedIn: 0, - guest: 1, - member: 2, -}; diff --git a/src/features/user/hooks.ts b/src/features/user/hooks.ts deleted file mode 100644 index bf0178e9..00000000 --- a/src/features/user/hooks.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { USER_STATUS } from './constants'; -import { useUserStore } from './store'; - -export const useUserStatus = () => { - const status = useUserStore((state) => state.loginStatus); - - return { - isGuest: status === USER_STATUS.guest, - isMember: status === USER_STATUS.member, - }; -}; diff --git a/src/features/user/index.ts b/src/features/user/index.ts deleted file mode 100644 index c123d7c5..00000000 --- a/src/features/user/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export { useUserStore } from './store'; -export { USER_STATUS } from './constants'; -export { useUserStatus } from './hooks'; diff --git a/src/features/user/store.ts b/src/features/user/store.ts deleted file mode 100644 index d38110bd..00000000 --- a/src/features/user/store.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { create } from 'zustand'; -import { persist } from 'zustand/middleware'; - -import { USER_STATUS } from './constants'; - -interface UserState { - loginStatus: number; - setLogin: (isGuest: boolean) => void; - setLogout: () => void; - userCode: string; - setUserCode: (userCode: string) => void; -} - -export const useUserStore = create()( - persist( - (set) => ({ - loginStatus: USER_STATUS.notLoggedIn, - setLogin: (isGuest) => - set(() => { - if (isGuest) { - return { loginStatus: USER_STATUS.guest }; - } - return { loginStatus: USER_STATUS.member }; - }), - setLogout: () => set({ loginStatus: USER_STATUS.notLoggedIn }), - userCode: '', - setUserCode: (userCode) => set({ userCode }), - }), - { - name: 'user-state', - }, - ), -); diff --git a/src/features/websocket/hooks.ts b/src/features/websocket/hooks.ts index 3c965155..3bd58fbd 100644 --- a/src/features/websocket/hooks.ts +++ b/src/features/websocket/hooks.ts @@ -1,16 +1,16 @@ +import { useAtomValue } from 'jotai'; import { useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; -import { type Socket } from './model/Socket'; -import { useWebSocketStore } from './store'; +import { gameWebSocketAtom, webSocketAtom } from './store'; -import { ROUTE } from '@/constants/routes'; +import { ROUTE } from '@/shared/constants'; export const useWebSocket = ( onMessage: (message: T) => void, ) => { const navigate = useNavigate(); - const client = useWebSocketStore((state) => state.webSocket) as Socket; + const webSocket = useAtomValue(webSocketAtom); const handleMessage = (message: T) => { if (message.action === 'invalidToken') { @@ -21,18 +21,24 @@ export const useWebSocket = ( }; useEffect(() => { - if (!client.isConnected()) { - client.connect(); + if (!webSocket.isConnected()) { + webSocket.connect(); } - client.onMessage(handleMessage); + webSocket.onMessage(handleMessage); - client.onClose(() => { - client.connect(); - client.onMessage(handleMessage); + webSocket.onClose(() => { + webSocket.connect(); + webSocket.onMessage(handleMessage); }); return () => { - client.onUnmount(); + webSocket.cleanUp(); }; }, []); }; + +export const useGameWebSocket = () => { + const webSocket = useAtomValue(gameWebSocketAtom); + + return { webSocket }; +}; diff --git a/src/features/websocket/index.ts b/src/features/websocket/index.ts index 3f848728..4cc90d02 100644 --- a/src/features/websocket/index.ts +++ b/src/features/websocket/index.ts @@ -1,2 +1 @@ -export { useWebSocket } from './hooks'; -export { useWebSocketStore } from './store'; +export * from './hooks'; diff --git a/src/features/websocket/model/GameSocket.ts b/src/features/websocket/model/GameSocket.ts index 1ea53a72..e9e17895 100644 --- a/src/features/websocket/model/GameSocket.ts +++ b/src/features/websocket/model/GameSocket.ts @@ -1,7 +1,5 @@ import { Socket } from './Socket'; -import { characterInfo, characterInfoIndex } from '@/types/ingame'; - export class GameSocket extends Socket { private messageQueue: ArrayBuffer[]; @@ -33,8 +31,8 @@ export class GameSocket extends Socket { this.webSocket.send(buffer); } - inGameUnconnected(onCloseEvent: () => void) { - this.webSocket.onclose = onCloseEvent; + onDisconnect(callback: () => void) { + this.webSocket.onclose = callback; } } diff --git a/src/features/websocket/model/Socket.ts b/src/features/websocket/model/Socket.ts index 019233e7..f4af105f 100644 --- a/src/features/websocket/model/Socket.ts +++ b/src/features/websocket/model/Socket.ts @@ -45,7 +45,7 @@ export class Socket { }; } - onUnmount() { + cleanUp() { this.webSocket.onmessage = () => {}; this.webSocket.onclose = () => {}; } diff --git a/src/features/websocket/store.ts b/src/features/websocket/store.ts index a87fa288..b10dae3f 100644 --- a/src/features/websocket/store.ts +++ b/src/features/websocket/store.ts @@ -1,11 +1,8 @@ -import { create } from 'zustand'; +import { atom } from 'jotai'; -import { GameSocket } from '@/features/websocket/model/GameSocket'; +import { GameSocket } from './model/GameSocket'; +import { type Socket } from './model/Socket'; -interface WebsocketStore { - webSocket: GameSocket; -} +export const gameWebSocketAtom = atom(new GameSocket()); -export const useWebSocketStore = create(() => ({ - webSocket: new GameSocket(), -})); +export const webSocketAtom = atom((get) => get(gameWebSocketAtom)); diff --git a/src/features/websocket/types.d.ts b/src/features/websocket/types.d.ts index 7904ed6e..8a0ba5c9 100644 --- a/src/features/websocket/types.d.ts +++ b/src/features/websocket/types.d.ts @@ -11,7 +11,7 @@ type WebSocketMessageMap = { }; friendlyMatch: { - roomInfo: Lobby; + roomInfo: EnteredRoom; gameStart: null; }; }; @@ -34,3 +34,5 @@ type WebSocketMessage = | WebsocketMessageAuth | WebsocketMessageRankedMatch | WebsocketMessageFriendlyMatch; + +type GameSocket = import('./model/GameSocket').GameSocket; diff --git a/src/hooks/README.md b/src/hooks/README.md deleted file mode 100644 index e97e7257..00000000 --- a/src/hooks/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# hooks - -### description - -* react hooks를 정의합니다. \ No newline at end of file diff --git a/src/hooks/useModal.tsx b/src/hooks/useModal.tsx deleted file mode 100644 index 58fa5816..00000000 --- a/src/hooks/useModal.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import { ReactNode, useCallback, useRef } from 'react'; - -import ModalComponent from '@/components/modal'; - -interface ModalProps { - children: ReactNode; -} - -const useModal = ( - props: { - onOpen?: () => void; - onClose?: () => void; - } = {}, -) => { - const { onOpen, onClose } = props; - const ref = useRef(null); - - const openModal = () => { - ref.current?.showModal(); - if (onOpen) { - onOpen(); - } - }; - - const closeModal = () => { - ref.current?.close(); - if (onClose) { - onClose(); - } - }; - - const Modal = useCallback(({ children }: ModalProps) => { - return {children}; - }, []); - - return { Modal, openModal, closeModal }; -}; - -export default useModal; diff --git a/src/models/collection/api.ts b/src/models/collection/api.ts new file mode 100644 index 00000000..d184fc52 --- /dev/null +++ b/src/models/collection/api.ts @@ -0,0 +1,16 @@ +import { COLLECTION_KEY } from './constants'; + +import { client, queryClient } from '@/shared/api'; + +export const getCollection = async () => { + return client.get('/collection').then((response) => { + const { data } = response; + console.debug(data); + return data; + }); +}; + +export const refetchCollection = () => + queryClient.invalidateQueries({ + queryKey: [COLLECTION_KEY], + }); diff --git a/src/models/collection/constants.ts b/src/models/collection/constants.ts new file mode 100644 index 00000000..7b211afe --- /dev/null +++ b/src/models/collection/constants.ts @@ -0,0 +1 @@ +export const COLLECTION_KEY = 'collection'; diff --git a/src/models/collection/hooks.ts b/src/models/collection/hooks.ts new file mode 100644 index 00000000..8dec89ce --- /dev/null +++ b/src/models/collection/hooks.ts @@ -0,0 +1,32 @@ +import { useQuery } from '@tanstack/react-query'; +import { getDefaultStore, useSetAtom } from 'jotai'; + +import { getCollection } from './api'; +import { COLLECTION_KEY } from './constants'; +import { collectionStaleAtom } from './store'; + +export const useCollection = () => { + const store = getDefaultStore(); + const isStale = store.get(collectionStaleAtom); + + const { data } = useQuery({ + queryKey: [COLLECTION_KEY], + queryFn: async () => { + const collection = await getCollection(); + store.set(collectionStaleAtom, false); + return collection; + }, + refetchOnMount: isStale, + refetchOnWindowFocus: false, + }); + + return { collection: data }; +}; + +export const useCollectionStale = () => { + const setStale = useSetAtom(collectionStaleAtom); + + return { + setCollectionStale: () => setStale(true), + }; +}; diff --git a/src/models/collection/index.ts b/src/models/collection/index.ts new file mode 100644 index 00000000..66a26bb4 --- /dev/null +++ b/src/models/collection/index.ts @@ -0,0 +1,2 @@ +export { refetchCollection } from './api'; +export * from './hooks'; diff --git a/src/models/collection/store.ts b/src/models/collection/store.ts new file mode 100644 index 00000000..3a2b9b5e --- /dev/null +++ b/src/models/collection/store.ts @@ -0,0 +1,3 @@ +import { atom } from 'jotai'; + +export const collectionStaleAtom = atom(false); diff --git a/src/types/collections.d.ts b/src/models/collection/types.d.ts similarity index 57% rename from src/types/collections.d.ts rename to src/models/collection/types.d.ts index 31f05bb0..9e117b98 100644 --- a/src/types/collections.d.ts +++ b/src/models/collection/types.d.ts @@ -1,5 +1,4 @@ - -export interface Skins{ +interface Skin { id: number; name: string; link: string; @@ -7,7 +6,7 @@ export interface Skins{ equipped: boolean; } -export interface Labels { +interface Label { id: number; name: string; condition: string; @@ -15,14 +14,15 @@ export interface Labels { equipped: boolean; } -export interface Achievements { +interface Achievement { id: number; name: string; reward: string; achieved: boolean; } -export interface Collections { - skins: Skins[]; - labels: Labels[]; - achievements: Achievements[]; + +interface Collections { + skins: Skin[]; + labels: Label[]; + achievements: Achievement[]; } diff --git a/src/models/map/constants.ts b/src/models/map/constants.ts new file mode 100644 index 00000000..94dab081 --- /dev/null +++ b/src/models/map/constants.ts @@ -0,0 +1,26 @@ +export const MAPS = { + 0: { + imgSrc: '/images/map/background/random-map-rainbow-h450w450.png', + mapName: '랜덤', + }, + 4: { + imgSrc: '/images/map/background/factorymap.png', + mapName: '팩토리', + }, + 3: { + imgSrc: '/images/map/background/foodmap2.png', + mapName: '푸디2', + }, + 1: { + imgSrc: '/images/map/background/basicmap.png', + mapName: '베이직', + }, + 2: { + imgSrc: '/images/map/background/foodmap.png', + mapName: '푸디', + }, +} as const satisfies Record; + +export const MAP_ID_LIST = Object.keys(MAPS).map( + (key) => Number(key) as keyof typeof MAPS, +); diff --git a/src/models/map/index.ts b/src/models/map/index.ts new file mode 100644 index 00000000..62ac708c --- /dev/null +++ b/src/models/map/index.ts @@ -0,0 +1,2 @@ +export * from './constants'; +export { default } from './ui'; diff --git a/src/models/map/types.d.ts b/src/models/map/types.d.ts new file mode 100644 index 00000000..93265057 --- /dev/null +++ b/src/models/map/types.d.ts @@ -0,0 +1 @@ +type MapId = keyof typeof import('./constants').MAPS; diff --git a/src/models/map/ui.tsx b/src/models/map/ui.tsx new file mode 100644 index 00000000..33d20b77 --- /dev/null +++ b/src/models/map/ui.tsx @@ -0,0 +1,27 @@ +import { MAPS } from './constants'; + +import Chip from '@/components/chip'; +import ImageBox from '@/components/image-box'; + +interface Props { + mapId: MapId; + size?: Parameters[0]['size']; + selected?: boolean; +} + +const Map = ({ size = 'full', mapId, selected }: Props) => { + const { imgSrc, mapName } = MAPS[mapId]; + + return ( + + + + ); +}; + +export default Map; diff --git a/src/models/player/index.ts b/src/models/player/index.ts new file mode 100644 index 00000000..fb22b089 --- /dev/null +++ b/src/models/player/index.ts @@ -0,0 +1 @@ +export const PLAYER_COLORS = ['pink', 'green', 'yellow', 'blue'] as const; diff --git a/src/models/player/types.d.ts b/src/models/player/types.d.ts new file mode 100644 index 00000000..0dc57b46 --- /dev/null +++ b/src/models/player/types.d.ts @@ -0,0 +1,8 @@ +type Player = Pick< + User, + 'userCode' | 'nickname' | 'tier' | 'skin' | 'label' +> & { + ready: boolean; + isMaster: boolean; + key: string; +}; diff --git a/src/models/room/api.ts b/src/models/room/api.ts new file mode 100644 index 00000000..5a23c546 --- /dev/null +++ b/src/models/room/api.ts @@ -0,0 +1,12 @@ +import { client } from '@/shared/api'; + +export const leaveRoom = async () => { + return client + .get('/play/friendly/out') + .then(({ data }) => { + console.debug('leave room:', data); + }) + .catch((err) => { + console.debug(err); + }); +}; diff --git a/src/models/room/index.ts b/src/models/room/index.ts new file mode 100644 index 00000000..d9b9c566 --- /dev/null +++ b/src/models/room/index.ts @@ -0,0 +1 @@ +export { leaveRoom } from './api'; diff --git a/src/models/room/types.d.ts b/src/models/room/types.d.ts new file mode 100644 index 00000000..d1bd7d7a --- /dev/null +++ b/src/models/room/types.d.ts @@ -0,0 +1,13 @@ +interface Room { + roomTitle: string; + mapId: MapId; + roomCode: string; + roomPassword: string; +} + +interface EnteredRoom extends Room { + roomUuid: string; + masterIndex: number; + readyState: boolean[]; + players: Player[]; +} diff --git a/src/constants/tier.ts b/src/models/tier/constants.ts similarity index 100% rename from src/constants/tier.ts rename to src/models/tier/constants.ts diff --git a/src/models/tier/index.tsx b/src/models/tier/index.tsx new file mode 100644 index 00000000..6f1968bc --- /dev/null +++ b/src/models/tier/index.tsx @@ -0,0 +1 @@ +export { default } from './ui'; diff --git a/src/models/tier/types.d.ts b/src/models/tier/types.d.ts new file mode 100644 index 00000000..607743db --- /dev/null +++ b/src/models/tier/types.d.ts @@ -0,0 +1 @@ +type TierValue = (typeof import('./constants').TIER)[number]; diff --git a/src/components/tier/index.css.ts b/src/models/tier/ui/index.css.ts similarity index 100% rename from src/components/tier/index.css.ts rename to src/models/tier/ui/index.css.ts diff --git a/src/components/tier/index.stories.ts b/src/models/tier/ui/index.stories.ts similarity index 97% rename from src/components/tier/index.stories.ts rename to src/models/tier/ui/index.stories.ts index ab3b8803..3012f646 100644 --- a/src/components/tier/index.stories.ts +++ b/src/models/tier/ui/index.stories.ts @@ -1,6 +1,7 @@ -import Tier from './ui'; import { size, tier } from './variants.css'; +import Tier from './index'; + import type { Meta, StoryObj } from '@storybook/react-vite'; const meta = { diff --git a/src/components/tier/ui.tsx b/src/models/tier/ui/index.tsx similarity index 100% rename from src/components/tier/ui.tsx rename to src/models/tier/ui/index.tsx diff --git a/src/components/tier/variants.css.ts b/src/models/tier/ui/variants.css.ts similarity index 91% rename from src/components/tier/variants.css.ts rename to src/models/tier/ui/variants.css.ts index 0c27759c..7026df2d 100644 --- a/src/components/tier/variants.css.ts +++ b/src/models/tier/ui/variants.css.ts @@ -1,8 +1,9 @@ import { style } from '@vanilla-extract/css'; +import { TIER } from '../constants'; + import { variant } from '@/styles/utils'; -import { TIER } from '@/constants/tier'; export const tier = variant([...TIER], (name) => style({ diff --git a/src/models/user/api.ts b/src/models/user/api.ts new file mode 100644 index 00000000..22502af0 --- /dev/null +++ b/src/models/user/api.ts @@ -0,0 +1,9 @@ +import { client } from '@/shared/api'; + +export const getUser = async () => { + return client.get(`/user`).then((response) => { + const { data } = response; + console.debug('user:', data); + return data; + }); +}; diff --git a/src/models/user/constants.ts b/src/models/user/constants.ts new file mode 100644 index 00000000..b5bb4812 --- /dev/null +++ b/src/models/user/constants.ts @@ -0,0 +1 @@ +export const queryKey = ['user']; diff --git a/src/models/user/hooks.ts b/src/models/user/hooks.ts new file mode 100644 index 00000000..cf518270 --- /dev/null +++ b/src/models/user/hooks.ts @@ -0,0 +1,49 @@ +import { useQuery } from '@tanstack/react-query'; +import { getDefaultStore, useAtomValue, useSetAtom } from 'jotai'; + +import { getUser } from './api'; +import { queryKey } from './constants'; +import { updateUserCodeAtom, userCodeAtom, userStaleAtom } from './store'; +import { invalidateUserQuery } from './utils'; + +export const useUserInfo = () => { + const setUserCode = useSetAtom(updateUserCodeAtom); + const store = getDefaultStore(); + const isStale = store.get(userStaleAtom); + + const { data } = useQuery({ + queryKey, + queryFn: async () => { + const user = await getUser(); + setUserCode(user.userCode); + return user; + }, + throwOnError: true, + refetchOnWindowFocus: false, + refetchOnMount: isStale, + }); + + return { + user: data, + refetchUser: invalidateUserQuery, + }; +}; + +export const useUserStatus = () => { + const { user } = useUserInfo(); + + return { + isGuest: user && user.guest, + isMember: user && !user.guest, + }; +}; + +export const useUserCode = () => useAtomValue(userCodeAtom); + +export const useUserStale = () => { + const setIsStale = useSetAtom(userStaleAtom); + + return { + setUserStale: () => setIsStale(true), + }; +}; diff --git a/src/models/user/index.ts b/src/models/user/index.ts new file mode 100644 index 00000000..fd70c425 --- /dev/null +++ b/src/models/user/index.ts @@ -0,0 +1,2 @@ +export * from './hooks'; +export * from './utils'; diff --git a/src/models/user/store.ts b/src/models/user/store.ts new file mode 100644 index 00000000..74affe5e --- /dev/null +++ b/src/models/user/store.ts @@ -0,0 +1,12 @@ +import { atom } from 'jotai'; + +type UserCode = User['userCode']; + +export const userStaleAtom = atom(false); + +export const userCodeAtom = atom(''); + +export const updateUserCodeAtom = atom(null, (_, set, code: UserCode) => { + set(userCodeAtom, code); + set(userStaleAtom, false); +}); diff --git a/src/models/user/types.d.ts b/src/models/user/types.d.ts new file mode 100644 index 00000000..fcc8743c --- /dev/null +++ b/src/models/user/types.d.ts @@ -0,0 +1,16 @@ +type Profile = { + rank: number; + nickname: string; + skin: string; + label: string; + rating: number; + tier: TierValue; +}; + +type User = Profile & { + exp: number; + expRequire: number; + guest: boolean; + level: number; + userCode: string; +}; diff --git a/src/models/user/utils.ts b/src/models/user/utils.ts new file mode 100644 index 00000000..ed47d356 --- /dev/null +++ b/src/models/user/utils.ts @@ -0,0 +1,8 @@ +import { queryKey } from './constants'; + +import { queryClient } from '@/shared/api'; + +export const removeUserQuery = () => queryClient.removeQueries({ queryKey }); + +export const invalidateUserQuery = () => + queryClient.invalidateQueries({ queryKey }); diff --git a/src/pages/collection/Achievement.tsx b/src/pages/collection/Achievement.tsx deleted file mode 100644 index d2aad89f..00000000 --- a/src/pages/collection/Achievement.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { useEffect, useState } from 'react'; - -import * as styles from './index.css'; - -import { Achievements } from '@/types/collections'; - -import { getCollections } from '@/services/collections'; - -const Achievement = () => { - const [achievements, setAchievement] = useState([]); - - useEffect(() => { - getCollections().then((collections) => { - if (collections && collections.achievements) { - setAchievement(collections.achievements); - } - }); - }, []); - - return ( -
-
- {achievements.map((achievement) => ( -
-
-
{achievement.name}
-
{achievement.reward}
-
-
- ))} -
-
- ); -}; - -export default Achievement; diff --git a/src/pages/collection/Label.tsx b/src/pages/collection/Label.tsx deleted file mode 100644 index b46a8767..00000000 --- a/src/pages/collection/Label.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { useEffect, useState } from 'react'; -import {useLoaderData} from "react-router-dom"; - -import * as styles from './index.css'; - -import {User} from "@/types/auth"; -import { Labels } from '@/types/collections'; - -import { getCollections } from '@/services/collections'; - -import { useCollectionStateStore } from '@/states/collection'; - -const Label = () => { - const user = useLoaderData() as User; - const { setLabel, setLabelName } = useCollectionStateStore(); - const [labels, setLabels] = useState([]); - - useEffect(() => { - if(user.label){ - setLabelName(user.label); - } - - getCollections().then((collections) => { - if (collections && collections.labels) { - setLabels(collections.labels); - } - }); - }, []); - - const labelClick = (label: Labels) => { - if (label.own) { - setLabel(label.id); - setLabelName(label.name); - } - }; - - return ( -
-
- {labels.map((label) => ( -
labelClick(label)} - className={label.own ? styles.achievedBox : styles.notAchievedBox} - > -
-
{label.name}
-
-
- ))} -
-
- ); -}; - -export default Label; diff --git a/src/pages/collection/Skin.tsx b/src/pages/collection/Skin.tsx deleted file mode 100644 index 10b42eb8..00000000 --- a/src/pages/collection/Skin.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import { useEffect, useState } from 'react'; - -import * as styles from './index.css'; - -import { Skins } from '@/types/collections'; - -import RoundCornerImageBox from '@/components/image-box'; - -import { getCollections } from '@/services/collections'; - -import { useCollectionStateStore } from '@/states/collection'; - -const Skin = () => { - const { skinId, setSkinId, setSkinUrl } = useCollectionStateStore(); - const [skins, setSkins] = useState([]); - - useEffect(() => { - getCollections().then((collections) => { - if (collections && collections.skins) { - setSkins(collections.skins); - setSkinId(-1); - } - }); - }, []); - - return ( -
-
- {skins.map((skin) => ( -
{ - if (skin.own) { - setSkinId(skin.id); - setSkinUrl(skin.link); - } - }} - > - - {!skin.own && ( -
-
-
- )} - -
- ))} -
-
- ); -}; - -export default Skin; diff --git a/src/pages/collection/api.ts b/src/pages/collection/api.ts new file mode 100644 index 00000000..8b7a007e --- /dev/null +++ b/src/pages/collection/api.ts @@ -0,0 +1,23 @@ +import { client } from '@/shared/api'; + +export const updateSkin = async (skinId: number) => { + return client + .patch('/collection/skin', { skinId }) + .then((response) => { + const { data } = response; + console.debug(data); + return data; + }); +}; + +export const updateLabel = async (labelId: number) => { + return client + .patch('/collection/label', { + labelId, + }) + .then((response) => { + const { data } = response; + console.debug(data); + return data; + }); +}; diff --git a/src/pages/collection/constatns.ts b/src/pages/collection/constants.ts similarity index 94% rename from src/pages/collection/constatns.ts rename to src/pages/collection/constants.ts index b3315ac0..15fb03ac 100644 --- a/src/pages/collection/constatns.ts +++ b/src/pages/collection/constants.ts @@ -6,8 +6,6 @@ export const CATEGORY_BUTTON_GAP = '16px'; export const GRID_GAP = '8px'; -export const SUBMIT_BUTTON_MARGIN = '8px'; - export const BOX_STYLE = { WIDTH: '200px', HEIGHT: '130px', diff --git a/src/pages/collection/hooks.ts b/src/pages/collection/hooks.ts new file mode 100644 index 00000000..8ed194d8 --- /dev/null +++ b/src/pages/collection/hooks.ts @@ -0,0 +1,24 @@ +import { useAtomValue, useSetAtom } from 'jotai'; + +import { label, labelName, skin, skinUrl } from './store'; + +export const useSelectedSkinUrl = () => useAtomValue(skinUrl); + +export const useSelectedLabelName = () => useAtomValue(labelName); + +export const useSelectedCollection = () => { + return { + skin: useAtomValue(skin), + label: useAtomValue(label), + }; +}; + +export const useSetCollection = () => { + const setSelectedSkin = useSetAtom(skin); + const setSelectedLabel = useSetAtom(label); + + return { + setSelectedSkin, + setSelectedLabel, + }; +}; diff --git a/src/pages/collection/index.tsx b/src/pages/collection/index.tsx index 00f4b164..0c0a8664 100644 --- a/src/pages/collection/index.tsx +++ b/src/pages/collection/index.tsx @@ -1,119 +1 @@ -import { useEffect, useState } from 'react'; -import { useLoaderData } from 'react-router-dom'; - -import * as styles from './index.css'; - -import { User } from '@/types/auth'; - -import SettingTextButton from '@/components/button/SettingTextButton'; -import BasicContentFrame from '@/components/frame/with-buttons'; - -import Achievement from '@/pages/collection/Achievement'; -import Label from '@/pages/collection/Label'; -import Skin from '@/pages/collection/Skin'; - -import { patchLabelChange, patchSkinChange } from '@/services/collections'; - -import { useCollectionStateStore } from '@/states/collection'; - -const Collection = () => { - const user = useLoaderData() as User; - const { skinId, skinUrl, labelId, labelName, setLabelName, setSkinUrl } = - useCollectionStateStore(); - const [saveSuccess, setSaveSuccess] = useState(false); - const [selectedCategory, setSelectedCategory] = useState('skin'); - - const renderComponent = () => { - switch (selectedCategory) { - case 'skin': - return ; - case 'label': - return