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 (
<>
메뉴
-
);
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(
- ,
- document.getElementById('modal') as HTMLDivElement,
- );
-});
+const Modal = memo(
+ forwardRef(({ children }, ref) => {
+ return createPortal(
+ ,
+ 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 = ({
@@ -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}
- >
-
-
- ))}
-
-
- );
-};
-
-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 ;
- case 'achievement':
- return ;
- default:
- return ;
- }
- };
-
- const categorySubmit = async () => {
- setSaveSuccess(false);
-
- if (selectedCategory === 'skin') {
- await patchSkinChange(skinId);
- }
- if (selectedCategory === 'label') {
- await patchLabelChange(labelId);
- }
- setSaveSuccess(true);
- setTimeout(() => setSaveSuccess(false), 2000);
- };
-
- useEffect(() => {
- if (user.label && user.skin) {
- setLabelName(user.label);
- setSkinUrl(user.skin);
- }
- }, [setLabelName, user.label]);
-
- return (
-
-
-
- setSelectedCategory('skin')}
- size='small'
- colorStyle={selectedCategory === 'skin' ? 'black' : 'realDark'}
- >
- 스킨
-
- setSelectedCategory('label')}
- size='small'
- colorStyle={selectedCategory === 'label' ? 'black' : 'realDark'}
- >
- 칭호
-
- setSelectedCategory('achievement')}
- size='small'
- colorStyle={
- selectedCategory === 'achievement' ? 'black' : 'realDark'
- }
- >
- 업적
-
-
-
- {saveSuccess && (
-
장착 완료!
- )}
-
-
- {`< ${labelName} >`}
-
-
- {selectedCategory !== 'achievement' && (
-
- 저장
-
- )}
-
-
-
-
{renderComponent()}
-
-
- );
-};
-
-export default Collection;
+export { default } from './ui/page';
diff --git a/src/pages/collection/provider.tsx b/src/pages/collection/provider.tsx
new file mode 100644
index 00000000..51c42b1e
--- /dev/null
+++ b/src/pages/collection/provider.tsx
@@ -0,0 +1,25 @@
+import { createStore, Provider } from 'jotai';
+import { useMemo } from 'react';
+
+import { labelName, skinUrl } from './store';
+
+interface Props {
+ children: React.ReactNode;
+ equippedSkinUrl: string;
+ equippedLabel: string;
+}
+
+export const CollectionProvider = ({
+ children,
+ equippedSkinUrl,
+ equippedLabel,
+}: Props) => {
+ const collectionStore = useMemo(() => {
+ const store = createStore();
+ store.set(skinUrl, equippedSkinUrl);
+ store.set(labelName, equippedLabel);
+ return store;
+ }, []);
+
+ return {children};
+};
diff --git a/src/pages/collection/store.ts b/src/pages/collection/store.ts
new file mode 100644
index 00000000..cf934b12
--- /dev/null
+++ b/src/pages/collection/store.ts
@@ -0,0 +1,27 @@
+import { atom } from 'jotai';
+
+export const skinUrl = atom('');
+export const skinId = atom(0);
+export const skin = atom(
+ (get) => ({
+ url: get(skinUrl),
+ id: get(skinId),
+ }),
+ (_, set, selected: Skin) => {
+ set(skinUrl, selected.link);
+ set(skinId, selected.id);
+ },
+);
+
+export const labelName = atom('');
+export const labelId = atom(0);
+export const label = atom(
+ (get) => ({
+ name: get(labelName),
+ id: get(labelId),
+ }),
+ (_, set, selected: Label) => {
+ set(labelName, selected.name);
+ set(labelId, selected.id);
+ },
+);
diff --git a/src/pages/collection/ui/menu/achievement-menu.tsx b/src/pages/collection/ui/menu/achievement-menu.tsx
new file mode 100644
index 00000000..0e65179c
--- /dev/null
+++ b/src/pages/collection/ui/menu/achievement-menu.tsx
@@ -0,0 +1,27 @@
+import * as styles from './index.css';
+
+interface Props {
+ achievements: Achievement[];
+}
+
+const AchievementMenu = ({ achievements }: Props) => {
+ return (
+
+ {achievements.map((achievement) => (
+
+
+
{achievement.name}
+
{achievement.reward}
+
+
+ ))}
+
+ );
+};
+
+export default AchievementMenu;
diff --git a/src/pages/collection/index.css.ts b/src/pages/collection/ui/menu/index.css.ts
similarity index 70%
rename from src/pages/collection/index.css.ts
rename to src/pages/collection/ui/menu/index.css.ts
index 96461852..1f9789fd 100644
--- a/src/pages/collection/index.css.ts
+++ b/src/pages/collection/ui/menu/index.css.ts
@@ -1,36 +1,11 @@
import { style } from '@vanilla-extract/css';
+import { recipe } from '@vanilla-extract/recipes';
-import * as constants from './constatns';
+import * as constants from '../../constants';
import { flexOptions } from '@/styles/common.css';
import { sprinkles } from '@/styles/sprinkles.css';
-export const gridBox = style({
- display: 'grid',
- gridTemplateColumns: '35% 65%',
- gridTemplateRows: '15% 1fr',
- gridTemplateAreas: `
- ". a"
- "b c"
- `,
- height: '100%',
- overflow: 'hidden',
-});
-
-export const characterWrapper = style({
- gridArea: 'b',
- display: 'flex',
- flexDirection: 'column',
- alignItems: 'center',
- justifyContent: 'flex-end',
-});
-
-export const character = style([
- sprinkles({
- width: 'full',
- }),
-]);
-
export const collections = style([
sprinkles({
borderRadius: '2x',
@@ -48,50 +23,7 @@ export const categoryButton = style([
gridArea: 'a',
gap: constants.CATEGORY_BUTTON_GAP,
},
-]);
-
-export const label = style([
- sprinkles({
- fontWeight: 'accent',
- textSize: '1x',
- width: 'full',
- }),
- {
- position: 'absolute',
- textAlign: 'center',
- },
-]);
-
-export const mySkin = style([
- flexOptions({ option: 'column' }),
- sprinkles({
- width: 'full',
- }),
- {
- position: 'relative',
- overflow: 'hidden',
- aspectRatio: '1 / 1',
- backgroundRepeat: 'no-repeat',
- backgroundClip: 'border-box',
- backgroundSize: 'cover',
- backgroundPosition: 'center',
- },
-]);
-
-export const submitButtonWrapper = style([
- flexOptions({ option: 'columnCenter' }),
- {
- position: 'relative',
- marginTop: constants.SUBMIT_BUTTON_MARGIN,
- },
-]);
-
-export const submitSuccessMessage = style([
- sprinkles({
- color: 'blue',
- textSize: '1.5x',
- marginBottom: '2x',
- }),
+ sprinkles({ paddingBottom: '1x' }),
]);
export const skinWrapper = style([
@@ -198,3 +130,25 @@ export const mosaic = style({
width: '65%',
height: '20%',
});
+
+export const skinItem = recipe({
+ base: {
+ backgroundColor: 'transparent',
+ },
+ variants: {
+ owned: {
+ false: {
+ ':hover': {
+ cursor: 'not-allowed',
+ },
+ },
+ },
+ },
+});
+
+export const menu = style([
+ flexOptions({ option: 'column' }),
+ {
+ width: '65%',
+ },
+]);
diff --git a/src/pages/collection/ui/menu/index.tsx b/src/pages/collection/ui/menu/index.tsx
new file mode 100644
index 00000000..14a84e57
--- /dev/null
+++ b/src/pages/collection/ui/menu/index.tsx
@@ -0,0 +1,59 @@
+import { useMemo, useState } from 'react';
+
+import AchievementMenu from './achievement-menu';
+import * as styles from './index.css';
+import LabelMenu from './label-menu';
+import SkinMenu from './skin-menu';
+
+import SettingTextButton from '@/components/button/SettingTextButton';
+
+import { useCollection } from '@/models/collection';
+
+const menu = {
+ skin: '스킨',
+ label: '칭호',
+ achievement: '업적',
+};
+
+type Category = keyof typeof menu;
+
+const CollectionMenu = () => {
+ const [selectedCategory, setSelectedCategory] = useState('skin');
+
+ const { collection } = useCollection();
+
+ const collectionMenu = useMemo(() => {
+ if (!collection) {
+ return null;
+ }
+
+ switch (selectedCategory) {
+ case 'label':
+ return ;
+ case 'achievement':
+ return ;
+ default:
+ return ;
+ }
+ }, [collection, selectedCategory]);
+
+ return (
+
+
+ {Object.entries(menu).map(([key, title]) => (
+ setSelectedCategory(key as Category)}
+ size='small'
+ colorStyle={selectedCategory === key ? 'black' : 'realDark'}
+ >
+ {title}
+
+ ))}
+
+
{collectionMenu}
+
+ );
+};
+
+export default CollectionMenu;
diff --git a/src/pages/collection/ui/menu/label-menu.tsx b/src/pages/collection/ui/menu/label-menu.tsx
new file mode 100644
index 00000000..700a51c4
--- /dev/null
+++ b/src/pages/collection/ui/menu/label-menu.tsx
@@ -0,0 +1,25 @@
+import { useSetCollection } from '../../hooks';
+import * as styles from './index.css';
+
+const LabelMenu = ({ labels }: { labels: Label[] }) => {
+ const { setSelectedLabel } = useSetCollection();
+
+ return (
+
+ {labels.map((label) => (
+
+ ))}
+
+ );
+};
+
+export default LabelMenu;
diff --git a/src/pages/collection/ui/menu/skin-menu.tsx b/src/pages/collection/ui/menu/skin-menu.tsx
new file mode 100644
index 00000000..db8a111d
--- /dev/null
+++ b/src/pages/collection/ui/menu/skin-menu.tsx
@@ -0,0 +1,81 @@
+import { memo, useState } from 'react';
+
+import { useSetCollection } from '../../hooks';
+import * as styles from './index.css';
+
+import RoundCornerImageBox from '@/components/image-box';
+
+const getBorderColor = (equipped: boolean, selected: boolean) => {
+ if (equipped) {
+ return 'blue';
+ }
+ if (selected) {
+ return 'yellow';
+ }
+ return 'black';
+};
+
+const SkinItem = memo(
+ ({
+ skin,
+ onClick,
+ selected,
+ }: {
+ skin: Skin;
+ onClick: () => void;
+ selected: boolean;
+ }) => {
+ return (
+
+ );
+ },
+ (prevProps, nextProps) => {
+ return (
+ prevProps.skin.equipped === nextProps.skin.equipped &&
+ prevProps.selected === nextProps.selected
+ );
+ },
+);
+
+const SkinMenu = ({ skins }: { skins: Skin[] }) => {
+ const [selectedId, setSelectedId] = useState(null);
+ const { setSelectedSkin } = useSetCollection();
+
+ return (
+
+ {skins.map((skin) => (
+ {
+ if (skin.own) {
+ setSelectedId(skin.id);
+ setSelectedSkin(skin);
+ }
+ }}
+ selected={skin.id === selectedId}
+ />
+ ))}
+
+ );
+};
+
+export default SkinMenu;
diff --git a/src/pages/collection/ui/page.css.ts b/src/pages/collection/ui/page.css.ts
new file mode 100644
index 00000000..7260f186
--- /dev/null
+++ b/src/pages/collection/ui/page.css.ts
@@ -0,0 +1,49 @@
+import { style } from '@vanilla-extract/css';
+
+import { flexOptions } from '@/styles/common.css';
+import { sprinkles } from '@/styles/sprinkles.css';
+
+export const gridBox = style({
+ display: 'flex',
+ height: '100%',
+ overflow: 'hidden',
+});
+
+export const characterWrapper = style({
+ width: '35%',
+ display: 'flex',
+ flexDirection: 'column',
+ alignItems: 'center',
+ justifyContent: 'flex-end',
+});
+
+export const label = style([
+ sprinkles({
+ fontWeight: 'accent',
+ textSize: '1x',
+ width: 'full',
+ }),
+ {
+ textAlign: 'center',
+ },
+]);
+
+export const mySkin = style([
+ sprinkles({
+ width: 'full',
+ aspectRatio: 'square',
+ }),
+ {
+ position: 'relative',
+ objectFit: 'cover',
+ objectPosition: 'center',
+ },
+]);
+
+export const submitButtonWrapper = style([
+ flexOptions({ option: 'columnCenter' }),
+ sprinkles({
+ position: 'relative',
+ marginTop: '2x',
+ }),
+]);
diff --git a/src/pages/collection/ui/page.tsx b/src/pages/collection/ui/page.tsx
new file mode 100644
index 00000000..29709b22
--- /dev/null
+++ b/src/pages/collection/ui/page.tsx
@@ -0,0 +1,94 @@
+import { updateLabel, updateSkin } from '../api';
+import {
+ useSelectedCollection,
+ useSelectedLabelName,
+ useSelectedSkinUrl,
+} from '../hooks';
+import { CollectionProvider } from '../provider';
+import CollectionMenu from './menu';
+import * as styles from './page.css';
+
+import SettingTextButton from '@/components/button/SettingTextButton';
+
+import { refetchCollection } from '@/models/collection';
+import { useUserInfo } from '@/models/user';
+
+const SelectedSkin = () => {
+ const skin = useSelectedSkinUrl();
+
+ return
;
+};
+
+const SelectedLabel = () => {
+ const label = useSelectedLabelName();
+
+ return {`< ${label} >`};
+};
+
+const SaveButton = ({
+ equipped,
+ onSave,
+}: {
+ equipped: {
+ skin: string;
+ label: string;
+ };
+ onSave: () => Promise;
+}) => {
+ const { skin, label } = useSelectedCollection();
+ const isSkinChanged = equipped.skin !== skin.url;
+ const isLabelChanged = equipped.label !== label.name;
+ const isDisabled = !isSkinChanged && !isLabelChanged;
+
+ const onClick = async () => {
+ const promises = [];
+
+ if (isSkinChanged) {
+ promises.push(updateSkin(skin.id));
+ }
+ if (isLabelChanged) {
+ promises.push(updateLabel(label.id));
+ }
+
+ if (promises.length > 0) {
+ await Promise.all(promises);
+ await Promise.all([refetchCollection(), onSave()]);
+ }
+ };
+
+ return (
+
+ 저장
+
+ );
+};
+
+const CollectionPage = () => {
+ const { user, refetchUser } = useUserInfo();
+
+ if (!user) {
+ return null;
+ }
+
+ return (
+
+
+
+ );
+};
+
+export default CollectionPage;
diff --git a/src/pages/error/index.tsx b/src/pages/error/index.tsx
index 6a784665..ea52c5d9 100644
--- a/src/pages/error/index.tsx
+++ b/src/pages/error/index.tsx
@@ -4,11 +4,10 @@ import * as constants from './constants';
import * as styles from './index.css';
import ColoredButton from '@/components/button/ColoredButton';
-import BasicContentFrame from '@/components/frame/with-buttons';
import type { errorCode } from '@/pages/error/type';
-import { ROUTE } from '@/constants/routes';
+import { ROUTE } from '@/shared/constants';
const Error = () => {
const navigate = useNavigate();
@@ -23,19 +22,10 @@ const Error = () => {
};
return (
-
-
-
- {constants.ERROR_MESSAGE[code]}
-
-
-
-
+
+
{constants.ERROR_MESSAGE[code]}
+
+
);
};
diff --git a/src/pages/finder/api.ts b/src/pages/finder/api.ts
new file mode 100644
index 00000000..d3245c6a
--- /dev/null
+++ b/src/pages/finder/api.ts
@@ -0,0 +1,25 @@
+import { api } from '@/shared/api';
+
+export const getRoomList = async (offset: number) => {
+ return api
+ .get(true, `play/friendly/list/${offset}`)
+ .then(({ data }) => {
+ console.debug('room list:', data);
+ return data;
+ });
+};
+
+export const postEnterRoom = async (roomInfo: EnterRoomRequest) => {
+ return (
+ api
+ .post(true, 'play/friendly/enter', roomInfo)
+ .then((res) => {
+ return res.data;
+ })
+ // eslint-disable-next-line
+ .catch((error) => {
+ console.log(error);
+ return '';
+ })
+ );
+};
diff --git a/src/pages/finder/index.ts b/src/pages/finder/index.ts
new file mode 100644
index 00000000..b5ed19f6
--- /dev/null
+++ b/src/pages/finder/index.ts
@@ -0,0 +1,2 @@
+export { default } from './page';
+export { getRoomList } from './api';
diff --git a/src/pages/play/finder/index.css.ts b/src/pages/finder/page.css.ts
similarity index 53%
rename from src/pages/play/finder/index.css.ts
rename to src/pages/finder/page.css.ts
index d41c1ca5..afe88706 100644
--- a/src/pages/play/finder/index.css.ts
+++ b/src/pages/finder/page.css.ts
@@ -1,6 +1,5 @@
import { style } from '@vanilla-extract/css';
-
-import * as constants from './constants';
+import { calc } from '@vanilla-extract/css-utils';
import { flexOptions } from '@/styles/common.css';
import { sprinkles } from '@/styles/sprinkles.css';
@@ -30,7 +29,10 @@ export const partyListWrapper = style([
display: 'grid',
gridTemplateRows: '33% 33% 33%',
gridTemplateColumns: '50% 50%',
- height: constants.FINDER_WRAPPER_HEIGHT,
+ height: calc.subtract(
+ calc.subtract(calc.subtract('100%', '48px'), '16px'),
+ '32px',
+ ),
paddingLeft: '3%',
paddingRight: '3%',
},
@@ -39,52 +41,6 @@ export const partyListWrapper = style([
}),
]);
-export const lobbyItemWrapper = style([
- sprinkles({
- borderRadius: '2x',
- margin: '1x',
- backgroundColor: 'navy',
- }),
- {
- display: 'flex',
-
- padding: '3%',
- },
-]);
-
-export const lobbyTitleText = style([
- sprinkles({
- textSize: '1.25x',
- color: 'white',
- alignItems: 'center',
- }),
- {
- display: 'inline-flex',
- whiteSpace: 'nowrap',
- overflow: 'hidden',
- textOverflow: 'ellipsis',
- maxWidth: '100%',
- },
-]);
-
-export const textsWrapper = style([
- {
- display: 'grid',
- gridTemplateRows: '70% 30%',
- paddingLeft: '5%',
- maxWidth: '100%',
- },
-]);
-export const playerCountText = style([
- sprinkles({
- textSize: '0.75x',
- color: 'gray300',
- display: 'flex',
-
- alignItems: 'center',
- }),
-]);
-
export const bottomButtonsWrapper = style([
flexOptions({ option: 'center' }),
sprinkles({
diff --git a/src/pages/finder/page.tsx b/src/pages/finder/page.tsx
new file mode 100644
index 00000000..635eebe7
--- /dev/null
+++ b/src/pages/finder/page.tsx
@@ -0,0 +1,87 @@
+import { useState } from 'react';
+import { useLoaderData } from 'react-router-dom';
+
+import { getRoomList } from './api';
+import * as styles from './page.css';
+import CreateLobby from './ui/CreateLobby';
+import LobbyItem from './ui/LobbyItem';
+import SearchLobby from './ui/SearchLobby';
+
+import ColoredButton from '@/components/button/ColoredButton';
+import SettingTextButton from '@/components/button/SettingTextButton';
+
+const Finder = () => {
+ const roomListData = useLoaderData() as RoomPreview[];
+ const [roomList, setRoomList] = useState(roomListData);
+ const [index, setIndex] = useState(1);
+ const [maxIndex, setMaxIndex] = useState(
+ roomList.length ? Math.ceil(roomList.length / 6) : 1,
+ );
+
+ const itemPerPage = 6;
+ const offset = (index - 1) * itemPerPage;
+
+ const addIndex = () => {
+ setIndex(index + 1);
+ };
+
+ const subIndex = () => {
+ setIndex(index - 1);
+ };
+
+ const refreshRoomList = async () => {
+ setIndex(1);
+
+ const list = await getRoomList(1);
+ if (list.length > 0) {
+ setRoomList(list);
+ setMaxIndex(Math.ceil(roomList.length / itemPerPage));
+ } else {
+ setMaxIndex(1);
+ }
+ };
+
+ const currentItems = roomList.slice(offset, offset + itemPerPage);
+
+ return (
+
+
+
+
+
+
+
+
+
+ {currentItems.map((item) => (
+
+ ))}
+
+
+ {} : subIndex}
+ >
+ {`<`}
+
+ {}}>
+ {index}
+
+ {} : addIndex}
+ >
+ {`>`}
+
+
+
+ );
+};
+export default Finder;
diff --git a/src/pages/finder/types.d.ts b/src/pages/finder/types.d.ts
new file mode 100644
index 00000000..d0f557b6
--- /dev/null
+++ b/src/pages/finder/types.d.ts
@@ -0,0 +1,5 @@
+interface RoomPreview extends Omit {
+ userNumber: number;
+}
+
+type EnterRoomRequest = Pick;
diff --git a/src/pages/play/finder/Modal/CreateLobby/index.tsx b/src/pages/finder/ui/CreateLobby/index.tsx
similarity index 53%
rename from src/pages/play/finder/Modal/CreateLobby/index.tsx
rename to src/pages/finder/ui/CreateLobby/index.tsx
index 86f6ad93..6bcc6a42 100644
--- a/src/pages/play/finder/Modal/CreateLobby/index.tsx
+++ b/src/pages/finder/ui/CreateLobby/index.tsx
@@ -1,14 +1,10 @@
import ColoredButton from '@/components/button/ColoredButton';
+import Modal, { useModal } from '@/components/modal';
-import useModal from '@/hooks/useModal';
-
-import { MAPS } from '@/pages/play/finder/constants';
-import ModalContent from '@/pages/play/finder/Modal/CreateLobby/ModalContent';
-
-import { postCreateRoom } from '@/services/finder';
+import RoomSetting from '@/features/room-setting';
const CreateLobby = () => {
- const { Modal, openModal, closeModal } = useModal();
+ const { modalRef, openModal, closeModal } = useModal();
return (
<>
@@ -18,13 +14,13 @@ const CreateLobby = () => {
color='red'
onClick={openModal}
/>
-
-
+
diff --git a/src/pages/play/finder/LobbyItem.tsx b/src/pages/finder/ui/LobbyItem.tsx
similarity index 55%
rename from src/pages/play/finder/LobbyItem.tsx
rename to src/pages/finder/ui/LobbyItem.tsx
index 80f0ee1c..72ffc5dd 100644
--- a/src/pages/play/finder/LobbyItem.tsx
+++ b/src/pages/finder/ui/LobbyItem.tsx
@@ -1,35 +1,24 @@
-import * as constants from './constants';
-
-import type { RoomListItem } from '@/types/play';
+import * as styles from './index.css';
+import MessageModalContent from './MessageModalContent';
+import PasswordModal from './PasswordModal';
import ColoredButton from '@/components/button/ColoredButton';
import RoundCornerImageBox from '@/components/image-box';
+import Modal, { useModal } from '@/components/modal';
-import useModal from '@/hooks/useModal';
-
-import * as styles from '@/pages/play/finder/index.css';
-import * as modalStyles from '@/pages/play/finder/Modal/index.css';
-import MessageModalContent from '@/pages/play/finder/Modal/MessageModalContent';
-import PasswordModal from '@/pages/play/finder/Modal/PasswordModal';
+import { MAPS } from '@/models/map';
interface Props {
- roomInfo: RoomListItem;
+ roomInfo: RoomPreview;
}
const LobbyItem = ({ roomInfo }: Props) => {
- const { Modal, openModal, closeModal } = useModal();
-
- const getImgSrc = () => {
- const mapItem = constants.MAPS.find(
- (item) => item.mapId === roomInfo.mapId,
- );
- return mapItem ? mapItem.imgSrc : constants.MAPS[0].imgSrc;
- };
+ const { modalRef, openModal, closeModal } = useModal();
const renderModalContent = () => {
if (roomInfo.userNumber === 4) {
return (
-
+
{
className={styles.lobbyItemWrapper}
onClick={openModal}
>
-
+
{roomInfo.roomTitle}
{
>{`${roomInfo.userNumber}/4`}
-
{renderModalContent()}
+
{renderModalContent()}
>
);
};
diff --git a/src/pages/finder/ui/MessageModalContent.tsx b/src/pages/finder/ui/MessageModalContent.tsx
new file mode 100644
index 00000000..4a314f6d
--- /dev/null
+++ b/src/pages/finder/ui/MessageModalContent.tsx
@@ -0,0 +1,35 @@
+import * as styles from './index.css';
+
+const FAIL_MESSAGE_MODAL = {
+ SEARCH: {
+ message: '진짜로 방장이\n허락했나요?',
+ subMessage: '방 코드나 비밀번호를 한 번 더 확인해주세요',
+ },
+ PASSWORD: {
+ message: '9998번만\n더 시도해보시는건 어때요?',
+ subMessage: '비밀번호를 한 번 더 확인해주세요',
+ },
+ ENTER: {
+ message: '노컬러랜드는 5인 이상\n거리두기 수칙을 준수합니다',
+ subMessage: '플레이 가능 인원을 초과하였습니다',
+ },
+} as const;
+
+interface Props {
+ failed: keyof typeof FAIL_MESSAGE_MODAL;
+}
+
+const MessageModalContent = ({ failed }: Props) => {
+ return (
+ <>
+
+ {FAIL_MESSAGE_MODAL[failed].message}
+
+
+ {FAIL_MESSAGE_MODAL[failed].subMessage}
+
+ >
+ );
+};
+
+export default MessageModalContent;
diff --git a/src/pages/play/finder/Modal/PasswordModal.tsx b/src/pages/finder/ui/PasswordModal.tsx
similarity index 83%
rename from src/pages/play/finder/Modal/PasswordModal.tsx
rename to src/pages/finder/ui/PasswordModal.tsx
index ecef71cc..72b95cda 100644
--- a/src/pages/play/finder/Modal/PasswordModal.tsx
+++ b/src/pages/finder/ui/PasswordModal.tsx
@@ -1,17 +1,14 @@
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
-import type { RequestEnterRoom } from '@/types/play';
+import { postEnterRoom } from '../api';
+import * as styles from './index.css';
+import MessageModalContent from './MessageModalContent';
import ColoredButton from '@/components/button/ColoredButton';
import Input from '@/components/input';
-import * as styles from '@/pages/play/finder/Modal/index.css';
-import MessageModalContent from '@/pages/play/finder/Modal/MessageModalContent';
-
-import { postEnterRoom } from '@/services/finder';
-
-import { ROUTE } from '@/constants/routes';
+import { ROUTE } from '@/shared/constants';
interface Props {
closeModal: () => void;
@@ -21,7 +18,7 @@ interface Props {
const PasswordModal = ({ roomCode, closeModal }: Props) => {
const navigate = useNavigate();
const [isValid, setIsValid] = useState(true);
- const [roomInfo, setRoomInfo] = useState
({
+ const [roomInfo, setRoomInfo] = useState({
roomCode,
roomPassword: '',
});
@@ -38,7 +35,7 @@ const PasswordModal = ({ roomCode, closeModal }: Props) => {
setIsValid(false);
} else {
setIsValid(true);
- navigate(`${ROUTE.lobby}/${roomId}`);
+ navigate(`${ROUTE.lobby}/${roomId}`, { replace: true });
}
};
diff --git a/src/pages/play/finder/Modal/SearchLobby/SearchModalContent.tsx b/src/pages/finder/ui/SearchLobby/SearchModalContent.tsx
similarity index 87%
rename from src/pages/play/finder/Modal/SearchLobby/SearchModalContent.tsx
rename to src/pages/finder/ui/SearchLobby/SearchModalContent.tsx
index 4805512e..130fe72e 100644
--- a/src/pages/play/finder/Modal/SearchLobby/SearchModalContent.tsx
+++ b/src/pages/finder/ui/SearchLobby/SearchModalContent.tsx
@@ -1,18 +1,14 @@
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
+import { postEnterRoom } from '../../api';
import * as styles from '../index.css';
-
-import type { RequestEnterRoom } from '@/types/play';
+import MessageModalContent from '../MessageModalContent';
import ColoredButton from '@/components/button/ColoredButton';
import Input from '@/components/input';
-import MessageModalContent from '@/pages/play/finder/Modal/MessageModalContent';
-
-import { postEnterRoom } from '@/services/finder';
-
-import { ROUTE } from '@/constants/routes';
+import { ROUTE } from '@/shared/constants';
interface Props {
closeModal: () => void;
@@ -21,7 +17,7 @@ interface Props {
const SearchModalContent = ({ closeModal }: Props) => {
const navigate = useNavigate();
const [isValid, setIsValid] = useState(true);
- const [roomInfo, setRoomInfo] = useState({
+ const [roomInfo, setRoomInfo] = useState({
roomCode: '',
roomPassword: '',
});
@@ -42,7 +38,7 @@ const SearchModalContent = ({ closeModal }: Props) => {
setIsValid(false);
} else {
setIsValid(true);
- navigate(`${ROUTE.lobby}/${roomId}`);
+ navigate(`${ROUTE.lobby}/${roomId}`, { replace: true });
}
};
diff --git a/src/pages/play/finder/Modal/SearchLobby/index.tsx b/src/pages/finder/ui/SearchLobby/index.tsx
similarity index 63%
rename from src/pages/play/finder/Modal/SearchLobby/index.tsx
rename to src/pages/finder/ui/SearchLobby/index.tsx
index f2ad5409..b2e758b3 100644
--- a/src/pages/play/finder/Modal/SearchLobby/index.tsx
+++ b/src/pages/finder/ui/SearchLobby/index.tsx
@@ -1,11 +1,11 @@
-import ColoredButton from '@/components/button/ColoredButton';
+import SearchModalContent from './SearchModalContent';
-import useModal from '@/hooks/useModal';
+import ColoredButton from '@/components/button/ColoredButton';
+import Modal, { useModal } from '@/components/modal';
-import SearchModalContent from '@/pages/play/finder/Modal/SearchLobby/SearchModalContent';
const SearchLobby = () => {
- const { Modal, openModal, closeModal } = useModal();
+ const { modalRef, openModal, closeModal } = useModal();
return (
<>
@@ -15,7 +15,7 @@ const SearchLobby = () => {
color='green'
onClick={openModal}
/>
-
+
>
diff --git a/src/pages/finder/ui/index.css.ts b/src/pages/finder/ui/index.css.ts
new file mode 100644
index 00000000..b3c7f28a
--- /dev/null
+++ b/src/pages/finder/ui/index.css.ts
@@ -0,0 +1,108 @@
+import { style } from '@vanilla-extract/css';
+import { recipe } from '@vanilla-extract/recipes';
+
+import { flexOptions } from '@/styles/common.css';
+import { sprinkles } from '@/styles/sprinkles.css';
+
+export const modalTwoButtonWrapper = style([
+ flexOptions({
+ option: 'rowCenter',
+ }),
+ sprinkles({
+ width: 'full',
+ }),
+]);
+
+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',
+ }),
+ },
+ },
+});
+
+// LobbyItem.tsx
+export const lobbyItemWrapper = style([
+ sprinkles({
+ borderRadius: '2x',
+ margin: '1x',
+ backgroundColor: 'navy',
+ }),
+ {
+ display: 'flex',
+
+ padding: '3%',
+ },
+]);
+
+export const lobbyTitleText = style([
+ sprinkles({
+ textSize: '1.25x',
+ color: 'white',
+ alignItems: 'center',
+ }),
+ {
+ display: 'inline-flex',
+ whiteSpace: 'nowrap',
+ overflow: 'hidden',
+ textOverflow: 'ellipsis',
+ maxWidth: '100%',
+ },
+]);
+
+export const textsWrapper = style([
+ {
+ display: 'grid',
+ gridTemplateRows: '70% 30%',
+ paddingLeft: '5%',
+ maxWidth: '100%',
+ },
+]);
+export const playerCountText = style([
+ sprinkles({
+ textSize: '0.75x',
+ color: 'gray300',
+ display: 'flex',
+
+ alignItems: 'center',
+ }),
+]);
diff --git a/src/pages/home/SignupBanner.tsx b/src/pages/home/SignupBanner.tsx
index adacd734..9dd0a000 100644
--- a/src/pages/home/SignupBanner.tsx
+++ b/src/pages/home/SignupBanner.tsx
@@ -2,13 +2,12 @@ import * as constants from './constants';
import * as styles from './index.css';
import ColoredButton from '@/components/button/ColoredButton';
-
-import useModal from '@/hooks/useModal';
+import Modal, { useModal } from '@/components/modal';
import SignUp from '@/features/sign-up';
const SignupBanner = () => {
- const { Modal, openModal, closeModal } = useModal();
+ const { modalRef, openModal, closeModal } = useModal();
return (
@@ -27,8 +26,8 @@ const SignupBanner = () => {
color='yellow'
onClick={openModal}
/>
-
-
+
+
);
diff --git a/src/pages/home/UserDashboard/index.tsx b/src/pages/home/UserDashboard/index.tsx
index 8712d9b0..8f498c6c 100644
--- a/src/pages/home/UserDashboard/index.tsx
+++ b/src/pages/home/UserDashboard/index.tsx
@@ -2,14 +2,14 @@ import { assignInlineVars } from '@vanilla-extract/dynamic';
import * as styles from './index.css';
-import TierBox from '@/components/tier';
+import TierBox from '@/models/tier';
interface Props {
nickname: string;
level: number;
cp: number;
maxCp: number;
- tier: tierRange;
+ tier: TierValue;
// rankScore: number;
}
diff --git a/src/pages/home/index.css.ts b/src/pages/home/index.css.ts
index f0a4e359..749fe30d 100644
--- a/src/pages/home/index.css.ts
+++ b/src/pages/home/index.css.ts
@@ -75,7 +75,6 @@ export const TopContentsWrapper = style([
flexOptions({ option: 'row' }),
sprinkles({
justifyContent: 'spaceBetween',
- paddingTop: '4x',
}),
{
height: '50%',
diff --git a/src/pages/home/index.tsx b/src/pages/home/index.tsx
index 1e65fb82..8caf2d16 100644
--- a/src/pages/home/index.tsx
+++ b/src/pages/home/index.tsx
@@ -1,20 +1,17 @@
-import { useLoaderData, useNavigate } from 'react-router-dom';
+import { useNavigate } from 'react-router-dom';
import * as styles from './index.css';
import SignupBanner from './SignupBanner';
-import type { User } from '@/types/auth';
-
import ColoredIconButton from '@/components/button/ColoredIconButton';
-import BasicContentFrame from '@/components/frame/with-buttons';
-import Error from '@/pages/error';
import UserDashboard from '@/pages/home/UserDashboard';
-import { ROUTE } from '@/constants/routes';
+import { useUserInfo } from '@/models/user';
+import { ROUTE } from '@/shared/constants';
const Home = () => {
- const user = useLoaderData() as User;
+ const { user } = useUserInfo();
const navigate = useNavigate();
@@ -29,11 +26,11 @@ const Home = () => {
};
if (!user) {
- return ;
+ return null;
}
return (
-
+ <>
{user.guest && }
-
+ >
);
};
diff --git a/src/pages/landing/index.css.ts b/src/pages/landing/index.css.ts
index 9cf3042a..4d52a89a 100644
--- a/src/pages/landing/index.css.ts
+++ b/src/pages/landing/index.css.ts
@@ -18,6 +18,7 @@ export const landingWrapper = style([
]);
export const contentWrapper = style([flexOptions({ option: 'columnCenter' })]);
+
export const buttonWrapper = style([
flexOptions({ option: 'row' }),
sprinkles({
@@ -53,22 +54,3 @@ export const logoImage = style([
.toString(),
},
]);
-
-export const logInFailMessageWrapper = style([
- sprinkles({
- marginY: '8x',
- }),
- {
- textAlign: 'center',
- },
-]);
-
-export const logInFailMessage = style([
- sprinkles({
- fontSize: '1.5x',
- marginY: '2x',
- }),
- {
- lineHeight: '32px',
- },
-]);
diff --git a/src/pages/landing/index.tsx b/src/pages/landing/index.tsx
index 0d1e989a..96a3e6a3 100644
--- a/src/pages/landing/index.tsx
+++ b/src/pages/landing/index.tsx
@@ -4,24 +4,12 @@ import * as styles from './index.css';
import ColoredButton from '@/components/button/ColoredButton/index';
-import { getGuestLogin } from '@/services/auth';
-import { setFullScreen } from '@/services/landing';
-
-import { ROUTE } from '@/constants/routes';
+import { useGuestLogin } from '@/features/auth';
+import { ROUTE } from '@/shared/constants';
const Landing = () => {
const navigate = useNavigate();
-
- const clickGuestLogin = async () => {
- return getGuestLogin().then((isSuccess) => {
- if (!isSuccess) {
- navigate(`${ROUTE.error}/500`);
- return;
- }
- navigate(ROUTE.tutorial);
- setFullScreen();
- });
- };
+ const { guestLogin } = useGuestLogin();
const clickLogIn = () => {
navigate(ROUTE.login);
@@ -39,7 +27,7 @@ const Landing = () => {
text='GUEST'
color='gray300'
size='large'
- onClick={clickGuestLogin}
+ onClick={guestLogin}
/>
{
- useUserStateStore.getState().setLogout();
-
return (
diff --git a/src/pages/landing/logIn/constants.ts b/src/pages/landing/logIn/constants.ts
deleted file mode 100644
index 41ccbfab..00000000
--- a/src/pages/landing/logIn/constants.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-export const ERROR_MESSAGE = {
- loginFail: '컬러랜드 입국정보를\n다시 한 번 확인해주세요.',
- welcome: '환영합니다. 어쩌면 진짜 Origin',
- sameId: '중복된 ID!! 진짜 당신은 누구죠?',
- blank: '당신의 역사가 비어있네요? 수상합니다.',
- notSamePassword: '암호가 일치하지 않습니다.',
- invalidInput: '당신의 정보가 너무 짧거나, 너무 길거나.',
- inValidNickname: 'Origin께서 허락하지 못한 이름입니다.',
- inValidPasswordLength: '비밀번호는 숫자 6자리여야 합니다.',
- inValidPassword: '경고! 현재 PASSWORD와 다릅니다!!',
- inValidSignOut: 'Origin 의 허락 없이 삭제는 불가능합니다.',
-};
diff --git a/src/pages/landing/logIn/LogInFail.tsx b/src/pages/logIn/LogInFail.tsx
similarity index 59%
rename from src/pages/landing/logIn/LogInFail.tsx
rename to src/pages/logIn/LogInFail.tsx
index ff25c106..2a36896d 100644
--- a/src/pages/landing/logIn/LogInFail.tsx
+++ b/src/pages/logIn/LogInFail.tsx
@@ -1,8 +1,8 @@
-import * as styles from '../index.css';
+import * as styles from './index.css';
-import ColoredButton from '@/components/button/ColoredButton/index';
+import ColoredButton from '@/components/button/ColoredButton';
-import * as constants from '@/pages/landing/logIn/constants';
+import { ERROR_MESSAGE } from '@/shared/constants';
interface Props {
closeModal: () => void;
@@ -12,9 +12,7 @@ const LogInFail = ({ closeModal }: Props) => {
return (
<>
-
- {constants.ERROR_MESSAGE.loginFail}
-
+
{ERROR_MESSAGE.loginFail}
{
const [isClicked, setIsClicked] = useState(false);
- const [logInInfo, setLogInInfo] = useState({
+ const [logInInfo, setLogInInfo] = useState({
id: '',
password: '',
});
- const navigate = useNavigate();
+ const { login } = useLogin();
- const { Modal, openModal, closeModal } = useModal();
+ const { modalRef, openModal, closeModal } = useModal();
const handleChange = (event: React.ChangeEvent) => {
const { name, value } = event.target;
@@ -36,15 +30,11 @@ const LogIn = () => {
};
const handleLogInClick = async () => {
- return postMemberLogin(logInInfo).then((isLoginSuccess) => {
- if (!isLoginSuccess) {
- setIsClicked(false);
- openModal();
- return;
- }
- navigate(ROUTE.home);
- setFullScreen();
- });
+ const isLoginSuccess = await login(logInInfo);
+ if (!isLoginSuccess) {
+ setIsClicked(false);
+ openModal();
+ }
};
const handleSignUpClick = () => {
@@ -85,7 +75,7 @@ const LogIn = () => {
onClick={handleLogInClick}
/>
-
+
{isClicked ? (
) : (
diff --git a/src/pages/play/finder/Modal/CreateLobby/MapItem.tsx b/src/pages/play/finder/Modal/CreateLobby/MapItem.tsx
deleted file mode 100644
index f7270ef8..00000000
--- a/src/pages/play/finder/Modal/CreateLobby/MapItem.tsx
+++ /dev/null
@@ -1,27 +0,0 @@
-import Chip from '@/components/chip';
-import RoundCornerImageBox from '@/components/image-box';
-
-import * as styles from '@/pages/play/finder/Modal/index.css';
-
-interface Props {
- mapName: string;
- imgSrc: string;
- isSelected?: boolean;
- onClick: () => void;
-}
-
-const MapItem = ({ mapName, imgSrc, isSelected, onClick }: Props) => {
- return (
-
- );
-};
-export default MapItem;
diff --git a/src/pages/play/finder/Modal/MessageModalContent.tsx b/src/pages/play/finder/Modal/MessageModalContent.tsx
deleted file mode 100644
index 052803d6..00000000
--- a/src/pages/play/finder/Modal/MessageModalContent.tsx
+++ /dev/null
@@ -1,23 +0,0 @@
-import * as constants from '../constants';
-import * as styles from './index.css';
-
-import type { failType } from '@/pages/play/finder/types';
-
-interface Props {
- failed: failType;
-}
-
-const MessageModalContent = ({ failed }: Props) => {
- return (
- <>
- {`${constants.FAIL_MESSAGE_MODAL[failed].message}`}
-
- {constants.FAIL_MESSAGE_MODAL[failed].subMessage}
-
- >
- );
-};
-
-export default MessageModalContent;
diff --git a/src/pages/play/finder/constants.ts b/src/pages/play/finder/constants.ts
deleted file mode 100644
index b20e58aa..00000000
--- a/src/pages/play/finder/constants.ts
+++ /dev/null
@@ -1,55 +0,0 @@
-import { calc } from '@vanilla-extract/css-utils';
-
-import type { GameMap } from './types';
-
-export const FINDER_WRAPPER_HEIGHT = calc.subtract(
- calc.subtract(calc.subtract('100%', '48px'), '16px'),
- '32px',
-);
-
-export const FAIL_MESSAGE_MODAL = {
- SEARCH: {
- message: '진짜로 방장이\n허락했나요?',
- subMessage: '방 코드나 비밀번호를 한 번 더 확인해주세요',
- },
- PASSWORD: {
- message: '9998번만\n더 시도해보시는건 어때요?',
- subMessage: '비밀번호를 한 번 더 확인해주세요',
- },
- ENTER: {
- message: '노컬러랜드는 5인 이상\n거리두기 수칙을 준수합니다',
- subMessage: '플레이 가능 인원을 초과하였습니다',
- },
-} as const;
-
-export const MAPS: GameMap[] = [
- {
- mapId: 0,
- imgSrc: '/images/map/background/random-map-rainbow-h450w450.png',
- mapName: '랜덤',
- },
- {
- mapId: 4,
- imgSrc: '/images/map/background/factorymap.png',
- mapName: '팩토리',
- },
- {
- mapId: 3,
- imgSrc: '/images/map/background/foodmap2.png',
- mapName: '푸디2',
- },
- {
- mapId: 1,
- imgSrc: '/images/map/background/basicmap.png',
- mapName: '베이직',
- },
- {
- mapId: 2,
- imgSrc: '/images/map/background/foodmap.png',
- mapName: '푸디',
- },
-] as const;
-
-export const MAP_ITEM_HEIGHT = '100px';
-
-export const ALERT_MESSAGE = ' 정보를 모두 입력해주세요.';
diff --git a/src/pages/play/finder/index.tsx b/src/pages/play/finder/index.tsx
deleted file mode 100644
index 7c676bed..00000000
--- a/src/pages/play/finder/index.tsx
+++ /dev/null
@@ -1,98 +0,0 @@
-import { useState } from 'react';
-import { useLoaderData } from 'react-router-dom';
-
-import * as styles from './index.css';
-import SearchLobby from './Modal/SearchLobby/index';
-
-import type { RoomListItem } from '@/types/play';
-
-import ColoredButton from '@/components/button/ColoredButton';
-import SettingTextButton from '@/components/button/SettingTextButton';
-import BasicContentFrame from '@/components/frame/with-buttons';
-
-import LobbyItem from '@/pages/play/finder/LobbyItem';
-import CreateLobby from '@/pages/play/finder/Modal/CreateLobby';
-
-import { getRoomList } from '@/services/finder';
-
-const Finder = () => {
- const roomListData = useLoaderData() as RoomListItem[];
- const [roomList, setRoomList] = useState(roomListData);
- const [index, setIndex] = useState(1);
- const [maxIndex, setMaxIndex] = useState(
- roomList.length ? Math.ceil(roomList.length / 6) : 1,
- );
-
- const itemPerPage = 6;
- const offset = (index - 1) * itemPerPage;
-
- const addIndex = () => {
- setIndex(index + 1);
- };
-
- const subIndex = () => {
- setIndex(index - 1);
- };
-
- const refreshRoomList = async () => {
- setIndex(1);
-
- const list = await getRoomList(1);
- if (list.length > 0) {
- setRoomList(list);
- setMaxIndex(Math.ceil(roomList.length / itemPerPage));
- } else {
- setMaxIndex(1);
- }
- };
-
- const currentItems = roomList.slice(offset, offset + itemPerPage);
-
- return (
-
-
-
-
-
-
-
-
-
-
- {currentItems.map((item) => (
-
- ))}
-
-
- {} : subIndex}
- >
- {`<`}
-
- {}}
- >
- {index}
-
- {} : addIndex}
- >
- {`>`}
-
-
-
-
- );
-};
-export default Finder;
diff --git a/src/pages/play/finder/types.d.ts b/src/pages/play/finder/types.d.ts
deleted file mode 100644
index 5e3c27b3..00000000
--- a/src/pages/play/finder/types.d.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-import * as constants from './constants';
-
-type failType = keyof typeof constants.FAIL_MESSAGE_MODAL;
-
-interface GameMap {
- mapId: number;
- imgSrc: string;
- mapName: string;
-}
diff --git a/src/pages/play/game/hooks.ts b/src/pages/play/game/hooks.ts
new file mode 100644
index 00000000..9139d57d
--- /dev/null
+++ b/src/pages/play/game/hooks.ts
@@ -0,0 +1,14 @@
+import { useCollectionStale } from '@/models/collection';
+import { useUserStale } from '@/models/user';
+
+export const useDataStale = () => {
+ const { setUserStale } = useUserStale();
+ const { setCollectionStale } = useCollectionStale();
+
+ return {
+ setDataStale: () => {
+ setUserStale();
+ setCollectionStale();
+ },
+ };
+};
diff --git a/src/pages/play/game/index.tsx b/src/pages/play/game/index.tsx
index b46a841b..c7c1bf3d 100644
--- a/src/pages/play/game/index.tsx
+++ b/src/pages/play/game/index.tsx
@@ -1,39 +1,40 @@
-import { useEffect } from 'react';
-import { useNavigate } from 'react-router-dom';
+import { useEffect, useState } from 'react';
+import { Navigate, useNavigate } from 'react-router-dom';
-import BasicContentFrame from '@/components/frame';
+import { useDataStale } from './hooks';
-import { useGameControl } from '@/states/game';
+import BasicContentFrame from '@/components/frame';
-import { ROUTE } from '@/constants/routes';
import { config, scenesConfig } from '@/features/game';
-import { useWebSocketStore } from '@/features/websocket';
+import { useGameWebSocket } from '@/features/websocket';
+import { ROUTE } from '@/shared/constants';
const Game = () => {
const navigate = useNavigate();
- const { isActive, setIsActive } = useGameControl();
+ const [isActive, setIsActive] = useState(true);
+ const { webSocket } = useGameWebSocket();
+ const { setDataStale } = useDataStale();
- const { webSocket } = useWebSocketStore.getState();
- const inGameDisconnect = () => {
- navigate(`${ROUTE.error}/400`, { replace: true });
- };
useEffect(() => {
- if (!isActive) {
- webSocket.inGameUnconnected(() => {});
- navigate(ROUTE.result, { replace: true });
- return () => {};
- }
+ webSocket.onDisconnect(() =>
+ navigate(`${ROUTE.error}/400`, { replace: true }),
+ );
const game = new Phaser.Game({
...config,
- scene: scenesConfig(setIsActive, inGameDisconnect),
+ scene: scenesConfig(setIsActive, webSocket),
});
return () => {
- setIsActive(true);
+ webSocket.cleanUp();
game.destroy(true);
+ setDataStale();
};
- }, [isActive]);
+ }, []);
+
+ if (!isActive) {
+ return ;
+ }
return (
diff --git a/src/pages/play/lobby/MapInfo.tsx b/src/pages/play/lobby/MapInfo.tsx
deleted file mode 100644
index ab05300c..00000000
--- a/src/pages/play/lobby/MapInfo.tsx
+++ /dev/null
@@ -1,22 +0,0 @@
-import * as styles from './index.css';
-
-import Chip from '@/components/chip';
-import RoundCornerImageBox from '@/components/image-box';
-
-import type { GameMap } from '@/pages/play/finder/types';
-
-interface Props {
- map: GameMap;
-}
-
-const MapInfo = ({ map }: Props) => {
- return (
-
-
-
-
-
- );
-};
-
-export default MapInfo;
diff --git a/src/pages/play/lobby/Players/Character.tsx b/src/pages/play/lobby/Players/Character.tsx
index b979e423..6969f57a 100644
--- a/src/pages/play/lobby/Players/Character.tsx
+++ b/src/pages/play/lobby/Players/Character.tsx
@@ -1,11 +1,9 @@
+import { EMPTY_PLAYER_COLOR, PLAYER_COLORS } from '../constants';
import * as styles from './index.css';
-import type { Player } from '@/types/play';
-
+import Chip from '@/components/chip';
import RoundCornerImageBox from '@/components/image-box';
-import State from '@/pages/play/lobby/Players/State';
-
const getState = (player: Player) => {
if (player.isMaster) {
return '방장';
@@ -16,21 +14,36 @@ const getState = (player: Player) => {
return '';
};
-interface Props {
+const PLAYER_ICON = Array.from(
+ { length: 4 },
+ (_, i) =>
+ `/images/ui/icon/shape/icon-shape-white-small-player${i}-h16w16.png`,
+);
+
+type Props = {
player: Player;
-}
+ index: number;
+};
-const Character = ({ player }: Props) => {
+const Character = ({ player, index }: Props) => {
const state = getState(player);
+ const color = player.userCode ? PLAYER_COLORS[index] : EMPTY_PLAYER_COLOR;
return (
-
+
- {state && }
+ {state && (
+
+ )}
);
diff --git a/src/pages/play/lobby/Players/Info.tsx b/src/pages/play/lobby/Players/Info.tsx
index d5454d71..4e6aaf29 100644
--- a/src/pages/play/lobby/Players/Info.tsx
+++ b/src/pages/play/lobby/Players/Info.tsx
@@ -1,27 +1,35 @@
-import * as styles from './index.css';
+import { memo } from 'react';
-import type { Player } from '@/types/play';
+import * as styles from './index.css';
-import TierIconBox from '@/components/tier';
+import { PLAYER_COLORS } from '@/models/player';
+import TierIconBox from '@/models/tier';
interface Props {
player: Player;
+ index: number;
}
-const PlayerInfo = ({ player }: Props) => {
- if (!player.userCode) {
- return
?
;
- }
+const PlayerInfo = memo(
+ ({ player, index }: Props) => {
+ if (!player.userCode) {
+ return
?
;
+ }
- return (
-
-
-
-
{player.label}
-
{player.nickname}
+ return (
+
+
+
+
{player.label}
+
{player.nickname}
+
-
- );
-};
+ );
+ },
+ (prevProps, nextProps) =>
+ prevProps.player.userCode === nextProps.player.userCode,
+);
export default PlayerInfo;
diff --git a/src/pages/play/lobby/Players/ReadyButton.tsx b/src/pages/play/lobby/Players/ReadyButton.tsx
index 7e50bc5a..b1aea1ae 100644
--- a/src/pages/play/lobby/Players/ReadyButton.tsx
+++ b/src/pages/play/lobby/Players/ReadyButton.tsx
@@ -1,77 +1,66 @@
import { useState } from 'react';
+import { getReady } from '../api';
+import { EMPTY_PLAYER_COLOR, MESSAGE, PLAYER_COLORS } from '../constants';
import * as styles from './index.css';
-import type { Player } from '@/types/play';
+import type { AxiosError } from 'axios';
import ColoredButton from '@/components/button/ColoredButton';
-import ColoredIconButton, {
- ColoredIconButtonProps,
-} from '@/components/button/ColoredIconButton';
+import ColoredIconButton from '@/components/button/ColoredIconButton';
+import Modal, { useModal } from '@/components/modal';
-import useModal from '@/hooks/useModal';
-import * as constants from '@/pages/play/lobby/constants';
-import { EMPTY_PLAYER_COLOR } from '@/pages/play/lobby/constants';
-
-import { getReady } from '@/services/lobby';
-
-const getButtonProps = (
- player: Player,
- startGame: () => void,
-): ColoredIconButtonProps => {
- const props = {
- icon: constants.PLAYER_ICON_LARGE[player.color],
- color: player.color,
- size: 'large' as const,
- onClick: getReady,
- };
+const getText = (player: Player): string => {
if (player.isMaster) {
- return {
- ...props,
- text: '게임시작',
- onClick: startGame,
- };
+ return '게임시작';
}
if (player.ready) {
- return {
- ...props,
- text: '준비취소',
- color: EMPTY_PLAYER_COLOR,
- };
+ return '준비취소';
}
- return {
- ...props,
- text: '게임준비',
- };
+ return '게임준비';
};
+const PLAYER_ICON_LARGE = Array.from(
+ { length: 4 },
+ (_, i) => `/images/ui/icon/shape/icon-shape-white-big-player${i}-h48w48.png`,
+);
+
interface Props {
myInfo: Player;
+ index: number;
}
-const ReadyButton = ({ myInfo }: Props) => {
- const { Modal, openModal, closeModal } = useModal();
+const ReadyButton = ({ myInfo, index }: Props) => {
+ const { modalRef, openModal, closeModal } = useModal();
const [errorMessage, setErrorMessage] = useState('');
const startGame = async () => {
- return getReady().catch((err) => {
- if (err.response.status === 400) {
- setErrorMessage(constants.LONELY_MASTER_MESSAGE);
- } else if (err.response.status === 406) {
- setErrorMessage(constants.SOMEBODY_NOT_READY_MESSAGE);
+ return getReady().catch(({ response }: AxiosError) => {
+ if (response?.status === 400) {
+ setErrorMessage(MESSAGE.LONELY_MASTER);
+ } else if (response?.status === 406) {
+ setErrorMessage(MESSAGE.SOMEBODY_NOT_READY);
}
openModal();
});
};
- const buttonProps = getButtonProps(myInfo, startGame);
+ const color =
+ !myInfo.isMaster && myInfo.ready
+ ? EMPTY_PLAYER_COLOR
+ : PLAYER_COLORS[index];
return (
<>
-
-
-
+
+
{errorMessage}
diff --git a/src/pages/play/lobby/Players/State.tsx b/src/pages/play/lobby/Players/State.tsx
deleted file mode 100644
index 8158996c..00000000
--- a/src/pages/play/lobby/Players/State.tsx
+++ /dev/null
@@ -1,29 +0,0 @@
-import type { Player } from '@/types/play';
-
-import Chip from '@/components/chip';
-
-import * as constants from '@/pages/play/lobby/constants';
-import { EMPTY_PLAYER_COLOR } from '@/pages/play/lobby/constants';
-import * as styles from '@/pages/play/lobby/Players/index.css';
-
-interface Props {
- player: Player;
- state: string;
-}
-
-const State = ({ player, state }: Props) => {
- if (player.color === EMPTY_PLAYER_COLOR) {
- return
?
;
- }
-
- return (
-
- );
-};
-
-export default State;
diff --git a/src/pages/play/lobby/Players/index.css.ts b/src/pages/play/lobby/Players/index.css.ts
index 0e3dc6bc..d7f4418a 100644
--- a/src/pages/play/lobby/Players/index.css.ts
+++ b/src/pages/play/lobby/Players/index.css.ts
@@ -2,10 +2,10 @@ import { style } from '@vanilla-extract/css';
import { recipe } from '@vanilla-extract/recipes';
import * as constants from '@/pages/play/lobby/constants';
-import type { playerColorType } from '@/pages/play/lobby/types';
import { flexOptions } from '@/styles/common.css';
import { sprinkles } from '@/styles/sprinkles.css';
+import { variant } from '@/styles/utils';
export const playerWrapper = style([
flexOptions({ option: 'row' }),
@@ -79,12 +79,9 @@ const playerInfo = style([
},
]);
-const backgroudColorVariant = constants.PLAYER_COLORS.reduce(
- (colorVariants, color) => {
- colorVariants[color] = style([sprinkles({ backgroundColor: color })]);
- return colorVariants;
- },
- {} as Record
>,
+const backgroudColorVariant = variant(
+ [...constants.PLAYER_COLORS],
+ (backgroundColor) => sprinkles({ backgroundColor }),
);
export const playerInfoColored = recipe({
@@ -153,15 +150,9 @@ export const playerInfoGray = style([
]);
// Chraracter.tsx
-const outlineColorVariants = [
- ...constants.PLAYER_COLORS,
- constants.EMPTY_PLAYER_COLOR,
-].reduce(
- (variants, color) => {
- variants[color] = style([sprinkles({ outlineColor: color })]);
- return variants;
- },
- {} as Record>,
+const outlineColorVariants = variant(
+ [...constants.PLAYER_COLORS, constants.EMPTY_PLAYER_COLOR],
+ (outlineColor) => sprinkles({ outlineColor }),
);
export const character = recipe({
@@ -181,16 +172,6 @@ export const character = recipe({
},
});
-// State.tsx
-export const empty = style([
- flexOptions({ option: 'center' }),
- sprinkles({
- textSize: '2x',
- height: 'full',
- color: 'gray300',
- }),
-]);
-
export const readyWrapper = style([
flexOptions({ option: 'columnCenter' }),
sprinkles({
diff --git a/src/pages/play/lobby/Players/index.tsx b/src/pages/play/lobby/Players/index.tsx
index 0e60117a..6541f171 100644
--- a/src/pages/play/lobby/Players/index.tsx
+++ b/src/pages/play/lobby/Players/index.tsx
@@ -2,26 +2,23 @@ import { useNavigate } from 'react-router-dom';
import * as styles from './index.css';
-import type { Player } from '@/types/play';
-
import ColoredButton from '@/components/button/ColoredButton';
import Character from '@/pages/play/lobby/Players/Character';
import PlayerInfo from '@/pages/play/lobby/Players/Info';
import ReadyButton from '@/pages/play/lobby/Players/ReadyButton';
-import { useUserStateStore } from '@/states/user';
-
-import { ROUTE } from '@/constants/routes';
+import { ROUTE } from '@/shared/constants';
interface Props {
players: Player[];
+ myCode: User['userCode'];
}
-const Players = ({ players }: Props) => {
+const Players = ({ players, myCode }: Props) => {
const navigate = useNavigate();
- const myCode = useUserStateStore((state) => state.userCode);
- const myInfo = players.find((player) => player.userCode === myCode);
+ const myIndex = players.findIndex((player) => player.userCode === myCode);
+ const myInfo = players[myIndex];
if (!myInfo) {
return (
@@ -42,17 +39,17 @@ const Players = ({ players }: Props) => {
return (
- {players.map((player) => (
-
+ {players.map((player, index) => (
+
))}
- {players.map((player) => (
-
+ {players.map((player, index) => (
+
))}
-
+
);
diff --git a/src/pages/play/lobby/SettingButton.tsx b/src/pages/play/lobby/SettingButton.tsx
index 887072ee..6e5f8639 100644
--- a/src/pages/play/lobby/SettingButton.tsx
+++ b/src/pages/play/lobby/SettingButton.tsx
@@ -1,19 +1,15 @@
import * as styles from './index.css';
-import type { Lobby } from '@/types/play';
+import Modal, { useModal } from '@/components/modal';
-import useModal from '@/hooks/useModal';
-
-import ModalContent from '@/pages/play/finder/Modal/CreateLobby/ModalContent';
-
-import { updateRoom } from '@/services/lobby';
+import RoomSetting from '@/features/room-setting';
interface Props {
- lobby: Lobby;
+ lobby: EnteredRoom;
}
const SettingButton = ({ lobby }: Props) => {
- const { Modal, openModal, closeModal } = useModal();
+ const { modalRef, openModal, closeModal } = useModal();
return (
<>
@@ -25,13 +21,13 @@ const SettingButton = ({ lobby }: Props) => {
/>
설정 변경
-
-
+
diff --git a/src/pages/play/lobby/api.ts b/src/pages/play/lobby/api.ts
new file mode 100644
index 00000000..c032eff1
--- /dev/null
+++ b/src/pages/play/lobby/api.ts
@@ -0,0 +1,19 @@
+import { client } from '@/shared/api';
+
+export const getLobbyInfo = async (roodId: string | undefined) => {
+ if (!roodId) {
+ return { masterIndex: -1 } as EnteredRoom;
+ }
+
+ return client
+ .get(`/play/friendly/lobby/${roodId}`)
+ .then(({ data }) => data)
+ .catch((err) => {
+ console.log(err);
+ return { masterIndex: -1 } as EnteredRoom;
+ });
+};
+
+export const getReady = async () => {
+ return client.get('/play/friendly/ready');
+};
diff --git a/src/pages/play/lobby/constants.ts b/src/pages/play/lobby/constants.ts
deleted file mode 100644
index c2a82302..00000000
--- a/src/pages/play/lobby/constants.ts
+++ /dev/null
@@ -1,59 +0,0 @@
-import { calc } from '@vanilla-extract/css-utils';
-
-import { vars } from '@/styles/vars.css';
-
-export const PLAYER_COLORS = ['pink', 'green', 'yellow', 'blue'] as const;
-
-export const EMPTY_PLAYER_COLOR = 'gray300' as const;
-
-export const PLAYER_ICON = {
- pink: '/images/ui/icon/shape/icon-shape-white-small-player0-h16w16.png',
- green: '/images/ui/icon/shape/icon-shape-white-small-player1-h16w16.png',
- yellow: '/images/ui/icon/shape/icon-shape-white-small-player2-h16w16.png',
- blue: '/images/ui/icon/shape/icon-shape-white-small-player3-h16w16.png',
-};
-
-export const PLAYER_ICON_LARGE = {
- pink: '/images/ui/icon/shape/icon-shape-white-big-player0-h48w48.png',
- green: '/images/ui/icon/shape/icon-shape-white-big-player1-h48w48.png',
- yellow: '/images/ui/icon/shape/icon-shape-white-big-player2-h48w48.png',
- blue: '/images/ui/icon/shape/icon-shape-white-big-player3-h48w48.png',
- gray300: '',
-};
-
-export const TITLE_MARGIN_BOTTOM = '1x';
-
-export const TITLE_SIZE = '1.5x';
-
-export const PLAYER_WRAPPER_HEIGHT = calc('100%')
- .subtract(vars.fontSize[TITLE_SIZE])
- .subtract(vars.space[TITLE_MARGIN_BOTTOM])
- .toString();
-
-export const PLAYER_INFO = {
- default: {
- titleSize: vars.fontSize['0.75x'],
- nameSize: vars.fontSize['1x'],
- padding: vars.space['2x'],
- questionMarkSize: vars.fontSize['1.5x'],
- },
- small: {
- titleSize: vars.fontSize['0.5x'],
- nameSize: vars.fontSize['0.75x'],
- padding: vars.space['1.5x'],
- questionMarkSize: vars.fontSize['1.25x'],
- },
-};
-
-export const PLAYER_INFO_HEIGHT = (size: keyof typeof PLAYER_INFO) =>
- calc(PLAYER_INFO[size].titleSize)
- .add(PLAYER_INFO[size].nameSize)
- .add(PLAYER_INFO[size].padding)
- .add(PLAYER_INFO[size].padding)
- .toString();
-
-export const LONELY_MASTER_MESSAGE: string =
- '혼자서 싸울 수는 없습니다. \n 혹시 친구가 없나요?';
-
-export const SOMEBODY_NOT_READY_MESSAGE: string =
- '싸움을 두려워하는 겁쟁이가 있나본데요?';
diff --git a/src/pages/play/lobby/constants/index.ts b/src/pages/play/lobby/constants/index.ts
new file mode 100644
index 00000000..fc6c8309
--- /dev/null
+++ b/src/pages/play/lobby/constants/index.ts
@@ -0,0 +1,40 @@
+import { calc } from '@vanilla-extract/css-utils';
+
+import { vars } from '@/styles/vars.css';
+
+export { PLAYER_COLORS } from '@/models/player';
+
+export const EMPTY_PLAYER_COLOR = 'gray300' as const;
+
+export const TITLE_MARGIN_BOTTOM = '1x';
+
+export const TITLE_SIZE = '1.5x';
+
+export const PLAYER_WRAPPER_HEIGHT = calc('100%')
+ .subtract(vars.fontSize[TITLE_SIZE])
+ .subtract(vars.space[TITLE_MARGIN_BOTTOM])
+ .toString();
+
+export const PLAYER_INFO = {
+ default: {
+ titleSize: vars.fontSize['0.75x'],
+ nameSize: vars.fontSize['1x'],
+ padding: vars.space['2x'],
+ questionMarkSize: vars.fontSize['1.5x'],
+ },
+ small: {
+ titleSize: vars.fontSize['0.5x'],
+ nameSize: vars.fontSize['0.75x'],
+ padding: vars.space['1.5x'],
+ questionMarkSize: vars.fontSize['1.25x'],
+ },
+};
+
+export const PLAYER_INFO_HEIGHT = (size: keyof typeof PLAYER_INFO) =>
+ calc(PLAYER_INFO[size].titleSize)
+ .add(PLAYER_INFO[size].nameSize)
+ .add(PLAYER_INFO[size].padding)
+ .add(PLAYER_INFO[size].padding)
+ .toString();
+
+export * as MESSAGE from './messages';
diff --git a/src/pages/play/lobby/constants/messages.ts b/src/pages/play/lobby/constants/messages.ts
new file mode 100644
index 00000000..b5d81ea9
--- /dev/null
+++ b/src/pages/play/lobby/constants/messages.ts
@@ -0,0 +1,3 @@
+export const LONELY_MASTER =
+ '혼자서 싸울 수는 없습니다. \n 혹시 친구가 없나요?';
+export const SOMEBODY_NOT_READY = '싸움을 두려워하는 겁쟁이가 있나본데요?';
diff --git a/src/pages/play/lobby/index.css.ts b/src/pages/play/lobby/index.css.ts
index 95c76ae9..b115b500 100644
--- a/src/pages/play/lobby/index.css.ts
+++ b/src/pages/play/lobby/index.css.ts
@@ -79,7 +79,6 @@ export const text = style([
},
]);
-// MapInfo.tsx
export const mapInfo = style([
sprinkles({
marginTop: '4x',
diff --git a/src/pages/play/lobby/index.ts b/src/pages/play/lobby/index.ts
new file mode 100644
index 00000000..5ba2b64b
--- /dev/null
+++ b/src/pages/play/lobby/index.ts
@@ -0,0 +1,2 @@
+export { default } from './page';
+export { getLobbyInfo } from './api';
diff --git a/src/pages/play/lobby/index.tsx b/src/pages/play/lobby/page.tsx
similarity index 58%
rename from src/pages/play/lobby/index.tsx
rename to src/pages/play/lobby/page.tsx
index 14ec89f1..9d79fd51 100644
--- a/src/pages/play/lobby/index.tsx
+++ b/src/pages/play/lobby/page.tsx
@@ -1,32 +1,21 @@
import { useEffect, useState } from 'react';
import { useLoaderData, useNavigate } from 'react-router-dom';
-import * as constants from './constants';
import * as styles from './index.css';
+import Players from './Players';
+import SettingButton from './SettingButton';
-import type { Lobby } from '@/types/play';
-
+import SettingNavigationButton from '@/components/button/SettingNavigationButton';
import Chip from '@/components/chip';
-import BasicContentFrame from '@/components/frame/with-buttons';
-
-
-import { MAPS } from '@/pages/play/finder/constants';
-import MapInfo from '@/pages/play/lobby/MapInfo';
-import Players from '@/pages/play/lobby/Players';
-import SettingButton from '@/pages/play/lobby/SettingButton';
-import { getOut } from '@/services/lobby';
-
-import { useUserStateStore } from '@/states/user';
-
-import { ROUTE } from '@/constants/routes';
import { useWebSocket } from '@/features/websocket';
+import Map from '@/models/map';
+import { leaveRoom } from '@/models/room';
+import { useUserCode } from '@/models/user';
+import { ROUTE } from '@/shared/constants';
-const getLobbyInfo = (lobby: Lobby) => {
+const getLobbyInfo = (lobby: EnteredRoom) => {
lobby.players.forEach((player, index) => {
- player.color = player.userCode
- ? constants.PLAYER_COLORS[index]
- : constants.EMPTY_PLAYER_COLOR;
player.isMaster = index === lobby.masterIndex;
player.key = `${player.userCode}${index}`;
});
@@ -34,13 +23,11 @@ const getLobbyInfo = (lobby: Lobby) => {
};
const Lobby = () => {
- const myUserCode = useUserStateStore((state) => state.userCode);
+ const myUserCode = useUserCode();
const navigate = useNavigate();
- const lobbyData = useLoaderData() as Lobby;
+ const lobbyData = useLoaderData() as EnteredRoom;
const [lobbyInfo, setLobbyInfo] = useState(getLobbyInfo(lobbyData));
- const mapInfo = MAPS.find((map) => map.mapId === lobbyInfo.mapId);
-
const isMaster =
lobbyInfo.players[lobbyInfo.masterIndex].userCode === myUserCode;
@@ -56,21 +43,24 @@ const Lobby = () => {
useEffect(() => {
return () => {
- getOut();
+ leaveRoom();
};
}, []);
return (
-
+ <>
+ navigate(ROUTE.finder, { replace: true })}
+ position='leftTop'
+ />
{isMaster &&
}
- {mapInfo &&
}
+
+
+
{lobbyInfo.roomCode}
{lobbyInfo.roomPassword}
@@ -78,10 +68,10 @@ const Lobby = () => {
{lobbyInfo.roomTitle}
-
+
-
+ >
);
};
diff --git a/src/pages/play/lobby/types.d.ts b/src/pages/play/lobby/types.d.ts
deleted file mode 100644
index 2353cb9a..00000000
--- a/src/pages/play/lobby/types.d.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-import * as constants from './constants';
-
-type playerColorType =
- | (typeof constants.PLAYER_COLORS)[number]
- | typeof constants.EMPTY_PLAYER_COLOR;
diff --git a/src/pages/play/mode/Matching.tsx b/src/pages/play/mode/Matching.tsx
index aa5adacc..dddd8f59 100644
--- a/src/pages/play/mode/Matching.tsx
+++ b/src/pages/play/mode/Matching.tsx
@@ -9,8 +9,8 @@ import { MatchingText } from '@/pages/play/mode/MatchingText';
import { deleteMatching } from '@/services/matching';
-import { ROUTE } from '@/constants/routes';
import { useWebSocket } from '@/features/websocket';
+import { ROUTE } from '@/shared/constants';
interface Props {
imgSrc: string;
diff --git a/src/pages/play/mode/index.tsx b/src/pages/play/mode/index.tsx
index dd075f22..6cd53698 100644
--- a/src/pages/play/mode/index.tsx
+++ b/src/pages/play/mode/index.tsx
@@ -1,31 +1,29 @@
import { useState } from 'react';
-import { useLoaderData, useNavigate } from 'react-router-dom';
+import { useNavigate } from 'react-router-dom';
import * as constants from './constants';
import * as styles from './index.css';
-import { User } from '@/types/auth';
-
import ColoredIconButton from '@/components/button/ColoredIconButton';
-import BasicContentFrame from '@/components/frame/with-buttons';
+import Modal, { useModal } from '@/components/modal';
import RankingItemBox from '@/components/ranking';
-import useModal from '@/hooks/useModal';
import Matching from '@/pages/play/mode/Matching';
import { getMatching } from '@/services/matching';
-import { ROUTE } from '@/constants/routes';
+import { useUserInfo } from '@/models/user';
+import { ROUTE } from '@/shared/constants';
const Mode = () => {
const [isModalOpen, setIsModalOpen] = useState(false);
- const { Modal, openModal, closeModal } = useModal({
+ const { modalRef, openModal, closeModal } = useModal({
onOpen: () => setIsModalOpen(true),
onClose: () => setIsModalOpen(false),
});
const navigate = useNavigate();
- const user = useLoaderData() as User;
+ const { user } = useUserInfo();
const startMatching = async () => {
const matchingSuccess = await getMatching();
@@ -36,8 +34,12 @@ const Mode = () => {
}
};
+ if (!user) {
+ return null;
+ }
+
return (
-
+ <>
{
-
+
-
+ >
);
};
diff --git a/src/pages/ranking/index.tsx b/src/pages/ranking/index.tsx
index 66992acb..5c994cda 100644
--- a/src/pages/ranking/index.tsx
+++ b/src/pages/ranking/index.tsx
@@ -3,41 +3,39 @@ import { useLoaderData } from 'react-router-dom';
import * as constants from './constants';
import * as styles from './index.css';
-import type { User } from '@/types/auth';
-import type { RankPlayer } from '@/types/rank';
-
-import BasicContentFrame from '@/components/frame/with-buttons';
import RankingItemBox from '@/components/ranking';
+import { useUserInfo } from '@/models/user';
+
const Ranking = () => {
- const { rankList, myRank } = useLoaderData() as {
- rankList: RankPlayer[];
- myRank: User;
+ const { rankList } = useLoaderData() as {
+ rankList: Profile[];
};
+ const { user: myRank } = useUserInfo();
+
+ if (!myRank) {
+ return null;
+ }
return (
-
-
-
-
- {constants.RANKING_TITLE}
-
- {myRank.guest && (
-
{constants.GUEST_TEXT}
- )}
-
-
- {rankList.map((item) => (
-
- ))}
-
- {myRank && (
-
-
-
+
+
+
{constants.RANKING_TITLE}
+ {myRank.guest && (
+
{constants.GUEST_TEXT}
)}
-
+
+ {rankList.map((item) => (
+
+ ))}
+
+ {myRank && (
+
+
+
+ )}
+
);
};
diff --git a/src/pages/result/ResultInfoBox.tsx b/src/pages/result/ResultInfoBox.tsx
deleted file mode 100644
index a4a7e1df..00000000
--- a/src/pages/result/ResultInfoBox.tsx
+++ /dev/null
@@ -1,34 +0,0 @@
-import PlayerInfo from '@/components/player-info';
-
-import * as styles from '@/pages/result/index.css';
-import type { colorStyles } from '@/pages/result/types';
-
-export interface ResultInfoBoxProps {
- rank: number | string;
- imgSrc: string;
- label: string;
- nickname: string;
- gameScore: number;
- myResult: boolean;
- colorStyle: colorStyles;
-}
-
-const ResultInfoBox = ({
- rank,
- imgSrc,
- label,
- nickname,
- gameScore,
- myResult,
- colorStyle,
-}: ResultInfoBoxProps) => {
- return (
-
-
{rank}
-
-
{gameScore}
-
- );
-};
-
-export default ResultInfoBox;
diff --git a/src/pages/result/api.ts b/src/pages/result/api.ts
new file mode 100644
index 00000000..74c9d1d5
--- /dev/null
+++ b/src/pages/result/api.ts
@@ -0,0 +1,5 @@
+import { client } from '@/shared/api';
+
+export const getGameResult = async () => {
+ return client.get
('/ingame').then(({ data }) => data);
+};
diff --git a/src/pages/result/constants.ts b/src/pages/result/constants.ts
index 96bc071c..a47c79b0 100644
--- a/src/pages/result/constants.ts
+++ b/src/pages/result/constants.ts
@@ -47,5 +47,3 @@ export const MODAL_REWARDBOX =
export const TIER_UPGRADE_MESSAGE = 'Origin에 한 발자국 더 나아갔습니다!';
export const REWARD_MESSAGE = '잃어버린 색을 찾았습니다!';
-
-export const COLOR_STYLES = ['pink', 'yellow', 'green', 'blue'] as const;
diff --git a/src/pages/result/index.ts b/src/pages/result/index.ts
new file mode 100644
index 00000000..b5aa0bb6
--- /dev/null
+++ b/src/pages/result/index.ts
@@ -0,0 +1,2 @@
+export { default } from './ui/page';
+export { getGameResult } from './api';
diff --git a/src/pages/result/types.d.ts b/src/pages/result/types.d.ts
index 36003711..fbbe5ad7 100644
--- a/src/pages/result/types.d.ts
+++ b/src/pages/result/types.d.ts
@@ -1,3 +1,19 @@
-import * as resultConstants from './constants';
+type PlayerRecord = {
+ index: number;
+ score: number;
+} & Pick;
-type colorStyles = (typeof resultConstants.COLOR_STYLES)[number];
+interface TierReward {
+ oldtier: TierValue;
+ newtier: TierValue;
+ upgrade: boolean;
+}
+
+interface GameResult {
+ roomUuid?: string;
+ players: PlayerRecord[];
+ reward: {
+ tier?: TierReward;
+ skins: string[];
+ };
+}
diff --git a/src/pages/result/ui/ResultInfoBox.tsx b/src/pages/result/ui/ResultInfoBox.tsx
new file mode 100644
index 00000000..3e11957a
--- /dev/null
+++ b/src/pages/result/ui/ResultInfoBox.tsx
@@ -0,0 +1,25 @@
+import * as styles from './index.css';
+
+import PlayerInfo from '@/components/player-info';
+
+import { PLAYER_COLORS } from '@/models/player';
+
+interface Props {
+ player: PlayerRecord;
+ myResult: boolean;
+}
+
+const ResultInfoBox = ({ player, myResult }: Props) => {
+ const { rank, label, skin, nickname, score, index } = player;
+ const colorStyle = PLAYER_COLORS[index];
+
+ return (
+
+ );
+};
+
+export default ResultInfoBox;
diff --git a/src/pages/result/RewardsModal/SkinReward.tsx b/src/pages/result/ui/RewardsModal/SkinReward.tsx
similarity index 96%
rename from src/pages/result/RewardsModal/SkinReward.tsx
rename to src/pages/result/ui/RewardsModal/SkinReward.tsx
index 7e17e07a..283689e4 100644
--- a/src/pages/result/RewardsModal/SkinReward.tsx
+++ b/src/pages/result/ui/RewardsModal/SkinReward.tsx
@@ -1,4 +1,4 @@
-import * as constants from '../constants';
+import * as constants from '../../constants';
import * as styles from '../index.css';
import ColoredButton from '@/components/button/ColoredButton';
diff --git a/src/pages/result/RewardsModal/TierUpgrade.tsx b/src/pages/result/ui/RewardsModal/TierUpgrade.tsx
similarity index 90%
rename from src/pages/result/RewardsModal/TierUpgrade.tsx
rename to src/pages/result/ui/RewardsModal/TierUpgrade.tsx
index 2caeb57a..321bb04e 100644
--- a/src/pages/result/RewardsModal/TierUpgrade.tsx
+++ b/src/pages/result/ui/RewardsModal/TierUpgrade.tsx
@@ -1,10 +1,9 @@
-import * as constants from '../constants';
+import * as constants from '../../constants';
import * as styles from '../index.css';
-import type { TierReward } from '@/types/result';
-
import ColoredButton from '@/components/button/ColoredButton';
-import TierBox from '@/components/tier';
+
+import TierBox from '@/models/tier';
interface Props {
closeModal: () => void;
diff --git a/src/pages/result/RewardsModal/index.tsx b/src/pages/result/ui/RewardsModal/index.tsx
similarity index 78%
rename from src/pages/result/RewardsModal/index.tsx
rename to src/pages/result/ui/RewardsModal/index.tsx
index aa1ed8b0..b0c159b0 100644
--- a/src/pages/result/RewardsModal/index.tsx
+++ b/src/pages/result/ui/RewardsModal/index.tsx
@@ -1,9 +1,7 @@
import { useState } from 'react';
-import type { TierReward } from '@/types/result';
-
-import SkinReward from '@/pages/result/RewardsModal/SkinReward';
-import TierUpgrade from '@/pages/result/RewardsModal/TierUpgrade';
+import SkinReward from './SkinReward';
+import TierUpgrade from './TierUpgrade';
interface Props {
tier?: TierReward;
diff --git a/src/pages/result/index.css.ts b/src/pages/result/ui/index.css.ts
similarity index 99%
rename from src/pages/result/index.css.ts
rename to src/pages/result/ui/index.css.ts
index 56541c92..56e13e5b 100644
--- a/src/pages/result/index.css.ts
+++ b/src/pages/result/ui/index.css.ts
@@ -1,7 +1,7 @@
import { style } from '@vanilla-extract/css';
import { recipe } from '@vanilla-extract/recipes';
-import * as constants from './constants';
+import * as constants from '../constants';
import { flexOptions } from '@/styles/common.css';
import { sprinkles } from '@/styles/sprinkles.css';
diff --git a/src/pages/result/index.tsx b/src/pages/result/ui/page.tsx
similarity index 57%
rename from src/pages/result/index.tsx
rename to src/pages/result/ui/page.tsx
index 757b73bd..d7740b89 100644
--- a/src/pages/result/index.tsx
+++ b/src/pages/result/ui/page.tsx
@@ -1,38 +1,27 @@
import { useEffect } from 'react';
import { useLoaderData, useNavigate } from 'react-router-dom';
-import * as constants from './constants';
+import * as constants from '../constants';
import * as styles from './index.css';
-
-import type { GameResult } from '@/types/result';
+import ResultInfoBox from './ResultInfoBox';
+import { RewardsModal } from './RewardsModal';
import ColoredButton from '@/components/button/ColoredButton';
-import BasicContentFrame from '@/components/frame/with-buttons';
-
-import useModal from '@/hooks/useModal';
-
-import ResultInfoBox from '@/pages/result/ResultInfoBox';
-import { RewardsModal } from '@/pages/result/RewardsModal/index';
-
-import { getOut } from '@/services/lobby';
-
-import { useUserStateStore } from '@/states/user';
-
-import { ROUTE } from '@/constants/routes';
-import { useWebSocketStore } from '@/features/websocket';
+import Modal, { useModal } from '@/components/modal';
+import { leaveRoom } from '@/models/room';
+import { useUserCode } from '@/models/user';
+import { ROUTE } from '@/shared/constants';
const Result = () => {
const navigate = useNavigate();
const { roomUuid, players, reward } = useLoaderData() as GameResult;
- const myCode = useUserStateStore.getState().userCode;
- const { Modal, openModal, closeModal } = useModal();
+ const myCode = useUserCode();
+ const { modalRef, openModal, closeModal } = useModal();
const hasTier = !!reward.tier && reward.tier.upgrade;
const noReward = !hasTier && reward.skins.length === 0;
- const { webSocket } = useWebSocketStore.getState();
useEffect(() => {
- webSocket.inGameUnconnected(() => {});
if (!noReward) {
openModal();
}
@@ -40,7 +29,7 @@ const Result = () => {
const handleClickExit = async () => {
if (roomUuid) {
- await getOut();
+ await leaveRoom();
}
navigate(ROUTE.home, { replace: true });
};
@@ -54,23 +43,16 @@ const Result = () => {
};
return (
-
+ <>
{constants.RESULTTEXT}
-
{players.map((item) => (
-
-
-
+
))}
@@ -91,7 +73,7 @@ const Result = () => {
{!noReward && (
-
+
{
/>
)}
-
+ >
);
};
diff --git a/src/pages/settings/Logout.tsx b/src/pages/settings/Logout.tsx
index 7dc4a98e..88dd5a66 100644
--- a/src/pages/settings/Logout.tsx
+++ b/src/pages/settings/Logout.tsx
@@ -1,20 +1,15 @@
-import { useNavigate } from 'react-router-dom';
-
import * as styles from './index.css';
import ColoredButton from '@/components/button/ColoredButton';
+import { useLogout } from '@/features/auth';
+
interface Props {
onClose: () => void;
}
const Logout = ({ onClose }: Props) => {
- const navigate = useNavigate();
-
- const removeToken = () => {
- window.localStorage.removeItem('token');
- navigate('/');
- };
+ const { logout } = useLogout();
return (
@@ -24,12 +19,7 @@ const Logout = ({ onClose }: Props) => {
영원히 기억됩니다.
-
+
void;
@@ -27,7 +25,7 @@ const NicknameChange = ({ onClose }: Props) => {
const nicknameChange = async () => {
if (nickname.length < 2 || nickname.length > 9) {
- setErrorMessage(constants.ERROR_MESSAGE.inValidNickname);
+ setErrorMessage(ERROR_MESSAGE.inValidNickname);
return;
}
const confirmNickname = await patchNicknameChange(nickname);
diff --git a/src/pages/settings/PasswordChange.tsx b/src/pages/settings/PasswordChange.tsx
index f9d041a9..a98ac9ab 100644
--- a/src/pages/settings/PasswordChange.tsx
+++ b/src/pages/settings/PasswordChange.tsx
@@ -5,10 +5,10 @@ import * as styles from './index.css';
import ColoredButton from '@/components/button/ColoredButton';
import Input from '@/components/input';
-import * as constants from '@/pages/landing/logIn/constants';
-
import { patchPasswordChange } from '@/services/auth';
+import { ERROR_MESSAGE } from '@/shared/constants';
+
interface Props {
onClose: () => void;
}
@@ -28,12 +28,12 @@ const PasswordChange = ({ onClose }: Props) => {
const passwordChangeClick = async () => {
if (password.length !== 6 || newPassword.length !== 6) {
- setErrorMessage(constants.ERROR_MESSAGE.inValidPasswordLength);
+ setErrorMessage(ERROR_MESSAGE.inValidPasswordLength);
return;
}
const validPassword = await patchPasswordChange(password, newPassword);
if (!validPassword) {
- setErrorMessage(constants.ERROR_MESSAGE.inValidPassword);
+ setErrorMessage(ERROR_MESSAGE.inValidPassword);
return;
}
onClose();
diff --git a/src/pages/settings/delete-account.tsx b/src/pages/settings/delete-account.tsx
index 663fb893..6111f65f 100644
--- a/src/pages/settings/delete-account.tsx
+++ b/src/pages/settings/delete-account.tsx
@@ -5,10 +5,10 @@ import * as styles from './index.css';
import ColoredButton from '@/components/button/ColoredButton';
import Input from '@/components/input';
-import * as constants from '@/pages/landing/logIn/constants';
-
import { deleteUserInfo, postConfirmPassword } from '@/services/auth';
+import { ERROR_MESSAGE } from '@/shared/constants';
+
interface Props {
onClose: () => void;
}
@@ -28,7 +28,7 @@ const DeleteAccount = ({ onClose }: Props) => {
setCheck(true);
}
if (!isConfirmed) {
- setErrorMessage(constants.ERROR_MESSAGE.inValidSignOut);
+ setErrorMessage(ERROR_MESSAGE.inValidSignOut);
}
};
diff --git a/src/pages/settings/index.tsx b/src/pages/settings/index.tsx
index 370f6ecf..910057cd 100644
--- a/src/pages/settings/index.tsx
+++ b/src/pages/settings/index.tsx
@@ -1,23 +1,24 @@
+import { Navigate } from 'react-router-dom';
+
import * as styles from './index.css';
import MemberSetting from './member-setting';
-import BasicContentFrame from '@/components/frame/with-buttons';
-
-import { useUserStatus } from '@/features/user';
+import { useUserStatus } from '@/models/user';
+import { ROUTE } from '@/shared/constants';
const Settings = () => {
const { isMember } = useUserStatus();
- return (
-
- {isMember && (
-
- )}
-
- );
+ if (isMember) {
+ return (
+
+ );
+ }
+
+ return ;
};
export default Settings;
diff --git a/src/pages/tutorial/Info.tsx b/src/pages/tutorial/Info.tsx
index 239a8347..3a3757e0 100644
--- a/src/pages/tutorial/Info.tsx
+++ b/src/pages/tutorial/Info.tsx
@@ -9,7 +9,7 @@ import Info0 from '@/pages/tutorial/Info0';
import Info1 from '@/pages/tutorial/Info1';
import Info2 from '@/pages/tutorial/Info2';
-import { ROUTE } from '@/constants/routes';
+import { ROUTE } from '@/shared/constants';
interface Props {
page: number;
@@ -38,7 +38,7 @@ const Info = ({ page }: Props) => {
text={constants.INFO_DESC.BUTTON}
color='green'
onClick={() => {
- navigate(ROUTE.home, { replace: true });
+ navigate(ROUTE.home);
}}
/>
diff --git a/src/pages/tutorial/index.tsx b/src/pages/tutorial/index.tsx
index ef92dd7f..b22fa9ff 100644
--- a/src/pages/tutorial/index.tsx
+++ b/src/pages/tutorial/index.tsx
@@ -3,7 +3,6 @@ import { useState } from 'react';
import * as styles from './index.css';
import SettingTextButton from '@/components/button/SettingTextButton';
-import BasicContentFrame from '@/components/frame/with-buttons';
import { MAX_PAGE_SIZE } from '@/pages/tutorial/constants';
import Info from '@/pages/tutorial/Info';
@@ -30,8 +29,7 @@ const Tutorial = () => {
};
return (
-
-
+
-
);
};
diff --git a/src/services/auth.ts b/src/services/auth.ts
index db86caae..8ea474f5 100644
--- a/src/services/auth.ts
+++ b/src/services/auth.ts
@@ -1,94 +1,6 @@
-import { redirect } from 'react-router-dom';
+import { NicknameInfo, PasswordInfo } from '@/types/auth';
-import {
- LogInInfo,
- NicknameInfo,
- PasswordInfo,
- SignUpInfo,
- User,
-} from '@/types/auth';
-
-
-import { useUserStateStore } from '@/states/user';
-
-import { ROUTE } from '@/constants/routes';
-import { api } from '@/features/api';
-
-export const getGuestLogin = async () => {
- return api
- .get
(false, '/user/guest')
- .then((response) => {
- localStorage.setItem('token', response.data);
- return true;
- })
- .catch(() => false);
-};
-
-export const postMemberLogin = async (logInInfo: LogInInfo) => {
- return await api
- .post(false, `/user/login`, logInInfo)
- .then((response) => {
- // console.log(response.data);
- if (response.status === 200) {
- localStorage.setItem('token', response.data);
- return true;
- }
- if (response.status === 401) {
- redirect(`${ROUTE.error}/${401}`);
- return false;
- }
- })
- // eslint-disable-next-line
- .catch((e) => {
- redirect(`${ROUTE.error}/${500}`);
- console.log(e);
- return false;
- });
-};
-
-export const getUser = async () => {
- return api.get(true, `/user`).then((response) => {
- const user = response.data;
- const { setLogin, setUserCode } = useUserStateStore.getState();
- setLogin(user.guest);
- setUserCode(user.userCode);
- return user;
- });
-};
-
-export const getIdCheck = async (id: string) => {
- try {
- const response = await api.get(false, `/user/dup/${id}`);
- return response.data; // 중복이 안되면 false. true면 오류
- } catch (e) {
- return false;
- }
-};
-
-export const postGuestSignUp = async (signUpInfo: SignUpInfo) => {
- return (
- api
- .post(true, '/user/guest', signUpInfo)
- .then(() => {
- return true;
- })
- // eslint-disable-next-line
- .catch((error) => {
- console.log(error);
- return false;
- })
- );
-};
-
-export const postSignUp = async (signUpInfo: SignUpInfo) => {
- return api
- .post(false, '/user/signup', signUpInfo)
- .then((res) => {
- localStorage.setItem('token', res.data);
- return true;
- })
- .catch(() => false);
-};
+import { api } from '@/shared/api';
export const patchNicknameChange = async (nickname: string) => {
try {
diff --git a/src/services/collections.ts b/src/services/collections.ts
deleted file mode 100644
index 901f36dd..00000000
--- a/src/services/collections.ts
+++ /dev/null
@@ -1,49 +0,0 @@
-import { Collections } from '@/types/collections';
-
-import { api } from '@/features/api';
-
-export const getCollections = async (): Promise => {
- return (
- api
- .get(true, '/collection')
- .then((response) => {
- return response.data;
- })
- // eslint-disable-next-line
- .catch((err) => {
- console.log(err);
- })
- );
-};
-
-export const patchSkinChange = async (skinId: number) => {
- return (
- api
- .patch(true, '/collection/skin', { skinId })
- .then((res) => {
- return res.data;
- })
- // eslint-disable-next-line
- .catch((error) => {
- console.log(error);
- return false;
- })
- );
-};
-
-export const patchLabelChange = async (labelId: number) => {
- return (
- api
- .patch(true, '/collection/label', {
- labelId,
- })
- .then((res) => {
- return res.data;
- })
- // eslint-disable-next-line
- .catch((error) => {
- console.log(error);
- return false;
- })
- );
-};
diff --git a/src/services/finder.ts b/src/services/finder.ts
deleted file mode 100644
index 2ed99a73..00000000
--- a/src/services/finder.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-import type { CreateRoom, RequestEnterRoom, RoomListItem } from '@/types/play';
-
-import { api } from '@/features/api';
-
-export const getRoomList = async (offset: number) => {
- return api
- .get(true, `play/friendly/list/${offset}`)
- .then((response) => {
- return response.data;
- });
-};
-
-export const postCreateRoom = async (roomRequest: CreateRoom) => {
- return (
- api
- .post(true, 'play/friendly', roomRequest)
- .then((res) => {
- // console.log('방 생성 요청 성공');
- return res.data;
- })
- // eslint-disable-next-line
- .catch((error) => {
- console.log(error);
- return '';
- })
- );
-};
-
-export const postEnterRoom = async (roomInfo: RequestEnterRoom) => {
- return await api
- .post(true, 'play/friendly/enter', roomInfo)
- .then((res) => {
- return res.data;
- })
- // eslint-disable-next-line
- .catch((error) => {
- console.log(error);
- return '';
- });
-};
diff --git a/src/services/ingame.ts b/src/services/ingame.ts
deleted file mode 100644
index 191ef829..00000000
--- a/src/services/ingame.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import { IngameReady } from '@/types/ingame';
-
-import { api } from '@/features/api';
-
-export const getIngameReady = async () => {
- try {
- const response = await api.get(true, '/ingame/ready');
- return response.data;
- } catch (e) {
- // console.log(e);
- return null;
- }
-};
diff --git a/src/services/lobby.ts b/src/services/lobby.ts
deleted file mode 100644
index 3a917eab..00000000
--- a/src/services/lobby.ts
+++ /dev/null
@@ -1,51 +0,0 @@
-import type { CreateRoom, Lobby } from '@/types/play';
-
-import { api } from '@/features/api';
-
-export const getLobbyInfo = async (roodId: string | undefined) => {
- if (!roodId) return { masterIndex: -1 } as Lobby;
- return (
- api
- .get(true, `/play/friendly/lobby/${roodId}`)
- .then((res) => res.data)
- // eslint-disable-next-line
- .catch((err) => {
- console.log(err);
- return { masterIndex: -1 } as Lobby;
- })
- );
-};
-
-export const getReady = async () => {
- return api.get(true, '/play/friendly/ready');
-};
-
-export const getOut = async () => {
- return (
- api
- .get(true, '/play/friendly/out')
- .then((res) => {
- console.debug('방 나가기 요청 성공', res.data);
- })
- // eslint-disable-next-line
- .catch((err) => {
- console.log(err);
- })
- );
-};
-
-export const updateRoom = async (room: CreateRoom) => {
- return (
- api
- .post(true, '/play/friendly/renew', room)
- .then(() => {
- // console.log('방 정보 수정 요청 성공');
- return '';
- })
- // eslint-disable-next-line
- .catch((err) => {
- console.log(err);
- return '';
- })
- );
-};
diff --git a/src/services/matching.ts b/src/services/matching.ts
index 49028d90..1451cbf3 100644
--- a/src/services/matching.ts
+++ b/src/services/matching.ts
@@ -1,4 +1,4 @@
-import { api } from '@/features/api';
+import { api } from '@/shared/api';
export const getMatching = async () => {
return (
diff --git a/src/services/rank.ts b/src/services/rank.ts
index 212dbe60..c47fcc8a 100644
--- a/src/services/rank.ts
+++ b/src/services/rank.ts
@@ -1,27 +1,13 @@
-import type { RankInfo, RankPlayer } from '@/types/rank';
-
-import { getUser } from '@/services/auth';
-
-import { api } from '@/features/api';
+import { api } from '@/shared/api';
const requestRankList = async () => {
- return (
- api
- .get(true, 'rank/list')
- .then((res) => {
- return res.data.players;
- })
- // eslint-disable-next-line
- .catch((error) => {
- console.log(error);
- return [] as RankPlayer[];
- })
- );
+ return api.get(true, 'rank/list').then((res) => {
+ return res.data.players;
+ });
};
export const getRank = async () => {
const rankList = await requestRankList();
- const myRank = await getUser();
- return { rankList, myRank };
+ return { rankList };
};
diff --git a/src/services/result.ts b/src/services/result.ts
deleted file mode 100644
index 089cc0a6..00000000
--- a/src/services/result.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-import type { GameResult } from '@/types/result';
-
-import { api } from '@/features/api';
-
-export const getGameResult = async () => {
- return api.get(true, '/ingame').then((res) => {
- return res.data;
- });
-};
diff --git a/src/features/api/index.ts b/src/shared/api/axios.ts
similarity index 93%
rename from src/features/api/index.ts
rename to src/shared/api/axios.ts
index 3e55b925..4d89576b 100644
--- a/src/features/api/index.ts
+++ b/src/shared/api/axios.ts
@@ -1,9 +1,9 @@
import axios, { type AxiosResponse } from 'axios';
-import { API_BASE_URL } from '@/features/api/constants';
+const baseURL = import.meta.env.VITE_API_BASE_URL;
const client = axios.create({
- baseURL: API_BASE_URL,
+ baseURL,
headers: {
'Content-Type': 'application/json',
},
@@ -70,4 +70,4 @@ const api = {
},
};
-export { api };
+export { api, client };
diff --git a/src/shared/api/index.ts b/src/shared/api/index.ts
new file mode 100644
index 00000000..0165cbb1
--- /dev/null
+++ b/src/shared/api/index.ts
@@ -0,0 +1,2 @@
+export { api, client } from './axios';
+export { queryClient } from './react-query';
diff --git a/src/shared/api/react-query.ts b/src/shared/api/react-query.ts
new file mode 100644
index 00000000..6d46de59
--- /dev/null
+++ b/src/shared/api/react-query.ts
@@ -0,0 +1,3 @@
+import { QueryClient } from '@tanstack/react-query';
+
+export const queryClient = new QueryClient();
diff --git a/src/shared/constants/error-messages.ts b/src/shared/constants/error-messages.ts
new file mode 100644
index 00000000..0ecf78c5
--- /dev/null
+++ b/src/shared/constants/error-messages.ts
@@ -0,0 +1,12 @@
+export const loginFail = '컬러랜드 입국정보를\n다시 한 번 확인해주세요.';
+
+export const welcome = '환영합니다. 어쩌면 진짜 Origin';
+export const sameId = '중복된 ID!! 진짜 당신은 누구죠?';
+export const blank = '당신의 역사가 비어있네요? 수상합니다.';
+export const notSamePassword = '암호가 일치하지 않습니다.';
+export const invalidInput = '당신의 정보가 너무 짧거나, 너무 길거나.';
+
+export const inValidNickname = 'Origin께서 허락하지 못한 이름입니다.';
+export const inValidPasswordLength = '비밀번호는 숫자 6자리여야 합니다.';
+export const inValidPassword = '경고! 현재 PASSWORD와 다릅니다!!';
+export const inValidSignOut = 'Origin 의 허락 없이 삭제는 불가능합니다.';
diff --git a/src/shared/constants/index.ts b/src/shared/constants/index.ts
new file mode 100644
index 00000000..bb3d699a
--- /dev/null
+++ b/src/shared/constants/index.ts
@@ -0,0 +1,3 @@
+export * as ROUTE from './routes';
+export * as ERROR_MESSAGE from './error-messages';
+export * from './screen';
diff --git a/src/shared/constants/routes.ts b/src/shared/constants/routes.ts
new file mode 100644
index 00000000..4d07ea81
--- /dev/null
+++ b/src/shared/constants/routes.ts
@@ -0,0 +1,13 @@
+export const main = '/';
+export const login = '/login';
+export const tutorial = '/tutorial';
+export const play = '/play';
+export const finder = '/play/finder';
+export const game = '/play/game';
+export const lobby = '/play/lobby';
+export const ranking = '/ranking';
+export const collection = '/collection';
+export const home = '/home';
+export const result = '/result';
+export const setting = '/settings';
+export const error = '/error';
diff --git a/src/constants/screen.ts b/src/shared/constants/screen.ts
similarity index 100%
rename from src/constants/screen.ts
rename to src/shared/constants/screen.ts
diff --git a/src/shared/utils/index.ts b/src/shared/utils/index.ts
new file mode 100644
index 00000000..86195d3d
--- /dev/null
+++ b/src/shared/utils/index.ts
@@ -0,0 +1 @@
+export * from './screen';
diff --git a/src/services/landing.ts b/src/shared/utils/screen.ts
similarity index 100%
rename from src/services/landing.ts
rename to src/shared/utils/screen.ts
diff --git a/src/states/README.md b/src/states/README.md
deleted file mode 100644
index da505468..00000000
--- a/src/states/README.md
+++ /dev/null
@@ -1,10 +0,0 @@
-#state
-
-### description
-
-상태관리 관련 파일을 저장하는 폴더입니다.
-
-### 사용법
-
-* 상태관리 파일을 생성합니다
-* 상태관리 파일을 사용하는 컴포넌트에서 import하여 사용합니다
diff --git a/src/states/collection.ts b/src/states/collection.ts
deleted file mode 100644
index dacba782..00000000
--- a/src/states/collection.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-import { create } from 'zustand';
-
-interface CollectionState {
- skinId: number;
- skinUrl: string;
- labelId: number;
- labelName: string;
- setSkinId: (id: number) => void;
- setSkinUrl: (url: string) => void;
- setLabel: (id: number) => void;
- setLabelName: (name: string) => void;
-}
-
-export const useCollectionStateStore = create((set) => ({
- skinId: -1,
- labelId: -1,
- skinUrl: '',
- labelName: '',
- setSkinId: (id: number) => {
- set({ skinId: id });
- },
- setSkinUrl: (url: string) => {
- set({ skinUrl: url });
- },
- setLabel: (id: number) => {
- set({ labelId: id });
- },
- setLabelName: (name: string) => {
- set({ labelName: name });
- },
-}));
diff --git a/src/states/effect.ts b/src/states/effect.ts
deleted file mode 100644
index 8e11bc3d..00000000
--- a/src/states/effect.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-import { create } from 'zustand';
-
-interface EffectStoreState {
- isEffect: boolean;
- setEffectSound: (enabled: boolean) => void;
- playEffectSound: () => void;
-}
-
-const useEffectSoundStore = create((set) => ({
- isEffect: localStorage.getItem('effectSound') === 'true',
- setEffectSound: (enabled: boolean) => {
- localStorage.setItem('effectSound', enabled.toString());
- set({ isEffect: enabled });
- },
- playEffectSound: () => {
- if (localStorage.getItem('effectSound') === 'true') {
- const effectSound = new Audio('/music/blop.mp3');
- effectSound.play().catch((error) => console.error('이펙트 오류', error));
- }
- },
-}));
-
-export default useEffectSoundStore;
diff --git a/src/states/game.ts b/src/states/game.ts
deleted file mode 100644
index c29b9263..00000000
--- a/src/states/game.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-import { create } from 'zustand';
-
-interface GameControlState {
- isActive: boolean; // 게임이 활성화 상태인지 여부
- setIsActive: (isActive: boolean) => void; // 게임 활성화 상태를 설정하는 함수
-}
-
-export const useGameControl = create((set) => ({
- isActive: true,
- setIsActive: (isActive: boolean) => set({ isActive }),
-}));
diff --git a/src/states/user.ts b/src/states/user.ts
deleted file mode 100644
index 3c40909c..00000000
--- a/src/states/user.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { useUserStore as useUserStateStore } from '@/features/user';
diff --git a/src/types/auth.d.ts b/src/types/auth.d.ts
index 0fe7c993..914ebfa0 100644
--- a/src/types/auth.d.ts
+++ b/src/types/auth.d.ts
@@ -1,29 +1,3 @@
-export interface User {
- exp: number;
- expRequire: number;
- guest: boolean;
- level: number;
- nickname: string;
- rating: number;
- skin: string;
- tier: tierRange;
- label: string;
- rank: number;
- userCode: string;
-}
-
-export interface LogInInfo {
- id: string;
- password: string;
-}
-
-export interface SignUpInfo {
- id: string;
- password: string;
- passwordConfirm: string;
- nickname: string;
-}
-
export interface NicknameInfo {
nickname: string;
}
diff --git a/src/types/play.d.ts b/src/types/play.d.ts
deleted file mode 100644
index 77662c62..00000000
--- a/src/types/play.d.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-import type { playerColorType } from '@/pages/play/lobby/types';
-
-interface Player {
- userCode: string;
- nickname: string;
- tier: tierRange;
- skin: string;
- label: string;
- ready: boolean;
- isMaster: boolean;
- color: playerColorType;
- key: string;
-}
-
-interface Room {
- roomTitle: string;
- mapId: number;
-}
-
-export interface CreateRoom extends Room {
- roomPassword: string;
-}
-
-interface CodeRoom extends Room {
- roomCode: string;
- roomPassword: string;
-}
-
-interface RoomListItem extends CodeRoom {
- userNumber: number;
-}
-
-interface Lobby extends CodeRoom {
- roomUuid: string;
- masterIndex: number;
- readyState: boolean[];
- players: Player[];
-}
-
-interface RequestEnterRoom {
- roomCode: string;
- roomPassword: string;
-}
diff --git a/src/types/rank.d.ts b/src/types/rank.d.ts
index af4e6870..7de94732 100644
--- a/src/types/rank.d.ts
+++ b/src/types/rank.d.ts
@@ -1,14 +1,4 @@
-export interface RankPlayer {
- rank: number;
- nickname: string;
- skin: string;
- label: string;
- rating: number;
- tier: tierRange;
-}
-
-// 일단 refreshTime 을 받고 있는데, 이건 나중에 디벨롭 할 때 다시 이야기.
-export interface RankInfo {
+interface RankInfo {
refreshTime: string;
- players: RankPlayer[];
+ players: Profile[];
}
diff --git a/src/types/result.d.ts b/src/types/result.d.ts
deleted file mode 100644
index 59effbd4..00000000
--- a/src/types/result.d.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-interface ResultPlayer {
- nickname: string;
- label: string;
- index: number;
- skin: string;
- rank: number;
- score: number;
- userCode: string;
-}
-
-interface TierReward {
- oldtier: tierRange;
- newtier: tierRange;
- upgrade: boolean;
-}
-
-export interface GameResult {
- roomUuid?: string;
- players: ResultPlayer[];
- reward: {
- tier?: TierReward;
- skins: string[];
- };
-}
diff --git a/src/types/tier.d.ts b/src/types/tier.d.ts
deleted file mode 100644
index 9e18624c..00000000
--- a/src/types/tier.d.ts
+++ /dev/null
@@ -1 +0,0 @@
-type tierRange = (typeof import('@/constants/tier').TIER)[number];