diff --git a/package.json b/package.json
index 07ddfe4..a30c007 100644
--- a/package.json
+++ b/package.json
@@ -7,12 +7,14 @@
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
- "preview": "vite preview"
+ "preview": "vite preview",
+ "generate:api": "openapi-typescript http://3.34.130.196:8081/v3/api-docs -o src/shared/types/schema.ts"
},
"dependencies": {
"@tanstack/react-query": "^5.90.17",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
+ "jwt-decode": "^4.0.0",
"ky": "^1.14.2",
"react": "^19.2.0",
"react-dom": "^19.2.0",
@@ -40,6 +42,7 @@
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.24",
"globals": "^16.5.0",
+ "openapi-typescript": "^7.13.0",
"postcss": "^8.5.6",
"prettier": "^3.7.4",
"tailwindcss": "^3.4.19",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index de38cdd..baafd03 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -17,6 +17,9 @@ importers:
clsx:
specifier: ^2.1.1
version: 2.1.1
+ jwt-decode:
+ specifier: ^4.0.0
+ version: 4.0.0
ky:
specifier: ^1.14.2
version: 1.14.2
@@ -93,6 +96,9 @@ importers:
globals:
specifier: ^16.5.0
version: 16.5.0
+ openapi-typescript:
+ specifier: ^7.13.0
+ version: 7.13.0(typescript@5.9.3)
postcss:
specifier: ^8.5.6
version: 8.5.6
@@ -458,6 +464,16 @@ packages:
resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==}
engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
+ '@redocly/ajv@8.17.4':
+ resolution: {integrity: sha512-BieiCML/IgP6x99HZByJSt7fJE4ipgzO7KAFss92Bs+PEI35BhY7vGIysFXLT+YmS7nHtQjZjhOQyPPEf7xGHA==}
+
+ '@redocly/config@0.22.2':
+ resolution: {integrity: sha512-roRDai8/zr2S9YfmzUfNhKjOF0NdcOIqF7bhf4MVC5UxpjIysDjyudvlAiVbpPHp3eDRWbdzUgtkK1a7YiDNyQ==}
+
+ '@redocly/openapi-core@1.34.6':
+ resolution: {integrity: sha512-2+O+riuIUgVSuLl3Lyh5AplWZyVMNuG2F98/o6NrutKJfW4/GTZdPpZlIphS0HGgcOHgmWcCSHj+dWFlZaGSHw==}
+ engines: {node: '>=18.17.0', npm: '>=9.5.0'}
+
'@rolldown/pluginutils@1.0.0-beta.53':
resolution: {integrity: sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==}
@@ -932,9 +948,17 @@ packages:
engines: {node: '>=0.4.0'}
hasBin: true
+ agent-base@7.1.4:
+ resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==}
+ engines: {node: '>= 14'}
+
ajv@6.12.6:
resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
+ ansi-colors@4.1.3:
+ resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==}
+ engines: {node: '>=6'}
+
ansi-styles@4.3.0:
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
engines: {node: '>=8'}
@@ -1056,6 +1080,9 @@ packages:
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
engines: {node: '>=10'}
+ change-case@5.4.4:
+ resolution: {integrity: sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==}
+
chokidar@3.6.0:
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
engines: {node: '>= 8.10.0'}
@@ -1074,6 +1101,9 @@ packages:
color-name@1.1.4:
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
+ colorette@1.4.0:
+ resolution: {integrity: sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==}
+
commander@4.1.1:
resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==}
engines: {node: '>= 6'}
@@ -1385,6 +1415,9 @@ packages:
fast-levenshtein@2.0.6:
resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
+ fast-uri@3.1.0:
+ resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==}
+
fastq@1.20.1:
resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==}
@@ -1521,6 +1554,10 @@ packages:
hermes-parser@0.25.1:
resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==}
+ https-proxy-agent@7.0.6:
+ resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==}
+ engines: {node: '>= 14'}
+
ignore@5.3.2:
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
engines: {node: '>= 4'}
@@ -1537,6 +1574,10 @@ packages:
resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
engines: {node: '>=0.8.19'}
+ index-to-position@1.2.0:
+ resolution: {integrity: sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw==}
+ engines: {node: '>=18'}
+
internal-slot@1.1.0:
resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==}
engines: {node: '>= 0.4'}
@@ -1665,6 +1706,10 @@ packages:
resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==}
hasBin: true
+ js-levenshtein@1.1.6:
+ resolution: {integrity: sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==}
+ engines: {node: '>=0.10.0'}
+
js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
@@ -1686,6 +1731,9 @@ packages:
json-schema-traverse@0.4.1:
resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
+ json-schema-traverse@1.0.0:
+ resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==}
+
json-stable-stringify-without-jsonify@1.0.1:
resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
@@ -1702,6 +1750,10 @@ packages:
resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==}
engines: {node: '>=4.0'}
+ jwt-decode@4.0.0:
+ resolution: {integrity: sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==}
+ engines: {node: '>=18'}
+
keyv@4.5.4:
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
@@ -1822,6 +1874,10 @@ packages:
minimatch@3.1.2:
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
+ minimatch@5.1.6:
+ resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==}
+ engines: {node: '>=10'}
+
minimatch@9.0.5:
resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==}
engines: {node: '>=16 || 14 >=14.17'}
@@ -1894,6 +1950,12 @@ packages:
resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==}
engines: {node: '>= 0.4'}
+ openapi-typescript@7.13.0:
+ resolution: {integrity: sha512-EFP392gcqXS7ntPvbhBzbF8TyBA+baIYEm791Hy5YkjDYKTnk/Tn5OQeKm5BIZvJihpp8Zzr4hzx0Irde1LNGQ==}
+ hasBin: true
+ peerDependencies:
+ typescript: ^5.x
+
optionator@0.9.4:
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
engines: {node: '>= 0.8.0'}
@@ -1918,6 +1980,10 @@ packages:
resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==}
engines: {node: '>=8'}
+ parse-json@8.3.0:
+ resolution: {integrity: sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==}
+ engines: {node: '>=18'}
+
path-exists@4.0.0:
resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
engines: {node: '>=8'}
@@ -1952,6 +2018,10 @@ packages:
resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==}
engines: {node: '>= 6'}
+ pluralize@8.0.0:
+ resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==}
+ engines: {node: '>=4'}
+
possible-typed-array-names@1.1.0:
resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==}
engines: {node: '>= 0.4'}
@@ -2084,6 +2154,10 @@ packages:
resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==}
engines: {node: '>= 0.4'}
+ require-from-string@2.0.2:
+ resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
+ engines: {node: '>=0.10.0'}
+
resolve-from@4.0.0:
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
engines: {node: '>=4'}
@@ -2222,6 +2296,10 @@ packages:
engines: {node: '>=16 || 14 >=14.17'}
hasBin: true
+ supports-color@10.2.2:
+ resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==}
+ engines: {node: '>=18'}
+
supports-color@7.2.0:
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
engines: {node: '>=8'}
@@ -2295,6 +2373,10 @@ packages:
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
engines: {node: '>= 0.8.0'}
+ type-fest@4.41.0:
+ resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==}
+ engines: {node: '>=16'}
+
typed-array-buffer@1.0.3:
resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==}
engines: {node: '>= 0.4'}
@@ -2426,6 +2508,13 @@ packages:
yallist@3.1.1:
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
+ yaml-ast-parser@0.0.43:
+ resolution: {integrity: sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==}
+
+ yargs-parser@21.1.1:
+ resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
+ engines: {node: '>=12'}
+
yocto-queue@0.1.0:
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
engines: {node: '>=10'}
@@ -2464,7 +2553,7 @@ snapshots:
'@babel/types': 7.28.5
'@jridgewell/remapping': 2.3.5
convert-source-map: 2.0.0
- debug: 4.4.3
+ debug: 4.4.3(supports-color@10.2.2)
gensync: 1.0.0-beta.2
json5: 2.2.3
semver: 6.3.1
@@ -2546,7 +2635,7 @@ snapshots:
'@babel/parser': 7.28.5
'@babel/template': 7.27.2
'@babel/types': 7.28.5
- debug: 4.4.3
+ debug: 4.4.3(supports-color@10.2.2)
transitivePeerDependencies:
- supports-color
@@ -2659,7 +2748,7 @@ snapshots:
'@eslint/config-array@0.21.1':
dependencies:
'@eslint/object-schema': 2.1.7
- debug: 4.4.3
+ debug: 4.4.3(supports-color@10.2.2)
minimatch: 3.1.2
transitivePeerDependencies:
- supports-color
@@ -2675,7 +2764,7 @@ snapshots:
'@eslint/eslintrc@3.3.3':
dependencies:
ajv: 6.12.6
- debug: 4.4.3
+ debug: 4.4.3(supports-color@10.2.2)
espree: 10.4.0
globals: 14.0.0
ignore: 5.3.2
@@ -2746,6 +2835,29 @@ snapshots:
'@pkgr/core@0.2.9': {}
+ '@redocly/ajv@8.17.4':
+ dependencies:
+ fast-deep-equal: 3.1.3
+ fast-uri: 3.1.0
+ json-schema-traverse: 1.0.0
+ require-from-string: 2.0.2
+
+ '@redocly/config@0.22.2': {}
+
+ '@redocly/openapi-core@1.34.6(supports-color@10.2.2)':
+ dependencies:
+ '@redocly/ajv': 8.17.4
+ '@redocly/config': 0.22.2
+ colorette: 1.4.0
+ https-proxy-agent: 7.0.6(supports-color@10.2.2)
+ js-levenshtein: 1.1.6
+ js-yaml: 4.1.1
+ minimatch: 5.1.6
+ pluralize: 8.0.0
+ yaml-ast-parser: 0.0.43
+ transitivePeerDependencies:
+ - supports-color
+
'@rolldown/pluginutils@1.0.0-beta.53': {}
'@rollup/pluginutils@5.3.0(rollup@4.54.0)':
@@ -2991,7 +3103,7 @@ snapshots:
'@typescript-eslint/types': 8.51.0
'@typescript-eslint/typescript-estree': 8.51.0(typescript@5.9.3)
'@typescript-eslint/visitor-keys': 8.51.0
- debug: 4.4.3
+ debug: 4.4.3(supports-color@10.2.2)
eslint: 9.39.2(jiti@1.21.7)
typescript: 5.9.3
transitivePeerDependencies:
@@ -3003,7 +3115,7 @@ snapshots:
'@typescript-eslint/types': 8.52.0
'@typescript-eslint/typescript-estree': 8.52.0(typescript@5.9.3)
'@typescript-eslint/visitor-keys': 8.52.0
- debug: 4.4.3
+ debug: 4.4.3(supports-color@10.2.2)
eslint: 9.39.2(jiti@1.21.7)
typescript: 5.9.3
transitivePeerDependencies:
@@ -3013,7 +3125,7 @@ snapshots:
dependencies:
'@typescript-eslint/tsconfig-utils': 8.51.0(typescript@5.9.3)
'@typescript-eslint/types': 8.51.0
- debug: 4.4.3
+ debug: 4.4.3(supports-color@10.2.2)
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
@@ -3022,7 +3134,7 @@ snapshots:
dependencies:
'@typescript-eslint/tsconfig-utils': 8.52.0(typescript@5.9.3)
'@typescript-eslint/types': 8.52.0
- debug: 4.4.3
+ debug: 4.4.3(supports-color@10.2.2)
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
@@ -3050,7 +3162,7 @@ snapshots:
'@typescript-eslint/types': 8.51.0
'@typescript-eslint/typescript-estree': 8.51.0(typescript@5.9.3)
'@typescript-eslint/utils': 8.51.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
- debug: 4.4.3
+ debug: 4.4.3(supports-color@10.2.2)
eslint: 9.39.2(jiti@1.21.7)
ts-api-utils: 2.3.0(typescript@5.9.3)
typescript: 5.9.3
@@ -3062,7 +3174,7 @@ snapshots:
'@typescript-eslint/types': 8.52.0
'@typescript-eslint/typescript-estree': 8.52.0(typescript@5.9.3)
'@typescript-eslint/utils': 8.52.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
- debug: 4.4.3
+ debug: 4.4.3(supports-color@10.2.2)
eslint: 9.39.2(jiti@1.21.7)
ts-api-utils: 2.4.0(typescript@5.9.3)
typescript: 5.9.3
@@ -3079,7 +3191,7 @@ snapshots:
'@typescript-eslint/tsconfig-utils': 8.51.0(typescript@5.9.3)
'@typescript-eslint/types': 8.51.0
'@typescript-eslint/visitor-keys': 8.51.0
- debug: 4.4.3
+ debug: 4.4.3(supports-color@10.2.2)
minimatch: 9.0.5
semver: 7.7.3
tinyglobby: 0.2.15
@@ -3094,7 +3206,7 @@ snapshots:
'@typescript-eslint/tsconfig-utils': 8.52.0(typescript@5.9.3)
'@typescript-eslint/types': 8.52.0
'@typescript-eslint/visitor-keys': 8.52.0
- debug: 4.4.3
+ debug: 4.4.3(supports-color@10.2.2)
minimatch: 9.0.5
semver: 7.7.3
tinyglobby: 0.2.15
@@ -3212,6 +3324,8 @@ snapshots:
acorn@8.15.0: {}
+ agent-base@7.1.4: {}
+
ajv@6.12.6:
dependencies:
fast-deep-equal: 3.1.3
@@ -3219,6 +3333,8 @@ snapshots:
json-schema-traverse: 0.4.1
uri-js: 4.4.1
+ ansi-colors@4.1.3: {}
+
ansi-styles@4.3.0:
dependencies:
color-convert: 2.0.1
@@ -3373,6 +3489,8 @@ snapshots:
ansi-styles: 4.3.0
supports-color: 7.2.0
+ change-case@5.4.4: {}
+
chokidar@3.6.0:
dependencies:
anymatch: 3.1.3
@@ -3397,6 +3515,8 @@ snapshots:
color-name@1.1.4: {}
+ colorette@1.4.0: {}
+
commander@4.1.1: {}
concat-map@0.0.1: {}
@@ -3446,9 +3566,11 @@ snapshots:
dependencies:
ms: 2.1.3
- debug@4.4.3:
+ debug@4.4.3(supports-color@10.2.2):
dependencies:
ms: 2.1.3
+ optionalDependencies:
+ supports-color: 10.2.2
deep-is@0.1.4: {}
@@ -3653,7 +3775,7 @@ snapshots:
eslint-import-resolver-typescript@4.4.4(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@1.21.7)):
dependencies:
- debug: 4.4.3
+ debug: 4.4.3(supports-color@10.2.2)
eslint: 9.39.2(jiti@1.21.7)
eslint-import-context: 0.1.9(unrs-resolver@1.11.1)
get-tsconfig: 4.13.0
@@ -3778,7 +3900,7 @@ snapshots:
ajv: 6.12.6
chalk: 4.1.2
cross-spawn: 7.0.6
- debug: 4.4.3
+ debug: 4.4.3(supports-color@10.2.2)
escape-string-regexp: 4.0.0
eslint-scope: 8.4.0
eslint-visitor-keys: 4.2.1
@@ -3838,6 +3960,8 @@ snapshots:
fast-levenshtein@2.0.6: {}
+ fast-uri@3.1.0: {}
+
fastq@1.20.1:
dependencies:
reusify: 1.1.0
@@ -3969,6 +4093,13 @@ snapshots:
dependencies:
hermes-estree: 0.25.1
+ https-proxy-agent@7.0.6(supports-color@10.2.2):
+ dependencies:
+ agent-base: 7.1.4
+ debug: 4.4.3(supports-color@10.2.2)
+ transitivePeerDependencies:
+ - supports-color
+
ignore@5.3.2: {}
ignore@7.0.5: {}
@@ -3980,6 +4111,8 @@ snapshots:
imurmurhash@0.1.4: {}
+ index-to-position@1.2.0: {}
+
internal-slot@1.1.0:
dependencies:
es-errors: 1.3.0
@@ -4119,6 +4252,8 @@ snapshots:
jiti@1.21.7: {}
+ js-levenshtein@1.1.6: {}
+
js-tokens@4.0.0: {}
js-yaml@4.1.1:
@@ -4133,6 +4268,8 @@ snapshots:
json-schema-traverse@0.4.1: {}
+ json-schema-traverse@1.0.0: {}
+
json-stable-stringify-without-jsonify@1.0.1: {}
json5@1.0.2:
@@ -4148,6 +4285,8 @@ snapshots:
object.assign: 4.1.7
object.values: 1.2.1
+ jwt-decode@4.0.0: {}
+
keyv@4.5.4:
dependencies:
json-buffer: 3.0.1
@@ -4244,6 +4383,10 @@ snapshots:
dependencies:
brace-expansion: 1.1.12
+ minimatch@5.1.6:
+ dependencies:
+ brace-expansion: 2.0.2
+
minimatch@9.0.5:
dependencies:
brace-expansion: 2.0.2
@@ -4317,6 +4460,16 @@ snapshots:
define-properties: 1.2.1
es-object-atoms: 1.1.1
+ openapi-typescript@7.13.0(typescript@5.9.3):
+ dependencies:
+ '@redocly/openapi-core': 1.34.6(supports-color@10.2.2)
+ ansi-colors: 4.1.3
+ change-case: 5.4.4
+ parse-json: 8.3.0
+ supports-color: 10.2.2
+ typescript: 5.9.3
+ yargs-parser: 21.1.1
+
optionator@0.9.4:
dependencies:
deep-is: 0.1.4
@@ -4351,6 +4504,12 @@ snapshots:
json-parse-even-better-errors: 2.3.1
lines-and-columns: 1.2.4
+ parse-json@8.3.0:
+ dependencies:
+ '@babel/code-frame': 7.27.1
+ index-to-position: 1.2.0
+ type-fest: 4.41.0
+
path-exists@4.0.0: {}
path-key@3.1.1: {}
@@ -4369,6 +4528,8 @@ snapshots:
pirates@4.0.7: {}
+ pluralize@8.0.0: {}
+
possible-typed-array-names@1.1.0: {}
postcss-import@15.1.0(postcss@8.5.6):
@@ -4487,6 +4648,8 @@ snapshots:
gopd: 1.2.0
set-function-name: 2.0.2
+ require-from-string@2.0.2: {}
+
resolve-from@4.0.0: {}
resolve-pkg-maps@1.0.0: {}
@@ -4692,6 +4855,8 @@ snapshots:
tinyglobby: 0.2.15
ts-interface-checker: 0.1.13
+ supports-color@10.2.2: {}
+
supports-color@7.2.0:
dependencies:
has-flag: 4.0.0
@@ -4778,6 +4943,8 @@ snapshots:
dependencies:
prelude-ls: 1.2.1
+ type-fest@4.41.0: {}
+
typed-array-buffer@1.0.3:
dependencies:
call-bound: 1.0.4
@@ -4882,7 +5049,7 @@ snapshots:
vite-tsconfig-paths@6.0.4(typescript@5.9.3)(vite@7.3.0(@types/node@24.10.4)(jiti@1.21.7)(lightningcss@1.30.2)):
dependencies:
- debug: 4.4.3
+ debug: 4.4.3(supports-color@10.2.2)
globrex: 0.1.2
tsconfck: 3.1.6(typescript@5.9.3)
optionalDependencies:
@@ -4954,6 +5121,10 @@ snapshots:
yallist@3.1.1: {}
+ yaml-ast-parser@0.0.43: {}
+
+ yargs-parser@21.1.1: {}
+
yocto-queue@0.1.0: {}
zod-validation-error@4.0.2(zod@4.3.4):
diff --git a/src/app/router/lazy.ts b/src/app/router/lazy.ts
index e378aa9..57fcaee 100644
--- a/src/app/router/lazy.ts
+++ b/src/app/router/lazy.ts
@@ -2,6 +2,11 @@ import { lazy } from 'react';
export const MainPage = lazy(() => import('@page/main-page'));
export const MyPage = lazy(() => import('@page/my/my-page'));
-export const NickNamePAge = lazy(() => import('@page/my/nickName-change'));
+export const NickNamePage = lazy(() => import('@page/my/nickName-change'));
+export const NickNameChagePage = lazy(() => import('@page/my/nickName-change'));
export const CreatPage = lazy(() => import('@page/create-page'));
export const PostDetailPage = lazy(() => import('@page/post-detail-page'));
+export const OnboardingPage = lazy(() => import('@page/onboarding-page'));
+export const LoginCallbackPage = lazy(
+ () => import('@page/login-callback-page'),
+);
diff --git a/src/app/router/path.ts b/src/app/router/path.ts
index 3ff7b68..b6f9ccd 100644
--- a/src/app/router/path.ts
+++ b/src/app/router/path.ts
@@ -1,9 +1,11 @@
export const routePath = {
- MAIN: '/',
+ ONBOARDING: '/',
+ MAIN: '/main',
MY: '/my',
CREATE: '/create',
- DETAIL: '/posts/:id',
+ DETAIL: '/posts/:feedId',
NICKNAMECHAGE: '/my/nickname',
+ LOGIN_CALLBACK: '/oauth/success',
} as const;
export type Routes = (typeof routePath)[keyof typeof routePath];
diff --git a/src/app/router/private-route.tsx b/src/app/router/private-route.tsx
new file mode 100644
index 0000000..d360a90
--- /dev/null
+++ b/src/app/router/private-route.tsx
@@ -0,0 +1,12 @@
+import { routePath } from '@app/router/path';
+import { Navigate, Outlet } from 'react-router-dom';
+
+export default function PrivateRoute() {
+ const token = localStorage.getItem('accessToken');
+
+ if (!token) {
+ return ;
+ }
+
+ return ;
+}
diff --git a/src/app/router/routes/global-routes.tsx b/src/app/router/routes/global-routes.tsx
index 5ec5ddb..7c918d6 100644
--- a/src/app/router/routes/global-routes.tsx
+++ b/src/app/router/routes/global-routes.tsx
@@ -1,26 +1,48 @@
import { routePath } from '@app/router/path';
-import { CreatPage, MainPage, MyPage, PostDetailPage } from '@app/router/lazy';
-import NickNameChage from '@page/my/nickName-change';
+import {
+ CreatPage,
+ LoginCallbackPage,
+ MainPage,
+ MyPage,
+ NickNameChagePage,
+ OnboardingPage,
+ PostDetailPage,
+} from '@app/router/lazy';
+
+import PrivateRoute from '@app/router/private-route';
export const globalRoutes = [
{
- path: routePath.MAIN,
- element: ,
- },
- {
- path: '/my',
- element: ,
- },
- {
- path: '/create',
- element: ,
+ path: routePath.ONBOARDING,
+ element: ,
},
{
- path: '/posts/:id',
- element: ,
+ path: routePath.LOGIN_CALLBACK,
+ element: ,
},
{
- path: '/my/nickname',
- element: ,
+ element: ,
+ children: [
+ {
+ path: routePath.MAIN,
+ element: ,
+ },
+ {
+ path: routePath.MY,
+ element: ,
+ },
+ {
+ path: routePath.CREATE,
+ element: ,
+ },
+ {
+ path: routePath.DETAIL,
+ element: ,
+ },
+ {
+ path: routePath.NICKNAMECHAGE,
+ element: ,
+ },
+ ],
},
];
diff --git a/src/features/.gitkeep b/src/features/.gitkeep
deleted file mode 100644
index e69de29..0000000
diff --git a/src/features/auth/login-callback.tsx b/src/features/auth/login-callback.tsx
new file mode 100644
index 0000000..357b8c3
--- /dev/null
+++ b/src/features/auth/login-callback.tsx
@@ -0,0 +1,27 @@
+import { useEffect } from 'react';
+import { useNavigate } from 'react-router-dom';
+import { routePath } from '@app/router/path';
+
+export default function LoginCallback() {
+ const navigate = useNavigate();
+
+ useEffect(() => {
+ const params = new URLSearchParams(window.location.search);
+ const token = params.get('token');
+
+ const finalToken = token ?? localStorage.getItem('accessToken');
+
+ if (!finalToken) {
+ navigate(routePath.ONBOARDING, { replace: true });
+ return;
+ }
+
+ if (token) {
+ localStorage.setItem('accessToken', token);
+ }
+
+ navigate(routePath.MAIN, { replace: true });
+ }, [navigate]);
+
+ return
로그인 처리중...
;
+}
diff --git a/src/features/location-picker/types.ts b/src/features/location-picker/types.ts
new file mode 100644
index 0000000..7368f61
--- /dev/null
+++ b/src/features/location-picker/types.ts
@@ -0,0 +1,6 @@
+export interface LocationSelection {
+ name: string;
+ address: string;
+ latitude: number;
+ longitude: number;
+}
diff --git a/src/features/location-picker/ui/location-picker.tsx b/src/features/location-picker/ui/location-picker.tsx
new file mode 100644
index 0000000..f68fc00
--- /dev/null
+++ b/src/features/location-picker/ui/location-picker.tsx
@@ -0,0 +1,287 @@
+import { useEffect, useRef, useState } from 'react';
+import Input, { type InputSize } from '@shared/ui/input';
+import { FloatingActionButton } from '@shared/ui/floatingActionButton';
+import LocationIcon from '@shared/assets/icon/material-symbols_my-location-outline-rounded.svg?react';
+import { loadKakaoMap } from '@shared/lib/kakao-map/load-kakao-map';
+import type { LocationSelection } from '@features/location-picker/types';
+
+const DEFAULT_CENTER = {
+ latitude: 37.5665,
+ longitude: 126.978,
+};
+
+interface LocationPickerProps {
+ value: LocationSelection | null;
+ onChange: (value: LocationSelection) => void;
+ inputSize?: InputSize;
+ inputPlaceholder?: string;
+ containerClassName?: string;
+ searchRowClassName?: string;
+ mapClassName?: string;
+}
+
+export function LocationPicker({
+ value,
+ onChange,
+ inputSize = 'sm',
+ inputPlaceholder = '동을 입력해주세요. 예) 역삼동',
+ containerClassName = 'flex flex-col gap-[1.6rem] px-[2.4rem] pb-[2.4rem]',
+ searchRowClassName = 'flex items-center gap-[1.2rem]',
+ mapClassName = 'relative h-[24rem] w-full overflow-hidden rounded-[16px] border border-gray-200',
+}: LocationPickerProps) {
+ const mapContainerRef = useRef(null);
+ const mapRef = useRef(null);
+ const placesRef = useRef(null);
+ const geocoderRef = useRef(null);
+ const syncTokenRef = useRef(0);
+ const idleTimeoutRef = useRef(null);
+ const ignoreNextIdleSyncRef = useRef(false);
+ const suppressSearchRef = useRef(false);
+ const [keyword, setKeyword] = useState(value?.name || value?.address || '');
+ const [isMapReady, setIsMapReady] = useState(false);
+
+ const syncCenterToSelection = (
+ latitude: number,
+ longitude: number,
+ fallbackName?: string,
+ ) => {
+ if (!geocoderRef.current || !window.kakao?.maps?.services) {
+ onChange({
+ name: fallbackName || '선택한 위치',
+ address: fallbackName || '선택한 위치',
+ latitude,
+ longitude,
+ });
+ return;
+ }
+
+ syncTokenRef.current += 1;
+ const currentToken = syncTokenRef.current;
+
+ geocoderRef.current.coord2Address(
+ longitude,
+ latitude,
+ (result: any[], status: string) => {
+ if (currentToken !== syncTokenRef.current) return;
+
+ const address =
+ status === window.kakao.maps.services.Status.OK
+ ? result[0]?.road_address?.address_name ||
+ result[0]?.address?.address_name ||
+ fallbackName ||
+ '선택한 위치'
+ : fallbackName || '선택한 위치';
+
+ onChange({
+ name: fallbackName || address,
+ address,
+ latitude,
+ longitude,
+ });
+ },
+ );
+ };
+
+ const moveMapCenter = (
+ latitude: number,
+ longitude: number,
+ fallbackName?: string,
+ ) => {
+ if (!window.kakao?.maps || !mapRef.current) return;
+
+ ignoreNextIdleSyncRef.current = true;
+ const position = new window.kakao.maps.LatLng(latitude, longitude);
+ mapRef.current.panTo(position);
+ syncCenterToSelection(latitude, longitude, fallbackName);
+ };
+
+ useEffect(() => {
+ let isMounted = true;
+
+ loadKakaoMap()
+ .then((kakao) => {
+ if (!isMounted || !mapContainerRef.current) return;
+
+ const center = new kakao.maps.LatLng(
+ value?.latitude ?? DEFAULT_CENTER.latitude,
+ value?.longitude ?? DEFAULT_CENTER.longitude,
+ );
+
+ const map = new kakao.maps.Map(mapContainerRef.current, {
+ center,
+ level: 3,
+ });
+
+ kakao.maps.event.addListener(map, 'idle', () => {
+ if (ignoreNextIdleSyncRef.current) {
+ ignoreNextIdleSyncRef.current = false;
+ return;
+ }
+
+ if (idleTimeoutRef.current) {
+ window.clearTimeout(idleTimeoutRef.current);
+ }
+
+ const nextCenter = map.getCenter();
+
+ idleTimeoutRef.current = window.setTimeout(() => {
+ syncCenterToSelection(nextCenter.getLat(), nextCenter.getLng());
+ }, 180);
+ });
+
+ mapRef.current = map;
+ placesRef.current = new kakao.maps.services.Places();
+ geocoderRef.current = new kakao.maps.services.Geocoder();
+ setIsMapReady(true);
+ })
+ .catch((error) => {
+ console.error(error);
+ });
+
+ return () => {
+ isMounted = false;
+
+ if (idleTimeoutRef.current) {
+ window.clearTimeout(idleTimeoutRef.current);
+ }
+ };
+ }, []);
+
+ useEffect(() => {
+ if (!isMapReady || !value || !mapRef.current || !window.kakao?.maps) return;
+
+ const center = mapRef.current.getCenter();
+ const lat = center.getLat();
+ const lng = center.getLng();
+
+ if (Math.abs(lat - value.latitude) < 0.000001 && Math.abs(lng - value.longitude) < 0.000001) {
+ return;
+ }
+
+ ignoreNextIdleSyncRef.current = true;
+ const position = new window.kakao.maps.LatLng(
+ value.latitude,
+ value.longitude,
+ );
+ mapRef.current.panTo(position);
+ }, [isMapReady, value]);
+
+ useEffect(() => {
+ if (!isMapReady || !placesRef.current || !window.kakao?.maps?.services) return;
+
+ const trimmedKeyword = keyword.trim();
+
+ if (trimmedKeyword.length < 2) {
+ return;
+ }
+
+ const timeoutId = window.setTimeout(() => {
+ if (suppressSearchRef.current) {
+ suppressSearchRef.current = false;
+ return;
+ }
+
+ geocoderRef.current?.addressSearch(
+ trimmedKeyword,
+ (addressData: any[], addressStatus: string) => {
+ if (
+ addressStatus === window.kakao.maps.services.Status.OK &&
+ addressData[0]
+ ) {
+ const firstAddress = addressData[0];
+ moveMapCenter(
+ Number(firstAddress.y),
+ Number(firstAddress.x),
+ firstAddress.address_name,
+ );
+ return;
+ }
+
+ placesRef.current.keywordSearch(
+ trimmedKeyword,
+ (placeData: any[], placeStatus: string) => {
+ if (
+ placeStatus !== window.kakao.maps.services.Status.OK ||
+ !placeData[0]
+ ) {
+ return;
+ }
+
+ const firstPlace = placeData[0];
+ moveMapCenter(
+ Number(firstPlace.y),
+ Number(firstPlace.x),
+ firstPlace.place_name,
+ );
+ },
+ );
+ },
+ );
+ }, 400);
+
+ return () => {
+ window.clearTimeout(timeoutId);
+ };
+ }, [isMapReady, keyword]);
+
+ const handleCurrentLocation = () => {
+ if (!navigator.geolocation) {
+ window.alert('현재 위치를 지원하지 않는 환경입니다.');
+ return;
+ }
+
+ navigator.geolocation.getCurrentPosition(
+ (position) => {
+ moveMapCenter(
+ position.coords.latitude,
+ position.coords.longitude,
+ '현재 위치',
+ );
+ },
+ (error) => {
+ console.error(error);
+
+ if (error.code === error.PERMISSION_DENIED) {
+ window.alert('위치 권한이 꺼져 있어요. 브라우저 위치 권한을 허용해주세요.');
+ return;
+ }
+
+ window.alert('현재 위치를 가져오지 못했습니다. 위치 권한과 네트워크를 확인해주세요.');
+ },
+ {
+ enableHighAccuracy: true,
+ timeout: 10000,
+ maximumAge: 0,
+ },
+ );
+ };
+
+ return (
+
+
+ setKeyword(event.target.value)}
+ placeholder={inputPlaceholder}
+ />
+ }
+ />
+
+
+
+
+
+

+
+
+
+ );
+}
diff --git a/src/page/create-page.tsx b/src/page/create-page.tsx
index 01634d1..8a65b6a 100644
--- a/src/page/create-page.tsx
+++ b/src/page/create-page.tsx
@@ -13,6 +13,11 @@ import { AddImage } from '@widgets/create/add-image';
import Modal from '@widgets/create/modal/modal';
import { ModalLocationSearch } from '@widgets/create/modal/contents/modal-location-search';
import { DateTimePicker } from '@widgets/create/modal/contents/wheel/date-time-picker';
+import { useMutation, useQueryClient } from '@tanstack/react-query';
+import { FEED_MUTATION_OPTIONS } from '@shared/api/domain/feeds/query'; // 네가 만든 위치
+import { FEED_QUERY_KEY } from '@shared/api/query-key';
+import { getPresignedUpload } from '@shared/api/domain/controller/query';
+import type { LocationSelection } from '@features/location-picker/types';
type CreateModalType = 'location' | 'datetime' | null;
const CreatPage = () => {
@@ -23,19 +28,58 @@ const CreatPage = () => {
const [time, setTime] = useState('');
const [openModal, setOpenModal] = useState(null);
const navigate = useNavigate();
- const [location, setLocation] = useState(''); // 최종 확정 값
- const [tempLocation, setTempLocation] = useState(''); // 모달용 임시 값
+ const [location, setLocation] = useState(null);
+ const [tempLocation, setTempLocation] = useState(
+ null,
+ );
const [dateTime, setDateTime] = useState<{
dateText: string;
hour: string;
minute: string;
} | null>(null);
+ const [imageUrl, setImageUrl] = useState('');
const [tempDateTime, setTempDateTime] = useState<{
dateText: string;
hour: string;
minute: string;
} | null>(null);
+ const queryClient = useQueryClient();
+
+ const { mutate } = useMutation({
+ ...FEED_MUTATION_OPTIONS.CREATE(),
+ onSuccess: () => {
+ queryClient.invalidateQueries({
+ queryKey: FEED_QUERY_KEY.LIST(),
+ });
+
+ navigate('/main');
+ },
+ });
+ const handleImageUpload = async (file: File) => {
+ try {
+ // 1️⃣ presigned 요청 (우리 서버)
+ const { uploadUrl, fileUrl } = await getPresignedUpload(
+ 'feed',
+ file.type,
+ );
+
+ // 2️⃣ S3 업로드 (외부 URL)
+ await fetch(uploadUrl, {
+ method: 'put',
+ headers: {
+ 'Content-Type': file.type,
+ },
+ body: file,
+ });
+
+ // 3️⃣ 상태 저장
+ setImageUrl(fileUrl);
+ } catch (e) {
+ console.error('이미지 업로드 실패', e);
+ }
+ };
+
return (
{
/>
-
- 클릭하여 장소 선택
-
- )
+
+
+ 클릭하여 장소 선택
+
}
onClick={() => {
setTempLocation(location);
@@ -95,11 +138,12 @@ const CreatPage = () => {
setOpenModal('datetime')}
/>
@@ -112,7 +156,7 @@ const CreatPage = () => {
onChange={(e) => setText(e.target.value)}
/>
-
+